feat: Integrate ant d table component into DatasetPanel (#21948)
Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
35e0e5bfe6
commit
defe5c8ba7
|
|
@ -24,8 +24,8 @@ module.exports = {
|
|||
builder: 'webpack5',
|
||||
},
|
||||
stories: [
|
||||
'../src/@(components|common|filters|explore)/**/*.stories.@(tsx|jsx)',
|
||||
'../src/@(components|common|filters|explore)/**/*.*.@(mdx)',
|
||||
'../src/@(components|common|filters|explore|views)/**/*.stories.@(tsx|jsx)',
|
||||
'../src/@(components|common|filters|explore|views)/**/*.*.@(mdx)',
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg width="152" height="152" viewBox="0 0 152 152" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.5 1H8.5C4.35786 1 1 4.35786 1 8.5V143.5C1 147.642 4.35786 151 8.5 151H143.5C147.642 151 151 147.642 151 143.5V8.5C151 4.35786 147.642 1 143.5 1ZM68.5 136H16V106H68.5V136ZM68.5 91H16V61H68.5V91ZM136 136H83.5V106H136V136ZM136 91H83.5V61H136V91ZM136 46H16V16H136V46Z" fill="#F7F7F7"/>
|
||||
<path d="M68.5 136V136.5H69V136H68.5ZM16 136H15.5V136.5H16V136ZM16 106V105.5H15.5V106H16ZM68.5 106H69V105.5H68.5V106ZM68.5 91V91.5H69V91H68.5ZM16 91H15.5V91.5H16V91ZM16 61V60.5H15.5V61H16ZM68.5 61H69V60.5H68.5V61ZM136 136V136.5H136.5V136H136ZM83.5 136H83V136.5H83.5V136ZM83.5 106V105.5H83V106H83.5ZM136 106H136.5V105.5H136V106ZM136 91V91.5H136.5V91H136ZM83.5 91H83V91.5H83.5V91ZM83.5 61V60.5H83V61H83.5ZM136 61H136.5V60.5H136V61ZM136 46V46.5H136.5V46H136ZM16 46H15.5V46.5H16V46ZM16 16V15.5H15.5V16H16ZM136 16H136.5V15.5H136V16ZM143.5 0.5H8.5V1.5H143.5V0.5ZM8.5 0.5C4.08172 0.5 0.5 4.08172 0.5 8.5H1.5C1.5 4.63401 4.63401 1.5 8.5 1.5V0.5ZM0.5 8.5V143.5H1.5V8.5H0.5ZM0.5 143.5C0.5 147.918 4.08172 151.5 8.5 151.5V150.5C4.63401 150.5 1.5 147.366 1.5 143.5H0.5ZM8.5 151.5H143.5V150.5H8.5V151.5ZM143.5 151.5C147.918 151.5 151.5 147.918 151.5 143.5H150.5C150.5 147.366 147.366 150.5 143.5 150.5V151.5ZM151.5 143.5V8.5H150.5V143.5H151.5ZM151.5 8.5C151.5 4.08172 147.918 0.5 143.5 0.5V1.5C147.366 1.5 150.5 4.63401 150.5 8.5H151.5ZM68.5 135.5H16V136.5H68.5V135.5ZM16.5 136V106H15.5V136H16.5ZM16 106.5H68.5V105.5H16V106.5ZM68 106V136H69V106H68ZM68.5 90.5H16V91.5H68.5V90.5ZM16.5 91V61H15.5V91H16.5ZM16 61.5H68.5V60.5H16V61.5ZM68 61V91H69V61H68ZM136 135.5H83.5V136.5H136V135.5ZM84 136V106H83V136H84ZM83.5 106.5H136V105.5H83.5V106.5ZM135.5 106V136H136.5V106H135.5ZM136 90.5H83.5V91.5H136V90.5ZM84 91V61H83V91H84ZM83.5 61.5H136V60.5H83.5V61.5ZM135.5 61V91H136.5V61H135.5ZM136 45.5H16V46.5H136V45.5ZM16.5 46V16H15.5V46H16.5ZM16 16.5H136V15.5H16V16.5ZM135.5 16V46H136.5V16H135.5Z" fill="#E0E0E0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -31,7 +31,7 @@ export enum EmptyStateSize {
|
|||
export interface EmptyStateSmallProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
image: ReactNode;
|
||||
image?: ReactNode;
|
||||
}
|
||||
|
||||
export interface EmptyStateProps extends EmptyStateSmallProps {
|
||||
|
|
@ -156,7 +156,7 @@ export const EmptyStateBig = ({
|
|||
className,
|
||||
}: EmptyStateProps) => (
|
||||
<EmptyStateContainer className={className}>
|
||||
<ImageContainer image={image} size={EmptyStateSize.Big} />
|
||||
{image && <ImageContainer image={image} size={EmptyStateSize.Big} />}
|
||||
<TextContainer
|
||||
css={(theme: SupersetTheme) =>
|
||||
css`
|
||||
|
|
@ -187,7 +187,7 @@ export const EmptyStateMedium = ({
|
|||
buttonText,
|
||||
}: EmptyStateProps) => (
|
||||
<EmptyStateContainer>
|
||||
<ImageContainer image={image} size={EmptyStateSize.Medium} />
|
||||
{image && <ImageContainer image={image} size={EmptyStateSize.Medium} />}
|
||||
<TextContainer
|
||||
css={(theme: SupersetTheme) =>
|
||||
css`
|
||||
|
|
@ -216,7 +216,7 @@ export const EmptyStateSmall = ({
|
|||
description,
|
||||
}: EmptyStateSmallProps) => (
|
||||
<EmptyStateContainer>
|
||||
<ImageContainer image={image} size={EmptyStateSize.Small} />
|
||||
{image && <ImageContainer image={image} size={EmptyStateSize.Small} />}
|
||||
<TextContainer
|
||||
css={(theme: SupersetTheme) =>
|
||||
css`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import DatasetPanel from './DatasetPanel';
|
||||
import { exampleColumns } from './fixtures';
|
||||
|
||||
export default {
|
||||
title: 'Superset App/views/CRUD/data/dataset/DatasetPanel',
|
||||
component: DatasetPanel,
|
||||
} as ComponentMeta<typeof DatasetPanel>;
|
||||
|
||||
export const Basic: ComponentStory<typeof DatasetPanel> = args => (
|
||||
<ThemeProvider theme={supersetTheme}>
|
||||
<div style={{ height: '350px' }}>
|
||||
<DatasetPanel {...args} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
Basic.args = {
|
||||
tableName: 'example_table',
|
||||
loading: false,
|
||||
hasError: false,
|
||||
columnList: exampleColumns,
|
||||
};
|
||||
|
|
@ -18,24 +18,124 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import DatasetPanel from 'src/views/CRUD/data/dataset/AddDataset/DatasetPanel';
|
||||
import DatasetPanel, {
|
||||
REFRESHING,
|
||||
ALT_LOADING,
|
||||
tableColumnDefinition,
|
||||
COLUMN_TITLE,
|
||||
} from './DatasetPanel';
|
||||
import { exampleColumns } from './fixtures';
|
||||
import {
|
||||
SELECT_MESSAGE,
|
||||
CREATE_MESSAGE,
|
||||
VIEW_DATASET_MESSAGE,
|
||||
SELECT_TABLE_TITLE,
|
||||
NO_COLUMNS_TITLE,
|
||||
NO_COLUMNS_DESCRIPTION,
|
||||
ERROR_TITLE,
|
||||
ERROR_DESCRIPTION,
|
||||
} from './MessageContent';
|
||||
|
||||
jest.mock(
|
||||
'src/components/Icons/Icon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) =>
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />,
|
||||
);
|
||||
|
||||
describe('DatasetPanel', () => {
|
||||
it('renders a blank state DatasetPanel', () => {
|
||||
render(<DatasetPanel />);
|
||||
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
|
||||
|
||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||
const blankDatasetTitle = screen.getByText(/select dataset source/i);
|
||||
const blankDatasetDescription = screen.getByText(
|
||||
/datasets can be created from database tables or sql queries\. select a database table to the left or to open sql lab\. from there you can save the query as a dataset\./i,
|
||||
);
|
||||
const sqlLabLink = screen.getByRole('button', {
|
||||
name: /create dataset from sql query/i,
|
||||
});
|
||||
|
||||
expect(blankDatasetImg).toBeVisible();
|
||||
const blankDatasetTitle = screen.getByText(SELECT_TABLE_TITLE);
|
||||
expect(blankDatasetTitle).toBeVisible();
|
||||
expect(blankDatasetDescription).toBeVisible();
|
||||
const blankDatasetDescription1 = screen.getByText(SELECT_MESSAGE, {
|
||||
exact: false,
|
||||
});
|
||||
expect(blankDatasetDescription1).toBeVisible();
|
||||
const blankDatasetDescription2 = screen.getByText(VIEW_DATASET_MESSAGE, {
|
||||
exact: false,
|
||||
});
|
||||
expect(blankDatasetDescription2).toBeVisible();
|
||||
const sqlLabLink = screen.getByRole('button', {
|
||||
name: CREATE_MESSAGE,
|
||||
});
|
||||
expect(sqlLabLink).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a no columns screen', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError={false}
|
||||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||
expect(blankDatasetImg).toBeVisible();
|
||||
const noColumnsTitle = screen.getByText(NO_COLUMNS_TITLE);
|
||||
expect(noColumnsTitle).toBeVisible();
|
||||
const noColumnsDescription = screen.getByText(NO_COLUMNS_DESCRIPTION);
|
||||
expect(noColumnsDescription).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a loading screen', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError={false}
|
||||
columnList={[]}
|
||||
loading
|
||||
/>,
|
||||
);
|
||||
|
||||
const blankDatasetImg = screen.getByAltText(ALT_LOADING);
|
||||
expect(blankDatasetImg).toBeVisible();
|
||||
const blankDatasetTitle = screen.getByText(REFRESHING);
|
||||
expect(blankDatasetTitle).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders an error screen', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError
|
||||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const errorTitle = screen.getByText(ERROR_TITLE);
|
||||
expect(errorTitle).toBeVisible();
|
||||
const errorDescription = screen.getByText(ERROR_DESCRIPTION);
|
||||
expect(errorDescription).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a table with columns displayed', async () => {
|
||||
const tableName = 'example_name';
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName={tableName}
|
||||
hasError={false}
|
||||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
/>,
|
||||
);
|
||||
expect(await screen.findByText(tableName)).toBeVisible();
|
||||
expect(screen.getByText(COLUMN_TITLE)).toBeVisible();
|
||||
expect(
|
||||
screen.getByText(tableColumnDefinition[0].title as string),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(tableColumnDefinition[1].title as string),
|
||||
).toBeInTheDocument();
|
||||
exampleColumns.forEach(row => {
|
||||
expect(screen.getByText(row.name)).toBeInTheDocument();
|
||||
expect(screen.getByText(row.type)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { supersetTheme, t, styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import Table, { ColumnsType, TableSize } from 'src/components/Table';
|
||||
import { alphabeticalSort } from 'src/components/Table/sorters';
|
||||
// @ts-ignore
|
||||
import LOADING_GIF from 'src/assets/images/loading.gif';
|
||||
import { ITableColumn } from './types';
|
||||
import MessageContent from './MessageContent';
|
||||
|
||||
/**
|
||||
* Enum defining CSS position options
|
||||
*/
|
||||
enum EPosition {
|
||||
ABSOLUTE = 'absolute',
|
||||
RELATIVE = 'relative',
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for StyledHeader
|
||||
*/
|
||||
interface StyledHeaderProps {
|
||||
/**
|
||||
* Determine the CSS positioning type
|
||||
* Vertical centering of loader, No columns screen, and select table screen
|
||||
* gets offset when the header position is relative and needs to be absolute, but table
|
||||
* needs this positioned relative to render correctly
|
||||
*/
|
||||
position: EPosition;
|
||||
}
|
||||
|
||||
const LOADER_WIDTH = 200;
|
||||
const SPINNER_WIDTH = 120;
|
||||
const HALF = 0.5;
|
||||
const MARGIN_MULTIPLIER = 3;
|
||||
|
||||
const StyledHeader = styled.div<StyledHeaderProps>`
|
||||
position: ${(props: StyledHeaderProps) => props.position};
|
||||
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
margin-top: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
||||
font-size: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.medium};
|
||||
padding-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.anticon:first-of-type {
|
||||
margin-right: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
||||
}
|
||||
|
||||
.anticon:nth-of-type(2) {
|
||||
margin-left: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
margin-left: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||
`;
|
||||
|
||||
const LoaderContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
||||
${({ theme }) => theme.gridUnit * 6}px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledLoader = styled.div`
|
||||
max-width: 50%;
|
||||
width: ${LOADER_WIDTH}px;
|
||||
|
||||
img {
|
||||
width: ${SPINNER_WIDTH}px;
|
||||
margin-left: ${(LOADER_WIDTH - SPINNER_WIDTH) * HALF}px;
|
||||
}
|
||||
|
||||
div {
|
||||
width: 100%;
|
||||
margin-top: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
text-align: center;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.normal};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.l}px;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
}
|
||||
`;
|
||||
|
||||
const TableContainer = styled.div`
|
||||
position: relative;
|
||||
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||
overflow: scroll;
|
||||
height: calc(100% - ${({ theme }) => theme.gridUnit * 36}px);
|
||||
`;
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
export const REFRESHING = t('Refreshing columns');
|
||||
export const COLUMN_TITLE = t('Table columns');
|
||||
export const ALT_LOADING = t('Loading');
|
||||
|
||||
const pageSizeOptions = ['5', '10', '15', '25'];
|
||||
|
||||
// Define the columns for Table instance
|
||||
export const tableColumnDefinition: ColumnsType<ITableColumn> = [
|
||||
{
|
||||
title: 'Column Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
sorter: (a: ITableColumn, b: ITableColumn) =>
|
||||
alphabeticalSort('name', a, b),
|
||||
},
|
||||
{
|
||||
title: 'Datatype',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: '100px',
|
||||
sorter: (a: ITableColumn, b: ITableColumn) =>
|
||||
alphabeticalSort('type', a, b),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Props interface for DatasetPanel
|
||||
*/
|
||||
export interface IDatasetPanelProps {
|
||||
/**
|
||||
* Name of the database table
|
||||
*/
|
||||
tableName?: string | null;
|
||||
/**
|
||||
* Array of ITableColumn instances with name and type attributes
|
||||
*/
|
||||
columnList: ITableColumn[];
|
||||
/**
|
||||
* Boolean indicating if there is an error state
|
||||
*/
|
||||
hasError: boolean;
|
||||
/**
|
||||
* Boolean indicating if the component is in a loading state
|
||||
*/
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const DatasetPanel = ({
|
||||
tableName,
|
||||
columnList,
|
||||
loading,
|
||||
hasError,
|
||||
}: IDatasetPanelProps) => {
|
||||
const hasColumns = columnList?.length > 0 ?? false;
|
||||
|
||||
let component;
|
||||
if (loading) {
|
||||
component = (
|
||||
<LoaderContainer>
|
||||
<StyledLoader>
|
||||
<img alt={ALT_LOADING} src={LOADING_GIF} />
|
||||
<div>{REFRESHING}</div>
|
||||
</StyledLoader>
|
||||
</LoaderContainer>
|
||||
);
|
||||
} else if (tableName && hasColumns && !hasError) {
|
||||
component = (
|
||||
<>
|
||||
<StyledTitle>{COLUMN_TITLE}</StyledTitle>
|
||||
<TableContainer>
|
||||
<StyledTable
|
||||
loading={loading}
|
||||
size={TableSize.SMALL}
|
||||
columns={tableColumnDefinition}
|
||||
data={columnList}
|
||||
pageSizeOptions={pageSizeOptions}
|
||||
defaultPageSize={10}
|
||||
/>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<MessageContent
|
||||
hasColumns={hasColumns}
|
||||
hasError={hasError}
|
||||
tableName={tableName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{tableName && (
|
||||
<StyledHeader
|
||||
position={
|
||||
!loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE
|
||||
}
|
||||
title={tableName || ''}
|
||||
>
|
||||
{tableName && (
|
||||
<Icons.Table iconColor={supersetTheme.colors.grayscale.base} />
|
||||
)}
|
||||
{tableName}
|
||||
</StyledHeader>
|
||||
)}
|
||||
{component}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetPanel;
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
||||
${({ theme }) => theme.gridUnit * 6}px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledEmptyStateBig = styled(EmptyStateBig)`
|
||||
max-width: 50%;
|
||||
|
||||
p {
|
||||
width: ${({ theme }) => theme.gridUnit * 115}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SELECT_MESSAGE = t(
|
||||
'Datasets can be created from database tables or SQL queries. Select a database table to the left or ',
|
||||
);
|
||||
export const CREATE_MESSAGE = t('create dataset from SQL query');
|
||||
export const VIEW_DATASET_MESSAGE = t(
|
||||
' to open SQL Lab. From there you can save the query as a dataset.',
|
||||
);
|
||||
|
||||
const renderEmptyDescription = () => (
|
||||
<>
|
||||
{SELECT_MESSAGE}
|
||||
<span
|
||||
role="button"
|
||||
onClick={() => {
|
||||
window.location.href = `/superset/sqllab`;
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{CREATE_MESSAGE}
|
||||
</span>
|
||||
{VIEW_DATASET_MESSAGE}
|
||||
</>
|
||||
);
|
||||
|
||||
export const SELECT_TABLE_TITLE = t('Select dataset source');
|
||||
export const NO_COLUMNS_TITLE = t('No table columns');
|
||||
export const NO_COLUMNS_DESCRIPTION = t(
|
||||
'This database table does not contain any data. Please select a different table.',
|
||||
);
|
||||
export const ERROR_TITLE = t('An Error Occurred');
|
||||
export const ERROR_DESCRIPTION = t(
|
||||
'Unable to load columns for the selected table. Please select a different table.',
|
||||
);
|
||||
|
||||
interface MessageContentProps {
|
||||
hasError: boolean;
|
||||
tableName?: string | null;
|
||||
hasColumns: boolean;
|
||||
}
|
||||
|
||||
export const MessageContent = (props: MessageContentProps) => {
|
||||
const { hasError, tableName, hasColumns } = props;
|
||||
let currentImage: string | undefined = 'empty-dataset.svg';
|
||||
let currentTitle = SELECT_TABLE_TITLE;
|
||||
let currentDescription = renderEmptyDescription();
|
||||
if (hasError) {
|
||||
currentTitle = ERROR_TITLE;
|
||||
currentDescription = <>{ERROR_DESCRIPTION}</>;
|
||||
currentImage = undefined;
|
||||
} else if (tableName && !hasColumns) {
|
||||
currentImage = 'no-columns.svg';
|
||||
currentTitle = NO_COLUMNS_TITLE;
|
||||
currentDescription = <>{NO_COLUMNS_DESCRIPTION}</>;
|
||||
}
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledEmptyStateBig
|
||||
image={currentImage}
|
||||
title={currentTitle}
|
||||
description={currentDescription}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageContent;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ITableColumn } from './types';
|
||||
|
||||
export const exampleColumns: ITableColumn[] = [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'STRING',
|
||||
},
|
||||
{
|
||||
name: 'height_in_inches',
|
||||
type: 'NUMBER',
|
||||
},
|
||||
{
|
||||
name: 'birth_date',
|
||||
type: 'DATE',
|
||||
},
|
||||
];
|
||||
|
|
@ -16,78 +16,112 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { supersetTheme, t, styled } from '@superset-ui/core';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { EmptyStateBig } from 'src/components/EmptyState';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import DatasetPanel from './DatasetPanel';
|
||||
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
|
||||
|
||||
type DatasetPanelProps = {
|
||||
/**
|
||||
* Interface for the getTableMetadata API call
|
||||
*/
|
||||
interface IColumnProps {
|
||||
/**
|
||||
* Unique id of the database
|
||||
*/
|
||||
dbId: number;
|
||||
/**
|
||||
* Name of the table
|
||||
*/
|
||||
tableName: string;
|
||||
/**
|
||||
* Name of the schema
|
||||
*/
|
||||
schema: string;
|
||||
}
|
||||
|
||||
export interface IDatasetPanelWrapperProps {
|
||||
/**
|
||||
* Name of the database table
|
||||
*/
|
||||
tableName?: string | null;
|
||||
};
|
||||
/**
|
||||
* Database ID
|
||||
*/
|
||||
dbId?: number;
|
||||
/**
|
||||
* The selected schema for the database
|
||||
*/
|
||||
schema?: string | null;
|
||||
setHasColumns?: Function;
|
||||
}
|
||||
|
||||
const StyledEmptyStateBig = styled(EmptyStateBig)`
|
||||
p {
|
||||
width: ${({ theme }) => theme.gridUnit * 115}px;
|
||||
}
|
||||
`;
|
||||
const DatasetPanelWrapper = ({
|
||||
tableName,
|
||||
dbId,
|
||||
schema,
|
||||
setHasColumns,
|
||||
}: IDatasetPanelWrapperProps) => {
|
||||
const [columnList, setColumnList] = useState<ITableColumn[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const tableNameRef = useRef(tableName);
|
||||
|
||||
const StyledDatasetPanel = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
||||
${({ theme }) => theme.gridUnit * 6}px;
|
||||
const getTableMetadata = async (props: IColumnProps) => {
|
||||
const { dbId, tableName, schema } = props;
|
||||
setLoading(true);
|
||||
setHasColumns?.(false);
|
||||
const path = `/api/v1/database/${dbId}/table/${tableName}/${schema}/`;
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: path,
|
||||
});
|
||||
|
||||
.table-name {
|
||||
font-size: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
font-weight: ${({ theme }) => theme.typography.weights.medium};
|
||||
padding-bottom: ${({ theme }) => theme.gridUnit * 20}px;
|
||||
margin: 0;
|
||||
|
||||
.anticon:first-of-type {
|
||||
margin-right: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
if (isIDatabaseTable(response?.json)) {
|
||||
const table: IDatabaseTable = response.json as IDatabaseTable;
|
||||
/**
|
||||
* The user is able to click other table columns while the http call for last selected table column is made
|
||||
* This check ensures we process the response that matches the last selected table name and ignore the others
|
||||
*/
|
||||
if (table.name === tableNameRef.current) {
|
||||
setColumnList(table.columns);
|
||||
setHasColumns?.(table.columns.length > 0);
|
||||
setHasError(false);
|
||||
}
|
||||
} else {
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setHasError(true);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`The API response from ${path} does not match the IDatabaseTable interface.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setHasError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
.anticon:nth-of-type(2) {
|
||||
margin-left: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
useEffect(() => {
|
||||
tableNameRef.current = tableName;
|
||||
if (tableName && schema && dbId) {
|
||||
getTableMetadata({ tableName, dbId, schema });
|
||||
}
|
||||
}
|
||||
// getTableMetadata is a const and should not be independency array
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tableName, dbId, schema]);
|
||||
|
||||
span {
|
||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||
}
|
||||
`;
|
||||
|
||||
const renderEmptyDescription = () => (
|
||||
<>
|
||||
{t(
|
||||
'Datasets can be created from database tables or SQL queries. Select a database table to the left or ',
|
||||
)}
|
||||
<span
|
||||
role="button"
|
||||
onClick={() => {
|
||||
window.location.href = `/superset/sqllab`;
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
{t('create dataset from SQL query')}
|
||||
</span>
|
||||
{t(' to open SQL Lab. From there you can save the query as a dataset.')}
|
||||
</>
|
||||
);
|
||||
|
||||
const DatasetPanel = ({ tableName }: DatasetPanelProps) =>
|
||||
tableName ? (
|
||||
<StyledDatasetPanel>
|
||||
<div className="table-name">
|
||||
<Icons.Table iconColor={supersetTheme.colors.grayscale.base} />
|
||||
{tableName}
|
||||
</div>
|
||||
<span>{t('Table columns')}</span>
|
||||
</StyledDatasetPanel>
|
||||
) : (
|
||||
<StyledEmptyStateBig
|
||||
image="empty-dataset.svg"
|
||||
title={t('Select dataset source')}
|
||||
description={renderEmptyDescription()}
|
||||
return (
|
||||
<DatasetPanel
|
||||
columnList={columnList}
|
||||
hasError={hasError}
|
||||
loading={loading}
|
||||
tableName={tableName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasetPanel;
|
||||
export default DatasetPanelWrapper;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for table columns dataset
|
||||
*/
|
||||
export interface ITableColumn {
|
||||
/**
|
||||
* Name of the column
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Datatype of the column
|
||||
*/
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given item matches the ITableColumn interface
|
||||
* @param item Object to check if it matches the ITableColumn interface
|
||||
* @returns boolean true if matches interface
|
||||
*/
|
||||
export const isITableColumn = (item: any): boolean => {
|
||||
let match = true;
|
||||
const BASE_ERROR =
|
||||
'The object provided to isITableColumn does match the interface.';
|
||||
if (typeof item?.name !== 'string') {
|
||||
match = false;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${BASE_ERROR} The property 'name' is required and must be a string`,
|
||||
);
|
||||
}
|
||||
if (match && typeof item?.type !== 'string') {
|
||||
match = false;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${BASE_ERROR} The property 'type' is required and must be a string`,
|
||||
);
|
||||
}
|
||||
return match;
|
||||
};
|
||||
|
||||
export interface IDatabaseTable {
|
||||
name: string;
|
||||
columns: ITableColumn[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given item matches the isIDatabsetTable interface
|
||||
* @param item Object to check if it matches the isIDatabsetTable interface
|
||||
* @returns boolean true if matches interface
|
||||
*/
|
||||
export const isIDatabaseTable = (item: any): boolean => {
|
||||
let match = true;
|
||||
if (typeof item?.name !== 'string') {
|
||||
match = false;
|
||||
}
|
||||
if (match && !Array.isArray(item.columns)) {
|
||||
match = false;
|
||||
}
|
||||
if (match && item.columns.length > 0) {
|
||||
const invalid = item.columns.some((column: any, index: number) => {
|
||||
const valid = isITableColumn(column);
|
||||
if (!valid) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`The provided object does not match the IDatabaseTable interface. columns[${index}] is invalid and does not match the ITableColumn interface`,
|
||||
);
|
||||
}
|
||||
return !valid;
|
||||
});
|
||||
match = !invalid;
|
||||
}
|
||||
return match;
|
||||
};
|
||||
|
|
@ -36,6 +36,7 @@ const mockPropsWithDataset = {
|
|||
dataset_name: 'Untitled',
|
||||
table_name: 'real_info',
|
||||
},
|
||||
hasColumns: true,
|
||||
};
|
||||
|
||||
describe('Footer', () => {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ interface FooterProps {
|
|||
addDangerToast: () => void;
|
||||
datasetObject?: Partial<DatasetObject> | null;
|
||||
onDatasetAdd?: (dataset: DatasetObject) => void;
|
||||
hasColumns?: boolean;
|
||||
}
|
||||
|
||||
const INPUT_FIELDS = ['db', 'schema', 'table_name'];
|
||||
|
|
@ -46,7 +47,12 @@ const LOG_ACTIONS = [
|
|||
LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION,
|
||||
];
|
||||
|
||||
function Footer({ url, datasetObject, addDangerToast }: FooterProps) {
|
||||
function Footer({
|
||||
url,
|
||||
datasetObject,
|
||||
addDangerToast,
|
||||
hasColumns = false,
|
||||
}: FooterProps) {
|
||||
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
|
||||
'dataset',
|
||||
t('dataset'),
|
||||
|
|
@ -107,7 +113,7 @@ function Footer({ url, datasetObject, addDangerToast }: FooterProps) {
|
|||
<Button onClick={cancelButtonOnClick}>Cancel</Button>
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
disabled={!datasetObject?.table_name}
|
||||
disabled={!datasetObject?.table_name || !hasColumns}
|
||||
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
|
||||
onClick={onSave}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,14 +16,8 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
SetStateAction,
|
||||
Dispatch,
|
||||
} from 'react';
|
||||
import { SupersetClient, t, styled, FAST_DEBOUNCE } from '@superset-ui/core';
|
||||
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react';
|
||||
import { SupersetClient, t, styled } from '@superset-ui/core';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { Form } from 'src/components/Form';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
|
@ -34,7 +28,6 @@ import Loading from 'src/components/Loading';
|
|||
import DatabaseSelector, {
|
||||
DatabaseObject,
|
||||
} from 'src/components/DatabaseSelector';
|
||||
import { debounce } from 'lodash';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { DatasetActionType } from '../types';
|
||||
|
|
@ -208,17 +201,6 @@ export default function LeftPanel({
|
|||
}
|
||||
}, [resetTables]);
|
||||
|
||||
const search = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${dbId}/${encodedSchema}/`,
|
||||
);
|
||||
getTablesList(endpoint);
|
||||
}, FAST_DEBOUNCE),
|
||||
[dbId, encodedSchema],
|
||||
);
|
||||
|
||||
const filteredOptions = tableOptions.filter(option =>
|
||||
option?.value?.toLowerCase().includes(searchVal.toLowerCase()),
|
||||
);
|
||||
|
|
@ -266,7 +248,6 @@ export default function LeftPanel({
|
|||
value={searchVal}
|
||||
prefix={<SearchIcon iconSize="l" />}
|
||||
onChange={evt => {
|
||||
search(evt.target.value);
|
||||
setSearchVal(evt.target.value);
|
||||
}}
|
||||
className="table-form"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useReducer, Reducer } from 'react';
|
||||
import React, { useReducer, Reducer, useState } from 'react';
|
||||
import Header from './Header';
|
||||
import DatasetPanel from './DatasetPanel';
|
||||
import LeftPanel from './LeftPanel';
|
||||
|
|
@ -72,6 +72,7 @@ export default function AddDataset() {
|
|||
const [dataset, setDataset] = useReducer<
|
||||
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
||||
>(datasetReducer, null);
|
||||
const [hasColumns, setHasColumns] = useState(false);
|
||||
|
||||
const HeaderComponent = () => (
|
||||
<Header setDataset={setDataset} title={dataset?.table_name} />
|
||||
|
|
@ -86,11 +87,16 @@ export default function AddDataset() {
|
|||
);
|
||||
|
||||
const DatasetPanelComponent = () => (
|
||||
<DatasetPanel tableName={dataset?.table_name} />
|
||||
<DatasetPanel
|
||||
tableName={dataset?.table_name}
|
||||
dbId={dataset?.db?.id}
|
||||
schema={dataset?.schema}
|
||||
setHasColumns={setHasColumns}
|
||||
/>
|
||||
);
|
||||
|
||||
const FooterComponent = () => (
|
||||
<Footer url={prevUrl} datasetObject={dataset} />
|
||||
<Footer url={prevUrl} datasetObject={dataset} hasColumns={hasColumns} />
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const RightColumn = styled(Column)`
|
|||
height: auto;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
width: auto;
|
||||
width: calc(100% - ${({ theme }) => theme.gridUnit * 80}px);
|
||||
`;
|
||||
|
||||
const Row = styled.div`
|
||||
|
|
|
|||
Loading…
Reference in New Issue