-
-
-
- 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