Feat : Add New roles and policy UI - Part 1 (#6651)

*  Feat : Add New roles and policy UI

* Add rules column

* Remove rules column

* Add roles detail page

* Add policy details page

* Fix table columns width

* Add roles column to policies table

* Minor change

* Addressing review comments

* Add support for add role
This commit is contained in:
Sachin Chaurasiya 2022-08-10 20:28:19 +05:30 committed by GitHub
parent 455c2d039e
commit 18d204d883
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1179 additions and 3 deletions

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 16.0011C11.9333 16.0011 11.8667 15.9878 11.804 15.9611C11.6493 15.8951 8 14.3058 8 11.0371V8.35847C8 8.14447 8.136 7.95447 8.33933 7.88514L11.8393 6.69447C11.9433 6.65914 12.0567 6.65914 12.1607 6.69447L15.6607 7.88514C15.864 7.95447 16 8.14447 16 8.35847V11.0371C16 14.3058 12.3507 15.8951 12.196 15.9618C12.1333 15.9878 12.0667 16.0011 12 16.0011V16.0011ZM9 8.71647V11.0365C9 13.2585 11.304 14.5945 12 14.9465C12.696 14.5945 15 13.2585 15 11.0365V8.71647L12 7.6958L9 8.71647Z" fill="#37352F"/>
<path d="M11.8332 12.9987C11.7012 12.9987 11.5732 12.9467 11.4798 12.852L10.1465 11.5187C9.95117 11.3233 9.95117 11.0067 10.1465 10.8113C10.3418 10.616 10.6585 10.616 10.8538 10.8113L11.7798 11.7373L13.4332 9.53198C13.5972 9.31132 13.9118 9.26598 14.1332 9.43265C14.3538 9.59798 14.3985 9.91132 14.2325 10.1327L12.2325 12.7993C12.1458 12.9153 12.0132 12.9873 11.8678 12.998C11.8565 12.998 11.8452 12.9987 11.8332 12.9987Z" fill="#37352F"/>
<path d="M7.06 14H1.83333C0.822 14 0 13.178 0 12.1667V1.83333C0 0.822 0.822 0 1.83333 0H9.5C10.5113 0 11.3333 0.822 11.3333 1.83333V5.45333C11.3333 5.72933 11.1093 5.95333 10.8333 5.95333C10.5573 5.95333 10.3333 5.72933 10.3333 5.45333V1.83333C10.3333 1.374 9.95933 1 9.5 1H1.83333C1.374 1 1 1.374 1 1.83333V12.1667C1 12.626 1.374 13 1.83333 13H7.06C7.336 13 7.56 13.224 7.56 13.5C7.56 13.776 7.336 14 7.06 14Z" fill="#37352F"/>
<path d="M8.83333 6.33203H2.5C2.224 6.33203 2 6.10803 2 5.83203C2 5.55603 2.224 5.33203 2.5 5.33203H8.83333C9.10933 5.33203 9.33333 5.55603 9.33333 5.83203C9.33333 6.10803 9.10933 6.33203 8.83333 6.33203Z" fill="#37352F"/>
<path d="M6.16667 9H2.5C2.224 9 2 8.776 2 8.5C2 8.224 2.224 8 2.5 8H6.16667C6.44267 8 6.66667 8.224 6.66667 8.5C6.66667 8.776 6.44267 9 6.16667 9Z" fill="#37352F"/>
<path d="M5.5 3.66797H2.5C2.224 3.66797 2 3.44397 2 3.16797C2 2.89197 2.224 2.66797 2.5 2.66797H5.5C5.776 2.66797 6 2.89197 6 3.16797C6 3.44397 5.776 3.66797 5.5 3.66797Z" fill="#37352F"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,88 @@
/*
* Copyright 2021 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 { AxiosResponse } from 'axios';
import { CreateRole } from '../generated/api/teams/createRole';
import { Policy } from '../generated/entity/policies/policy';
import { Role } from '../generated/entity/teams/role';
import { Paging } from '../generated/type/paging';
import APIClient from './index';
export const getRoles = async (
fields: string,
after?: string,
before?: string,
defaultRoles = false,
limit = 10
) => {
const params = {
default: defaultRoles,
limit,
fields,
after,
before,
};
const response = await APIClient.get<{ data: Role[]; paging: Paging }>(
'/roles',
{ params }
);
return response.data;
};
export const getPolicies = async (
fields: string,
after?: string,
before?: string,
limit = 10
) => {
const params = {
limit,
fields,
after,
before,
};
const response = await APIClient.get<{ data: Policy[]; paging: Paging }>(
'/policies',
{ params }
);
return response.data;
};
export const getRoleByName = async (name: string, fields: string) => {
const response = await APIClient.get<Role>(`/roles/name/${name}`, {
params: { fields },
});
return response.data;
};
export const getPolicyByName = async (name: string, fields: string) => {
const response = await APIClient.get<Policy>(`/policies/name/${name}`, {
params: { fields },
});
return response.data;
};
export const addRole = async (data: CreateRole) => {
const dataResponse = await APIClient.post<CreateRole, AxiosResponse<Role>>(
'/roles',
data
);
return dataResponse.data;
};

View File

@ -226,6 +226,7 @@ export const ROUTES = {
TASK_DETAIL: `/tasks/${PLACEHOLDER_TASK_ID}`,
ACTIVITY_PUSH_FEED: '/api/v1/push/feed',
ADD_ROLE: '/settings/access/roles/add-role',
};
export const SOCKET_EVENTS = {

View File

@ -22,6 +22,7 @@ export const GLOBAL_SETTINGS_MENU = [
{ label: 'Users', isProtected: true, icon: Icons.USERS },
{ label: 'Admins', isProtected: true, icon: Icons.USERS },
{ label: 'Roles', isProtected: true, icon: Icons.ROLE_GREY },
{ label: 'Policies', isProtected: true, icon: Icons.POLICIES },
],
},
{

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
.policies-detail {
.roles-list-table {
.ant-table-thead > tr > th {
font-weight: bold;
background: #fff;
}
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
}
table {
border: 1px solid #dde3ea;
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
}
}

View File

@ -0,0 +1,174 @@
/*
* Copyright 2021 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, Col, Empty, Row, Table, Tabs } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { isEmpty, uniqueId } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { getPolicyByName } from '../../../axiosAPIs/rolesAPIV1';
import Description from '../../../components/common/description/Description';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/globalSettings.constants';
import { Policy } from '../../../generated/entity/policies/policy';
import { EntityReference } from '../../../generated/type/entityReference';
import { getEntityName } from '../../../utils/CommonUtils';
import { getRoleWithFqnPath, getSettingPath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import './PoliciesDetail.less';
const { TabPane } = Tabs;
const RolesList = ({ roles }: { roles: EntityReference[] }) => {
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => (
<Link
className="hover:tw-underline tw-cursor-pointer"
to={getRoleWithFqnPath(record.fullyQualifiedName || '')}>
{getEntityName(record)}
</Link>
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
];
}, []);
return (
<Table
className="roles-list-table"
columns={columns}
dataSource={roles}
pagination={false}
size="middle"
/>
);
};
const PoliciesDetailPage = () => {
const { fqn } = useParams<{ fqn: string }>();
const [policy, setPolicy] = useState<Policy>({} as Policy);
const [isLoading, setLoading] = useState<boolean>(false);
const breadcrumb = useMemo(
() => [
{
name: 'Policies',
url: getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.POLICIES
),
},
{
name: fqn,
url: '',
},
],
[fqn]
);
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);
}
};
useEffect(() => {
fetchPolicy();
}, [fqn]);
if (isLoading) {
return <Loader />;
}
return (
<div data-testid="policy-details-container">
<TitleBreadcrumb titleLinks={breadcrumb} />
{isEmpty(policy) ? (
<Empty description={`No policy found for ${fqn}`} />
) : (
<div className="policies-detail" data-testid="policy-details">
<div className="tw--ml-5">
<Description description={policy.description || ''} />
</div>
<Tabs defaultActiveKey="roles">
<TabPane key="roles" tab="Roles">
<RolesList roles={policy.roles ?? []} />
</TabPane>
<TabPane key="teams" tab="Teams">
{isEmpty(policy.teams) ? (
<Empty description="No teams found" />
) : (
<Row gutter={[16, 16]}>
{policy.teams?.map((team) => (
<Col key={uniqueId()} span={6}>
<Card title={getEntityName(team)}>
<RichTextEditorPreviewer
markdown={team.description || ''}
/>
</Card>
</Col>
))}
</Row>
)}
</TabPane>
<TabPane key="rules" tab="Rules">
{isEmpty(policy.rules) ? (
<Empty description="No rules found" />
) : (
<Row gutter={[16, 16]}>
{policy.rules.map((rule) => (
<Col key={uniqueId()} span={8}>
<Card title={rule.name}>
<RichTextEditorPreviewer
markdown={rule.description || ''}
/>
</Card>
</Col>
))}
</Row>
)}
</TabPane>
</Tabs>
</div>
)}
</div>
);
};
export default PoliciesDetailPage;

View File

@ -0,0 +1,35 @@
/*
* 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.
*/
.policies-list-container {
.ant-btn {
border-radius: 4px;
}
}
.policies-list-table {
.ant-table-thead > tr > th {
font-weight: bold;
background: #fff;
}
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
}
table {
border: 1px solid #dde3ea;
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2021 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 { Space, Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { uniqueId } from 'lodash';
import React, { FC, useMemo } from 'react';
import { Link } from 'react-router-dom';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { Policy } from '../../../generated/entity/policies/policy';
import { getEntityName } from '../../../utils/CommonUtils';
import {
getPolicyWithFqnPath,
getRoleWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
interface PolicyListProps {
policies: Policy[];
}
const PoliciesList: FC<PolicyListProps> = ({ policies }) => {
const columns: ColumnsType<Policy> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => (
<Link
className="hover:tw-underline tw-cursor-pointer"
to={getPolicyWithFqnPath(record.fullyQualifiedName || '')}>
{getEntityName(record)}
</Link>
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: 'Roles',
dataIndex: 'roles',
width: '200px',
key: 'roles',
render: (_, record) => {
return record.roles?.length ? (
<Space wrap size={4}>
{record.roles.map((role) => (
<Link
key={uniqueId()}
to={getRoleWithFqnPath(role.fullyQualifiedName || '')}>
{getEntityName(role)}
</Link>
))}
</Space>
) : (
'-- '
);
},
},
{
title: 'Actions',
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: () => {
return <SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />;
},
},
];
}, []);
return (
<Table
className="policies-list-table"
columns={columns}
dataSource={policies}
pagination={false}
size="middle"
/>
);
};
export default PoliciesList;

View File

@ -0,0 +1,66 @@
/*
* Copyright 2021 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, Col, Row, Space } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { getPolicies } from '../../../axiosAPIs/rolesAPIV1';
import Loader from '../../../components/Loader/Loader';
import { Policy } from '../../../generated/entity/policies/policy';
import { Paging } from '../../../generated/type/paging';
import { showErrorToast } from '../../../utils/ToastUtils';
import PoliciesList from './PoliciesList';
import './PoliciesList.less';
const PoliciesListPage = () => {
const [policies, setPolicies] = useState<Policy[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const fetchPolicies = async (paging?: Paging) => {
setIsLoading(true);
try {
const data = await getPolicies(
'owner,location,roles,teams',
paging?.after,
paging?.before
);
setPolicies(data.data || []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchPolicies();
}, []);
return isLoading ? (
<Loader />
) : (
<Row className="policies-list-container" gutter={[16, 16]}>
<Col span={24}>
<Space align="center" className="tw-w-full tw-justify-end" size={16}>
<Button type="primary">Add Policy</Button>
</Space>
</Col>
<Col span={24}>
<PoliciesList policies={policies} />
</Col>
</Row>
);
};
export default PoliciesListPage;

View File

@ -0,0 +1,167 @@
/*
* Copyright 2021 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, Input, Row, Select, Space } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
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,
GlobalSettingsMenuCategory,
} from '../../../constants/globalSettings.constants';
import { Policy } from '../../../generated/entity/policies/policy';
import { getRoleWithFqnPath, getSettingPath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
const { Option } = Select;
const breadcrumb = [
{
name: 'Roles',
url: getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.ROLES
),
},
{
name: 'Add New Role',
url: '',
},
];
const AddRolePage = () => {
const history = useHistory();
const [policies, setPolicies] = useState<Policy[]>([]);
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [selectedPolicies, setSelectedPolicies] = useState<string[]>([]);
const fetchPolicies = async () => {
try {
const data = await getPolicies('owner,location,roles,teams');
setPolicies(data.data || []);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const handleCancel = () => {
history.push(
getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.ROLES
)
);
};
const handleSumbit = async () => {
const data = {
name,
description,
policies: selectedPolicies.map((policy) => ({
id: policy,
type: 'policy',
})),
};
try {
const dataResponse = await addRole(data);
if (dataResponse) {
history.push(getRoleWithFqnPath(dataResponse.fullyQualifiedName || ''));
}
} catch (error) {
showErrorToast(error as AxiosError);
}
};
useEffect(() => {
fetchPolicies();
}, []);
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<TitleBreadcrumb titleLinks={breadcrumb} />
</Col>
<Col span={18}>
<Card title="Add New Role">
<Form
data-testid="role-form"
id="role-form"
layout="vertical"
onFinish={handleSumbit}>
<Form.Item
label="Name:"
name="name"
rules={[
{
required: true,
max: 128,
min: 1,
},
]}>
<Input
placeholder="Role name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Form.Item>
<Form.Item label="Description:" name="description">
<RichTextEditor
height="200px"
initialValue={description}
placeHolder="write your description"
style={{ margin: 0 }}
onTextChange={(value) => setDescription(value)}
/>
</Form.Item>
<Form.Item
label="Select a policy:"
name="policies"
rules={[
{
required: true,
},
]}>
<Select
mode="multiple"
placeholder="Select Policy"
value={selectedPolicies}
onChange={(values) => setSelectedPolicies(values)}>
{policies.map((policy) => (
<Option key={policy.id}>
{policy.displayName || policy.name}
</Option>
))}
</Select>
</Form.Item>
<Space align="center" className="tw-w-full tw-justify-end">
<Button type="link" onClick={handleCancel}>
Cancel
</Button>
<Button form="role-form" htmlType="submit" type="primary">
Submit
</Button>
</Space>
</Form>
</Card>
</Col>
</Row>
);
};
export default AddRolePage;

View File

@ -0,0 +1,31 @@
/*
* 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.
*/
.roles-detail {
.policies-list-table {
.ant-table-thead > tr > th {
font-weight: bold;
background: #fff;
}
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
}
table {
border: 1px solid #dde3ea;
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
}
}

View File

@ -0,0 +1,177 @@
/*
* Copyright 2021 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, Col, Empty, Row, Table, Tabs } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { isEmpty, uniqueId } from 'lodash';
import { EntityReference } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import { getRoleByName } from '../../../axiosAPIs/rolesAPIV1';
import Description from '../../../components/common/description/Description';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/globalSettings.constants';
import { Role } from '../../../generated/entity/teams/role';
import { getEntityName } from '../../../utils/CommonUtils';
import {
getPolicyWithFqnPath,
getSettingPath,
} from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import './RolesDetail.less';
const { TabPane } = Tabs;
const PoliciesList = ({ policies }: { policies: EntityReference[] }) => {
const columns: ColumnsType<EntityReference> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => (
<Link
className="hover:tw-underline tw-cursor-pointer"
to={getPolicyWithFqnPath(record.fullyQualifiedName || '')}>
{getEntityName(record)}
</Link>
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
];
}, []);
return (
<Table
className="policies-list-table"
columns={columns}
dataSource={policies}
pagination={false}
size="middle"
/>
);
};
const RolesDetailPage = () => {
const { fqn } = useParams<{ fqn: string }>();
const [role, setRole] = useState<Role>({} as Role);
const [isLoading, setLoading] = useState<boolean>(false);
const breadcrumb = useMemo(
() => [
{
name: 'Roles',
url: getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.ROLES
),
},
{
name: fqn,
url: '',
},
],
[fqn]
);
const fetchRole = async () => {
setLoading(true);
try {
const data = await getRoleByName(fqn, 'policies,teams,users');
setRole(data ?? ({} as Role));
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRole();
}, [fqn]);
if (isLoading) {
return <Loader />;
}
return (
<div data-testid="role-details-container">
<TitleBreadcrumb titleLinks={breadcrumb} />
{isEmpty(role) ? (
<Empty description={`No roles found for ${fqn}`} />
) : (
<div className="roles-detail" data-testid="role-details">
<div className="tw--ml-5">
<Description description={role.description || ''} />
</div>
<Tabs defaultActiveKey="policies">
<TabPane key="policies" tab="Policies">
<PoliciesList policies={role.policies ?? []} />
</TabPane>
<TabPane key="teams" tab="Teams">
{isEmpty(role.teams) ? (
<Empty description="No teams found" />
) : (
<Row gutter={[16, 16]}>
{role.teams?.map((team) => (
<Col key={uniqueId()} span={6}>
<Card title={getEntityName(team)}>
<RichTextEditorPreviewer
markdown={team.description || ''}
/>
</Card>
</Col>
))}
</Row>
)}
</TabPane>
<TabPane key="users" tab="Users">
{isEmpty(role.users) ? (
<Empty description="No users found" />
) : (
<Row gutter={[16, 16]}>
{role.users?.map((user) => (
<Col key={uniqueId()} span={6}>
<Card title={getEntityName(user)}>
<RichTextEditorPreviewer
markdown={user.description || ''}
/>
</Card>
</Col>
))}
</Row>
)}
</TabPane>
</Tabs>
</div>
)}
</div>
);
};
export default RolesDetailPage;

View File

@ -0,0 +1,35 @@
/*
* 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.
*/
.roles-list-container {
.ant-btn {
border-radius: 4px;
}
}
.roles-list-table {
.ant-table-thead > tr > th {
font-weight: bold;
background: #fff;
}
.ant-table-row .ant-table-cell:first-child,
.ant-table-thead .ant-table-cell:first-child {
padding-left: 16px;
}
table {
border: 1px solid #dde3ea;
box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.12);
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2021 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 { Space, Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { uniqueId } from 'lodash';
import React, { FC, useMemo } from 'react';
import { Link } from 'react-router-dom';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { Role } from '../../../generated/entity/teams/role';
import { getEntityName } from '../../../utils/CommonUtils';
import {
getPolicyWithFqnPath,
getRoleWithFqnPath,
} from '../../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
interface RolesListProps {
roles: Role[];
}
const RolesList: FC<RolesListProps> = ({ roles }) => {
const columns: ColumnsType<Role> = useMemo(() => {
return [
{
title: 'Name',
dataIndex: 'name',
width: '200px',
key: 'name',
render: (_, record) => (
<Link
className="hover:tw-underline tw-cursor-pointer"
to={getRoleWithFqnPath(record.fullyQualifiedName || '')}>
{getEntityName(record)}
</Link>
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
render: (_, record) => (
<RichTextEditorPreviewer markdown={record?.description || ''} />
),
},
{
title: 'Policies',
dataIndex: 'policies',
width: '200px',
key: 'policies',
render: (_, record) => {
return record.policies?.length ? (
<Space wrap size={4}>
{record.policies.map((policy) => (
<Link
key={uniqueId()}
to={getPolicyWithFqnPath(policy.fullyQualifiedName || '')}>
{getEntityName(policy)}
</Link>
))}
</Space>
) : (
'-- '
);
},
},
{
title: 'Actions',
dataIndex: 'actions',
width: '80px',
key: 'actions',
render: () => {
return <SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />;
},
},
];
}, []);
return (
<Table
className="roles-list-table"
columns={columns}
dataSource={roles}
pagination={false}
size="middle"
/>
);
};
export default RolesList;

View File

@ -0,0 +1,76 @@
/*
* Copyright 2021 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, Col, Row, Space } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getRoles } from '../../../axiosAPIs/rolesAPIV1';
import Loader from '../../../components/Loader/Loader';
import { ROUTES } from '../../../constants/constants';
import { Role } from '../../../generated/entity/teams/role';
import { Paging } from '../../../generated/type/paging';
import { showErrorToast } from '../../../utils/ToastUtils';
import RolesList from './RolesList';
import './RolesList.less';
const RolesListPage = () => {
const history = useHistory();
const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const fetchRoles = async (paging?: Paging) => {
setIsLoading(true);
try {
const data = await getRoles(
'policies,teams,users',
paging?.after,
paging?.before
);
setRoles(data.data || []);
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
const handleAddRole = () => {
history.push(ROUTES.ADD_ROLE);
};
useEffect(() => {
fetchRoles();
}, []);
return isLoading ? (
<Loader />
) : (
<Row className="roles-list-container" gutter={[16, 16]}>
<Col span={24}>
<Space align="center" className="tw-w-full tw-justify-end" size={16}>
<Button type="primary" onClick={handleAddRole}>
Add Role
</Button>
</Space>
</Col>
<Col span={24}>
<RolesList roles={roles} />
</Col>
</Row>
);
};
export default RolesListPage;

View File

@ -13,6 +13,7 @@
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { ROUTES } from '../constants/constants';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
@ -35,9 +36,28 @@ const CustomPropertiesPageV1 = withSuspenseFallback(
() => import('../pages/CustomPropertiesPage/CustomPropertiesPageV1')
)
);
const RolesPageComponent = withSuspenseFallback(
React.lazy(() => import('../pages/RolesPage/RolesPage.component'))
const RolesListPage = withSuspenseFallback(
React.lazy(() => import('../pages/RolesPage/RolesListPage/RolesListPage'))
);
const RolesDetailPage = withSuspenseFallback(
React.lazy(() => import('../pages/RolesPage/RolesDetailPage/RolesDetailPage'))
);
const AddRolePage = withSuspenseFallback(
React.lazy(() => import('../pages/RolesPage/AddRolePage/AddRolePage'))
);
const PoliciesDetailPage = withSuspenseFallback(
React.lazy(
() => import('../pages/PoliciesPage/PoliciesDetailPage/PoliciesDetailPage')
)
);
const PoliciesListPage = withSuspenseFallback(
React.lazy(
() => import('../pages/PoliciesPage/PoliciesListPage/PoliciesListPage')
)
);
const UserListPageV1 = withSuspenseFallback(
React.lazy(() => import('../pages/UserListPage/UserListPageV1'))
);
@ -70,14 +90,48 @@ const GlobalSettingRouter = () => {
true
)}
/>
{/* Roles route start
* Do not change the order of these route
*/}
<Route
exact
component={RolesPageComponent}
component={RolesListPage}
path={getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.ROLES
)}
/>
<Route exact component={AddRolePage} path={ROUTES.ADD_ROLE} />
<Route
exact
component={RolesDetailPage}
path={getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.ROLES,
true
)}
/>
{/* Roles route end
* Do not change the order of these route
*/}
<Route
exact
component={PoliciesListPage}
path={getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.POLICIES
)}
/>
<Route
exact
component={PoliciesDetailPage}
path={getSettingPath(
GlobalSettingsMenuCategory.ACCESS,
GlobalSettingOptions.POLICIES,
true
)}
/>
<Route
exact
component={UserListPageV1}

