mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-24 17:08:28 +00:00
* ✨ Feat(#3031) Support policy authoring through UI (outside of role page) * Change remove text to remove icon * - [x] Add confirmation for removing items from the roles and policies details page * Change rules styling according to the mocks * Make Access category protected! * Changed condition from normal text to code * Create rule form component * Fix spacing * Add support for "Add and Edit" rule for policy * Add helper text for Add "roles" and "policy" page
This commit is contained in:
parent
9c8ee19497
commit
d942c55e34
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 16C3.58887 16 0 12.4111 0 8C0 3.58887 3.58887 0 8 0C12.4111 0 16 3.58887 16 8C16 12.4111 12.4111 16 8 16ZM8 1C4.14013 1 1 4.14013 1 8C1 11.8599 4.14013 15 8 15C11.8599 15 15 11.8599 15 8C15 4.14013 11.8599 1 8 1ZM12 8C12 7.72363 11.7764 7.5 11.5 7.5H4.5C4.22363 7.5 4 7.72363 4 8C4 8.27637 4.22363 8.5 4.5 8.5H11.5C11.7764 8.5 12 8.27637 12 8Z" fill="#37352F"/>
|
||||
</svg>
|
After Width: | Height: | Size: 477 B |
@ -24,3 +24,15 @@ export const CONFIGURE_WEBHOOK_TEXT =
|
||||
|
||||
export const CONFIGURE_SLACK_TEXT =
|
||||
'Automatically send out event notifications to registered Slack webhooks through OpenMetadata. Enter the webhook name, and an Endpoint URL to receive the HTTP call back on. Use Event Filters to only receive notifications for the required entities. Filter events based on when an entity is created, updated, or deleted. Add a description to note the use case of the webhook. You can use advanced configuration to set up a shared secret key to verify the Slack webhook events using HMAC signature.';
|
||||
|
||||
export const ADD_ROLE_TEXT = `Roles are assigned to Users. In OpenMetadata, Roles are a collection of
|
||||
Policies. Each Role must have at least one policy attached to it. A Role
|
||||
supports multiple policies with a one to many relationship. Ensure that
|
||||
the necessary policies are created before creating a new role. Build
|
||||
rich access control roles with well-defined policies based on
|
||||
conditional rules.`;
|
||||
|
||||
export const ADD_POLICY_TEXT = `
|
||||
Policies are assigned to Teams. In OpenMetadata, a Policy is a collection of Rules, which define access based on certain conditions. We support rich SpEL (Spring Expression Language) based conditions. All the operations supported by an Entity are published. Use these fine grained operations to define the conditional Rules for each Policy.
|
||||
Create well-defined policies based on conditional rules to build rich access control roles.
|
||||
`;
|
||||
|
@ -85,6 +85,7 @@ export const PLACEHOLDER_TASK_ID = ':taskId';
|
||||
export const PLACEHOLDER_SETTING_CATEGORY = ':settingCategory';
|
||||
export const PLACEHOLDER_USER_BOT = ':bot';
|
||||
export const PLACEHOLDER_WEBHOOK_TYPE = ':webhookType';
|
||||
export const PLACEHOLDER_RULE_NAME = ':ruleName';
|
||||
|
||||
export const pagingObject = { after: '', before: '', total: 0 };
|
||||
|
||||
@ -229,6 +230,8 @@ export const ROUTES = {
|
||||
ACTIVITY_PUSH_FEED: '/api/v1/push/feed',
|
||||
ADD_ROLE: '/settings/access/roles/add-role',
|
||||
ADD_POLICY: '/settings/access/policies/add-policy',
|
||||
ADD_POLICY_RULE: `/settings/access/policies/${PLACEHOLDER_ROUTE_FQN}/add-rule`,
|
||||
EDIT_POLICY_RULE: `/settings/access/policies/${PLACEHOLDER_ROUTE_FQN}/edit-rule/${PLACEHOLDER_RULE_NAME}`,
|
||||
};
|
||||
|
||||
export const SOCKET_EVENTS = {
|
||||
|
@ -49,7 +49,7 @@ export const GLOBAL_SETTINGS_MENU = [
|
||||
},
|
||||
{
|
||||
category: 'Access',
|
||||
isProtected: false,
|
||||
isProtected: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Roles',
|
||||
|
@ -19,35 +19,30 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { capitalize, startCase, uniq } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { addPolicy, getPolicyResources } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import { addPolicy } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { GlobalSettingOptions } from '../../../constants/globalSettings.constants';
|
||||
import { ADD_POLICY_TEXT } from '../../../constants/HelperTextUtil';
|
||||
import {
|
||||
CreatePolicy,
|
||||
Effect,
|
||||
Operation,
|
||||
PolicyType,
|
||||
Rule,
|
||||
} from '../../../generated/api/policies/createPolicy';
|
||||
import { ResourceDescriptor } from '../../../generated/entity/policies/accessControl/resourceDescriptor';
|
||||
import {
|
||||
getPath,
|
||||
getPolicyWithFqnPath,
|
||||
getSettingPath,
|
||||
} from '../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
|
||||
const { Option } = Select;
|
||||
import RuleForm from '../RuleForm/RuleForm';
|
||||
|
||||
const policiesPath = getPath(GlobalSettingOptions.POLICIES);
|
||||
|
||||
@ -68,9 +63,7 @@ const breadcrumb = [
|
||||
|
||||
const AddPolicyPage = () => {
|
||||
const history = useHistory();
|
||||
const [policyResources, setPolicyResources] = useState<ResourceDescriptor[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const [name, setName] = useState<string>('');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
const [ruleData, setRuleData] = useState<Rule>({
|
||||
@ -81,69 +74,6 @@ const AddPolicyPage = () => {
|
||||
effect: Effect.Allow,
|
||||
});
|
||||
|
||||
/**
|
||||
* Derive the resources from policy resources
|
||||
*/
|
||||
const resourcesOptions = useMemo(() => {
|
||||
const resources = policyResources.filter(
|
||||
(resource) => resource.name !== 'all'
|
||||
);
|
||||
const option = [
|
||||
{
|
||||
title: 'All',
|
||||
value: 'all',
|
||||
key: 'all',
|
||||
children: resources.map((resource) => ({
|
||||
title: startCase(resource.name),
|
||||
value: resource.name,
|
||||
key: resource.name,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return option;
|
||||
}, [policyResources]);
|
||||
|
||||
/**
|
||||
* Derive the operations from selected resources
|
||||
*/
|
||||
const operationOptions = useMemo(() => {
|
||||
const selectedResources = policyResources.filter((resource) =>
|
||||
ruleData.resources?.includes(resource.name || '')
|
||||
);
|
||||
const operations = selectedResources
|
||||
.reduce(
|
||||
(prev: Operation[], curr: ResourceDescriptor) =>
|
||||
uniq([...prev, ...(curr.operations || [])]),
|
||||
[]
|
||||
)
|
||||
.filter((operation) => operation !== Operation.All);
|
||||
|
||||
const option = [
|
||||
{
|
||||
title: 'All',
|
||||
value: Operation.All,
|
||||
key: 'All',
|
||||
children: operations.map((operation) => ({
|
||||
title: operation,
|
||||
value: operation,
|
||||
key: operation,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return option;
|
||||
}, [ruleData.resources, policyResources]);
|
||||
|
||||
const fetchPolicyResources = async () => {
|
||||
try {
|
||||
const data = await getPolicyResources();
|
||||
setPolicyResources(data.data || []);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(policiesPath);
|
||||
};
|
||||
@ -168,13 +98,9 @@ const AddPolicyPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicyResources();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Row className="tw-bg-body-main tw-h-auto" gutter={[16, 16]}>
|
||||
<Col offset={5} span={14}>
|
||||
<Col offset={4} span={12}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumb} />
|
||||
<Card>
|
||||
<Typography.Paragraph className="tw-text-base">
|
||||
@ -217,98 +143,7 @@ const AddPolicyPage = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Divider>Add Rule</Divider>
|
||||
<Form.Item
|
||||
label="Rule Name:"
|
||||
name="ruleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
max: 128,
|
||||
min: 1,
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
placeholder="Rule Name"
|
||||
type="text"
|
||||
value={ruleData.name}
|
||||
onChange={(e) =>
|
||||
setRuleData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Description:" name="ruleDescription">
|
||||
<RichTextEditor
|
||||
height="200px"
|
||||
initialValue={ruleData.description || ''}
|
||||
placeHolder="Write your description"
|
||||
style={{ margin: 0 }}
|
||||
onTextChange={(value) =>
|
||||
setRuleData((prev) => ({ ...prev, description: value }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Resources:"
|
||||
name="resources"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="tw-w-full"
|
||||
placeholder="Select Resources"
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={resourcesOptions}
|
||||
onChange={(values) => {
|
||||
setRuleData((prev) => ({
|
||||
...prev,
|
||||
resources: values,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Operations:"
|
||||
name="operations"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="tw-w-full"
|
||||
placeholder="Select Operations"
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={operationOptions}
|
||||
onChange={(values) => {
|
||||
setRuleData((prev) => ({
|
||||
...prev,
|
||||
operations: values,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Effect:"
|
||||
name="ruleEffect"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
placeholder="Select Rule Effect"
|
||||
value={ruleData.effect}
|
||||
onChange={(value) =>
|
||||
setRuleData((prev) => ({ ...prev, effect: value }))
|
||||
}>
|
||||
<Option key={Effect.Allow}>{capitalize(Effect.Allow)}</Option>
|
||||
<Option key={Effect.Deny}>{capitalize(Effect.Deny)}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<RuleForm ruleData={ruleData} setRuleData={setRuleData} />
|
||||
|
||||
<Space align="center" className="tw-w-full tw-justify-end">
|
||||
<Button type="link" onClick={handleCancel}>
|
||||
@ -321,6 +156,12 @@ const AddPolicyPage = () => {
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col className="tw-mt-4" span={4}>
|
||||
<Typography.Paragraph className="tw-text-base tw-font-medium">
|
||||
Add Policy
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text>{ADD_POLICY_TEXT}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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 { Button, Card, Col, Form, Row, Space, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { getPolicyByName, patchPolicy } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import Loader from '../../../components/Loader/Loader';
|
||||
import { GlobalSettingOptions } from '../../../constants/globalSettings.constants';
|
||||
import { Effect, Rule } from '../../../generated/api/policies/createPolicy';
|
||||
import { Policy } from '../../../generated/entity/policies/policy';
|
||||
import { getEntityName } from '../../../utils/CommonUtils';
|
||||
import {
|
||||
getPath,
|
||||
getPolicyWithFqnPath,
|
||||
getSettingPath,
|
||||
} from '../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import RuleForm from '../RuleForm/RuleForm';
|
||||
|
||||
const policiesPath = getPath(GlobalSettingOptions.POLICIES);
|
||||
|
||||
const AddRulePage = () => {
|
||||
const history = useHistory();
|
||||
const { fqn } = useParams<{ fqn: string }>();
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [policy, setPolicy] = useState<Policy>({} as Policy);
|
||||
const [ruleData, setRuleData] = useState<Rule>({
|
||||
name: '',
|
||||
description: '',
|
||||
resources: [],
|
||||
operations: [],
|
||||
effect: Effect.Allow,
|
||||
});
|
||||
|
||||
const breadcrumb = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'Settings',
|
||||
url: getSettingPath(),
|
||||
},
|
||||
{
|
||||
name: 'Policies',
|
||||
url: policiesPath,
|
||||
},
|
||||
{
|
||||
name: getEntityName(policy),
|
||||
url: getPolicyWithFqnPath(fqn),
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Add New Rule',
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
[fqn, policy]
|
||||
);
|
||||
|
||||
const fetchPolicy = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getPolicyByName(fqn, 'owner,location,teams,roles');
|
||||
setPolicy(data ?? ({} as Policy));
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
history.push(getPolicyWithFqnPath(fqn));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const patch = compare(policy, {
|
||||
...policy,
|
||||
rules: [...policy.rules, ruleData],
|
||||
});
|
||||
try {
|
||||
const data = await patchPolicy(patch, policy.id);
|
||||
if (data) {
|
||||
handleBack();
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicy();
|
||||
}, [fqn]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="tw-bg-body-main tw-h-auto" gutter={[16, 16]}>
|
||||
<Col offset={5} span={14}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumb} />
|
||||
<Card>
|
||||
<Typography.Paragraph className="tw-text-base">
|
||||
Add New 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="tw-w-full tw-justify-end">
|
||||
<Button type="link" onClick={handleBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button form="rule-form" htmlType="submit" type="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddRulePage;
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 { Button, Card, Col, Form, Row, Space, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { getPolicyByName, patchPolicy } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import Loader from '../../../components/Loader/Loader';
|
||||
import { GlobalSettingOptions } from '../../../constants/globalSettings.constants';
|
||||
import { Effect, Rule } from '../../../generated/api/policies/createPolicy';
|
||||
import { Policy } from '../../../generated/entity/policies/policy';
|
||||
import { getEntityName } from '../../../utils/CommonUtils';
|
||||
import {
|
||||
getPath,
|
||||
getPolicyWithFqnPath,
|
||||
getSettingPath,
|
||||
} from '../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import RuleForm from '../RuleForm/RuleForm';
|
||||
|
||||
const policiesPath = getPath(GlobalSettingOptions.POLICIES);
|
||||
|
||||
const InitialData = {
|
||||
name: '',
|
||||
description: '',
|
||||
resources: [],
|
||||
operations: [],
|
||||
effect: Effect.Allow,
|
||||
};
|
||||
|
||||
const EditRulePage = () => {
|
||||
const history = useHistory();
|
||||
const { fqn, ruleName } = useParams<{ fqn: string; ruleName: string }>();
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [policy, setPolicy] = useState<Policy>({} as Policy);
|
||||
const [ruleData, setRuleData] = useState<Rule>(InitialData);
|
||||
|
||||
const breadcrumb = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'Settings',
|
||||
url: getSettingPath(),
|
||||
},
|
||||
{
|
||||
name: 'Policies',
|
||||
url: policiesPath,
|
||||
},
|
||||
{
|
||||
name: getEntityName(policy),
|
||||
url: getPolicyWithFqnPath(fqn),
|
||||
},
|
||||
|
||||
{
|
||||
name: ruleName,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
[fqn, policy, ruleName]
|
||||
);
|
||||
|
||||
const fetchPolicy = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getPolicyByName(fqn, 'owner,location,teams,roles');
|
||||
if (data) {
|
||||
setPolicy(data);
|
||||
const selectedRule = data.rules.find((rule) => rule.name === ruleName);
|
||||
setRuleData(selectedRule ?? InitialData);
|
||||
} else {
|
||||
setPolicy({} as Policy);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
history.push(getPolicyWithFqnPath(fqn));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const existingRules = policy.rules;
|
||||
const updatedRules = existingRules.map((rule) => {
|
||||
if (rule.name === ruleName) {
|
||||
return ruleData;
|
||||
} else {
|
||||
return rule;
|
||||
}
|
||||
});
|
||||
const patch = compare(policy, {
|
||||
...policy,
|
||||
rules: updatedRules,
|
||||
});
|
||||
try {
|
||||
const data = await patchPolicy(patch, policy.id);
|
||||
if (data) {
|
||||
handleBack();
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicy();
|
||||
}, [fqn]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="tw-bg-body-main tw-h-auto" gutter={[16, 16]}>
|
||||
<Col offset={5} span={14}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumb} />
|
||||
<Card>
|
||||
<Typography.Paragraph className="tw-text-base">
|
||||
Edit 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,
|
||||
}}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}>
|
||||
<RuleForm ruleData={ruleData} setRuleData={setRuleData} />
|
||||
<Space align="center" className="tw-w-full tw-justify-end">
|
||||
<Button type="link" onClick={handleBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button form="rule-form" htmlType="submit" type="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditRulePage;
|
@ -11,6 +11,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@primary: #7147e8;
|
||||
@white: #ffffff;
|
||||
|
||||
.policies-detail {
|
||||
.list-table {
|
||||
.ant-table-row .ant-table-cell:first-child,
|
||||
@ -18,4 +21,28 @@
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
.ant-collapse {
|
||||
background-color: @white;
|
||||
.ant-collapse-item {
|
||||
.ant-collapse-header {
|
||||
padding: 6px 16px;
|
||||
.ant-collapse-arrow {
|
||||
color: @primary;
|
||||
font-size: 14px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-collapse-content {
|
||||
border-top: none;
|
||||
.ant-collapse-content-box {
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.rules-tab {
|
||||
> .ant-space-item:first-child {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,10 @@
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
Empty,
|
||||
Row,
|
||||
Modal,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Typography,
|
||||
@ -26,7 +24,7 @@ import {
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isEmpty, uniqueId } from 'lodash';
|
||||
import { isEmpty, isUndefined, uniqueId } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { getPolicyByName, patchPolicy } from '../../../axiosAPIs/rolesAPIV1';
|
||||
@ -39,19 +37,26 @@ import {
|
||||
GlobalSettingsMenuCategory,
|
||||
} from '../../../constants/globalSettings.constants';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { Effect, Policy } from '../../../generated/entity/policies/policy';
|
||||
import { Policy } from '../../../generated/entity/policies/policy';
|
||||
import { EntityReference } from '../../../generated/type/entityReference';
|
||||
import { getEntityName } from '../../../utils/CommonUtils';
|
||||
import {
|
||||
getAddPolicyRulePath,
|
||||
getEditPolicyRulePath,
|
||||
getRoleWithFqnPath,
|
||||
getSettingPath,
|
||||
getTeamsWithFqnPath,
|
||||
} from '../../../utils/RouterUtils';
|
||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import './PoliciesDetail.less';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
type Attribute = 'roles' | 'teams';
|
||||
|
||||
const List = ({
|
||||
list,
|
||||
type,
|
||||
@ -107,7 +112,7 @@ const List = ({
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Button type="text" onClick={() => onDelete(record)}>
|
||||
Remove
|
||||
<SVGIcons alt="remove" icon={Icons.ICON_REMOVE} title="Remove" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
@ -133,6 +138,8 @@ const PoliciesDetailPage = () => {
|
||||
const [policy, setPolicy] = useState<Policy>({} as Policy);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [editDescription, setEditDescription] = useState<boolean>(false);
|
||||
const [selectedEntity, setEntity] =
|
||||
useState<{ attribute: Attribute; record: EntityReference }>();
|
||||
|
||||
const policiesPath = getSettingPath(
|
||||
GlobalSettingsMenuCategory.ACCESS,
|
||||
@ -177,10 +184,7 @@ const PoliciesDetailPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (
|
||||
data: EntityReference,
|
||||
attribute: 'roles' | 'teams'
|
||||
) => {
|
||||
const handleDelete = async (data: EntityReference, attribute: Attribute) => {
|
||||
const attributeData =
|
||||
(policy[attribute as keyof Policy] as EntityReference[]) ?? [];
|
||||
const updatedAttributeData = attributeData.filter(
|
||||
@ -238,76 +242,137 @@ const PoliciesDetailPage = () => {
|
||||
{isEmpty(policy.rules) ? (
|
||||
<Empty description="No rules found" />
|
||||
) : (
|
||||
<Row gutter={[16, 16]}>
|
||||
{policy.rules.map((rule) => (
|
||||
<Col key={uniqueId()} span={24}>
|
||||
<Card>
|
||||
<Space
|
||||
align="baseline"
|
||||
className="tw-w-full tw-justify-between"
|
||||
size={4}>
|
||||
<Typography.Paragraph className="tw-font-medium tw-text-base">
|
||||
{rule.name}
|
||||
</Typography.Paragraph>
|
||||
<div>
|
||||
<Switch
|
||||
checked={rule.effect === Effect.Allow}
|
||||
size="small"
|
||||
/>
|
||||
<span className="tw-ml-1">Active</span>
|
||||
</div>
|
||||
</Space>
|
||||
<Space className="tw-w-full rules-tab" direction="vertical">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => history.push(getAddPolicyRulePath(fqn))}>
|
||||
Add Rule
|
||||
</Button>
|
||||
<Space className="tw-w-full" direction="vertical">
|
||||
{policy.rules.map((rule) => (
|
||||
<Collapse key={uniqueId()}>
|
||||
<Panel
|
||||
header={
|
||||
<Space
|
||||
className="tw-w-full"
|
||||
direction="vertical"
|
||||
size={4}>
|
||||
<Space
|
||||
align="baseline"
|
||||
className="tw-w-full tw-justify-between"
|
||||
size={4}>
|
||||
<Typography.Text className="tw-font-medium tw-text-base">
|
||||
{rule.name}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
data-testid="edit-rule"
|
||||
type="text"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
history.push(
|
||||
getEditPolicyRulePath(
|
||||
fqn,
|
||||
rule.name || ''
|
||||
)
|
||||
);
|
||||
}}>
|
||||
<SVGIcons alt="edit" icon={Icons.EDIT} />
|
||||
</Button>
|
||||
</Space>
|
||||
<div
|
||||
className="tw--ml-5"
|
||||
data-testid="description">
|
||||
<Typography.Text className="tw-text-grey-muted">
|
||||
Description:
|
||||
</Typography.Text>
|
||||
<RichTextEditorPreviewer
|
||||
markdown={rule.description || ''}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
key={rule.name || 'rule'}>
|
||||
<Space direction="vertical">
|
||||
<Space
|
||||
data-testid="resources"
|
||||
direction="vertical"
|
||||
size={4}>
|
||||
<Typography.Text className="tw-text-grey-muted tw-mb-0">
|
||||
Resources:
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{rule.resources?.join(', ')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
|
||||
<div className="tw-mb-3" data-testid="description">
|
||||
<Typography.Text className="tw-text-grey-muted">
|
||||
Description:
|
||||
</Typography.Text>
|
||||
<RichTextEditorPreviewer
|
||||
markdown={rule.description || ''}
|
||||
/>
|
||||
</div>
|
||||
<Space direction="vertical">
|
||||
<Space data-testid="resources" direction="vertical">
|
||||
<Typography.Text className="tw-text-grey-muted tw-mb-0">
|
||||
Resources:
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{rule.resources?.join(', ')}
|
||||
</Typography.Text>
|
||||
<Space
|
||||
data-testid="operations"
|
||||
direction="vertical"
|
||||
size={4}>
|
||||
<Typography.Text className="tw-text-grey-muted">
|
||||
Operations:
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{rule.operations?.join(', ')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{rule.condition && (
|
||||
<Space
|
||||
data-testid="condition"
|
||||
direction="vertical"
|
||||
size={4}>
|
||||
<Typography.Text className="tw-text-grey-muted">
|
||||
Condition:
|
||||
</Typography.Text>
|
||||
<code>{rule.condition}</code>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
<Space data-testid="operations" direction="vertical">
|
||||
<Typography.Text className="tw-text-grey-muted">
|
||||
Operations:
|
||||
</Typography.Text>
|
||||
<Typography.Text>
|
||||
{rule.operations?.join(', ')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
))}
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
</TabPane>
|
||||
<TabPane key="roles" tab="Roles">
|
||||
<List
|
||||
list={policy.roles ?? []}
|
||||
type="role"
|
||||
onDelete={(record) => handleDelete(record, 'roles')}
|
||||
onDelete={(record) => setEntity({ record, attribute: 'roles' })}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="teams" tab="Teams">
|
||||
<List
|
||||
list={policy.teams ?? []}
|
||||
type="team"
|
||||
onDelete={(record) => handleDelete(record, 'teams')}
|
||||
onDelete={(record) => setEntity({ record, attribute: 'teams' })}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
{selectedEntity && (
|
||||
<Modal
|
||||
centered
|
||||
okText="Confirm"
|
||||
title={`Remove ${getEntityName(
|
||||
selectedEntity.record
|
||||
)} from ${getEntityName(policy)}`}
|
||||
visible={!isUndefined(selectedEntity.record)}
|
||||
onCancel={() => setEntity(undefined)}
|
||||
onOk={() => {
|
||||
handleDelete(selectedEntity.record, selectedEntity.attribute);
|
||||
setEntity(undefined);
|
||||
}}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to remove the{' '}
|
||||
{`${getEntityName(selectedEntity.record)} from ${getEntityName(
|
||||
policy
|
||||
)}?`}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,8 +16,13 @@ import { AxiosError } from 'axios';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getPolicies } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import NextPrevious from '../../../components/common/next-previous/NextPrevious';
|
||||
import Loader from '../../../components/Loader/Loader';
|
||||
import { ROUTES } from '../../../constants/constants';
|
||||
import {
|
||||
INITIAL_PAGING_VALUE,
|
||||
PAGE_SIZE,
|
||||
ROUTES,
|
||||
} from '../../../constants/constants';
|
||||
import { Policy } from '../../../generated/entity/policies/policy';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
@ -28,6 +33,8 @@ const PoliciesListPage = () => {
|
||||
const history = useHistory();
|
||||
const [policies, setPolicies] = useState<Policy[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [paging, setPaging] = useState<Paging>();
|
||||
const [currentPage, setCurrentPage] = useState<number>(INITIAL_PAGING_VALUE);
|
||||
|
||||
const fetchPolicies = async (paging?: Paging) => {
|
||||
setIsLoading(true);
|
||||
@ -39,6 +46,7 @@ const PoliciesListPage = () => {
|
||||
);
|
||||
|
||||
setPolicies(data.data || []);
|
||||
setPaging(data.paging);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
@ -50,6 +58,11 @@ const PoliciesListPage = () => {
|
||||
history.push(ROUTES.ADD_POLICY);
|
||||
};
|
||||
|
||||
const handlePaging = (_: string | number, activePage?: number) => {
|
||||
setCurrentPage(activePage ?? INITIAL_PAGING_VALUE);
|
||||
fetchPolicies(paging);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicies();
|
||||
}, []);
|
||||
@ -68,6 +81,17 @@ const PoliciesListPage = () => {
|
||||
<Col span={24}>
|
||||
<PoliciesList fetchPolicies={fetchPolicies} policies={policies} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{paging && paging.total > PAGE_SIZE && (
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
paging={paging}
|
||||
pagingHandler={handlePaging}
|
||||
totalCount={paging.total}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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 { Form, Input, Select, TreeSelect } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { capitalize, startCase, uniq } from 'lodash';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { getPolicyResources } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import {
|
||||
Effect,
|
||||
Operation,
|
||||
Rule,
|
||||
} from '../../../generated/api/policies/createPolicy';
|
||||
import { ResourceDescriptor } from '../../../generated/entity/policies/accessControl/resourceDescriptor';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface RuleFormProps {
|
||||
ruleData: Rule;
|
||||
setRuleData: (value: React.SetStateAction<Rule>) => void;
|
||||
}
|
||||
|
||||
const RuleForm: FC<RuleFormProps> = ({ ruleData, setRuleData }) => {
|
||||
const [policyResources, setPolicyResources] = useState<ResourceDescriptor[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Derive the resources from policy resources
|
||||
*/
|
||||
const resourcesOptions = useMemo(() => {
|
||||
const resources = policyResources.filter(
|
||||
(resource) => resource.name !== 'all'
|
||||
);
|
||||
const option = [
|
||||
{
|
||||
title: 'All',
|
||||
value: 'all',
|
||||
key: 'all',
|
||||
children: resources.map((resource) => ({
|
||||
title: startCase(resource.name),
|
||||
value: resource.name,
|
||||
key: resource.name,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return option;
|
||||
}, [policyResources]);
|
||||
|
||||
/**
|
||||
* Derive the operations from selected resources
|
||||
*/
|
||||
const operationOptions = useMemo(() => {
|
||||
const selectedResources = policyResources.filter((resource) =>
|
||||
ruleData.resources?.includes(resource.name || '')
|
||||
);
|
||||
const operations = selectedResources
|
||||
.reduce(
|
||||
(prev: Operation[], curr: ResourceDescriptor) =>
|
||||
uniq([...prev, ...(curr.operations || [])]),
|
||||
[]
|
||||
)
|
||||
.filter((operation) => operation !== Operation.All);
|
||||
|
||||
const option = [
|
||||
{
|
||||
title: 'All',
|
||||
value: Operation.All,
|
||||
key: 'All',
|
||||
children: operations.map((operation) => ({
|
||||
title: operation,
|
||||
value: operation,
|
||||
key: operation,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return option;
|
||||
}, [ruleData.resources, policyResources]);
|
||||
|
||||
const fetchPolicyResources = async () => {
|
||||
try {
|
||||
const data = await getPolicyResources();
|
||||
setPolicyResources(data.data || []);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchPolicyResources();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Rule Name:"
|
||||
name="ruleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
max: 128,
|
||||
min: 1,
|
||||
},
|
||||
]}>
|
||||
<Input
|
||||
placeholder="Rule Name"
|
||||
type="text"
|
||||
value={ruleData.name}
|
||||
onChange={(e) =>
|
||||
setRuleData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Description:" name="ruleDescription">
|
||||
<RichTextEditor
|
||||
height="200px"
|
||||
initialValue={ruleData.description || ''}
|
||||
placeHolder="Write your description"
|
||||
style={{ margin: 0 }}
|
||||
onTextChange={(value) =>
|
||||
setRuleData((prev) => ({ ...prev, description: value }))
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Resources:"
|
||||
name="resources"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="tw-w-full"
|
||||
placeholder="Select Resources"
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={resourcesOptions}
|
||||
onChange={(values) => {
|
||||
setRuleData((prev) => ({
|
||||
...prev,
|
||||
resources: values,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Operations:"
|
||||
name="operations"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<TreeSelect
|
||||
treeCheckable
|
||||
className="tw-w-full"
|
||||
placeholder="Select Operations"
|
||||
showCheckedStrategy={TreeSelect.SHOW_PARENT}
|
||||
treeData={operationOptions}
|
||||
onChange={(values) => {
|
||||
setRuleData((prev) => ({
|
||||
...prev,
|
||||
operations: values,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Effect:"
|
||||
name="ruleEffect"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
placeholder="Select Rule Effect"
|
||||
value={ruleData.effect}
|
||||
onChange={(value) =>
|
||||
setRuleData((prev) => ({ ...prev, effect: value }))
|
||||
}>
|
||||
<Option key={Effect.Allow}>{capitalize(Effect.Allow)}</Option>
|
||||
<Option key={Effect.Deny}>{capitalize(Effect.Deny)}</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleForm;
|
@ -29,6 +29,7 @@ import { addRole, getPolicies } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import RichTextEditor from '../../../components/common/rich-text-editor/RichTextEditor';
|
||||
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { GlobalSettingOptions } from '../../../constants/globalSettings.constants';
|
||||
import { ADD_ROLE_TEXT } from '../../../constants/HelperTextUtil';
|
||||
import { Policy } from '../../../generated/entity/policies/policy';
|
||||
import {
|
||||
getPath,
|
||||
@ -101,7 +102,7 @@ const AddRolePage = () => {
|
||||
|
||||
return (
|
||||
<Row className="tw-bg-body-main tw-h-full" gutter={[16, 16]}>
|
||||
<Col offset={5} span={14}>
|
||||
<Col offset={4} span={12}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumb} />
|
||||
<Card>
|
||||
<Typography.Paragraph className="tw-text-base">
|
||||
@ -170,6 +171,12 @@ const AddRolePage = () => {
|
||||
</Form>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col className="tw-mt-4" span={4}>
|
||||
<Typography.Paragraph className="tw-text-base tw-font-medium">
|
||||
Add Role
|
||||
</Typography.Paragraph>
|
||||
<Typography.Text>{ADD_ROLE_TEXT}</Typography.Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -11,11 +11,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Empty, Table, Tabs } from 'antd';
|
||||
import { Button, Empty, Modal, Table, Tabs, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import { EntityReference } from 'Models';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
@ -37,11 +37,14 @@ import {
|
||||
getSettingPath,
|
||||
getTeamsWithFqnPath,
|
||||
} from '../../../utils/RouterUtils';
|
||||
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import './RolesDetail.less';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
type Attribute = 'policies' | 'teams' | 'users';
|
||||
|
||||
const List = ({
|
||||
list,
|
||||
type,
|
||||
@ -101,7 +104,7 @@ const List = ({
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<Button type="text" onClick={() => onDelete(record)}>
|
||||
Remove
|
||||
<SVGIcons alt="remove" icon={Icons.ICON_REMOVE} title="Remove" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
@ -127,6 +130,8 @@ const RolesDetailPage = () => {
|
||||
const [role, setRole] = useState<Role>({} as Role);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [editDescription, setEditDescription] = useState<boolean>(false);
|
||||
const [selectedEntity, setEntity] =
|
||||
useState<{ attribute: Attribute; record: EntityReference }>();
|
||||
|
||||
const rolesPath = getSettingPath(
|
||||
GlobalSettingsMenuCategory.ACCESS,
|
||||
@ -171,10 +176,7 @@ const RolesDetailPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (
|
||||
data: EntityReference,
|
||||
attribute: 'policies' | 'teams' | 'users'
|
||||
) => {
|
||||
const handleDelete = async (data: EntityReference, attribute: Attribute) => {
|
||||
const attributeData =
|
||||
(role[attribute as keyof Role] as EntityReference[]) ?? [];
|
||||
const updatedAttributeData = attributeData.filter(
|
||||
@ -232,26 +234,49 @@ const RolesDetailPage = () => {
|
||||
<List
|
||||
list={role.policies ?? []}
|
||||
type="policy"
|
||||
onDelete={(record) => handleDelete(record, 'policies')}
|
||||
onDelete={(record) =>
|
||||
setEntity({ record, attribute: 'policies' })
|
||||
}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="teams" tab="Teams">
|
||||
<List
|
||||
list={role.teams ?? []}
|
||||
type="team"
|
||||
onDelete={(record) => handleDelete(record, 'teams')}
|
||||
onDelete={(record) => setEntity({ record, attribute: 'teams' })}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane key="users" tab="Users">
|
||||
<List
|
||||
list={role.users ?? []}
|
||||
type="user"
|
||||
onDelete={(record) => handleDelete(record, 'users')}
|
||||
onDelete={(record) => setEntity({ record, attribute: 'users' })}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
{selectedEntity && (
|
||||
<Modal
|
||||
centered
|
||||
okText="Confirm"
|
||||
title={`Remove ${getEntityName(
|
||||
selectedEntity.record
|
||||
)} from ${getEntityName(role)}`}
|
||||
visible={!isUndefined(selectedEntity.record)}
|
||||
onCancel={() => setEntity(undefined)}
|
||||
onOk={() => {
|
||||
handleDelete(selectedEntity.record, selectedEntity.attribute);
|
||||
setEntity(undefined);
|
||||
}}>
|
||||
<Typography.Text>
|
||||
Are you sure you want to remove the{' '}
|
||||
{`${getEntityName(selectedEntity.record)} from ${getEntityName(
|
||||
role
|
||||
)}?`}
|
||||
</Typography.Text>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,8 +16,13 @@ import { AxiosError } from 'axios';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getRoles } from '../../../axiosAPIs/rolesAPIV1';
|
||||
import NextPrevious from '../../../components/common/next-previous/NextPrevious';
|
||||
import Loader from '../../../components/Loader/Loader';
|
||||
import { ROUTES } from '../../../constants/constants';
|
||||
import {
|
||||
INITIAL_PAGING_VALUE,
|
||||
PAGE_SIZE,
|
||||
ROUTES,
|
||||
} from '../../../constants/constants';
|
||||
import { Role } from '../../../generated/entity/teams/role';
|
||||
import { Paging } from '../../../generated/type/paging';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
@ -29,6 +34,8 @@ const RolesListPage = () => {
|
||||
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [paging, setPaging] = useState<Paging>();
|
||||
const [currentPage, setCurrentPage] = useState<number>(INITIAL_PAGING_VALUE);
|
||||
|
||||
const fetchRoles = async (paging?: Paging) => {
|
||||
setIsLoading(true);
|
||||
@ -40,6 +47,7 @@ const RolesListPage = () => {
|
||||
);
|
||||
|
||||
setRoles(data.data || []);
|
||||
setPaging(data.paging);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
@ -51,6 +59,11 @@ const RolesListPage = () => {
|
||||
history.push(ROUTES.ADD_ROLE);
|
||||
};
|
||||
|
||||
const handlePaging = (_: string | number, activePage?: number) => {
|
||||
setCurrentPage(activePage ?? INITIAL_PAGING_VALUE);
|
||||
fetchRoles(paging);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
}, []);
|
||||
@ -69,6 +82,17 @@ const RolesListPage = () => {
|
||||
<Col span={24}>
|
||||
<RolesList fetchRoles={fetchRoles} roles={roles} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
{paging && paging.total > PAGE_SIZE && (
|
||||
<NextPrevious
|
||||
currentPage={currentPage}
|
||||
pageSize={PAGE_SIZE}
|
||||
paging={paging}
|
||||
pagingHandler={handlePaging}
|
||||
totalCount={paging.total}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -183,6 +183,17 @@ const AddPolicyPage = withSuspenseFallback(
|
||||
React.lazy(() => import('../pages/PoliciesPage/AddPolicyPage/AddPolicyPage'))
|
||||
);
|
||||
|
||||
const AddRulePage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() => import('../pages/PoliciesPage/PoliciesDetailPage/AddRulePage')
|
||||
)
|
||||
);
|
||||
const EditRulePage = withSuspenseFallback(
|
||||
React.lazy(
|
||||
() => import('../pages/PoliciesPage/PoliciesDetailPage/EditRulePage')
|
||||
)
|
||||
);
|
||||
|
||||
const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
return (
|
||||
<Switch>
|
||||
@ -368,6 +379,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
component={AddPolicyPage}
|
||||
path={ROUTES.ADD_POLICY}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={AddRulePage}
|
||||
path={ROUTES.ADD_POLICY_RULE}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={EditRulePage}
|
||||
path={ROUTES.EDIT_POLICY_RULE}
|
||||
/>
|
||||
|
||||
<Route exact component={GlobalSettingPage} path={ROUTES.SETTINGS} />
|
||||
<Route
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
PLACEHOLDER_ROUTE_SERVICE_CAT,
|
||||
PLACEHOLDER_ROUTE_SERVICE_FQN,
|
||||
PLACEHOLDER_ROUTE_TAB,
|
||||
PLACEHOLDER_RULE_NAME,
|
||||
PLACEHOLDER_SETTING_CATEGORY,
|
||||
ROUTES,
|
||||
} from '../constants/constants';
|
||||
@ -301,3 +302,21 @@ export const getProfilerDashboardWithFqnPath = (entityTypeFQN: string) => {
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getAddPolicyRulePath = (fqn: string) => {
|
||||
let path = ROUTES.ADD_POLICY_RULE;
|
||||
|
||||
path = path.replace(PLACEHOLDER_ROUTE_FQN, fqn);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getEditPolicyRulePath = (fqn: string, ruleName: string) => {
|
||||
let path = ROUTES.EDIT_POLICY_RULE;
|
||||
|
||||
path = path
|
||||
.replace(PLACEHOLDER_ROUTE_FQN, fqn)
|
||||
.replace(PLACEHOLDER_RULE_NAME, ruleName);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
@ -136,6 +136,7 @@ import IconProfilerColor from '../assets/svg/profiler-color.svg';
|
||||
import IconProfiler from '../assets/svg/profiler.svg';
|
||||
import IconHelpCircle from '../assets/svg/question-circle.svg';
|
||||
import IconReaction from '../assets/svg/Reaction.svg';
|
||||
import IconRemove from '../assets/svg/Remove.svg';
|
||||
import IconReplyFeed from '../assets/svg/Reply.svg';
|
||||
import IconRequest from '../assets/svg/request-icon.svg';
|
||||
import IconSampleDataColor from '../assets/svg/sample-data-colored.svg';
|
||||
@ -329,6 +330,7 @@ export const Icons = {
|
||||
FOREGIN_KEY: 'foreign-key',
|
||||
ROLE_GREY: 'role-grey',
|
||||
POLICIES: 'policies',
|
||||
ICON_REMOVE: 'icon-remove',
|
||||
};
|
||||
|
||||
const SVGIcons: FunctionComponent<Props> = ({
|
||||
@ -963,6 +965,10 @@ const SVGIcons: FunctionComponent<Props> = ({
|
||||
case Icons.POLICIES:
|
||||
IconComponent = IconPolicies;
|
||||
|
||||
break;
|
||||
case Icons.ICON_REMOVE:
|
||||
IconComponent = IconRemove;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
Loading…
x
Reference in New Issue
Block a user