From 5eacee31c451bfcad493094361b0abea851dff78 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 21 Oct 2022 14:49:11 -0700 Subject: [PATCH] Initial implementation --- ui/public/assets/data/registration.json | 20 +++ ui/public/assets/data/registrations.json | 1 + .../schema/dynamic-registration/oidc.json | 13 ++ ui/src/app/App.js | 4 +- ui/src/app/core/components/Header.js | 4 + ui/src/app/dashboard/view/Dashboard.js | 4 +- .../dashboard/view/DynamicRegistrationsTab.js | 7 +- .../DynamicRegistration.js | 49 +++---- ui/src/app/dynamic-registration/api.js | 26 ++++ .../component/DynamicRegistrationForm.js | 58 ++++++++ .../component/DynamicRegistrationList.js | 33 ++++- .../hoc/DynamicRegistrationContext.js | 107 +++++++++++++- .../view/DynamicRegistrationCreate.js | 82 +++++++++++ .../view/DynamicRegistrationDetail.js | 138 ++++++++++++++++++ .../view/DynamicRegistrationEdit.js | 89 +++++++++++ .../app/metadata/component/MetadataSection.js | 3 - 16 files changed, 595 insertions(+), 43 deletions(-) create mode 100644 ui/public/assets/data/registration.json create mode 100644 ui/public/assets/schema/dynamic-registration/oidc.json create mode 100644 ui/src/app/dynamic-registration/api.js create mode 100644 ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js create mode 100644 ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js create mode 100644 ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js create mode 100644 ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js diff --git a/ui/public/assets/data/registration.json b/ui/public/assets/data/registration.json new file mode 100644 index 000000000..e7434acd6 --- /dev/null +++ b/ui/public/assets/data/registration.json @@ -0,0 +1,20 @@ +{ + "resourceId": "foobar", + "name": "Foobar", + "redirect_uris": [], + "response_types": [], + "grant_types": "", + "application_type": "", + "contacts": "", + "subject_type": "", + "jwks": "", + "jwks_uri": "", + "token_endpoint_auth_method": "", + "logo_uri": "", + "policy_uri": "", + "tos_uri": "", + "scope": "", + "enabled": true, + "modifiedDate": "2022-10-21T11:57:24.391649", + "createdData": "2022-10-21T11:57:24.391649" +} diff --git a/ui/public/assets/data/registrations.json b/ui/public/assets/data/registrations.json index c3c60440c..90dd2b1af 100644 --- a/ui/public/assets/data/registrations.json +++ b/ui/public/assets/data/registrations.json @@ -1,5 +1,6 @@ [ { + "resourceId": "foobar", "name": "Foobar", "redirect_uris": [], "response_types": [], diff --git a/ui/public/assets/schema/dynamic-registration/oidc.json b/ui/public/assets/schema/dynamic-registration/oidc.json new file mode 100644 index 000000000..c746c5d29 --- /dev/null +++ b/ui/public/assets/schema/dynamic-registration/oidc.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": ["name"], + "properties": { + "name": { + "title": "label.dynamic-registration-name", + "description": "tooltip.dynamic-registration-name", + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } +} diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 0f6057141..dfaf9bb2c 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -85,9 +85,7 @@ function App() { - - - + } /> diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 114b73a8c..8c4398f56 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -63,6 +63,10 @@ export function Header () { } + + + + {isAdmin && diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js index 4db3f8327..1ea72de09 100644 --- a/ui/src/app/dashboard/view/Dashboard.js +++ b/ui/src/app/dashboard/view/Dashboard.js @@ -85,7 +85,7 @@ export function Dashboard () { } - + Dynamic Registration @@ -111,7 +111,7 @@ export function Dashboard () { - + diff --git a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js index 97b88e3be..5cfae3e25 100644 --- a/ui/src/app/dashboard/view/DynamicRegistrationsTab.js +++ b/ui/src/app/dashboard/view/DynamicRegistrationsTab.js @@ -5,16 +5,17 @@ import { Ordered } from '../component/Ordered'; import { Search } from '../component/Search'; import {DynamicRegistrationList} from '../../dynamic-registration/component/DynamicRegistrationList'; +import { useDynamicRegistrationCollection, useDynamicRegistrationApi } from '../../dynamic-registration/hoc/DynamicRegistrationContext'; const searchProps = ['name']; export function DynamicRegistrationsTab () { - // const dispatcher = useDynamicRegistrationDispatcher(); + const registrations = useDynamicRegistrationCollection(); + const { load } = useDynamicRegistrationApi(); /*eslint-disable react-hooks/exhaustive-deps*/ - // React.useEffect(() => { loadRegistrations() }, []); - const registrations = []; + React.useEffect(() => { load() }, []); return (
diff --git a/ui/src/app/dynamic-registration/DynamicRegistration.js b/ui/src/app/dynamic-registration/DynamicRegistration.js index 95d753d99..db3fab4b4 100644 --- a/ui/src/app/dynamic-registration/DynamicRegistration.js +++ b/ui/src/app/dynamic-registration/DynamicRegistration.js @@ -1,38 +1,31 @@ import React from 'react'; -import { useDynamicRegistrationDispatcher } from './hoc/DynamicRegistrationContext'; -import Translate from '../i18n/components/translate'; -import { Search } from '../dashboard/component/Search'; +import { Switch, Route, useRouteMatch } from 'react-router-dom'; -import {DynamicRegistrationList} from './component/DynamicRegistrationList'; +import { DynamicRegistrationDetail } from './view/DynamicRegistrationDetail'; -const searchProps = ['name']; +import { DynamicRegistrationsApi } from './hoc/DynamicRegistrationContext'; +import { DynamicRegistrationEdit } from './view/DynamicRegistrationEdit'; +import { DynamicRegistrationCreate } from './view/DynamicRegistrationCreate'; export function DynamicRegistration () { - // const dispatcher = useDynamicRegistrationDispatcher(); - - /*eslint-disable react-hooks/exhaustive-deps*/ - // React.useEffect(() => { loadRegistrations() }, []); - const registrations = []; + const { path } = useRouteMatch(); return ( -
-
- <> -
- - Dynamic Registrations - -
-
- - {(searched) => - - } - -
- -
-
+
+ + + + + } /> + + + } /> + + + } /> + + +
) } \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/api.js b/ui/src/app/dynamic-registration/api.js new file mode 100644 index 000000000..0d557f4f0 --- /dev/null +++ b/ui/src/app/dynamic-registration/api.js @@ -0,0 +1,26 @@ +import useFetch from 'use-http'; + +import {API_BASE_PATH, BASE_PATH} from '../App.constant'; + +export function useDynamicRegistration(id, opts = {}, onMount) { + // + return useFetch(`${BASE_PATH}/assets/data/registration.json`, opts, onMount); +} + +export function useDynamicRegistrations(opts = {}, onMount) { + // + return useFetch(`${BASE_PATH}/assets/data/registrations.json`, opts, onMount); +} + +export function useDynamicRegistrationJsonSchema(opts = {}) { + return useFetch(`${BASE_PATH}/assets/schema/dynamic-registration/oidc.json`, opts, []); +} + +export function useDynamicRegistrationUiSchema() { + return {}; +} +export function useDynamicRegistrationValidator() { + return (formData, errors) => { + return errors; + } +} \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js new file mode 100644 index 000000000..1590d23be --- /dev/null +++ b/ui/src/app/dynamic-registration/component/DynamicRegistrationForm.js @@ -0,0 +1,58 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +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 { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; +import { useDynamicRegistrationUiSchema, useDynamicRegistrationValidator } from '../api'; + +export function DynamicRegistrationForm ({registration = {}, errors = [], loading = false, schema, onSave, onCancel}) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({formData, errors}) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = useDynamicRegistrationUiSchema(); + const validator = useDynamicRegistrationValidator(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + validate={validator} + schema={schema} + uiSchema={uiSchema} + liveValidate={true}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/component/DynamicRegistrationList.js b/ui/src/app/dynamic-registration/component/DynamicRegistrationList.js index f06d7f240..a70c7e3f9 100644 --- a/ui/src/app/dynamic-registration/component/DynamicRegistrationList.js +++ b/ui/src/app/dynamic-registration/component/DynamicRegistrationList.js @@ -1,5 +1,34 @@ import React from 'react'; +import { Link } from 'react-router-dom'; +import Translate from '../../i18n/components/translate'; +import { Scroller } from '../../dashboard/component/Scroller'; -export function DynamicRegistrationList ({entities}) { - return <>
{JSON.stringify(entities, null, 4)}
+export function DynamicRegistrationList ({entities, children}) { + return ( + + + {(limited) => +
+ + + + + + + + {limited.map((reg, idx) => + + + + )} + +
Title
+ {reg.name} +
+
+ } +
+ {children} +
+ ); } \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationContext.js b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationContext.js index ff19ae8ef..f922f2247 100644 --- a/ui/src/app/dynamic-registration/hoc/DynamicRegistrationContext.js +++ b/ui/src/app/dynamic-registration/hoc/DynamicRegistrationContext.js @@ -1,5 +1,6 @@ import React from 'react'; +import {useDynamicRegistrations, useDynamicRegistration} from '../api'; const initialState = { registrations: [], @@ -55,6 +56,16 @@ export const deleteRegistration = (payload) => { function reducer(state, action) { switch (action.type) { + case DynamicRegistrationActions.LOAD_REGISTRATIONS: + return { + ...state, + registrations: action.payload + }; + case DynamicRegistrationActions.SELECT_REGISTRATION: + return { + ...state, + selected: action.payload + }; default: return state; } @@ -62,12 +73,74 @@ function reducer(state, action) { /*eslint-disable react-hooks/exhaustive-deps*/ function DynamicRegistrationsApi({ children, initial = {} }) { + const [state, dispatch] = React.useReducer(reducer, initialState); + const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); + + const loader = useDynamicRegistrations({ + cachePolicy: 'no-cache' + }); + + const selector = useDynamicRegistration({ + cachePolicy: 'no-cache' + }); + + async function load() { + const s = await loader.get(); + if (loader.response.ok) { + dispatch(loadRegistrations(s)); + } + } + + async function select(id) { + const s = await selector.get(); + if (selector.response.ok) { + dispatch(selectRegistration(s)); + } + } + + async function update(id) { + /*const s = await selector.update(); + if (selector.response.ok) { + dispatch(selectRegistration(s)); + }*/ + return Promise.resolve(id); + } - const [contextValue, setContextValue] = React.useState({...initialState}); + async function enable(id) { + /*const s = await selector.update(); + if (selector.response.ok) { + dispatch(selectRegistration(s)); + }*/ + return Promise.resolve(id); + } + + async function create(body) { + /*const s = await selector.update(); + if (selector.response.ok) { + dispatch(selectRegistration(s)); + }*/ + return Promise.resolve(body); + } + + async function remove(id) { + /*const s = await selector.update(); + if (selector.response.ok) { + dispatch(selectRegistration(s)); + }*/ + return Promise.resolve(id); + } return ( - {children} + {children} ); } @@ -82,10 +155,40 @@ function useDynamicRegistrationDispatcher () { return dispatch; } +function useDynamicRegistrationState () { + const { state } = useDynamicRegistrationContext(); + return state; +} + +function useDynamicRegistrationCollection () { + const state = useDynamicRegistrationState(); + return state.registrations; +} + +function useSelectedDynamicRegistration () { + const state = useDynamicRegistrationState(); + return state.selected; +} + +function useDynamicRegistrationApi () { + const {load, select, enable, create, update, remove} = useDynamicRegistrationContext(); + return { + load, + select, + enable, + create, + update, + remove + }; +} + export { DynamicRegistrationsApi, useDynamicRegistrationContext, useDynamicRegistrationDispatcher, + useDynamicRegistrationCollection, + useSelectedDynamicRegistration, + useDynamicRegistrationApi, Provider as MetadataFormProvider, Consumer as MetadataFormConsumer }; \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js new file mode 100644 index 000000000..8b38b4ad3 --- /dev/null +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationCreate.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { Prompt, useHistory, useParams } from 'react-router-dom'; + +import { useDynamicRegistrationApi } from '../hoc/DynamicRegistrationContext'; +import Translate from '../../i18n/components/translate'; +import { BASE_PATH } from '../../App.constant'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; +import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function DynamicRegistrationCreate () { + + const history = useHistory(); + const translator = useTranslator(); + const notifier = useNotificationDispatcher(); + const { create } = useDynamicRegistrationApi(); + + async function save(reg) { + let toast; + const resp = await create(``, reg); + if (resp.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/dashboard/dynamic-registration`, state); + }; + + const [blocking, setBlocking] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ New Dynamic Registration +
+
+
+
+ + {(schema) => + + {(data, errors) => + <> + save(data)} + onCancel={() => cancel()} /> + } + } + +
+
+
+ ) +} \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js new file mode 100644 index 000000000..4a0d1d0ef --- /dev/null +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationDetail.js @@ -0,0 +1,138 @@ +import React from 'react'; +import { Link, useHistory, useParams } from 'react-router-dom'; + +import { useDynamicRegistrationApi, useSelectedDynamicRegistration } from '../hoc/DynamicRegistrationContext'; +import Translate from '../../i18n/components/translate'; +import FormattedDate from '../../core/components/FormattedDate'; +import Button from 'react-bootstrap/esm/Button'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faEdit, faHistory, faToggleOff, faToggleOn, faTrash } from '@fortawesome/free-solid-svg-icons'; +import Badge from 'react-bootstrap/esm/Badge'; + +export function DynamicRegistrationDetail () { + + const { id } = useParams(); + const history = useHistory(); + + const { select, enable, remove } = useDynamicRegistrationApi(); + + const reselect = React.useCallback(() => select(id), [id, select]); + const detail = useSelectedDynamicRegistration(); + + const redirectOnDelete = () => history.push('/dashboard'); + + const edit = (section) => { + history.push(`/dynamic-registration/${id}/edit`); + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { reselect() }, [id]); + + return ( +
+
+
+ {detail && + + + +
+ +
+
+
+
+

+ Saved:  + + + +

+

+ By:  + {detail.createdBy } +

+
+
+ + +
+
+ +

+ + Enabled + +

+ +
+
+ + + +
+
+
+

+ + + +

+
+ +
+
+
+
{JSON.stringify(detail, null, 4)}
+
+
+
+ +
+
+ } +
+
+
+ + ) +} \ No newline at end of file diff --git a/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js new file mode 100644 index 000000000..e6d33f66f --- /dev/null +++ b/ui/src/app/dynamic-registration/view/DynamicRegistrationEdit.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { Prompt, useHistory, useParams } from 'react-router-dom'; + +import { useDynamicRegistrationApi, useSelectedDynamicRegistration } from '../hoc/DynamicRegistrationContext'; +import Translate from '../../i18n/components/translate'; +import { useDynamicRegistrationJsonSchema } from '../api'; +import Form from '../../form/Form'; +import { BASE_PATH } from '../../App.constant'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; +import { DynamicRegistrationForm } from '../component/DynamicRegistrationForm'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function DynamicRegistrationEdit () { + + const { id } = useParams(); + const history = useHistory(); + const translator = useTranslator(); + const notifier = useNotificationDispatcher(); + const { select, create } = useDynamicRegistrationApi(); + + React.useEffect(() => { select(id) }, [id]); + + const detail = useSelectedDynamicRegistration(); + + async function save(reg) { + let toast; + const resp = await create(``, reg); + if (resp.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added group successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/dynamic-registration/${id}`, state); + }; + + const [blocking, setBlocking] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Edit Dynamic Registration +
+
+
+
+ + {(schema) => + + {(data, errors) => + <> + save(data)} + onCancel={() => cancel()} /> + } + } + +
+
+
+ ) +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataSection.js b/ui/src/app/metadata/component/MetadataSection.js index f28cd723d..6b9ef1c31 100644 --- a/ui/src/app/metadata/component/MetadataSection.js +++ b/ui/src/app/metadata/component/MetadataSection.js @@ -5,9 +5,6 @@ import Button from 'react-bootstrap/Button'; import Translate from '../../i18n/components/translate'; export function MetadataSection ({ section, index = -1, onEdit, children }) { - - - return ( <>