From 6f3ec2fa40f2baaeb2bc34571ed48c24306e4f2d Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 28 Apr 2021 15:00:25 -0700 Subject: [PATCH] Adding filter list --- ui/src/app/App.constant.js | 5 + ui/src/app/core/utility/is_valid_regex.js | 11 ++ ui/src/app/core/utility/remove_null.js | 23 ++++ ui/src/app/dashboard/component/Ordered.js | 10 +- .../app/dashboard/container/ProvidersTab.js | 2 +- ui/src/app/metadata/Metadata.js | 4 +- .../app/metadata/component/MetadataOptions.js | 11 +- .../component/properties/ObjectProperty.js | 6 +- .../component/properties/PropertyValue.js | 2 +- .../EntityAttributesFilterDefinition.js | 109 ++++++++++++++++++ .../domain/filter/NameIdFilterDefinition.js | 81 +++++++++++++ .../domain/filter/component/MetadataFilter.js | 0 .../MetadataFilterConfigurationList.js | 46 ++++++++ .../MetadataFilterConfigurationListItem.js | 67 +++++++++++ .../component/MetadataFilterEditorList.js | 88 ++++++++++++++ .../filter/component/MetadataFilters.js | 26 +++++ ui/src/app/metadata/domain/filter/index.js | 16 +++ ui/src/app/metadata/domain/index.js | 6 +- .../domain/provider/component/ProviderList.js | 76 ------------ ui/src/app/metadata/hoc/MetadataSchema.js | 9 +- ui/src/app/metadata/hoc/MetadataSelector.js | 2 +- ui/src/app/metadata/hooks/configuration.js | 5 +- ui/src/theme/project/filters.scss | 15 +++ ui/src/theme/project/index.scss | 5 +- 24 files changed, 518 insertions(+), 107 deletions(-) create mode 100644 ui/src/app/core/utility/is_valid_regex.js create mode 100644 ui/src/app/core/utility/remove_null.js create mode 100644 ui/src/app/metadata/domain/filter/EntityAttributesFilterDefinition.js create mode 100644 ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilter.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js create mode 100644 ui/src/app/metadata/domain/filter/component/MetadataFilters.js create mode 100644 ui/src/app/metadata/domain/filter/index.js create mode 100644 ui/src/theme/project/filters.scss diff --git a/ui/src/app/App.constant.js b/ui/src/app/App.constant.js index 206ee74c7..c08fda543 100644 --- a/ui/src/app/App.constant.js +++ b/ui/src/app/App.constant.js @@ -1,2 +1,7 @@ export const API_BASE_PATH = 'api'; + +export const FILTER_PLUGIN_TYPES = ['RequiredValidUntil', 'SignatureValidation', 'EntityRoleWhiteList']; + + + export default API_BASE_PATH; diff --git a/ui/src/app/core/utility/is_valid_regex.js b/ui/src/app/core/utility/is_valid_regex.js new file mode 100644 index 000000000..3f18d0191 --- /dev/null +++ b/ui/src/app/core/utility/is_valid_regex.js @@ -0,0 +1,11 @@ +export function isValidRegex(pattern) { + if (!pattern) { + return false; + } + try { + new RegExp(pattern); + } catch (err) { + return false; + } + return true; +}; diff --git a/ui/src/app/core/utility/remove_null.js b/ui/src/app/core/utility/remove_null.js new file mode 100644 index 000000000..30f342172 --- /dev/null +++ b/ui/src/app/core/utility/remove_null.js @@ -0,0 +1,23 @@ +export function checkByType(value) { + switch (typeof value) { + case 'object': { + return Object.keys(value).filter(k => !!value[k]).length > 0; + } + default: { + return true; + } + } +} + +export function removeNull(attribute, discardObjects = false) { + if (!attribute) { return {}; } + let removed = Object.keys(attribute).reduce((coll, val, index) => { + if (attribute[val] !== null) { + if (!discardObjects || checkByType(attribute[val])) { + coll[val] = attribute[val]; + } + } + return coll; + }, {}); + return removed; +} \ No newline at end of file diff --git a/ui/src/app/dashboard/component/Ordered.js b/ui/src/app/dashboard/component/Ordered.js index 0fe8b1675..76d713202 100644 --- a/ui/src/app/dashboard/component/Ordered.js +++ b/ui/src/app/dashboard/component/Ordered.js @@ -5,10 +5,6 @@ import last from 'lodash/last'; import API_BASE_PATH from '../../App.constant'; import { array_move } from '../../core/utility/array_move'; -const orderPaths = { - provider: `/MetadataResolversPositionOrder` -}; - export const getId = (entity) => { return entity.resourceId ? entity.resourceId : entity.id; }; @@ -24,7 +20,7 @@ export const mergeOrderFn = (entities, order) => { return ordered; }; -export function Ordered ({type = 'provider', entities, children}) { +export function Ordered ({path = '/MetadataResolvers', entities, children}) { const orderEntities = (orderById, list) => { setOrdered(mergeOrderFn(list, orderById)); @@ -41,7 +37,7 @@ export function Ordered ({type = 'provider', entities, children}) { const [lastId, setLastId] = React.useState(null); async function changeOrder(resourceIds) { - await post(`${orderPaths[type]}`, { + await post(path, { resourceIds }); if (response.ok) { @@ -62,7 +58,7 @@ export function Ordered ({type = 'provider', entities, children}) { }; async function loadOrder () { - const o = await get(`${orderPaths[type]}`); + const o = await get(path); if (response.ok) { const ids = o.resourceIds; setOrder(ids); diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js index 27d89ece5..7da1c306f 100644 --- a/ui/src/app/dashboard/container/ProvidersTab.js +++ b/ui/src/app/dashboard/container/ProvidersTab.js @@ -39,7 +39,7 @@ export function ProvidersTab () {
- + {(ordered, first, last, onOrderUp, onOrderDown) => {(searched) => + {(entity) => - + @@ -37,6 +38,7 @@ export function Metadata () { + } ); } \ No newline at end of file diff --git a/ui/src/app/metadata/component/MetadataOptions.js b/ui/src/app/metadata/component/MetadataOptions.js index 13ce53b00..439d449e5 100644 --- a/ui/src/app/metadata/component/MetadataOptions.js +++ b/ui/src/app/metadata/component/MetadataOptions.js @@ -15,6 +15,9 @@ import { MetadataConfiguration } from './MetadataConfiguration'; import { useMetadataConfiguration } from '../hooks/configuration'; import { MetadataViewToggle } from './MetadataViewToggle'; import { DeleteSourceConfirmation } from '../domain/source/component/DeleteSourceConfirmation'; +import { MetadataFilters } from '../domain/filter/component/MetadataFilters'; +import { MetadataFilterConfigurationList } from '../domain/filter/component/MetadataFilterConfigurationList'; +import { MetadataFilterTypes } from '../domain/filter'; export function MetadataOptions () { @@ -87,11 +90,9 @@ export function MetadataOptions () {
- {/**/} + + + } diff --git a/ui/src/app/metadata/component/properties/ObjectProperty.js b/ui/src/app/metadata/component/properties/ObjectProperty.js index f9a03aaac..8494d24a7 100644 --- a/ui/src/app/metadata/component/properties/ObjectProperty.js +++ b/ui/src/app/metadata/component/properties/ObjectProperty.js @@ -5,15 +5,13 @@ import { ArrayProperty } from './ArrayProperty'; import Translate from '../../../i18n/components/translate'; export function ObjectProperty ({ property, columns, onPreview }) { - - const getProperty = (prop, idx) => { switch(prop.type) { case 'array': return case 'object': return - {prop.name &&
} + {prop.name &&
}
default: @@ -32,7 +30,7 @@ export function ObjectProperty ({ property, columns, onPreview }) { -
{{ prop.name | translate }}
+
{{ prop.name | translate }}
diff --git a/ui/src/app/metadata/component/properties/PropertyValue.js b/ui/src/app/metadata/component/properties/PropertyValue.js index 6ca8e8f53..566489d77 100644 --- a/ui/src/app/metadata/component/properties/PropertyValue.js +++ b/ui/src/app/metadata/component/properties/PropertyValue.js @@ -12,7 +12,7 @@ export function PropertyValue ({ name, value, columns }) { return ( <> - { name && value && value !== false ? + { name && value !== null && value !== undefined ? <> { + let errors; + // iterate all customer + Object.keys(value).forEach((key) => { + const item = value[key]; + const validatorKey = `/${key}`; + const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; + const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + if (error && error.invalidate) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }, + '/name': (value, property, form) => { + const err = namesList.indexOf(value) > -1 ? { + code: 'INVALID_NAME', + path: `#${property.path}`, + message: 'message.name-must-be-unique', + params: [value], + invalidate: true + } : null; + return err; + }, + '/relyingPartyOverrides': (value, property, form) => { + if (!value.signAssertion && value.dontSignResponse) { + return { + code: 'INVALID_SIGNING', + path: `#${property.path}`, + message: 'message.invalid-signing', + params: [value], + invalidate: false + }; + } + return null; + }, + '/entityAttributesFilterTarget': (value, property, form) => { + if (!form || !form.value || !form.value.entityAttributesFilterTarget || + form.value.entityAttributesFilterTarget.entityAttributesFilterTargetType !== 'REGEX') { + return null; + } + return isValidRegex(value.value[0]) ? null : { + code: 'INVALID_REGEX', + path: `#${property.path}`, + message: 'message.invalid-regex-pattern', + params: [value.value[0]], + invalidate: true + }; + }, + }; + return validators; + }, + parser: (changes) => { + return { + ...changes, + relyingPartyOverrides: removeNull(changes) + }; + }, + formatter: (changes) => changes +}; + + +export const EntityAttributesFilterEditor= { + ...EntityAttributesFilterWizard, + steps: [ + { + id: 'common', + label: 'label.target', + index: 1, + fields: [ + 'name', + '@type', + 'resourceId', + 'filterEnabled', + 'entityAttributesFilterTarget' + ] + }, + { + id: 'options', + label: 'label.options', + index: 2, + initialValues: [], + fields: [ + 'relyingPartyOverrides' + ] + }, + { + id: 'attributes', + label: 'label.attributes', + index: 3, + fields: [ + 'attributeRelease' + ] + } + ] +}; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js new file mode 100644 index 000000000..7ff51a827 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/NameIdFilterDefinition.js @@ -0,0 +1,81 @@ +import API_BASE_PATH from "../../../App.constant"; +import { isValidRegex } from "../../../core/utility/is_valid_regex"; + +export const NameIDFilterWizard = { + label: 'NameIDFormat', + type: 'NameIDFormat', + schema: `${API_BASE_PATH}/ui/NameIdFormatFilter`, + steps: [], + //validatorParams: [getFilterNames], + getValidators(namesList) { + const validators = { + '/': (value, property, form_current) => { + let errors; + // iterate all customer + Object.keys(value).forEach((key) => { + const item = value[key]; + const validatorKey = `/${key}`; + const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; + const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }, + '/name': (value, property, form) => { + const err = namesList.indexOf(value) > -1 ? { + code: 'INVALID_NAME', + path: `#${property.path}`, + message: 'message.name-must-be-unique', + params: [value] + } : null; + return err; + }, + '/nameIdFormatFilterTarget': (value, property, form) => { + if (!form || !form.value || !form.value.nameIdFormatFilterTarget || + form.value.nameIdFormatFilterTarget.nameIdFormatFilterTargetType !== 'REGEX') { + return null; + } + return isValidRegex(value.value[0]) ? null : { + code: 'INVALID_REGEX', + path: `#${property.path}`, + message: 'message.invalid-regex-pattern', + params: [value.value[0]] + }; + } + }; + return validators; + }, + parser: (changes) => changes, + formatter: (changes) => changes +}; + +export const NameIDFilterEditor = { + ...NameIDFilterWizard, + steps: [ + { + id: 'common', + label: 'label.target', + index: 1, + fields: [ + 'name', + 'filterEnabled', + '@type', + 'resourceId', + 'nameIdFormatFilterTarget' + ] + }, + { + id: 'options', + label: 'label.options', + index: 1, + initialValues: [], + fields: [ + 'removeExistingFormats', + 'formats' + ] + } + ] +}; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilter.js b/ui/src/app/metadata/domain/filter/component/MetadataFilter.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js new file mode 100644 index 000000000..86dae0368 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationList.js @@ -0,0 +1,46 @@ +import React from 'react'; + +import { Ordered } from '../../../../dashboard/component/Ordered'; +import { Translate } from '../../../../i18n/components/translate'; +import { MetadataFiltersContext } from './MetadataFilters'; + +import { MetadataFilterConfigurationListItem } from './MetadataFilterConfigurationListItem'; + +export function MetadataFilterConfigurationList ({provider}) { + const filters = React.useContext(MetadataFiltersContext); + const removeFilter = () => {} + const editable = true; + + return ( + + {(ordered, first, last, onOrderUp, onOrderDown) => + <> + {ordered.length > 0 && +
    + {ordered.map((filter, i) => +
  • + +
  • + )} +
+ } + { filters && filters.length < 1 && +
+

No Filters

+

No filters have been added to this Metadata Provider

+
+ } + + } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js new file mode 100644 index 000000000..e3f863f40 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterConfigurationListItem.js @@ -0,0 +1,67 @@ +import React from 'react'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleDown, faArrowCircleUp } from '@fortawesome/free-solid-svg-icons'; + +import { Translate } from '../../../../i18n/components/translate'; + +export function MetadataFilterConfigurationListItem ({ filter, isLast, isFirst, onOrderUp, onOrderDown, editable, onRemove, index }) { + const [open, setOpen] = React.useState(false); + + return (<> +
+ { index + 1 } + {editable && +
+ + +
+ } + + { filter['@type'] } + + + + + +
+ + ); +} + +/* + +
+
+
+
+ +   + Edit + + +
+
+ + +
+*/ \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js new file mode 100644 index 000000000..de698b027 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilterEditorList.js @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faArrowCircleDown, faArrowCircleUp } from '@fortawesome/free-solid-svg-icons'; + +import { Ordered } from '../../../../dashboard/component/Ordered'; +import { Translate } from '../../../../i18n/components/translate'; +import { MetadataFiltersContext } from './MetadataFilters'; + +export function MetadataFilterEditorList ({provider}) { + const filters = React.useContext(MetadataFiltersContext); + + const onToggleEnabled = () => {} + const removeFilter = () => {} + + const disabled = false; + + return ( + + {(ordered, first, last, onOrderUp, onOrderDown) => + + + + + + + + + + + + + + {ordered.map((filter, i) => + + + + + + + + + + )} + +
Filter NameFilter TypeEnabled?EditDelete
+
+ + +
+
{i + 1}{filter.name}{filter['@type']} +
+
+ onToggleEnabled(filter)} /> + +
+ {filter.disabled && } +
+
+ + + Edit + + + +
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js new file mode 100644 index 000000000..6969dfcb7 --- /dev/null +++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { useMetadataEntities } from '../../../hooks/api'; + +export const MetadataFiltersContext = React.createContext(); + +export function MetadataFilters ({ providerId, types = [], children }) { + + const { get, response } = useMetadataEntities('provider'); + + const [filters, setFilters] = React.useState([]); + + async function loadFilters(id) { + const list = await get(`/${id}/Filters`); + if (response.ok) { + setFilters(list.filter(f => types.length > 1 ? types.indexOf(f['@type']) > -1 : true)); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadFilters(providerId) }, [providerId]); + + + return ( + {children} + ); +} \ No newline at end of file diff --git a/ui/src/app/metadata/domain/filter/index.js b/ui/src/app/metadata/domain/filter/index.js new file mode 100644 index 000000000..f51b8bc5a --- /dev/null +++ b/ui/src/app/metadata/domain/filter/index.js @@ -0,0 +1,16 @@ +import { EntityAttributesFilterWizard, EntityAttributesFilterEditor } from './EntityAttributesFilterDefinition'; +import { NameIDFilterWizard, NameIDFilterEditor } from './NameIdFilterDefinition'; + +export const MetadataFilterWizardTypes = { + EntityAttributes: EntityAttributesFilterWizard, + NameIDFormat: NameIDFilterWizard +}; + +export const MetadataFilterEditorTypes = [ + EntityAttributesFilterEditor, + NameIDFilterEditor +]; + +export const MetadataFilterTypes = [ + ...MetadataFilterEditorTypes.map((t) => t.type) +]; \ No newline at end of file diff --git a/ui/src/app/metadata/domain/index.js b/ui/src/app/metadata/domain/index.js index 5cd284162..6565c1e85 100644 --- a/ui/src/app/metadata/domain/index.js +++ b/ui/src/app/metadata/domain/index.js @@ -1,3 +1,4 @@ +import { MetadataFilterEditorTypes } from './filter'; import { MetadataProviderEditorTypes } from './provider'; import { SourceEditor } from "./source/SourceDefinition"; @@ -8,8 +9,9 @@ export const editors = { export const ProviderEditorTypes = [ ...MetadataProviderEditorTypes ]; -export const FilterEditorTypes = []; - +export const FilterEditorTypes = [ + ...MetadataFilterEditorTypes +]; export const getDefinition = (type) => ProviderEditorTypes.find(def => def.type === type) || diff --git a/ui/src/app/metadata/domain/provider/component/ProviderList.js b/ui/src/app/metadata/domain/provider/component/ProviderList.js index 9c353e0de..dc8171bc1 100644 --- a/ui/src/app/metadata/domain/provider/component/ProviderList.js +++ b/ui/src/app/metadata/domain/provider/component/ProviderList.js @@ -51,25 +51,6 @@ export default function ProviderList({ entities, reorder = true, first, last, on - - - {/* -
-   - - - - */ } {provider.name} @@ -91,60 +72,3 @@ export default function ProviderList({ entities, reorder = true, first, last, on ); } - -/* - - - {{ resolver.name }} - - - {{ resolver.name }} - - {{ resolver.getDisplayId() }} - {{ resolver.createdBy ? resolver.createdBy : '—' }} - - {{ resolver.getCreationDate() ? (resolver.getCreationDate() | customDate) : '—' }} - - - - - - Incomplete Form - - - - - - - - - {{ (resolver.enabled ? 'value.enabled' : 'value.disabled') | translate }} - - - - - - - - */ \ No newline at end of file diff --git a/ui/src/app/metadata/hoc/MetadataSchema.js b/ui/src/app/metadata/hoc/MetadataSchema.js index 54b29c681..2dcbb76ae 100644 --- a/ui/src/app/metadata/hoc/MetadataSchema.js +++ b/ui/src/app/metadata/hoc/MetadataSchema.js @@ -2,20 +2,17 @@ import React from 'react'; import { useParams } from 'react-router'; import { useMetadataSchema } from '../hooks/api'; import { getDefinition } from '../domain/index'; -import { MetadataObjectContext } from './MetadataSelector'; export const MetadataSchemaContext = React.createContext(); export const MetadataDefinitionContext = React.createContext(); -export function MetadataSchema({ children }) { - - const metadata = React.useContext(MetadataObjectContext); +export function MetadataSchema({ entity, children }) { const { type } = useParams(); const definition = React.useMemo(() => getDefinition( - type === 'source' ? type : metadata['@type'] - ), [type, metadata]); + type === 'source' ? type : entity['@type'] + ), [type, entity]); const { get, response } = useMetadataSchema(); diff --git a/ui/src/app/metadata/hoc/MetadataSelector.js b/ui/src/app/metadata/hoc/MetadataSelector.js index be7ab62c6..88ebc82a1 100644 --- a/ui/src/app/metadata/hoc/MetadataSelector.js +++ b/ui/src/app/metadata/hoc/MetadataSelector.js @@ -29,7 +29,7 @@ export function MetadataSelector ({ children }) { {type && {metadata && metadata.version && - {children} + {children(metadata)} } } diff --git a/ui/src/app/metadata/hooks/configuration.js b/ui/src/app/metadata/hooks/configuration.js index d7a16edd4..b65b72a0f 100644 --- a/ui/src/app/metadata/hooks/configuration.js +++ b/ui/src/app/metadata/hooks/configuration.js @@ -7,5 +7,8 @@ export function useMetadataConfiguration(models) { const definition = React.useContext(MetadataDefinitionContext); const schema = React.useContext(MetadataSchemaContext); - return getConfigurationSections(models, definition, schema); + const processed = definition.schemaPreprocessor ? + definition.schemaPreprocessor(schema) : schema; + + return getConfigurationSections(models, definition, processed); } \ No newline at end of file diff --git a/ui/src/theme/project/filters.scss b/ui/src/theme/project/filters.scss new file mode 100644 index 000000000..e38d11791 --- /dev/null +++ b/ui/src/theme/project/filters.scss @@ -0,0 +1,15 @@ +.filter-list.table { + .td-sm { + max-width: 100px; + } + .td-xs { + max-width: 20px; + } + .td-lg { + width: 30%; + } + + td { + vertical-align: middle; + } +} \ No newline at end of file diff --git a/ui/src/theme/project/index.scss b/ui/src/theme/project/index.scss index e1640fd10..2fcd0651a 100644 --- a/ui/src/theme/project/index.scss +++ b/ui/src/theme/project/index.scss @@ -7,9 +7,10 @@ @import './typography'; @import './list'; @import './tabs'; -@import './table.scss'; +@import './table'; @import './utility'; -@import './notifications.scss'; +@import './notifications'; +@import './filters'; html, body { height: 100%;