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:
Phillip Kelley-Dotson 2023-02-02 17:36:11 -08:00 committed by GitHub
parent 4b05a1eddd
commit c05871eb37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 232 additions and 70 deletions

View File

@ -26,6 +26,7 @@ jest.mock('react-router-dom', () => ({
useHistory: () => ({
push: mockHistoryPush,
}),
useParams: () => ({ datasetId: undefined }),
}));
describe('AddDataset', () => {

View File

@ -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();
});

View File

@ -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;

View File

@ -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 },

View File

@ -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()}
/>
);

View File

@ -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 && (

View File

@ -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 };
};