mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
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: () => ({
|
useHistory: () => ({
|
||||||
push: mockHistoryPush,
|
push: mockHistoryPush,
|
||||||
}),
|
}),
|
||||||
|
useParams: () => ({ datasetId: undefined }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AddDataset', () => {
|
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, {
|
fetchMock.get(tablesEndpoint, {
|
||||||
tableLength: 3,
|
count: 3,
|
||||||
result: [
|
result: [
|
||||||
{ value: 'Sheet1', type: 'table', extra: null },
|
{ value: 'Sheet1', type: 'table', extra: null },
|
||||||
{ value: 'Sheet2', type: 'table', extra: null },
|
{ value: 'Sheet2', type: 'table', extra: null },
|
||||||
|
@ -16,17 +16,11 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, {
|
import React, { useReducer, Reducer, useEffect, useState } from 'react';
|
||||||
useReducer,
|
import { useParams } from 'react-router-dom';
|
||||||
Reducer,
|
import { useDatasetsList } from 'src/views/CRUD/data/hooks';
|
||||||
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 Header from './Header';
|
import Header from './Header';
|
||||||
|
import EditPage from './EditDataset';
|
||||||
import DatasetPanel from './DatasetPanel';
|
import DatasetPanel from './DatasetPanel';
|
||||||
import LeftPanel from './LeftPanel';
|
import LeftPanel from './LeftPanel';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
@ -82,35 +76,19 @@ 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 [editPageIsVisible, setEditPageIsVisible] = useState(false);
|
||||||
const datasetNames = datasets.map(dataset => dataset.table_name);
|
|
||||||
const encodedSchema = dataset?.schema
|
|
||||||
? encodeURIComponent(dataset?.schema)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const getDatasetsList = useCallback(async () => {
|
const { datasets, datasetNames } = useDatasetsList(
|
||||||
if (dataset?.schema) {
|
dataset?.db,
|
||||||
const filters = [
|
dataset?.schema,
|
||||||
{ 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 { datasetId: id } = useParams<{ datasetId: string }>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataset?.schema) {
|
if (!Number.isNaN(parseInt(id, 10))) {
|
||||||
getDatasetsList();
|
setEditPageIsVisible(true);
|
||||||
}
|
}
|
||||||
}, [dataset?.schema, getDatasetsList]);
|
}, [id]);
|
||||||
|
|
||||||
const HeaderComponent = () => (
|
const HeaderComponent = () => (
|
||||||
<Header setDataset={setDataset} title={dataset?.table_name} />
|
<Header setDataset={setDataset} title={dataset?.table_name} />
|
||||||
@ -124,6 +102,8 @@ export default function AddDataset() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const EditPageComponent = () => <EditPage id={id} />;
|
||||||
|
|
||||||
const DatasetPanelComponent = () => (
|
const DatasetPanelComponent = () => (
|
||||||
<DatasetPanel
|
<DatasetPanel
|
||||||
tableName={dataset?.table_name}
|
tableName={dataset?.table_name}
|
||||||
@ -146,8 +126,10 @@ export default function AddDataset() {
|
|||||||
return (
|
return (
|
||||||
<DatasetLayout
|
<DatasetLayout
|
||||||
header={HeaderComponent()}
|
header={HeaderComponent()}
|
||||||
leftPanel={LeftPanelComponent()}
|
leftPanel={editPageIsVisible ? null : LeftPanelComponent()}
|
||||||
datasetPanel={DatasetPanelComponent()}
|
datasetPanel={
|
||||||
|
editPageIsVisible ? EditPageComponent() : DatasetPanelComponent()
|
||||||
|
}
|
||||||
footer={FooterComponent()}
|
footer={FooterComponent()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -50,12 +50,11 @@ export default function DatasetLayout({
|
|||||||
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
|
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
|
||||||
{header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
|
{header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
|
||||||
<OuterRow>
|
<OuterRow>
|
||||||
<LeftColumn>
|
{leftPanel && (
|
||||||
{leftPanel && (
|
<LeftColumn>
|
||||||
<StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
|
<StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
|
||||||
)}
|
</LeftColumn>
|
||||||
</LeftColumn>
|
)}
|
||||||
|
|
||||||
<RightColumn>
|
<RightColumn>
|
||||||
<PanelRow>
|
<PanelRow>
|
||||||
{datasetPanel && (
|
{datasetPanel && (
|
||||||
|
@ -16,15 +16,17 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { SupersetClient, logging, t } from '@superset-ui/core';
|
import { SupersetClient, logging, t } from '@superset-ui/core';
|
||||||
|
import rison from 'rison';
|
||||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||||
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||||
import rison from 'rison';
|
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
||||||
|
|
||||||
type BaseQueryObject = {
|
type BaseQueryObject = {
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
||||||
queries,
|
queries,
|
||||||
fetchData,
|
fetchData,
|
||||||
@ -81,35 +83,96 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
|
|||||||
/**
|
/**
|
||||||
* Retrieves all pages of dataset results
|
* Retrieves all pages of dataset results
|
||||||
*/
|
*/
|
||||||
export const UseGetDatasetsList = async (filters: object[]) => {
|
export const useDatasetsList = (
|
||||||
let results: DatasetObject[] = [];
|
db:
|
||||||
let page = 0;
|
| (DatabaseObject & {
|
||||||
let count;
|
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
|
const getDatasetsList = useCallback(async (filters: object[]) => {
|
||||||
// asynchronously retrieve a page of dataset results
|
let results: DatasetObject[] = [];
|
||||||
while (count === undefined || results.length < count) {
|
let page = 0;
|
||||||
const queryParams = rison.encode_uri({ filters, page });
|
let count;
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const response = await SupersetClient.get({
|
|
||||||
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reassign local count to response's count
|
// If count is undefined or less than results, we need to
|
||||||
({ count } = response.json);
|
// 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 {
|
// Reassign local count to response's count
|
||||||
json: { result },
|
({ count } = response.json);
|
||||||
} = response;
|
|
||||||
|
|
||||||
results = [...results, ...result];
|
const {
|
||||||
|
json: { result },
|
||||||
|
} = response;
|
||||||
|
|
||||||
page += 1;
|
results = [...results, ...result];
|
||||||
} catch (error) {
|
|
||||||
addDangerToast(t('There was an error fetching dataset'));
|
page += 1;
|
||||||
logging.error(t('There was an error fetching dataset'), error);
|
} 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
Block a user