Skip to content

Commit

Permalink
Implemented support for additional approvers in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
rmathis committed Oct 18, 2022
1 parent 7e40fa3 commit 76209a6
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 70 deletions.
8 changes: 7 additions & 1 deletion backend/src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
12 changes: 6 additions & 6 deletions ui/src/app/admin/container/ApprovalActions.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions ui/src/app/core/user/UserContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -103,7 +112,9 @@ export {
useCurrentUser,
useIsAdmin,
useIsAdminOrInGroup,
useIsApprover,
useCanEnable,
useCanApprove,
useCurrentUserLoading,
useCurrentUserLoader,
useUserGroupRegexValidator,
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/dashboard/component/Scroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -23,9 +23,9 @@ export function Scroller ({ entities, children }) {

return (
<InfiniteScroll
dataLength={limited.length} //This is important field to render the next data
dataLength={limited?.length} //This is important field to render the next data
next={() => loadNext()}
hasMore={entities.length > limited.length}
hasMore={entities?.length > limited?.length}
>
{ children(limited) }
</InfiniteScroll>
Expand Down
59 changes: 33 additions & 26 deletions ui/src/app/dashboard/view/ActionsTab.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -33,54 +32,62 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, reloadA
activeKey="/home"
onSelect={(selectedKey) => alert(`selected ${selectedKey}`)}
>
{sources !== null &&
<Nav.Item>
<NavLink className="nav-link" to={`${path}/enable`}>
<NavLink className="nav-link" to={`${path}/enable`} id="enable-btn">
<Translate value="label.enable-metadata-sources">Enable Metadata Sources</Translate>
{ sources.length ? <Badge pill bg="danger" className="ms-1">{sources.length}</Badge> : '' }
</NavLink>
</Nav.Item>
}
<Nav.Item>
<NavLink className="nav-link" to={`${path}/useraccess`}>
<Translate value="label.user-access-request">User Access Request</Translate>
{ users.length ? <Badge pill bg="danger" className="ms-1">{users.length}</Badge> : '' }
<NavLink className="nav-link" to={`${path}/approve`} id="approve-btn">
<Translate value="label.approve-metadata-sources">Approve Metadata Sources</Translate>
</NavLink>
</Nav.Item>
{users !== null &&
<Nav.Item>
<NavLink className="nav-link" to={`${path}/approve`}>
<Translate value="label.approve-metadata-sources">Approve Metadata Sources</Translate>
<NavLink className="nav-link" to={`${path}/useraccess`} id="user-access-btn">
<Translate value="label.user-access-request">User Access Request</Translate>
{ users.length ? <Badge pill bg="danger" className="ms-1">{users.length}</Badge> : '' }
</NavLink>
</Nav.Item>
}
</Nav>
<hr />
</div>
<div className="px-3 pb-0">
<Switch>
<Route exact path={`${path}`}>
<Redirect to={`${url}/enable`} />
<Redirect to={`${url}/approve`} />
</Route>
<Route path={`${path}/enable`}>
<MetadataActions type="source">
{(enable) =>
<SourceList entities={sources} onDelete={reloadSources} onEnable={(s, e) => enable(s, e, reloadSources)}>
{loadingSources && <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> }
</SourceList>
}
</MetadataActions>
</Route>
<Route path={`${path}/useraccess`} render={() =>
<UserActions users={users} reloadUsers={reloadUsers}>
{loadingUsers && <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> }
</UserActions>
} />
<Route path={`${path}/approve`} render={() =>
<ApprovalActions users={users} reloadUsers={reloadApprovals}>
<ApprovalActions entities={approvals} reloadUsers={reloadApprovals}>
{(approve) =>
<SourceList entities={sources} onDelete={reloadSources} onApprove={(s, e) => approve(s, e, reloadApprovals)}>
<SourceList entities={approvals} onDelete={reloadSources} onApprove={(s, e) => approve(s, e, reloadApprovals)}>
{loadingApprovals && <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> }
</SourceList>
}
</ApprovalActions>
} />
<Route path={`${path}/enable`}>
<ProtectRoute redirectTo="/dashboard">
<MetadataActions type="source">
{(enable) =>
<SourceList entities={sources} onDelete={reloadSources} onEnable={(s, e) => enable(s, e, reloadSources)}>
{loadingSources && <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> }
</SourceList>
}
</MetadataActions>
</ProtectRoute>
</Route>
<Route path={`${path}/useraccess`} render={() =>
<ProtectRoute redirectTo="/dashboard">
<UserActions users={users} reloadUsers={reloadUsers}>
{loadingUsers && <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> }
</UserActions>
</ProtectRoute>
} />
</Switch>
</div>
</div>
Expand Down
64 changes: 34 additions & 30 deletions ui/src/app/dashboard/view/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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}`, {
Expand All @@ -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 (
<div className="container-fluid p-3" role="navigation">
Expand All @@ -97,39 +100,40 @@ export function Dashboard () {
<Translate value="label.admin">Admin</Translate>
</NavLink>
</Nav.Item>
<Nav.Item>
<NavLink className="nav-link d-flex align-items-center" to={`${path}/admin/actions`}>
<Translate value="label.action-required">Action Required</Translate>
<Badge pill bg="danger" className="ms-1">{actions}</Badge>
</NavLink>
</Nav.Item>
</>
}
{isApprover &&
<Nav.Item>
<NavLink className="nav-link d-flex align-items-center" to={`${path}/admin/actions`}>
<Translate value="label.action-required">Action Required</Translate>
<Badge pill bg="danger" className="ms-1">{actions}</Badge>
</NavLink>
</Nav.Item>
}
</Nav>
<Switch>
<Route exact path={`${path}`}>
<Redirect to={`${url}/metadata/manager/resolvers`} />
</Route>
<Route path={`${path}/metadata/manager/resolvers`} component={SourcesTab} />
<Route path={`${path}/metadata/manager/providers`} component={ProvidersTab} />
<Route path={`${path}/admin/actions`} render={() =>
<ActionsTab
sources={sources}
users={users}
approvals={approvals}
reloadSources={loadSources}
reloadUsers={loadUsers}
reloadApprovals={loadApprovals}
loadingApprovals={approvalLoader.loading}
loadingSources={sourceLoader.loading}
loadingUsers={loading} />
} />
<Route path={`${path}/admin/management`} render={() =>
<ProtectRoute redirectTo="/dashboard">
<AdminTab />
</ProtectRoute>
} />
<Route path={`${path}/admin/actions`} render={() =>
<ProtectRoute redirectTo="/dashboard">
<ActionsTab
sources={sources}
users={users}
reloadSources={loadSources}
reloadUsers={loadUsers}
reloadApprovals={loadApprovals}
loadingApprovals={approvalLoader.loading}
loadingSources={sourceLoader.loading}
loadingUsers={loading} />
</ProtectRoute>
} />
<Route exact path={`${path}/*`}>
<Redirect to={`${url}/metadata/manager/resolvers`} />
</Route>
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/form/component/widgets/MultiSelectWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const MultiSelectWidget = ({
labelKey={ (option) => enumNames[enums.indexOf(option)] }
onChange={ onChange }
options={enums}
multiple
placeholder="Choose approval groups..."
selected={value}
/>
Expand Down
Loading

0 comments on commit 76209a6

Please sign in to comment.