Skip to content

Commit

Permalink
UI for additional approval
Browse files Browse the repository at this point in the history
  • Loading branch information
rmathis committed Oct 13, 2022
1 parent bd80aed commit 37156e5
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 39 deletions.
11 changes: 6 additions & 5 deletions ui/src/app/admin/Groups.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';
import React, { Fragment } from 'react';
import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom';
import { GroupsProvider } from './hoc/GroupsProvider';
import { NewGroup } from './container/NewGroup';
import { EditGroup } from './container/EditGroup';
import { GroupsList } from './container/GroupsList';
import Spinner from '../core/components/Spinner';

export function Groups() {

Expand All @@ -21,15 +22,15 @@ export function Groups() {
} />
<Route path={`${path}/new`} render={() =>
<GroupsProvider>
{(groups) =>
<NewGroup groups={groups} />
{(groups, onDelete, loading) =>
<Fragment>{ loading ? <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> : <NewGroup groups={groups} /> }</Fragment>
}
</GroupsProvider>
} />
<Route path={`${path}/:id/edit`} render={() =>
<GroupsProvider>
{(groups) =>
<EditGroup groups={groups} />
{(groups, onDelete, loading) =>
<Fragment>{ loading ? <div className="d-flex justify-content-center text-primary"><Spinner size="4x" /></div> : <EditGroup groups={groups} /> }</Fragment>
}
</GroupsProvider>
} />
Expand Down
22 changes: 17 additions & 5 deletions ui/src/app/admin/component/GroupForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import Form from '../../form/Form';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons';
import Translate from '../../i18n/components/translate';
import set from 'lodash/set';

import { useGroupUiSchema, useGroupUiValidator } from '../hooks';
import { useGroupUiSchema, useGroupUiValidator, useGroupSchema, useGroupParser, useGroupFormatter} from '../hooks';
import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager';

export function GroupForm ({group = {}, errors = [], context = {}, loading = false, schema, onSave, onCancel}) {
Expand All @@ -18,6 +19,18 @@ export function GroupForm ({group = {}, errors = [], context = {}, loading = fal

const uiSchema = useGroupUiSchema();
const validator = useGroupUiValidator();
const groupSchema = React.useMemo(() => {
const filtered = context.groups.filter(g => !([group.resourceId].indexOf(g.resourceId) > -1));
const enumList = filtered.map(g => g.resourceId);
const enumNames = filtered.map(g => g.name);
let s = { ...schema };
s = set(s, 'properties.approversList.items.enum', enumList);
s = set(s, 'properties.approversList.items.enumNames', enumNames);
return s;
}, [schema, context.groups, group.resourceId]);

const parser = useGroupParser();
const formatter = useGroupFormatter();

return (<>
<div className="container-fluid">
Expand All @@ -41,12 +54,11 @@ export function GroupForm ({group = {}, errors = [], context = {}, loading = fal
<hr />
<div className="row">
<div className="col-12 col-lg-6 order-2">
<Form formData={group}
formContext={ context }
<Form formData={formatter(group)}
noHtml5Validate={true}
onChange={(form) => onChange(form)}
onChange={(form) => onChange({ ...form, formData: parser(form.formData) })}
validate={validator}
schema={schema}
schema={groupSchema}
uiSchema={uiSchema}
liveValidate={true}>
<></>
Expand Down
56 changes: 56 additions & 0 deletions ui/src/app/admin/container/ApprovalActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { DeleteConfirmation } from '../../core/components/DeleteConfirmation';
import { useMetadataActivator, useMetadataEntity } from '../../metadata/hooks/api';

import { NotificationContext, createNotificationAction, NotificationTypes } from '../../notifications/hoc/Notifications';

export function ApprovalActions ({type, children}) {

const { dispatch } = React.useContext(NotificationContext);

const { del, response } = useMetadataEntity(type, {
cachePolicy: 'no-cache'
});

const activator = useMetadataActivator(type);

async function approveEntity(entity, enabled, cb = () => {}) {
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'}.`
));
cb();
} else {
const { errorCode, errorMessage, cause } = activator?.response?.data;
dispatch(createNotificationAction(
`${errorCode}: ${errorMessage} ${cause ? `-${cause}` : ''}`,
NotificationTypes.ERROR
));
}
}

async function deleteEntity(id, cb = () => {}) {
await del(`/${id}`);
if (response.ok) {
dispatch(createNotificationAction(
`Metadata ${type} has been deleted.`
));
cb();
} else {
const { errorCode, errorMessage, cause } = activator?.response?.data;
dispatch(createNotificationAction(
`${errorCode}: ${errorMessage} ${cause ? `-${cause}` : ''}`,
NotificationTypes.ERROR
));
}
}

return (
<DeleteConfirmation title={`message.delete-${type}-title`} body={`message.delete-${type}-body`}>
{(block) =>
<>{children(approveEntity, (id, cb) => block(() => deleteEntity(id, cb)))}</>
}
</DeleteConfirmation>
);
}
14 changes: 7 additions & 7 deletions ui/src/app/admin/container/NewGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ export function NewGroup({ groups }) {
{(data, errors) =>
<>
<GroupForm
context={ { groups } }
group={data}
errors={errors}
schema={schema}
loading={loading}
onSave={(data) => save(data)}
onCancel={() => cancel()} />
context={ { groups } }
group={data}
errors={errors}
schema={schema}
loading={loading}
onSave={(data) => save(data)}
onCancel={() => cancel()} />
</>}
</FormManager> }
</Schema>
Expand Down
38 changes: 34 additions & 4 deletions ui/src/app/admin/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import isNil from 'lodash/isNil';
import {isValidRegex} from '../core/utility/is_valid_regex';
import API_BASE_PATH from '../App.constant';

import set from 'lodash/set';

export function useGroups (opts = { cachePolicy: 'no-cache' }) {
return useFetch(`${API_BASE_PATH}/admin/groups`, opts);
}
Expand All @@ -23,6 +25,16 @@ export function useRole(id) {
});
}

export function useGroupSchema (schema, groups, invalid = []) {
const filtered = groups.filter(g => !(invalid.indexOf(g.resourceId) > -1));
const enumList = filtered.map(g => g.resourceId);
const enumNames = filtered.map(g => g.name);
let s = { ...schema };
s = set(s, 'properties.approversList.items.enum', enumList);
s = set(s, 'properties.approversList.items.enumNames', enumNames);
return s;
}

export function useGroupUiSchema () {
return {
description: {
Expand All @@ -31,15 +43,33 @@ export function useGroupUiSchema () {
approversList: {
'ui:options': {
'widget': 'MultiSelectWidget',
'enum': [
'Foo',
'Bar'
]
}
}
};
}

export function useGroupFormatter () {
return (group) => ({
...group,
approversList: [
...(group?.approversList?.length ? group.approversList[0].approverGroupIds : [] )
]
});
}

export function useGroupParser () {
return (group = {}) => ({
...group,
approversList: [
{
approverGroupIds: [
...group?.approversList
]
}
]
});
}

export function useGroupUiValidator() {
return (formData, errors) => {
if (!isNil(formData?.validationRegex) && formData?.validationRegex !== '') {
Expand Down
15 changes: 13 additions & 2 deletions ui/src/app/dashboard/view/ActionsTab.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ 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, loadingSources, loadingUsers }) {
export function ActionsTab({ sources, users, reloadSources, reloadUsers, reloadApprovals, loadingSources, loadingUsers, loadingApprovals }) {

const { path, url } = useRouteMatch();

Expand Down Expand Up @@ -52,7 +53,7 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, loading
</Nav>
<hr />
</div>

<div className="px-3 pb-0">
<Switch>
<Route exact path={`${path}`}>
<Redirect to={`${url}/enable`} />
Expand All @@ -71,7 +72,17 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, loading
{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}>
{(approve) =>
<SourceList entities={sources} 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>
} />
</Switch>
</div>
</div>
</section>
</>
Expand Down
16 changes: 14 additions & 2 deletions ui/src/app/dashboard/view/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ActionsTab } from './ActionsTab';
import { useCurrentUserLoading, useIsAdmin } from '../../core/user/UserContext';
import useFetch from 'use-http';
import API_BASE_PATH from '../../App.constant';
import { useNonAdminSources } from '../../metadata/hooks/api';
import { useNonAdminSources, useUnapprovedSources} from '../../metadata/hooks/api';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import Badge from 'react-bootstrap/Badge';
Expand All @@ -31,12 +31,14 @@ export function Dashboard () {
const [actions, setActions] = React.useState(0);
const [users, setUsers] = React.useState([]);
const [sources, setSources] = React.useState([]);
const [approvals, setApprovals] = React.useState([]);

const { get, response, loading } = useFetch(`${API_BASE_PATH}`, {
cachePolicy: 'no-cache'
});

const sourceLoader = useNonAdminSources();
const approvalLoader = useUnapprovedSources();

async function loadUsers() {
const users = await get('/admin/users')
Expand All @@ -52,14 +54,22 @@ export function Dashboard () {
}
}

async function loadApprovals() {
const s = await approvalLoader.get();
if (response.ok) {
setApprovals(s);
}
}

/*eslint-disable react-hooks/exhaustive-deps*/
React.useEffect(() => {
loadSources();
loadUsers();
loadApprovals();
}, [location]);

React.useEffect(() => {
setActions(users.length + sources.length);
setActions(users.length + sources.length + approvals.length);
}, [users, sources]);

return (
Expand Down Expand Up @@ -114,6 +124,8 @@ export function Dashboard () {
users={users}
reloadSources={loadSources}
reloadUsers={loadUsers}
reloadApprovals={loadApprovals}
loadingApprovals={approvalLoader.loading}
loadingSources={sourceLoader.loading}
loadingUsers={loading} />
</ProtectRoute>
Expand Down
35 changes: 21 additions & 14 deletions ui/src/app/form/component/widgets/MultiSelectWidget.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef } from "react";

import ListGroup from "react-bootstrap/ListGroup";
import Form from "react-bootstrap/Form";
Expand All @@ -24,22 +24,28 @@ const MultiSelectWidget = ({
onBlur,
onFocus,
autofocus,
options,
schema,
rawErrors = [],
formContext,
...props
}) => {
// const inputType = (type || schema.type) === 'string' ? 'text' : `${type || schema.type}`;

const opts = [];
const typeahead = useRef();

React.useEffect(() => console.log(formContext), [formContext]);
React.useEffect(() => console.log(props), [props]);
const [enums, setEnums] = React.useState(schema.items.enum);
const [enumNames, setEnumNames] = React.useState(schema.items.enumNames);

React.useEffect(() => {
const { items } = schema;
setEnums(items.enum);
setEnumNames(items.enumNames);
}, [schema]);

const [touched, setTouched] = React.useState(false);

const [multiSelections, setMultiSelections] = React.useState([]);
React.useEffect(() => {

}, [schema]);

return (
<Form.Group style={{ marginTop: '20px' }}>
Expand All @@ -51,13 +57,14 @@ const MultiSelectWidget = ({
{schema.description && <InfoIcon value={schema.description} />}
</Form.Label>
<Typeahead
id={`option-selector-items-${id}`}
labelKey="name"
multiple
onChange={setMultiSelections}
options={opts}
placeholder="Choose approval groups..."
selected={multiSelections}
id={`option-selector-items-${id}`}
inputProps={{ id: `option-selector-${id}` }}
ref={typeahead}
labelKey={ (option) => enumNames[enums.indexOf(option)] }
onChange={ onChange }
options={enums}
placeholder="Choose approval groups..."
selected={value}
/>
{rawErrors?.length > 0 && touched && (
<ListGroup as="ul">
Expand Down
6 changes: 6 additions & 0 deletions ui/src/app/metadata/hooks/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export function useNonAdminSources() {
});
}

export function useUnapprovedSources() {
return useFetch(`${API_BASE_PATH}${getMetadataListPath('source')}/needsApproval`, {
cachePolicy: 'no-cache'
});
}

export function getMetadataListPath(type) {
return `/${lists[type]}`;
}
Expand Down

0 comments on commit 37156e5

Please sign in to comment.