From 196d0bb6499d998029bbf4f5fc013aedb30a4014 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 1 Mar 2023 08:32:08 -0700 Subject: [PATCH 1/2] Fixed Proxy --- ui/src/setupProxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js index f3e901ecd..bbd4523b8 100644 --- a/ui/src/setupProxy.js +++ b/ui/src/setupProxy.js @@ -3,7 +3,7 @@ const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { - const port = 8081; + const port = 8080; app.use( '/api', From d7db42035d58b378da7b37bbb2a60d61a12e4052 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 1 Mar 2023 14:21:16 -0700 Subject: [PATCH 2/2] SHIBUI-2539: Fixed bug with property values changing on deletion --- ui/src/app/App.router.js | 19 +-- .../app/admin/component/ConfigurationForm.js | 127 +++++++----------- ui/src/app/admin/component/PropertyInput.js | 57 ++++++++ .../app/admin/component/PropertySelector.js | 3 +- .../app/admin/container/ConfigurationList.js | 5 +- .../app/admin/container/EditConfiguration.js | 78 +++++------ ui/src/app/admin/container/MetadataActions.js | 2 +- .../app/admin/container/NewConfiguration.js | 67 ++++----- ui/src/app/admin/container/UserManagement.js | 2 +- .../app/admin/hoc/ConfigurationsProvider.js | 43 ------ ui/src/app/admin/hoc/PropertiesProvider.js | 47 ------- ui/src/app/core/hooks/useValidator.js | 32 +++++ ui/src/app/dashboard/view/ActionsTab.js | 6 +- ui/src/app/dashboard/view/AdminTab.js | 2 +- .../dashboard/view/ApproveRegistrations.js | 2 +- ui/src/app/dashboard/view/ApproveSources.js | 2 +- ui/src/app/dashboard/view/Dashboard.js | 6 +- .../dashboard/view/DynamicRegistrationsTab.js | 2 +- .../app/dashboard/view/EnableRegistrations.js | 2 +- ui/src/app/dashboard/view/EnableSources.js | 2 +- ui/src/app/dashboard/view/SourcesTab.js | 2 +- ui/src/app/dashboard/view/UserAccess.js | 2 +- .../component/DynamicRegistrationForm.js | 2 +- .../hoc/DynamicRegistrationActions.js | 2 +- .../view/DynamicRegistrationCreate.js | 2 +- .../view/DynamicRegistrationDetail.js | 2 +- .../view/DynamicRegistrationEdit.js | 2 +- ui/src/app/metadata/hoc/MetadataSelector.js | 2 +- .../store/configurations/ConfigurationApi.js | 71 ++++++++++ .../configurations/ConfigurationSlice.js | 45 +++++++ .../app/store/configurations/PropertyApi.js | 19 +++ ...tionSlice.js => DynamicRegistrationApi.js} | 0 ui/src/app/store/errorHandler.js | 17 +++ ui/src/app/store/index.js | 18 ++- .../metadata/{SourceSlice.js => SourceApi.js} | 0 ...{CurrentUserSlice.js => CurrentUserApi.js} | 0 .../store/user/{UserSlice.js => UserApi.js} | 0 37 files changed, 400 insertions(+), 292 deletions(-) create mode 100644 ui/src/app/admin/component/PropertyInput.js delete mode 100644 ui/src/app/admin/hoc/ConfigurationsProvider.js delete mode 100644 ui/src/app/admin/hoc/PropertiesProvider.js create mode 100644 ui/src/app/core/hooks/useValidator.js create mode 100644 ui/src/app/store/configurations/ConfigurationApi.js create mode 100644 ui/src/app/store/configurations/ConfigurationSlice.js create mode 100644 ui/src/app/store/configurations/PropertyApi.js rename ui/src/app/store/dynamic-registration/{DynamicRegistrationSlice.js => DynamicRegistrationApi.js} (100%) create mode 100644 ui/src/app/store/errorHandler.js rename ui/src/app/store/metadata/{SourceSlice.js => SourceApi.js} (100%) rename ui/src/app/store/user/{CurrentUserSlice.js => CurrentUserApi.js} (100%) rename ui/src/app/store/user/{UserSlice.js => UserApi.js} (100%) diff --git a/ui/src/app/App.router.js b/ui/src/app/App.router.js index 1091dd091..04f434c20 100644 --- a/ui/src/app/App.router.js +++ b/ui/src/app/App.router.js @@ -45,7 +45,6 @@ import { Fragment } from "react"; import { NewGroup } from "./admin/container/NewGroup"; import { EditGroup } from "./admin/container/EditGroup"; -import { ConfigurationsProvider } from './admin/hoc/ConfigurationsProvider'; import { ConfigurationList } from './admin/container/ConfigurationList'; import { NewConfiguration } from './admin/container/NewConfiguration'; import { EditConfiguration } from './admin/container/EditConfiguration'; @@ -308,27 +307,15 @@ export const router = createBrowserRouter([ children: [ { path: `list`, - element: - {(configurations, onDelete) => - - } - , + element: , }, { path: `new`, - element: - {(configurations) => - - } - , + element: , }, { path: `:id/edit`, - element: - {(configurations) => - - } - , + element: , }, { index: true, diff --git a/ui/src/app/admin/component/ConfigurationForm.js b/ui/src/app/admin/component/ConfigurationForm.js index 9e2020dce..a2900843f 100644 --- a/ui/src/app/admin/component/ConfigurationForm.js +++ b/ui/src/app/admin/component/ConfigurationForm.js @@ -1,46 +1,48 @@ import React from 'react'; import Button from 'react-bootstrap/Button'; -import { useFieldArray, useForm } from 'react-hook-form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import PropertySelector from './PropertySelector'; -import { useProperties } from '../hoc/PropertiesProvider'; - import Form from 'react-bootstrap/Form'; -import FloatingLabel from 'react-bootstrap/FloatingLabel'; import { useTranslator } from '../../i18n/hooks'; import { includes, uniqBy } from 'lodash'; +import { useGetPropertiesQuery } from '../../store/configurations/PropertyApi'; +import { nameUpdated, propertyAdded, propertyRemoved, propertyUpdated, useConfiguration } from '../../store/configurations/ConfigurationSlice'; +import { useDispatch } from 'react-redux'; +import { PropertyInput } from './PropertyInput'; + +import { useValidator } from '../../core/hooks/useValidator'; export function ConfigurationForm({ configurations, configuration = {}, loading, onSave, onCancel }) { - const [names, setNames] = React.useState([]); - - const { control, register, getValues, watch, formState, handleSubmit } = useForm({ - defaultValues: { - ...configuration - }, - reValidateMode: 'onChange', - mode: 'onChange', - }); + const names = React.useMemo(() => configurations.map(p => p.name), [configurations]); + const nameList = React.useMemo(() => names.filter(n => n !== configuration.name), [configuration, names]); + const [touched, setTouched] = React.useState(false); + + const { name, ids, entities } = useConfiguration(); - const { fields, append, remove } = useFieldArray({ - control, - name: "properties", - rules: { - minLength: 1 + const dispatch = useDispatch(); + const translate = useTranslator(); + const { errors } = useValidator({ name }, { + name: { + required: { + value: true, + message: translate(`message.name-required`), + }, + custom: { + isValid: (value) => !includes(nameList, value), + message: translate(`message.must-be-unique`), + } } }); - const { errors, isValid } = formState; - - const properties = useProperties(); + const { data: properties = [] } = useGetPropertiesQuery(); const addProperties = (props) => { - - const selected = getValues('properties'); + const selected = Object.values(entities); const parsed = props.reduce((coll, prop, idx) => { if (prop.isCategory) { @@ -56,11 +58,11 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, const deduped = uniqBy(filtered, (i) => i.propertyName); - append(deduped); + deduped.forEach((p) => dispatch(propertyAdded(p))); }; - const saveConfig = (formValues) => { - const parsed = formValues.properties.map(p => ({ + const saveConfig = ({ name, properties }) => { + const parsed = properties.map(p => ({ propertyName: p.propertyName, propertyValue: p.propertyValue, configFile: p.configFile, @@ -68,18 +70,17 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, displayType: p.displayType })); onSave({ - ...formValues, + name, properties: parsed }); }; const translator = useTranslator(); + const remove = React.useCallback((id) => dispatch(propertyRemoved(id)), [dispatch]); - React.useEffect(() => { - setNames(configurations.map(p => p.name)); - }, [configurations]); - - const onNext = (data) => {}; + const updateValue = React.useCallback(({ id, propertyValue }) => { + dispatch(propertyUpdated({ id, changes: { propertyValue } })) + }, [dispatch]); return (<>
@@ -87,8 +88,8 @@ export function ConfigurationForm({ configurations, configuration = {}, loading,

-
+
@@ -111,28 +112,22 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, v.trim() === configuration.name || !includes(names, v) - } - })} /> - - {errors?.name?.type === 'unique' && } - {errors?.name?.type === 'required' && } - + maxLength={255} + onChange={ (ev) => dispatch(nameUpdated(ev.target.value)) } + value={name} + isInvalid={ touched && errors.name } + onFocus={() => setTouched(true)} /> + {touched && errors?.name && + { errors?.name } + }
- +
@@ -151,41 +146,23 @@ export function ConfigurationForm({ configurations, configuration = {}, loading, - {fields.map((p, idx) => ( + {ids.map((id, idx) => ( - { p.propertyName } - { p.category } - { p.displayType === 'number' ? 'integer' : p.displayType } + { entities[id].propertyName } + { entities[id].category } + { entities[id].displayType === 'number' ? 'integer' : entities[id].displayType } - {p.displayType !== 'boolean' ? - - (p.displayType === 'number' ? parseInt(v) : v), - maxLength: 255, - })} /> - - : - - } + - ))} - {fields.length === 0 && + {ids.length === 0 && At least one property is required. diff --git a/ui/src/app/admin/component/PropertyInput.js b/ui/src/app/admin/component/PropertyInput.js new file mode 100644 index 000000000..5db0b6d1b --- /dev/null +++ b/ui/src/app/admin/component/PropertyInput.js @@ -0,0 +1,57 @@ +import React, { Fragment } from 'react'; +import FloatingLabel from 'react-bootstrap/FloatingLabel'; +import Form from 'react-bootstrap/Form'; +import { useValidator } from '../../core/hooks/useValidator'; +import { useTranslator } from '../../i18n/hooks'; + +export function PropertyInput({ property, onChange }) { + + const translate = useTranslator(); + + const value = React.useMemo(() => { + const { propertyValue, displayType } = property; + switch (displayType) { + case 'number': + return parseInt(propertyValue); + case 'boolean': + return propertyValue ? Boolean(propertyValue) : false; + default: + return propertyValue || ''; + } + }, [property]); + + const { errors } = useValidator({ value }, { + value: { + custom: { + isValid: (value) => !value || value?.length <= 255, + message: translate(`message.must-be-unique`), + } + } + }); + + return ( + + {property.displayType !== 'boolean' ? + + onChange({ id: property.resourceId, propertyValue: evt.target.value }) } + maxLength={255} + isInvalid={ errors.value } /> + + : + onChange({ id: property.resourceId, propertyValue: evt.target.checked }) } + className="my-3" /> + } + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/PropertySelector.js b/ui/src/app/admin/component/PropertySelector.js index 9f219e403..30091f98a 100644 --- a/ui/src/app/admin/component/PropertySelector.js +++ b/ui/src/app/admin/component/PropertySelector.js @@ -20,6 +20,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) { } const cat = {category: item, propertyName: item, isCategory: true}; const catSelected = selected.some(s => s.propertyName === item); + const sorted = orderBy(grouped[item], 'propertyName'); return ( {index !== 0 && } @@ -32,7 +33,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) { {item} - Add all - {grouped[item].map((i) => { + {sorted.map((i) => { if (!properties.some((p) => p.propertyName === i.propertyName)) { index = index + 1; const item = diff --git a/ui/src/app/admin/container/ConfigurationList.js b/ui/src/app/admin/container/ConfigurationList.js index 6595dbe78..d025db4bc 100644 --- a/ui/src/app/admin/container/ConfigurationList.js +++ b/ui/src/app/admin/container/ConfigurationList.js @@ -15,8 +15,9 @@ import { useTranslator } from '../../i18n/hooks'; import useFetch from 'use-http'; import API_BASE_PATH from '../../App.constant'; import { downloadAsZip } from '../../core/utility/download_as'; +import { useGetConfigurationsQuery } from '../../store/configurations/ConfigurationApi'; -export function ConfigurationList({ configurations, onDelete, loading }) { +export function ConfigurationList({ onDelete, loading }) { const remove = (id) => { onDelete(id); @@ -24,6 +25,8 @@ export function ConfigurationList({ configurations, onDelete, loading }) { const translate = useTranslator(); + const { data: configurations } = useGetConfigurationsQuery(); + const downloader = useFetch(`${API_BASE_PATH}/shib/property/set`, { cachePolicy: 'no-cache', headers: { diff --git a/ui/src/app/admin/container/EditConfiguration.js b/ui/src/app/admin/container/EditConfiguration.js index 05978d9e7..5555e7fdc 100644 --- a/ui/src/app/admin/container/EditConfiguration.js +++ b/ui/src/app/admin/container/EditConfiguration.js @@ -1,64 +1,52 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; +import React, { Fragment } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import Translate from '../../i18n/components/translate'; -import { useConfiguration } from '../hooks'; import { ConfigurationForm } from '../component/ConfigurationForm'; -import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice'; -import { useTranslator } from '../../i18n/hooks'; -import { PropertiesProvider } from '../hoc/PropertiesProvider'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { usePrompt } from '../../core/hooks/usePrompt'; +import { useGetConfigurationQuery, useGetConfigurationsQuery, useUpdateConfigurationMutation } from '../../store/configurations/ConfigurationApi'; +import { configurationReceived } from '../../store/configurations/ConfigurationSlice'; +import { useDispatch } from 'react-redux'; -export function EditConfiguration({ configurations }) { +export function EditConfiguration() { const navigate = useNavigate(); - const notifier = useDispatch(); - const translator = useTranslator(); const { id } = useParams(); + const dispatch = useDispatch(); - const { put, get, response, loading } = useConfiguration({}); + const { data: configuration = {}, isFetching: loadingDetail, isSuccess: loaded } = useGetConfigurationQuery(id); + const { data: configurations = [], isFetching: loadingList } = useGetConfigurationsQuery(); const [blocking, setBlocking] = React.useState(false); - const [configuration, setConfiguration] = React.useState(); - - async function save(config) { - let toast; - const resp = await put(`${config.resourceId}`, config); - if (response.ok) { - gotoList(); - toast = createNotificationAction(`Updated configuration successfully.`, NotificationTypes.SUCCESS); - } else { - toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); - } - if (toast) { - notifier(toast); - } - }; - - async function loadConfiguration(id) { - const config = await get(`/${id}`); - if (response.ok) { - setConfiguration(config); - } - } - - /*eslint-disable react-hooks/exhaustive-deps*/ - React.useEffect(() => { loadConfiguration(id) }, []); + const [update, {isSuccess}] = useUpdateConfigurationMutation(); const cancel = () => { gotoList(); }; - const gotoList = () => { + const gotoList = React.useCallback(() => { setBlocking(false); setTimeout(() => navigate(`/configurations`), 0); - }; + }, [navigate]); + + React.useEffect(() => { + if (isSuccess) { + gotoList({ refresh: true }); + } + }, [isSuccess, gotoList]); + + React.useEffect(() => { + if (loaded) { + dispatch(configurationReceived(configuration)); + } + }, [loaded, configuration, dispatch]); const prompt = usePrompt(blocking, `message.unsaved-editor`); + const { resourceId } = configuration; + return (
{prompt} @@ -71,19 +59,23 @@ export function EditConfiguration({ configurations }) {
- {loading ? + {loadingDetail || loadingList ?
: - + {configuration && save(data)} - onCancel={() => cancel()} /> } - + loading={loadingDetail || loadingList} + onSave={(configuration) => update({ id: resourceId, configuration: { + ...configuration, + resourceId + } })} + onCancel={() => cancel()} /> + } + }
diff --git a/ui/src/app/admin/container/MetadataActions.js b/ui/src/app/admin/container/MetadataActions.js index 80adbf8ec..3fd77264a 100644 --- a/ui/src/app/admin/container/MetadataActions.js +++ b/ui/src/app/admin/container/MetadataActions.js @@ -3,7 +3,7 @@ import { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; import { useMetadataActivator, useMetadataApprover, useMetadataEntity } from '../../metadata/hooks/api'; import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice'; -import { useApproveSourceMutation, useDeleteSourceMutation, useEnableSourceMutation } from '../../store/metadata/SourceSlice'; +import { useApproveSourceMutation, useDeleteSourceMutation, useEnableSourceMutation } from '../../store/metadata/SourceApi'; import { useDispatch } from 'react-redux'; export function MetadataActions ({type, children}) { diff --git a/ui/src/app/admin/container/NewConfiguration.js b/ui/src/app/admin/container/NewConfiguration.js index 4e0f96a80..fb644893f 100644 --- a/ui/src/app/admin/container/NewConfiguration.js +++ b/ui/src/app/admin/container/NewConfiguration.js @@ -2,39 +2,22 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import Translate from '../../i18n/components/translate'; -import { useConfiguration } from '../hooks'; -import { Schema } from '../../form/Schema'; import { ConfigurationForm } from '../component/ConfigurationForm'; - -import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice'; -import { useTranslator } from '../../i18n/hooks'; -import { BASE_PATH } from '../../App.constant'; -import { PropertiesProvider } from '../hoc/PropertiesProvider'; -import { useDispatch } from 'react-redux'; import { usePrompt } from '../../core/hooks/usePrompt'; +import { useGetConfigurationsQuery, useSaveConfigurationMutation } from '../../store/configurations/ConfigurationApi'; +import { useDispatch } from 'react-redux'; +import { configurationReceived } from '../../store/configurations/ConfigurationSlice'; -export function NewConfiguration({ configurations }) { +export function NewConfiguration() { const navigate = useNavigate(); - const notifier = useDispatch(); - const translator = useTranslator(); - - const { post, response, loading } = useConfiguration({}); + const dispatch = useDispatch(); const [blocking, setBlocking] = React.useState(false); - async function save(config) { - let toast; - const resp = await post(``, config); - if (response.ok) { - gotoList({ refresh: true }); - toast = createNotificationAction(`Added configuration successfully.`, NotificationTypes.SUCCESS); - } else { - toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); - } - if (toast) { - notifier(toast); - } - }; + const [create, {isSuccess, isLoading: loading}] = useSaveConfigurationMutation(); + const { data: configurations = [] } = useGetConfigurationsQuery(); + + const save = React.useCallback(async (configuration) => await create({configuration}), [create]); const cancel = () => { gotoList(); @@ -42,10 +25,20 @@ export function NewConfiguration({ configurations }) { const [configuration] = React.useState({}); - const gotoList = () => { + const gotoList = React.useCallback(() => { setBlocking(false); setTimeout(() => navigate(`/configurations`), 0); - }; + }, [navigate]); + + React.useEffect(() => { + dispatch(configurationReceived({name: '', properties: []})); + }, [dispatch]); + + React.useEffect(() => { + if (isSuccess) { + gotoList({ refresh: true }); + } + }, [isSuccess, gotoList]); const prompt = usePrompt(blocking, `message.unsaved-editor`); @@ -61,18 +54,12 @@ export function NewConfiguration({ configurations }) {
- - - {(schema) => - save(data)} - onCancel={() => cancel()} />} - - + save(data)} + onCancel={() => cancel()} />
diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js index 0dd84a099..ba1fd5fbd 100644 --- a/ui/src/app/admin/container/UserManagement.js +++ b/ui/src/app/admin/container/UserManagement.js @@ -8,7 +8,7 @@ import Button from 'react-bootstrap/Button'; import Translate from '../../i18n/components/translate'; import API_BASE_PATH from '../../App.constant'; -import { useRemoveUserMutation, useSetUserGroupRequestMutation, useSetUserRoleRequestMutation } from '../../store/user/UserSlice'; +import { useRemoveUserMutation, useSetUserGroupRequestMutation, useSetUserRoleRequestMutation } from '../../store/user/UserApi'; import { cloneDeep } from 'lodash'; export default function UserManagement({ users = [], children, reload}) { diff --git a/ui/src/app/admin/hoc/ConfigurationsProvider.js b/ui/src/app/admin/hoc/ConfigurationsProvider.js deleted file mode 100644 index 0a710a579..000000000 --- a/ui/src/app/admin/hoc/ConfigurationsProvider.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { useConfigurations } from '../hooks'; -import { createNotificationAction, NotificationTypes } from '../../store/notifications/NotificationSlice'; -import { useTranslator } from '../../i18n/hooks'; -import { useDispatch } from 'react-redux'; - -export function ConfigurationsProvider({ children, cache = 'no-cache' }) { - - const [configurations, setConfigurations] = React.useState([]); - - const notifier = useDispatch(); - const translator = useTranslator(); - - const { get, del, response, loading } = useConfigurations({ - cachePolicy: cache - }); - - async function loadConfigurations() { - const list = await get(`shib/property/set`); - if (response.ok) { - setConfigurations(list); - } - } - - async function removeConfiguration(id) { - let toast; - const resp = await del(`shib/property/set/${id}`); - if (response.ok) { - loadConfigurations(); - toast = createNotificationAction(`Deleted property successfully.`, NotificationTypes.SUCCESS); - } else { - toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); - } - if (toast) { - notifier(toast); - } - } - - /*eslint-disable react-hooks/exhaustive-deps*/ - React.useEffect(() => { loadConfigurations() }, []); - - return (<>{children(configurations, removeConfiguration, loading)}); -} diff --git a/ui/src/app/admin/hoc/PropertiesProvider.js b/ui/src/app/admin/hoc/PropertiesProvider.js deleted file mode 100644 index 5ab7bde75..000000000 --- a/ui/src/app/admin/hoc/PropertiesProvider.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import useFetch from 'use-http'; -import API_BASE_PATH from '../../App.constant'; - -const PropertiesContext = React.createContext(); - -const { Provider, Consumer } = PropertiesContext; - -function PropertiesProvider({ children, cache = 'no-cache' }) { - - const [properties, setProperties] = React.useState([]); - - - const { get, response, loading } = useFetch('', { - cachePolicy: cache - }); - - async function loadProperties() { - const list = await get(`${API_BASE_PATH}/shib/properties`); - if (response.ok) { - setProperties(list); - } - } - - /*eslint-disable react-hooks/exhaustive-deps*/ - React.useEffect(() => { loadProperties() }, []); - - return ({children}); -} - -function useProperties() { - const { properties } = React.useContext(PropertiesContext); - return properties.map((p, idx) => !p.category || p.category === '?' ? { ...p, category: 'Misc' } : p);; -} - -function usePropertiesLoading() { - const { loading } = React.useContext(PropertiesContext); - return loading; -} - -export { - PropertiesProvider, - PropertiesContext, - Consumer as PropertiesConsumer, - useProperties, - usePropertiesLoading, -}; diff --git a/ui/src/app/core/hooks/useValidator.js b/ui/src/app/core/hooks/useValidator.js new file mode 100644 index 000000000..c4f0c5fef --- /dev/null +++ b/ui/src/app/core/hooks/useValidator.js @@ -0,0 +1,32 @@ +import React from 'react'; + +export function useValidator (data, validations) { + + const errors = React.useMemo(() => { + const errors = {}; + for (const key in validations) { + const value = data[key]; + const validation = validations[key]; + if (validation?.required?.value && !value) { + errors[key] = validation?.required?.message; + } + + const pattern = validation?.pattern; + if (pattern?.value && !RegExp(pattern.value).test(value)) { + errors[key] = pattern.message; + } + + const custom = validation?.custom; + if (custom?.isValid && !custom.isValid(value)) { + errors[key] = custom.message; + } + } + + return errors; + }, [data, validations]); + + return { + data, + errors, + }; +}; \ No newline at end of file diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js index 4706be423..077b2f78e 100644 --- a/ui/src/app/dashboard/view/ActionsTab.js +++ b/ui/src/app/dashboard/view/ActionsTab.js @@ -9,13 +9,13 @@ import { NavLink, Outlet } from 'react-router-dom'; import Translate from '../../i18n/components/translate'; -import { useGetNewUsersQuery } from '../../store/user/UserSlice'; -import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice'; +import { useGetNewUsersQuery } from '../../store/user/UserApi'; +import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi'; import { useGetDisabledRegistrationsQuery, useGetUnapprovedRegistrationsQuery -} from '../../store/dynamic-registration/DynamicRegistrationSlice'; +} from '../../store/dynamic-registration/DynamicRegistrationApi'; import { useIsAdmin, useIsApprover } from '../../core/user/UserContext'; export function ActionsTab() { diff --git a/ui/src/app/dashboard/view/AdminTab.js b/ui/src/app/dashboard/view/AdminTab.js index a5b3f8ba8..8b476f99b 100644 --- a/ui/src/app/dashboard/view/AdminTab.js +++ b/ui/src/app/dashboard/view/AdminTab.js @@ -4,7 +4,7 @@ import UserMaintenance from '../../admin/component/UserMaintenance'; import Translate from '../../i18n/components/translate'; import Spinner from '../../core/components/Spinner'; -import { useGetUsersQuery } from '../../store/user/UserSlice'; +import { useGetUsersQuery } from '../../store/user/UserApi'; export function AdminTab () { diff --git a/ui/src/app/dashboard/view/ApproveRegistrations.js b/ui/src/app/dashboard/view/ApproveRegistrations.js index fb536450d..c7bad45a4 100644 --- a/ui/src/app/dashboard/view/ApproveRegistrations.js +++ b/ui/src/app/dashboard/view/ApproveRegistrations.js @@ -2,7 +2,7 @@ import React from 'react'; import Spinner from '../../core/components/Spinner'; import { DynamicRegistrationList } from '../../dynamic-registration/component/DynamicRegistrationList'; import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions'; -import { useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi'; export function ApproveRegistrations () { diff --git a/ui/src/app/dashboard/view/ApproveSources.js b/ui/src/app/dashboard/view/ApproveSources.js index b8adc082d..d8645212f 100644 --- a/ui/src/app/dashboard/view/ApproveSources.js +++ b/ui/src/app/dashboard/view/ApproveSources.js @@ -2,7 +2,7 @@ import React from 'react'; import { MetadataActions } from '../../admin/container/MetadataActions'; import Spinner from '../../core/components/Spinner'; import SourceList from '../../metadata/domain/source/component/SourceList'; -import { useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice'; +import { useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi'; export function ApproveSources () { diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js index e43753559..5d5ac7826 100644 --- a/ui/src/app/dashboard/view/Dashboard.js +++ b/ui/src/app/dashboard/view/Dashboard.js @@ -10,9 +10,9 @@ import { useCurrentUserLoading, useIsAdmin, useIsApprover } from '../../core/use import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import Badge from 'react-bootstrap/Badge'; -import { useGetNewUsersQuery } from '../../store/user/UserSlice'; -import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceSlice'; -import { useGetDisabledRegistrationsQuery, useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useGetNewUsersQuery } from '../../store/user/UserApi'; +import { useGetDisabledSourcesQuery, useGetUnapprovedSourcesQuery } from '../../store/metadata/SourceApi'; +import { useGetDisabledRegistrationsQuery, useGetUnapprovedRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi'; export function Dashboard () { diff --git a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js index 0de7b27d7..2292795a3 100644 --- a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js +++ b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js @@ -5,7 +5,7 @@ import { Search } from '../component/Search'; import {DynamicRegistrationList} from '../../dynamic-registration/component/DynamicRegistrationList'; import { useGetDynamicRegistrationsQuery -} from '../../store/dynamic-registration/DynamicRegistrationSlice'; +} from '../../store/dynamic-registration/DynamicRegistrationApi'; import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Link } from 'react-router-dom'; diff --git a/ui/src/app/dashboard/view/EnableRegistrations.js b/ui/src/app/dashboard/view/EnableRegistrations.js index 0d0fe2814..dd29f95d9 100644 --- a/ui/src/app/dashboard/view/EnableRegistrations.js +++ b/ui/src/app/dashboard/view/EnableRegistrations.js @@ -3,7 +3,7 @@ import { ProtectRoute } from '../../core/components/ProtectRoute'; import Spinner from '../../core/components/Spinner'; import { DynamicRegistrationList } from '../../dynamic-registration/component/DynamicRegistrationList'; import { DynamicRegistrationActions } from '../../dynamic-registration/hoc/DynamicRegistrationActions'; -import { useGetDisabledRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useGetDisabledRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi'; export function EnableRegistrations () { diff --git a/ui/src/app/dashboard/view/EnableSources.js b/ui/src/app/dashboard/view/EnableSources.js index 27b2aaa53..b6ff81c1f 100644 --- a/ui/src/app/dashboard/view/EnableSources.js +++ b/ui/src/app/dashboard/view/EnableSources.js @@ -3,7 +3,7 @@ import { MetadataActions } from '../../admin/container/MetadataActions'; import { ProtectRoute } from '../../core/components/ProtectRoute'; import Spinner from '../../core/components/Spinner'; import SourceList from '../../metadata/domain/source/component/SourceList'; -import { useGetDisabledSourcesQuery } from '../../store/metadata/SourceSlice'; +import { useGetDisabledSourcesQuery } from '../../store/metadata/SourceApi'; export function EnableSources () { diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js index d2553b19c..4f03ed4fb 100644 --- a/ui/src/app/dashboard/view/SourcesTab.js +++ b/ui/src/app/dashboard/view/SourcesTab.js @@ -6,7 +6,7 @@ import SourceList from '../../metadata/domain/source/component/SourceList'; import { Search } from '../component/Search'; import { Spinner } from '../../core/components/Spinner'; -import { useChangeSourceGroupMutation, useGetSourcesQuery } from '../../store/metadata/SourceSlice'; +import { useChangeSourceGroupMutation, useGetSourcesQuery } from '../../store/metadata/SourceApi'; import { Link } from 'react-router-dom'; import { faCube } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; diff --git a/ui/src/app/dashboard/view/UserAccess.js b/ui/src/app/dashboard/view/UserAccess.js index c26af6c89..eab07d4de 100644 --- a/ui/src/app/dashboard/view/UserAccess.js +++ b/ui/src/app/dashboard/view/UserAccess.js @@ -2,7 +2,7 @@ import React from 'react'; import UserActions from '../../admin/container/UserActions'; import { ProtectRoute } from '../../core/components/ProtectRoute'; import Spinner from '../../core/components/Spinner'; -import { useGetNewUsersQuery } from '../../store/user/UserSlice'; +import { useGetNewUsersQuery } from '../../store/user/UserApi'; export function UserAccess () { diff --git a/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js index ad876a73e..1c70eeead 100644 --- a/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js +++ b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js @@ -9,7 +9,7 @@ import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/F import { useDynamicRegistrationUiSchema } from '../api'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; -import { useGetDynamicRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useGetDynamicRegistrationsQuery } from '../../store/dynamic-registration/DynamicRegistrationApi'; import DynamicRegistrationDefinition from '../hoc/DynamicConfigurationDefinition'; diff --git a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js index 768ff3172..6658923ed 100644 --- a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js +++ b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationActions.js @@ -7,7 +7,7 @@ import { useApproveDynamicRegistrationMutation, useEnableDynamicRegistrationMutation, useChangeDynamicRegistrationGroupMutation, -} from '../../store/dynamic-registration/DynamicRegistrationSlice'; +} from '../../store/dynamic-registration/DynamicRegistrationApi'; export function DynamicRegistrationActions ({ children }) { diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js index 1aaee37e9..43359576e 100644 --- a/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js @@ -6,7 +6,7 @@ import { Schema } from '../../form/Schema'; import { FormManager } from '../../form/FormManager'; import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm'; import DynamicConfigurationDefinition from '../hoc/DynamicConfigurationDefinition'; -import { useCreateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useCreateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationApi'; import Spinner from '../../core/components/Spinner'; import { usePrompt } from '../../core/hooks/usePrompt'; diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js index f21eff8d0..1c0564840 100644 --- a/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js @@ -13,7 +13,7 @@ import { MetadataConfiguration } from '../../metadata/component/MetadataConfigur import { Schema } from '../../form/Schema'; import definition from '../hoc/DynamicConfigurationDefinition'; -import { useSelectDynamicRegistrationQuery } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useSelectDynamicRegistrationQuery } from '../../store/dynamic-registration/DynamicRegistrationApi'; import { DynamicRegistrationActions } from '../hoc/DynamicRegistrationActions'; import { useCanEnable, useIsAdmin } from '../../core/user/UserContext'; import { GroupsProvider } from '../../admin/hoc/GroupsProvider'; diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js index 0170e45c6..15a1bdb78 100644 --- a/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js @@ -6,7 +6,7 @@ import { Schema } from '../../form/Schema'; import { FormManager } from '../../form/FormManager'; import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm'; import DynamicConfigurationDefinition from '../hoc/DynamicConfigurationDefinition'; -import { useSelectDynamicRegistrationQuery, useUpdateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationSlice'; +import { useSelectDynamicRegistrationQuery, useUpdateDynamicRegistrationMutation } from '../../store/dynamic-registration/DynamicRegistrationApi'; import Spinner from '../../core/components/Spinner'; import { usePrompt } from '../../core/hooks/usePrompt'; diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index 5fffb7c63..5ced7e7ce 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -1,7 +1,7 @@ import React from 'react'; import { useLocation, useParams } from 'react-router'; import Spinner from '../../core/components/Spinner'; -import { useLazySelectSourceQuery } from '../../store/metadata/SourceSlice'; +import { useLazySelectSourceQuery } from '../../store/metadata/SourceApi'; import { useMetadataEntity } from '../hooks/api'; export const MetadataTypeContext = React.createContext(); diff --git a/ui/src/app/store/configurations/ConfigurationApi.js b/ui/src/app/store/configurations/ConfigurationApi.js new file mode 100644 index 000000000..a7404bb80 --- /dev/null +++ b/ui/src/app/store/configurations/ConfigurationApi.js @@ -0,0 +1,71 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; +import { getBaseQuery } from '../baseQuery'; +import { createNotificationAction } from '../notifications/NotificationSlice'; + +export const ConfigurationApi = createApi({ + reducerPath: 'configurationApi', + tagTypes: ['Configuration'], + baseQuery: getBaseQuery(), + endpoints: (builder) => ({ + getConfigurations: builder.query({ + query: () => `/shib/property/set`, + providesTags: ['Configuration'], + }), + getConfiguration: builder.query({ + query: (id) => `/shib/property/set/${id}`, + providesTags: ['Configuration'], + }), + saveConfiguration: builder.mutation({ + query: ({ configuration: body }) => ({ + url: `/shib/property/set`, + method: 'POST', + body, + }), + invalidatesTags: ['Configuration'], + async onQueryStarted( + arg, + { dispatch, queryFulfilled } + ) { + await queryFulfilled; + dispatch(createNotificationAction(`Added configuration successfully.`)); + }, + }), + updateConfiguration: builder.mutation({ + query: ({ id, configuration: body }) => ({ + url: `/shib/property/set/${id}`, + method: 'PUT', + body, + }), + invalidatesTags: ['Configuration'], + async onQueryStarted( + arg, + { dispatch, queryFulfilled } + ) { + await queryFulfilled; + dispatch(createNotificationAction(`Updated configuration successfully.`)); + }, + }), + deleteConfiguration: builder.mutation({ + query: ({id}) => ({ + url: `/shib/property/set/${id}`, + method: 'DELETE' + }), + invalidatesTags: ['Configuration'], + async onQueryStarted( + arg, + { dispatch, queryFulfilled } + ) { + await queryFulfilled; + dispatch(createNotificationAction(`Deleted configuration successfully.`)); + }, + }) + }), +}) + +export const { + useGetConfigurationsQuery, + useGetConfigurationQuery, + useDeleteConfigurationMutation, + useSaveConfigurationMutation, + useUpdateConfigurationMutation, +} = ConfigurationApi; diff --git a/ui/src/app/store/configurations/ConfigurationSlice.js b/ui/src/app/store/configurations/ConfigurationSlice.js new file mode 100644 index 000000000..9bb4e7348 --- /dev/null +++ b/ui/src/app/store/configurations/ConfigurationSlice.js @@ -0,0 +1,45 @@ +import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'; + +import { useSelector } from 'react-redux'; + +const PropertiesAdapter = createEntityAdapter({ + // Keep the "all IDs" array sorted based on book titles + sortComparer: (a, b) => a.propertyName.localeCompare(b.propertyName), + selectId: (prop) => prop.resourceId, +}) + +export const ConfigurationSlice = createSlice({ + name: 'properties', + initialState: PropertiesAdapter.getInitialState({ + name: '', + }), + reducers: { + propertyAdded: PropertiesAdapter.addOne, + configurationReceived(state, { payload: { properties, name } }) { + PropertiesAdapter.setAll(state, properties); + state.name = name; + }, + nameUpdated (state, {payload}) { + state.name = payload + }, + propertyUpdated: PropertiesAdapter.updateOne, + propertyRemoved: PropertiesAdapter.removeOne + } +}); + +const selectConfigurationState = state => state.configuration; + +export function useConfiguration() { + const config = useSelector(selectConfigurationState); + return ({ ...config }); +} + +export const { + propertyAdded, + propertyUpdated, + propertyRemoved, + configurationReceived, + nameUpdated, +} = ConfigurationSlice.actions + +export default ConfigurationSlice.reducer; \ No newline at end of file diff --git a/ui/src/app/store/configurations/PropertyApi.js b/ui/src/app/store/configurations/PropertyApi.js new file mode 100644 index 000000000..6c6a62966 --- /dev/null +++ b/ui/src/app/store/configurations/PropertyApi.js @@ -0,0 +1,19 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; +import { getBaseQuery } from '../baseQuery'; + +export const PropertyApi = createApi({ + reducerPath: 'propertyApi', + tagTypes: ['Property'], + baseQuery: getBaseQuery(), + endpoints: (builder) => ({ + getProperties: builder.query({ + query: () => `/shib/properties`, + providesTags: ['Property'], + transformResponse: (properties) => properties.map((p) => !p.category || p.category === '?' ? { ...p, category: 'Misc' } : p) + }), + }), +}) + +export const { + useGetPropertiesQuery +} = PropertyApi; diff --git a/ui/src/app/store/dynamic-registration/DynamicRegistrationSlice.js b/ui/src/app/store/dynamic-registration/DynamicRegistrationApi.js similarity index 100% rename from ui/src/app/store/dynamic-registration/DynamicRegistrationSlice.js rename to ui/src/app/store/dynamic-registration/DynamicRegistrationApi.js diff --git a/ui/src/app/store/errorHandler.js b/ui/src/app/store/errorHandler.js new file mode 100644 index 000000000..e8ad1b3f9 --- /dev/null +++ b/ui/src/app/store/errorHandler.js @@ -0,0 +1,17 @@ +import { isRejectedWithValue } from '@reduxjs/toolkit'; +import { createNotificationAction, NotificationTypes } from './notifications/NotificationSlice'; + + +export const rtkQueryErrorLogger = (api) => (next) => (action) => { + + const { dispatch } = api; + + if (isRejectedWithValue(action)) { + const { payload } = action; + const { data } = payload; + const { errorCode, errorMessage } = data; + dispatch(createNotificationAction(`${errorCode} - ${errorMessage}`, NotificationTypes.ERROR)); + } + + return next(action); +} \ No newline at end of file diff --git a/ui/src/app/store/index.js b/ui/src/app/store/index.js index a37083ca7..d05c31b9f 100644 --- a/ui/src/app/store/index.js +++ b/ui/src/app/store/index.js @@ -1,22 +1,32 @@ import { configureStore } from '@reduxjs/toolkit'; import { setupListeners } from '@reduxjs/toolkit/query'; -import { UserAdminApi } from './user/UserSlice'; -import { DynamicRegistrationApi } from './dynamic-registration/DynamicRegistrationSlice'; -import { MetadataSourceApi } from './metadata/SourceSlice'; +import { UserAdminApi } from './user/UserApi'; +import { DynamicRegistrationApi } from './dynamic-registration/DynamicRegistrationApi'; +import { MetadataSourceApi } from './metadata/SourceApi'; import { NotificationSlice } from './notifications/NotificationSlice'; +import { ConfigurationApi } from './configurations/ConfigurationApi'; +import { PropertyApi } from './configurations/PropertyApi'; +import { rtkQueryErrorLogger } from './errorHandler'; +import { ConfigurationSlice } from './configurations/ConfigurationSlice'; export const store = configureStore({ reducer: { [UserAdminApi.reducerPath]: UserAdminApi.reducer, [DynamicRegistrationApi.reducerPath]: DynamicRegistrationApi.reducer, [MetadataSourceApi.reducerPath]: MetadataSourceApi.reducer, + [PropertyApi.reducerPath]: PropertyApi.reducer, + [ConfigurationApi.reducerPath]: ConfigurationApi.reducer, notification: NotificationSlice.reducer, + configuration: ConfigurationSlice.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( + rtkQueryErrorLogger, UserAdminApi.middleware, DynamicRegistrationApi.middleware, - MetadataSourceApi.middleware + MetadataSourceApi.middleware, + PropertyApi.middleware, + ConfigurationApi.middleware, ) }); diff --git a/ui/src/app/store/metadata/SourceSlice.js b/ui/src/app/store/metadata/SourceApi.js similarity index 100% rename from ui/src/app/store/metadata/SourceSlice.js rename to ui/src/app/store/metadata/SourceApi.js diff --git a/ui/src/app/store/user/CurrentUserSlice.js b/ui/src/app/store/user/CurrentUserApi.js similarity index 100% rename from ui/src/app/store/user/CurrentUserSlice.js rename to ui/src/app/store/user/CurrentUserApi.js diff --git a/ui/src/app/store/user/UserSlice.js b/ui/src/app/store/user/UserApi.js similarity index 100% rename from ui/src/app/store/user/UserSlice.js rename to ui/src/app/store/user/UserApi.js