Skip to content

Commit 6db6db2

Browse files
authored
fix(DatabaseModal): Improve database modal validation and fix visual Issues (#33826)
1 parent 98b3512 commit 6db6db2

File tree

9 files changed

+253
-236
lines changed

9 files changed

+253
-236
lines changed

superset-frontend/cypress-base/cypress/e2e/database/modal.test.ts

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,53 +32,87 @@ describe('Add database', () => {
3232
});
3333

3434
beforeEach(() => {
35+
cy.intercept('POST', '**/api/v1/database/validate_parameters/**').as(
36+
'validateParams',
37+
);
38+
cy.intercept('POST', '**/api/v1/database/').as('createDb');
39+
3540
closeModal();
3641
cy.getBySel('btn-create-database').click();
3742
});
3843

3944
it('should open dynamic form', () => {
40-
// click postgres dynamic form
4145
cy.get('.preferred > :nth-child(1)').click();
4246

43-
// make sure all the fields are rendering
4447
cy.get('input[name="host"]').should('have.value', '');
4548
cy.get('input[name="port"]').should('have.value', '');
4649
cy.get('input[name="database"]').should('have.value', '');
50+
cy.get('input[name="username"]').should('have.value', '');
4751
cy.get('input[name="password"]').should('have.value', '');
4852
cy.get('input[name="database_name"]').should('have.value', '');
4953
});
5054

5155
it('should open sqlalchemy form', () => {
52-
// click postgres dynamic form
5356
cy.get('.preferred > :nth-child(1)').click();
54-
5557
cy.getBySel('sqla-connect-btn').click();
5658

57-
// check if the sqlalchemy form is showing up
5859
cy.getBySel('database-name-input').should('be.visible');
5960
cy.getBySel('sqlalchemy-uri-input').should('be.visible');
6061
});
6162

6263
it('show error alerts on dynamic form for bad host', () => {
63-
// click postgres dynamic form
6464
cy.get('.preferred > :nth-child(1)').click();
65-
cy.get('input[name="host"]').focus();
66-
cy.focused().type('badhost', { force: true });
67-
cy.get('input[name="port"]').focus();
68-
cy.focused().type('5432', { force: true });
69-
cy.get('.ant-form-item-explain-error').contains(
70-
"The hostname provided can't be resolved",
71-
);
65+
66+
cy.get('input[name="host"]').type('badhost', { force: true });
67+
cy.get('input[name="port"]').type('5432', { force: true });
68+
cy.get('input[name="username"]').type('testusername', { force: true });
69+
cy.get('input[name="database"]').type('testdb', { force: true });
70+
cy.get('input[name="password"]').type('testpass', { force: true });
71+
72+
cy.get('body').click(0, 0);
73+
74+
cy.wait('@validateParams', { timeout: 30000 });
75+
76+
cy.getBySel('btn-submit-connection').should('not.be.disabled');
77+
cy.getBySel('btn-submit-connection').click({ force: true });
78+
79+
cy.wait('@validateParams', { timeout: 30000 }).then(() => {
80+
cy.wait('@createDb', { timeout: 60000 }).then(() => {
81+
cy.contains(
82+
'.ant-form-item-explain-error',
83+
"The hostname provided can't be resolved",
84+
).should('exist');
85+
});
86+
});
7287
});
7388

7489
it('show error alerts on dynamic form for bad port', () => {
75-
// click postgres dynamic form
7690
cy.get('.preferred > :nth-child(1)').click();
77-
cy.get('input[name="host"]').focus();
78-
cy.focused().type('localhost', { force: true });
79-
cy.get('input[name="port"]').focus();
80-
cy.focused().type('123', { force: true });
81-
cy.get('input[name="database"]').focus();
82-
cy.get('.ant-form-item-explain-error').contains('The port is closed');
91+
92+
cy.get('input[name="host"]').type('localhost', { force: true });
93+
cy.get('body').click(0, 0);
94+
cy.wait('@validateParams', { timeout: 30000 });
95+
96+
cy.get('input[name="port"]').type('5430', { force: true });
97+
cy.get('input[name="database"]').type('testdb', { force: true });
98+
cy.get('input[name="username"]').type('testusername', { force: true });
99+
100+
cy.wait('@validateParams', { timeout: 30000 });
101+
102+
cy.get('input[name="password"]').type('testpass', { force: true });
103+
cy.wait('@validateParams');
104+
105+
cy.getBySel('btn-submit-connection').should('not.be.disabled');
106+
cy.getBySel('btn-submit-connection').click({ force: true });
107+
cy.wait('@validateParams', { timeout: 30000 }).then(() => {
108+
cy.get('body').click(0, 0);
109+
cy.getBySel('btn-submit-connection').click({ force: true });
110+
cy.wait('@createDb', { timeout: 60000 }).then(() => {
111+
cy.contains(
112+
'.ant-form-item-explain-error',
113+
'The port is closed',
114+
).should('exist');
115+
});
116+
});
83117
});
84118
});

superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx

Lines changed: 60 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
* specific language governing permissions and limitations
1717
* under the License.
1818
*/
19-
import { styled, css, SupersetTheme, t } from '../..';
20-
import { error as errorIcon } from '../assets/svgs';
21-
import { Button, Icons, InfoTooltip, Tooltip } from '..';
19+
import { styled, t } from '@superset-ui/core';
20+
import { Button, Icons, InfoTooltip, Tooltip, Flex } from '..';
2221
import { Input } from '../Input';
2322
import { FormLabel } from './FormLabel';
2423
import { FormItem } from './FormItem';
@@ -32,28 +31,6 @@ const StyledInputPassword = styled(Input.Password)`
3231
margin: ${({ theme }) => `${theme.sizeUnit}px 0 ${theme.sizeUnit * 2}px`};
3332
`;
3433

