From bef53e255e2b5d59d469c5cb83abc3787b6c6c4a Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 13 May 2021 14:28:33 -0700 Subject: [PATCH] Added source wizard --- ui/src/app/App.js | 9 +- ui/src/app/core/components/Header.js | 36 +--- .../app/form/component/widgets/TextWidget.js | 13 +- ui/src/app/i18n/context/I18n.provider.js | 4 +- ui/src/app/metadata/NewProvider.js | 26 +++ ui/src/app/metadata/NewSource.js | 104 +++++----- ui/src/app/metadata/copy/CopySource.js | 2 +- ui/src/app/metadata/copy/SaveCopy.js | 2 +- ui/src/app/metadata/domain/index.js | 11 +- .../domain/provider/BaseProviderDefinition.js | 10 +- .../DynamicHttpMetadataProviderDefinition.js | 2 +- ...ileBackedHttpMetadataProviderDefinition.js | 2 +- .../FileSystemMetadataProviderDefinition.js | 2 +- .../LocalDynamicMetadataProviderDefinition.js | 2 +- .../domain/source/SourceDefinition.js | 195 ++++++++++++++---- .../app/metadata/hoc/MetadataFormContext.js | 29 +++ ui/src/app/metadata/hoc/MetadataSchema.js | 14 +- ui/src/app/metadata/hooks/utility.js | 3 - ui/src/app/metadata/view/MetadataCopy.js | 6 +- ui/src/app/metadata/view/MetadataUpload.js | 2 - ui/src/app/metadata/view/MetadataWizard.js | 19 +- .../metadata/wizard/MetadataProviderWizard.js | 5 + .../metadata/wizard/MetadataSourceWizard.js | 95 +++++++++ .../app/metadata/wizard/MetadataWizardForm.js | 4 +- ui/src/app/metadata/wizard/Wizard.js | 126 +++++++++++ ui/src/app/metadata/wizard/WizardNav.js | 99 +++++++++ 26 files changed, 660 insertions(+), 162 deletions(-) create mode 100644 ui/src/app/metadata/NewProvider.js create mode 100644 ui/src/app/metadata/wizard/MetadataProviderWizard.js create mode 100644 ui/src/app/metadata/wizard/MetadataSourceWizard.js create mode 100644 ui/src/app/metadata/wizard/Wizard.js create mode 100644 ui/src/app/metadata/wizard/WizardNav.js diff --git a/ui/src/app/App.js b/ui/src/app/App.js index 12244347c..a97ab36bb 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -21,6 +21,7 @@ import { Notifications } from './notifications/hoc/Notifications'; import { NotificationList } from './notifications/component/NotificationList'; import { UserConfirmation, ConfirmWindow } from './core/components/UserConfirmation'; import { NewSource } from './metadata/NewSource'; +import { NewProvider } from './metadata/NewProvider'; @@ -55,6 +56,7 @@ function App() { + @@ -71,11 +73,4 @@ function App() { ); } - -/* -
- -
-*/ - export default App; diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index e5ed72cc8..294be7943 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -32,11 +32,11 @@ export function Header () { - + Metadata Source - + Metadata Provider @@ -57,36 +57,4 @@ export function Header () { ); } -/* -
  • - - -
  • -*/ - export default Header; \ No newline at end of file diff --git a/ui/src/app/form/component/widgets/TextWidget.js b/ui/src/app/form/component/widgets/TextWidget.js index 583e24291..e9da932ce 100644 --- a/ui/src/app/form/component/widgets/TextWidget.js +++ b/ui/src/app/form/component/widgets/TextWidget.js @@ -29,10 +29,17 @@ const TextWidget = ({ const _onFocus = ({target: { value }} ) => onFocus(id, value); const inputType = (type || schema.type) === 'string' ? 'text' : `${type || schema.type}`; + const [touched, setTouched] = React.useState(false); + + const onCustomBlur = (evt) => { + setTouched(true); + _onBlur(evt); + }; + // const classNames = [rawErrors.length > 0 ? "is-invalid" : "", type === 'file' ? 'custom-file-label': ""] return ( - 0 ? "text-danger" : ""}`}> + 0 && touched ? "text-danger" : ""}`}> {(label || schema.title) && required ? @@ -47,12 +54,12 @@ const TextWidget = ({ required={required} disabled={disabled} readOnly={readonly} - className={rawErrors.length > 0 ? "is-invalid" : ""} + className={rawErrors.length > 0 && touched ? "is-invalid" : ""} list={schema.examples ? `examples_${id}` : undefined} type={inputType} value={value || value === 0 ? value : ""} onChange={_onChange} - onBlur={_onBlur} + onBlur={onCustomBlur} onFocus={_onFocus} /> {schema.examples ? ( diff --git a/ui/src/app/i18n/context/I18n.provider.js b/ui/src/app/i18n/context/I18n.provider.js index e1849eff9..42c33e921 100644 --- a/ui/src/app/i18n/context/I18n.provider.js +++ b/ui/src/app/i18n/context/I18n.provider.js @@ -25,7 +25,9 @@ function I18nProvider ({ children }) { const [messages, setMessages] = React.useState({}); return ( - {children} + <> + {Object.keys(messages).length > 1 && {children}} + ); } diff --git a/ui/src/app/metadata/NewProvider.js b/ui/src/app/metadata/NewProvider.js new file mode 100644 index 000000000..3cf143726 --- /dev/null +++ b/ui/src/app/metadata/NewProvider.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Translate from '../i18n/components/translate'; +import { MetadataSchema } from './hoc/MetadataSchema'; +import { MetadataWizard } from './view/MetadataWizard'; + +export function NewProvider() { + + return ( +
    +
    +
    +
    +
    + Add a new metadata provider +
    +
    +
    +
    + + + +
    +
    +
    + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/NewSource.js b/ui/src/app/metadata/NewSource.js index 95b7adce9..146d041b7 100644 --- a/ui/src/app/metadata/NewSource.js +++ b/ui/src/app/metadata/NewSource.js @@ -10,7 +10,9 @@ import { faCopy, faLink, faPlusSquare } from '@fortawesome/free-solid-svg-icons' export function NewSource() { - let { path } = useRouteMatch(); + const { path } = useRouteMatch(); + + const [showNav, setShowNav] = React.useState(true); return (
    @@ -23,64 +25,66 @@ export function NewSource() {
    -

    How are you adding the metadata information?

    -
    -
    -
    -
    -
    - - Upload/URL - - -
    -
    -  or  -
    -
    - - Create - - -
    -
    -  or  -
    -
    - - Copy - - + {showNav && <> +

    How are you adding the metadata information?

    +
    +
    +
    +
    +
    + + Upload/URL + + +
    +
    +  or  +
    +
    + + Create + + +
    +
    +  or  +
    +
    + + Copy + + +
    +
    -
    -
    - + } + - + { setShowNav(s) }} /> } /> - + } /> - + { setShowNav(s) } } /> } /> diff --git a/ui/src/app/metadata/copy/CopySource.js b/ui/src/app/metadata/copy/CopySource.js index 49114d01a..f81a5e229 100644 --- a/ui/src/app/metadata/copy/CopySource.js +++ b/ui/src/app/metadata/copy/CopySource.js @@ -56,7 +56,7 @@ export function CopySource({ copy, onNext }) { React.useEffect(() => { setValue('properties', selected); - }, [selected]); + }, [selected, setValue]); return ( <> diff --git a/ui/src/app/metadata/copy/SaveCopy.js b/ui/src/app/metadata/copy/SaveCopy.js index 09e607c6b..a8739fcf0 100644 --- a/ui/src/app/metadata/copy/SaveCopy.js +++ b/ui/src/app/metadata/copy/SaveCopy.js @@ -36,7 +36,7 @@ export function SaveCopy ({ copy, saving, onSave, onBack }) { const model = useCopiedModel(copy); const configuration = useCopiedConfiguration(model, schema, definition); - const { register, handleSubmit, getValues } = useForm({ + const { register, handleSubmit } = useForm({ mode: 'onChange', reValidateMode: 'onBlur', defaultValues: { diff --git a/ui/src/app/metadata/domain/index.js b/ui/src/app/metadata/domain/index.js index 6565c1e85..16d176953 100644 --- a/ui/src/app/metadata/domain/index.js +++ b/ui/src/app/metadata/domain/index.js @@ -1,11 +1,15 @@ import { MetadataFilterEditorTypes } from './filter'; import { MetadataProviderEditorTypes } from './provider'; -import { SourceEditor } from "./source/SourceDefinition"; +import { SourceEditor, SourceWizard } from "./source/SourceDefinition"; export const editors = { source: SourceEditor }; +export const wizards = { + source: SourceWizard +}; + export const ProviderEditorTypes = [ ...MetadataProviderEditorTypes ]; @@ -13,6 +17,11 @@ export const FilterEditorTypes = [ ...MetadataFilterEditorTypes ]; +export const getWizard = (type) => + ProviderEditorTypes.find(def => def.type === type) || + FilterEditorTypes.find(def => def.type === type) || + SourceWizard; + export const getDefinition = (type) => ProviderEditorTypes.find(def => def.type === type) || FilterEditorTypes.find(def => def.type === type) || diff --git a/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js index 1506cdd4a..3148c1683 100644 --- a/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js @@ -59,7 +59,7 @@ export const HttpMetadataResolverAttributesSchema = { groups: [ { title: 'label.http-security-attributes', - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', size: 12, fields: [ 'disregardTLSCertificate' @@ -67,7 +67,7 @@ export const HttpMetadataResolverAttributesSchema = { }, { title: 'label.http-connection-attributes', - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', size: 12, fields: [ 'connectionRequestTimeout', @@ -77,7 +77,7 @@ export const HttpMetadataResolverAttributesSchema = { }, { title: 'label.http-proxy-attributes', - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', size: 12, fields: [ 'proxyHost', @@ -88,7 +88,7 @@ export const HttpMetadataResolverAttributesSchema = { }, { title: 'label.http-caching-attributes', - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', size: 12, fields: [ 'httpCaching', @@ -153,7 +153,7 @@ export const MetadataFilterPluginsSchema = { 'ui:title': false, items: { 'ui:options': { - classNames: 'bg-light border rounded px-4 pt-4 pb-1' + classNames: 'bg-light border rounded px-4 pt-4 pb-3' }, '@type': { 'ui:widget': 'hidden' diff --git a/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js index 34505852c..e22122e6b 100644 --- a/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/DynamicHttpMetadataProviderDefinition.js @@ -57,7 +57,7 @@ export const DynamicHttpMetadataProviderWizard = { groups: [ { size: 9, - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', fields: [ 'name', '@type', diff --git a/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js index 1d93696dd..d7faad4d7 100644 --- a/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/FileBackedHttpMetadataProviderDefinition.js @@ -61,7 +61,7 @@ export const FileBackedHttpMetadataProviderWizard = { groups: [ { size: 9, - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', fields: [ 'name', '@type', diff --git a/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js index 150bcc074..5fb5dc68d 100644 --- a/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/FileSystemMetadataProviderDefinition.js @@ -74,7 +74,7 @@ export const FileSystemMetadataProviderWizard = { groups: [ { size: 9, - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', fields: [ 'name', '@type', diff --git a/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js b/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js index 4afa15807..b6e3847f6 100644 --- a/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/LocalDynamicMetadataProviderDefinition.js @@ -73,7 +73,7 @@ export const LocalDynamicMetadataProviderWizard = { groups: [ { size: 9, - classNames: 'bg-light border rounded px-4 pt-4 pb-1 mb-4', + classNames: 'bg-light border rounded px-4 pt-4 pb-3 mb-4', fields: [ 'name', '@type', diff --git a/ui/src/app/metadata/domain/source/SourceDefinition.js b/ui/src/app/metadata/domain/source/SourceDefinition.js index 6f9fb0070..1674e8dac 100644 --- a/ui/src/app/metadata/domain/source/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/SourceDefinition.js @@ -1,3 +1,4 @@ +import { defaults } from 'lodash'; import defaultsDeep from 'lodash/defaultsDeep'; // import API_BASE_PATH from '../../../App.constant'; import {removeNull} from '../../../core/utility/remove_null'; @@ -274,7 +275,6 @@ export const SourceBase = { } } - export const SourceEditor = { ...SourceBase, uiSchema: defaultsDeep({}, SourceBase.uiSchema), @@ -289,24 +289,161 @@ export const SourceEditor = { 'serviceEnabled', 'organization', 'contacts' - ], - fieldsets: [ + ] + }, + { + index: 3, + id: 'metadata-ui', + label: 'label.metadata-ui', + fields: [ + 'mdui' + ] + }, + { + index: 4, + id: 'descriptor-info', + label: 'label.descriptor-info', + fields: [ + 'serviceProviderSsoDescriptor' + ] + }, + { + index: 5, + id: 'logout-endpoints', + label: 'label.logout-endpoints', + fields: [ + 'logoutEndpoints' + ] + }, + { + index: 6, + id: 'key-info', + label: 'label.key-info', + fields: [ + 'securityInfo' + ] + }, + { + index: 7, + id: 'assertion', + label: 'label.assertion', + fields: [ + 'assertionConsumerServices' + ] + }, + { + index: 8, + id: 'relying-party', + label: 'label.relying-party', + fields: [ + 'relyingPartyOverrides' + ] + }, + { + index: 9, + id: 'attribute', + label: 'label.attribute-release', + fields: [ + 'attributeRelease' + ] + } + ] +}; + +export const SourceWizard = { + ...SourceEditor, + uiSchema: defaults({ + layout: { + groups: [ { - type: 'group', + size: 6, + classNames: 'bg-light border rounded px-4 pt-4 pb-3', fields: [ 'serviceProviderName', - 'entityId', - 'serviceEnabled', - 'organization' + 'entityId' ] }, { - type: 'group', + size: 6, + fields: [ + 'organization', + ], + }, + { + size: 6, fields: [ 'contacts' + ], + }, + { + size: 12, + fields: [ + 'mdui' + ], + }, + { + size: 6, + fields: [ + 'serviceProviderSsoDescriptor' + ], + }, + { + size: 6, + fields: [ + 'logoutEndpoints' + ], + }, + { + size: 12, + fields: [ + 'securityInfo' + ], + }, + { + size: 6, + fields: [ + 'assertionConsumerServices' + ], + }, + { + size: 6, + fields: [ + 'relyingPartyOverrides' + ], + }, + { + size: 6, + fields: [ + 'attributeRelease' + ], + }, + { + size: 6, + fields: [ + 'serviceEnabled' ] } ] + } + }, SourceBase.uiSchema), + steps: [ + { + index: 1, + id: 'common', + label: 'label.name-and-entity-id', + fields: [ + 'serviceProviderName', + 'entityId' + ] + }, + { + index: 2, + id: 'org-info', + label: 'label.org-info', + fields: [ + 'organization', + 'contacts' + ] }, { index: 3, @@ -330,14 +467,6 @@ export const SourceEditor = { label: 'label.logout-endpoints', fields: [ 'logoutEndpoints' - ], - fieldsets: [ - { - type: 'group', - fields: [ - 'logoutEndpoints' - ] - } ] }, { @@ -354,14 +483,6 @@ export const SourceEditor = { label: 'label.assertion', fields: [ 'assertionConsumerServices' - ], - fieldsets: [ - { - type: 'group', - fields: [ - 'assertionConsumerServices' - ] - } ] }, { @@ -370,14 +491,6 @@ export const SourceEditor = { label: 'label.relying-party', fields: [ 'relyingPartyOverrides' - ], - fieldsets: [ - { - type: 'group', - fields: [ - 'relyingPartyOverrides' - ] - } ] }, { @@ -386,15 +499,15 @@ export const SourceEditor = { label: 'label.attribute-release', fields: [ 'attributeRelease' - ], - fieldsets: [ - { - type: 'group', - fields: [ - 'attributeRelease' - ] - } + ] + }, + { + index: 10, + id: 'summary', + label: 'label.finished', + fields: [ + 'serviceEnabled' ] } ] -}; \ No newline at end of file +} \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataFormContext.js b/ui/src/app/metadata/hoc/MetadataFormContext.js index 60e8146f6..f03035a03 100644 --- a/ui/src/app/metadata/hoc/MetadataFormContext.js +++ b/ui/src/app/metadata/hoc/MetadataFormContext.js @@ -106,10 +106,39 @@ function useFormattedMetadata() { return definition.formatter(React.useContext(MetadataObjectContext), schema); } +function useMetadataFormContext () { + return React.useContext(MetadataFormContext); +} + +function useMetadataFormDispatcher () { + const { dispatch } = useMetadataFormContext(); + return dispatch; +} + +function useMetadataFormState () { + const { state } = useMetadataFormContext(); + return state; +} + +function useMetadataFormData() { + const { metadata } = useMetadataFormState(); + return metadata; +} + +function useMetadataFormErrors() { + const { errors } = useMetadataFormState(); + return errors; +} + export { usePagesWithErrors, useFormErrors, useFormattedMetadata, + useMetadataFormContext, + useMetadataFormDispatcher, + useMetadataFormState, + useMetadataFormData, + useMetadataFormErrors, MetadataForm, MetadataFormContext, Provider as MetadataFormProvider, diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 322eafd1e..061ee6a36 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -1,13 +1,13 @@ import React from 'react'; -import { getDefinition } from '../domain/index'; +import { getDefinition, getWizard } from '../domain/index'; import useFetch from 'use-http'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); -export function MetadataSchema({ type, children }) { +export function MetadataSchema({ type, children, wizard = false }) { - const definition = React.useMemo(() => getDefinition(type), [type]); + const definition = React.useMemo(() => wizard ? getWizard(type) : getDefinition(type), [type, wizard]); const { get, response } = useFetch(``, {}, []); @@ -34,6 +34,14 @@ export function MetadataSchema({ type, children }) { ); } +export function useMetadataSchemaContext () { + return React.useContext(MetadataSchemaContext); +} + +export function useMetadataDefinitionContext() { + return React.useContext(MetadataDefinitionContext); +} + //getConfigurationSections export default MetadataSchema; \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/utility.js b/ui/src/app/metadata/hooks/utility.js index 035fb24f1..d8279ec07 100644 --- a/ui/src/app/metadata/hooks/utility.js +++ b/ui/src/app/metadata/hooks/utility.js @@ -207,9 +207,6 @@ export const getSplitSchema = (schema, step) => { if (required && required.length) { s.required = required; } - if (step.fieldsets) { - s.fieldsets = step.fieldsets; - } return s; }; diff --git a/ui/src/app/metadata/view/MetadataCopy.js b/ui/src/app/metadata/view/MetadataCopy.js index 7240ae42d..1584e4897 100644 --- a/ui/src/app/metadata/view/MetadataCopy.js +++ b/ui/src/app/metadata/view/MetadataCopy.js @@ -6,7 +6,7 @@ import { SaveCopy } from '../copy/SaveCopy'; import { useMetadataEntity } from '../hooks/api'; import { useHistory } from 'react-router'; -export function MetadataCopy () { +export function MetadataCopy ({ onShowNav }) { const { post, response, loading } = useMetadataEntity('source'); const history = useHistory(); @@ -21,11 +21,13 @@ export function MetadataCopy () { const next = (data) => { setCopy(data); - setConfirm(true) + setConfirm(true); + onShowNav(false); }; const back = (data) => { setConfirm(false); + onShowNav(true); }; async function save (data) { diff --git a/ui/src/app/metadata/view/MetadataUpload.js b/ui/src/app/metadata/view/MetadataUpload.js index 35c0fdb56..74a114ead 100644 --- a/ui/src/app/metadata/view/MetadataUpload.js +++ b/ui/src/app/metadata/view/MetadataUpload.js @@ -70,8 +70,6 @@ export function MetadataUpload() { const { errors, isValid } = formState; - React.useEffect(() => console.log(isValid), [isValid]); - const watchFile = watch('file'); const watchUrl = watch('url'); diff --git a/ui/src/app/metadata/view/MetadataWizard.js b/ui/src/app/metadata/view/MetadataWizard.js index 23ded576d..8b6785bc9 100644 --- a/ui/src/app/metadata/view/MetadataWizard.js +++ b/ui/src/app/metadata/view/MetadataWizard.js @@ -1,6 +1,21 @@ import React from 'react'; -export function MetadataWizard () { +import { MetadataForm } from '../hoc/MetadataFormContext'; +import { MetadataSourceWizard } from '../wizard/MetadataSourceWizard'; +import { MetadataProviderWizard } from '../wizard/MetadataProviderWizard'; +import { Wizard } from '../wizard/Wizard'; - return(<>wizard); +export function MetadataWizard ({type, onShowNav}) { + + return ( + + + {type === 'source' ? + + : + + } + + + ); } \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/MetadataProviderWizard.js b/ui/src/app/metadata/wizard/MetadataProviderWizard.js new file mode 100644 index 000000000..c7d8de9d5 --- /dev/null +++ b/ui/src/app/metadata/wizard/MetadataProviderWizard.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export function MetadataProviderWizard () { + return (<>Provider Wizard); +} \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/MetadataSourceWizard.js b/ui/src/app/metadata/wizard/MetadataSourceWizard.js new file mode 100644 index 000000000..e0b7cebd4 --- /dev/null +++ b/ui/src/app/metadata/wizard/MetadataSourceWizard.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { WizardNav } from './WizardNav'; +import { MetadataWizardForm } from './MetadataWizardForm'; +import { setWizardIndexAction, useCurrentIndex, useIsFirstPage, useIsLastPage, useWizardDispatcher } from './Wizard'; +import { useMetadataDefinitionContext, useMetadataSchemaContext } from '../hoc/MetadataSchema'; +import { useMetadataSchema } from '../hooks/schema'; +import { useMetadataFormDispatcher, setFormDataAction, setFormErrorAction, useMetadataFormData, useMetadataFormErrors } from '../hoc/MetadataFormContext'; +import { MetadataConfiguration } from '../component/MetadataConfiguration'; +import { Configuration } from '../hoc/Configuration'; +import { useMetadataEntity } from '../hooks/api'; +import { useHistory } from 'react-router'; +import { removeNull } from '../../core/utility/remove_null'; + +export function MetadataSourceWizard ({ onShowNav }) { + + const { post, loading, response } = useMetadataEntity('source'); + const history = useHistory(); + + const definition = useMetadataDefinitionContext(); + const schema = useMetadataSchemaContext(); + const processed = useMetadataSchema(definition, schema); + + const formDispatch = useMetadataFormDispatcher(); + const metadata = useMetadataFormData(); + const errors = useMetadataFormErrors(); + + const isFirst = useIsFirstPage(); + const isLast = useIsLastPage(); + + const wizardDispatch = useWizardDispatcher(); + + React.useEffect(() => { + onShowNav(isFirst); + }, [isFirst, onShowNav]); + + const current = useCurrentIndex(); + + const onChange = (changes) => { + formDispatch(setFormDataAction(changes.formData)); + formDispatch(setFormErrorAction(current, changes.errors)); + // console.log('change', changes); + }; + + const onEditFromSummary = (idx) => { + wizardDispatch(setWizardIndexAction(idx)); + }; + + const onBlur = (form) => { + console.log(form); + } + + async function save () { + const body = removeNull(metadata, true); + console.log(body); + await post('', body); + if (response.ok) { + history.push('/'); + } else { + console.log(response.body); + } + } + + // console.log(errors); + + return ( + <> +
    +
    + 0 || loading } saving={loading} /> +
    +
    +
    +
    +
    + +
    +
    + {isLast && +
    +
    + + {(config) => } + +
    +
    + } + + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/MetadataWizardForm.js b/ui/src/app/metadata/wizard/MetadataWizardForm.js index 7b34b5476..8597eb577 100644 --- a/ui/src/app/metadata/wizard/MetadataWizardForm.js +++ b/ui/src/app/metadata/wizard/MetadataWizardForm.js @@ -12,7 +12,7 @@ function ErrorListTemplate () { return (<>); } -export function MetadataWizardForm ({ metadata, definition, schema, current, onChange }) { +export function MetadataWizardForm ({ metadata, definition, schema, current, onChange, onBlur = false }) { const {uiSchema} = useUiSchema(definition, schema, current); @@ -32,6 +32,7 @@ export function MetadataWizardForm ({ metadata, definition, schema, current, onC noHtml5Validate={true} onChange={(form) => onChange(form)} onSubmit={() => onSubmit()} + onBlur={() => onBlur(data)} schema={schema} uiSchema={uiSchema} FieldTemplate={templates.FieldTemplate} @@ -45,7 +46,6 @@ export function MetadataWizardForm ({ metadata, definition, schema, current, onC <>
    - ); } \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/Wizard.js b/ui/src/app/metadata/wizard/Wizard.js new file mode 100644 index 000000000..44efbfbb3 --- /dev/null +++ b/ui/src/app/metadata/wizard/Wizard.js @@ -0,0 +1,126 @@ +import React from 'react'; +import { useMetadataDefinitionContext } from '../hoc/MetadataSchema'; + +const WizardContext = React.createContext(); + +const { Provider, Consumer } = WizardContext; + +const initialState = { + current: 'common', + disabled: false, + loading: false +}; + +const WizardActions = { + SET_INDEX: 'SET INDEX' +}; + +const setWizardIndexAction = (payload) => { + return { + type: WizardActions.SET_INDEX, + payload + } +} + +function reducer(state, action) { + const {type, payload} = action; + switch (type) { + case WizardActions.SET_INDEX: + return { + ...state, + current: payload + }; + default: + return state; + } +} + +function Wizard ({children}) { + + const [state, dispatch] = React.useReducer(reducer, { + ...initialState + }); + + const contextValue = React.useMemo(() => ({ state, dispatch }), [state, dispatch]); + + return ( + {children} + ) +} + +function useWizardContext () { + return React.useContext(WizardContext); +} + +function useWizardState() { + const { state } = useWizardContext(); + return state; +} + +function useCurrentIndex() { + const { current } = useWizardState(); + + return current; +} + +function useCurrentPage() { + const definition = useMetadataDefinitionContext(); + const current = useCurrentIndex(); + + return definition.steps.find(s => s.id === current); +} + +function usePreviousPage() { + const definition = useMetadataDefinitionContext(); + const current = useCurrentIndex(); + const idx = definition.steps.findIndex(s => s.id === current); + return definition.steps[idx - 1]; +} + +function useNextPage() { + const definition = useMetadataDefinitionContext(); + const current = useCurrentIndex(); + const idx = definition.steps.findIndex(s => s.id === current); + return definition.steps[idx + 1]; +} + +function useLastPage () { + const definition = useMetadataDefinitionContext(); + return definition.steps[definition.steps.length - 1]; +} + +function useIsFirstPage () { + const definition = useMetadataDefinitionContext(); + const current = useCurrentIndex(); + return definition.steps[0].id === current; +} + +function useIsLastPage () { + const current = useCurrentIndex(); + const last = useLastPage(); + return last.id === current; +} + +function useWizardDispatcher () { + const { dispatch } = useWizardContext(); + return dispatch; +} + +export { + useWizardContext, + useWizardState, + useWizardDispatcher, + useCurrentIndex, + useCurrentPage, + useNextPage, + usePreviousPage, + useLastPage, + useIsFirstPage, + useIsLastPage, + setWizardIndexAction, + WizardActions, + Wizard, + WizardContext, + Provider as WizardProvider, + Consumer as WizardConsumer +}; \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/WizardNav.js b/ui/src/app/metadata/wizard/WizardNav.js new file mode 100644 index 000000000..9be7ed9fb --- /dev/null +++ b/ui/src/app/metadata/wizard/WizardNav.js @@ -0,0 +1,99 @@ +import React from 'react'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import { faCheck, faSpinner, faSave, faArrowCircleRight, faArrowCircleLeft } from '@fortawesome/free-solid-svg-icons'; + +import {Translate} from '../../i18n/components/translate'; +import { + useCurrentPage, + useLastPage, + useNextPage, + usePreviousPage, + setWizardIndexAction, + useWizardDispatcher +} from './Wizard'; + +export const ICONS = { + CHECK: 'CHECK', + INDEX: 'INDEX' +} + +export function WizardNav ({ disabled = false, onSave, saving }) { + + const dispatch = useWizardDispatcher(); + + const current = useCurrentPage(); + const previous = usePreviousPage(); + const next = useNextPage(); + const last = useLastPage(); + + const onSetIndex = idx => { + dispatch(setWizardIndexAction(idx)); + }; + + const currentIcon = (last && current.index === last.index) ? : current.index; + + return ( + + + ); +} \ No newline at end of file