mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 03:54:23 +00:00
feat(ui): allow toggle to see both deleted and non-deleted chart on dashboard page (#22963)
* add deleted/non-deleted charts functionality * add e2e test * fix test * minor refactor
This commit is contained in:
parent
b1a1cd89a7
commit
ef162cec89
@ -11,17 +11,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
import { BIG_ENTITY_DELETE_TIMEOUT } from '../../constant/delete';
|
||||||
|
import { DashboardClass } from '../../support/entity/DashboardClass';
|
||||||
|
import { EntityTypeEndpoint } from '../../support/entity/Entity.interface';
|
||||||
import { DashboardServiceClass } from '../../support/entity/service/DashboardServiceClass';
|
import { DashboardServiceClass } from '../../support/entity/service/DashboardServiceClass';
|
||||||
import { performAdminLogin } from '../../utils/admin';
|
import { performAdminLogin } from '../../utils/admin';
|
||||||
import { redirectToHomePage } from '../../utils/common';
|
import { redirectToHomePage, toastNotification } from '../../utils/common';
|
||||||
import {
|
import {
|
||||||
assignTagToChildren,
|
assignTagToChildren,
|
||||||
generateEntityChildren,
|
generateEntityChildren,
|
||||||
removeTagsFromChildren,
|
removeTagsFromChildren,
|
||||||
|
restoreEntity,
|
||||||
} from '../../utils/entity';
|
} from '../../utils/entity';
|
||||||
import { test } from '../fixtures/pages';
|
import { test } from '../fixtures/pages';
|
||||||
|
|
||||||
const dashboardEntity = new DashboardServiceClass();
|
const dashboardEntity = new DashboardServiceClass();
|
||||||
|
const dashboard = new DashboardClass();
|
||||||
|
|
||||||
test.describe('Dashboards', () => {
|
test.describe('Dashboards', () => {
|
||||||
test.slow(true);
|
test.slow(true);
|
||||||
@ -84,6 +89,111 @@ test.describe('Dashboards', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Dashboard and Charts deleted toggle', () => {
|
||||||
|
test.slow(true);
|
||||||
|
|
||||||
|
test.beforeAll('Setup pre-requests', async ({ browser }) => {
|
||||||
|
const { apiContext, afterAction } = await performAdminLogin(browser);
|
||||||
|
|
||||||
|
await dashboard.create(apiContext);
|
||||||
|
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll('Clean up', async ({ browser }) => {
|
||||||
|
const { afterAction, apiContext } = await performAdminLogin(browser);
|
||||||
|
|
||||||
|
await dashboard.delete(apiContext);
|
||||||
|
await afterAction();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach('Visit home page', async ({ page }) => {
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able to toggle between deleted and non-deleted charts', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await dashboard.visitEntityPage(page);
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
|
|
||||||
|
await page.click('[data-testid="manage-button"]');
|
||||||
|
await page.click('[data-testid="delete-button"]');
|
||||||
|
|
||||||
|
await page.waitForSelector('[role="dialog"].ant-modal');
|
||||||
|
|
||||||
|
await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible();
|
||||||
|
|
||||||
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
|
const deleteResponse = page.waitForResponse(
|
||||||
|
`/api/v1/${EntityTypeEndpoint.Dashboard}/async/*?hardDelete=false&recursive=true`
|
||||||
|
);
|
||||||
|
await page.click('[data-testid="confirm-button"]');
|
||||||
|
|
||||||
|
await deleteResponse;
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
await toastNotification(
|
||||||
|
page,
|
||||||
|
/(deleted successfully!|Delete operation initiated)/,
|
||||||
|
BIG_ENTITY_DELETE_TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
|
// Retry mechanism for checking deleted badge
|
||||||
|
let deletedBadge = page.locator('[data-testid="deleted-badge"]');
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 5;
|
||||||
|
|
||||||
|
while (attempts < maxAttempts) {
|
||||||
|
const isVisible = await deletedBadge.isVisible();
|
||||||
|
if (isVisible) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForSelector('[data-testid="loader"]', {
|
||||||
|
state: 'detached',
|
||||||
|
});
|
||||||
|
deletedBadge = page.locator('[data-testid="deleted-badge"]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(deletedBadge).toHaveText('Deleted');
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('charts-table').getByTestId('no-data-placeholder')
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('show-deleted').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('charts-table').getByTestId('no-data-placeholder')
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await restoreEntity(page);
|
||||||
|
await page.reload();
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('charts-table').getByTestId('no-data-placeholder')
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByTestId('show-deleted').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByTestId('charts-table').getByTestId('no-data-placeholder')
|
||||||
|
).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('Data Model', () => {
|
test.describe('Data Model', () => {
|
||||||
test('expand / collapse should not appear after updating nested fields for dashboardDataModels', async ({
|
test('expand / collapse should not appear after updating nested fields for dashboardDataModels', async ({
|
||||||
page,
|
page,
|
||||||
|
|||||||
@ -11,23 +11,27 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Switch, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { compare, Operation } from 'fast-json-patch';
|
import { compare, Operation } from 'fast-json-patch';
|
||||||
import { groupBy, isEmpty, isUndefined, uniqBy } from 'lodash';
|
import { groupBy, isUndefined, uniqBy } from 'lodash';
|
||||||
import { EntityTags, TagFilterOptions } from 'Models';
|
import { EntityTags, TagFilterOptions } from 'Models';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { INITIAL_CHART_FILTERS } from '../../../constants/constants';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS,
|
DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS,
|
||||||
TABLE_COLUMNS_KEYS,
|
TABLE_COLUMNS_KEYS,
|
||||||
} from '../../../constants/TableKeys.constants';
|
} from '../../../constants/TableKeys.constants';
|
||||||
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider';
|
||||||
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
|
import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface';
|
||||||
|
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
|
||||||
import { EntityType } from '../../../enums/entity.enum';
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
import { TagLabel, TagSource } from '../../../generated/entity/data/chart';
|
import { TagLabel, TagSource } from '../../../generated/entity/data/chart';
|
||||||
import { Dashboard } from '../../../generated/entity/data/dashboard';
|
import { Dashboard } from '../../../generated/entity/data/dashboard';
|
||||||
|
import { useTableFilters } from '../../../hooks/useTableFilters';
|
||||||
import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
||||||
import { updateChart } from '../../../rest/chartAPI';
|
import { updateChart } from '../../../rest/chartAPI';
|
||||||
import { fetchCharts } from '../../../utils/DashboardDetailsUtils';
|
import { fetchCharts } from '../../../utils/DashboardDetailsUtils';
|
||||||
@ -72,6 +76,9 @@ export const DashboardChartTable = ({
|
|||||||
chart: ChartType;
|
chart: ChartType;
|
||||||
index: number;
|
index: number;
|
||||||
}>();
|
}>();
|
||||||
|
const { filters: chartFilters, setFilters } = useTableFilters(
|
||||||
|
INITIAL_CHART_FILTERS
|
||||||
|
);
|
||||||
|
|
||||||
const fetchChartPermissions = useCallback(async (id: string) => {
|
const fetchChartPermissions = useCallback(async (id: string) => {
|
||||||
try {
|
try {
|
||||||
@ -120,7 +127,10 @@ export const DashboardChartTable = ({
|
|||||||
|
|
||||||
const initializeCharts = useCallback(async () => {
|
const initializeCharts = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchCharts(listChartIds);
|
const res = await fetchCharts(
|
||||||
|
listChartIds,
|
||||||
|
chartFilters.showDeletedCharts
|
||||||
|
);
|
||||||
setCharts(res);
|
setCharts(res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(
|
showErrorToast(
|
||||||
@ -130,7 +140,7 @@ export const DashboardChartTable = ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [listChartIds]);
|
}, [listChartIds, chartFilters.showDeletedCharts]);
|
||||||
|
|
||||||
const handleUpdateChart = (chart: ChartType, index: number) => {
|
const handleUpdateChart = (chart: ChartType, index: number) => {
|
||||||
setEditChart({ chart, index });
|
setEditChart({ chart, index });
|
||||||
@ -261,6 +271,13 @@ export const DashboardChartTable = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowDeletedCharts = useCallback(
|
||||||
|
(value: boolean) => {
|
||||||
|
setFilters({ showDeletedCharts: value });
|
||||||
|
},
|
||||||
|
[setFilters, chartFilters]
|
||||||
|
);
|
||||||
|
|
||||||
const tableColumn: ColumnsType<ChartType> = useMemo(
|
const tableColumn: ColumnsType<ChartType> = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -390,11 +407,14 @@ export const DashboardChartTable = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeCharts();
|
initializeCharts();
|
||||||
}, [listChartIds, isCustomizationPage]);
|
}, [listChartIds, isCustomizationPage, initializeCharts]);
|
||||||
|
|
||||||
if (isEmpty(charts)) {
|
useEffect(() => {
|
||||||
return <ErrorPlaceHolder className="border-default border-radius-sm" />;
|
setFilters({
|
||||||
}
|
showDeletedCharts:
|
||||||
|
chartFilters.showDeletedCharts ?? dashboardDetails?.deleted,
|
||||||
|
});
|
||||||
|
}, [dashboardDetails?.deleted, chartFilters.showDeletedCharts, setFilters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -404,6 +424,26 @@ export const DashboardChartTable = ({
|
|||||||
data-testid="charts-table"
|
data-testid="charts-table"
|
||||||
dataSource={charts}
|
dataSource={charts}
|
||||||
defaultVisibleColumns={DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS}
|
defaultVisibleColumns={DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS}
|
||||||
|
extraTableFilters={
|
||||||
|
<span>
|
||||||
|
<Switch
|
||||||
|
checked={chartFilters.showDeletedCharts}
|
||||||
|
data-testid="show-deleted"
|
||||||
|
onClick={handleShowDeletedCharts}
|
||||||
|
/>
|
||||||
|
<Typography.Text className="m-l-xs">
|
||||||
|
{t('label.deleted')}
|
||||||
|
</Typography.Text>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
locale={{
|
||||||
|
emptyText: (
|
||||||
|
<ErrorPlaceHolder
|
||||||
|
className="border-none mt-0-important"
|
||||||
|
type={ERROR_PLACEHOLDER_TYPE.NO_DATA}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="fullyQualifiedName"
|
rowKey="fullyQualifiedName"
|
||||||
scroll={{ x: 1200 }}
|
scroll={{ x: 1200 }}
|
||||||
|
|||||||
@ -434,6 +434,10 @@ export const INITIAL_TABLE_FILTERS = {
|
|||||||
showDeletedTables: false,
|
showDeletedTables: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const INITIAL_CHART_FILTERS = {
|
||||||
|
showDeletedCharts: false,
|
||||||
|
};
|
||||||
|
|
||||||
export const MAX_VISIBLE_OWNERS_FOR_FEED_TAB = 4;
|
export const MAX_VISIBLE_OWNERS_FOR_FEED_TAB = 4;
|
||||||
export const MAX_VISIBLE_OWNERS_FOR_FEED_CARD = 2;
|
export const MAX_VISIBLE_OWNERS_FOR_FEED_CARD = 2;
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum';
|
|||||||
import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum';
|
import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum';
|
||||||
import { Dashboard } from '../generated/entity/data/dashboard';
|
import { Dashboard } from '../generated/entity/data/dashboard';
|
||||||
import { PageType } from '../generated/system/ui/page';
|
import { PageType } from '../generated/system/ui/page';
|
||||||
|
import { Include } from '../generated/type/include';
|
||||||
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
|
import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface';
|
||||||
import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component';
|
||||||
import { getChartById } from '../rest/chartAPI';
|
import { getChartById } from '../rest/chartAPI';
|
||||||
@ -36,13 +37,19 @@ import { t } from './i18next/LocalUtil';
|
|||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
export const defaultFields = `${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`;
|
export const defaultFields = `${TabSpecificField.DOMAINS},${TabSpecificField.OWNERS}, ${TabSpecificField.FOLLOWERS}, ${TabSpecificField.TAGS}, ${TabSpecificField.CHARTS},${TabSpecificField.VOTES},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`;
|
||||||
|
|
||||||
export const fetchCharts = async (charts: Dashboard['charts']) => {
|
export const fetchCharts = async (
|
||||||
|
charts: Dashboard['charts'],
|
||||||
|
showDeleted = false
|
||||||
|
) => {
|
||||||
let chartsData: ChartType[] = [];
|
let chartsData: ChartType[] = [];
|
||||||
let promiseArr: Array<Promise<ChartType>> = [];
|
let promiseArr: Array<Promise<ChartType>> = [];
|
||||||
try {
|
try {
|
||||||
if (charts?.length) {
|
if (charts?.length) {
|
||||||
promiseArr = charts.map((chart) =>
|
promiseArr = charts.map((chart) =>
|
||||||
getChartById(chart.id, { fields: TabSpecificField.TAGS })
|
getChartById(chart.id, {
|
||||||
|
fields: TabSpecificField.TAGS,
|
||||||
|
include: showDeleted ? Include.Deleted : Include.NonDeleted,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const res = await Promise.allSettled(promiseArr);
|
const res = await Promise.allSettled(promiseArr);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user