diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties
index 1c103c695..749267367 100644
--- a/backend/src/main/resources/i18n/messages.properties
+++ b/backend/src/main/resources/i18n/messages.properties
@@ -66,6 +66,10 @@ action.custom-entity-attributes=Custom Entity Attributes
action.enable=Enable
action.disable=Disable
+action.add-new-role=Add new role
+action.roles=Roles
+action.source-role=Role
+
value.enabled=Enabled
value.disabled=Disabled
value.current=Current
@@ -483,6 +487,16 @@ label.by=By
label.source=Metadata Source
label.provider=Metadata Provider
+label.roles-management=Role Management
+label.new-role=New Role
+label.role-name=Role Name
+label.role-description=Role Description
+label.role=Role
+
+message.delete-role-title=Delete Role?
+
+message.delete-role-body=You are requesting to delete a role. If you complete this process the role will be removed. This cannot be undone. Do you wish to continue?
+
message.delete-user-title=Delete User?
message.delete-user-body=You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue?
@@ -669,4 +683,7 @@ tooltip.match=A regular expression against which the entityID is evaluated.
tooltip.remove-existing-formats=Whether to remove any existing formats from a role if any are added by the filter (unmodified roles will be untouched regardless of this setting)
tooltip.nameid-formats-format=Format
tooltip.nameid-formats-value=Value
-tooltip.nameid-formats-type=Type
\ No newline at end of file
+tooltip.nameid-formats-type=Type
+
+tooltip.role-name=Role Name
+tooltip.role-description=Role Description
\ No newline at end of file
diff --git a/ui/public/assets/schema/roles/role.json b/ui/public/assets/schema/roles/role.json
new file mode 100644
index 000000000..8145fae88
--- /dev/null
+++ b/ui/public/assets/schema/roles/role.json
@@ -0,0 +1,15 @@
+{
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "title": "label.role-name",
+ "description": "tooltip.role-name",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/App.js b/ui/src/app/App.js
index 5d0c4e1e4..c65324bb9 100644
--- a/ui/src/app/App.js
+++ b/ui/src/app/App.js
@@ -26,6 +26,7 @@ import { NewProvider } from './metadata/new/NewProvider';
import { Filter } from './metadata/Filter';
import { Contention } from './metadata/contention/ContentionContext';
import { SessionModal } from './core/user/SessionModal';
+import { Roles } from './admin/Roles';
import Button from 'react-bootstrap/Button';
@@ -79,6 +80,7 @@ function App() {
+
diff --git a/ui/src/app/admin/Roles.js b/ui/src/app/admin/Roles.js
new file mode 100644
index 000000000..08daed90d
--- /dev/null
+++ b/ui/src/app/admin/Roles.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom';
+import { RolesProvider } from './hoc/RolesProvider';
+import { NewRole } from './container/NewRole';
+import { EditRole } from './container/EditRole';
+import { RoleList } from './container/RoleList';
+
+export function Roles() {
+
+ let { path } = useRouteMatch();
+
+ return (
+ <>
+
+
+
+ {(roles, onDelete) =>
+
+ }
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/component/RoleForm.js b/ui/src/app/admin/component/RoleForm.js
new file mode 100644
index 000000000..074cb5b09
--- /dev/null
+++ b/ui/src/app/admin/component/RoleForm.js
@@ -0,0 +1,68 @@
+import React from 'react';
+import Button from 'react-bootstrap/Button';
+import Form from '@rjsf/bootstrap-4';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons';
+import Translate from '../../i18n/components/translate';
+
+import { useRoleUiSchema } from '../hooks';
+import { fields, widgets } from '../../form/component';
+import { templates } from '../../form/component';
+import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager';
+
+function ErrorListTemplate() {
+ return (<>>);
+}
+
+export function RoleForm({ role = {}, errors = [], loading = false, schema, onSave, onCancel }) {
+
+ const { dispatch } = React.useContext(FormContext);
+ const onChange = ({ formData, errors }) => {
+ dispatch(setFormDataAction(formData));
+ dispatch(setFormErrorAction(errors));
+ };
+
+ const uiSchema = useRoleUiSchema();
+
+ return (<>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >)
+}
+/**/
\ No newline at end of file
diff --git a/ui/src/app/admin/container/EditRole.js b/ui/src/app/admin/container/EditRole.js
new file mode 100644
index 000000000..cc8d802ed
--- /dev/null
+++ b/ui/src/app/admin/container/EditRole.js
@@ -0,0 +1,91 @@
+import React from 'react';
+
+import { Prompt, useHistory } from 'react-router';
+import { useParams } from 'react-router-dom';
+import Translate from '../../i18n/components/translate';
+import { useRoles } from '../hooks';
+import { Schema } from '../../form/Schema';
+import { FormManager } from '../../form/FormManager';
+
+import { RoleForm } from '../component/RoleForm';
+import { RoleProvider } from '../hoc/RoleProvider';
+import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications';
+import { useTranslator } from '../../i18n/hooks';
+
+export function EditRole() {
+
+ const { id } = useParams();
+
+ const notifier = useNotificationDispatcher();
+ const translator = useTranslator();
+
+ const history = useHistory();
+
+ const { put, response, loading } = useRoles();
+
+ const [blocking, setBlocking] = React.useState(false);
+
+ async function save(role) {
+ let toast;
+ const resp = await put(``, role);
+ if (response.ok) {
+ gotoDetail({ refresh: true });
+ toast = createNotificationAction(`Updated role successfully.`, NotificationTypes.SUCCESS);
+ } else {
+ toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR);
+ }
+ if (toast) {
+ notifier(toast);
+ }
+ };
+
+ const cancel = () => {
+ gotoDetail();
+ };
+
+ const gotoDetail = (state = null) => {
+ setBlocking(false);
+ history.push(`/roles`, state);
+ };
+
+ return (
+