Chore(ui): Layout and styling fixes according to the new design (#20647)

* Refactor: Remove AlertsActivityFeedPage and update related components

- Deleted AlertsActivityFeedPage component since it was old and not being used.
- Removed unused import and route for AlertsActivityFeedPage in SettingsRouter.
- Fix the layout for the following pages:
1. Add and edit alert page
2. Alert details page
3. Role details page
4. Policy details page
5. Add user and admin page
6. Add and edit Rule page
7. Custom property settings page.

* Fix the type error in the CI

* Fix the vertical alignment of ingestion table rows

* Fix the unit tests

* Fix SettingsRouter test
This commit is contained in:
Aniket Katkar 2025-04-05 11:02:45 +05:30 committed by GitHub
parent 188f575180
commit 7331dea463
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 503 additions and 668 deletions

View File

@ -26,11 +26,6 @@ jest.mock('../../pages/AlertDetailsPage/AlertDetailsPage', () => ({
default: jest.fn().mockReturnValue(<div>AlertDetailsPage</div>),
}));
jest.mock('../../pages/AlertsActivityFeedPage/AlertsActivityFeedPage', () => ({
__esModule: true,
default: jest.fn().mockReturnValue(<div>AlertsActivityFeedPage</div>),
}));
jest.mock('../../pages/Application/ApplicationPage', () => ({
__esModule: true,
default: jest.fn().mockReturnValue(<div>ApplicationPage</div>),
@ -450,18 +445,6 @@ describe('SettingsRouter', () => {
expect(await screen.findByText('ApplicationPage')).toBeInTheDocument();
});
it('should render AlertsActivityFeedPage component for alerts activity feed route', async () => {
render(
<MemoryRouter initialEntries={[`/settings/notifications/activityFeeds`]}>
<SettingsRouter />
</MemoryRouter>
);
expect(
await screen.findByText('AlertsActivityFeedPage')
).toBeInTheDocument();
});
it('should render AlertDetailsPage component for alert details route', async () => {
render(
<MemoryRouter

View File

@ -23,7 +23,6 @@ import { Operation } from '../../generated/entity/policies/accessControl/resourc
import { TeamType } from '../../generated/entity/teams/team';
import AddNotificationPage from '../../pages/AddNotificationPage/AddNotificationPage';
import AlertDetailsPage from '../../pages/AlertDetailsPage/AlertDetailsPage';
import AlertsActivityFeedPage from '../../pages/AlertsActivityFeedPage/AlertsActivityFeedPage';
import AppearanceConfigSettingsPage from '../../pages/AppearanceConfigSettingsPage/AppearanceConfigSettingsPage';
import ApplicationPage from '../../pages/Application/ApplicationPage';
import BotsPageV1 from '../../pages/BotsPageV1/BotsPageV1.component';
@ -375,16 +374,6 @@ const SettingsRouter = () => {
path={getSettingCategoryPath(GlobalSettingsMenuCategory.SERVICES)}
/>
<AdminProtectedRoute
exact
component={AlertsActivityFeedPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.NOTIFICATIONS,
GlobalSettingOptions.ACTIVITY_FEED
)}
/>
<AdminProtectedRoute
exact
component={CustomPropertiesPageV1}

View File

@ -66,6 +66,7 @@ import Table from '../../../../common/Table/Table';
import EntityDeleteModal from '../../../../Modals/EntityDeleteModal/EntityDeleteModal';
import { SelectedRowDetails } from '../ingestion.interface';
import { IngestionRecentRuns } from '../IngestionRecentRun/IngestionRecentRuns.component';
import './ingestion-list-table.less';
import {
IngestionListTableProps,
ModifiedIngestionPipeline,
@ -448,7 +449,7 @@ function IngestionListTable({
return (
<>
<div
className={classNames(tableContainerClassName)}
className={classNames('ingestion-list-table', tableContainerClassName)}
data-testid="ingestion-table">
<Table
className={tableClassName}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2022 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,24 +10,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.roles-detail {
.list-table {
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
.ingestion-list-table {
.ant-table {
.ant-table-thead tr > th {
text-transform: none;
}
}
.role-detail-tab.ant-space {
> .ant-space-item:first-child {
align-self: flex-end;
.ant-table-cell {
vertical-align: middle;
}
}
}
.link-hover {
&:hover {
text-decoration: underline;
cursor: pointer;
}
}

View File

@ -54,12 +54,6 @@
tr > td:first-child.name-column {
padding-left: 16px;
}
.ant-table-thead tr > th {
text-transform: none;
}
.ant-table-cell {
vertical-align: middle;
}
}
}
}

View File

@ -14,7 +14,7 @@
export enum AlertDetailTabs {
CONFIGURATION = 'configuration',
RECENT_EVENTS = 'recentEvents',
DIAGNOSTIC_INFO = 'diagnostic info',
DIAGNOSTIC_INFO = 'diagnostic-info',
}
export enum AlertRecentEventFilters {

View File

@ -1508,6 +1508,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -286,6 +286,10 @@ export interface Connection {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -3377,6 +3377,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -1392,6 +1392,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -1924,6 +1924,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -48,10 +48,6 @@ export interface SnowflakeConnection {
* Optional configuration for ingestion of streams, By default, it will skip the streams.
*/
includeStreams?: boolean;
/**
* Optional configuration for ingestion of streams, By default, it will skip the streams.
*/
includeStreams?: boolean;
/**
* Optional configuration for ingestion of TRANSIENT tables, By default, it will skip the
* TRANSIENT tables.

View File

@ -1549,6 +1549,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -3888,6 +3888,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -392,6 +392,10 @@ export interface Connection {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -1593,6 +1593,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -1629,6 +1629,10 @@ export interface ConfigClass {
* List of IDs of your DBT cloud jobs seperated by comma `,`
*/
jobIds?: string[];
/**
* Number of runs to fetch from DBT cloud
*/
numberOfRuns?: number;
/**
* List of IDs of your DBT cloud projects seperated by comma `,`
*/

View File

@ -13,6 +13,7 @@
*/
import {
Button,
Card,
Col,
Divider,
Form,
@ -226,8 +227,8 @@ const AddNotificationPage = () => {
firstPanel={{
className: 'content-resizable-panel-containere',
children: (
<div className="steps-form-container service-form-container">
<Row className="page-container" gutter={[16, 16]}>
<Card className="steps-form-container">
<Row gutter={[16, 16]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumb} />
</Col>
@ -344,10 +345,11 @@ const AddNotificationPage = () => {
</Form>
</Col>
</Row>
</div>
</Card>
),
minWidth: 700,
flex: 0.7,
wrapInCard: false,
}}
pageTitle={t('label.add-entity', {
entity: t('label.notification-alert'),

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Button, Col, Divider, Form, Input, Row, Typography } from 'antd';
import { Button, Card, Col, Divider, Form, Input, Row, Typography } from 'antd';
import { useForm } from 'antd/lib/form/Form';
import { isEmpty, isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
@ -203,8 +203,8 @@ function AddObservabilityPage() {
firstPanel={{
className: 'content-resizable-panel-container ',
children: (
<div className="steps-form-container service-form-container">
<Row className="p-x-lg p-t-md" gutter={[16, 16]}>
<Card className="steps-form-container">
<Row gutter={[16, 16]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumb} />
</Col>
@ -329,10 +329,11 @@ function AddObservabilityPage() {
</Form>
</Col>
</Row>
</div>
</Card>
),
minWidth: 700,
flex: 0.7,
wrapInCard: false,
}}
pageTitle={t('label.add-entity', {
entity: t('label.observability'),

View File

@ -136,10 +136,8 @@ jest.mock(
})
);
jest.mock('../../components/common/ResizablePanels/ResizablePanels', () =>
jest
.fn()
.mockImplementation(({ firstPanel }) => <div>{firstPanel.children}</div>)
jest.mock('../../components/PageLayoutV1/PageLayoutV1', () =>
jest.fn().mockImplementation(({ children }) => <div>{children}</div>)
);
jest.mock(

View File

@ -12,7 +12,7 @@
*/
import { SyncOutlined } from '@ant-design/icons';
import { Button, Col, Row, Skeleton, Space, Tabs, Tooltip } from 'antd';
import { Button, Card, Col, Row, Skeleton, Space, Tabs, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isUndefined, omitBy } from 'lodash';
@ -29,9 +29,9 @@ import DescriptionV1 from '../../components/common/EntityDescription/Description
import ErrorPlaceHolder from '../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../components/common/Loader/Loader';
import { OwnerLabel } from '../../components/common/OwnerLabel/OwnerLabel.component';
import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels';
import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
import EntityHeaderTitle from '../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1';
import { DE_ACTIVE_COLOR, ROUTES } from '../../constants/constants';
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
@ -48,7 +48,6 @@ import {
EventSubscription,
ProviderType,
} from '../../generated/events/eventSubscription';
import { withPageLayout } from '../../hoc/withPageLayout';
import { useFqn } from '../../hooks/useFqn';
import { updateNotificationAlert } from '../../rest/alertsAPI';
import {
@ -59,7 +58,6 @@ import {
} from '../../rest/observabilityAPI';
import { getAlertExtraInfo } from '../../utils/Alerts/AlertsUtil';
import { getEntityName } from '../../utils/EntityUtils';
import i18n from '../../utils/i18next/LocalUtil';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import {
getNotificationAlertDetailsPath,
@ -336,158 +334,140 @@ function AlertDetailsPage({
}
return (
<ResizablePanels
hideSecondPanel
className="content-height-with-resizable-panel"
firstPanel={{
className: 'content-resizable-panel-container ',
children: loadingCount ? (
<Loader />
) : (
<div
className="steps-form-container service-form-container"
data-testid="alert-details-container">
<Row
className="add-notification-container p-x-lg p-t-md"
gutter={[0, 16]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumb} />
</Col>
<Col span={24}>
<Row justify="space-between">
<Col span={20}>
<Row gutter={[16, 16]}>
<Col span={24}>
<EntityHeaderTitle
displayName={alertDetails?.displayName}
icon={alertIcon}
name={alertDetails?.name ?? ''}
serviceName=""
/>
</Col>
<Col span={24}>
<div className="d-flex items-center flex-wrap gap-2">
{ownerLoading ? (
<Skeleton.Button
active
className="extra-info-skeleton"
/>
) : (
<OwnerLabel
hasPermission={editOwnersPermission}
owners={alertDetails?.owners}
onUpdate={onOwnerUpdate}
/>
)}
{extraInfo}
</div>
</Col>
</Row>
</Col>
<Col>
<Space align="center" size={8}>
<Tooltip
title={t('label.sync-alert-offset', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="sync-button"
icon={<SyncOutlined height={16} width={16} />}
loading={isSyncing}
onClick={handleAlertSync}
/>
</Tooltip>
{editPermission &&
alertDetails?.provider !== ProviderType.System && (
<Tooltip
title={t('label.edit-entity', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="edit-button"
icon={
<EditIcon
color={DE_ACTIVE_COLOR}
height={16}
width={16}
/>
}
onClick={handleAlertEdit}
/>
</Tooltip>
)}
{deletePermission &&
alertDetails?.provider !== ProviderType.System && (
<Tooltip
title={t('label.delete-entity', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="delete-button"
icon={<DeleteIcon height={16} width={16} />}
onClick={() => setShowDeleteModal(true)}
/>
</Tooltip>
)}
</Space>
</Col>
</Row>
</Col>
<Col
className="alert-description"
data-testid="alert-description"
span={24}>
<DescriptionV1
description={alertDetails?.description}
entityType={EntityType.EVENT_SUBSCRIPTION}
hasEditAccess={editDescriptionPermission}
showCommentsIcon={false}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Col>
<Col span={24}>
<Tabs
activeKey={tab}
className="tabs-new"
items={tabItems}
onTabClick={handleTabChange}
/>
</Col>
</Row>
<DeleteWidgetModal
afterDeleteAction={handleAlertDelete}
allowSoftDelete={false}
entityId={alertDetails?.id ?? ''}
entityName={getEntityName(alertDetails)}
entityType={EntityType.SUBSCRIPTION}
visible={showDeleteModal}
onCancel={hideDeleteModal}
/>
</div>
),
minWidth: 700,
flex: 0.7,
}}
<PageLayoutV1
pageTitle={t('label.entity-detail-plural', {
entity: t('label.alert'),
})}
secondPanel={{
children: <></>,
minWidth: 0,
className: 'content-resizable-panel-container',
}}
/>
})}>
{loadingCount ? (
<Loader />
) : (
<Card
className="steps-form-container"
data-testid="alert-details-container">
<Row className="add-notification-container" gutter={[0, 16]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumb} />
</Col>
<Col span={24}>
<Row justify="space-between">
<Col span={20}>
<Row gutter={[16, 16]}>
<Col span={24}>
<EntityHeaderTitle
displayName={alertDetails?.displayName}
icon={alertIcon}
name={alertDetails?.name ?? ''}
serviceName=""
/>
</Col>
<Col span={24}>
<div className="d-flex items-center flex-wrap gap-2">
{ownerLoading ? (
<Skeleton.Button
active
className="extra-info-skeleton"
/>
) : (
<OwnerLabel
hasPermission={editOwnersPermission}
owners={alertDetails?.owners}
onUpdate={onOwnerUpdate}
/>
)}
{extraInfo}
</div>
</Col>
</Row>
</Col>
<Col>
<Space align="center" size={8}>
<Tooltip
title={t('label.sync-alert-offset', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="sync-button"
icon={<SyncOutlined height={16} width={16} />}
loading={isSyncing}
onClick={handleAlertSync}
/>
</Tooltip>
{editPermission &&
alertDetails?.provider !== ProviderType.System && (
<Tooltip
title={t('label.edit-entity', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="edit-button"
icon={
<EditIcon
color={DE_ACTIVE_COLOR}
height={16}
width={16}
/>
}
onClick={handleAlertEdit}
/>
</Tooltip>
)}
{deletePermission &&
alertDetails?.provider !== ProviderType.System && (
<Tooltip
title={t('label.delete-entity', {
entity: t('label.alert'),
})}>
<Button
className="flex flex-center"
data-testid="delete-button"
icon={<DeleteIcon height={16} width={16} />}
onClick={() => setShowDeleteModal(true)}
/>
</Tooltip>
)}
</Space>
</Col>
</Row>
</Col>
<Col
className="alert-description"
data-testid="alert-description"
span={24}>
<DescriptionV1
description={alertDetails?.description}
entityType={EntityType.EVENT_SUBSCRIPTION}
hasEditAccess={editDescriptionPermission}
showCommentsIcon={false}
onDescriptionUpdate={onDescriptionUpdate}
/>
</Col>
<Col span={24}>
<Tabs
activeKey={tab}
className="tabs-new"
items={tabItems}
onTabClick={handleTabChange}
/>
</Col>
</Row>
<DeleteWidgetModal
afterDeleteAction={handleAlertDelete}
allowSoftDelete={false}
entityId={alertDetails?.id ?? ''}
entityName={getEntityName(alertDetails)}
entityType={EntityType.SUBSCRIPTION}
visible={showDeleteModal}
onCancel={hideDeleteModal}
/>
</Card>
)}
</PageLayoutV1>
);
}
export default withPageLayout<AlertDetailsPageProps>(
i18n.t('label.entity-detail-plural', {
entity: i18n.t('label.alert'),
})
)(AlertDetailsPage);
export default AlertDetailsPage;

View File

@ -1,103 +0,0 @@
/*
* Copyright 2022 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 { Card } from 'antd';
import { noop, trim } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Loader from '../../components/common/Loader/Loader';
import { AlertDetailsComponent } from '../../components/Settings/Alerts/AlertsDetails/AlertDetails.component';
import { EventFilterRule } from '../../generated/events/eventFilterRule';
import {
EventSubscription,
FilteringRules,
} from '../../generated/events/eventSubscription';
import { withPageLayout } from '../../hoc/withPageLayout';
import { getAlertsFromName } from '../../rest/alertsAPI';
import { getEntityName } from '../../utils/EntityUtils';
import i18n from '../../utils/i18next/LocalUtil';
import { showErrorToast } from '../../utils/ToastUtils';
const AlertsActivityFeedPage = () => {
const [loading, setLoading] = useState(false);
const [alert, setAlert] = useState<EventSubscription>();
const { t } = useTranslation();
const fetchActivityFeedAlert = useCallback(async () => {
try {
setLoading(true);
const response = await getAlertsFromName('ActivityFeedAlert');
const requestFilteringRules =
response.filteringRules?.rules?.map((curr) => {
const [fullyQualifiedName, filterRule] = curr.condition.split('(');
return {
...curr,
fullyQualifiedName,
condition: filterRule
.replaceAll("'", '')
.replace(new RegExp(`\\)`), '')
.split(',')
.map(trim),
} as unknown as EventFilterRule;
}) ?? [];
setAlert({
...response,
filteringRules: {
...(response.filteringRules as FilteringRules),
rules: requestFilteringRules,
},
});
} catch (error) {
showErrorToast(
t('server.entity-fetch-error', {
entity: t('label.activity-feed-plural'),
})
);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchActivityFeedAlert();
}, []);
const pageHeaderData = useMemo(
() => ({
header: getEntityName(alert),
subHeader: alert?.description || '',
}),
[alert]
);
if (loading) {
return <Card loading={loading} />;
}
return alert ? (
<AlertDetailsComponent
alerts={alert}
allowDelete={false}
pageHeaderData={pageHeaderData}
onDelete={noop}
/>
) : (
<Loader />
);
};
export default withPageLayout(i18n.t('label.alert-details'))(
AlertsActivityFeedPage
);

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { Card } from 'antd';
import { AxiosError } from 'axios';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
@ -216,7 +217,7 @@ const CreateUserPage = () => {
<PageLayoutV1
center
pageTitle={t('label.create-entity', { entity: t('label.user') })}>
<div className="service-form-container w-800">
<Card className="service-form-container w-800">
<TitleBreadcrumb titleLinks={slashedBreadcrumbList} />
<div className="m-t-md">
<CreateUserComponent
@ -227,7 +228,7 @@ const CreateUserPage = () => {
onSave={handleAddUserSave}
/>
</div>
</div>
</Card>
</PageLayoutV1>
);
};

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Button, Col, Row, Tabs } from 'antd';
import { Button, Card, Col, Row, Tabs } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isUndefined, startCase } from 'lodash';
@ -228,11 +228,11 @@ const CustomEntityDetailV1 = () => {
),
key: EntityTabs.CUSTOM_PROPERTIES,
children: (
<div data-testid="entity-custom-fields">
<Card data-testid="entity-custom-fields">
<div className="flex justify-end">
{editPermission && (
<Button
className="m-b-md p-y-xss p-x-xs rounded-4"
className="m-b-md"
data-testid="add-field-button"
size="middle"
type="primary"
@ -250,20 +250,18 @@ const CustomEntityDetailV1 = () => {
isLoading={isLoading}
updateEntityType={updateEntityType}
/>
</div>
</Card>
),
},
{
label: t('label.schema'),
key: EntityTabs.SCHEMA,
children: (
<div data-testid="entity-schema">
<SchemaEditor
className="custom-properties-schemaEditor p-y-md"
editorClass="custom-entity-schema"
value={JSON.parse(schema ?? '{}')}
/>
</div>
<SchemaEditor
className="custom-properties-schemaEditor"
editorClass="custom-entity-schema"
value={JSON.parse(schema ?? '{}')}
/>
),
},
];

View File

@ -11,7 +11,10 @@
* limitations under the License.
*/
@import (reference) url('../../styles/variables.less');
.custom-properties-schemaEditor {
border: 1px solid #dce3ec;
border-radius: 6px;
border: 1px solid @border-color;
border-radius: @border-radius-sm;
overflow: hidden;
}

View File

@ -296,7 +296,7 @@ const ObservabilityAlertsPage = () => {
return (
<PageLayoutV1 pageTitle={t('label.observability-alert')}>
<Row className="p-x-lg p-t-md" gutter={[0, 16]}>
<Row gutter={[0, 16]}>
<Col span={24}>
<div className="d-flex justify-between">
<PageHeader data={pageHeaderData} />

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Button, Col, Form, Row, Space, Typography } from 'antd';
import { Button, Card, Form, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { trim } from 'lodash';
@ -144,39 +144,37 @@ const AddRulePage = () => {
pageTitle={t('label.add-new-entity', {
entity: t('label.rule'),
})}>
<Row className="h-auto p-y-xss" gutter={[16, 16]}>
<Col offset={5} span={14}>
<TitleBreadcrumb className="m-b-md" titleLinks={breadcrumb} />
<Card className="service-form-container w-800">
<TitleBreadcrumb className="m-b-md" titleLinks={breadcrumb} />
<Typography.Paragraph
className="text-base"
data-testid="add-rule-title">
{t('label.add-new-entity', { entity: t('label.rule') })}
</Typography.Paragraph>
<Form
data-testid="rule-form"
id="rule-form"
initialValues={{
ruleEffect: ruleData.effect,
}}
layout="vertical"
onFinish={handleSubmit}>
<RuleForm ruleData={ruleData} setRuleData={setRuleData} />
<Space align="center" className="w-full justify-end">
<Button data-testid="cancel-btn" type="link" onClick={handleBack}>
{t('label.cancel')}
</Button>
<Button
data-testid="submit-btn"
form="rule-form"
htmlType="submit"
type="primary">
{t('label.submit')}
</Button>
</Space>
</Form>
</Col>
</Row>
<Typography.Paragraph
className="text-base"
data-testid="add-rule-title">
{t('label.add-new-entity', { entity: t('label.rule') })}
</Typography.Paragraph>
<Form
data-testid="rule-form"
id="rule-form"
initialValues={{
ruleEffect: ruleData.effect,
}}
layout="vertical"
onFinish={handleSubmit}>
<RuleForm ruleData={ruleData} setRuleData={setRuleData} />
<Space align="center" className="w-full justify-end">
<Button data-testid="cancel-btn" type="link" onClick={handleBack}>
{t('label.cancel')}
</Button>
<Button
data-testid="submit-btn"
form="rule-form"
htmlType="submit"
type="primary">
{t('label.submit')}
</Button>
</Space>
</Form>
</Card>
</PageLayoutV1>
);
};

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Button, Card, Col, Form, Row, Space, Typography } from 'antd';
import { Button, Card, Form, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { trim } from 'lodash';
@ -164,52 +164,45 @@ const EditRulePage = () => {
return (
<PageLayoutV1
pageTitle={t('label.edit-entity', { entity: t('label.rule') })}>
<Row className="h-auto p-y-xss" gutter={[16, 16]}>
<Col offset={5} span={14}>
<TitleBreadcrumb className="m-b-md" titleLinks={breadcrumb} />
<Card>
<Typography.Paragraph
className="text-base"
data-testid="edit-rule-title">
{t('label.edit-entity', { entity: t('label.rule') })}{' '}
{`"${ruleName}"`}
</Typography.Paragraph>
<Form
data-testid="rule-form"
id="rule-form"
initialValues={{
ruleEffect: ruleData.effect,
ruleName: ruleData.name,
resources: ruleData.resources,
operations: ruleData.operations,
condition: ruleData.condition,
}}
layout="vertical"
onFinish={handleSubmit}>
<RuleForm
description={selectedRuleRef.current?.description}
ruleData={ruleData}
setRuleData={setRuleData}
/>
<Space align="center" className="w-full justify-end">
<Button
data-testid="cancel-btn"
type="link"
onClick={handleBack}>
{t('label.cancel')}
</Button>
<Button
data-testid="submit-btn"
form="rule-form"
htmlType="submit"
type="primary">
{t('label.submit')}
</Button>
</Space>
</Form>
</Card>
</Col>
</Row>
<Card className="service-form-container w-800">
<TitleBreadcrumb className="m-b-md" titleLinks={breadcrumb} />
<Typography.Paragraph
className="text-base"
data-testid="edit-rule-title">
{t('label.edit-entity', { entity: t('label.rule') })}{' '}
{`"${ruleName}"`}
</Typography.Paragraph>
<Form
data-testid="rule-form"
id="rule-form"
initialValues={{
ruleEffect: ruleData.effect,
ruleName: ruleData.name,
resources: ruleData.resources,
operations: ruleData.operations,
condition: ruleData.condition,
}}
layout="vertical"
onFinish={handleSubmit}>
<RuleForm
description={selectedRuleRef.current?.description}
ruleData={ruleData}
setRuleData={setRuleData}
/>
<Space align="center" className="w-full justify-end">
<Button data-testid="cancel-btn" type="link" onClick={handleBack}>
{t('label.cancel')}
</Button>
<Button
data-testid="submit-btn"
form="rule-form"
htmlType="submit"
type="primary">
{t('label.submit')}
</Button>
</Space>
</Form>
</Card>
</PageLayoutV1>
);
};

View File

@ -71,11 +71,8 @@ import {
getSettingPath,
} from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import './policies-detail.less';
import PoliciesDetailsList from './PoliciesDetailsList.component';
const { TabPane } = Tabs;
type Attribute = 'roles' | 'teams';
const PoliciesDetailPage = () => {
@ -371,6 +368,150 @@ const PoliciesDetailPage = () => {
}
};
const rulesTab = useMemo(() => {
return (
<Card>
{isEmpty(policy.rules) ? (
<ErrorPlaceHolder />
) : (
<>
<div className="flex justify-end m-b-md">
<Button
data-testid="add-rule"
type="primary"
onClick={() => history.push(getAddPolicyRulePath(fqn))}>
{t('label.add-entity', {
entity: t('label.rule'),
})}
</Button>
</div>
<Space className="w-full" direction="vertical" size={20}>
{policy.rules.map((rule) => (
<Card data-testid="rule-card" key={rule.name || 'rule'}>
<Space
align="baseline"
className="w-full justify-between p-b-lg"
direction="horizontal">
<Typography.Text
className="font-medium text-base text-grey-body"
data-testid="rule-name">
{rule.name}
</Typography.Text>
{getRuleActionElement(rule)}
</Space>
<Space className="w-full" direction="vertical" size={12}>
{rule.description && (
<Row data-testid="description">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.description')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<RichTextEditorPreviewerV1
markdown={rule.description || ''}
/>
</Col>
</Row>
)}
<Row data-testid="resources">
<Col span={2}>
<Typography.Text className="text-grey-muted m-b-0">
{`${t('label.resource-plural')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{rule.resources
?.map((resource) => startCase(resource))
?.join(', ')}
</Typography.Text>
</Col>
</Row>
<Row data-testid="operations">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.operation-plural')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{rule.operations?.join(', ')}
</Typography.Text>
</Col>
</Row>
<Row data-testid="effect">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.effect')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{startCase(rule.effect)}
</Typography.Text>
</Col>
</Row>
{rule.condition && (
<Row data-testid="condition">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.condition')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<code>{rule.condition}</code>
</Col>
</Row>
)}
</Space>
</Card>
))}
</Space>
</>
)}
</Card>
);
}, [policy]);
const tabItems = useMemo(() => {
return [
{
key: 'rules',
label: t('label.rule-plural'),
children: rulesTab,
},
{
key: 'roles',
label: t('label.role-plural'),
children: (
<PoliciesDetailsList
hasAccess
list={policy.roles ?? []}
type="role"
onDelete={(record) => setEntity({ record, attribute: 'roles' })}
/>
),
},
{
key: 'teams',
label: t('label.team-plural'),
children: (
<PoliciesDetailsList
hasAccess
list={policy.teams ?? []}
type="team"
onDelete={(record) => setEntity({ record, attribute: 'teams' })}
/>
),
},
];
}, [rulesTab, policy]);
useEffect(() => {
init();
}, [fqn, policyPermission]);
@ -449,138 +590,11 @@ const PoliciesDetailPage = () => {
onDescriptionUpdate={handleDescriptionUpdate}
/>
<Tabs className="tabs-new" defaultActiveKey="rules">
<TabPane key="rules" tab={t('label.rule-plural')}>
{isEmpty(policy.rules) ? (
<ErrorPlaceHolder />
) : (
<Space
className="w-full tabpane-space"
direction="vertical">
<Button
data-testid="add-rule"
type="primary"
onClick={() => history.push(getAddPolicyRulePath(fqn))}>
{t('label.add-entity', {
entity: t('label.rule'),
})}
</Button>
<Space className="w-full" direction="vertical" size={20}>
{policy.rules.map((rule) => (
<Card
data-testid="rule-card"
key={rule.name || 'rule'}>
<Space
align="baseline"
className="w-full justify-between p-b-lg"
direction="horizontal">
<Typography.Text
className="font-medium text-base text-grey-body"
data-testid="rule-name">
{rule.name}
</Typography.Text>
{getRuleActionElement(rule)}
</Space>
<Space
className="w-full"
direction="vertical"
size={12}>
{rule.description && (
<Row data-testid="description">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.description')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<RichTextEditorPreviewerV1
markdown={rule.description || ''}
/>
</Col>
</Row>
)}
<Row data-testid="resources">
<Col span={2}>
<Typography.Text className="text-grey-muted m-b-0">
{`${t('label.resource-plural')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{rule.resources
?.map((resource) => startCase(resource))
?.join(', ')}
</Typography.Text>
</Col>
</Row>
<Row data-testid="operations">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.operation-plural')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{rule.operations?.join(', ')}
</Typography.Text>
</Col>
</Row>
<Row data-testid="effect">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.effect')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<Typography.Text className="text-grey-body">
{startCase(rule.effect)}
</Typography.Text>
</Col>
</Row>
{rule.condition && (
<Row data-testid="condition">
<Col span={2}>
<Typography.Text className="text-grey-muted">
{`${t('label.condition')}:`}
</Typography.Text>
</Col>
<Col span={22}>
<code>{rule.condition}</code>
</Col>
</Row>
)}
</Space>
</Card>
))}
</Space>
</Space>
)}
</TabPane>
<TabPane key="roles" tab={t('label.role-plural')}>
<PoliciesDetailsList
hasAccess
list={policy.roles ?? []}
type="role"
onDelete={(record) =>
setEntity({ record, attribute: 'roles' })
}
/>
</TabPane>
<TabPane key="teams" tab={t('label.team-plural')}>
<PoliciesDetailsList
hasAccess
list={policy.teams ?? []}
type="team"
onDelete={(record) =>
setEntity({ record, attribute: 'teams' })
}
/>
</TabPane>
</Tabs>
<Tabs
className="tabs-new"
defaultActiveKey="rules"
items={tabItems}
/>
</div>
)}
</>

View File

@ -1,51 +0,0 @@
/*
* Copyright 2022 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 url('../../../styles/variables.less');
.policies-detail {
.list-table {
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
}
}
.ant-collapse {
background-color: @white;
border: 1px solid @border-color;
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.12);
border-radius: 4px;
.ant-collapse-item {
.ant-collapse-header {
padding: 16px;
align-items: flex-start;
.ant-collapse-arrow {
color: @text-color;
font-size: 14px;
}
}
}
.ant-collapse-content {
border-top: none;
.ant-collapse-content-box {
padding-top: 0px;
}
}
}
.ant-space.tabpane-space {
> .ant-space-item:first-child {
align-self: flex-end;
}
}
}

View File

@ -12,7 +12,7 @@
*/
import Icon from '@ant-design/icons';
import { Button, Col, Modal, Row, Space, Tabs, Typography } from 'antd';
import { Button, Card, Col, Modal, Row, Tabs, Typography } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
@ -49,11 +49,8 @@ import { getEntityName } from '../../../utils/EntityUtils';
import { getSettingPath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import AddAttributeModal from '../AddAttributeModal/AddAttributeModal';
import './roles-detail.less';
import RolesDetailPageList from './RolesDetailPageList.component';
const { TabPane } = Tabs;
type Attribute = 'policies' | 'teams' | 'users';
interface AddAttribute {
@ -292,6 +289,67 @@ const RolesDetailPage = () => {
}
};
const tabItems = useMemo(() => {
return [
{
key: 'policies',
label: t('label.policy-plural'),
children: (
<Card>
<div className="flex justify-end m-b-md">
<Button
data-testid="add-policy"
type="primary"
onClick={() =>
setAddAttribute({
type: EntityType.POLICY,
selectedData: role.policies || [],
})
}>
{t('label.add-entity', {
entity: t('label.policy'),
})}
</Button>
</div>
<RolesDetailPageList
hasAccess
list={role.policies ?? []}
type="policy"
onDelete={(record) =>
setEntity({ record, attribute: 'policies' })
}
/>
</Card>
),
},
{
key: 'teams',
label: t('label.team-plural'),
children: (
<RolesDetailPageList
hasAccess
list={role.teams ?? []}
type="team"
onDelete={(record) => setEntity({ record, attribute: 'teams' })}
/>
),
},
{
key: 'users',
label: t('label.user-plural'),
children: (
<RolesDetailPageList
hasAccess
list={role.users ?? []}
type="user"
onDelete={(record) => setEntity({ record, attribute: 'users' })}
/>
),
},
];
}, [role]);
useEffect(() => {
init();
}, [fqn, rolePermission]);
@ -307,7 +365,6 @@ const RolesDetailPage = () => {
})}>
<div data-testid="role-details-container">
<TitleBreadcrumb titleLinks={breadcrumb} />
<>
{isEmpty(role) ? (
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.CUSTOM}>
@ -328,7 +385,7 @@ const RolesDetailPage = () => {
</div>
</ErrorPlaceHolder>
) : (
<div className="roles-detail" data-testid="role-details">
<>
<Row className="flex justify-between">
<Col span={23}>
<EntityHeaderTitle
@ -377,57 +434,10 @@ const RolesDetailPage = () => {
<Tabs
className="tabs-new"
data-testid="tabs"
defaultActiveKey="policies">
<TabPane key="policies" tab={t('label.policy-plural')}>
<Space
className="role-detail-tab w-full"
direction="vertical">
<Button
data-testid="add-policy"
type="primary"
onClick={() =>
setAddAttribute({
type: EntityType.POLICY,
selectedData: role.policies || [],
})
}>
{t('label.add-entity', {
entity: t('label.policy'),
})}
</Button>
<RolesDetailPageList
hasAccess
list={role.policies ?? []}
type="policy"
onDelete={(record) =>
setEntity({ record, attribute: 'policies' })
}
/>
</Space>
</TabPane>
<TabPane key="teams" tab={t('label.team-plural')}>
<RolesDetailPageList
hasAccess
list={role.teams ?? []}
type="team"
onDelete={(record) =>
setEntity({ record, attribute: 'teams' })
}
/>
</TabPane>
<TabPane key="users" tab={t('label.user-plural')}>
<RolesDetailPageList
hasAccess
list={role.users ?? []}
type="user"
onDelete={(record) =>
setEntity({ record, attribute: 'users' })
}
/>
</TabPane>
</Tabs>
</div>
defaultActiveKey="policies"
items={tabItems}
/>
</>
)}
</>

View File

@ -13,8 +13,7 @@
// Common add edit form page styling
.steps-form-container {
width: 70%;
margin: 16px auto 0;
padding-bottom: 16px;
margin: 0 auto;
.ant-form-item {
margin: 0px;

View File

@ -25,7 +25,7 @@ button {
}
}
.ant-btn.ant-btn-default {
.ant-btn.ant-btn-default:not(:hover) {
border-color: @blue-15;
}