From c05871eb378344e340ed7841e6db13eaa5efcf95 Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Thu, 2 Feb 2023 17:36:11 -0800 Subject: [PATCH] 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 --- .../dataset/AddDataset/AddDataset.test.tsx | 1 + .../EditDataset/EditDataset.test.tsx | 43 +++++++ .../dataset/AddDataset/EditDataset/index.tsx | 74 +++++++++++ .../AddDataset/LeftPanel/LeftPanel.test.tsx | 2 +- .../CRUD/data/dataset/AddDataset/index.tsx | 56 +++------ .../CRUD/data/dataset/DatasetLayout/index.tsx | 9 +- .../src/views/CRUD/data/hooks.ts | 117 ++++++++++++++---- 7 files changed, 232 insertions(+), 70 deletions(-) create mode 100644 superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/EditDataset.test.tsx create mode 100644 superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/index.tsx diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx index 7e5a7de12a..ea595c0f90 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx @@ -26,6 +26,7 @@ jest.mock('react-router-dom', () => ({ useHistory: () => ({ push: mockHistoryPush, }), + useParams: () => ({ datasetId: undefined }), })); describe('AddDataset', () => { diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/EditDataset.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/EditDataset.test.tsx new file mode 100644 index 0000000000..c24496facc --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/EditDataset.test.tsx @@ -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(); + + 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(); +}); diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/index.tsx new file mode 100644 index 0000000000..7abc676fca --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/EditDataset/index.tsx @@ -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 = ( + + {TRANSLATIONS.USAGE_TEXT} + {usageCount > 0 && } + + ); + + return ( + + + + + + ); +}; + +export default EditPage; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/LeftPanel/LeftPanel.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/LeftPanel/LeftPanel.test.tsx index d44a441923..3996dc0fec 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/LeftPanel/LeftPanel.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/LeftPanel/LeftPanel.test.tsx @@ -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 }, diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx index a6d84ff10e..67b108ab36 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx @@ -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 | null, DSReducerActionType> >(datasetReducer, null); const [hasColumns, setHasColumns] = useState(false); - const [datasets, setDatasets] = useState([]); - 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 = () => (
@@ -124,6 +102,8 @@ export default function AddDataset() { /> ); + const EditPageComponent = () => ; + const DatasetPanelComponent = () => ( ); diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx index b5691fe5cb..7702efcb59 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx @@ -50,12 +50,11 @@ export default function DatasetLayout({ {header && {header}} - - {leftPanel && ( + {leftPanel && ( + {leftPanel} - )} - - + + )} {datasetPanel && ( diff --git a/superset-frontend/src/views/CRUD/data/hooks.ts b/superset-frontend/src/views/CRUD/data/hooks.ts index 0c1528f07f..4e7a51de35 100644 --- a/superset-frontend/src/views/CRUD/data/hooks.ts +++ b/superset-frontend/src/views/CRUD/data/hooks.ts @@ -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({ queries, fetchData, @@ -81,35 +83,96 @@ export function useQueryPreviewState({ /** * 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([]); + 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 }; };