View File

@ -47,6 +47,8 @@ import { Pipeline } from '../generated/entity/data/pipeline';
import { Table } from '../generated/entity/data/table';
import { Topic } from '../generated/entity/data/topic';
import { ThreadTaskStatus, ThreadType } from '../generated/entity/feed/thread';
import { Policy } from '../generated/entity/policies/policy';
import { Role } from '../generated/entity/teams/role';
import { Team } from '../generated/entity/teams/team';
import { EntityReference, User } from '../generated/entity/teams/user';
import { Paging } from '../generated/type/paging';
@ -630,6 +632,8 @@ export const getEntityName = (
| Table
| Pipeline
| Team
| Policy
| Role
) => {
return entity?.displayName || entity?.name || '';
};

View File

@ -239,3 +239,25 @@ export const getTeamsWithFqnPath = (fqn: string) => {
return path;
};
export const getRoleWithFqnPath = (fqn: string) => {
let path = ROUTES.SETTINGS_WITH_TAB_FQN;
path = path
.replace(PLACEHOLDER_SETTING_CATEGORY, GlobalSettingsMenuCategory.ACCESS)
.replace(PLACEHOLDER_ROUTE_TAB, GlobalSettingOptions.ROLES)
.replace(PLACEHOLDER_ROUTE_FQN, fqn);
return path;
};
export const getPolicyWithFqnPath = (fqn: string) => {
let path = ROUTES.SETTINGS_WITH_TAB_FQN;
path = path
.replace(PLACEHOLDER_SETTING_CATEGORY, GlobalSettingsMenuCategory.ACCESS)
.replace(PLACEHOLDER_ROUTE_TAB, GlobalSettingOptions.POLICIES)
.replace(PLACEHOLDER_ROUTE_FQN, fqn);
return path;
};

View File

@ -131,6 +131,7 @@ import IconPipelineGrey from '../assets/svg/pipeline-grey.svg';
import IconPipeline from '../assets/svg/pipeline.svg';
import IconPlusPrimery from '../assets/svg/plus-primery.svg';
import IconPlus from '../assets/svg/plus.svg';
import IconPolicies from '../assets/svg/policies.svg';
import IconProfilerColor from '../assets/svg/profiler-color.svg';
import IconProfiler from '../assets/svg/profiler.svg';
import IconHelpCircle from '../assets/svg/question-circle.svg';
@ -327,6 +328,7 @@ export const Icons = {
TASK_OPEN: 'task-open',
FOREGIN_KEY: 'foreign-key',
ROLE_GREY: 'role-grey',
POLICIES: 'policies',
};
const SVGIcons: FunctionComponent<Props> = ({
@ -957,6 +959,10 @@ const SVGIcons: FunctionComponent<Props> = ({
case Icons.FOREGIN_KEY:
IconComponent = IconForeignKey;
break;
case Icons.POLICIES:
IconComponent = IconPolicies;
break;
default: