mirror of https://github.com/apache/superset.git
feat: add tabs to edit dataset page (#22043)
Co-authored-by: AAfghahi <48933336+AAfghahi@users.noreply.github.com> Co-authored-by: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Co-authored-by: lyndsiWilliams <kcatgirl@gmail.com>
This commit is contained in:
parent
4b05a1eddd
commit
c05871eb37
|
@ -26,6 +26,7 @@ jest.mock('react-router-dom', () => ({
|
|||
useHistory: () => ({
|
||||
push: mockHistoryPush,
|
||||
}),
|
||||
useParams: () => ({ datasetId: undefined }),
|
||||
}));
|
||||
|
||||
describe('AddDataset', () => {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* 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 fetchMock from 'fetch-mock';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import EditDataset from './index';
|
||||
|
||||
const DATASET_ENDPOINT = 'glob:*api/v1/dataset/1/related_objects';
|
||||
|
||||
const mockedProps = {
|
||||
id: '1',
|
||||
};
|
||||
|
||||
fetchMock.get(DATASET_ENDPOINT, { charts: { results: [], count: 2 } });
|
||||
|
||||
test('should render edit dataset view with tabs', async () => {
|
||||
render(<EditDataset {...mockedProps} />);
|
||||
|
||||
const columnTab = await screen.findByRole('tab', { name: /columns/i });
|
||||
const metricsTab = screen.getByRole('tab', { name: /metrics/i });
|
||||
const usageTab = screen.getByRole('tab', { name: /usage/i });
|
||||
|
||||
expect(fetchMock.calls(DATASET_ENDPOINT)).toBeTruthy();
|
||||
expect(columnTab).toBeInTheDocument();
|
||||
expect(metricsTab).toBeInTheDocument();
|
||||
expect(usageTab).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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 { styled, t } from '@superset-ui/core';
|
||||
import React from 'react';
|
||||
import { useGetDatasetRelatedCounts } from 'src/views/CRUD/data/hooks';
|
||||
import Badge from 'src/components/Badge';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
${({ theme }) => `
|
||||
margin-top: ${theme.gridUnit * 8.5}px;
|
||||
padding-left: ${theme.gridUnit * 4}px;
|
||||
|
||||
.ant-tabs-top > .ant-tabs-nav::before {
|
||||
width: ${theme.gridUnit * 50}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const TabStyles = styled.div`
|
||||
${({ theme }) => `
|
||||
.ant-badge {
|
||||
width: ${theme.gridUnit * 8}px;
|
||||
margin-left: ${theme.gridUnit * 2.5}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
interface EditPageProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const TRANSLATIONS = {
|
||||
USAGE_TEXT: t('Usage'),
|
||||
COLUMNS_TEXT: t('Columns'),
|
||||
METRICS_TEXT: t('Metrics'),
|
||||
};
|
||||
|
||||
const EditPage = ({ id }: EditPageProps) => {
|
||||
const { usageCount } = useGetDatasetRelatedCounts(id);
|
||||
|
||||
const usageTab = (
|
||||
<TabStyles>
|
||||
<span>{TRANSLATIONS.USAGE_TEXT}</span>
|
||||
{usageCount > 0 && <Badge count={usageCount} />}
|
||||
</TabStyles>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTabs moreIcon={null} fullWidth={false}>
|
||||
<Tabs.TabPane tab={TRANSLATIONS.COLUMNS_TEXT} key="1" />
|
||||
<Tabs.TabPane tab={TRANSLATIONS.METRICS_TEXT} key="2" />
|
||||
<Tabs.TabPane tab={usageTab} key="3" />
|
||||
</StyledTabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPage;
|
|
@ -137,7 +137,7 @@ fetchMock.get(schemasEndpoint, {
|
|||
});
|
||||
|
||||
fetchMock.get(tablesEndpoint, {
|
||||
tableLength: 3,
|
||||
count: 3,
|
||||
result: [
|
||||
{ value: 'Sheet1', type: 'table', extra: null },
|
||||
{ value: 'Sheet2', type: 'table', extra: null },
|
||||
|
|
|
@ -16,17 +16,11 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useReducer,
|
||||
Reducer,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { logging, t } from '@superset-ui/core';
|
||||
import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import React, { useReducer, Reducer, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useDatasetsList } from 'src/views/CRUD/data/hooks';
|
||||
import Header from './Header';
|
||||
import EditPage from './EditDataset';
|
||||
import DatasetPanel from './DatasetPanel';
|
||||
import LeftPanel from './LeftPanel';
|
||||
import Footer from './Footer';
|
||||
|
@ -82,35 +76,19 @@ export default function AddDataset() {
|
|||
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
||||
>(datasetReducer, null);
|
||||
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 [editPageIsVisible, setEditPageIsVisible] = useState(false);
|
||||
|
||||
const getDatasetsList = useCallback(async () => {
|
||||
if (dataset?.schema) {
|
||||
const filters = [
|
||||
{ col: 'database', opr: 'rel_o_m', value: dataset?.db?.id },
|
||||
{ col: 'schema', opr: 'eq', value: encodedSchema },
|
||||
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: true },
|
||||
];
|
||||
await UseGetDatasetsList(filters)
|
||||
.then(results => {
|
||||
setDatasets(results);
|
||||
})
|
||||
.catch(error => {
|
||||
addDangerToast(t('There was an error fetching dataset'));
|
||||
logging.error(t('There was an error fetching dataset'), error);
|
||||
});
|
||||
}
|
||||
}, [dataset?.db?.id, dataset?.schema, encodedSchema]);
|
||||
const { datasets, datasetNames } = useDatasetsList(
|
||||
dataset?.db,
|
||||
dataset?.schema,
|
||||
);
|
||||
|
||||
const { datasetId: id } = useParams<{ datasetId: string }>();
|
||||
useEffect(() => {
|
||||
if (dataset?.schema) {
|
||||
getDatasetsList();
|
||||
if (!Number.isNaN(parseInt(id, 10))) {
|
||||
setEditPageIsVisible(true);
|
||||
}
|
||||
}, [dataset?.schema, getDatasetsList]);
|
||||
}, [id]);
|
||||
|
||||
const HeaderComponent = () => (
|
||||
<Header setDataset={setDataset} title={dataset?.table_name} />
|
||||
|
@ -124,6 +102,8 @@ export default function AddDataset() {
|
|||
/>
|
||||
);
|
||||
|
||||
const EditPageComponent = () => <EditPage id={id} />;
|
||||
|
||||
const DatasetPanelComponent = () => (
|
||||
<DatasetPanel
|
||||
tableName={dataset?.table_name}
|
||||
|
@ -146,8 +126,10 @@ export default function AddDataset() {
|
|||
return (
|
||||
<DatasetLayout
|
||||
header={HeaderComponent()}
|
||||
leftPanel={LeftPanelComponent()}
|
||||
datasetPanel={DatasetPanelComponent()}
|
||||
leftPanel={editPageIsVisible ? null : LeftPanelComponent()}
|
||||
datasetPanel={
|
||||
editPageIsVisible ? EditPageComponent() : DatasetPanelComponent()
|
||||
}
|
||||
footer={FooterComponent()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -50,12 +50,11 @@ export default function DatasetLayout({
|
|||
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
|
||||
{header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
|
||||
<OuterRow>
|
||||
<LeftColumn>
|
||||
{leftPanel && (
|
||||
{leftPanel && (
|
||||
<LeftColumn>
|
||||
<StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
|
||||
)}
|
||||
</LeftColumn>
|
||||
|
||||
</LeftColumn>
|
||||
)}
|
||||
<RightColumn>
|
||||
<PanelRow>
|
||||
{datasetPanel && (
|
||||
|
|
|
@ -16,15 +16,17 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { SupersetClient, logging, t } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||
import rison from 'rison';
|
||||
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
||||
|
||||
type BaseQueryObject = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
||||
queries,
|
||||
fetchData,
|
||||
|
@ -81,35 +83,96 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
|||
/**
|
||||
* Retrieves all pages of dataset results
|
||||
*/
|
||||
export const UseGetDatasetsList = async (filters: object[]) => {
|
||||
let results: DatasetObject[] = [];
|
||||
let page = 0;
|
||||
let count;
|
||||
export const useDatasetsList = (
|
||||
db:
|
||||
| (DatabaseObject & {
|
||||
owners: [number];
|
||||
})
|
||||
| undefined,
|
||||
schema: string | null | undefined,
|
||||
) => {
|
||||
const [datasets, setDatasets] = useState<DatasetObject[]>([]);
|
||||
const encodedSchema = schema ? encodeURIComponent(schema) : undefined;
|
||||
|
||||
// If count is undefined or less than results, we need to
|
||||
// asynchronously retrieve a page of dataset results
|
||||
while (count === undefined || results.length < count) {
|
||||
const queryParams = rison.encode_uri({ filters, page });
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
||||
});
|
||||
const getDatasetsList = useCallback(async (filters: object[]) => {
|
||||
let results: DatasetObject[] = [];
|
||||
let page = 0;
|
||||
let count;
|
||||
|
||||
// Reassign local count to response's count
|
||||
({ count } = response.json);
|
||||
// If count is undefined or less than results, we need to
|
||||
// asynchronously retrieve a page of dataset results
|
||||
while (count === undefined || results.length < count) {
|
||||
const queryParams = rison.encode_uri({ filters, page });
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
||||
});
|
||||
|
||||
const {
|
||||
json: { result },
|
||||
} = response;
|
||||
// Reassign local count to response's count
|
||||
({ count } = response.json);
|
||||
|
||||
results = [...results, ...result];
|
||||
const {
|
||||
json: { result },
|
||||
} = response;
|
||||
|
||||
page += 1;
|
||||
} catch (error) {
|
||||
addDangerToast(t('There was an error fetching dataset'));
|
||||
logging.error(t('There was an error fetching dataset'), error);
|
||||
results = [...results, ...result];
|
||||
|
||||
page += 1;
|
||||
} catch (error) {
|
||||
addDangerToast(t('There was an error fetching dataset'));
|
||||
logging.error(t('There was an error fetching dataset'), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
|
||||
setDatasets(results);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const filters = [
|
||||
{ col: 'database', opr: 'rel_o_m', value: db?.id },
|
||||
{ col: 'schema', opr: 'eq', value: encodedSchema },
|
||||
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: true },
|
||||
];
|
||||
|
||||
if (schema) {
|
||||
getDatasetsList(filters);
|
||||
}
|
||||
}, [db?.id, schema, encodedSchema, getDatasetsList]);
|
||||
|
||||
const datasetNames = datasets?.map(dataset => dataset.table_name);
|
||||
|
||||
return { datasets, datasetNames };
|
||||
};
|
||||
|
||||
export const useGetDatasetRelatedCounts = (id: string) => {
|
||||
const [usageCount, setUsageCount] = useState(0);
|
||||
|
||||
const getDatasetRelatedObjects = useCallback(
|
||||
() =>
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/${id}/related_objects`,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
setUsageCount(json?.charts.count);
|
||||
})
|
||||
.catch(error => {
|
||||
addDangerToast(
|
||||
t(`There was an error fetching dataset's related objects`),
|
||||
);
|
||||
logging.error(error);
|
||||
}),
|
||||
[id],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Todo: this useEffect should be used to call all count methods conncurently
|
||||
// when we populate data for the new tabs. For right separating out this
|
||||
// api call for building the usage page.
|
||||
if (id) {
|
||||
getDatasetRelatedObjects();
|
||||
}
|
||||
}, [id, getDatasetRelatedObjects]);
|
||||
|
||||
return { usageCount };
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue