Skip to content

Commit

Permalink
feat: Database Connection UI (#14881)
Browse files Browse the repository at this point in the history
  • Loading branch information
hughhhh authored Jul 1, 2021
1 parent 7f2f51b commit d4480f5
Show file tree
Hide file tree
Showing 33 changed files with 3,247 additions and 961 deletions.
8 changes: 8 additions & 0 deletions docs/src/pages/docs/Miscellaneous/issue_codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,14 @@ The results stored in the backend were stored in a different format, and no long

The query results were stored in a format that is no longer supported. Please re-run your query.

## Issue 1034

```
The database port provided is invalid.
```

Please check that the provided database port is an integer between 0 and 65535 (inclusive).

## Issue 1035

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,71 +21,50 @@ import { DATABASE_LIST } from './helper';
describe('Add database', () => {
beforeEach(() => {
cy.login();
});

it('should keep create modal open when error', () => {
cy.visit(DATABASE_LIST);

// open modal
cy.wait(3000);
cy.get('[data-test="btn-create-database"]').click();
});

// values should be blank
cy.get('[data-test="database-modal"] input[name="database_name"]').should(
'have.value',
'',
);
cy.get('[data-test="database-modal"] input[name="sqlalchemy_uri"]').should(
'have.value',
'',
);

// type values
cy.get('[data-test="database-modal"] input[name="database_name"]')
.focus()
.type('cypress');
cy.get('[data-test="database-modal"] input[name="sqlalchemy_uri"]')
.focus()
.type('bad_db_uri');

// click save
cy.get('[data-test="modal-confirm-button"]:not(:disabled)').click();

// should show error alerts and keep modal open
cy.get('.toast').contains('error');
cy.wait(1000); // wait for potential (incorrect) closing annimation
cy.get('[data-test="database-modal"]').should('be.visible');
it('should open dynamic form', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();

// should be able to close modal
cy.get('[data-test="modal-cancel-button"]').click();
cy.get('[data-test="database-modal"]').should('not.be.visible');
// make sure all the fields are rendering
cy.get('input[name="host"]').should('have.value', '');
cy.get('input[name="port"]').should('have.value', '');
cy.get('input[name="database"]').should('have.value', '');
cy.get('input[name="password"]').should('have.value', '');
cy.get('input[name="database_name"]').should('have.value', '');
});

it('should keep update modal open when error', () => {
// open modal
cy.get('[data-test="database-edit"]:last').click();

// it should show saved values
cy.get('[data-test="database-modal"]:last input[name="sqlalchemy_uri"]')
.invoke('val')
.should('not.be.empty');
cy.get('[data-test="database-modal"] input[name="database_name"]')
.invoke('val')
.should('not.be.empty');
it('should open sqlalchemy form', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();

cy.get('[data-test="database-modal"]:last input[name="sqlalchemy_uri"]')
.focus()
.dblclick()
.type('{selectall}{backspace}bad_uri');
cy.get('[data-test="sqla-connect-btn"]').click();

// click save
cy.get('[data-test="modal-confirm-button"]:not(:disabled)').click();
// check if the sqlalchemy form is showing up
cy.get('[data-test=database-name-input]').should('be.visible');
cy.get('[data-test="sqlalchemy-uri-input"]').should('be.visible');
});

// should show error alerts
// TODO(hugh): Update this test
// cy.get('.toast').contains('error').should('be.visible');
it('show error alerts on dynamic form for bad host', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();
cy.get('input[name="host"]').focus().type('badhost');
cy.get('input[name="port"]').focus().type('5432');
cy.get('.ant-form-item-explain-error').contains(
"The hostname provided can't be resolved",
);
});

// modal should still be open
// cy.wait(1000); // wait for potential (incorrect) closing annimation
// cy.get('[data-test="database-modal"]').should('be.visible');
it('show error alerts on dynamic form for bad port', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();
cy.get('input[name="host"]').focus().type('localhost');
cy.get('input[name="port"]').focus().type('123');
cy.get('input[name="database"]').focus();
cy.get('.ant-form-item-explain-error').contains('The port is closed');
});
});
21 changes: 21 additions & 0 deletions superset-frontend/images/icons/default_db_image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
"react-js-cron": "^1.2.0",
"react-json-tree": "^0.11.2",
"react-jsonschema-form": "^1.2.0",
"react-lines-ellipsis": "^0.15.0",
"react-loadable": "^5.5.0",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.0",
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/components/ErrorMessage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ErrorTypeEnum = {
CONNECTION_INVALID_PASSWORD_ERROR: 'CONNECTION_INVALID_PASSWORD_ERROR',
CONNECTION_INVALID_HOSTNAME_ERROR: 'CONNECTION_INVALID_HOSTNAME_ERROR',
CONNECTION_PORT_CLOSED_ERROR: 'CONNECTION_PORT_CLOSED_ERROR',
CONNECTION_INVALID_PORT_ERROR: 'CONNECTION_INVALID_PORT_ERROR',
CONNECTION_HOST_DOWN_ERROR: 'CONNECTION_HOST_DOWN_ERROR',
CONNECTION_ACCESS_DENIED_ERROR: 'CONNECTION_ACCESS_DENIED_ERROR',
CONNECTION_UNKNOWN_DATABASE_ERROR: 'CONNECTION_UNKNOWN_DATABASE_ERROR',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const InteractiveLabeledErrorBoundInput = ({
placeholder,
type,
id,
tooltipText,
}: LabeledErrorBoundInputProps) => {
const [currentValue, setCurrentValue] = useState(value);

Expand All @@ -58,6 +59,8 @@ export const InteractiveLabeledErrorBoundInput = ({
placeholder={placeholder}
type={type}
required
hasTooltip
tooltipText={tooltipText}
/>
);
};
Expand All @@ -66,6 +69,7 @@ InteractiveLabeledErrorBoundInput.args = {
name: 'Username',
placeholder: 'Example placeholder text...',
id: 1,
tooltipText: 'This is a tooltip',
};

InteractiveLabeledErrorBoundInput.argTypes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import { render, fireEvent, screen } from 'spec/helpers/testing-library';
import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';

const defaultProps = {
Expand All @@ -27,6 +27,8 @@ const defaultProps = {
validationMethods: () => {},
errorMessage: '',
helpText: 'This is a line of example help text',
hasTooltip: false,
tooltipText: 'This is a tooltip',
value: '',
placeholder: 'Example placeholder text...',
type: 'textbox',
Expand Down Expand Up @@ -58,4 +60,19 @@ describe('LabeledErrorBoundInput', () => {
expect(textboxInput).toBeVisible();
expect(errorText).toBeVisible();
});
it('renders a LabledErrorBoundInput with a InfoTooltip', async () => {
defaultProps.hasTooltip = true;
render(<LabeledErrorBoundInput {...defaultProps} />);

const label = screen.getByText(/username/i);
const textboxInput = screen.getByRole('textbox');
const tooltipIcon = screen.getByRole('img');

fireEvent.mouseOver(tooltipIcon);

expect(tooltipIcon).toBeVisible();
expect(label).toBeVisible();
expect(textboxInput).toBeVisible();
expect(await screen.findByText('This is a tooltip')).toBeInTheDocument();
});
});
35 changes: 30 additions & 5 deletions superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import React from 'react';
import { Input } from 'antd';
import { styled, css, SupersetTheme } from '@superset-ui/core';
import InfoTooltip from 'src/components/InfoTooltip';
import errorIcon from 'images/icons/error.svg';
import FormItem from './FormItem';
import FormLabel from './FormLabel';

Expand All @@ -30,6 +32,8 @@ export interface LabeledErrorBoundInputProps {
errorMessage: string | null;
helpText?: string;
required?: boolean;
hasTooltip?: boolean;
tooltipText?: string | null;
id?: string;
classname?: string;
[x: string]: any;
Expand All @@ -46,42 +50,63 @@ const alertIconStyles = (theme: SupersetTheme, hasError: boolean) => css`
${hasError &&
`.ant-form-item-control-input-content {
position: relative;
&:after {
content: ' ';
display: inline-block;
background: ${theme.colors.error.base};
mask: url('/images/icons/error.svg');
mask: url(${errorIcon});
mask-size: cover;
width: ${theme.gridUnit * 4}px;
height: ${theme.gridUnit * 4}px;
position: absolute;
right: 7px;
top: 15px;
right: ${theme.gridUnit * 1.25}px;
top: ${theme.gridUnit * 2.75}px;
}
}`}
`;

const StyledFormGroup = styled('div')`
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
margin-bottom: ${({ theme }) => theme.gridUnit * 5}px;
.ant-form-item {
margin-bottom: 0;
}
`;

const infoTooltip = (theme: SupersetTheme) => css`
svg {
vertical-align: bottom;
margin-bottom: ${theme.gridUnit * 0.25}px;
}
`;

const LabeledErrorBoundInput = ({
label,
validationMethods,
errorMessage,
helpText,
required = false,
hasTooltip = false,
tooltipText,
id,
className,
...props
}: LabeledErrorBoundInputProps) => (
<StyledFormGroup className={className}>
<FormLabel htmlFor={id} required={required}>
<FormLabel
htmlFor={id}
required={required}
css={(theme: SupersetTheme) => infoTooltip(theme)}
>
{label}
</FormLabel>
{hasTooltip && (
<InfoTooltip tooltip={`${tooltipText}`} viewBox="0 -6 24 24" />
)}
<FormItem
css={(theme: SupersetTheme) => alertIconStyles(theme, !!errorMessage)}
validateTrigger={Object.keys(validationMethods)}
Expand Down
3 changes: 3 additions & 0 deletions superset-frontend/src/components/Icon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
import { ReactComponent as WarningSolidIcon } from 'images/icons/warning_solid.svg';
import { ReactComponent as XLargeIcon } from 'images/icons/x-large.svg';
import { ReactComponent as XSmallIcon } from 'images/icons/x-small.svg';
import { ReactComponent as DefaultDatabaseIcon } from 'images/icons/default_db_image.svg';

export type IconName =
| 'alert'
Expand Down Expand Up @@ -184,6 +185,7 @@ export type IconName =
| 'copy'
| 'cursor-target'
| 'database'
| 'default-database'
| 'dataset-physical'
| 'dataset-virtual'
| 'dataset-virtual-greyscale'
Expand Down Expand Up @@ -299,6 +301,7 @@ export const iconsRegistry: Record<
'circle-check-solid': CircleCheckSolidIcon,
'color-palette': ColorPaletteIcon,
'cursor-target': CursorTargeIcon,
'default-database': DefaultDatabaseIcon,
'dataset-physical': DatasetPhysicalIcon,
'dataset-virtual': DatasetVirtualIcon,
'dataset-virtual-greyscale': DatasetVirtualGreyscaleIcon,
Expand Down
Loading

0 comments on commit d4480f5

Please sign in to comment.