35-
const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css`
36-
.ant-form-item-children-icon {
37-
display: none;
38-
}
39-
${hasError &&
40-
`.ant-form-item-control-input-content {
41-
position: relative;
42-
&:after {
43-
content: ' ';
44-
display: inline-block;
45-
background: ${theme.colorError};
46-
mask: url(${errorIcon});
47-
mask-size: cover;
48-
width: ${theme.sizeUnit * 4}px;
49-
height: ${theme.sizeUnit * 4}px;
50-
position: absolute;
51-
right: ${theme.sizeUnit * 1.25}px;
52-
top: ${theme.sizeUnit * 2.75}px;
53-
}
54-
}`}
55-
`;
56-
5734
const StyledFormGroup = styled('div')`
5835
input::-webkit-outer-spin-button,
5936
input::-webkit-inner-spin-button {
@@ -66,21 +43,10 @@ const StyledFormGroup = styled('div')`
6643
}
6744
`;
6845

69-
const StyledAlignment = styled.div`
70-
display: flex;
71-
align-items: center;
72-
`;
73-
7446
const StyledFormLabel = styled(FormLabel)`
7547
margin-bottom: 0;
7648
`;
7749

78-
const iconReset = css`
79-
&.anticon > * {
80-
line-height: 0;
81-
}
82-
`;
83-
8450
export const LabeledErrorBoundInput = ({
8551
label,
8652
validationMethods,
@@ -94,60 +60,62 @@ export const LabeledErrorBoundInput = ({
9460
visibilityToggle,
9561
get_url,
9662
description,
63+
isValidating = false,
9764
...props
98-
}: LabeledErrorBoundInputProps) => (
99-
<StyledFormGroup className={className}>
100-
<StyledAlignment>
101-
<StyledFormLabel htmlFor={id} required={required}>
102-
{label}
103-
</StyledFormLabel>
104-
{hasTooltip && <InfoTooltip tooltip={`${tooltipText}`} />}
105-
</StyledAlignment>
106-
<FormItem
107-
css={(theme: SupersetTheme) => alertIconStyles(theme, !!errorMessage)}
108-
validateTrigger={Object.keys(validationMethods)}
109-
validateStatus={errorMessage ? 'error' : 'success'}
110-
help={errorMessage || helpText}
111-
hasFeedback={!!errorMessage}
112-
>
113-
{visibilityToggle || props.name === 'password' ? (
114-
<StyledInputPassword
115-
{...props}
116-
{...validationMethods}
117-
iconRender={visible =>
118-
visible ? (
119-
<Tooltip title={t('Hide password.')}>
120-
<Icons.EyeInvisibleOutlined iconSize="m" css={iconReset} />
121-
</Tooltip>
122-
) : (
123-
<Tooltip title={t('Show password.')}>
124-
<Icons.EyeOutlined
125-
iconSize="m"
126-
css={iconReset}
127-
data-test="icon-eye"
128-
/>
129-
</Tooltip>
130-
)
131-
}
132-
role="textbox"
133-
/>
134-
) : (
135-
<StyledInput {...props} {...validationMethods} />
136-
)}
137-
{get_url && description ? (
138-
<Button
139-
htmlType="button"
140-
buttonStyle="secondary"
141-
onClick={() => {
142-
window.open(get_url);
143-
return true;
144-
}}
145-
>
146-
Get {description}
147-
</Button>
148-
) : (
149-
<br />
150-
)}
151-
</FormItem>
152-
</StyledFormGroup>
153-
);
65+
}: LabeledErrorBoundInputProps) => {
66+
const hasError = !!errorMessage;
67+
return (
68+
<StyledFormGroup className={className}>
69+
<Flex align="center">
70+
<StyledFormLabel htmlFor={id} required={required}>
71+
{label}
72+
</StyledFormLabel>
73+
{hasTooltip && <InfoTooltip tooltip={`${tooltipText}`} />}
74+
</Flex>
75+
<FormItem
76+
validateTrigger={Object.keys(validationMethods)}
77+
validateStatus={
78+
isValidating ? 'validating' : hasError ? 'error' : 'success'
79+
}
80+
help={errorMessage || helpText}
81+
hasFeedback={!!hasError}
82+
>
83+
{visibilityToggle || props.name === 'password' ? (
84+
<StyledInputPassword
85+
{...props}
86+
{...validationMethods}
87+
iconRender={visible =>
88+
visible ? (
89+
<Tooltip title={t('Hide password.')}>
90+
<Icons.EyeInvisibleOutlined iconSize="m" />
91+
</Tooltip>
92+
) : (
93+
<Tooltip title={t('Show password.')}>
94+
<Icons.EyeOutlined iconSize="m" data-test="icon-eye" />
95+
</Tooltip>
96+
)
97+
}
98+
role="textbox"
99+
/>
100+
) : (
101+
<StyledInput {...props} {...validationMethods} />
102+
)}
103+
{get_url && description ? (
104+
<Button
105+
type="link"
106+
htmlType="button"
107+
onClick={() => {
108+
window.open(get_url);
109+
return true;
110+
}}
111+
>
112+
Get {description}
113+
</Button>
114+
) : (
115+
<br />
116+
)}
117+
</FormItem>
118+
</StyledFormGroup>
119+
);
120+
};
121+
export default LabeledErrorBoundInput;

0 commit comments

Comments
 (0)