UI: Adding Policy UI. (#2259)

* [WIP]UI: Adding Policy UI.

* Integrating API to get Roles and Policy.

* Adding API integration to create and update role

* Adding form for Rule

* changing types

* removing suggested rules

* Changing rule form modal layout.

* Adding support for adding rules

* Adding support for deleting rule

* Adding support for editing rules

* Adding validation for rules

* Adding isAdminOnly check for roles page

* Adding license to new file

* Minor Fix

* replacing break-all with break-words class.

* Addressing review comments
This commit is contained in:
Sachin Chaurasiya 2022-01-22 20:46:45 +05:30 committed by GitHub
parent 6693b4ae40
commit 590a0ab624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1060 additions and 3 deletions

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 { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { Policy } from '../pages/RolesPage/policy.interface';
import { getURLWithQueryFields } from '../utils/APIUtils';
import APIClient from './index';
export const getRoles = (
arrQueryFields?: string | string[]
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields('/roles', arrQueryFields);
return APIClient.get(`${url}${arrQueryFields ? '&' : '?'}limit=1000000`);
};
export const getRoleByName = (
name: string,
arrQueryFields?: string | string[]
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/roles/name/${name}`, arrQueryFields);
return APIClient.get(url);
};
export const createRole = (
data: Record<string, string>
): Promise<AxiosResponse> => {
return APIClient.post('/roles', data);
};
export const updateRole = (
id: string,
patch: Operation[]
): Promise<AxiosResponse> => {
const configOptions = {
headers: { 'Content-type': 'application/json-patch+json' },
};
return APIClient.patch(`/roles/${id}`, patch, configOptions);
};
export const getPolicy = (
id: string,
arrQueryFields?: string | string[]
): Promise<AxiosResponse> => {
const url = getURLWithQueryFields(`/policies/${id}`, arrQueryFields);
return APIClient.get(url);
};
export const updatePolicy = (
data: Pick<Policy, 'name' | 'policyType' | 'rules'>
): Promise<AxiosResponse> => {
return APIClient.put(`/policies`, data);
};

View File

@ -0,0 +1,169 @@
/*
* 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 classNames from 'classnames';
import { isUndefined } from 'lodash';
import { FormErrorData } from 'Models';
import React, { FC, useState } from 'react';
import { RuleAccess } from '../../../enums/rule.enum';
import {
Operation,
Rule,
} from '../../../generated/entity/policies/accessControl/rule';
import { errorMsg } from '../../../utils/CommonUtils';
import { Button } from '../../buttons/Button/Button';
interface AddRuleProps {
header: string;
errorData?: FormErrorData;
initialData: Rule;
isEditing?: boolean;
onCancel: () => void;
onSave: (data: Rule) => void;
onChange?: (data: Rule) => void;
}
const AddRuleModal: FC<AddRuleProps> = ({
onCancel,
header,
initialData,
errorData,
onSave,
isEditing = false,
onChange,
}: AddRuleProps) => {
const [data, setData] = useState<Rule>(initialData);
const [access, setAccess] = useState<RuleAccess>(
initialData.allow ? RuleAccess.ALLOW : RuleAccess.DENY
);
const [isEnabled, setIsEnabled] = useState<boolean>(
Boolean(initialData.enabled)
);
const onChangeHadler = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
e.persist();
let rule = data;
setData((prevState) => {
rule = {
...prevState,
[e.target.name]: e.target.value,
};
return rule;
});
onChange?.(rule);
};
const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const rule = {
...data,
allow: access === RuleAccess.ALLOW,
enabled: isEnabled,
};
onSave(rule);
onChange?.(rule);
};
return (
<dialog className="tw-modal" data-testid="modal-container">
<div className="tw-modal-backdrop" onClick={() => onCancel()} />
<div className="tw-modal-container tw-overflow-y-auto tw-max-h-screen tw-w-120">
<form onSubmit={onSubmitHandler}>
<div className="tw-modal-header">
<p
className="tw-modal-title tw-text-grey-body"
data-testid="header">
{header}
</p>
</div>
<div className="tw-modal-body">
{!isUndefined(initialData.operation) && (
<div className="tw-mb-4">
<label className="tw-form-label required-field">
Operation
</label>
<select
className={classNames(
'tw-text-sm tw-appearance-none tw-border tw-border-main',
'tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight',
'focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover tw-h-10 tw-bg-white',
{ 'tw-cursor-not-allowed tw-opacity-60': isEditing }
)}
disabled={isEditing}
name="operation"
value={data.operation}
onChange={onChangeHadler}>
<option value="">Select Operation</option>
<option value={Operation.UpdateDescription}>
Update Description
</option>
<option value={Operation.UpdateOwner}>Update Owner</option>
<option value={Operation.UpdateTags}>Update Tags</option>
</select>
{errorData?.operation && errorMsg(errorData.operation)}
</div>
)}
<div className="tw-mb-4">
<label className="tw-form-label">Access</label>
<select
required
className="tw-text-sm tw-appearance-none tw-border tw-border-main
tw-rounded tw-w-full tw-py-2 tw-px-3 tw-text-grey-body tw-leading-tight
focus:tw-outline-none focus:tw-border-focus hover:tw-border-hover tw-h-10 tw-bg-white"
name="access"
value={access}
onChange={(e) => setAccess(e.target.value as RuleAccess)}>
<option value={RuleAccess.ALLOW}>ALLOW</option>
<option value={RuleAccess.DENY}>DENY</option>
</select>
</div>
<div className="tw-flex tw-items-center">
<label>Enable</label>
<div
className={classNames(
'toggle-switch tw-ml-4',
isEnabled ? 'open' : null
)}
data-testid="rule-switch"
onClick={() => setIsEnabled((pre) => !pre)}>
<div className="switch" />
</div>
</div>
</div>
<div className="tw-modal-footer" data-testid="cta-container">
<Button
size="regular"
theme="primary"
variant="link"
onClick={onCancel}>
Cancel
</Button>
<Button
data-testid="saveButton"
size="regular"
theme="primary"
type="submit"
variant="contained">
{isEditing ? 'Update' : 'Save'}
</Button>
</div>
</form>
</div>
</dialog>
);
};
export default AddRuleModal;

View File

@ -49,7 +49,9 @@ const Toast = (props: ToastProps) => {
/> />
</div> </div>
<div className="tw-font-semibold tw-self-center tw-px-1">{body}</div> <div className="tw-font-semibold tw-self-center tw-px-1 tw-break-words">
{body}
</div>
<button <button
className="tw-font-semibold" className="tw-font-semibold"
data-testid="dismiss" data-testid="dismiss"

View File

@ -150,6 +150,7 @@ export const ROUTES = {
ONBOARDING: '/onboarding', ONBOARDING: '/onboarding',
INGESTION: '/ingestion', INGESTION: '/ingestion',
USER_LIST: '/user-list', USER_LIST: '/user-list',
ROLES: '/roles',
}; };
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = { export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
@ -271,6 +272,7 @@ export const navLinkDevelop = [
export const navLinkSettings = [ export const navLinkSettings = [
{ name: 'Teams', to: '/teams', disabled: false }, { name: 'Teams', to: '/teams', disabled: false },
{ name: 'Roles', to: '/roles', disabled: false, isAdminOnly: true },
{ name: 'Users', to: '/user-list', disabled: false, isAdminOnly: true }, { name: 'Users', to: '/user-list', disabled: false, isAdminOnly: true },
{ name: 'Tags', to: '/tags', disabled: false }, { name: 'Tags', to: '/tags', disabled: false },
// { name: 'Store', to: '/store', disabled: false }, // { name: 'Store', to: '/store', disabled: false },

View File

@ -0,0 +1,17 @@
/*
* 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.
*/
export enum RuleAccess {
ALLOW = 'allow',
DENY = 'deny',
}

View File

@ -38,6 +38,10 @@ export interface Role {
href?: string; href?: string;
id: string; id: string;
name: string; name: string;
/**
* Policy that is attached to this role.
*/
policy?: EntityReference;
/** /**
* Last update time corresponding to the new version of the entity in Unix epoch time * Last update time corresponding to the new version of the entity in Unix epoch time
* milliseconds. * milliseconds.
@ -47,6 +51,10 @@ export interface Role {
* User who made the update. * User who made the update.
*/ */
updatedBy?: string; updatedBy?: string;
/**
* Users that have this role assigned.
*/
users?: EntityReference[];
/** /**
* Metadata version of the entity. * Metadata version of the entity.
*/ */
@ -93,3 +101,42 @@ export interface FieldChange {
*/ */
oldValue?: any; oldValue?: any;
} }
/**
* Policy that is attached to this role.
*
* This schema defines the EntityReference type used for referencing an entity.
* EntityReference is used for capturing relationships from one entity to another. For
* example, a table has an attribute called database of type EntityReference that captures
* the relationship of a table `belongs to a` database.
*
* Users that have this role assigned.
*/
export interface EntityReference {
/**
* Optional description of entity.
*/
description?: string;
/**
* Display Name that identifies this entity.
*/
displayName?: string;
/**
* Link to the entity resource.
*/
href?: string;
/**
* Unique identifier that identifies an entity instance.
*/
id: string;
/**
* Name of the entity instance. For entities such as tables, databases where the name is not
* unique, fullyQualifiedName is returned in this field.
*/
name?: string;
/**
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
* `dashboardService`...
*/
type: string;
}

View File

@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* 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.
*/
/**
* This schema defines the EntityRelationship type used for establishing relationship
* between two entities. EntityRelationship is used for capturing relationships from one
* entity to another. For example, a database contains tables.
*/
export interface EntityRelationship {
/**
* `true` indicates the relationship has been soft deleted.
*/
deleted?: boolean;
/**
* Type of the entity from which the relationship originates. Examples: `database`, `table`,
* `metrics` ...
*/
fromEntity: string;
/**
* Fully qualified name of the entity from which the relationship originates.
*/
fromFQN?: string;
/**
* Unique identifier that identifies the entity from which the relationship originates.
*/
fromId?: string;
/**
* Describes relationship between the two entities.
*/
relation: string;
/**
* Type of the entity towards which the relationship refers to. Examples: `database`,
* `table`, `metrics` ...
*/
toEntity: string;
/**
* Fully qualified name of the entity towards which the relationship refers to.
*/
toFQN?: string;
/**
* Unique identifier that identifies the entity towards which the relationship refers to.
*/
toId?: string;
}

View File

@ -0,0 +1,665 @@
/*
* 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 { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames';
import { compare } from 'fast-json-patch';
import { isUndefined, toLower } from 'lodash';
import { observer } from 'mobx-react';
import { FormErrorData } from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import {
createRole,
getPolicy,
getRoleByName,
getRoles,
updatePolicy,
updateRole,
} from '../../axiosAPIs/rolesAPI';
import { Button } from '../../components/buttons/Button/Button';
import Description from '../../components/common/description/Description';
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
import NonAdminAction from '../../components/common/non-admin-action/NonAdminAction';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import PageLayout from '../../components/containers/PageLayout';
import Loader from '../../components/Loader/Loader';
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../../components/Modals/FormModal';
import AddRuleModal from '../../components/Modals/RulesModal/AddRuleModal';
import {
ERROR404,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import {
Operation,
Rule,
} from '../../generated/entity/policies/accessControl/rule';
import { Role } from '../../generated/entity/teams/role';
import { EntityReference } from '../../generated/entity/teams/user';
import { useAuth } from '../../hooks/authHooks';
import useToastContext from '../../hooks/useToastContext';
import { getActiveCatClass, isEven } from '../../utils/CommonUtils';
import SVGIcons from '../../utils/SvgUtils';
import Form from '../teams/Form';
import UserCard from '../teams/UserCard';
import { Policy } from './policy.interface';
const getActiveTabClass = (tab: number, currentTab: number) => {
return tab === currentTab ? 'active' : '';
};
const RolesPage = () => {
const showToast = useToastContext();
const [roles, setRoles] = useState<Array<Role>>([]);
const { isAuthDisabled, isAdminUser } = useAuth();
const [currentRole, setCurrentRole] = useState<Role>();
const [currentPolicy, setCurrentPolicy] = useState<Policy>();
const [error, setError] = useState<string>('');
const [currentTab, setCurrentTab] = useState<number>(1);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isLoadingPolicy, setIsLoadingPolicy] = useState<boolean>(false);
const [isAddingRole, setIsAddingRole] = useState<boolean>(false);
const [isAddingRule, setIsAddingRule] = useState<boolean>(false);
const [errorData, setErrorData] = useState<FormErrorData>();
const [isEditable, setIsEditable] = useState<boolean>(false);
const [deletingRule, setDeletingRule] = useState<{
rule: Rule | undefined;
state: boolean;
}>({ rule: undefined, state: false });
const [editingRule, setEditingRule] = useState<{
rule: Rule | undefined;
state: boolean;
}>({ rule: undefined, state: false });
const onNewDataChange = (data: Role, forceSet = false) => {
if (errorData || forceSet) {
const errData: { [key: string]: string } = {};
if (!data.name.trim()) {
errData['name'] = 'Name is required';
} else if (
!isUndefined(
roles.find((item) => toLower(item.name) === toLower(data.name))
)
) {
errData['name'] = 'Name already exists';
} else if (data.name.length < 1 || data.name.length > 128) {
errData['name'] = 'Name size must be between 1 and 128';
}
if (!data.displayName?.trim()) {
errData['displayName'] = 'Display name is required';
} else if (data.displayName.length < 1 || data.displayName.length > 128) {
errData['displayName'] = 'Display name size must be between 1 and 128';
}
setErrorData(errData);
return errData;
}
return {};
};
const validateRuleData = (data: Rule, forceSet = false) => {
if (errorData || forceSet) {
const errData: { [key: string]: string } = {};
if (!data.operation) {
errData['operation'] = 'Operation is required.';
}
setErrorData(errData);
return errData;
}
return {};
};
const onDescriptionEdit = (): void => {
setIsEditable(true);
};
const onCancel = (): void => {
setIsEditable(false);
};
const fetchPolicy = (id: string) => {
setIsLoadingPolicy(true);
getPolicy(
id,
'displayName,description,owner,policyUrl,enabled,rules,location'
)
.then((res: AxiosResponse) => {
setCurrentPolicy(res.data);
})
.catch(() => {
showToast({
variant: 'error',
body: 'Error while getting policy',
});
})
.finally(() => setIsLoadingPolicy(false));
};
const fetchRoles = () => {
setIsLoading(true);
getRoles(['policy', 'users'])
.then((res: AxiosResponse) => {
const { data } = res.data;
setRoles(data);
setCurrentRole(data[0]);
})
.catch(() => {
setError('Error while getting roles');
showToast({
variant: 'error',
body: 'Error while getting roles',
});
})
.finally(() => setIsLoading(false));
};
const createNewRole = (data: Role) => {
const errData = onNewDataChange(data, true);
const { description, name, displayName } = data;
if (!Object.values(errData).length) {
createRole({
description: description as string,
name,
displayName: displayName as string,
})
.then((res: AxiosResponse) => {
if (res.data) {
fetchRoles();
}
})
.catch((error: AxiosError) => {
showToast({
variant: 'error',
body: error.message ?? 'Something went wrong!',
});
})
.finally(() => {
setIsAddingRole(false);
});
}
};
const fetchCurrentRole = (name: string, update = false) => {
if (currentRole?.name !== name || update) {
setIsLoading(true);
getRoleByName(name, ['users', 'policy'])
.then((res: AxiosResponse) => {
setCurrentRole(res.data);
if (roles.length <= 0) {
fetchRoles();
}
})
.catch((err: AxiosError) => {
if (err?.response?.data.code) {
setError(ERROR404);
}
})
.finally(() => {
setIsLoading(false);
});
}
};
const onDescriptionUpdate = (updatedHTML: string) => {
if (currentRole?.description !== updatedHTML) {
const updatedRole = { ...currentRole, description: updatedHTML };
const jsonPatch = compare(currentRole as Role, updatedRole);
updateRole(currentRole?.id as string, jsonPatch).then(
(res: AxiosResponse) => {
if (res.data) {
fetchCurrentRole(res.data.name, true);
}
}
);
setIsEditable(false);
} else {
setIsEditable(false);
}
};
const createRule = (data: Rule) => {
const errData = validateRuleData(data, true);
if (!Object.values(errData).length) {
const newRule = {
...data,
name: `${currentPolicy?.name}-${data.operation}`,
userRoleAttr: currentRole?.name,
};
const updatedPolicy = {
name: currentPolicy?.name as string,
policyType: currentPolicy?.policyType as string,
rules: [...(currentPolicy?.rules as Rule[]), newRule],
};
updatePolicy(updatedPolicy)
.then((res: AxiosResponse) => {
setCurrentPolicy(res.data);
})
.catch((err: AxiosError) => {
showToast({
variant: 'error',
body: err.response?.data?.message ?? 'Error while adding new rule',
});
})
.finally(() => setIsAddingRule(false));
}
};
const onRuleUpdate = (data: Rule) => {
const rules = currentPolicy?.rules?.map((rule) => {
if (rule.name === data.name) {
return data;
} else {
return rule;
}
});
const updatedPolicy = {
name: currentPolicy?.name as string,
policyType: currentPolicy?.policyType as string,
rules: rules as Rule[],
};
updatePolicy(updatedPolicy)
.then((res: AxiosResponse) => {
setCurrentPolicy(res.data);
})
.catch((err: AxiosError) => {
showToast({
variant: 'error',
body:
err.response?.data?.message ??
`Error while updating ${data.name} rule`,
});
})
.finally(() => setEditingRule({ rule: undefined, state: false }));
};
const deleteRule = (data: Rule) => {
const updatedPolicy = {
name: currentPolicy?.name as string,
policyType: currentPolicy?.policyType as string,
rules: currentPolicy?.rules?.filter(
(rule) => rule.operation !== data.operation
) as Rule[],
};
updatePolicy(updatedPolicy)
.then((res: AxiosResponse) => {
setCurrentPolicy(res.data);
})
.catch((err: AxiosError) => {
showToast({
variant: 'error',
body: err.response?.data?.message ?? 'Error while deleting rule',
});
})
.finally(() => {
setDeletingRule({ rule: undefined, state: false });
});
};
const getTabs = () => {
return (
<div className="tw-mb-3 ">
<nav
className="tw-flex tw-flex-row tw-gh-tabs-container"
data-testid="tabs">
<button
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(
1,
currentTab
)}`}
data-testid="users"
onClick={() => {
setCurrentTab(1);
}}>
Policy
</button>
<button
className={`tw-pb-2 tw-px-4 tw-gh-tabs ${getActiveTabClass(
2,
currentTab
)}`}
data-testid="assets"
onClick={() => {
setCurrentTab(2);
}}>
Users
</button>
</nav>
</div>
);
};
const fetchLeftPanel = () => {
return (
<>
<div className="tw-flex tw-justify-between tw-items-center tw-mb-3 tw-border-b">
<h6 className="tw-heading tw-text-base">Roles</h6>
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-7 tw-px-2 tw-mb-4', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
data-testid="add-role"
size="small"
theme="primary"
variant="contained"
onClick={() => {
setErrorData(undefined);
setIsAddingRole(true);
}}>
<i aria-hidden="true" className="fa fa-plus" />
</Button>
</NonAdminAction>
</div>
{roles &&
roles.map((role) => (
<div
className={`tw-group tw-text-grey-body tw-cursor-pointer tw-text-body tw-mb-3 tw-flex tw-justify-between ${getActiveCatClass(
role.name,
currentRole?.name
)}`}
key={role.name}
onClick={() => setCurrentRole(role)}>
<p
className="tag-category label-category tw-self-center tw-truncate tw-w-52"
title={role.displayName}>
{role.displayName}
</p>
</div>
))}
</>
);
};
const getPolicyRules = (rules: Array<Rule>) => {
if (!rules.length) {
return (
<div className="tw-text-center tw-py-5">
<p className="tw-text-base">No Rules Added.</p>
</div>
);
}
return (
<div className="tw-bg-white">
<table className="tw-w-full tw-overflow-x-auto" data-testid="table">
<thead>
<tr className="tableHead-row">
<th className="tableHead-cell" data-testid="heading-description">
Operation
</th>
<th className="tableHead-cell" data-testid="heading-description">
Access
</th>
<th className="tableHead-cell" data-testid="heading-description">
Enabled
</th>
<th className="tableHead-cell" data-testid="heading-description">
Action
</th>
</tr>
</thead>
<tbody className="tw-text-sm" data-testid="table-body">
{rules.map((rule, index) => (
<tr
className={`tableBody-row ${!isEven(index + 1) && 'odd-row'}`}
key={index}>
<td className="tableBody-cell">
<p>{rule.operation}</p>
</td>
<td className="tableBody-cell">
<p
className={classNames(
rule.allow
? 'tw-text-status-success'
: 'tw-text-status-failed'
)}>
{rule.allow ? 'ALLOW' : 'DENY'}
</p>
</td>
<td className="tableBody-cell">
<div
className={classNames(
'toggle-switch tw-ml-4',
rule.enabled ? 'open' : null
)}
data-testid="rule-switch"
onClick={() =>
onRuleUpdate({ ...rule, enabled: !rule.enabled })
}>
<div className="switch" />
</div>
</td>
<td className="tableBody-cell">
<div className="tw-flex">
<span onClick={() => setEditingRule({ rule, state: true })}>
<SVGIcons
alt="icon-edit"
className="tw-cursor-pointer"
icon="icon-edit"
title="Edit"
width="12"
/>
</span>
<span
onClick={() => setDeletingRule({ rule, state: true })}>
<SVGIcons
alt="icon-delete"
className="tw-ml-4 tw-cursor-pointer"
icon="icon-delete"
title="Delete"
width="12"
/>
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
const getRoleUsers = (users: Array<EntityReference>) => {
if (!users.length) {
return (
<div className="tw-text-center tw-py-5">
<p className="tw-text-base">No Users Added.</p>
</div>
);
}
return (
<div className="tw-grid tw-grid-cols-4 tw-gap-x-2">
{users.map((user) => (
<UserCard
isIconVisible
item={{
description: user.displayName as string,
name: user.name as string,
id: user.id,
}}
key={user.id}
/>
))}
</div>
);
};
useEffect(() => {
fetchRoles();
}, []);
useEffect(() => {
if (currentRole) {
fetchPolicy(currentRole?.policy?.id as string);
}
}, [currentRole]);
return (
<>
{error ? (
<ErrorPlaceHolder />
) : (
<PageContainerV1 className="tw-py-4">
<PageLayout leftPanel={fetchLeftPanel()}>
{isLoading ? (
<Loader />
) : (
<div className="tw-pb-3" data-testid="role-container">
{roles.length > 0 ? (
<>
<div
className="tw-flex tw-justify-between tw-items-center"
data-testid="header">
<div className="tw-heading tw-text-link tw-text-base">
{currentRole?.displayName}
</div>
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-3', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
data-testid="add-new-user-button"
size="small"
theme="primary"
variant="contained"
onClick={() => {
setErrorData(undefined);
setIsAddingRule(true);
}}>
Add new rule
</Button>
</NonAdminAction>
</div>
<div
className="tw-mb-3 tw--ml-5"
data-testid="description-container">
<Description
description={currentRole?.description || ''}
entityName={currentRole?.displayName}
isEdit={isEditable}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
/>
</div>
{getTabs()}
{currentTab === 1 ? (
<Fragment>
{isLoadingPolicy ? (
<Loader />
) : (
<>{getPolicyRules(currentPolicy?.rules ?? [])}</>
)}
</Fragment>
) : null}
{currentTab === 2
? getRoleUsers(currentRole?.users ?? [])
: null}
</>
) : (
<ErrorPlaceHolder>
<p className="w-text-lg tw-text-center">No Roles Added.</p>
<p className="w-text-lg tw-text-center">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className="link-text tw-underline"
onClick={() => {
setErrorData(undefined);
setIsAddingRole(true);
}}>
Click here
</button>
{' to add new Role'}
</NonAdminAction>
</p>
</ErrorPlaceHolder>
)}
{isAddingRole && (
<FormModal
errorData={errorData}
form={Form}
header="Adding new role"
initialData={{
name: '',
description: '',
displayName: '',
}}
onCancel={() => setIsAddingRole(false)}
onChange={(data) => onNewDataChange(data as Role)}
onSave={(data) => createNewRole(data as Role)}
/>
)}
{isAddingRule && (
<AddRuleModal
errorData={errorData}
header={`Adding new rule for ${toLower(
currentRole?.displayName
)}`}
initialData={
{ name: '', operation: '' as Operation } as Rule
}
onCancel={() => setIsAddingRule(false)}
onChange={(data) => validateRuleData(data as Rule)}
onSave={createRule}
/>
)}
{editingRule.state && (
<AddRuleModal
isEditing
header={`Edit rule ${editingRule.rule?.name}`}
initialData={editingRule.rule as Rule}
onCancel={() =>
setEditingRule({ rule: undefined, state: false })
}
onSave={onRuleUpdate}
/>
)}
{deletingRule.state && (
<ConfirmationModal
bodyText={`Are you sure want to delete ${deletingRule.rule?.name}?`}
cancelText="Cancel"
confirmText="Confirm"
header="Deleting rule"
onCancel={() =>
setDeletingRule({ rule: undefined, state: false })
}
onConfirm={() => {
deleteRule(deletingRule.rule as Rule);
}}
/>
)}
</div>
)}
</PageLayout>
</PageContainerV1>
)}
</>
);
};
export default observer(RolesPage);

View File

@ -0,0 +1,30 @@
/*
* 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 { Rule } from '../../generated/entity/policies/accessControl/rule';
export interface Policy {
id: string;
name: string;
fullyQualifiedName: string;
displayName: string;
description: string;
href: string;
policyType: string;
enabled: boolean;
version: number;
updatedAt: number;
updatedBy: string;
rules: Rule[];
deleted: boolean;
}

View File

@ -25,6 +25,7 @@ import ExplorePage from '../pages/explore/ExplorePage.component';
import IngestionPage from '../pages/IngestionPage/IngestionPage.component'; import IngestionPage from '../pages/IngestionPage/IngestionPage.component';
import MyDataPage from '../pages/MyDataPage/MyDataPage.component'; import MyDataPage from '../pages/MyDataPage/MyDataPage.component';
import PipelineDetailsPage from '../pages/PipelineDetails/PipelineDetailsPage.component'; import PipelineDetailsPage from '../pages/PipelineDetails/PipelineDetailsPage.component';
import RolesPage from '../pages/RolesPage/RolesPage.component';
import ServicePage from '../pages/service'; import ServicePage from '../pages/service';
import ServicesPage from '../pages/services'; import ServicesPage from '../pages/services';
import SignupPage from '../pages/signup'; import SignupPage from '../pages/signup';
@ -89,7 +90,10 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<Route exact component={EntityVersionPage} path={ROUTES.ENTITY_VERSION} /> <Route exact component={EntityVersionPage} path={ROUTES.ENTITY_VERSION} />
<Route exact component={IngestionPage} path={ROUTES.INGESTION} /> <Route exact component={IngestionPage} path={ROUTES.INGESTION} />
{isAuthDisabled || isAdminUser ? ( {isAuthDisabled || isAdminUser ? (
<Route exact component={UserListPage} path={ROUTES.USER_LIST} /> <>
<Route exact component={RolesPage} path={ROUTES.ROLES} />
<Route exact component={UserListPage} path={ROUTES.USER_LIST} />
</>
) : null} ) : null}
<Redirect to={ROUTES.NOT_FOUND} /> <Redirect to={ROUTES.NOT_FOUND} />

View File

@ -205,7 +205,7 @@
} }
.tw-notification { .tw-notification {
@apply tw-bg-white tw-transition tw-duration-300 tw-ease-linear tw-relative tw-flex tw-justify-between tw-pointer-events-auto @apply tw-bg-white tw-transition tw-duration-300 tw-ease-linear tw-relative tw-flex tw-justify-between tw-pointer-events-auto
tw-mb-3.5 tw-p-4 tw-w-80 tw-h-auto tw-rounded tw-shadow tw-text-white tw-bg-left-top tw-bg-no-repeat; tw-mb-3.5 tw-p-4 tw-w-96 tw-h-auto tw-rounded tw-shadow tw-text-white tw-bg-left-top tw-bg-no-repeat;
} }
/* Toaster CSS end */ /* Toaster CSS end */