mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
feat: Flow for tables that already have a dataset (#22136)
This commit is contained in:
parent
96de314c0c
commit
04b7a26365
@ -24,7 +24,7 @@ import DatasetPanel, {
|
|||||||
tableColumnDefinition,
|
tableColumnDefinition,
|
||||||
COLUMN_TITLE,
|
COLUMN_TITLE,
|
||||||
} from './DatasetPanel';
|
} from './DatasetPanel';
|
||||||
import { exampleColumns } from './fixtures';
|
import { exampleColumns, exampleDataset } from './fixtures';
|
||||||
import {
|
import {
|
||||||
SELECT_MESSAGE,
|
SELECT_MESSAGE,
|
||||||
CREATE_MESSAGE,
|
CREATE_MESSAGE,
|
||||||
@ -44,7 +44,7 @@ jest.mock(
|
|||||||
);
|
);
|
||||||
|
|
||||||
describe('DatasetPanel', () => {
|
describe('DatasetPanel', () => {
|
||||||
it('renders a blank state DatasetPanel', () => {
|
test('renders a blank state DatasetPanel', () => {
|
||||||
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
|
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
|
||||||
|
|
||||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||||
@ -65,7 +65,7 @@ describe('DatasetPanel', () => {
|
|||||||
expect(sqlLabLink).toBeVisible();
|
expect(sqlLabLink).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a no columns screen', () => {
|
test('renders a no columns screen', () => {
|
||||||
render(
|
render(
|
||||||
<DatasetPanel
|
<DatasetPanel
|
||||||
tableName="Name"
|
tableName="Name"
|
||||||
@ -83,7 +83,7 @@ describe('DatasetPanel', () => {
|
|||||||
expect(noColumnsDescription).toBeVisible();
|
expect(noColumnsDescription).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a loading screen', () => {
|
test('renders a loading screen', () => {
|
||||||
render(
|
render(
|
||||||
<DatasetPanel
|
<DatasetPanel
|
||||||
tableName="Name"
|
tableName="Name"
|
||||||
@ -99,7 +99,7 @@ describe('DatasetPanel', () => {
|
|||||||
expect(blankDatasetTitle).toBeVisible();
|
expect(blankDatasetTitle).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders an error screen', () => {
|
test('renders an error screen', () => {
|
||||||
render(
|
render(
|
||||||
<DatasetPanel
|
<DatasetPanel
|
||||||
tableName="Name"
|
tableName="Name"
|
||||||
@ -115,7 +115,7 @@ describe('DatasetPanel', () => {
|
|||||||
expect(errorDescription).toBeVisible();
|
expect(errorDescription).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a table with columns displayed', async () => {
|
test('renders a table with columns displayed', async () => {
|
||||||
const tableName = 'example_name';
|
const tableName = 'example_name';
|
||||||
render(
|
render(
|
||||||
<DatasetPanel
|
<DatasetPanel
|
||||||
@ -138,4 +138,23 @@ describe('DatasetPanel', () => {
|
|||||||
expect(screen.getByText(row.type)).toBeInTheDocument();
|
expect(screen.getByText(row.type)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders an info banner if table already has a dataset', async () => {
|
||||||
|
render(
|
||||||
|
<DatasetPanel
|
||||||
|
tableName="example_table"
|
||||||
|
hasError={false}
|
||||||
|
columnList={exampleColumns}
|
||||||
|
loading={false}
|
||||||
|
datasets={exampleDataset}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// This is text in the info banner
|
||||||
|
expect(
|
||||||
|
await screen.findByText(
|
||||||
|
/this table already has a dataset associated with it. you can only associate one dataset with a table./i,
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,12 +17,14 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { supersetTheme, t, styled } from '@superset-ui/core';
|
import { t, styled, useTheme } from '@superset-ui/core';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
|
import Alert from 'src/components/Alert';
|
||||||
import Table, { ColumnsType, TableSize } from 'src/components/Table';
|
import Table, { ColumnsType, TableSize } from 'src/components/Table';
|
||||||
import { alphabeticalSort } from 'src/components/Table/sorters';
|
import { alphabeticalSort } from 'src/components/Table/sorters';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import LOADING_GIF from 'src/assets/images/loading.gif';
|
import LOADING_GIF from 'src/assets/images/loading.gif';
|
||||||
|
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||||
import { ITableColumn } from './types';
|
import { ITableColumn } from './types';
|
||||||
import MessageContent from './MessageContent';
|
import MessageContent from './MessageContent';
|
||||||
|
|
||||||
@ -53,43 +55,56 @@ const HALF = 0.5;
|
|||||||
const MARGIN_MULTIPLIER = 3;
|
const MARGIN_MULTIPLIER = 3;
|
||||||
|
|
||||||
const StyledHeader = styled.div<StyledHeaderProps>`
|
const StyledHeader = styled.div<StyledHeaderProps>`
|
||||||
|
${({ theme }) => `
|
||||||
position: ${(props: StyledHeaderProps) => props.position};
|
position: ${(props: StyledHeaderProps) => props.position};
|
||||||
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
margin: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px
|
||||||
margin-top: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
${theme.gridUnit * MARGIN_MULTIPLIER}px
|
||||||
font-size: ${({ theme }) => theme.gridUnit * 6}px;
|
${theme.gridUnit * MARGIN_MULTIPLIER}px
|
||||||
font-weight: ${({ theme }) => theme.typography.weights.medium};
|
${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
|
||||||
padding-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
font-size: ${theme.gridUnit * 6}px;
|
||||||
|
font-weight: ${theme.typography.weights.medium};
|
||||||
|
padding-bottom: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
.anticon:first-of-type {
|
.anticon:first-of-type {
|
||||||
margin-right: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
margin-right: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anticon:nth-of-type(2) {
|
.anticon:nth-of-type(2) {
|
||||||
margin-left: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
|
||||||
}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTitle = styled.div`
|
const StyledTitle = styled.div`
|
||||||
margin-left: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
${({ theme }) => `
|
||||||
margin-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
|
||||||
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
margin-bottom: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||||
|
font-weight: ${theme.typography.weights.bold};
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const LoaderContainer = styled.div`
|
const LoaderContainer = styled.div`
|
||||||
padding: ${({ theme }) => theme.gridUnit * 8}px
|
${({ theme }) => `
|
||||||
${({ theme }) => theme.gridUnit * 6}px;
|
padding: ${theme.gridUnit * 8}px
|
||||||
|
${theme.gridUnit * 6}px;
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLoader = styled.div`
|
const StyledLoader = styled.div`
|
||||||
|
${({ theme }) => `
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
width: ${LOADER_WIDTH}px;
|
width: ${LOADER_WIDTH}px;
|
||||||
|
|
||||||
@ -100,19 +115,23 @@ const StyledLoader = styled.div`
|
|||||||
|
|
||||||
div {
|
div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
margin-top: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: ${({ theme }) => theme.typography.weights.normal};
|
font-weight: ${theme.typography.weights.normal};
|
||||||
font-size: ${({ theme }) => theme.typography.sizes.l}px;
|
font-size: ${theme.typography.sizes.l}px;
|
||||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
color: ${theme.colors.grayscale.light1};
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TableContainer = styled.div`
|
const TableContainer = styled.div`
|
||||||
|
${({ theme }) => `
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px;
|
margin: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
|
||||||
|
margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
height: calc(100% - ${({ theme }) => theme.gridUnit * 36}px);
|
height: calc(100% - ${theme.gridUnit * 36}px);
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTable = styled(Table)`
|
const StyledTable = styled(Table)`
|
||||||
@ -123,6 +142,26 @@ const StyledTable = styled(Table)`
|
|||||||
right: 0;
|
right: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledAlert = styled(Alert)`
|
||||||
|
${({ theme }) => `
|
||||||
|
border: 1px solid ${theme.colors.info.base};
|
||||||
|
padding: ${theme.gridUnit * 4}px;
|
||||||
|
margin: ${theme.gridUnit * 6}px ${theme.gridUnit * 6}px
|
||||||
|
${theme.gridUnit * 8}px;
|
||||||
|
.view-dataset-button {
|
||||||
|
position: absolute;
|
||||||
|
top: ${theme.gridUnit * 4}px;
|
||||||
|
right: ${theme.gridUnit * 4}px;
|
||||||
|
font-weight: ${theme.typography.weights.normal};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.secondary.dark3};
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
export const REFRESHING = t('Refreshing columns');
|
export const REFRESHING = t('Refreshing columns');
|
||||||
export const COLUMN_TITLE = t('Table columns');
|
export const COLUMN_TITLE = t('Table columns');
|
||||||
export const ALT_LOADING = t('Loading');
|
export const ALT_LOADING = t('Loading');
|
||||||
@ -168,19 +207,57 @@ export interface IDatasetPanelProps {
|
|||||||
* Boolean indicating if the component is in a loading state
|
* Boolean indicating if the component is in a loading state
|
||||||
*/
|
*/
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
datasets?: DatasetObject[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXISTING_DATASET_DESCRIPTION = t(
|
||||||
|
'This table already has a dataset associated with it. You can only associate one dataset with a table.\n',
|
||||||
|
);
|
||||||
|
const VIEW_DATASET = t('View Dataset');
|
||||||
|
|
||||||
|
const renderExistingDatasetAlert = (dataset?: DatasetObject) => (
|
||||||
|
<StyledAlert
|
||||||
|
closable={false}
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message={t('This table already has a dataset')}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{EXISTING_DATASET_DESCRIPTION}
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(
|
||||||
|
dataset?.explore_url,
|
||||||
|
'_blank',
|
||||||
|
'noreferrer noopener popup=false',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
className="view-dataset-button"
|
||||||
|
>
|
||||||
|
{VIEW_DATASET}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const DatasetPanel = ({
|
const DatasetPanel = ({
|
||||||
tableName,
|
tableName,
|
||||||
columnList,
|
columnList,
|
||||||
loading,
|
loading,
|
||||||
hasError,
|
hasError,
|
||||||
|
datasets,
|
||||||
}: IDatasetPanelProps) => {
|
}: IDatasetPanelProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
const hasColumns = columnList?.length > 0 ?? false;
|
const hasColumns = columnList?.length > 0 ?? false;
|
||||||
|
const datasetNames = datasets?.map(dataset => dataset.table_name);
|
||||||
|
|
||||||
let component;
|
let component;
|
||||||
|
let loader;
|
||||||
if (loading) {
|
if (loading) {
|
||||||
component = (
|
loader = (
|
||||||
<LoaderContainer>
|
<LoaderContainer>
|
||||||
<StyledLoader>
|
<StyledLoader>
|
||||||
<img alt={ALT_LOADING} src={LOADING_GIF} />
|
<img alt={ALT_LOADING} src={LOADING_GIF} />
|
||||||
@ -188,7 +265,9 @@ const DatasetPanel = ({
|
|||||||
</StyledLoader>
|
</StyledLoader>
|
||||||
</LoaderContainer>
|
</LoaderContainer>
|
||||||
);
|
);
|
||||||
} else if (tableName && hasColumns && !hasError) {
|
}
|
||||||
|
if (!loading) {
|
||||||
|
if (!loading && tableName && hasColumns && !hasError) {
|
||||||
component = (
|
component = (
|
||||||
<>
|
<>
|
||||||
<StyledTitle>{COLUMN_TITLE}</StyledTitle>
|
<StyledTitle>{COLUMN_TITLE}</StyledTitle>
|
||||||
@ -213,10 +292,16 @@ const DatasetPanel = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{tableName && (
|
{tableName && (
|
||||||
|
<>
|
||||||
|
{datasetNames?.includes(tableName) &&
|
||||||
|
renderExistingDatasetAlert(
|
||||||
|
datasets?.find(dataset => dataset.table_name === tableName),
|
||||||
|
)}
|
||||||
<StyledHeader
|
<StyledHeader
|
||||||
position={
|
position={
|
||||||
!loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE
|
!loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE
|
||||||
@ -224,12 +309,14 @@ const DatasetPanel = ({
|
|||||||
title={tableName || ''}
|
title={tableName || ''}
|
||||||
>
|
>
|
||||||
{tableName && (
|
{tableName && (
|
||||||
<Icons.Table iconColor={supersetTheme.colors.grayscale.base} />
|
<Icons.Table iconColor={theme.colors.grayscale.base} />
|
||||||
)}
|
)}
|
||||||
{tableName}
|
{tableName}
|
||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{component}
|
{component}
|
||||||
|
{loader}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||||
import { ITableColumn } from './types';
|
import { ITableColumn } from './types';
|
||||||
|
|
||||||
export const exampleColumns: ITableColumn[] = [
|
export const exampleColumns: ITableColumn[] = [
|
||||||
@ -32,3 +33,16 @@ export const exampleColumns: ITableColumn[] = [
|
|||||||
type: 'DATE',
|
type: 'DATE',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const exampleDataset: DatasetObject[] = [
|
||||||
|
{
|
||||||
|
db: {
|
||||||
|
id: 1,
|
||||||
|
database_name: 'test_database',
|
||||||
|
owners: [1],
|
||||||
|
},
|
||||||
|
schema: 'test_schema',
|
||||||
|
dataset_name: 'example_dataset',
|
||||||
|
table_name: 'example_table',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { SupersetClient } from '@superset-ui/core';
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||||
import DatasetPanel from './DatasetPanel';
|
import DatasetPanel from './DatasetPanel';
|
||||||
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
|
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ export interface IDatasetPanelWrapperProps {
|
|||||||
*/
|
*/
|
||||||
schema?: string | null;
|
schema?: string | null;
|
||||||
setHasColumns?: Function;
|
setHasColumns?: Function;
|
||||||
|
datasets?: DatasetObject[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DatasetPanelWrapper = ({
|
const DatasetPanelWrapper = ({
|
||||||
@ -60,6 +62,7 @@ const DatasetPanelWrapper = ({
|
|||||||
dbId,
|
dbId,
|
||||||
schema,
|
schema,
|
||||||
setHasColumns,
|
setHasColumns,
|
||||||
|
datasets,
|
||||||
}: IDatasetPanelWrapperProps) => {
|
}: IDatasetPanelWrapperProps) => {
|
||||||
const [columnList, setColumnList] = useState<ITableColumn[]>([]);
|
const [columnList, setColumnList] = useState<ITableColumn[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -120,6 +123,7 @@ const DatasetPanelWrapper = ({
|
|||||||
hasError={hasError}
|
hasError={hasError}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
|
datasets={datasets}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@ const mockPropsWithDataset = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('Footer', () => {
|
describe('Footer', () => {
|
||||||
it('renders a Footer with a cancel button and a disabled create button', () => {
|
test('renders a Footer with a cancel button and a disabled create button', () => {
|
||||||
render(<Footer {...mockedProps} />, { useRedux: true });
|
render(<Footer {...mockedProps} />, { useRedux: true });
|
||||||
|
|
||||||
const saveButton = screen.getByRole('button', {
|
const saveButton = screen.getByRole('button', {
|
||||||
@ -55,7 +55,7 @@ describe('Footer', () => {
|
|||||||
expect(createButton).toBeDisabled();
|
expect(createButton).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Create Dataset button when a table is selected', () => {
|
test('renders a Create Dataset button when a table is selected', () => {
|
||||||
render(<Footer {...mockPropsWithDataset} />, { useRedux: true });
|
render(<Footer {...mockPropsWithDataset} />, { useRedux: true });
|
||||||
|
|
||||||
const createButton = screen.getByRole('button', {
|
const createButton = screen.getByRole('button', {
|
||||||
@ -64,4 +64,16 @@ describe('Footer', () => {
|
|||||||
|
|
||||||
expect(createButton).toBeEnabled();
|
expect(createButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('create button becomes disabled when table already has a dataset', () => {
|
||||||
|
render(<Footer datasets={['real_info']} {...mockPropsWithDataset} />, {
|
||||||
|
useRedux: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createButton = screen.getByRole('button', {
|
||||||
|
name: /Create/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createButton).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -37,6 +37,7 @@ interface FooterProps {
|
|||||||
datasetObject?: Partial<DatasetObject> | null;
|
datasetObject?: Partial<DatasetObject> | null;
|
||||||
onDatasetAdd?: (dataset: DatasetObject) => void;
|
onDatasetAdd?: (dataset: DatasetObject) => void;
|
||||||
hasColumns?: boolean;
|
hasColumns?: boolean;
|
||||||
|
datasets?: (string | null | undefined)[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INPUT_FIELDS = ['db', 'schema', 'table_name'];
|
const INPUT_FIELDS = ['db', 'schema', 'table_name'];
|
||||||
@ -52,6 +53,7 @@ function Footer({
|
|||||||
datasetObject,
|
datasetObject,
|
||||||
addDangerToast,
|
addDangerToast,
|
||||||
hasColumns = false,
|
hasColumns = false,
|
||||||
|
datasets,
|
||||||
}: FooterProps) {
|
}: FooterProps) {
|
||||||
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
|
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
|
||||||
'dataset',
|
'dataset',
|
||||||
@ -108,16 +110,22 @@ function Footer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CREATE_DATASET_TEXT = t('Create Dataset');
|
||||||
|
const disabledCheck =
|
||||||
|
!datasetObject?.table_name ||
|
||||||
|
!hasColumns ||
|
||||||
|
datasets?.includes(datasetObject?.table_name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={cancelButtonOnClick}>Cancel</Button>
|
<Button onClick={cancelButtonOnClick}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
disabled={!datasetObject?.table_name || !hasColumns}
|
disabled={disabledCheck}
|
||||||
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
|
tooltip={!datasetObject?.table_name ? tooltipText : undefined}
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
>
|
>
|
||||||
{t('Create Dataset')}
|
{CREATE_DATASET_TEXT}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -209,6 +209,7 @@ test('searches for a table name', async () => {
|
|||||||
useRedux: true,
|
useRedux: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Click 'test-postgres' database to access schemas
|
||||||
const databaseSelect = screen.getByRole('combobox', {
|
const databaseSelect = screen.getByRole('combobox', {
|
||||||
name: /select database or type database name/i,
|
name: /select database or type database name/i,
|
||||||
});
|
});
|
||||||
@ -221,6 +222,7 @@ test('searches for a table name', async () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(schemaSelect).toBeEnabled());
|
await waitFor(() => expect(schemaSelect).toBeEnabled());
|
||||||
|
|
||||||
|
// Click 'public' schema to access tables
|
||||||
userEvent.click(schemaSelect);
|
userEvent.click(schemaSelect);
|
||||||
userEvent.click(screen.getAllByText('public')[1]);
|
userEvent.click(screen.getAllByText('public')[1]);
|
||||||
|
|
||||||
@ -238,3 +240,46 @@ test('searches for a table name', async () => {
|
|||||||
expect(screen.queryByText('Sheet3')).not.toBeInTheDocument();
|
expect(screen.queryByText('Sheet3')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders a warning icon when a table name has a pre-existing dataset', async () => {
|
||||||
|
render(
|
||||||
|
<LeftPanel
|
||||||
|
setDataset={mockFun}
|
||||||
|
schema="schema_a"
|
||||||
|
dbId={1}
|
||||||
|
datasets={['Sheet2']}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
useRedux: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click 'test-postgres' database to access schemas
|
||||||
|
const databaseSelect = screen.getByRole('combobox', {
|
||||||
|
name: /select database or type database name/i,
|
||||||
|
});
|
||||||
|
userEvent.click(databaseSelect);
|
||||||
|
userEvent.click(await screen.findByText('test-postgres'));
|
||||||
|
|
||||||
|
const schemaSelect = screen.getByRole('combobox', {
|
||||||
|
name: /select schema or type schema name/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(schemaSelect).toBeEnabled());
|
||||||
|
|
||||||
|
// Warning icon should not show yet
|
||||||
|
expect(
|
||||||
|
screen.queryByRole('img', { name: 'warning' }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click 'public' schema to access tables
|
||||||
|
userEvent.click(schemaSelect);
|
||||||
|
userEvent.click(screen.getAllByText('public')[1]);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Sheet2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sheet2 should now show the warning icon
|
||||||
|
expect(screen.getByRole('img', { name: 'warning' })).toBeVisible();
|
||||||
|
});
|
||||||
|
@ -17,7 +17,14 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react';
|
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react';
|
||||||
import { SupersetClient, t, styled } from '@superset-ui/core';
|
import {
|
||||||
|
SupersetClient,
|
||||||
|
t,
|
||||||
|
styled,
|
||||||
|
css,
|
||||||
|
useTheme,
|
||||||
|
logging,
|
||||||
|
} from '@superset-ui/core';
|
||||||
import { Input } from 'src/components/Input';
|
import { Input } from 'src/components/Input';
|
||||||
import { Form } from 'src/components/Form';
|
import { Form } from 'src/components/Form';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
@ -36,6 +43,7 @@ interface LeftPanelProps {
|
|||||||
setDataset: Dispatch<SetStateAction<object>>;
|
setDataset: Dispatch<SetStateAction<object>>;
|
||||||
schema?: string | null | undefined;
|
schema?: string | null | undefined;
|
||||||
dbId?: number;
|
dbId?: number;
|
||||||
|
datasets?: (string | null | undefined)[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchIcon = styled(Icons.Search)`
|
const SearchIcon = styled(Icons.Search)`
|
||||||
@ -78,6 +86,7 @@ const LeftPanelStyle = styled.div`
|
|||||||
top: ${theme.gridUnit * 92.25}px;
|
top: ${theme.gridUnit * 92.25}px;
|
||||||
left: ${theme.gridUnit * 3.25}px;
|
left: ${theme.gridUnit * 3.25}px;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: ${theme.gridUnit * 1.75}px;
|
padding: ${theme.gridUnit * 1.75}px;
|
||||||
@ -86,6 +95,7 @@ const LeftPanelStyle = styled.div`
|
|||||||
background-color: ${theme.colors.grayscale.light4}
|
background-color: ${theme.colors.grayscale.light4}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-highlighted {
|
.options-highlighted {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: ${theme.gridUnit * 1.75}px;
|
padding: ${theme.gridUnit * 1.75}px;
|
||||||
@ -93,6 +103,12 @@ const LeftPanelStyle = styled.div`
|
|||||||
background-color: ${theme.colors.primary.dark1};
|
background-color: ${theme.colors.primary.dark1};
|
||||||
color: ${theme.colors.grayscale.light5};
|
color: ${theme.colors.grayscale.light5};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.options, .options-highlighted {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
form > span[aria-label="refresh"] {
|
form > span[aria-label="refresh"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -124,7 +140,10 @@ export default function LeftPanel({
|
|||||||
setDataset,
|
setDataset,
|
||||||
schema,
|
schema,
|
||||||
dbId,
|
dbId,
|
||||||
|
datasets,
|
||||||
}: LeftPanelProps) {
|
}: LeftPanelProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const [tableOptions, setTableOptions] = useState<Array<TableOption>>([]);
|
const [tableOptions, setTableOptions] = useState<Array<TableOption>>([]);
|
||||||
const [resetTables, setResetTables] = useState(false);
|
const [resetTables, setResetTables] = useState(false);
|
||||||
const [loadTables, setLoadTables] = useState(false);
|
const [loadTables, setLoadTables] = useState(false);
|
||||||
@ -166,9 +185,9 @@ export default function LeftPanel({
|
|||||||
setResetTables(false);
|
setResetTables(false);
|
||||||
setRefresh(false);
|
setRefresh(false);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(error =>
|
||||||
console.log('error', e);
|
logging.error('There was an error fetching tables', error),
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSchema = (schema: string) => {
|
const setSchema = (schema: string) => {
|
||||||
@ -212,21 +231,32 @@ export default function LeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SELECT_DATABASE_AND_SCHEMA_TEXT = t('Select database & schema');
|
||||||
|
const TABLE_LOADING_TEXT = t('Table loading');
|
||||||
|
const NO_TABLES_FOUND_TITLE = t('No database tables found');
|
||||||
|
const NO_TABLES_FOUND_DESCRIPTION = t('Try selecting a different schema');
|
||||||
|
const SELECT_DATABASE_TABLE_TEXT = t('Select database table');
|
||||||
|
const REFRESH_TABLE_LIST_TOOLTIP = t('Refresh table list');
|
||||||
|
const REFRESH_TABLES_TEXT = t('Refresh tables');
|
||||||
|
const SEARCH_TABLES_PLACEHOLDER_TEXT = t('Search tables');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftPanelStyle>
|
<LeftPanelStyle>
|
||||||
<p className="section-title db-schema">Select database & schema</p>
|
<p className="section-title db-schema">
|
||||||
|
{SELECT_DATABASE_AND_SCHEMA_TEXT}
|
||||||
|
</p>
|
||||||
<DatabaseSelector
|
<DatabaseSelector
|
||||||
handleError={addDangerToast}
|
handleError={addDangerToast}
|
||||||
onDbChange={setDatabase}
|
onDbChange={setDatabase}
|
||||||
onSchemaChange={setSchema}
|
onSchemaChange={setSchema}
|
||||||
/>
|
/>
|
||||||
{loadTables && !refresh && Loader('Table loading')}
|
{loadTables && !refresh && Loader(TABLE_LOADING_TEXT)}
|
||||||
{schema && !loadTables && !tableOptions.length && !searchVal && (
|
{schema && !loadTables && !tableOptions.length && !searchVal && (
|
||||||
<div className="emptystate">
|
<div className="emptystate">
|
||||||
<EmptyStateMedium
|
<EmptyStateMedium
|
||||||
image="empty-table.svg"
|
image="empty-table.svg"
|
||||||
title={t('No database tables found')}
|
title={NO_TABLES_FOUND_TITLE}
|
||||||
description={t('Try selecting a different schema')}
|
description={NO_TABLES_FOUND_DESCRIPTION}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -234,15 +264,15 @@ export default function LeftPanel({
|
|||||||
{schema && (tableOptions.length > 0 || searchVal.length > 0) && (
|
{schema && (tableOptions.length > 0 || searchVal.length > 0) && (
|
||||||
<>
|
<>
|
||||||
<Form>
|
<Form>
|
||||||
<p className="table-title">Select database table</p>
|
<p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p>
|
||||||
<RefreshLabel
|
<RefreshLabel
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLoadTables(true);
|
setLoadTables(true);
|
||||||
setRefresh(true);
|
setRefresh(true);
|
||||||
}}
|
}}
|
||||||
tooltipContent={t('Refresh table list')}
|
tooltipContent={REFRESH_TABLE_LIST_TOOLTIP}
|
||||||
/>
|
/>
|
||||||
{refresh && Loader('Refresh tables')}
|
{refresh && Loader(REFRESH_TABLES_TEXT)}
|
||||||
{!refresh && (
|
{!refresh && (
|
||||||
<Input
|
<Input
|
||||||
value={searchVal}
|
value={searchVal}
|
||||||
@ -251,7 +281,7 @@ export default function LeftPanel({
|
|||||||
setSearchVal(evt.target.value);
|
setSearchVal(evt.target.value);
|
||||||
}}
|
}}
|
||||||
className="table-form"
|
className="table-form"
|
||||||
placeholder={t('Search tables')}
|
placeholder={SEARCH_TABLES_PLACEHOLDER_TEXT}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -269,6 +299,19 @@ export default function LeftPanel({
|
|||||||
onClick={() => setTable(option.value, i)}
|
onClick={() => setTable(option.value, i)}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
|
{datasets?.includes(option.value) && (
|
||||||
|
<Icons.Warning
|
||||||
|
iconColor={
|
||||||
|
selectedTable === i
|
||||||
|
? theme.colors.grayscale.light5
|
||||||
|
: theme.colors.info.base
|
||||||
|
}
|
||||||
|
iconSize="m"
|
||||||
|
css={css`
|
||||||
|
margin-right: ${theme.gridUnit * 6}px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { useReducer, Reducer, useState } from 'react';
|
import React, { useReducer, Reducer, useEffect, useState } from 'react';
|
||||||
|
import { logging } from '@superset-ui/core';
|
||||||
|
import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks';
|
||||||
|
import rison from 'rison';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import DatasetPanel from './DatasetPanel';
|
import DatasetPanel from './DatasetPanel';
|
||||||
import LeftPanel from './LeftPanel';
|
import LeftPanel from './LeftPanel';
|
||||||
@ -73,6 +76,36 @@ export default function AddDataset() {
|
|||||||
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
||||||
>(datasetReducer, null);
|
>(datasetReducer, null);
|
||||||
const [hasColumns, setHasColumns] = useState(false);
|
const [hasColumns, setHasColumns] = useState(false);
|
||||||
|
const [datasets, setDatasets] = useState<DatasetObject[]>([]);
|
||||||
|
const datasetNames = datasets.map(dataset => dataset.table_name);
|
||||||
|
const encodedSchema = dataset?.schema
|
||||||
|
? encodeURIComponent(dataset?.schema)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const queryParams = dataset?.schema
|
||||||
|
? rison.encode_uri({
|
||||||
|
filters: [
|
||||||
|
{ col: 'schema', opr: 'eq', value: encodedSchema },
|
||||||
|
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: '!t' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const getDatasetsList = async () => {
|
||||||
|
await UseGetDatasetsList(queryParams)
|
||||||
|
.then(json => {
|
||||||
|
setDatasets(json?.result);
|
||||||
|
})
|
||||||
|
.catch(error =>
|
||||||
|
logging.error('There was an error fetching dataset', error),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dataset?.schema) {
|
||||||
|
getDatasetsList();
|
||||||
|
}
|
||||||
|
}, [dataset?.schema]);
|
||||||
|
|
||||||
const HeaderComponent = () => (
|
const HeaderComponent = () => (
|
||||||
<Header setDataset={setDataset} title={dataset?.table_name} />
|
<Header setDataset={setDataset} title={dataset?.table_name} />
|
||||||
@ -83,6 +116,7 @@ export default function AddDataset() {
|
|||||||
setDataset={setDataset}
|
setDataset={setDataset}
|
||||||
schema={dataset?.schema}
|
schema={dataset?.schema}
|
||||||
dbId={dataset?.db?.id}
|
dbId={dataset?.db?.id}
|
||||||
|
datasets={datasetNames}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -92,11 +126,17 @@ export default function AddDataset() {
|
|||||||
dbId={dataset?.db?.id}
|
dbId={dataset?.db?.id}
|
||||||
schema={dataset?.schema}
|
schema={dataset?.schema}
|
||||||
setHasColumns={setHasColumns}
|
setHasColumns={setHasColumns}
|
||||||
|
datasets={datasets}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const FooterComponent = () => (
|
const FooterComponent = () => (
|
||||||
<Footer url={prevUrl} datasetObject={dataset} hasColumns={hasColumns} />
|
<Footer
|
||||||
|
url={prevUrl}
|
||||||
|
datasetObject={dataset}
|
||||||
|
hasColumns={hasColumns}
|
||||||
|
datasets={datasetNames}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -32,6 +32,7 @@ export interface DatasetObject {
|
|||||||
schema?: string | null;
|
schema?: string | null;
|
||||||
dataset_name: string;
|
dataset_name: string;
|
||||||
table_name?: string | null;
|
table_name?: string | null;
|
||||||
|
explore_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatasetReducerPayloadType {
|
export interface DatasetReducerPayloadType {
|
||||||
|
@ -82,6 +82,7 @@ export const StyledLayoutLeftPanel = styled.div`
|
|||||||
|
|
||||||
export const StyledLayoutDatasetPanel = styled.div`
|
export const StyledLayoutDatasetPanel = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLayoutRightPanel = styled.div`
|
export const StyledLayoutRightPanel = styled.div`
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { SupersetClient, logging } from '@superset-ui/core';
|
||||||
|
|
||||||
type BaseQueryObject = {
|
type BaseQueryObject = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -73,3 +74,12 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
|||||||
disableNext,
|
disableNext,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UseGetDatasetsList = (queryParams: string | undefined) =>
|
||||||
|
SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
||||||
|
})
|
||||||
|
.then(({ json }) => json)
|
||||||
|
.catch(error =>
|
||||||
|
logging.error('There was an error fetching dataset', error),
|
||||||
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user