UI - feedback bug fixes and improvements (#12592)

* fixed loading issue in KPI widget on landing page

* fixed redirection links for the notification box tabs

* Fixed redirection issue after soft and hard deleting data assets

* fixed version page layout issue

* Fixed data model restore issue

* Added loader to summary panel while fetching permissions

* Fixed alerts settings page error placeholder alignment issue

* changed the dashboard and pipeline url display method

* fixed cypress test

* updated the logic to perform the after delete action in DeleteWidgetModal

* changed the logic to update the history object through util functions

* added `experimentalMemoryManagement` to the cypress config

* reverted the cypress config changes

* fixed flaky test

* worked on comments

* removed unnecessary code
This commit is contained in:
Aniket Katkar 2023-07-28 18:20:55 +05:30 committed by GitHub
parent cb9e5d8b6f
commit fe3766e106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 347 additions and 188 deletions

View File

@ -706,10 +706,12 @@ export const deleteSoftDeletedUser = (username) => {
cy.get('[data-testid="search-error-placeholder"]').should('be.visible');
};
export const toastNotification = (msg) => {
export const toastNotification = (msg, closeToast = true) => {
cy.get('.Toastify__toast-body').should('be.visible').contains(msg);
cy.wait(200);
if (closeToast) {
cy.get('.Toastify__close-button').should('be.visible').click();
}
};
export const addCustomPropertiesForEntity = (

View File

@ -66,7 +66,7 @@ describe('Restore entity functionality should work properly', () => {
cy.get('[data-testid="confirm-button"]').click();
verifyResponseStatusCode('@softDeleteTable', 200);
toastNotification('Table deleted successfully!');
toastNotification('Table deleted successfully!', false);
});
it('Check Soft Deleted entity table', () => {

View File

@ -25,16 +25,17 @@ import { TOAST_OPTIONS } from 'constants/Toasts.constants';
import React, { FunctionComponent } from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { I18nextProvider } from 'react-i18next';
import { BrowserRouter as Router } from 'react-router-dom';
import { Router } from 'react-router-dom';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';
import { history } from 'utils/HistoryUtils';
import i18n from 'utils/i18next/LocalUtil';
const App: FunctionComponent = () => {
return (
<div className="main-container">
<div className="content-wrapper" data-testid="content-wrapper">
<Router>
<Router history={history}>
<I18nextProvider i18n={i18n}>
<ErrorBoundary>
<ApplicationConfigProvider>

View File

@ -67,6 +67,7 @@ import {
import { withActivityFeed } from 'components/router/withActivityFeed';
import TableDescription from 'components/TableDescription/TableDescription.component';
import { DisplayType } from 'components/Tag/TagsViewer/TagsViewer.interface';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
const DashboardDetails = ({
charts,
@ -723,6 +724,7 @@ const DashboardDetails = ({
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={dashboardDetails}
entityType={EntityType.DASHBOARD}
permissions={dashboardPermissions}

View File

@ -104,6 +104,7 @@ export const ExtraInfoLink = ({
export const DataAssetsHeader = ({
allowSoftDelete = true,
afterDeleteAction,
dataAsset,
onOwnerUpdate,
onTierUpdate,
@ -403,6 +404,7 @@ export const DataAssetsHeader = ({
/>
</Tooltip>
<ManageButton
afterDeleteAction={afterDeleteAction}
allowSoftDelete={!dataAsset.deleted && allowSoftDelete}
canDelete={permissions.Delete}
deleted={dataAsset.deleted}

View File

@ -75,6 +75,7 @@ export type DataAssetsHeaderProps = {
permissions: OperationPermission;
allowSoftDelete?: boolean;
isRecursiveDelete?: boolean;
afterDeleteAction?: (isSoftDelete?: boolean) => void;
onTierUpdate: (tier?: string) => Promise<void>;
onOwnerUpdate: (owner?: EntityReference) => Promise<void>;
onVersionClick?: () => void;

View File

@ -37,7 +37,7 @@ function DataAssetsVersionHeader({
return (
<Row className="p-x-lg" gutter={[8, 12]} justify="space-between">
<Col className="self-center">
<Col className="self-center" span={21}>
<Row gutter={[16, 12]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumbLinks} />
@ -83,6 +83,8 @@ function DataAssetsVersionHeader({
</Col>
</Row>
</Col>
<Col span={3}>
<Row justify="end">
<Col>
<Button
className="w-16 p-0"
@ -93,6 +95,8 @@ function DataAssetsVersionHeader({
</Button>
</Col>
</Row>
</Col>
</Row>
);
}

View File

@ -43,6 +43,7 @@ import KPILatestResultsV1 from './KPILatestResultsV1';
interface Props {
kpiList: Array<Kpi>;
selectedDays: number;
isKPIListLoading: boolean;
}
const EmptyPlaceholder = () => {
@ -78,13 +79,13 @@ const EmptyPlaceholder = () => {
);
};
const KPIChartV1: FC<Props> = ({ kpiList, selectedDays }) => {
const KPIChartV1: FC<Props> = ({ isKPIListLoading, kpiList, selectedDays }) => {
const { t } = useTranslation();
const [kpiResults, setKpiResults] = useState<KpiResult[]>([]);
const [kpiLatestResults, setKpiLatestResults] =
useState<Record<string, UIKpiResult>>();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const fetchKpiResults = useCallback(async () => {
setIsLoading(true);
@ -180,7 +181,7 @@ const KPIChartV1: FC<Props> = ({ kpiList, selectedDays }) => {
className="kpi-widget-card h-full"
data-testid="kpi-card"
id="kpi-charts"
loading={isLoading}>
loading={isKPIListLoading || isLoading}>
<Row>
<Col span={24}>
<Typography.Text className="font-medium">

View File

@ -12,6 +12,7 @@
*/
import { Card, Col, Row, Space, Tabs } from 'antd';
import { AxiosError } from 'axios';
import { useActivityFeedProvider } from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import { ActivityFeedTab } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
import ActivityThreadPanel from 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
@ -37,10 +38,13 @@ import { EntityTags } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { getFeedCounts } from 'utils/CommonUtils';
import { restoreDataModel } from 'rest/dataModelsAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { getFeedCounts, refreshPage } from 'utils/CommonUtils';
import { getEntityName, getEntityThreadLink } from 'utils/EntityUtils';
import { getEntityFieldThreadCounts } from 'utils/FeedUtils';
import { getTagsWithoutTier } from 'utils/TableUtils';
import { showErrorToast, showSuccessToast } from 'utils/ToastUtils';
import { DataModelDetailsProps } from './DataModelDetails.interface';
import ModelTab from './ModelTab/ModelTab.component';
@ -159,6 +163,26 @@ const DataModelDetails = ({
handleUpdateTags(updatedTags);
};
const handleRestoreDataModel = async () => {
try {
await restoreDataModel(dataModelData.id ?? '');
showSuccessToast(
t('message.restore-entities-success', {
entity: t('label.data-model'),
}),
2000
);
refreshPage();
} catch (error) {
showErrorToast(
error as AxiosError,
t('message.restore-entities-error', {
entity: t('label.data-model'),
})
);
}
};
const modelComponent = useMemo(() => {
return (
<Row gutter={[0, 16]} wrap={false}>
@ -335,13 +359,14 @@ const DataModelDetails = ({
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={dataModelData}
entityType={EntityType.DASHBOARD_DATA_MODEL}
permissions={dataModelPermissions}
onDisplayNameUpdate={handleUpdateDisplayName}
onFollowClick={handleFollowDataModel}
onOwnerUpdate={handleUpdateOwner}
onRestoreDataAsset={() => Promise.resolve()}
onRestoreDataAsset={handleRestoreDataModel}
onTierUpdate={handleUpdateTier}
onVersionClick={versionHandler}
/>

View File

@ -13,6 +13,7 @@
import { Drawer, Typography } from 'antd';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
import Loader from 'components/Loader/Loader';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import {
OperationPermission,
@ -51,18 +52,22 @@ export default function EntitySummaryPanel({
const { tab } = useParams<{ tab: string }>();
const { getEntityPermission } = usePermissionProvider();
const [isPermissionLoading, setIsPermissionLoading] =
useState<boolean>(false);
const [entityPermissions, setEntityPermissions] =
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const fetchResourcePermission = async (entityFqn: string) => {
try {
setIsPermissionLoading(true);
const type =
get(entityDetails, 'details.entityType') ?? ResourceEntity.TABLE;
const permissions = await getEntityPermission(type, entityFqn);
setEntityPermissions(permissions);
} catch (error) {
// Error
} finally {
setIsPermissionLoading(false);
}
};
@ -78,6 +83,9 @@ export default function EntitySummaryPanel({
);
const summaryComponent = useMemo(() => {
if (isPermissionLoading) {
return <Loader />;
}
if (!viewPermission) {
return (
<ErrorPlaceHolder
@ -117,7 +125,7 @@ export default function EntitySummaryPanel({
default:
return null;
}
}, [tab, entityDetails, viewPermission]);
}, [tab, entityDetails, viewPermission, isPermissionLoading]);
const entityLink = useMemo(
() =>

View File

@ -22,14 +22,18 @@ import './kpi-widget.less';
const KPIWidget = () => {
const [kpiList, setKpiList] = useState<Array<Kpi>>([]);
const [isKPIListLoading, setIsKPIListLoading] = useState<boolean>(false);
const fetchKpiList = async () => {
try {
setIsKPIListLoading(true);
const response = await getListKPIs({ fields: 'dataInsightChart' });
setKpiList(response.data);
} catch (_err) {
setKpiList([]);
showErrorToast(_err as AxiosError);
} finally {
setIsKPIListLoading(false);
}
};
@ -41,7 +45,11 @@ const KPIWidget = () => {
return (
<div className="kpi-widget-container h-full">
<KPIChartV1 kpiList={kpiList} selectedDays={CHART_WIDGET_DAYS_DURATION} />
<KPIChartV1
isKPIListLoading={isKPIListLoading}
kpiList={kpiList}
selectedDays={CHART_WIDGET_DAYS_DURATION}
/>
</div>
);
};

View File

@ -34,6 +34,7 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { restoreMlmodel } from 'rest/mlModelAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { getEntityName, getEntityThreadLink } from 'utils/EntityUtils';
import AppState from '../../AppState';
import { getMlModelDetailsPath } from '../../constants/constants';
@ -555,6 +556,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={mlModelDetail}
entityType={EntityType.MLMODEL}
permissions={mlModelPermissions}

View File

@ -15,7 +15,6 @@ import { Badge, Button, List, Tabs, Typography } from 'antd';
import { AxiosError } from 'axios';
import { ActivityFeedTabs } from 'components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface';
import { EntityTabs } from 'enums/entity.enum';
import { UserProfileTab } from 'enums/user.enum';
import { isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -62,7 +61,7 @@ const NotificationBox = ({
);
const notificationDropDownList = useMemo(() => {
return notifications.slice(0, 5).map((feed, idx) => {
return notifications.slice(0, 5).map((feed) => {
const mainFeed = {
message: feed.message,
postTs: feed.threadTs,
@ -79,7 +78,7 @@ const NotificationBox = ({
entityFQN={entityFQN as string}
entityType={entityType as string}
feedType={feed.type || ThreadType.Conversation}
key={`${mainFeed.from} ${idx}`}
key={`${mainFeed.from} ${mainFeed.id}`}
task={feed}
timestamp={mainFeed.postTs}
/>
@ -117,11 +116,13 @@ const NotificationBox = ({
getNotificationData(threadType, feedFilter);
setViewAllPath(
`${getUserPath(currentUser?.name as string)}/${(threadType ===
ThreadType.Conversation
? UserProfileTab.ACTIVITY
: threadType
).toLowerCase()}?feedFilter=${feedFilter}`
getUserPath(
currentUser?.name as string,
EntityTabs.ACTIVITY_FEED,
key === NotificationTabsKey.TASK
? ActivityFeedTabs.TASKS
: ActivityFeedTabs.MENTIONS
)
);
if (hasTaskNotification || hasMentionNotification) {
@ -132,7 +133,7 @@ const NotificationBox = ({
}, NOTIFICATION_READ_TIMER);
}
},
[currentUser, hasTaskNotification, hasMentionNotification]
[onTabChange, currentUser, hasTaskNotification, hasMentionNotification]
);
useEffect(() => {
@ -187,7 +188,7 @@ const NotificationBox = ({
size="small"
/>
),
[notifications]
[notifications, notificationDropDownList, viewAllPath]
);
return (

View File

@ -43,6 +43,7 @@ import { useTranslation } from 'react-i18next';
import { Link, useHistory, useParams } from 'react-router-dom';
import { postThread } from 'rest/feedsAPI';
import { restorePipeline } from 'rest/pipelineAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { ReactComponent as ExternalLinkIcon } from '../../assets/svg/external-links.svg';
import {
getPipelineDetailsPath,
@ -746,6 +747,7 @@ const PipelineDetails = ({
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={pipelineDetails}
entityType={EntityType.PIPELINE}
permissions={pipelinePermissions}

View File

@ -36,6 +36,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { restoreTopic } from 'rest/topicsAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { getEntityName, getEntityThreadLink } from 'utils/EntityUtils';
import { EntityField } from '../../constants/Feeds.constants';
import { EntityTabs, EntityType } from '../../enums/entity.enum';
@ -450,6 +451,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={topicDetails}
entityType={EntityType.TOPIC}
permissions={topicPermissions}

View File

@ -24,7 +24,7 @@ export interface DeleteWidgetModalProps {
entityId?: string;
prepareType?: boolean;
isRecursiveDelete?: boolean;
afterDeleteAction?: () => void;
afterDeleteAction?: (isSoftDelete?: boolean) => void;
}
export interface DeleteSectionProps {

View File

@ -22,15 +22,13 @@ import React, {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { deleteEntity } from 'rest/miscAPI';
import { ENTITY_DELETE_STATE } from '../../../constants/entity.constants';
import { EntityType } from '../../../enums/entity.enum';
import {
getEntityDeleteMessage,
Transi18next,
} from '../../../utils/CommonUtils';
import { getTitleCase } from '../../../utils/EntityUtils';
getDeleteMessage,
prepareEntityType,
} from 'utils/DeleteWidgetModalUtils';
import { ENTITY_DELETE_STATE } from '../../../constants/entity.constants';
import { Transi18next } from '../../../utils/CommonUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import { DeleteType, DeleteWidgetModalProps } from './DeleteWidget.interface';
@ -49,7 +47,6 @@ const DeleteWidgetModal = ({
afterDeleteAction,
}: DeleteWidgetModalProps) => {
const { t } = useTranslation();
const history = useHistory();
const [entityDeleteState, setEntityDeleteState] =
useState<typeof ENTITY_DELETE_STATE>(ENTITY_DELETE_STATE);
const [name, setName] = useState<string>('');
@ -58,123 +55,95 @@ const DeleteWidgetModal = ({
);
const [isLoading, setIsLoading] = useState(false);
const prepareDeleteMessage = (softDelete = false) => {
const softDeleteText = t('message.soft-delete-message-for-entity', {
entity: entityName,
});
const hardDeleteText = getEntityDeleteMessage(getTitleCase(entityType), '');
return softDelete ? softDeleteText : hardDeleteText;
};
const DELETE_OPTION = [
const DELETE_OPTION = useMemo(
() => [
{
title: `${t('label.delete')} ${entityType}${entityName}`,
description: `${prepareDeleteMessage(true)} ${softDeleteMessagePostFix}`,
description: `${getDeleteMessage(
entityName,
entityType,
true
)} ${softDeleteMessagePostFix}`,
type: DeleteType.SOFT_DELETE,
isAllowd: allowSoftDelete,
isAllowed: allowSoftDelete,
},
{
title: `${t('label.permanently-delete')} ${entityType}${entityName}`,
description: `${
deleteMessage || prepareDeleteMessage()
deleteMessage || getDeleteMessage(entityName, entityType)
} ${hardDeleteMessagePostFix}`,
type: DeleteType.HARD_DELETE,
isAllowd: true,
isAllowed: true,
},
];
],
[
entityType,
entityName,
softDeleteMessagePostFix,
allowSoftDelete,
deleteMessage,
hardDeleteMessagePostFix,
]
);
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleOnChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
}, []);
const handleOnEntityDelete = (softDelete = true) => {
const handleOnEntityDelete = useCallback((softDelete = true) => {
setEntityDeleteState((prev) => ({ ...prev, state: true, softDelete }));
};
}, []);
const handleOnEntityDeleteCancel = () => {
const handleOnEntityDeleteCancel = useCallback(() => {
setEntityDeleteState(ENTITY_DELETE_STATE);
setName('');
setValue(DeleteType.SOFT_DELETE);
onCancel();
};
}, [onCancel]);
const prepareEntityType = () => {
const services = [
EntityType.DASHBOARD_SERVICE,
EntityType.DATABASE_SERVICE,
EntityType.MESSAGING_SERVICE,
EntityType.PIPELINE_SERVICE,
EntityType.METADATA_SERVICE,
EntityType.STORAGE_SERVICE,
EntityType.MLMODEL_SERVICE,
];
const dataQuality = [EntityType.TEST_SUITE, EntityType.TEST_CASE];
if (services.includes((entityType || '') as EntityType)) {
return `services/${entityType}s`;
} else if (entityType === EntityType.GLOSSARY) {
return `glossaries`;
} else if (entityType === EntityType.POLICY) {
return 'policies';
} else if (entityType === EntityType.KPI) {
return entityType;
} else if (entityType === EntityType.DASHBOARD_DATA_MODEL) {
return `dashboard/datamodels`;
} else if (dataQuality.includes(entityType as EntityType)) {
return `dataQuality/${entityType}s`;
} else if (entityType === EntityType.SUBSCRIPTION) {
return `events/${entityType}s`;
} else {
return `${entityType}s`;
}
};
const handleOnEntityDeleteConfirm = () => {
const handleOnEntityDeleteConfirm = useCallback(async () => {
try {
setIsLoading(false);
setEntityDeleteState((prev) => ({ ...prev, loading: 'waiting' }));
deleteEntity(
prepareType ? prepareEntityType() : entityType,
const response = await deleteEntity(
prepareType ? prepareEntityType(entityType) : entityType,
entityId ?? '',
Boolean(isRecursiveDelete),
!entityDeleteState.softDelete
)
.then((res) => {
if (res.status === 200) {
setTimeout(() => {
handleOnEntityDeleteCancel();
);
if (response.status === 200) {
showSuccessToast(
t('server.entity-deleted-successfully', {
entity: startCase(entityType),
})
);
if (afterDeleteAction) {
afterDeleteAction();
} else {
setTimeout(() => {
history.push('/');
}, 500);
afterDeleteAction(entityDeleteState.softDelete);
}
}, 1000);
} else {
showErrorToast(t('server.unexpected-response'));
}
})
.catch((error: AxiosError) => {
} catch (error) {
showErrorToast(
error,
error as AxiosError,
t('server.delete-entity-error', {
entity: entityName,
})
);
})
.finally(() => {
} finally {
handleOnEntityDeleteCancel();
setIsLoading(false);
});
};
}
}, [
entityType,
entityId,
isRecursiveDelete,
entityDeleteState,
afterDeleteAction,
entityName,
handleOnEntityDeleteCancel,
]);
const isNameMatching = useCallback(() => {
return (
@ -183,11 +152,14 @@ const DeleteWidgetModal = ({
);
}, [name]);
const onChange = (e: RadioChangeEvent) => {
const onChange = useCallback(
(e: RadioChangeEvent) => {
const value = e.target.value;
setValue(value);
handleOnEntityDelete(value === DeleteType.SOFT_DELETE);
};
},
[handleOnEntityDelete]
);
useEffect(() => {
setValue(allowSoftDelete ? DeleteType.SOFT_DELETE : DeleteType.HARD_DELETE);
@ -217,7 +189,12 @@ const DeleteWidgetModal = ({
</Button>
</Space>
);
}, [entityDeleteState, isNameMatching]);
}, [
entityDeleteState,
handleOnEntityDeleteCancel,
handleOnEntityDeleteConfirm,
isNameMatching,
]);
return (
<Modal
@ -233,7 +210,7 @@ const DeleteWidgetModal = ({
<Radio.Group value={value} onChange={onChange}>
{DELETE_OPTION.map(
(option) =>
option.isAllowd && (
option.isAllowed && (
<Radio
data-testid={option.type}
key={option.type}

View File

@ -33,7 +33,7 @@ import './ManageButton.less';
interface Props {
allowSoftDelete?: boolean;
afterDeleteAction?: () => void;
afterDeleteAction?: (isSoftDelete?: boolean) => void;
buttonClassName?: string;
entityName: string;
entityId?: string;

View File

@ -167,29 +167,6 @@ const AlertsPage = () => {
[]
);
if (loading) {
return <Loader />;
}
if (isEmpty(alerts)) {
return (
<ErrorPlaceHolder
permission
doc={ALERTS_DOCS}
heading={t('label.alert')}
type={ERROR_PLACEHOLDER_TYPE.CREATE}
onClick={() =>
history.push(
getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ADD_ALERTS
)
)
}
/>
);
}
return (
<>
<Row gutter={[16, 16]}>
@ -212,6 +189,29 @@ const AlertsPage = () => {
bordered
columns={columns}
dataSource={alerts}
loading={{
spinning: loading,
indicator: <Loader size="small" />,
}}
locale={{
emptyText: !loading && (
<ErrorPlaceHolder
permission
className="p-y-md"
doc={ALERTS_DOCS}
heading={t('label.alert')}
type={ERROR_PLACEHOLDER_TYPE.CREATE}
onClick={() =>
history.push(
getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ADD_ALERTS
)
)
}
/>
),
}}
pagination={false}
rowKey="id"
size="middle"

View File

@ -60,6 +60,7 @@ import {
removeContainerFollower,
restoreContainer,
} from 'rest/storageAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import {
addToRecentViewed,
getCurrentUserId,
@ -690,6 +691,7 @@ const ContainerPage = () => {
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={containerData}
entityType={EntityType.CONTAINER}
permissions={containerPermissions}

View File

@ -22,7 +22,7 @@ import { EmptyGraphPlaceholder } from 'components/DataInsightDetail/EmptyGraphPl
import Loader from 'components/Loader/Loader';
import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
import { isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useHistory } from 'react-router-dom';
import { getListKPIs } from 'rest/KpiAPI';
@ -184,6 +184,10 @@ const KPIList = ({ viewKPIPermission }: { viewKPIPermission: boolean }) => {
fetchKpiList();
}, []);
const handleAfterDeleteAction = useCallback(() => {
fetchKpiList();
}, [fetchKpiList]);
const noDataPlaceHolder = useMemo(
() =>
viewKPIPermission ? (
@ -225,7 +229,7 @@ const KPIList = ({ viewKPIPermission }: { viewKPIPermission: boolean }) => {
{selectedKpi && (
<DeleteWidgetModal
afterDeleteAction={fetchKpiList}
afterDeleteAction={handleAfterDeleteAction}
allowSoftDelete={false}
deleteMessage={`Are you sure you want to delete ${getEntityName(
selectedKpi

View File

@ -60,6 +60,7 @@ import {
restoreDatabase,
} from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { default as appState } from '../../AppState';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import {
@ -748,8 +749,8 @@ const DatabaseDetails: FunctionComponent = () => {
<Row gutter={[0, 12]}>
<Col className="p-x-lg" span={24}>
<DataAssetsHeader
allowSoftDelete
isRecursiveDelete
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={database}
entityType={EntityType.DATABASE}
permissions={databasePermission}

View File

@ -55,6 +55,7 @@ import {
} from 'rest/databaseAPI';
import { getFeedCount, postThread } from 'rest/feedsAPI';
import { getTableList, TableListParams } from 'rest/tableAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import { default as appState } from '../../AppState';
import { getDatabaseSchemaDetailsPath } from '../../constants/constants';
import { EntityTabs, EntityType } from '../../enums/entity.enum';
@ -557,8 +558,8 @@ const DatabaseSchemaPage: FunctionComponent = () => {
/>
) : (
<DataAssetsHeader
allowSoftDelete
isRecursiveDelete
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={databaseSchema}
entityType={EntityType.DATABASE_SCHEMA}
permissions={databaseSchemaPermission}

View File

@ -18,7 +18,7 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from 'components/PermissionProvider/PermissionProvider.interface';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getEntityName } from 'utils/EntityUtils';
@ -181,6 +181,10 @@ const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
];
}, []);
const handleAfterDeleteAction = useCallback(() => {
fetchPolicies();
}, [fetchPolicies]);
return (
<>
<Table
@ -195,7 +199,7 @@ const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
/>
{selectedPolicy && deletePolicyPermission && (
<DeleteWidgetModal
afterDeleteAction={fetchPolicies}
afterDeleteAction={handleAfterDeleteAction}
allowSoftDelete={false}
deleteMessage={t('message.are-you-sure-delete-entity', {
entity: getEntityName(selectedPolicy),

View File

@ -18,7 +18,7 @@ import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichText
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from 'components/PermissionProvider/PermissionProvider.interface';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { getEntityName } from 'utils/EntityUtils';
@ -178,6 +178,10 @@ const RolesList: FC<RolesListProps> = ({ roles, fetchRoles }) => {
];
}, []);
const handleAfterDeleteAction = useCallback(() => {
fetchRoles();
}, [fetchRoles]);
return (
<>
<Table
@ -192,7 +196,7 @@ const RolesList: FC<RolesListProps> = ({ roles, fetchRoles }) => {
/>
{selectedRole && (
<DeleteWidgetModal
afterDeleteAction={fetchRoles}
afterDeleteAction={handleAfterDeleteAction}
allowSoftDelete={false}
deleteMessage={t('message.are-you-sure-delete-entity', {
entity: getEntityName(selectedRole),

View File

@ -72,6 +72,7 @@ import {
removeFollower,
restoreTable,
} from 'rest/tableAPI';
import { handleDataAssetAfterDeleteAction } from 'utils/Assets/AssetsUtils';
import {
addToRecentViewed,
getCurrentUserId,
@ -887,6 +888,7 @@ const TableDetailsPageV1 = () => {
{/* Entity Heading */}
<Col className="p-x-lg" data-testid="entity-page-header" span={24}>
<DataAssetsHeader
afterDeleteAction={handleDataAssetAfterDeleteAction}
dataAsset={tableDetails}
entityType={EntityType.TABLE}
permissions={tablePermissions}

View File

@ -16,6 +16,7 @@ import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { EntityHistory } from 'generated/type/entityHistory';
import { EntityReference } from 'generated/type/entityReference';
import { Include } from 'generated/type/include';
import { RestoreRequestType } from 'Models';
import { getURLWithQueryFields } from 'utils/APIUtils';
import APIClient from './index';
@ -117,3 +118,12 @@ export const getDataModelVersion = async (id: string, version: string) => {
return response.data;
};
export const restoreDataModel = async (id: string) => {
const response = await APIClient.put<
RestoreRequestType,
AxiosResponse<DashboardDataModel>
>('/dashboard/datamodels/restore', { id });
return response.data;
};

View File

@ -22,6 +22,7 @@ import { getPipelineByFqn, patchPipelineDetails } from 'rest/pipelineAPI';
import { getContainerByName, patchContainerDetails } from 'rest/storageAPI';
import { getTableDetailsByFQN, patchTableDetails } from 'rest/tableAPI';
import { getTopicByFqn, patchTopicDetails } from 'rest/topicsAPI';
import { history } from 'utils/HistoryUtils';
export const getAPIfromSource = (
source: AssetsUnion
@ -66,3 +67,13 @@ export const getEntityAPIfromSource = (
return getContainerByName;
}
};
export const handleDataAssetAfterDeleteAction = (isSoftDelete?: boolean) => {
if (isSoftDelete) {
setTimeout(() => {
history.go(0);
}, 1000);
} else {
history.push('/');
}
};

View File

@ -76,6 +76,7 @@ import { TagLabel } from '../generated/type/tagLabel';
import { EntityFieldThreadCount } from '../interface/feed.interface';
import { getEntityFeedLink, getTitleCase } from './EntityUtils';
import Fqn from './Fqn';
import { history } from './HistoryUtils';
import { serviceTypeLogo } from './ServiceUtils';
import { TASK_ENTITIES } from './TasksUtils';
import { showErrorToast } from './ToastUtils';
@ -701,7 +702,9 @@ export const getLoadingStatus = (
);
};
export const refreshPage = () => window.location.reload();
export const refreshPage = () => {
history.go(0);
};
// return array of id as strings
export const getEntityIdArray = (entities: EntityReference[]): string[] =>
entities.map((item) => item.id);

View File

@ -46,6 +46,7 @@ import {
getBreadcrumbForEntitiesWithServiceOnly,
getBreadcrumbForTable,
getEntityBreadcrumbs,
getEntityName,
} from './EntityUtils';
import { bytesToSize } from './StringsUtils';
import { getUsagePercentile } from './TableUtils';
@ -92,8 +93,8 @@ export const getDataAssetsHeaderInfo = (
{dashboardDetails.sourceUrl && (
<ExtraInfoLink
href={dashboardDetails.sourceUrl}
label={entityName}
value={dashboardDetails.sourceUrl}
label=""
value={getEntityName(dashboardDetails)}
/>
)}
{dashboardDetails.dashboardType && (
@ -126,7 +127,7 @@ export const getDataAssetsHeaderInfo = (
<ExtraInfoLink
href={pipelineDetails.sourceUrl}
label=""
value={pipelineDetails.sourceUrl}
value={getEntityName(pipelineDetails)}
/>
)}
</>

View File

@ -0,0 +1,62 @@
/*
* Copyright 2023 Collate.
* Licensed 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 { EntityType } from 'enums/entity.enum';
import { t } from 'i18next';
import { getEntityDeleteMessage } from './CommonUtils';
import { getTitleCase } from './EntityUtils';
export const prepareEntityType = (entityType: string) => {
const services = [
EntityType.DASHBOARD_SERVICE,
EntityType.DATABASE_SERVICE,
EntityType.MESSAGING_SERVICE,
EntityType.PIPELINE_SERVICE,
EntityType.METADATA_SERVICE,
EntityType.STORAGE_SERVICE,
EntityType.MLMODEL_SERVICE,
];
const dataQuality = [EntityType.TEST_SUITE, EntityType.TEST_CASE];
if (services.includes((entityType || '') as EntityType)) {
return `services/${entityType}s`;
} else if (entityType === EntityType.GLOSSARY) {
return `glossaries`;
} else if (entityType === EntityType.POLICY) {
return 'policies';
} else if (entityType === EntityType.KPI) {
return entityType;
} else if (entityType === EntityType.DASHBOARD_DATA_MODEL) {
return `dashboard/datamodels`;
} else if (dataQuality.includes(entityType as EntityType)) {
return `dataQuality/${entityType}s`;
} else if (entityType === EntityType.SUBSCRIPTION) {
return `events/${entityType}s`;
} else {
return `${entityType}s`;
}
};
export const getDeleteMessage = (
entityName: string,
entityType: string,
softDelete = false
) => {
const softDeleteText = t('message.soft-delete-message-for-entity', {
entity: entityName,
});
const hardDeleteText = getEntityDeleteMessage(getTitleCase(entityType), '');
return softDelete ? softDeleteText : hardDeleteText;
};

View File

@ -0,0 +1,16 @@
/*
* Copyright 2023 Collate.
* Licensed 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 { createBrowserHistory } from 'history';
export const history = createBrowserHistory();