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
Author |
Created Date |
Enabled |
+ {onApprove && Approval | }
{isAdmin && onChangeGroup && Group | }
{onDelete && isAdmin &&
@@ -61,7 +62,7 @@ export default function SourceList({ entities, onDelete, onEnable, onChangeGroup
| |
- {onEnable && canEnable ?
+ {onEnable && (canEnable || source.approved) ?
|
+ {onApprove &&
+
+
+ onApprove(source, checked)}
+ checked={source.approved}
+ >
+
+
+ |
+ }
{isAdmin && onChangeGroup &&
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) &&
|