From 76209a61986afd1416e8e02ca71b715452da7233 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Tue, 18 Oct 2022 13:28:51 -0700 Subject: [PATCH] Implemented support for additional approvers in UI --- .../main/resources/i18n/messages.properties | 8 ++- ui/src/app/admin/container/ApprovalActions.js | 12 ++-- ui/src/app/core/user/UserContext.js | 11 ++++ ui/src/app/dashboard/component/Scroller.js | 6 +- ui/src/app/dashboard/view/ActionsTab.js | 59 +++++++++-------- ui/src/app/dashboard/view/Dashboard.js | 64 ++++++++++--------- .../component/widgets/MultiSelectWidget.js | 1 + .../domain/source/component/SourceList.js | 22 ++++++- ui/src/app/metadata/hooks/api.js | 6 ++ ui/src/app/metadata/view/MetadataOptions.js | 2 +- 10 files changed, 121 insertions(+), 70 deletions(-) diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 2c50bc6b8..812ae4383 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -798,4 +798,10 @@ value.algorithm-cbc-192=CBC (192) - http://www.w3.org/2001/04/xmlenc#aes192-cbc value.algorithm-cbc-128=CBC (128) - http://www.w3.org/2001/04/xmlenc#aes128-cbc value.algorithm-cbc-tripledes=CBC (TRIPLEDES) - http://www.w3.org/2001/04/xmlenc#tripledes-cbc -message.algorithms-unique=Each algorithm may only be used once. \ No newline at end of file +message.algorithms-unique=Each algorithm may only be used once. + +label.approve=Approve +label.disapprove=Unapprove +label.approval=Approval +value.approved=Approved +value.disapproved=Not Approved \ No newline at end of file diff --git a/ui/src/app/admin/container/ApprovalActions.js b/ui/src/app/admin/container/ApprovalActions.js index 13e1a605e..2d4119727 100644 --- a/ui/src/app/admin/container/ApprovalActions.js +++ b/ui/src/app/admin/container/ApprovalActions.js @@ -1,24 +1,24 @@ import React from 'react'; import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; -import { useMetadataActivator, useMetadataEntity } from '../../metadata/hooks/api'; +import { useMetadataApprover, useMetadataEntity } from '../../metadata/hooks/api'; import { NotificationContext, createNotificationAction, NotificationTypes } from '../../notifications/hoc/Notifications'; -export function ApprovalActions ({type, children}) { +export function ApprovalActions ({type = 'source', children}) { const { dispatch } = React.useContext(NotificationContext); - const { del, response } = useMetadataEntity(type, { + const { del, response } = useMetadataEntity('source', { cachePolicy: 'no-cache' }); - const activator = useMetadataActivator(type); + const activator = useMetadataApprover('source'); async function approveEntity(entity, enabled, cb = () => {}) { - await activator.patch(`/${type === 'source' ? entity.id : entity.resourceId}/${enabled ? 'approve' : 'unapprove'}`); + await activator.patch(`${type === 'source' ? entity.id : entity.resourceId}/${enabled ? 'approve' : 'unapprove'}`); if (activator?.response.ok) { dispatch(createNotificationAction( - `Metadata ${type} has been ${enabled ? 'enabled' : 'disabled'}.` + `Metadata ${type} has been ${enabled ? 'approved' : 'unapproved'}.` )); cb(); } else { diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index ddd1897ef..2390c4d3a 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -75,6 +75,15 @@ function useCanEnable() { return isAdmin || isEnabler; } +function useCanApprove() { + return true; +} + +function useIsApprover() { + const user = useCurrentUser(); + return user.canApprove; +} + function useUserGroup() { const user = useCurrentUser(); return (user?.userGroups && user.userGroups.length > 0) ? user.userGroups[0] : null; @@ -103,7 +112,9 @@ export { useCurrentUser, useIsAdmin, useIsAdminOrInGroup, + useIsApprover, useCanEnable, + useCanApprove, useCurrentUserLoading, useCurrentUserLoader, useUserGroupRegexValidator, diff --git a/ui/src/app/dashboard/component/Scroller.js b/ui/src/app/dashboard/component/Scroller.js index 10baf42cf..ee3626bae 100644 --- a/ui/src/app/dashboard/component/Scroller.js +++ b/ui/src/app/dashboard/component/Scroller.js @@ -13,7 +13,7 @@ export function Scroller ({ entities, children }) { React.useEffect(() => { let maxIndex = (page * PAGE_LIMIT) - 1, minIndex = 0; - const l = entities.filter((resolver, index) => (maxIndex >= index && index >= minIndex)); + const l = entities?.filter((resolver, index) => (maxIndex >= index && index >= minIndex)); setLimited(l); }, [entities, page]) @@ -23,9 +23,9 @@ export function Scroller ({ entities, children }) { return ( loadNext()} - hasMore={entities.length > limited.length} + hasMore={entities?.length > limited?.length} > { children(limited) } diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js index a642ec001..9a3ace211 100644 --- a/ui/src/app/dashboard/view/ActionsTab.js +++ b/ui/src/app/dashboard/view/ActionsTab.js @@ -1,19 +1,18 @@ import React from 'react'; -import { useParams } from 'react-router'; import { MetadataActions } from '../../admin/container/MetadataActions'; import UserActions from '../../admin/container/UserActions'; import Spinner from '../../core/components/Spinner'; import Translate from '../../i18n/components/translate'; import SourceList from '../../metadata/domain/source/component/SourceList'; - +import { ProtectRoute } from '../../core/components/ProtectRoute'; import Nav from 'react-bootstrap/Nav'; import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; import Badge from 'react-bootstrap/Badge'; import { ApprovalActions } from '../../admin/container/ApprovalActions'; -export function ActionsTab({ sources, users, reloadSources, reloadUsers, reloadApprovals, loadingSources, loadingUsers, loadingApprovals }) { +export function ActionsTab({ sources, users, approvals, reloadSources, reloadUsers, reloadApprovals, loadingSources, loadingUsers, loadingApprovals }) { const { path, url } = useRouteMatch(); @@ -33,54 +32,62 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, reloadA activeKey="/home" onSelect={(selectedKey) => alert(`selected ${selectedKey}`)} > + {sources !== null && - + Enable Metadata Sources { sources.length ? {sources.length} : '' } + } - - User Access Request - { users.length ? {users.length} : '' } + + Approve Metadata Sources + {users !== null && - - Approve Metadata Sources + + User Access Request + { users.length ? {users.length} : '' } + }
- + - - - {(enable) => - enable(s, e, reloadSources)}> - {loadingSources &&
} -
- } -
-
- - - {loadingUsers &&
} -
- } /> - + {(approve) => - approve(s, e, reloadApprovals)}> + approve(s, e, reloadApprovals)}> {loadingApprovals &&
}
}
} /> + + + + {(enable) => + enable(s, e, reloadSources)}> + {loadingSources &&
} +
+ } +
+
+
+ + + + {loadingUsers &&
} +
+
+ } />
diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js index a4ce21669..76e9c5cbe 100644 --- a/ui/src/app/dashboard/view/Dashboard.js +++ b/ui/src/app/dashboard/view/Dashboard.js @@ -11,7 +11,7 @@ import { SourcesTab } from './SourcesTab'; import { ProvidersTab } from './ProvidersTab'; import { AdminTab } from './AdminTab'; import { ActionsTab } from './ActionsTab'; -import { useCurrentUserLoading, useIsAdmin } from '../../core/user/UserContext'; +import { useCurrentUserLoading, useIsAdmin, useIsApprover } from '../../core/user/UserContext'; import useFetch from 'use-http'; import API_BASE_PATH from '../../App.constant'; import { useNonAdminSources, useUnapprovedSources} from '../../metadata/hooks/api'; @@ -25,12 +25,13 @@ export function Dashboard () { const location = useLocation(); const isAdmin = useIsAdmin(); + const isApprover = useIsApprover(); const loadingUser = useCurrentUserLoading(); const [actions, setActions] = React.useState(0); - const [users, setUsers] = React.useState([]); - const [sources, setSources] = React.useState([]); + const [users, setUsers] = React.useState(null); + const [sources, setSources] = React.useState(null); const [approvals, setApprovals] = React.useState([]); const { get, response, loading } = useFetch(`${API_BASE_PATH}`, { @@ -49,28 +50,30 @@ export function Dashboard () { async function loadSources() { const s = await sourceLoader.get(); - if (response.ok) { + if (sourceLoader.response.ok) { setSources(s); } } async function loadApprovals() { - const s = await approvalLoader.get(); - if (response.ok) { - setApprovals(s); + const a = await approvalLoader.get(); + if (approvalLoader.response.ok) { + setApprovals(a); } } /*eslint-disable react-hooks/exhaustive-deps*/ React.useEffect(() => { - loadSources(); - loadUsers(); + if (isAdmin) { + loadSources(); + loadUsers(); + } loadApprovals(); }, [location]); React.useEffect(() => { - setActions(users.length + sources.length + approvals.length); - }, [users, sources]); + setActions((users?.length || 0) + (sources?.length || 0) + approvals.length); + }, [users, sources, approvals]); return (
@@ -97,14 +100,16 @@ export function Dashboard () { Admin - - - Action Required - {actions} - - } + {isApprover && + + + Action Required + {actions} + + + } @@ -112,24 +117,23 @@ export function Dashboard () { + + + } /> } /> - - - - - } /> diff --git a/ui/src/app/form/component/widgets/MultiSelectWidget.js b/ui/src/app/form/component/widgets/MultiSelectWidget.js index 4184df094..0752fcaf2 100644 --- a/ui/src/app/form/component/widgets/MultiSelectWidget.js +++ b/ui/src/app/form/component/widgets/MultiSelectWidget.js @@ -63,6 +63,7 @@ const MultiSelectWidget = ({ labelKey={ (option) => enumNames[enums.indexOf(option)] } onChange={ onChange } options={enums} + multiple placeholder="Choose approval groups..." selected={value} /> diff --git a/ui/src/app/metadata/domain/source/component/SourceList.js b/ui/src/app/metadata/domain/source/component/SourceList.js index 8e8a10825..6ae64eba9 100644 --- a/ui/src/app/metadata/domain/source/component/SourceList.js +++ b/ui/src/app/metadata/domain/source/component/SourceList.js @@ -16,7 +16,7 @@ import { useTranslator } from '../../../../i18n/hooks'; import { useCanEnable, useIsAdmin } from '../../../../core/user/UserContext'; import { GroupsProvider } from '../../../../admin/hoc/GroupsProvider'; -export default function SourceList({ entities, onDelete, onEnable, onChangeGroup, children }) { +export default function SourceList({ entities, onDelete, onEnable, onApprove, onChangeGroup, children }) { const translator = useTranslator(); const isAdmin = useIsAdmin(); @@ -24,7 +24,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup return ( - + {(limited) =>
@@ -35,6 +35,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup + {onApprove && } {isAdmin && onChangeGroup && } {onDelete && isAdmin && + {onApprove && + + } {isAdmin && onChangeGroup &&
Author Created Date EnabledApprovalGroup @@ -61,7 +62,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup - {onEnable && canEnable ? + {onEnable && (canEnable || source.approved) ? + + onApprove(source, checked)} + checked={source.approved} + > + + + diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 04cb14563..4caa66092 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -145,6 +145,12 @@ export function useMetadataActivator(type, opts = { return useFetch(`${API_BASE_PATH}/activate/${type === 'source' ? 'entityDescriptor' : 'MetadataResolvers'}/`, opts); } +export function useMetadataApprover(type, opts = { + cachePolicy: 'no-cache' +}) { + return useFetch(`${API_BASE_PATH}/approve/${type === 'source' ? 'entityDescriptor' : 'MetadataResolvers'}/`, opts); +} + export function useFilterActivator(providerId, opts = { cachePolicy: 'no-cache' }) { diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js index 60a075f1f..f9fed4bf8 100644 --- a/ui/src/app/metadata/view/MetadataOptions.js +++ b/ui/src/app/metadata/view/MetadataOptions.js @@ -69,7 +69,7 @@ export function MetadataOptions ({reload}) { model={metadata} showGroup={type === 'source'}>
- {enable && canEnable && + {enable && (canEnable || metadata.approved) &&