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'] }
+
+
+
+
+
+
+
+ >);
+}
+
+/*
+
+
+*/
\ 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) =>
+
+
+
+ |
+ |
+ Filter Name |
+ Filter Type |
+ Enabled? |
+ Edit |
+ Delete |
+
+
+
+ {ordered.map((filter, i) =>
+
+ |
+
+
+
+
+ |
+ {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%;