mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-07 13:07:22 +00:00
Add alert bar (#19047)
* add alert bar * add mock for PageLayotuV1 * fix sonar issues * update glossary page * add PageLayout in Pages * update test locator * update as per tests * add mock for page layout v1 * update alert styles * remove pagelayout from pipeline details * update layouting for ErrorPlaceholder * update to remove render function * update as per comments * updated local files * update as per comments * update tests * update pages to remove pagelayout v1 * updated locales * update test * fix sonar cloud issue * updated as per comments * add visible state to alert --------- Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
parent
a39ee72b6b
commit
ca3fa6dcea
@ -12,7 +12,12 @@
|
||||
*/
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { GlobalSettingOptions } from '../../constant/settings';
|
||||
import { descriptionBox, redirectToHomePage, uuid } from '../../utils/common';
|
||||
import {
|
||||
descriptionBox,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
uuid,
|
||||
} from '../../utils/common';
|
||||
import { settingClick } from '../../utils/sidebar';
|
||||
|
||||
const apiServiceConfig = {
|
||||
@ -89,10 +94,6 @@ test.describe('API service', () => {
|
||||
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,7 +25,11 @@ import {
|
||||
UPDATED_RULE_NAME,
|
||||
} from '../../constant/permission';
|
||||
import { GlobalSettingOptions } from '../../constant/settings';
|
||||
import { descriptionBox, redirectToHomePage } from '../../utils/common';
|
||||
import {
|
||||
descriptionBox,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
} from '../../utils/common';
|
||||
import { validateFormNameFieldInput } from '../../utils/form';
|
||||
import { settingClick } from '../../utils/sidebar';
|
||||
|
||||
@ -258,7 +262,8 @@ test.describe('Policy page should work properly', () => {
|
||||
await page.locator('[data-testid="delete-rule"]').click();
|
||||
|
||||
// Validate the error message
|
||||
await expect(page.locator('.Toastify__toast-body')).toContainText(
|
||||
await toastNotification(
|
||||
page,
|
||||
ERROR_MESSAGE_VALIDATION.lastRuleCannotBeRemoved
|
||||
);
|
||||
});
|
||||
|
||||
@ -12,7 +12,12 @@
|
||||
*/
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { GlobalSettingOptions } from '../../constant/settings';
|
||||
import { descriptionBox, redirectToHomePage, uuid } from '../../utils/common';
|
||||
import {
|
||||
descriptionBox,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
uuid,
|
||||
} from '../../utils/common';
|
||||
import { removePolicyFromRole } from '../../utils/roles';
|
||||
import { settingClick } from '../../utils/sidebar';
|
||||
|
||||
@ -201,9 +206,11 @@ test('Roles page should work properly', async ({ page }) => {
|
||||
// Removing the last policy and validating the error message
|
||||
await removePolicyFromRole(page, policies.dataConsumerPolicy, roleName);
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toContainText(
|
||||
await toastNotification(
|
||||
page,
|
||||
errorMessageValidation.lastPolicyCannotBeRemoved
|
||||
);
|
||||
|
||||
await expect(page.locator('.ant-table-row')).toContainText(
|
||||
policies.dataConsumerPolicy
|
||||
);
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
createNewPage,
|
||||
descriptionBoxReadOnly,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
} from '../../utils/common';
|
||||
import { addMultiOwner, assignTier } from '../../utils/entity';
|
||||
|
||||
@ -237,11 +238,7 @@ entities.forEach((EntityClass) => {
|
||||
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
|
||||
await page.reload();
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import {
|
||||
createNewPage,
|
||||
descriptionBoxReadOnly,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
} from '../../utils/common';
|
||||
import { addMultiOwner, assignTier } from '../../utils/entity';
|
||||
|
||||
@ -202,11 +203,7 @@ entities.forEach((EntityClass) => {
|
||||
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
|
||||
await page.reload();
|
||||
|
||||
|
||||
@ -116,15 +116,13 @@ export const toastNotification = async (
|
||||
page: Page,
|
||||
message: string | RegExp
|
||||
) => {
|
||||
await expect(
|
||||
page.locator('.Toastify__toast-body[role="alert"]').first()
|
||||
).toHaveText(message);
|
||||
await page.waitForSelector('[data-testid="alert-bar"]', { state: 'visible' });
|
||||
|
||||
await page
|
||||
.locator('.Toastify__toast')
|
||||
.getByLabel('close', { exact: true })
|
||||
.first()
|
||||
.click();
|
||||
await expect(page.getByTestId('alert-bar')).toHaveText(message);
|
||||
|
||||
await expect(page.getByTestId('alert-icon')).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('alert-icon-close')).toBeVisible();
|
||||
};
|
||||
|
||||
export const clickOutside = async (page: Page) => {
|
||||
@ -268,13 +266,9 @@ export const replaceAllSpacialCharWith_ = (text: string) => {
|
||||
// This error toast blocks the buttons at the top
|
||||
// Below logic closes the alert if it's present to avoid flakiness in tests
|
||||
export const closeFirstPopupAlert = async (page: Page) => {
|
||||
const toastLocator = '.Toastify__toast-body[role="alert"]';
|
||||
const toastElement = await page.$(toastLocator);
|
||||
if (toastElement) {
|
||||
await page
|
||||
.locator('.Toastify__toast')
|
||||
.getByLabel('close', { exact: true })
|
||||
.first()
|
||||
.click();
|
||||
const toastElement = page.getByTestId('alert-bar');
|
||||
|
||||
if ((await toastElement.count()) > 0) {
|
||||
await page.getByTestId('alert-icon-close').first().click();
|
||||
}
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
NAME_MAX_LENGTH_VALIDATION_ERROR,
|
||||
NAME_VALIDATION_ERROR,
|
||||
} from '../constant/common';
|
||||
import { toastNotification } from './common';
|
||||
|
||||
type CustomMetricDetails = {
|
||||
page: Page;
|
||||
@ -118,12 +119,11 @@ export const createCustomMetric = async ({
|
||||
await page.click('[data-testid="submit-button"]');
|
||||
await createMetricResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
await toastNotification(
|
||||
page,
|
||||
new RegExp(`${metric.name} created successfully.`)
|
||||
);
|
||||
|
||||
await page.locator('.Toastify__close-button').click();
|
||||
|
||||
// verify the created custom metric
|
||||
await expect(page).toHaveURL(/profiler/);
|
||||
await expect(
|
||||
|
||||
@ -24,7 +24,12 @@ import {
|
||||
} from '../constant/delete';
|
||||
import { ES_RESERVED_CHARACTERS } from '../constant/entity';
|
||||
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
|
||||
import { clickOutside, descriptionBox, redirectToHomePage } from './common';
|
||||
import {
|
||||
clickOutside,
|
||||
descriptionBox,
|
||||
redirectToHomePage,
|
||||
toastNotification,
|
||||
} from './common';
|
||||
|
||||
export const visitEntityPage = async (data: {
|
||||
page: Page;
|
||||
@ -792,7 +797,8 @@ const announcementForm = async (
|
||||
);
|
||||
await page.click('#announcement-submit');
|
||||
await announcementSubmit;
|
||||
await page.click('.Toastify__close-button');
|
||||
await page.click('[data-testid="announcement-close"]');
|
||||
await page.click('[data-testid="alert-icon-close"]');
|
||||
};
|
||||
|
||||
export const createAnnouncement = async (
|
||||
@ -1181,11 +1187,7 @@ export const restoreEntity = async (page: Page) => {
|
||||
await page.click('[data-testid="restore-button"]');
|
||||
await page.click('button:has-text("Restore")');
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/restored successfully/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /restored successfully/);
|
||||
|
||||
const exists = await page
|
||||
.locator('[data-testid="deleted-badge"]')
|
||||
@ -1224,11 +1226,7 @@ export const softDeleteEntity = async (
|
||||
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
|
||||
await page.reload();
|
||||
|
||||
@ -1293,11 +1291,7 @@ export const hardDeleteEntity = async (
|
||||
await page.click('[data-testid="confirm-button"]');
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
};
|
||||
|
||||
export const checkDataAssetWidget = async (page: Page, serviceType: string) => {
|
||||
|
||||
@ -156,11 +156,7 @@ export const softDeleteUserProfilePage = async (
|
||||
|
||||
await deleteResponse;
|
||||
|
||||
await expect(page.locator('.Toastify__toast-body')).toHaveText(
|
||||
/deleted successfully!/
|
||||
);
|
||||
|
||||
await page.click('.Toastify__close-button');
|
||||
await toastNotification(page, /deleted successfully!/);
|
||||
|
||||
await deletedUserChecks(page);
|
||||
};
|
||||
|
||||
@ -16,15 +16,12 @@ import React, { FC, useEffect } from 'react';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.min.css';
|
||||
import AppRouter from './components/AppRouter/AppRouter';
|
||||
import { AuthProvider } from './components/Auth/AuthProviders/AuthProvider';
|
||||
import ErrorBoundary from './components/common/ErrorBoundary/ErrorBoundary';
|
||||
import { EntityExportModalProvider } from './components/Entity/EntityExportModalProvider/EntityExportModalProvider.component';
|
||||
import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider';
|
||||
import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider';
|
||||
import { TOAST_OPTIONS } from './constants/Toasts.constants';
|
||||
import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider';
|
||||
import PermissionProvider from './context/PermissionProvider/PermissionProvider';
|
||||
import TourProvider from './context/TourProvider/TourProvider';
|
||||
@ -100,7 +97,6 @@ const App: FC = () => {
|
||||
</ErrorBoundary>
|
||||
</I18nextProvider>
|
||||
</Router>
|
||||
<ToastContainer {...TOAST_OPTIONS} newestOnTop />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 1.91L12.59 0.5L7 6.09L1.41 0.5L0 1.91L5.59 7.5L0 13.09L1.41 14.5L7 8.91L12.59 14.5L14 13.09L8.41 7.5L14 1.91Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 249 B |
@ -0,0 +1,12 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6713_202785)">
|
||||
<path d="M6.28843 0L0 6.28843V18.7116L6.28843 25H18.7116L25 18.7116V6.28843L18.7116 0L6.28843 0ZM23.5352 18.1048L18.1048 23.5352H6.89517L1.46484 18.1048V6.89517L6.89517 1.46484H18.1048L23.5352 6.89517V18.1048Z" fill="currentColor"/>
|
||||
<path d="M12.5 14.428C11.9606 14.428 11.5234 13.9908 11.5234 13.4514V7.59204C11.5234 7.05269 11.9606 6.61548 12.5 6.61548C13.0394 6.61548 13.4766 7.05269 13.4766 7.59204V13.4514C13.4766 13.9908 13.0394 14.428 12.5 14.428Z" fill="currentColor"/>
|
||||
<path d="M12.5 18.3342C13.0393 18.3342 13.4766 17.897 13.4766 17.3577C13.4766 16.8183 13.0393 16.3811 12.5 16.3811C11.9607 16.3811 11.5234 16.8183 11.5234 17.3577C11.5234 17.897 11.9607 18.3342 12.5 18.3342Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6713_202785">
|
||||
<rect width="25" height="25" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 962 B |
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 Collate.
|
||||
* Copyright 2024 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
|
||||
@ -10,12 +10,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AlertProps } from 'antd';
|
||||
|
||||
import { ToastOptions } from 'react-toastify';
|
||||
|
||||
export const TOAST_OPTIONS: ToastOptions = {
|
||||
autoClose: false,
|
||||
hideProgressBar: true,
|
||||
draggable: false,
|
||||
closeOnClick: false,
|
||||
};
|
||||
export interface AlertBarProps {
|
||||
type: AlertProps['type'];
|
||||
message: string | JSX.Element;
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { ReactComponent as CrossIcon } from '../../assets/svg/ic-cross.svg';
|
||||
import * as ToastUtils from '../../utils/ToastUtils';
|
||||
import AlertBar from './AlertBar';
|
||||
|
||||
const mockResetAlert = jest.fn();
|
||||
|
||||
jest.mock('../../hooks/useAlertStore', () => ({
|
||||
useAlertStore: jest.fn().mockImplementation(() => ({
|
||||
resetAlert: mockResetAlert,
|
||||
animationClass: 'test-animation-class',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/ToastUtils', () => ({
|
||||
getIconAndClassName: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('AlertBar', () => {
|
||||
(ToastUtils.getIconAndClassName as jest.Mock).mockReturnValue({
|
||||
icon: CrossIcon,
|
||||
className: 'test-class',
|
||||
});
|
||||
|
||||
it('should render AlertBar with the correct type and message', () => {
|
||||
const message = 'Test message';
|
||||
const type = 'success';
|
||||
|
||||
render(<AlertBar message={message} type={type} />);
|
||||
|
||||
const alertElement = screen.getByTestId('alert-bar');
|
||||
|
||||
expect(alertElement).toBeInTheDocument();
|
||||
expect(alertElement).toHaveClass(
|
||||
'alert-container test-class test-animation-class'
|
||||
);
|
||||
expect(screen.getByTestId('alert-icon')).toBeInTheDocument();
|
||||
expect(screen.getByText(message)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the CrossIcon as the close button', () => {
|
||||
const message = 'Test message';
|
||||
const type = 'info';
|
||||
|
||||
render(<AlertBar message={message} type={type} />);
|
||||
|
||||
const closeIcon = screen.getByTestId('alert-icon-close');
|
||||
|
||||
expect(closeIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply the correct animation class', () => {
|
||||
const message = 'Test message';
|
||||
const type = 'warning';
|
||||
|
||||
render(<AlertBar message={message} type={type} />);
|
||||
|
||||
const alertElement = screen.getByTestId('alert-bar');
|
||||
|
||||
expect(alertElement).toHaveClass('test-animation-class');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Alert } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ReactComponent as CrossIcon } from '../../assets/svg/ic-cross.svg';
|
||||
import { useAlertStore } from '../../hooks/useAlertStore';
|
||||
import { getIconAndClassName } from '../../utils/ToastUtils';
|
||||
import './alert-bar.style.less';
|
||||
import { AlertBarProps } from './AlertBar.interface';
|
||||
|
||||
const AlertBar = ({ type, message }: AlertBarProps): JSX.Element => {
|
||||
const { resetAlert, animationClass } = useAlertStore();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const { icon: AlertIcon, className } = useMemo(() => {
|
||||
return getIconAndClassName(type);
|
||||
}, [type]);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
closable
|
||||
showIcon
|
||||
afterClose={resetAlert}
|
||||
className={classNames('alert-container', className, animationClass)}
|
||||
closeIcon={
|
||||
<CrossIcon
|
||||
className="alert-close-icon"
|
||||
color="currentColor"
|
||||
data-testid="alert-icon-close"
|
||||
/>
|
||||
}
|
||||
data-testid="alert-bar"
|
||||
description={
|
||||
<>
|
||||
<span
|
||||
className={classNames('alert-message', { expanded })}
|
||||
data-testid="alert-message">
|
||||
{message}
|
||||
</span>
|
||||
{typeof message === 'string' && message.length > 400 && (
|
||||
<button
|
||||
className="alert-toggle-btn"
|
||||
data-testid="alert-toggle-btn"
|
||||
onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? 'Show Less' : 'Show More'}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
icon={AlertIcon && <AlertIcon data-testid="alert-icon" />}
|
||||
type={type}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertBar;
|
||||
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2024 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 (reference) url('../../styles/variables.less');
|
||||
|
||||
@keyframes resize-show-animation {
|
||||
from {
|
||||
height: 0px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
to {
|
||||
height: max-content;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resize-hide-animation {
|
||||
from {
|
||||
height: max-content;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
to {
|
||||
height: 0px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
backdrop-filter: blur(500px);
|
||||
height: 0px;
|
||||
|
||||
.alert-message {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Limits to 2 lines */
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 3em; /* Approx height for 2 lines */
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-message.expanded {
|
||||
-webkit-line-clamp: unset;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.alert-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&.show-alert {
|
||||
animation: resize-show-animation 0.3s ease-in-out forwards;
|
||||
}
|
||||
|
||||
&.hide-alert {
|
||||
animation: resize-hide-animation 0.2s ease-in-out forwards;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background-color: @info-bg-color;
|
||||
color: @info-color;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: @success-bg-color;
|
||||
color: @success-color;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: @warning-bg-color;
|
||||
color: @warning-color;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: @error-bg-color;
|
||||
color: @error-color;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.ant-alert-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.alert-close-icon {
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
@ -24,13 +24,13 @@ import React, {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
msalLoginRequest,
|
||||
parseMSALResponse,
|
||||
} from '../../../utils/AuthProvider.util';
|
||||
import { getPopupSettingLink } from '../../../utils/BrowserUtils';
|
||||
import { Transi18next } from '../../../utils/CommonUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import Loader from '../../common/Loader/Loader';
|
||||
import {
|
||||
AuthenticatorRef,
|
||||
@ -96,7 +96,7 @@ const MsalAuthenticator = forwardRef<AuthenticatorRef, Props>(
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
if (e?.message?.includes('popup_window_error')) {
|
||||
toast.error(
|
||||
showErrorToast(
|
||||
<Transi18next
|
||||
i18nKey="message.popup-block-message"
|
||||
renderElement={
|
||||
|
||||
@ -25,6 +25,7 @@ import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
|
||||
import { TabSpecificField } from '../../enums/entity.enum';
|
||||
import { Domain } from '../../generated/entity/domains/domain';
|
||||
import { Operation } from '../../generated/entity/policies/policy';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { useDomainStore } from '../../hooks/useDomainStore';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import {
|
||||
@ -82,9 +83,9 @@ const DomainPage = () => {
|
||||
];
|
||||
}, [permissions]);
|
||||
|
||||
const handleAddDomainClick = () => {
|
||||
const handleAddDomainClick = useCallback(() => {
|
||||
history.push(ROUTES.ADD_DOMAIN);
|
||||
};
|
||||
}, [history]);
|
||||
|
||||
const handleDomainUpdate = async (updatedData: Domain) => {
|
||||
if (activeDomain) {
|
||||
@ -182,49 +183,55 @@ const DomainPage = () => {
|
||||
|
||||
if (!(viewBasicDomainPermission || viewAllDomainPermission)) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
className="mt-0-important"
|
||||
type={ERROR_PLACEHOLDER_TYPE.PERMISSION}
|
||||
/>
|
||||
<div className="d-flex justify-center items-center full-height">
|
||||
<ErrorPlaceHolder
|
||||
className="mt-0-important"
|
||||
type={ERROR_PLACEHOLDER_TYPE.PERMISSION}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isEmpty(rootDomains)) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
buttonId="add-domain"
|
||||
className="mt-0-important"
|
||||
heading={t('label.domain')}
|
||||
permission={createDomainPermission}
|
||||
type={
|
||||
createDomainPermission
|
||||
? ERROR_PLACEHOLDER_TYPE.CREATE
|
||||
: ERROR_PLACEHOLDER_TYPE.CUSTOM
|
||||
}
|
||||
onClick={handleAddDomainClick}>
|
||||
{t('message.domains-not-configured')}
|
||||
</ErrorPlaceHolder>
|
||||
<div className="d-flex justify-center items-center full-height">
|
||||
<ErrorPlaceHolder
|
||||
buttonId="add-domain"
|
||||
className="mt-0-important"
|
||||
heading={t('label.domain')}
|
||||
permission={createDomainPermission}
|
||||
type={
|
||||
createDomainPermission
|
||||
? ERROR_PLACEHOLDER_TYPE.CREATE
|
||||
: ERROR_PLACEHOLDER_TYPE.CUSTOM
|
||||
}
|
||||
onClick={handleAddDomainClick}>
|
||||
{t('message.domains-not-configured')}
|
||||
</ErrorPlaceHolder>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: <DomainsLeftPanel domains={rootDomains} />,
|
||||
}}
|
||||
pageTitle={t('label.domain')}
|
||||
secondPanel={{
|
||||
children: domainPageRender,
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
<div className="m--t-sm">
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: <DomainsLeftPanel domains={rootDomains} />,
|
||||
}}
|
||||
pageTitle={t('label.domain')}
|
||||
secondPanel={{
|
||||
children: domainPageRender,
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainPage;
|
||||
export default withPageLayout('domain')(DomainPage);
|
||||
|
||||
@ -21,6 +21,8 @@ import React, {
|
||||
ReactNode,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { useAlertStore } from '../../hooks/useAlertStore';
|
||||
import AlertBar from '../AlertBar/AlertBar';
|
||||
import DocumentTitle from '../common/DocumentTitle/DocumentTitle';
|
||||
import './../../styles/layout/page-layout.less';
|
||||
|
||||
@ -60,6 +62,8 @@ const PageLayoutV1: FC<PageLayoutProp> = ({
|
||||
mainContainerClassName = '',
|
||||
pageContainerStyle = {},
|
||||
}: PageLayoutProp) => {
|
||||
const { alert } = useAlertStore();
|
||||
|
||||
const contentWidth = useMemo(() => {
|
||||
if (leftPanel && rightPanel) {
|
||||
return `calc(100% - ${leftPanelWidth + rightPanelWidth}px)`;
|
||||
@ -99,27 +103,40 @@ const PageLayoutV1: FC<PageLayoutProp> = ({
|
||||
{leftPanel}
|
||||
</Col>
|
||||
)}
|
||||
<Col
|
||||
className={classNames(
|
||||
'page-layout-v1-center p-t-sm page-layout-v1-vertical-scroll',
|
||||
{
|
||||
'flex justify-center': center,
|
||||
},
|
||||
mainContainerClassName
|
||||
)}
|
||||
flex={contentWidth}
|
||||
offset={center ? 3 : 0}
|
||||
span={center ? 18 : 24}>
|
||||
{children}
|
||||
</Col>
|
||||
{rightPanel && (
|
||||
<Col span={24}>
|
||||
<Col
|
||||
className="page-layout-rightpanel page-layout-v1-vertical-scroll"
|
||||
flex={rightPanelWidth + 'px'}
|
||||
id="right-panelV1">
|
||||
{rightPanel}
|
||||
className={classNames(
|
||||
`page-layout-v1-center page-layout-v1-vertical-scroll ${
|
||||
!alert && 'p-t-sm'
|
||||
}`,
|
||||
{
|
||||
'flex justify-center': center,
|
||||
},
|
||||
mainContainerClassName
|
||||
)}
|
||||
flex={contentWidth}
|
||||
offset={center ? 3 : 0}
|
||||
span={center ? 18 : 24}>
|
||||
<Row gutter={0}>
|
||||
<Col id={`${alert && 'page-alert'}`} span={24}>
|
||||
{alert && (
|
||||
<AlertBar message={alert.message} type={alert.type} />
|
||||
)}
|
||||
</Col>
|
||||
<Col className={`${alert && 'p-t-sm'}`} span={24}>
|
||||
{children}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{rightPanel && (
|
||||
<Col
|
||||
className="page-layout-rightpanel page-layout-v1-vertical-scroll"
|
||||
flex={rightPanelWidth + 'px'}
|
||||
id="right-panelV1">
|
||||
{rightPanel}
|
||||
</Col>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@ -57,7 +57,7 @@ const AnnouncementDrawer: FC<Props> = ({
|
||||
<Typography.Text className="font-medium break-all">
|
||||
{t('label.announcement-plural')}
|
||||
</Typography.Text>
|
||||
<CloseOutlined onClick={onClose} />
|
||||
<CloseOutlined data-testid="announcement-close" onClick={onClose} />
|
||||
</Space>
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
@ -10,9 +10,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
/**
|
||||
* This schema defines Document. A Generic entity to capture any kind of Json Payload.
|
||||
*/
|
||||
export interface CreateDocument {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Copyright 2025 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
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2025 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 React, { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PageLayoutV1 from '../components/PageLayoutV1/PageLayoutV1';
|
||||
|
||||
export const withPageLayout =
|
||||
<P,>(pageTitleKey: string) =>
|
||||
(Component: FC<P>) => {
|
||||
const WrappedComponent: FC<P> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PageLayoutV1 pageTitle={t(`label.${pageTitleKey}`)}>
|
||||
<Component {...props} />
|
||||
</PageLayoutV1>
|
||||
);
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2025 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 { act } from '@testing-library/react';
|
||||
import { AlertType, useAlertStore } from './useAlertStore';
|
||||
|
||||
describe('useAlertStore', () => {
|
||||
it('should update the alert state when addAlert is called', () => {
|
||||
const { alert, animationClass, addAlert } = useAlertStore.getState();
|
||||
|
||||
expect(alert).toBeUndefined();
|
||||
expect(animationClass).toBe('');
|
||||
|
||||
const testAlert: AlertType = {
|
||||
type: 'error',
|
||||
message: 'Test error message',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
addAlert(testAlert);
|
||||
});
|
||||
|
||||
expect(useAlertStore.getState().alert).toEqual(testAlert);
|
||||
expect(useAlertStore.getState().animationClass).toBe('show-alert');
|
||||
});
|
||||
|
||||
it('should reset the alert state when resetAlert is called', () => {
|
||||
const { resetAlert, addAlert } = useAlertStore.getState();
|
||||
|
||||
const testAlert: AlertType = {
|
||||
type: 'info',
|
||||
message: 'Test info message',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
addAlert(testAlert);
|
||||
});
|
||||
|
||||
expect(useAlertStore.getState().alert).toEqual(testAlert);
|
||||
|
||||
act(() => {
|
||||
resetAlert();
|
||||
});
|
||||
|
||||
expect(useAlertStore.getState().alert).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2024 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 { AlertProps } from 'antd';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type AlertType = {
|
||||
type: AlertProps['type'];
|
||||
message: string | JSX.Element;
|
||||
};
|
||||
|
||||
interface AlertStore {
|
||||
alert: AlertType | undefined;
|
||||
animationClass: string;
|
||||
addAlert: (alert: AlertType, timer?: number) => void;
|
||||
resetAlert: VoidFunction;
|
||||
}
|
||||
|
||||
export const useAlertStore = create<AlertStore>()((set) => ({
|
||||
alert: undefined,
|
||||
animationClass: '',
|
||||
addAlert: (alert: AlertType, timer?: number) => {
|
||||
set({ alert, animationClass: 'show-alert' });
|
||||
|
||||
const autoCloseTimer = timer ?? (alert.type === 'error' ? Infinity : 5000);
|
||||
|
||||
if (autoCloseTimer !== Infinity) {
|
||||
setTimeout(() => {
|
||||
set({ animationClass: 'hide-alert', alert: undefined });
|
||||
}, autoCloseTimer);
|
||||
}
|
||||
},
|
||||
resetAlert: () => {
|
||||
set({ alert: undefined });
|
||||
},
|
||||
}));
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Aggregate",
|
||||
"airflow-config-plural": "Airflow-Konfigurationen",
|
||||
"alert": "Warnung",
|
||||
"alert-detail-plural": "Waarschuwingsdetails",
|
||||
"alert-lowercase": "warnung",
|
||||
"alert-lowercase-plural": "warnungen",
|
||||
"alert-plural": "Warnungen",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Aggregate",
|
||||
"airflow-config-plural": "airflow configs",
|
||||
"alert": "Alert",
|
||||
"alert-detail-plural": "Alert Details",
|
||||
"alert-lowercase": "alert",
|
||||
"alert-lowercase-plural": "alerts",
|
||||
"alert-plural": "Alerts",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Agregar",
|
||||
"airflow-config-plural": "Configuraciones de airflow",
|
||||
"alert": "Alerta",
|
||||
"alert-detail-plural": "Detalles de la alerta",
|
||||
"alert-lowercase": "alerta",
|
||||
"alert-lowercase-plural": "alertas",
|
||||
"alert-plural": "Alertas",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Aggregate",
|
||||
"airflow-config-plural": "Configurations Airflow",
|
||||
"alert": "Alerte",
|
||||
"alert-detail-plural": "Détails de l'alerte",
|
||||
"alert-lowercase": "alerte",
|
||||
"alert-lowercase-plural": "alertes",
|
||||
"alert-plural": "Alertes",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Agrupar",
|
||||
"airflow-config-plural": "configuracións de Airflow",
|
||||
"alert": "Alerta",
|
||||
"alert-detail-plural": "Detalles de la alerta",
|
||||
"alert-lowercase": "alerta",
|
||||
"alert-lowercase-plural": "alertas",
|
||||
"alert-plural": "Alertas",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "כלול",
|
||||
"airflow-config-plural": "תצורות airflow",
|
||||
"alert": "התראה",
|
||||
"alert-detail-plural": "פרטי התראה",
|
||||
"alert-lowercase": "התראה",
|
||||
"alert-lowercase-plural": "התראות",
|
||||
"alert-plural": "התראות",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Aggregate",
|
||||
"airflow-config-plural": "Airflowの設定",
|
||||
"alert": "アラート",
|
||||
"alert-detail-plural": "アラートの詳細",
|
||||
"alert-lowercase": "alert",
|
||||
"alert-lowercase-plural": "alerts",
|
||||
"alert-plural": "アラート",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "एकूण",
|
||||
"airflow-config-plural": "एअरफ्लो संरचना",
|
||||
"alert": "सूचना",
|
||||
"alert-detail-plural": "सतर्कतेचा तपशील",
|
||||
"alert-lowercase": "सूचना",
|
||||
"alert-lowercase-plural": "सूचना",
|
||||
"alert-plural": "सूचना",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Agregaat",
|
||||
"airflow-config-plural": "Airflowconfiguraties",
|
||||
"alert": "Alert",
|
||||
"alert-detail-plural": "Alert Details",
|
||||
"alert-lowercase": "alert",
|
||||
"alert-lowercase-plural": "alerts",
|
||||
"alert-plural": "Alerts",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "تجمیع",
|
||||
"airflow-config-plural": "پیکربندیهای ایرفلو",
|
||||
"alert": "هشدار",
|
||||
"alert-detail-plural": "جزئیات هشدار",
|
||||
"alert-lowercase": "هشدار",
|
||||
"alert-lowercase-plural": "هشدارها",
|
||||
"alert-plural": "هشدارها",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Agregado",
|
||||
"airflow-config-plural": "configs do airflow",
|
||||
"alert": "Alerta",
|
||||
"alert-detail-plural": "Detalhes do Alerta",
|
||||
"alert-lowercase": "alerta",
|
||||
"alert-lowercase-plural": "alertas",
|
||||
"alert-plural": "Alertas",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Agregado",
|
||||
"airflow-config-plural": "configs do airflow",
|
||||
"alert": "Alerta",
|
||||
"alert-detail-plural": "Detalhes do Alerta",
|
||||
"alert-lowercase": "alerta",
|
||||
"alert-lowercase-plural": "alertas",
|
||||
"alert-plural": "Alertas",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "Aggregate",
|
||||
"airflow-config-plural": "конфиги airflow",
|
||||
"alert": "Предупреждение",
|
||||
"alert-detail-plural": "Детали оповещения",
|
||||
"alert-lowercase": "предупреждение",
|
||||
"alert-lowercase-plural": "предупреждения",
|
||||
"alert-plural": "Предупреждения",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "รวม",
|
||||
"airflow-config-plural": "การกำหนดค่าของ airflow",
|
||||
"alert": "การแจ้งเตือน",
|
||||
"alert-detail-plural": "รายละเอียดการแจ้งเตือน",
|
||||
"alert-lowercase": "การแจ้งเตือน",
|
||||
"alert-lowercase-plural": "การแจ้งเตือนหลายอย่าง",
|
||||
"alert-plural": "การแจ้งเตือนหลายอย่าง",
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
"aggregate": "聚合",
|
||||
"airflow-config-plural": "Airflow 配置",
|
||||
"alert": "提醒",
|
||||
"alert-detail-plural": "警报详情",
|
||||
"alert-lowercase": "提醒",
|
||||
"alert-lowercase-plural": "提醒",
|
||||
"alert-plural": "提醒",
|
||||
|
||||
@ -84,6 +84,19 @@ jest.mock('../../utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../components/Alerts/AlertDetails/AlertConfigDetails/AlertConfigDetails',
|
||||
() => jest.fn().mockImplementation(() => <div>AlertConfigDetails</div>)
|
||||
|
||||
@ -46,6 +46,7 @@ import {
|
||||
EventSubscription,
|
||||
ProviderType,
|
||||
} from '../../generated/events/eventSubscription';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import { updateNotificationAlert } from '../../rest/alertsAPI';
|
||||
import {
|
||||
@ -450,7 +451,9 @@ function AlertDetailsPage({
|
||||
minWidth: 700,
|
||||
flex: 0.7,
|
||||
}}
|
||||
pageTitle={t('label.entity-detail-plural', { entity: t('label.alert') })}
|
||||
pageTitle={t('label.entity-detail-plural', {
|
||||
entity: t('label.alert'),
|
||||
})}
|
||||
secondPanel={{
|
||||
children: <></>,
|
||||
minWidth: 0,
|
||||
@ -460,4 +463,6 @@ function AlertDetailsPage({
|
||||
);
|
||||
}
|
||||
|
||||
export default AlertDetailsPage;
|
||||
export default withPageLayout<AlertDetailsPageProps>('alert-detail-plural')(
|
||||
AlertDetailsPage
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
import { Col, Row } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Redirect,
|
||||
Route,
|
||||
@ -30,6 +30,7 @@ import { ResourceEntity } from '../../context/PermissionProvider/PermissionProvi
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
|
||||
import { DataInsightChartType } from '../../generated/dataInsight/dataInsightChartResult';
|
||||
import { Operation } from '../../generated/entity/policies/policy';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { DataInsightTabs } from '../../interface/data-insight.interface';
|
||||
import { SystemChartType } from '../../rest/DataInsightAPI';
|
||||
import { getDataInsightPathWithFqn } from '../../utils/DataInsightUtils';
|
||||
@ -75,16 +76,17 @@ const DataInsightPage = () => {
|
||||
SystemChartType | DataInsightChartType
|
||||
>();
|
||||
|
||||
const handleScrollToChart = (
|
||||
chartType: SystemChartType | DataInsightChartType
|
||||
) => {
|
||||
if (ENTITIES_CHARTS.includes(chartType as SystemChartType)) {
|
||||
history.push(getDataInsightPathWithFqn(DataInsightTabs.DATA_ASSETS));
|
||||
} else {
|
||||
history.push(getDataInsightPathWithFqn(DataInsightTabs.APP_ANALYTICS));
|
||||
}
|
||||
setSelectedChart(chartType);
|
||||
};
|
||||
const handleScrollToChart = useCallback(
|
||||
(chartType: SystemChartType | DataInsightChartType) => {
|
||||
if (ENTITIES_CHARTS.includes(chartType as SystemChartType)) {
|
||||
history.push(getDataInsightPathWithFqn(DataInsightTabs.DATA_ASSETS));
|
||||
} else {
|
||||
history.push(getDataInsightPathWithFqn(DataInsightTabs.APP_ANALYTICS));
|
||||
}
|
||||
setSelectedChart(chartType);
|
||||
},
|
||||
[history]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (selectedChart) {
|
||||
@ -125,52 +127,53 @@ const DataInsightPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: <LeftPanel />,
|
||||
}}
|
||||
pageTitle={t('label.data-insight')}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<DataInsightProvider>
|
||||
<Row
|
||||
className="page-container"
|
||||
data-testid="data-insight-container"
|
||||
gutter={[16, 16]}>
|
||||
{isHeaderVisible && (
|
||||
<div className="m--t-sm">
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: <LeftPanel />,
|
||||
}}
|
||||
pageTitle={t('label.data-insight')}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<DataInsightProvider>
|
||||
<Row
|
||||
className="page-container"
|
||||
data-testid="data-insight-container"
|
||||
gutter={[16, 16]}>
|
||||
{isHeaderVisible && (
|
||||
<Col span={24}>
|
||||
<DataInsightHeader onScrollToChart={handleScrollToChart} />
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<DataInsightHeader onScrollToChart={handleScrollToChart} />
|
||||
<Switch>
|
||||
{dataInsightTabs.map((tab) => (
|
||||
<Route
|
||||
exact
|
||||
component={tab.component}
|
||||
key={tab.key}
|
||||
path={tab.path}
|
||||
/>
|
||||
))}
|
||||
<Route exact path={ROUTES.DATA_INSIGHT}>
|
||||
<Redirect to={getDataInsightPathWithFqn()} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Switch>
|
||||
{dataInsightTabs.map((tab) => (
|
||||
<Route
|
||||
exact
|
||||
component={tab.component}
|
||||
key={tab.key}
|
||||
path={tab.path}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route exact path={ROUTES.DATA_INSIGHT}>
|
||||
<Redirect to={getDataInsightPathWithFqn()} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
</Row>
|
||||
</DataInsightProvider>
|
||||
),
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</DataInsightProvider>
|
||||
),
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataInsightPage;
|
||||
export default withPageLayout('data-insight')(DataInsightPage);
|
||||
|
||||
@ -40,6 +40,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () => {
|
||||
));
|
||||
});
|
||||
|
||||
jest.mock('../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/DataInsightUtils', () => ({
|
||||
getDataInsightPathWithFqn: jest.fn().mockReturnValue('/'),
|
||||
}));
|
||||
|
||||
@ -63,6 +63,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () => {
|
||||
));
|
||||
});
|
||||
|
||||
jest.mock('../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
return {
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
||||
@ -25,6 +25,7 @@ import LeftPanelCard from '../../components/common/LeftPanelCard/LeftPanelCard';
|
||||
import ResizableLeftPanels from '../../components/common/ResizablePanels/ResizableLeftPanels';
|
||||
import TabsLabel from '../../components/common/TabsLabel/TabsLabel.component';
|
||||
import { ROUTES } from '../../constants/constants';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { getDataQualityPagePath } from '../../utils/RouterUtils';
|
||||
import './data-quality-page.less';
|
||||
import DataQualityClassBase from './DataQualityClassBase';
|
||||
@ -68,77 +69,79 @@ const DataQualityPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: (
|
||||
<LeftPanelCard id="data-quality">
|
||||
<Menu
|
||||
className="custom-menu custom-menu-with-description data-quality-page-left-panel-menu"
|
||||
data-testid="tabs"
|
||||
items={menuItems}
|
||||
mode="inline"
|
||||
selectedKeys={[
|
||||
activeTab ?? DataQualityClassBase.getDefaultActiveTab(),
|
||||
]}
|
||||
onClick={handleTabChange}
|
||||
/>
|
||||
</LeftPanelCard>
|
||||
),
|
||||
}}
|
||||
pageTitle="Quality"
|
||||
secondPanel={{
|
||||
children: (
|
||||
<DataQualityProvider>
|
||||
<Row
|
||||
className="page-container"
|
||||
data-testid="data-insight-container"
|
||||
gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Typography.Title
|
||||
className="m-b-md p-x-md"
|
||||
data-testid="page-title"
|
||||
level={5}>
|
||||
{t('label.data-quality')}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
className="text-grey-muted p-x-md"
|
||||
data-testid="page-sub-title">
|
||||
{t('message.page-sub-header-for-data-quality')}
|
||||
</Typography.Paragraph>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Switch>
|
||||
{tabDetailsComponent.map((tab) => (
|
||||
<Route
|
||||
exact
|
||||
component={tab.component}
|
||||
key={tab.key}
|
||||
path={tab.path}
|
||||
/>
|
||||
))}
|
||||
<div className="m--t-sm">
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: (
|
||||
<LeftPanelCard id="data-quality">
|
||||
<Menu
|
||||
className="custom-menu custom-menu-with-description data-quality-page-left-panel-menu"
|
||||
data-testid="tabs"
|
||||
items={menuItems}
|
||||
mode="inline"
|
||||
selectedKeys={[
|
||||
activeTab ?? DataQualityClassBase.getDefaultActiveTab(),
|
||||
]}
|
||||
onClick={handleTabChange}
|
||||
/>
|
||||
</LeftPanelCard>
|
||||
),
|
||||
}}
|
||||
pageTitle="Quality"
|
||||
secondPanel={{
|
||||
children: (
|
||||
<DataQualityProvider>
|
||||
<Row
|
||||
className="page-container"
|
||||
data-testid="data-insight-container"
|
||||
gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Typography.Title
|
||||
className="m-b-md p-x-md"
|
||||
data-testid="page-title"
|
||||
level={5}>
|
||||
{t('label.data-quality')}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
className="text-grey-muted p-x-md"
|
||||
data-testid="page-sub-title">
|
||||
{t('message.page-sub-header-for-data-quality')}
|
||||
</Typography.Paragraph>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Switch>
|
||||
{tabDetailsComponent.map((tab) => (
|
||||
<Route
|
||||
exact
|
||||
component={tab.component}
|
||||
key={tab.key}
|
||||
path={tab.path}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route exact path={ROUTES.DATA_QUALITY}>
|
||||
<Redirect
|
||||
to={getDataQualityPagePath(
|
||||
DataQualityClassBase.getDefaultActiveTab()
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
</Row>
|
||||
</DataQualityProvider>
|
||||
),
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
<Route exact path={ROUTES.DATA_QUALITY}>
|
||||
<Redirect
|
||||
to={getDataQualityPagePath(
|
||||
DataQualityClassBase.getDefaultActiveTab()
|
||||
)}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Col>
|
||||
</Row>
|
||||
</DataQualityProvider>
|
||||
),
|
||||
className: 'content-resizable-panel-container p-t-sm',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataQualityPage;
|
||||
export default withPageLayout('quality')(DataQualityPage);
|
||||
|
||||
@ -46,6 +46,7 @@ import { EntityAction, TabSpecificField } from '../../../enums/entity.enum';
|
||||
import { Glossary } from '../../../generated/entity/data/glossary';
|
||||
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
|
||||
import { Operation } from '../../../generated/entity/policies/policy';
|
||||
import { withPageLayout } from '../../../hoc/withPageLayout';
|
||||
import { usePaging } from '../../../hooks/paging/usePaging';
|
||||
import { useElementInView } from '../../../hooks/useElementInView';
|
||||
import { useFqn } from '../../../hooks/useFqn';
|
||||
@ -146,9 +147,9 @@ const GlossaryPage = () => {
|
||||
[permissions, isGlossaryActive]
|
||||
);
|
||||
|
||||
const handleAddGlossaryClick = () => {
|
||||
const handleAddGlossaryClick = useCallback(() => {
|
||||
history.push(ROUTES.ADD_GLOSSARY);
|
||||
};
|
||||
}, [history]);
|
||||
|
||||
const fetchGlossaryList = useCallback(async () => {
|
||||
try {
|
||||
@ -236,7 +237,7 @@ const GlossaryPage = () => {
|
||||
}
|
||||
}, [paging, isInView, isMoreGlossaryLoading, pageSize]);
|
||||
|
||||
const fetchGlossaryTermDetails = async () => {
|
||||
const fetchGlossaryTermDetails = useCallback(async () => {
|
||||
setIsRightPanelLoading(true);
|
||||
try {
|
||||
const response = await getGlossaryTermByFQN(glossaryFqn, {
|
||||
@ -257,7 +258,7 @@ const GlossaryPage = () => {
|
||||
} finally {
|
||||
setIsRightPanelLoading(false);
|
||||
}
|
||||
};
|
||||
}, [glossaryFqn]);
|
||||
useEffect(() => {
|
||||
setIsRightPanelLoading(true);
|
||||
if (glossaries.length) {
|
||||
@ -277,22 +278,25 @@ const GlossaryPage = () => {
|
||||
}
|
||||
}, [isGlossaryActive, glossaryFqn, glossaries]);
|
||||
|
||||
const updateGlossary = async (updatedData: Glossary) => {
|
||||
const jsonPatch = compare(activeGlossary as Glossary, updatedData);
|
||||
const updateGlossary = useCallback(
|
||||
async (updatedData: Glossary) => {
|
||||
const jsonPatch = compare(activeGlossary as Glossary, updatedData);
|
||||
|
||||
try {
|
||||
const response = await patchGlossaries(activeGlossary?.id, jsonPatch);
|
||||
try {
|
||||
const response = await patchGlossaries(activeGlossary?.id, jsonPatch);
|
||||
|
||||
updateActiveGlossary({ ...updatedData, ...response });
|
||||
updateActiveGlossary({ ...updatedData, ...response });
|
||||
|
||||
if (activeGlossary?.name !== updatedData.name) {
|
||||
history.push(getGlossaryPath(response.fullyQualifiedName));
|
||||
fetchGlossaryList();
|
||||
if (activeGlossary?.name !== updatedData.name) {
|
||||
history.push(getGlossaryPath(response.fullyQualifiedName));
|
||||
fetchGlossaryList();
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
},
|
||||
[activeGlossary, updateActiveGlossary, history, fetchGlossaryList]
|
||||
);
|
||||
|
||||
const updateVote = useCallback(
|
||||
async (data: VotingDataProps) => {
|
||||
@ -318,35 +322,38 @@ const GlossaryPage = () => {
|
||||
[updateActiveGlossary, activeGlossary]
|
||||
);
|
||||
|
||||
const handleGlossaryDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteGlossary(id);
|
||||
showSuccessToast(
|
||||
t('server.entity-deleted-successfully', {
|
||||
entity: t('label.glossary'),
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
// check if the glossary available
|
||||
const updatedGlossaries = glossaries.filter((item) => item.id !== id);
|
||||
setGlossaries(updatedGlossaries);
|
||||
const glossaryPath =
|
||||
updatedGlossaries.length > 0
|
||||
? getGlossaryPath(updatedGlossaries[0].fullyQualifiedName)
|
||||
: getGlossaryPath();
|
||||
const handleGlossaryDelete = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await deleteGlossary(id);
|
||||
showSuccessToast(
|
||||
t('server.entity-deleted-successfully', {
|
||||
entity: t('label.glossary'),
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
// check if the glossary is available
|
||||
const updatedGlossaries = glossaries.filter((item) => item.id !== id);
|
||||
setGlossaries(updatedGlossaries);
|
||||
const glossaryPath =
|
||||
updatedGlossaries.length > 0
|
||||
? getGlossaryPath(updatedGlossaries[0].fullyQualifiedName)
|
||||
: getGlossaryPath();
|
||||
|
||||
history.push(glossaryPath);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.delete-entity-error', {
|
||||
entity: t('label.glossary'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
history.push(glossaryPath);
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.delete-entity-error', {
|
||||
entity: t('label.glossary'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[glossaries, history]
|
||||
);
|
||||
|
||||
const handleGlossaryTermUpdate = useCallback(
|
||||
async (updatedData: GlossaryTerm) => {
|
||||
@ -380,33 +387,38 @@ const GlossaryPage = () => {
|
||||
[activeGlossary]
|
||||
);
|
||||
|
||||
const handleGlossaryTermDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteGlossaryTerm(id);
|
||||
const handleGlossaryTermDelete = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
await deleteGlossaryTerm(id);
|
||||
|
||||
showSuccessToast(
|
||||
t('server.entity-deleted-successfully', {
|
||||
entity: t('label.glossary-term'),
|
||||
})
|
||||
);
|
||||
let fqn;
|
||||
if (glossaryFqn) {
|
||||
const fqnArr = Fqn.split(glossaryFqn);
|
||||
fqnArr.pop();
|
||||
fqn = fqnArr.join(FQN_SEPARATOR_CHAR);
|
||||
showSuccessToast(
|
||||
t('server.entity-deleted-successfully', {
|
||||
entity: t('label.glossary-term'),
|
||||
})
|
||||
);
|
||||
|
||||
let fqn;
|
||||
if (glossaryFqn) {
|
||||
const fqnArr = Fqn.split(glossaryFqn);
|
||||
fqnArr.pop();
|
||||
fqn = fqnArr.join(FQN_SEPARATOR_CHAR);
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
history.push(getGlossaryPath(fqn));
|
||||
fetchGlossaryList();
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
err as AxiosError,
|
||||
t('server.delete-entity-error', {
|
||||
entity: t('label.glossary-term'),
|
||||
})
|
||||
);
|
||||
}
|
||||
setIsLoading(true);
|
||||
history.push(getGlossaryPath(fqn));
|
||||
fetchGlossaryList();
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
err as AxiosError,
|
||||
t('server.delete-entity-error', {
|
||||
entity: t('label.glossary-term'),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
[glossaryFqn, history, fetchGlossaryList]
|
||||
);
|
||||
|
||||
const handleAssetClick = useCallback(
|
||||
(asset?: EntityDetailsObjectInterface) => {
|
||||
@ -420,24 +432,30 @@ const GlossaryPage = () => {
|
||||
}
|
||||
|
||||
if (!(viewBasicGlossaryPermission || viewAllGlossaryPermission)) {
|
||||
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
|
||||
return (
|
||||
<div className="d-flex justify-center items-center">
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (glossaries.length === 0 && !isLoading) {
|
||||
return (
|
||||
<ErrorPlaceHolder
|
||||
buttonId="add-glossary"
|
||||
className="mt-0-important"
|
||||
doc={GLOSSARIES_DOCS}
|
||||
heading={t('label.glossary')}
|
||||
permission={createGlossaryPermission}
|
||||
type={
|
||||
createGlossaryPermission
|
||||
? ERROR_PLACEHOLDER_TYPE.CREATE
|
||||
: ERROR_PLACEHOLDER_TYPE.NO_DATA
|
||||
}
|
||||
onClick={handleAddGlossaryClick}
|
||||
/>
|
||||
<div className="d-flex justify-center items-center full-height">
|
||||
<ErrorPlaceHolder
|
||||
buttonId="add-glossary"
|
||||
className="mt-0-important"
|
||||
doc={GLOSSARIES_DOCS}
|
||||
heading={t('label.glossary')}
|
||||
permission={createGlossaryPermission}
|
||||
type={
|
||||
createGlossaryPermission
|
||||
? ERROR_PLACEHOLDER_TYPE.CREATE
|
||||
: ERROR_PLACEHOLDER_TYPE.NO_DATA
|
||||
}
|
||||
onClick={handleAddGlossaryClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -518,7 +536,7 @@ const GlossaryPage = () => {
|
||||
/>
|
||||
);
|
||||
|
||||
return <>{resizableLayout}</>;
|
||||
return <div className="m--t-sm">{resizableLayout}</div>;
|
||||
};
|
||||
|
||||
export default GlossaryPage;
|
||||
export default withPageLayout('glossary')(GlossaryPage);
|
||||
|
||||
@ -53,6 +53,19 @@ jest.mock('../../../context/PermissionProvider/PermissionProvider', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../../components/Glossary/GlossaryV1.component', () => {
|
||||
return jest.fn().mockImplementation((props) => (
|
||||
<div>
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
LineageSettings,
|
||||
} from '../../generated/configuration/lineageSettings';
|
||||
import { Settings, SettingType } from '../../generated/settings/settings';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||
import {
|
||||
getSettingsByType,
|
||||
@ -76,7 +77,7 @@ const LineageConfigPage = () => {
|
||||
setActiveField(event.target.id);
|
||||
}, []);
|
||||
|
||||
const handleSave = async (values: LineageSettings) => {
|
||||
const handleSave = useCallback(async (values: LineageSettings) => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
|
||||
@ -88,6 +89,7 @@ const LineageConfigPage = () => {
|
||||
lineageLayer: values.lineageLayer,
|
||||
},
|
||||
};
|
||||
|
||||
const { data } = await updateSettingsConfig(configData as Settings);
|
||||
showSuccessToast(
|
||||
t('server.update-entity-success', {
|
||||
@ -105,7 +107,7 @@ const LineageConfigPage = () => {
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSearchConfig();
|
||||
@ -116,129 +118,131 @@ const LineageConfigPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizablePanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
children: (
|
||||
<div
|
||||
className="max-width-md w-9/10 service-form-container"
|
||||
data-testid="add-metric-container">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbs} />
|
||||
</Col>
|
||||
<div className="m--t-sm">
|
||||
<ResizablePanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
children: (
|
||||
<div
|
||||
className="max-width-md w-9/10 service-form-container"
|
||||
data-testid="add-metric-container">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbs} />
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Typography.Title
|
||||
className="m-b-0"
|
||||
data-testid="heading"
|
||||
level={5}>
|
||||
{t('label.lineage')}
|
||||
</Typography.Title>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form
|
||||
form={form}
|
||||
id="lineage-config"
|
||||
initialValues={lineageConfig}
|
||||
layout="vertical"
|
||||
onFinish={handleSave}
|
||||
onFocus={handleFieldFocus}>
|
||||
<Form.Item
|
||||
id="root/upstreamDepth"
|
||||
label={t('label.upstream-depth')}
|
||||
name="upstreamDepth"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('message.upstream-depth-message'),
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="field-upstream"
|
||||
max={5}
|
||||
min={1}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Col span={24}>
|
||||
<Typography.Title
|
||||
className="m-b-0"
|
||||
data-testid="heading"
|
||||
level={5}>
|
||||
{t('label.lineage')}
|
||||
</Typography.Title>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form
|
||||
form={form}
|
||||
id="lineage-config"
|
||||
initialValues={lineageConfig}
|
||||
layout="vertical"
|
||||
onFinish={handleSave}
|
||||
onFocus={handleFieldFocus}>
|
||||
<Form.Item
|
||||
id="root/upstreamDepth"
|
||||
label={t('label.upstream-depth')}
|
||||
name="upstreamDepth"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('message.upstream-depth-message'),
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="field-upstream"
|
||||
max={5}
|
||||
min={1}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="m-t-sm"
|
||||
id="root/downstreamDepth"
|
||||
label={t('label.downstream-depth')}
|
||||
name="downstreamDepth"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('message.downstream-depth-message'),
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="field-downstream"
|
||||
max={5}
|
||||
min={1}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-t-sm"
|
||||
id="root/downstreamDepth"
|
||||
label={t('label.downstream-depth')}
|
||||
name="downstreamDepth"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('message.downstream-depth-message'),
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
data-testid="field-downstream"
|
||||
max={5}
|
||||
min={1}
|
||||
type="number"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
className="m-t-sm"
|
||||
id="root/lineageLayer"
|
||||
label={t('label.lineage-layer')}
|
||||
name="lineageLayer">
|
||||
<Select data-testid="field-lineage-layer">
|
||||
<Select.Option value={LineageLayer.EntityLineage}>
|
||||
{t('label.entity-lineage')}
|
||||
</Select.Option>
|
||||
<Select.Option value={LineageLayer.ColumnLevelLineage}>
|
||||
{t('label.column-level-lineage')}
|
||||
</Select.Option>
|
||||
<Select.Option value={LineageLayer.DataObservability}>
|
||||
{t('label.data-observability')}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Row className="m-b-xl" justify="end">
|
||||
<Col className="d-flex justify-end gap-2" span={24}>
|
||||
<Button
|
||||
data-testid="cancel-button"
|
||||
onClick={() => history.goBack()}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="save-button"
|
||||
form="lineage-config"
|
||||
htmlType="submit"
|
||||
loading={isUpdating}
|
||||
type="primary">
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
minWidth: 700,
|
||||
flex: 0.7,
|
||||
}}
|
||||
pageTitle={t('label.lineage-config')}
|
||||
secondPanel={{
|
||||
className: 'service-doc-panel content-resizable-panel-container',
|
||||
minWidth: 400,
|
||||
flex: 0.3,
|
||||
children: (
|
||||
<ServiceDocPanel
|
||||
activeField={activeField}
|
||||
serviceName="LineageConfiguration"
|
||||
serviceType={OPEN_METADATA}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
className="m-t-sm"
|
||||
id="root/lineageLayer"
|
||||
label={t('label.lineage-layer')}
|
||||
name="lineageLayer">
|
||||
<Select data-testid="field-lineage-layer">
|
||||
<Select.Option value={LineageLayer.EntityLineage}>
|
||||
{t('label.entity-lineage')}
|
||||
</Select.Option>
|
||||
<Select.Option value={LineageLayer.ColumnLevelLineage}>
|
||||
{t('label.column-level-lineage')}
|
||||
</Select.Option>
|
||||
<Select.Option value={LineageLayer.DataObservability}>
|
||||
{t('label.data-observability')}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Row className="m-b-xl" justify="end">
|
||||
<Col className="d-flex justify-end gap-2" span={24}>
|
||||
<Button
|
||||
data-testid="cancel-button"
|
||||
onClick={() => history.goBack()}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="save-button"
|
||||
form="lineage-config"
|
||||
htmlType="submit"
|
||||
loading={isUpdating}
|
||||
type="primary">
|
||||
{t('label.save')}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
),
|
||||
minWidth: 700,
|
||||
flex: 0.7,
|
||||
}}
|
||||
pageTitle={t('label.lineage-config')}
|
||||
secondPanel={{
|
||||
className: 'service-doc-panel content-resizable-panel-container',
|
||||
minWidth: 400,
|
||||
flex: 0.3,
|
||||
children: (
|
||||
<ServiceDocPanel
|
||||
activeField={activeField}
|
||||
serviceName="LineageConfiguration"
|
||||
serviceType={OPEN_METADATA}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LineageConfigPage;
|
||||
export default withPageLayout('lineage-config')(LineageConfigPage);
|
||||
|
||||
@ -14,12 +14,21 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { getAllPersonas } from '../../../rest/PersonaAPI';
|
||||
import { PersonaPage } from './PersonaPage';
|
||||
jest.mock('../../../components/PageLayoutV1/PageLayoutV1', () => {
|
||||
return jest.fn().mockImplementation(({ children }) => <div>{children}</div>);
|
||||
});
|
||||
jest.mock('../../../components/PageHeader/PageHeader.component', () => {
|
||||
return jest.fn().mockImplementation(() => <div>PageHeader.component</div>);
|
||||
});
|
||||
jest.mock('../../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
jest.mock(
|
||||
'../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component',
|
||||
() => {
|
||||
|
||||
@ -23,19 +23,19 @@ import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb
|
||||
import { AddEditPersonaForm } from '../../../components/MyData/Persona/AddEditPersona/AddEditPersona.component';
|
||||
import { PersonaDetailsCard } from '../../../components/MyData/Persona/PersonaDetailsCard/PersonaDetailsCard';
|
||||
import PageHeader from '../../../components/PageHeader/PageHeader.component';
|
||||
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
|
||||
import { GlobalSettingsMenuCategory } from '../../../constants/GlobalSettings.constants';
|
||||
import { PAGE_HEADERS } from '../../../constants/PageHeaders.constant';
|
||||
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
|
||||
import { TabSpecificField } from '../../../enums/entity.enum';
|
||||
import { Persona } from '../../../generated/entity/teams/persona';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
import { withPageLayout } from '../../../hoc/withPageLayout';
|
||||
import { useAuth } from '../../../hooks/authHooks';
|
||||
import { usePaging } from '../../../hooks/paging/usePaging';
|
||||
import { getAllPersonas } from '../../../rest/PersonaAPI';
|
||||
import { getSettingPageEntityBreadCrumb } from '../../../utils/GlobalSettingsUtils';
|
||||
|
||||
export const PersonaPage = () => {
|
||||
const PersonaPageLayout = () => {
|
||||
const { isAdminUser } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -82,9 +82,9 @@ export const PersonaPage = () => {
|
||||
fetchPersonas();
|
||||
}, [pageSize]);
|
||||
|
||||
const handleAddNewPersona = () => {
|
||||
const handleAddNewPersona = useCallback(() => {
|
||||
setAddEditPersona({} as Persona);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const errorPlaceHolder = useMemo(
|
||||
() => (
|
||||
@ -101,28 +101,28 @@ export const PersonaPage = () => {
|
||||
[isAdminUser]
|
||||
);
|
||||
|
||||
const handlePersonalAddEditCancel = () => {
|
||||
const handlePersonalAddEditCancel = useCallback(() => {
|
||||
setAddEditPersona(undefined);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePersonaAddEditSave = () => {
|
||||
const handlePersonaAddEditSave = useCallback(() => {
|
||||
handlePersonalAddEditCancel();
|
||||
fetchPersonas();
|
||||
};
|
||||
}, [fetchPersonas]);
|
||||
|
||||
const handlePersonaPageChange = ({
|
||||
currentPage,
|
||||
cursorType,
|
||||
}: PagingHandlerParams) => {
|
||||
handlePageChange(currentPage);
|
||||
if (cursorType) {
|
||||
fetchPersonas({ [cursorType]: paging[cursorType] });
|
||||
}
|
||||
};
|
||||
const handlePersonaPageChange = useCallback(
|
||||
({ currentPage, cursorType }: PagingHandlerParams) => {
|
||||
handlePageChange(currentPage);
|
||||
if (cursorType) {
|
||||
fetchPersonas({ [cursorType]: paging[cursorType] });
|
||||
}
|
||||
},
|
||||
[handlePageChange, fetchPersonas, paging]
|
||||
);
|
||||
|
||||
if (isEmpty(persona) && !isLoading) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex-center full-height">
|
||||
{errorPlaceHolder}
|
||||
{Boolean(addEditPersona) && (
|
||||
<AddEditPersonaForm
|
||||
@ -131,64 +131,64 @@ export const PersonaPage = () => {
|
||||
onSave={handlePersonaAddEditSave}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayoutV1 pageTitle={t('label.persona-plural')}>
|
||||
<Row className="user-listing page-container p-b-md" gutter={[16, 16]}>
|
||||
<Row className="user-listing page-container p-b-md" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbs} />
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<PageHeader data={PAGE_HEADERS.PERSONAS} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Space align="center" className="w-full justify-end" size={16}>
|
||||
<Button
|
||||
data-testid="add-persona-button"
|
||||
type="primary"
|
||||
onClick={handleAddNewPersona}>
|
||||
{t('label.add-entity', { entity: t('label.persona') })}
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
{isLoading
|
||||
? [1, 2, 3].map((key) => (
|
||||
<Col key={key} span={8}>
|
||||
<Card>
|
||||
<Skeleton active paragraph title />
|
||||
</Card>
|
||||
</Col>
|
||||
))
|
||||
: persona?.map((persona) => (
|
||||
<Col key={persona.id} span={8}>
|
||||
<PersonaDetailsCard persona={persona} />
|
||||
</Col>
|
||||
))}
|
||||
|
||||
{showPagination && (
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbs} />
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<PageHeader data={PAGE_HEADERS.PERSONAS} />
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Space align="center" className="w-full justify-end" size={16}>
|
||||
<Button
|
||||
data-testid="add-persona-button"
|
||||
type="primary"
|
||||
onClick={handleAddNewPersona}>
|
||||
{t('label.add-entity', { entity: t('label.persona') })}
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
{isLoading
|
||||
? [1, 2, 3].map((key) => (
|
||||
<Col key={key} span={8}>
|
||||
<Card>
|
||||
<Skeleton active paragraph title />
|
||||
</Card>
|
||||
</Col>
|
||||
))
|
||||
: persona?.map((persona) => (
|
||||
<Col key={persona.id} span={8}>
|
||||
<PersonaDetailsCard persona={persona} />
|
||||
</Col>
|
||||
))}
|
||||
|
||||
{showPagination && (
|
||||
<Col span={24}>
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
isLoading={isLoading}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={handlePersonaPageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
{Boolean(addEditPersona) && (
|
||||
<AddEditPersonaForm
|
||||
persona={addEditPersona}
|
||||
onCancel={handlePersonalAddEditCancel}
|
||||
onSave={handlePersonaAddEditSave}
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
isLoading={isLoading}
|
||||
pageSize={pageSize}
|
||||
paging={paging}
|
||||
pagingHandler={handlePersonaPageChange}
|
||||
onShowSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</PageLayoutV1>
|
||||
</Col>
|
||||
)}
|
||||
{Boolean(addEditPersona) && (
|
||||
<AddEditPersonaForm
|
||||
persona={addEditPersona}
|
||||
onCancel={handlePersonalAddEditCancel}
|
||||
onSave={handlePersonaAddEditSave}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export const PersonaPage = withPageLayout('persona-plural')(PersonaPageLayout);
|
||||
|
||||
@ -240,6 +240,19 @@ jest.mock('../../components/common/ResizablePanels/ResizableLeftPanels', () =>
|
||||
))
|
||||
);
|
||||
|
||||
jest.mock('../../hoc/withPageLayout', () => ({
|
||||
withPageLayout: jest.fn().mockImplementation(
|
||||
() =>
|
||||
(Component: React.FC) =>
|
||||
(
|
||||
props: JSX.IntrinsicAttributes & {
|
||||
children?: React.ReactNode | undefined;
|
||||
}
|
||||
) =>
|
||||
<Component {...props} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'../../components/common/RichTextEditor/RichTextEditorPreviewerV1',
|
||||
() => {
|
||||
|
||||
@ -50,6 +50,7 @@ import {
|
||||
import { Classification } from '../../generated/entity/classification/classification';
|
||||
import { Tag } from '../../generated/entity/classification/tag';
|
||||
import { Operation } from '../../generated/entity/policies/accessControl/rule';
|
||||
import { withPageLayout } from '../../hoc/withPageLayout';
|
||||
import { useFqn } from '../../hooks/useFqn';
|
||||
import {
|
||||
createClassification,
|
||||
@ -237,11 +238,11 @@ const TagsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
const handleCancel = useCallback(() => {
|
||||
setEditTag(undefined);
|
||||
setIsAddingTag(false);
|
||||
setIsAddingClassification(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAfterDeleteAction = useCallback(() => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
@ -307,72 +308,75 @@ const TagsPage = () => {
|
||||
/**
|
||||
* It redirects to respective function call based on tag/Classification
|
||||
*/
|
||||
const handleConfirmClick = async () => {
|
||||
const handleConfirmClick = useCallback(async () => {
|
||||
if (deleteTags.data?.id) {
|
||||
await handleDeleteTag(deleteTags.data.id);
|
||||
}
|
||||
};
|
||||
}, [deleteTags.data?.id, handleDeleteTag]);
|
||||
|
||||
const handleUpdateClassification = async (
|
||||
updatedClassification: Classification
|
||||
) => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
setIsUpdateLoading(true);
|
||||
const handleUpdateClassification = useCallback(
|
||||
async (updatedClassification: Classification) => {
|
||||
if (!isUndefined(currentClassification)) {
|
||||
setIsUpdateLoading(true);
|
||||
|
||||
const patchData = compare(currentClassification, updatedClassification);
|
||||
try {
|
||||
const response = await patchClassification(
|
||||
currentClassification?.id ?? '',
|
||||
patchData
|
||||
);
|
||||
setClassifications((prev) =>
|
||||
prev.map((item) => {
|
||||
if (
|
||||
item.fullyQualifiedName ===
|
||||
currentClassification.fullyQualifiedName
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
...response,
|
||||
};
|
||||
}
|
||||
const patchData = compare(currentClassification, updatedClassification);
|
||||
try {
|
||||
const response = await patchClassification(
|
||||
currentClassification?.id ?? '',
|
||||
patchData
|
||||
);
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
setCurrentClassification((prev) => ({ ...prev, ...response }));
|
||||
if (
|
||||
currentClassification?.fullyQualifiedName !==
|
||||
updatedClassification.fullyQualifiedName ||
|
||||
currentClassification?.name !== updatedClassification.name
|
||||
) {
|
||||
history.push(getTagPath(response.fullyQualifiedName));
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT
|
||||
) {
|
||||
showErrorToast(
|
||||
t('server.entity-already-exist', {
|
||||
entity: t('label.classification'),
|
||||
entityPlural: t('label.classification-lowercase-plural'),
|
||||
name: updatedClassification.name,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.entity-updating-error', {
|
||||
entity: t('label.classification-lowercase'),
|
||||
setClassifications((prev) =>
|
||||
prev.map((item) => {
|
||||
if (
|
||||
item.fullyQualifiedName ===
|
||||
currentClassification.fullyQualifiedName
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
...response,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
setCurrentClassification((prev) => ({ ...prev, ...response }));
|
||||
|
||||
if (
|
||||
currentClassification?.fullyQualifiedName !==
|
||||
updatedClassification.fullyQualifiedName ||
|
||||
currentClassification?.name !== updatedClassification.name
|
||||
) {
|
||||
history.push(getTagPath(response.fullyQualifiedName));
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT
|
||||
) {
|
||||
showErrorToast(
|
||||
t('server.entity-already-exist', {
|
||||
entity: t('label.classification'),
|
||||
entityPlural: t('label.classification-lowercase-plural'),
|
||||
name: updatedClassification.name,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
t('server.entity-updating-error', {
|
||||
entity: t('label.classification-lowercase'),
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsEditClassification(false);
|
||||
setIsUpdateLoading(false);
|
||||
}
|
||||
} finally {
|
||||
setIsEditClassification(false);
|
||||
setIsUpdateLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[currentClassification, history]
|
||||
);
|
||||
|
||||
const handleCreatePrimaryTag = async (data: CreateTag) => {
|
||||
try {
|
||||
@ -454,37 +458,40 @@ const TagsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleActionDeleteTag = (record: Tag) => {
|
||||
if (currentClassification) {
|
||||
setDeleteTags({
|
||||
data: {
|
||||
id: record.id as string,
|
||||
name: record.name,
|
||||
categoryName: currentClassification?.fullyQualifiedName,
|
||||
isCategory: false,
|
||||
status: 'waiting',
|
||||
},
|
||||
state: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleActionDeleteTag = useCallback(
|
||||
(record: Tag) => {
|
||||
if (currentClassification) {
|
||||
setDeleteTags({
|
||||
data: {
|
||||
id: record.id as string,
|
||||
name: record.name,
|
||||
categoryName: currentClassification?.fullyQualifiedName,
|
||||
isCategory: false,
|
||||
status: 'waiting',
|
||||
},
|
||||
state: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[currentClassification]
|
||||
);
|
||||
|
||||
const handleEditTagClick = (selectedTag: Tag) => {
|
||||
const handleEditTagClick = useCallback((selectedTag: Tag) => {
|
||||
setIsAddingTag(true);
|
||||
setEditTag(selectedTag);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleAddNewTagClick = () => {
|
||||
const handleAddNewTagClick = useCallback(() => {
|
||||
setIsAddingTag(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEditDescriptionClick = () => {
|
||||
const handleEditDescriptionClick = useCallback(() => {
|
||||
setIsEditClassification(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleCancelEditDescription = () => {
|
||||
const handleCancelEditDescription = useCallback(() => {
|
||||
setIsEditClassification(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentClassification) {
|
||||
@ -516,21 +523,26 @@ const TagsPage = () => {
|
||||
history.push(getTagPath(category.fullyQualifiedName));
|
||||
};
|
||||
|
||||
const handleAddTagSubmit = async (data: SubmitProps) => {
|
||||
const updatedData = omit(data, 'color', 'iconURL');
|
||||
const style = {
|
||||
color: data.color,
|
||||
iconURL: data.iconURL,
|
||||
};
|
||||
if (editTag) {
|
||||
await handleUpdatePrimaryTag({ ...editTag, ...updatedData, style });
|
||||
} else {
|
||||
await handleCreatePrimaryTag({ ...updatedData, style });
|
||||
}
|
||||
};
|
||||
const handleAddTagSubmit = useCallback(
|
||||
async (data: SubmitProps) => {
|
||||
const updatedData = omit(data, 'color', 'iconURL');
|
||||
const style = {
|
||||
color: data.color,
|
||||
iconURL: data.iconURL,
|
||||
};
|
||||
|
||||
const handleCancelClassificationDelete = () =>
|
||||
if (editTag) {
|
||||
await handleUpdatePrimaryTag({ ...editTag, ...updatedData, style });
|
||||
} else {
|
||||
await handleCreatePrimaryTag({ ...updatedData, style });
|
||||
}
|
||||
},
|
||||
[editTag, handleUpdatePrimaryTag, handleCreatePrimaryTag]
|
||||
);
|
||||
|
||||
const handleCancelClassificationDelete = useCallback(() => {
|
||||
setDeleteTags({ data: undefined, state: false });
|
||||
}, []);
|
||||
|
||||
const leftPanelLayout = useMemo(
|
||||
() => (
|
||||
@ -701,87 +713,92 @@ const TagsPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: leftPanelLayout,
|
||||
}}
|
||||
pageTitle={t('label.tag-plural')}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<>
|
||||
{isUpdateLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<ClassificationDetails
|
||||
classificationPermissions={classificationPermissions}
|
||||
currentClassification={currentClassification}
|
||||
deleteTags={deleteTags}
|
||||
disableEditButton={disableEditButton}
|
||||
handleActionDeleteTag={handleActionDeleteTag}
|
||||
handleAddNewTagClick={handleAddNewTagClick}
|
||||
handleAfterDeleteAction={handleAfterDeleteAction}
|
||||
handleCancelEditDescription={handleCancelEditDescription}
|
||||
handleEditDescriptionClick={handleEditDescriptionClick}
|
||||
handleEditTagClick={handleEditTagClick}
|
||||
handleUpdateClassification={handleUpdateClassification}
|
||||
isAddingTag={isAddingTag}
|
||||
isEditClassification={isEditClassification}
|
||||
ref={classificationDetailsRef}
|
||||
/>
|
||||
)}
|
||||
<div className="m--t-sm">
|
||||
<ResizableLeftPanels
|
||||
className="content-height-with-resizable-panel"
|
||||
firstPanel={{
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 280,
|
||||
flex: 0.13,
|
||||
children: leftPanelLayout,
|
||||
}}
|
||||
pageTitle={t('label.tag-plural')}
|
||||
secondPanel={{
|
||||
children: (
|
||||
<>
|
||||
{isUpdateLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<ClassificationDetails
|
||||
classificationPermissions={classificationPermissions}
|
||||
currentClassification={currentClassification}
|
||||
deleteTags={deleteTags}
|
||||
disableEditButton={disableEditButton}
|
||||
handleActionDeleteTag={handleActionDeleteTag}
|
||||
handleAddNewTagClick={handleAddNewTagClick}
|
||||
handleAfterDeleteAction={handleAfterDeleteAction}
|
||||
handleCancelEditDescription={handleCancelEditDescription}
|
||||
handleEditDescriptionClick={handleEditDescriptionClick}
|
||||
handleEditTagClick={handleEditTagClick}
|
||||
handleUpdateClassification={handleUpdateClassification}
|
||||
isAddingTag={isAddingTag}
|
||||
isEditClassification={isEditClassification}
|
||||
ref={classificationDetailsRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Classification Form */}
|
||||
{isAddingClassification && (
|
||||
<TagsForm
|
||||
isClassification
|
||||
showMutuallyExclusive
|
||||
data={classifications}
|
||||
header={t('label.adding-new-classification')}
|
||||
isEditing={false}
|
||||
isLoading={isButtonLoading}
|
||||
isTier={isTier}
|
||||
visible={isAddingClassification}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleCreateClassification}
|
||||
/>
|
||||
)}
|
||||
{/* Classification Form */}
|
||||
{isAddingClassification && (
|
||||
<TagsForm
|
||||
isClassification
|
||||
showMutuallyExclusive
|
||||
data={classifications}
|
||||
header={t('label.adding-new-classification')}
|
||||
isEditing={false}
|
||||
isLoading={isButtonLoading}
|
||||
isTier={isTier}
|
||||
visible={isAddingClassification}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleCreateClassification}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tags Form */}
|
||||
{isAddingTag && (
|
||||
<TagsForm
|
||||
header={tagsFormHeader}
|
||||
initialValues={editTag}
|
||||
isEditing={!isUndefined(editTag)}
|
||||
isLoading={isButtonLoading}
|
||||
isSystemTag={editTag?.provider === ProviderType.System}
|
||||
isTier={isTier}
|
||||
permissions={tagsFormPermissions}
|
||||
visible={isAddingTag}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleAddTagSubmit}
|
||||
/>
|
||||
)}
|
||||
{/* Tags Form */}
|
||||
{isAddingTag && (
|
||||
<TagsForm
|
||||
header={tagsFormHeader}
|
||||
initialValues={editTag}
|
||||
isEditing={!isUndefined(editTag)}
|
||||
isLoading={isButtonLoading}
|
||||
isSystemTag={editTag?.provider === ProviderType.System}
|
||||
isTier={isTier}
|
||||
permissions={tagsFormPermissions}
|
||||
visible={isAddingTag}
|
||||
onCancel={handleCancel}
|
||||
onSubmit={handleAddTagSubmit}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EntityDeleteModal
|
||||
bodyText={getEntityDeleteMessage(deleteTags.data?.name ?? '', '')}
|
||||
entityName={deleteTags.data?.name ?? ''}
|
||||
entityType={t('label.classification')}
|
||||
visible={deleteTags.state}
|
||||
onCancel={handleCancelClassificationDelete}
|
||||
onConfirm={handleConfirmClick}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
<EntityDeleteModal
|
||||
bodyText={getEntityDeleteMessage(
|
||||
deleteTags.data?.name ?? '',
|
||||
''
|
||||
)}
|
||||
entityName={deleteTags.data?.name ?? ''}
|
||||
entityType={t('label.classification')}
|
||||
visible={deleteTags.state}
|
||||
onCancel={handleCancelClassificationDelete}
|
||||
onConfirm={handleConfirmClick}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
className: 'content-resizable-panel-container',
|
||||
minWidth: 800,
|
||||
flex: 0.87,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsPage;
|
||||
export default withPageLayout('tag-plural')(TagsPage);
|
||||
|
||||
@ -735,22 +735,6 @@ a[href].link-text-grey,
|
||||
background-color: @body-bg-color;
|
||||
}
|
||||
|
||||
/* react toastify */
|
||||
|
||||
.Toastify__toast-container {
|
||||
width: 450px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.Toastify__toast {
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.Toastify__toast-body {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ToastUI Editor style */
|
||||
.ProseMirror .placeholder {
|
||||
color: @text-grey-muted;
|
||||
|
||||
@ -20,6 +20,12 @@
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#page-alert {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.page-layout-v1-left-panel {
|
||||
border: @global-border;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
@ -173,6 +173,9 @@
|
||||
.m--t-xss {
|
||||
margin-top: -@margin-xss;
|
||||
}
|
||||
.m--t-sm {
|
||||
margin-top: -@margin-sm;
|
||||
}
|
||||
.m--t-md {
|
||||
margin-top: -@margin-md;
|
||||
}
|
||||
|
||||
@ -110,6 +110,10 @@
|
||||
@team-avatar-bg: #0950c51a;
|
||||
@om-navbar-height: ~'var(--ant-navbar-height)';
|
||||
@sidebar-width: 60px;
|
||||
@error-bg-color: rgb(from @error-color r g b / 0.1);
|
||||
@success-bg-color: rgb(from @success-color r g b / 0.1);
|
||||
@warning-bg-color: rgb(from @warning-color r g b / 0.1);
|
||||
@info-bg-color: rgb(from @info-color r g b / 0.1);
|
||||
|
||||
// Sizing
|
||||
@page-height: calc(100vh - @om-navbar-height);
|
||||
|
||||
@ -11,14 +11,55 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AxiosError, isCancel } from 'axios';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
WarningOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { AlertProps } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty, isString } from 'lodash';
|
||||
import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ReactComponent as ErrorIcon } from '../assets/svg/ic-error.svg';
|
||||
import { ClientErrors } from '../enums/Axios.enum';
|
||||
import { useAlertStore } from '../hooks/useAlertStore';
|
||||
import i18n from './i18next/LocalUtil';
|
||||
import { getErrorText } from './StringsUtils';
|
||||
|
||||
export const getIconAndClassName = (type: AlertProps['type']) => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return {
|
||||
icon: InfoCircleOutlined,
|
||||
className: 'info',
|
||||
};
|
||||
|
||||
case 'success':
|
||||
return {
|
||||
icon: CheckCircleOutlined,
|
||||
className: 'success',
|
||||
};
|
||||
|
||||
case 'warning':
|
||||
return {
|
||||
icon: WarningOutlined,
|
||||
className: 'warning',
|
||||
};
|
||||
|
||||
case 'error':
|
||||
return {
|
||||
icon: ErrorIcon,
|
||||
className: 'error',
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
icon: null,
|
||||
className: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const hashCode = (str: string) => {
|
||||
let hash = 0,
|
||||
i,
|
||||
@ -42,19 +83,18 @@ export const hashCode = (str: string) => {
|
||||
* @param autoCloseTimer Set the delay in ms to close the toast automatically.
|
||||
*/
|
||||
export const showErrorToast = (
|
||||
error: AxiosError | string,
|
||||
error: AxiosError | string | JSX.Element,
|
||||
fallbackText?: string,
|
||||
autoCloseTimer?: number,
|
||||
callback?: (value: React.SetStateAction<string>) => void
|
||||
callback?: (value: React.SetStateAction<string | JSX.Element>) => void
|
||||
) => {
|
||||
if (isCancel(error)) {
|
||||
return;
|
||||
}
|
||||
let errorMessage;
|
||||
if (isString(error)) {
|
||||
if (React.isValidElement(error)) {
|
||||
errorMessage = error;
|
||||
} else if (isString(error)) {
|
||||
errorMessage = error.toString();
|
||||
} else {
|
||||
const method = (error as AxiosError).config?.method?.toUpperCase();
|
||||
} else if ('config' in error && 'response' in error) {
|
||||
const method = error.config?.method?.toUpperCase();
|
||||
const fallback =
|
||||
fallbackText && fallbackText.length > 0
|
||||
? fallbackText
|
||||
@ -65,19 +105,20 @@ export const showErrorToast = (
|
||||
// except for principal domain mismatch errors
|
||||
if (
|
||||
error &&
|
||||
((error as AxiosError).response?.status === ClientErrors.UNAUTHORIZED ||
|
||||
((error as AxiosError).response?.status === ClientErrors.FORBIDDEN &&
|
||||
(error.response?.status === ClientErrors.UNAUTHORIZED ||
|
||||
(error.response?.status === ClientErrors.FORBIDDEN &&
|
||||
method === 'GET')) &&
|
||||
!errorMessage.includes('principal domain')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
errorMessage = fallbackText ?? i18n.t('server.unexpected-error');
|
||||
}
|
||||
callback && callback(errorMessage);
|
||||
toast.error(errorMessage, {
|
||||
toastId: hashCode(errorMessage),
|
||||
autoClose: autoCloseTimer,
|
||||
});
|
||||
useAlertStore
|
||||
.getState()
|
||||
.addAlert({ type: 'error', message: errorMessage }, autoCloseTimer);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -86,9 +127,9 @@ export const showErrorToast = (
|
||||
* @param autoCloseTimer Set the delay in ms to close the toast automatically. `Default: 5000`
|
||||
*/
|
||||
export const showSuccessToast = (message: string, autoCloseTimer = 5000) => {
|
||||
toast.success(message, {
|
||||
autoClose: autoCloseTimer,
|
||||
});
|
||||
useAlertStore
|
||||
.getState()
|
||||
.addAlert({ type: 'success', message }, autoCloseTimer);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -97,7 +138,5 @@ export const showSuccessToast = (message: string, autoCloseTimer = 5000) => {
|
||||
* @param autoCloseTimer Set the delay in ms to close the toast automatically. `Default: 5000`
|
||||
*/
|
||||
export const showInfoToast = (message: string, autoCloseTimer = 5000) => {
|
||||
toast.info(message, {
|
||||
autoClose: autoCloseTimer,
|
||||
});
|
||||
useAlertStore.getState().addAlert({ type: 'info', message }, autoCloseTimer);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user