diff --git a/ui/src/app/admin/Groups.js b/ui/src/app/admin/Groups.js index 1e1743d9c..0e2e062c5 100644 --- a/ui/src/app/admin/Groups.js +++ b/ui/src/app/admin/Groups.js @@ -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() { @@ -21,15 +22,15 @@ export function Groups() { } /> - {(groups) => - + {(groups, onDelete, loading) => + { loading ?
: }
}
} /> - {(groups) => - + {(groups, onDelete, loading) => + { loading ?
: }
}
} /> diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js index 7c9cd7a59..ce1999526 100644 --- a/ui/src/app/admin/component/GroupForm.js +++ b/ui/src/app/admin/component/GroupForm.js @@ -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}) { @@ -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 (<>
@@ -41,12 +54,11 @@ export function GroupForm ({group = {}, errors = [], context = {}, loading = fal
-
onChange(form)} + onChange={(form) => onChange({ ...form, formData: parser(form.formData) })} validate={validator} - schema={schema} + schema={groupSchema} uiSchema={uiSchema} liveValidate={true}> <> diff --git a/ui/src/app/admin/container/ApprovalActions.js b/ui/src/app/admin/container/ApprovalActions.js new file mode 100644 index 000000000..13e1a605e --- /dev/null +++ b/ui/src/app/admin/container/ApprovalActions.js @@ -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 ( + + {(block) => + <>{children(approveEntity, (id, cb) => block(() => deleteEntity(id, cb)))} + } + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js index 4d80e7e07..c24f4f516 100644 --- a/ui/src/app/admin/container/NewGroup.js +++ b/ui/src/app/admin/container/NewGroup.js @@ -66,13 +66,13 @@ export function NewGroup({ groups }) { {(data, errors) => <> save(data)} - onCancel={() => cancel()} /> + context={ { groups } } + group={data} + errors={errors} + schema={schema} + loading={loading} + onSave={(data) => save(data)} + onCancel={() => cancel()} /> } } diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 1786db57a..102b58220 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -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); } @@ -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: { @@ -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 !== '') { diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js index f48dc1ded..a642ec001 100644 --- a/ui/src/app/dashboard/view/ActionsTab.js +++ b/ui/src/app/dashboard/view/ActionsTab.js @@ -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(); @@ -52,7 +53,7 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, loading
- +
@@ -71,7 +72,17 @@ export function ActionsTab({ sources, users, reloadSources, reloadUsers, loading {loadingUsers &&
} } /> + + + {(approve) => + approve(s, e, reloadApprovals)}> + {loadingApprovals &&
} +
+ } +
+ } />
+
diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js index 7644b6def..a4ce21669 100644 --- a/ui/src/app/dashboard/view/Dashboard.js +++ b/ui/src/app/dashboard/view/Dashboard.js @@ -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'; @@ -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') @@ -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 ( @@ -114,6 +124,8 @@ export function Dashboard () { users={users} reloadSources={loadSources} reloadUsers={loadUsers} + reloadApprovals={loadApprovals} + loadingApprovals={approvalLoader.loading} loadingSources={sourceLoader.loading} loadingUsers={loading} /> diff --git a/ui/src/app/form/component/widgets/MultiSelectWidget.js b/ui/src/app/form/component/widgets/MultiSelectWidget.js index 3cc4678b5..4184df094 100644 --- a/ui/src/app/form/component/widgets/MultiSelectWidget.js +++ b/ui/src/app/form/component/widgets/MultiSelectWidget.js @@ -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"; @@ -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 ( @@ -51,13 +57,14 @@ const MultiSelectWidget = ({ {schema.description && } enumNames[enums.indexOf(option)] } + onChange={ onChange } + options={enums} + placeholder="Choose approval groups..." + selected={value} /> {rawErrors?.length > 0 && touched && ( diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index a67f9ef5a..04cb14563 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -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]}`; }