diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Dashboards.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Dashboards.spec.ts index b1133b14404..cd53074dea1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Dashboards.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Dashboards.spec.ts @@ -11,17 +11,22 @@ * limitations under the License. */ 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 { performAdminLogin } from '../../utils/admin'; -import { redirectToHomePage } from '../../utils/common'; +import { redirectToHomePage, toastNotification } from '../../utils/common'; import { assignTagToChildren, generateEntityChildren, removeTagsFromChildren, + restoreEntity, } from '../../utils/entity'; import { test } from '../fixtures/pages'; const dashboardEntity = new DashboardServiceClass(); +const dashboard = new DashboardClass(); test.describe('Dashboards', () => { 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('expand / collapse should not appear after updating nested fields for dashboardDataModels', async ({ page, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx index d88a10e5c61..ce46b974f0c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DashboardChartTable/DashboardChartTable.tsx @@ -11,23 +11,27 @@ * limitations under the License. */ +import { Switch, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; 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 { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; +import { INITIAL_CHART_FILTERS } from '../../../constants/constants'; import { DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS, TABLE_COLUMNS_KEYS, } from '../../../constants/TableKeys.constants'; import { usePermissionProvider } from '../../../context/PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../../../context/PermissionProvider/PermissionProvider.interface'; +import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum'; import { EntityType } from '../../../enums/entity.enum'; import { TagLabel, TagSource } from '../../../generated/entity/data/chart'; import { Dashboard } from '../../../generated/entity/data/dashboard'; +import { useTableFilters } from '../../../hooks/useTableFilters'; import { ChartType } from '../../../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { updateChart } from '../../../rest/chartAPI'; import { fetchCharts } from '../../../utils/DashboardDetailsUtils'; @@ -72,6 +76,9 @@ export const DashboardChartTable = ({ chart: ChartType; index: number; }>(); + const { filters: chartFilters, setFilters } = useTableFilters( + INITIAL_CHART_FILTERS + ); const fetchChartPermissions = useCallback(async (id: string) => { try { @@ -120,7 +127,10 @@ export const DashboardChartTable = ({ const initializeCharts = useCallback(async () => { try { - const res = await fetchCharts(listChartIds); + const res = await fetchCharts( + listChartIds, + chartFilters.showDeletedCharts + ); setCharts(res); } catch (error) { showErrorToast( @@ -130,7 +140,7 @@ export const DashboardChartTable = ({ }) ); } - }, [listChartIds]); + }, [listChartIds, chartFilters.showDeletedCharts]); const handleUpdateChart = (chart: ChartType, index: number) => { setEditChart({ chart, index }); @@ -261,6 +271,13 @@ export const DashboardChartTable = ({ } }; + const handleShowDeletedCharts = useCallback( + (value: boolean) => { + setFilters({ showDeletedCharts: value }); + }, + [setFilters, chartFilters] + ); + const tableColumn: ColumnsType = useMemo( () => [ { @@ -390,11 +407,14 @@ export const DashboardChartTable = ({ } initializeCharts(); - }, [listChartIds, isCustomizationPage]); + }, [listChartIds, isCustomizationPage, initializeCharts]); - if (isEmpty(charts)) { - return ; - } + useEffect(() => { + setFilters({ + showDeletedCharts: + chartFilters.showDeletedCharts ?? dashboardDetails?.deleted, + }); + }, [dashboardDetails?.deleted, chartFilters.showDeletedCharts, setFilters]); return ( <> @@ -404,6 +424,26 @@ export const DashboardChartTable = ({ data-testid="charts-table" dataSource={charts} defaultVisibleColumns={DEFAULT_DASHBOARD_CHART_VISIBLE_COLUMNS} + extraTableFilters={ + + + + {t('label.deleted')} + + + } + locale={{ + emptyText: ( + + ), + }} pagination={false} rowKey="fullyQualifiedName" scroll={{ x: 1200 }} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 1c2ca446391..89df4c6ac3c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -434,6 +434,10 @@ export const INITIAL_TABLE_FILTERS = { 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_CARD = 2; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx index 3362e30cfc1..cb3d4bf8906 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.tsx @@ -27,6 +27,7 @@ import { DetailPageWidgetKeys } from '../enums/CustomizeDetailPage.enum'; import { EntityTabs, EntityType, TabSpecificField } from '../enums/entity.enum'; import { Dashboard } from '../generated/entity/data/dashboard'; import { PageType } from '../generated/system/ui/page'; +import { Include } from '../generated/type/include'; import { WidgetConfig } from '../pages/CustomizablePage/CustomizablePage.interface'; import { ChartType } from '../pages/DashboardDetailsPage/DashboardDetailsPage.component'; import { getChartById } from '../rest/chartAPI'; @@ -36,13 +37,19 @@ import { t } from './i18next/LocalUtil'; // 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 fetchCharts = async (charts: Dashboard['charts']) => { +export const fetchCharts = async ( + charts: Dashboard['charts'], + showDeleted = false +) => { let chartsData: ChartType[] = []; let promiseArr: Array> = []; try { if (charts?.length) { 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);