diff --git a/ui/package.json b/ui/package.json index d3a2c9833..2964e049d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -24,6 +24,7 @@ "react-bootstrap": "^1.5.2", "react-bootstrap-typeahead": "^5.1.4", "react-dom": "^17.0.2", + "react-hook-form": "^7.5.2", "react-infinite-scroll-component": "^6.1.0", "react-jsonschema-form-layout-grid": "^2.1.0", "react-router-dom": "^5.2.0", diff --git a/ui/public/assets/schema/provider/dynamic-http.schema.json b/ui/public/assets/schema/provider/dynamic-http.schema.json index 5d0d1f428..a0a815c86 100644 --- a/ui/public/assets/schema/provider/dynamic-http.schema.json +++ b/ui/public/assets/schema/provider/dynamic-http.schema.json @@ -400,6 +400,10 @@ "id": "fieldset" }, "properties": { + "@type": { + "type": "string", + "default": "RequiredValidUntil" + }, "maxValidityInterval": { "title": "label.max-validity-interval", "description": "tooltip.max-validity-interval", @@ -416,6 +420,10 @@ "id": "fieldset" }, "properties": { + "@type": { + "type": "string", + "default": "SignatureValidation" + }, "requireSignedRoot": { "title": "label.require-signed-root", "description": "tooltip.require-signed-root", @@ -459,6 +467,10 @@ "id": "fieldset" }, "properties": { + "@type": { + "type": "string", + "default": "EntityRoleWhiteList" + }, "retainedRoles": { "title": "label.retained-roles", "description": "tooltip.retained-roles", diff --git a/ui/src/app/App.js b/ui/src/app/App.js index c0aefad0a..12244347c 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -20,6 +20,7 @@ import { Metadata } from './metadata/Metadata'; import { Notifications } from './notifications/hoc/Notifications'; import { NotificationList } from './notifications/component/NotificationList'; import { UserConfirmation, ConfirmWindow } from './core/components/UserConfirmation'; +import { NewSource } from './metadata/NewSource'; @@ -53,6 +54,7 @@ function App() { + diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index 55dff1927..e5ed72cc8 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -3,10 +3,10 @@ import { Link } from 'react-router-dom'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; -import NavDropdown from 'react-bootstrap/NavDropdown'; +import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslation } from '../../i18n/hooks'; @@ -26,13 +26,22 @@ export function Header () { - - Action - Another action - Something - - Separated link - + + + + + + + + + Metadata Source + + + + Metadata Provider + + + diff --git a/ui/src/app/core/utility/read_file_contents.js b/ui/src/app/core/utility/read_file_contents.js new file mode 100644 index 000000000..99d8a175a --- /dev/null +++ b/ui/src/app/core/utility/read_file_contents.js @@ -0,0 +1,15 @@ + + +export function readFileContents(file) { + return new Promise(function (resolve, reject) { + const fileReader = new FileReader(); + + fileReader.onload = (evt) => { + const reader = evt.target; + const txt = reader.result; + resolve(txt); + }; + fileReader.onerror = reject; + fileReader.readAsText(file); + }); +} \ No newline at end of file diff --git a/ui/src/app/metadata/Metadata.js b/ui/src/app/metadata/Metadata.js index 71ec50574..dc00b0e7f 100644 --- a/ui/src/app/metadata/Metadata.js +++ b/ui/src/app/metadata/Metadata.js @@ -16,44 +16,46 @@ export function Metadata () { let { path } = useRouteMatch(); return ( - - {(entity) => - - - - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - } /> - - - - - } - + <> + + {(entity) => + + + + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + } /> + + + + + } + + > ); } \ No newline at end of file diff --git a/ui/src/app/metadata/NewSource.js b/ui/src/app/metadata/NewSource.js new file mode 100644 index 000000000..95b7adce9 --- /dev/null +++ b/ui/src/app/metadata/NewSource.js @@ -0,0 +1,92 @@ +import React from 'react'; +import { NavLink, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom'; +import Translate from '../i18n/components/translate'; +import { MetadataSchema } from './hoc/MetadataSchema'; +import { MetadataWizard } from './view/MetadataWizard'; +import { MetadataCopy } from './view/MetadataCopy'; +import { MetadataUpload } from './view/MetadataUpload'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy, faLink, faPlusSquare } from '@fortawesome/free-solid-svg-icons'; + +export function NewSource() { + + let { path } = useRouteMatch(); + + return ( + + + + + + Add a new metadata source + + + + + How are you adding the metadata information? + + + + + + + Upload/URL + + + + + or + + + + Create + + + + + or + + + + Copy + + + + + + + + + + + + } /> + + + } /> + + + } /> + + + + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataConfiguration.js b/ui/src/app/metadata/component/MetadataConfiguration.js index 441de3935..79acec8c5 100644 --- a/ui/src/app/metadata/component/MetadataConfiguration.js +++ b/ui/src/app/metadata/component/MetadataConfiguration.js @@ -11,8 +11,6 @@ export function MetadataConfiguration ({ configuration, onEdit }) { const columns = configuration.dates?.length || 1; const width = usePropertyWidth(columns); - console.log(configuration) - return ( <> { configuration && configuration.sections.map((section, sidx) => diff --git a/ui/src/app/metadata/copy/CopySource.js b/ui/src/app/metadata/copy/CopySource.js new file mode 100644 index 000000000..19f40a821 --- /dev/null +++ b/ui/src/app/metadata/copy/CopySource.js @@ -0,0 +1,196 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import Check from 'react-bootstrap/FormCheck'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleRight, faAsterisk, faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; + +import { Translate } from '../../i18n/components/translate'; +import { EntityTypeahead } from './EntityTypeahead'; + +const sections = [ + { i18nKey: 'organizationInfo', property: 'organization' }, + { i18nKey: 'contacts', property: 'contacts' }, + { i18nKey: 'uiMduiInfo', property: 'mdui' }, + { i18nKey: 'spSsoDescriptorInfo', property: 'serviceProviderSsoDescriptor' }, + { i18nKey: 'logoutEndpoints', property: 'logoutEndpoints' }, + { i18nKey: 'securityDescriptorInfo', property: 'securityInfo' }, + { i18nKey: 'assertionConsumerServices', property: 'assertionConsumerServices' }, + { i18nKey: 'relyingPartyOverrides', property: 'relyingPartyOverrides' }, + { i18nKey: 'attributeRelease', property: 'attributeRelease' } +]; + +export function CopySource({ onNext }) { + + const [selected, setSelected] = React.useState([]); + const onSelect = (item, checked) => { + let s = [...selected]; + if (checked) { + s = [...s, item.property]; + } else { + s = s.filter(i => i === item.property); + } + setSelected(s); + }; + const onSelectAll = () => { + setSelected(sections.map(s => s.property)); + }; + const onUnselectAll = () => { + setSelected([]); + }; + + const { register, handleSubmit, control, formState, setValue, getValues } = useForm({ + mode: 'onChange', + reValidateMode: 'onBlur', + defaultValues: { + target: null, + serviceProviderName: null, + entityId: null, + properties: selected + }, + resolver: undefined, + context: undefined, + criteriaMode: "firstError", + shouldFocusError: true, + shouldUnregister: false, + }); + + const { errors, isValid } = formState; + + React.useEffect(() => { + setValue('properties', selected); + }, [selected]); + + return ( + <> + + + + + + 1 + 1. Name and EntityId + + + + onNext(getValues())} + disabled={!isValid} + aria-label="Next: Step 2, Organization information" + type="button"> + + + Finished! + + + + + Next + + + + + + + + + + + + + Select the Entity ID to copy + + + + {errors?.target?.type === 'required' && + + Entity ID to copy is Required + + } + + + + Metadata Source Name (Dashboard Display Only) + + + + {errors?.serviceProviderName?.type === 'required' && + Service Resolver Name is required + } + + + + New Entity ID + + + + {errors?.entityId && + + {errors.entityId.type === 'required' && + Entity ID is required + } + {errors.entityId.type === 'unique' && + Entity ID must be unique + } + + } + + + + + + + + + Sections to Copy? + Yes + + + + {sections.map((item, i) => + + + + onSelect(item, checked)} + checked={selected.indexOf(item.property) > -1} + /> + + + )} + + Check All Attributes + + onSelectAll()}> + + Check All Attributes + + + + + Clear All Attributes + + onUnselectAll()}> + + Clear All Attributes + + + + + + + + > + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/copy/EntityTypeahead.js b/ui/src/app/metadata/copy/EntityTypeahead.js new file mode 100644 index 000000000..be74fd1b3 --- /dev/null +++ b/ui/src/app/metadata/copy/EntityTypeahead.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { Typeahead } from 'react-bootstrap-typeahead'; +import { useController } from 'react-hook-form'; +import { useMetadataSources } from '../hooks/api'; + +export function EntityTypeahead ({control, name}) { + const { data = [] } = useMetadataSources({}, []); + const entities = React.useMemo(() => data.map(d => d.entityId), [data]); + + const { + field: { value, onChange, ...inputProps } + } = useController({ + name, + control, + rules: { required: true }, + defaultValue: "", + }); + + return ( + onChange(selected ? data.find(e => e.entityId === selected[0]) : '')} + defaultInputValue={value ? value.entityId : ''} + options={entities} + id="copySourceTypeahead" + /> + ) +} \ No newline at end of file diff --git a/ui/src/app/metadata/copy/SaveCopy.js b/ui/src/app/metadata/copy/SaveCopy.js new file mode 100644 index 000000000..afac0d6cb --- /dev/null +++ b/ui/src/app/metadata/copy/SaveCopy.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema'; +import { useMetadataConfiguration } from '../hooks/configuration'; +import { removeNull } from '../../core/utility/remove_null'; + +import { MetadataConfiguration } from '../component/MetadataConfiguration'; + +export function useCopiedConfiguration(copy, schema, definition) { + const { properties, target, serviceProviderName, entityId } = copy; + const copied = removeNull(properties.reduce((c, section) => ({ ...c, ...{ [section]: target[section] } }), {})); + const model = [{ + serviceProviderName, + entityId, + ...copied + }]; + return useMetadataConfiguration(model, schema, definition); +} + +export function SaveCopy ({ copy }) { + const definition = React.useContext(MetadataDefinitionContext); + const schema = React.useContext(MetadataSchemaContext); + + const configuration = useCopiedConfiguration(copy, schema, definition); + + return (); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js index 6861f2ae3..1506cdd4a 100644 --- a/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js +++ b/ui/src/app/metadata/domain/provider/BaseProviderDefinition.js @@ -3,28 +3,53 @@ import { DurationOptions } from '../data'; export const BaseProviderDefinition = { schemaPreprocessor: metadataFilterProcessor, - parser: (changes) => (changes.metadataFilters ? ({ - ...changes, - metadataFilters: [ - ...changes.metadataFilters.filter((filter, filterName) => ( - Object.keys(filter).length > 0 - )) - ] - }) : changes), - formatter: (changes) => (changes.metadataFilters ? ({ - ...changes, - metadataFilters: [ - {}, - {}, - {} - ] - }) : changes), + parser: (changes) => { + return (changes.metadataFilters ? ({ + ...changes, + metadataFilters: [ + ...changes.metadataFilters.filter((filter, filterName) => ( + Object.keys(filter).filter(k => k !== '@type').length > 0 + )) + ] + }) : changes) + }, + formatter: (changes, schema) => { + + const filterSchema = schema?.properties?.metadataFilters; + if (!filterSchema) { + return changes; + } + + const formatted = (changes.metadataFilters ? ({ + ...changes, + metadataFilters: Object.values(filterSchema.items).map(item => { + const filter = changes.metadataFilters.find(f => f['@type'] === item.$id); + if (filter) { + return filter; + } + return {}; + }) + }) : changes); + return formatted; + }, + display: (changes) => { + + if (!changes.metadataFilters) { + return changes; + } + return { + ...changes, + metadataFilters: { + ...(changes.metadataFilters || []).reduce((collection, filter) => ({ + ...collection, + [filter['@type']]: filter + }), {}) + } + }; + }, uiSchema: { name: { 'ui:help': 'message.must-be-unique' - }, - '@type': { - 'ui:disabled': true } } } @@ -130,6 +155,9 @@ export const MetadataFilterPluginsSchema = { 'ui:options': { classNames: 'bg-light border rounded px-4 pt-4 pb-1' }, + '@type': { + 'ui:widget': 'hidden' + }, retainedRoles: { 'ui:options': { orderable: false diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index de7e068eb..e9f2d9205 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -59,8 +59,8 @@ export default function ProviderList({ entities, reorder = true, first, last, on { provider.createdBy } - - + + diff --git a/ui/src/app/metadata/domain/source/SourceDefinition.js b/ui/src/app/metadata/domain/source/SourceDefinition.js index 80398a09b..6f9fb0070 100644 --- a/ui/src/app/metadata/domain/source/SourceDefinition.js +++ b/ui/src/app/metadata/domain/source/SourceDefinition.js @@ -15,6 +15,8 @@ export const SourceBase = { formatter: (changes, schema) => changes, + display: (changes) => changes, + getValidators: (entityIdList) => { const validators = { '/': (value, property, form_current) => { diff --git a/ui/src/app/metadata/hoc/MetadataFormContext.js b/ui/src/app/metadata/hoc/MetadataFormContext.js index f7f9c2ef5..60e8146f6 100644 --- a/ui/src/app/metadata/hoc/MetadataFormContext.js +++ b/ui/src/app/metadata/hoc/MetadataFormContext.js @@ -3,7 +3,7 @@ import React from 'react'; import uniq from 'lodash/uniq'; import intersection from 'lodash/intersection'; -import { MetadataDefinitionContext } from './MetadataSchema'; +import { MetadataDefinitionContext, MetadataSchemaContext } from './MetadataSchema'; import { MetadataObjectContext } from './MetadataSelector'; @@ -102,7 +102,8 @@ function usePagesWithErrors(definition) { function useFormattedMetadata() { const definition = React.useContext(MetadataDefinitionContext); - return definition.formatter(React.useContext(MetadataObjectContext)) + const schema = React.useContext(MetadataSchemaContext); + return definition.formatter(React.useContext(MetadataObjectContext), schema); } export { diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 2a8f4e5f8..322eafd1e 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -1,18 +1,13 @@ import React from 'react'; -import { useParams } from 'react-router'; import { getDefinition } from '../domain/index'; import useFetch from 'use-http'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); -export function MetadataSchema({ entity, children }) { +export function MetadataSchema({ type, children }) { - const { type } = useParams(); - - const definition = React.useMemo(() => getDefinition( - type === 'source' ? type : entity['@type'] - ), [type, entity]); + const definition = React.useMemo(() => getDefinition(type), [type]); const { get, response } = useFetch(``, {}, []); diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js index 897ed4cbf..e72747c7e 100644 --- a/ui/src/app/metadata/hooks/api.js +++ b/ui/src/app/metadata/hooks/api.js @@ -59,4 +59,8 @@ export function useMetadataHistory(type, id, opts = {}, i) { // EntityDescriptor/d07d6122-0dd2-433e-baec-b76413b4c842/Versions // MetadataResolvers/4161d661-2be7-4110-9e91-539669a691e3/Versions +} + +export function useMetadataSources(opts = {}, onMount) { + return useFetch(`${API_BASE_PATH}${getMetadataListPath('source')}`, opts, onMount); } \ No newline at end of file diff --git a/ui/src/app/metadata/hooks/configuration.js b/ui/src/app/metadata/hooks/configuration.js index 2095766bd..da0ba0c28 100644 --- a/ui/src/app/metadata/hooks/configuration.js +++ b/ui/src/app/metadata/hooks/configuration.js @@ -14,8 +14,6 @@ export const getLimitedConfigurationsFn = (configurations, limited) => { export function useMetadataConfiguration(models, schema, definition, limited = false) { - console.log(models, schema, definition) - if (!models || !schema || !definition) { return {}; } diff --git a/ui/src/app/metadata/hooks/utility.js b/ui/src/app/metadata/hooks/utility.js index 1ff365e91..035fb24f1 100644 --- a/ui/src/app/metadata/hooks/utility.js +++ b/ui/src/app/metadata/hooks/utility.js @@ -12,6 +12,7 @@ export function getStepProperty(property, model, definitions) { if (!property) { return null; } property = property.$ref ? { ...property, ...getDefinition(property.$ref, definitions) } : property; return { + ...property, name: property.title, value: model, type: property.type, @@ -84,7 +85,7 @@ export const getConfigurationSections = (models, definition, schema) => { label: step.label, properties: getStepProperties( getSplitSchema(schema, step), - definition.formatter({}), + definition.display({}), schema.definitions || {} ) }); @@ -93,7 +94,7 @@ export const getConfigurationSections = (models, definition, schema) => { .map((section) => { return { ...section, - properties: assignValueToProperties(models, section.properties, definition) + properties: assignValueToProperties(models, section.properties, definition, schema) }; }) .map((section) => ({ @@ -114,7 +115,7 @@ const getDifferences = (models, prop) => { }); }; -export const assignValueToProperties = (models, properties, definition) => { +export const assignValueToProperties = (models, properties, definition, schema) => { return properties.map(prop => { const differences = getDifferences(models, prop); @@ -137,9 +138,10 @@ export const assignValueToProperties = (models, properties, definition) => { return { ...prop, properties: assignValueToProperties( - models.map(model => definition.formatter(model)[prop.id] || {}), + models.map(model => definition.display(model, schema)[prop.id] || {}), prop.properties, - definition + definition, + schema ), differences: getDifferences(models, prop) }; diff --git a/ui/src/app/metadata/view/MetadataCopy.js b/ui/src/app/metadata/view/MetadataCopy.js new file mode 100644 index 000000000..03031cad8 --- /dev/null +++ b/ui/src/app/metadata/view/MetadataCopy.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleRight } from '@fortawesome/free-solid-svg-icons'; + +import { Translate } from '../../i18n/components/translate'; +import { MetadataSchema } from '../hoc/MetadataSchema'; + +import {CopySource} from '../copy/CopySource'; +import { SaveCopy } from '../copy/SaveCopy'; + +export function MetadataCopy () { + + const next = (data) => { + setCopy(data); + }; + + const [copy, setCopy] = React.useState(); + + return ( + + {!copy && + + } + {copy && + + + + } + next()}> + Next + + + + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/view/MetadataUpload.js b/ui/src/app/metadata/view/MetadataUpload.js new file mode 100644 index 000000000..687bfa270 --- /dev/null +++ b/ui/src/app/metadata/view/MetadataUpload.js @@ -0,0 +1,138 @@ +import React from 'react'; +import Form from 'react-bootstrap/Form'; +import { useForm } from "react-hook-form"; +import { useHistory } from 'react-router-dom'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import { faAsterisk, faSave } from '@fortawesome/free-solid-svg-icons'; + +import Translate from '../../i18n/components/translate'; +import { readFileContents } from '../../core/utility/read_file_contents'; +import { get_cookie } from '../../core/utility/get_cookie'; +import { getMetadataPath } from '../hooks/api'; +import { useNotificationDispatcher, createNotificationAction } from '../../notifications/hoc/Notifications'; +import API_BASE_PATH from '../../App.constant'; + +export function MetadataUpload() { + + const history = useHistory(); + const dispatch = useNotificationDispatcher(); + + async function save({serviceProviderName, file, url}) { + const f = file.length > 0 ? file[0] : null; + if (f) { + const body = await readFileContents(f); + const response = await fetch(`${API_BASE_PATH}${getMetadataPath('source')}?spName=${serviceProviderName}`, { + method: 'POST', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/xml', + 'X-XSRF-TOKEN': get_cookie('XSRF-TOKEN') + }, + body + }); + + if (response.ok) { + history.push('/dashboard'); + } else { + const message = await response.json(); + dispatch(createNotificationAction(`${message.errorCode}: Unable to upload file ... ${message.errorMessage}`, 'danger', 5000)); + } + } + } + + const { register, handleSubmit, watch, formState } = useForm({ + mode: 'onChange', + reValidateMode: 'onBlur', + defaultValues: {}, + resolver: undefined, + context: undefined, + criteriaMode: "firstError", + shouldFocusError: true, + shouldUnregister: false, + }); + + const { errors, isValid } = formState; + + // React.useEffect(() => console.log(formState), [formState]); + + const watchFile = watch('file'); + const watchUrl = watch('url'); + + // console.log(watchFile, errors); + + return ( + + + + + + + 1 + 1. Name and Upload Url + + + + + + + Save + + + + + Save + + + + + + + + Service Resolver Name (Dashboard Display Only) + + + + {errors?.serviceProviderName?.type === 'required' && + + Service Resolver Name is required + + } + + + 0 ? + watchFile[0].name + : + Select Resolver Metadata File + } + {...register('file')} + custom + /> + + + — + OR + — + + + Service Resolver Metadata URL + 0 } type="text" className="form-control" placeholder="" {...register('url')} /> + + + Note: You can only import a file with a single entityID (EntityDescriptor element) in it. Anything more in that file will result in an error. + + + + + + + ) +} \ No newline at end of file diff --git a/ui/src/app/metadata/view/MetadataWizard.js b/ui/src/app/metadata/view/MetadataWizard.js new file mode 100644 index 000000000..23ded576d --- /dev/null +++ b/ui/src/app/metadata/view/MetadataWizard.js @@ -0,0 +1,6 @@ +import React from 'react'; + +export function MetadataWizard () { + + return(<>wizard>); +} \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/MetadataWizardForm.js b/ui/src/app/metadata/wizard/MetadataWizardForm.js new file mode 100644 index 000000000..7b34b5476 --- /dev/null +++ b/ui/src/app/metadata/wizard/MetadataWizardForm.js @@ -0,0 +1,51 @@ +import React from 'react'; + +import Form from '@rjsf/bootstrap-4'; + +import { fields, widgets } from '../../form/component'; +import { templates } from '../../form/component'; +import { useUiSchema } from '../hooks/schema'; + +const invisErrors = ['const', 'oneOf'] + +function ErrorListTemplate () { + return (<>>); +} + +export function MetadataWizardForm ({ metadata, definition, schema, current, onChange }) { + + const {uiSchema} = useUiSchema(definition, schema, current); + + const [data, setData] = React.useState(metadata); + + React.useEffect(() => setData(metadata), [metadata, definition]); + + const onSubmit = () => {}; + + const transformErrors = (errors) => { + return errors.filter(e => invisErrors.indexOf(e.name) === -1); + } + return ( + <> + + onChange(form)} + onSubmit={() => onSubmit()} + schema={schema} + uiSchema={uiSchema} + FieldTemplate={templates.FieldTemplate} + ObjectFieldTemplate={templates.ObjectFieldTemplate} + ArrayFieldTemplate={templates.ArrayFieldTemplate} + fields={fields} + widgets={widgets} + liveValidate={true} + transformErrors={transformErrors} + ErrorList={ErrorListTemplate}> + <>> + + + + > + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/wizard/WizardButton.js b/ui/src/app/metadata/wizard/WizardButton.js new file mode 100644 index 000000000..d77308de0 --- /dev/null +++ b/ui/src/app/metadata/wizard/WizardButton.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export function WizardButton ({ ...props }) { + +} \ No newline at end of file diff --git a/ui/src/app/notifications/hoc/Notifications.js b/ui/src/app/notifications/hoc/Notifications.js index c2b910770..874a3fb28 100644 --- a/ui/src/app/notifications/hoc/Notifications.js +++ b/ui/src/app/notifications/hoc/Notifications.js @@ -22,7 +22,7 @@ export const NotificationTypes = { INFO: 'info' }; -export const createNotificationAction = (body, type = NotificationTypes.SUCCESS, timeout=20000) => { +export const createNotificationAction = (body, type = NotificationTypes.SUCCESS, timeout=5000) => { return { type: NotificationActions.ADD_NOTIFICATION, payload: { @@ -74,9 +74,15 @@ function Notifications ({ children }) { ); } +function useNotificationDispatcher() { + const {dispatch} = React.useContext(NotificationContext); + return dispatch; +} + export { Notifications, NotificationContext, + useNotificationDispatcher, Provider as NotificationProvider, Consumer as NotificationConsumer }; \ No newline at end of file diff --git a/ui/src/theme/project/buttons.scss b/ui/src/theme/project/buttons.scss index 25948a509..c1e8c5d79 100644 --- a/ui/src/theme/project/buttons.scss +++ b/ui/src/theme/project/buttons.scss @@ -14,6 +14,11 @@ } } +.resolver-nav-option { + min-width: 160px; + height: 100%; +} + .nav.nav-wizard { .nav-item { margin-right: $custom-control-spacer-x * 3; diff --git a/ui/src/theme/project/forms.scss b/ui/src/theme/project/forms.scss index 75428b7b0..b200b8191 100644 --- a/ui/src/theme/project/forms.scss +++ b/ui/src/theme/project/forms.scss @@ -106,6 +106,18 @@ select.form-control:disabled { border: 1px solid #ced4da; } +mark { + background: transparent; + font-weight: bold; + padding: 0; +} + +.dropdown-item.active { + &, & > mark { + color: white; + } +} + @media only screen and (max-width: 1200px) { .form-section:not(:first-child) { border-left: 0px; diff --git a/ui/yarn.lock b/ui/yarn.lock index d0a681d71..9d5b6062c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -9703,6 +9703,11 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-hook-form@^7.5.2: + version "7.5.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.5.2.tgz#4f8da6d22ae67aa18ce170a0980bd7e1e7bd6917" + integrity sha512-fRA6TieC+wsumcdAM7OTeWSa+s+qG2ZlWfTXB2gpOwVeWpJxDwUpZZRfZdU9Bj6TEn1v/ZPJg/1xbmUGVI+MWA== + react-icons@^3.10.0: version "3.11.0" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-3.11.0.tgz#2ca2903dfab8268ca18ebd8cc2e879921ec3b254"