diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/Remove.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/Remove.svg new file mode 100644 index 00000000000..f295d76a458 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/Remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts b/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts index d4d465c999b..f566285eaaf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/HelperTextUtil.ts @@ -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. + `; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index 0ca9b57940f..95e70b61093 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -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 = { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/globalSettings.constants.tsx b/openmetadata-ui/src/main/resources/ui/src/constants/globalSettings.constants.tsx index 7e3d8761132..b371c6f70fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/globalSettings.constants.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/constants/globalSettings.constants.tsx @@ -49,7 +49,7 @@ export const GLOBAL_SETTINGS_MENU = [ }, { category: 'Access', - isProtected: false, + isProtected: true, items: [ { label: 'Roles', diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx index 33ee2902e2c..6c385dd5089 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/AddPolicyPage/AddPolicyPage.tsx @@ -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( - [] - ); + const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [ruleData, setRuleData] = useState({ @@ -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 ( - + @@ -217,98 +143,7 @@ const AddPolicyPage = () => { Add Rule - - - setRuleData((prev) => ({ ...prev, name: e.target.value })) - } - /> - - - - setRuleData((prev) => ({ ...prev, description: value })) - } - /> - - - { - setRuleData((prev) => ({ - ...prev, - resources: values, - })); - }} - /> - - - { - setRuleData((prev) => ({ - ...prev, - operations: values, - })); - }} - /> - - - - + + + + + + + + ); +}; + +export default AddRulePage; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/EditRulePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/EditRulePage.tsx new file mode 100644 index 00000000000..332f986f51f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/EditRulePage.tsx @@ -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(false); + const [policy, setPolicy] = useState({} as Policy); + const [ruleData, setRuleData] = useState(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 ; + } + + return ( + + + + + + Edit Rule {`"${ruleName}"`} + +
+ + + + + + +
+ +
+ ); +}; + +export default EditRulePage; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetail.less b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetail.less index 209fd60a074..2d5fdb1d605 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetail.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetail.less @@ -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; + } + } } diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx index 8173ad633e2..d9f2a1c238b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage.tsx @@ -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 ( ); }, @@ -133,6 +138,8 @@ const PoliciesDetailPage = () => { const [policy, setPolicy] = useState({} as Policy); const [isLoading, setLoading] = useState(false); const [editDescription, setEditDescription] = useState(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) ? ( ) : ( - - {policy.rules.map((rule) => ( - - - - - {rule.name} - -
- - Active -
-
+ + + + {policy.rules.map((rule) => ( + + + + + {rule.name} + + + +
+ + Description: + + +
+
+ } + key={rule.name || 'rule'}> + + + + Resources: + + + {rule.resources?.join(', ')} + + -
- - Description: - - -
- - - - Resources: - - - {rule.resources?.join(', ')} - + + + Operations: + + + {rule.operations?.join(', ')} + + + {rule.condition && ( + + + Condition: + + {rule.condition} + + )} - - - - Operations: - - - {rule.operations?.join(', ')} - - - -
- - ))} -
+ + + ))} + + )} handleDelete(record, 'roles')} + onDelete={(record) => setEntity({ record, attribute: 'roles' })} /> handleDelete(record, 'teams')} + onDelete={(record) => setEntity({ record, attribute: 'teams' })} /> )} + {selectedEntity && ( + setEntity(undefined)} + onOk={() => { + handleDelete(selectedEntity.record, selectedEntity.attribute); + setEntity(undefined); + }}> + + Are you sure you want to remove the{' '} + {`${getEntityName(selectedEntity.record)} from ${getEntityName( + policy + )}?`} + + + )} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx index d4b7f592e93..3519634232a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/PoliciesListPage/PoliciesListPage.tsx @@ -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([]); const [isLoading, setIsLoading] = useState(false); + const [paging, setPaging] = useState(); + const [currentPage, setCurrentPage] = useState(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 = () => { + + {paging && paging.total > PAGE_SIZE && ( + + )} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/RuleForm/RuleForm.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/RuleForm/RuleForm.tsx new file mode 100644 index 00000000000..c8f50cb470c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PoliciesPage/RuleForm/RuleForm.tsx @@ -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) => void; +} + +const RuleForm: FC = ({ ruleData, setRuleData }) => { + const [policyResources, setPolicyResources] = useState( + [] + ); + + /** + * 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 ( + <> + + + setRuleData((prev) => ({ ...prev, name: e.target.value })) + } + /> + + + + setRuleData((prev) => ({ ...prev, description: value })) + } + /> + + + { + setRuleData((prev) => ({ + ...prev, + resources: values, + })); + }} + /> + + + { + setRuleData((prev) => ({ + ...prev, + operations: values, + })); + }} + /> + + + + + + ); +}; + +export default RuleForm; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx index 48e36f3a075..05716ef289d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/AddRolePage/AddRolePage.tsx @@ -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 ( - + @@ -170,6 +171,12 @@ const AddRolePage = () => { + + + Add Role + + {ADD_ROLE_TEXT} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx index 8049c80e85b..03959783ebc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesDetailPage/RolesDetailPage.tsx @@ -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 ( ); }, @@ -127,6 +130,8 @@ const RolesDetailPage = () => { const [role, setRole] = useState({} as Role); const [isLoading, setLoading] = useState(false); const [editDescription, setEditDescription] = useState(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 = () => { handleDelete(record, 'policies')} + onDelete={(record) => + setEntity({ record, attribute: 'policies' }) + } /> handleDelete(record, 'teams')} + onDelete={(record) => setEntity({ record, attribute: 'teams' })} /> handleDelete(record, 'users')} + onDelete={(record) => setEntity({ record, attribute: 'users' })} /> )} + {selectedEntity && ( + setEntity(undefined)} + onOk={() => { + handleDelete(selectedEntity.record, selectedEntity.attribute); + setEntity(undefined); + }}> + + Are you sure you want to remove the{' '} + {`${getEntityName(selectedEntity.record)} from ${getEntityName( + role + )}?`} + + + )} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx index 9dc2484906f..82a707847d7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/RolesPage/RolesListPage/RolesListPage.tsx @@ -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([]); const [isLoading, setIsLoading] = useState(false); + const [paging, setPaging] = useState(); + const [currentPage, setCurrentPage] = useState(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 = () => { + + {paging && paging.total > PAGE_SIZE && ( + + )} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx index 81810169a6e..6b275797610 100644 --- a/openmetadata-ui/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/router/AuthenticatedAppRouter.tsx @@ -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 ( @@ -368,6 +379,16 @@ const AuthenticatedAppRouter: FunctionComponent = () => { component={AddPolicyPage} path={ROUTES.ADD_POLICY} /> + + { 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; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index ab946c48c85..b9f59ca6f31 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -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 = ({ @@ -963,6 +965,10 @@ const SVGIcons: FunctionComponent = ({ case Icons.POLICIES: IconComponent = IconPolicies; + break; + case Icons.ICON_REMOVE: + IconComponent = IconRemove; + break; default: