diff --git a/ui/src/app/dashboard/view/ActionsTab.js b/ui/src/app/dashboard/view/ActionsTab.js
new file mode 100644
index 000000000..76d1ff592
--- /dev/null
+++ b/ui/src/app/dashboard/view/ActionsTab.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { SourcesActions } from '../../admin/container/SourcesActions';
+import UserActions from '../../admin/container/UserActions';
+
+import Translate from '../../i18n/components/translate';
+
+export function ActionsTab({ sources, users, reloadSources, reloadUsers }) {
+
+ return (
+ <>
+
+
+
+
+
+ Enable Metadata Sources
+
+
+
+
+
+
+
+
+
+
+
+
+
+ User Access Request
+
+
+
+
+
+
+ >
+
+ );
+}
+
+export default ActionsTab;
\ No newline at end of file
diff --git a/ui/src/app/dashboard/view/AdminTab.js b/ui/src/app/dashboard/view/AdminTab.js
new file mode 100644
index 000000000..a458c810a
--- /dev/null
+++ b/ui/src/app/dashboard/view/AdminTab.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import useFetch from 'use-http';
+import UserManagement from '../../admin/container/UserManagement';
+import UserMaintenance from '../../admin/component/UserMaintenance';
+import API_BASE_PATH from '../../App.constant';
+
+import Translate from '../../i18n/components/translate';
+
+export function AdminTab () {
+
+ const [users, setUsers] = React.useState([]);
+
+ const { get, response } = useFetch(`${API_BASE_PATH}/admin/users`, {
+ cachePolicy: 'no-cache'
+ }, []);
+
+ async function loadUsers() {
+ const users = await get('')
+ if (response.ok) {
+ setUsers(users);
+ }
+ }
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ React.useEffect(() => {
+ loadUsers();
+ }, []);
+
+
+ return (
+
+
+
+
+
+ {(u, roles, onChangeUserRole, onDeleteUser) =>
+ }
+
+
+
+
+ );
+}
+
+export default AdminTab;
\ No newline at end of file
diff --git a/ui/src/app/dashboard/view/Dashboard.js b/ui/src/app/dashboard/view/Dashboard.js
new file mode 100644
index 000000000..97b070874
--- /dev/null
+++ b/ui/src/app/dashboard/view/Dashboard.js
@@ -0,0 +1,108 @@
+import React from 'react';
+
+import { Nav, NavItem } from 'reactstrap';
+import { Switch, Route, Redirect, useRouteMatch } from 'react-router-dom';
+import { NavLink } from 'react-router-dom';
+
+import Translate from '../../i18n/components/translate';
+import { AdminRoute } from '../../core/components/AdminRoute';
+
+import './Dashboard.scss';
+import { SourcesTab } from './SourcesTab';
+import { ProvidersTab } from './ProvidersTab';
+import { AdminTab } from './AdminTab';
+import { ActionsTab } from './ActionsTab';
+import { useIsAdmin } from '../../core/user/UserContext';
+import useFetch from 'use-http';
+import API_BASE_PATH from '../../App.constant';
+import { getMetadataPath } from '../../metadata/hooks/api';
+
+export function Dashboard () {
+
+ const { path } = useRouteMatch();
+
+ const isAdmin = useIsAdmin();
+
+ const [actions, setActions] = React.useState(0);
+ const [users, setUsers] = React.useState([]);
+ const [sources, setSources] = React.useState([]);
+
+ const { get, response } = useFetch(`${API_BASE_PATH}`, {
+ cachePolicy: 'no-cache'
+ });
+
+ async function loadUsers() {
+ const users = await get('/admin/users')
+ if (response.ok) {
+ setUsers(users.filter(u => u.role === 'ROLE_NONE'));
+ }
+ }
+
+ async function loadSources() {
+ const s = await get(`/${getMetadataPath('source')}/disabledNonAdmin`);
+ if (response.ok) {
+ setSources(s);
+ }
+ }
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ React.useEffect(() => {
+ loadSources();
+ loadUsers();
+ }, []);
+
+ React.useEffect(() => {
+ setActions(users.length + sources.length);
+ }, [users, sources]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+/*
+
+ & nbsp;
+ {{ actionsRequired$ | async}}
+
+ */
+export default Dashboard;
\ No newline at end of file
diff --git a/ui/src/app/dashboard/view/Dashboard.scss b/ui/src/app/dashboard/view/Dashboard.scss
new file mode 100644
index 000000000..74201a46e
--- /dev/null
+++ b/ui/src/app/dashboard/view/Dashboard.scss
@@ -0,0 +1,2 @@
+@import '../../../theme/variables.scss';
+
diff --git a/ui/src/app/dashboard/view/ProvidersTab.js b/ui/src/app/dashboard/view/ProvidersTab.js
new file mode 100644
index 000000000..f5a2d719b
--- /dev/null
+++ b/ui/src/app/dashboard/view/ProvidersTab.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import { useMetadataEntities } from '../../metadata/hooks/api';
+import Translate from '../../i18n/components/translate';
+import ProviderList from '../../metadata/domain/provider/component/ProviderList';
+import {Search} from '../component/Search';
+import { Ordered } from '../component/Ordered';
+import { useIsAdmin } from '../../core/user/UserContext';
+import { Alert } from 'reactstrap';
+
+const searchProps = ['name', '@type', 'createdBy'];
+
+export function ProvidersTab () {
+
+ const [providers, setProviders] = React.useState([]);
+
+ const { get, response } = useMetadataEntities('provider');
+
+ async function loadProviders() {
+ const providers = await get('')
+ if (response.ok) {
+ setProviders(providers);
+ }
+ }
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ React.useEffect(() => { loadProviders() }, []);
+
+ const isAdmin = useIsAdmin();
+
+ return (
+
+
+ {isAdmin ?
+ <>
+
+
+ Current Metadata Providers
+
+
+
+
+ {(ordered, first, last, onOrderUp, onOrderDown) =>
+
+ {(searched) => }
+
+ }
+
+
+ >
+ :
+
Access Denied}
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/view/SourcesTab.js b/ui/src/app/dashboard/view/SourcesTab.js
new file mode 100644
index 000000000..94305795b
--- /dev/null
+++ b/ui/src/app/dashboard/view/SourcesTab.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import Translate from '../../i18n/components/translate';
+
+import SourceList from '../../metadata/domain/source/component/SourceList';
+import { useMetadataEntities } from '../../metadata/hooks/api';
+import { Search } from '../component/Search';
+
+const searchProps = ['serviceProviderName', 'entityId', 'createdBy'];
+
+export function SourcesTab () {
+
+ const [sources, setSources] = React.useState([]);
+
+ const { get, response } = useMetadataEntities('source', {
+ cachePolicy: 'no-cache'
+ });
+
+ async function loadSources() {
+ const sources = await get('/');
+ if (response.ok) {
+ setSources(sources);
+ }
+ }
+
+ const updateSources = () => loadSources();
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ React.useEffect(() => { loadSources() }, []);
+
+ return (
+
+
+
+
+ Current Metadata Sources
+
+
+
+
+ {(searched) => }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/hoc/Configuration.js b/ui/src/app/metadata/hoc/Configuration.js
new file mode 100644
index 000000000..1e12f541c
--- /dev/null
+++ b/ui/src/app/metadata/hoc/Configuration.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import { useMetadataConfiguration } from '../hooks/configuration';
+
+export function Configuration ({entities, schema, definition, limited, children}) {
+
+ const config = useMetadataConfiguration(entities, schema, definition, limited);
+
+ console.log(config)
+
+ return (
+ <>{children(config)}>
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/hoc/MetadataVersionLoader.js b/ui/src/app/metadata/hoc/MetadataVersionLoader.js
new file mode 100644
index 000000000..a2c62c10e
--- /dev/null
+++ b/ui/src/app/metadata/hoc/MetadataVersionLoader.js
@@ -0,0 +1,48 @@
+import React from 'react';
+import { useParams } from 'react-router';
+import { getMetadataPath } from '../hooks/api';
+import API_BASE_PATH from '../../App.constant';
+import useFetch from 'use-http';
+import { last } from 'lodash';
+
+export function MetadataVersionLoader ({versions, children}) {
+
+ const ref = React.useRef({});
+ const [list, setList] = React.useState({});
+
+ const { type, id } = useParams();
+
+ const { get, response } = useFetch(`/${API_BASE_PATH}${getMetadataPath(type)}/${id}/Versions`, {
+ cachePolicy: 'no-cache',
+ }, []);
+
+ async function loadVersion(v) {
+ const l = await get(`/${v}`);
+ if (response.ok) {
+ addToList(v, l);
+ if (last(versions) !== v) {
+ loadNext(versions[versions.indexOf(v) + 1]);
+ }
+ }
+ }
+
+ function addToList(version, item) {
+ ref.current = {
+ ...ref.current,
+ [version]: item
+ };
+ setList(ref.current);
+ }
+
+ function loadNext (v) {
+ loadVersion(v);
+ }
+
+ React.useEffect(() => {
+ loadNext(versions[0]);
+ }, []);
+
+ return (
+ {children(versions.map(v => list[v]).filter(v => !!v))}
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/view/MetadataComparison.js b/ui/src/app/metadata/view/MetadataComparison.js
new file mode 100644
index 000000000..5b04c013a
--- /dev/null
+++ b/ui/src/app/metadata/view/MetadataComparison.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import {
+ useQueryParam,
+ ArrayParam,
+ withDefault
+} from 'use-query-params';
+import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema';
+import { MetadataVersionLoader } from '../hoc/MetadataVersionLoader';
+import { Configuration } from '../hoc/Configuration';
+import { MetadataConfiguration } from '../component/MetadataConfiguration';
+import { Link, useParams } from 'react-router-dom';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faHistory } from '@fortawesome/free-solid-svg-icons';
+import Translate from '../../i18n/components/translate';
+import { CustomInput } from 'reactstrap';
+import { useTranslation } from '../../i18n/hooks';
+
+export function MetadataComparison () {
+
+ const { type, id } = useParams();
+
+ const [versions] = useQueryParam('versions', withDefault(ArrayParam, []));
+ const schema = React.useContext(MetadataSchemaContext);
+ const definition = React.useContext(MetadataDefinitionContext);
+
+ const [limited, setLimited] = React.useState(false);
+
+ const toggleLimited = useTranslation('action.view-only-changes');
+
+ return (
+ <>
+ {versions &&
+
+ {(v) =>
+
+ {(config) =>
+ 2 ? 'container-fluid' : 'container'}>
+
+
+
+
+ Version History
+
+
+
setLimited(!limited) } />
+
+
+
+ }
+
+ }
+
+ }
+ >
+ );
+}
+
+/*
+
+
+ Compare
+ Source
+ Provider
+ Configuration
+
+ 2,
+ 'container': (numVersions$ | async) <= 2
+}">
+
+
+
+ Version History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+*/
\ No newline at end of file
diff --git a/ui/src/app/metadata/view/MetadataHistory.js b/ui/src/app/metadata/view/MetadataHistory.js
new file mode 100644
index 000000000..605b506ed
--- /dev/null
+++ b/ui/src/app/metadata/view/MetadataHistory.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import { useHistory, useParams } from 'react-router';
+import { Link } from 'react-router-dom';
+import queryString from 'query-string';
+
+import FormattedDate from '../../core/components/FormattedDate';
+
+import Translate from '../../i18n/components/translate';
+import { useMetadataHistory } from '../hooks/api';
+
+const sortVersionsByDate = (versions) => {
+ return versions.sort((a, b) => {
+ const aDate = new Date(a.date).getTime();
+ const bDate = new Date(b.date).getTime();
+ return aDate === bDate ? 0 : aDate < bDate ? -1 : 1;
+ }).reverse();
+}
+
+export function MetadataHistory () {
+ const { type, id } = useParams();
+
+ const history = useHistory();
+
+ const { data, loading } = useMetadataHistory(type, id, {}, []);
+
+ const toggleVersionSelected = (version) => {
+ let s = [...selected];
+ if (s.indexOf(version) > -1) {
+ s = s.filter(i => i !== version);
+ } else {
+ s = [...s, version];
+ }
+ setSelected(s);
+ };
+ const restore = () => {};
+ const compare = (versions) => {
+ const sorted = sortVersionsByDate(versions);
+ const path = `/metadata/${type}/${id}/configuration/compare?${queryString.stringify({versions: sorted.map(s => s.id)}, {
+ skipNull: true,
+ })}`;
+ history.push(path);
+ };
+
+ const [selected, setSelected] = React.useState([]);
+
+ return (
+ <>
+
+ version history
+
+ {data && !loading &&
+ <>
+
+
+ >
+
}
+ {loading &&
+
+ Loading...
+
}
+ >
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/view/MetadataOptions.js b/ui/src/app/metadata/view/MetadataOptions.js
new file mode 100644
index 000000000..61ba46e3a
--- /dev/null
+++ b/ui/src/app/metadata/view/MetadataOptions.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import { faArrowDown, faArrowUp, faHistory, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Link, useHistory, useParams } from 'react-router-dom';
+
+import { scroller } from 'react-scroll';
+
+import Translate from '../../i18n/components/translate';
+
+import { MetadataObjectContext } from '../hoc/MetadataSelector';
+import { MetadataHeader } from '../component/MetadataHeader';
+import { MetadataConfiguration } from '../component/MetadataConfiguration';
+import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema';
+
+import { useMetadataConfiguration } from '../hooks/configuration';
+import { MetadataViewToggle } from '../component/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 () {
+
+ const metadata = React.useContext(MetadataObjectContext);
+ const definition = React.useContext(MetadataDefinitionContext);
+ const schema = React.useContext(MetadataSchemaContext);
+ const history = useHistory();
+
+ const { type, id } = useParams();
+
+ const configuration = useMetadataConfiguration([metadata], schema, definition);
+
+ const onScrollTo = (element, offset = 0) => {
+ scroller.scrollTo(element, {
+ duration: 500,
+ smooth: true,
+ offset
+ });
+ };
+
+ const redirectOnDelete = () => history.push('/dashboard');
+
+ return (
+
+ {(onDeleteSource) =>
+ <>
+
+
+
+ {type === 'source' &&
+
+ }
+
+
+
+
+
+ Version History
+
+ {type === 'provider' &&
+
+ }
+
+
+
+
+
+ {type === 'provider' &&
+ <>
+
+
+ Filters
+
+
+
+
+ Add Filter
+
+
+
+
+
+
+ >
+ }
+
+
+
+ >}
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/view/MetadataVersion.js b/ui/src/app/metadata/view/MetadataVersion.js
new file mode 100644
index 000000000..02a88d558
--- /dev/null
+++ b/ui/src/app/metadata/view/MetadataVersion.js
@@ -0,0 +1,110 @@
+import React from 'react';
+
+import { MetadataDefinitionContext, MetadataSchemaContext } from '../hoc/MetadataSchema';
+import { Configuration } from '../hoc/Configuration';
+import { MetadataConfiguration } from '../component/MetadataConfiguration';
+import { Link, useParams } from 'react-router-dom';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faArrowUp, faHistory, faPlus } from '@fortawesome/free-solid-svg-icons';
+
+import { scroller } from 'react-scroll';
+
+import Translate from '../../i18n/components/translate';
+import { useMetadataEntity } from '../hooks/api';
+import { MetadataHeader } from '../component/MetadataHeader';
+import { MetadataFilters } from '../domain/filter/component/MetadataFilters';
+import { MetadataFilterConfigurationList } from '../domain/filter/component/MetadataFilterConfigurationList';
+import { MetadataFilterTypes } from '../domain/filter';
+
+
+export function MetadataVersion() {
+
+ const { type, id, versionId } = useParams();
+
+ const [metadata, setMetadata] = React.useState();
+
+
+ const schema = React.useContext(MetadataSchemaContext);
+ const definition = React.useContext(MetadataDefinitionContext);
+
+ const { get, response } = useMetadataEntity(type, {
+ cachePolicy: 'no-cache',
+ }, []);
+
+ async function loadVersion(v) {
+ const l = await get(`/${id}/Versions/${v}`);
+ if (response.ok) {
+ setMetadata(l);
+ }
+ }
+
+ React.useEffect(() => {
+ loadVersion(versionId);
+ }, [versionId]);
+
+ const onScrollTo = (element, offset = 0) => {
+ scroller.scrollTo(element, {
+ duration: 500,
+ smooth: true,
+ offset
+ });
+ };
+
+ return (
+ <>
+ {metadata &&
+
+ {(config) =>
+ <>
+
+
+
+
+
+
+
+
+
+ Version History
+
+
+
+
+
+ {type === 'provider' &&
+ <>
+
+
+ Filters
+
+
+
+
+ Add Filter
+
+
+
+
+
+
+ >
+ }
+
+
+
+ >
+ }
+
+ }
+ >
+ );
+}
diff --git a/ui/src/app/metadata/view/MetadataXml.js b/ui/src/app/metadata/view/MetadataXml.js
new file mode 100644
index 000000000..4a39b22f9
--- /dev/null
+++ b/ui/src/app/metadata/view/MetadataXml.js
@@ -0,0 +1,45 @@
+import { faSave } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React from 'react';
+
+import { useParams } from 'react-router';
+import Translate from '../../i18n/components/translate';
+import { MetadataObjectContext } from '../hoc/MetadataSelector';
+
+import { MetadataXmlContext } from '../hoc/MetadataXmlLoader';
+import { MetadataViewToggle } from '../component/MetadataViewToggle';
+import { downloadAsXml } from '../../core/utility/download_as_xml';
+
+export function MetadataXml () {
+ const xml = React.useContext(MetadataXmlContext);
+ const entity = React.useContext(MetadataObjectContext);
+ const { type } = useParams();
+
+ const download = () => downloadAsXml(entity, xml);
+
+ return (
+ <>
+
+ Source Configuration
+
+
+
+
+
+
+
+ {xml}
+
+ { xml }
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/ui/src/setupProxy.js b/ui/src/setupProxy.js
index abb8ec30d..78c38f7fb 100644
--- a/ui/src/setupProxy.js
+++ b/ui/src/setupProxy.js
@@ -2,10 +2,13 @@ const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
+
+ const port = 10101;
+
app.use(
'/api',
createProxyMiddleware({
- target: 'http://localhost:8080',
+ target: `http://localhost:${port}`,
changeOrigin: true,
onProxyRes: function (proxyRes, req, res) {
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
@@ -16,7 +19,7 @@ module.exports = function (app) {
app.use(
'/actuator',
createProxyMiddleware({
- target: 'http://localhost:8080',
+ target: `http://localhost:${port}`,
changeOrigin: true
})
);
@@ -24,7 +27,7 @@ module.exports = function (app) {
app.use(
'/login',
createProxyMiddleware({
- target: 'http://localhost:8080',
+ target: `http://localhost:${port}`,
changeOrigin: true
})
);
@@ -32,7 +35,7 @@ module.exports = function (app) {
app.use(
'/logout',
createProxyMiddleware({
- target: 'http://localhost:8080',
+ target: `http://localhost:${port}`,
changeOrigin: true
})
);