diff --git a/ui/package.json b/ui/package.json
index ed49c77bf..cc7c1741b 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -10,6 +10,7 @@
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"bootstrap": "^4.6.0",
+ "date-fns": "^2.21.1",
"http-proxy-middleware": "^1.2.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
diff --git a/ui/src/app/App.js b/ui/src/app/App.js
index b5cc96e7d..f9acbb044 100644
--- a/ui/src/app/App.js
+++ b/ui/src/app/App.js
@@ -13,24 +13,27 @@ import Footer from './core/components/Footer';
import Dashboard from './dashboard/container/Dashboard';
import Header from './core/components/Header';
+import { UserProvider } from './core/user/UserContext';
function App() {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/ui/src/app/admin/container/UserManagement.js b/ui/src/app/admin/container/UserManagement.js
new file mode 100644
index 000000000..0cf2c8fb1
--- /dev/null
+++ b/ui/src/app/admin/container/UserManagement.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+
+import Translate from '../../i18n/components/translate';
+import { useCurrentUser } from '../../core/user/UserContext';
+
+export default function UserManagement({ users, roles, onDelete, onSetRole }) {
+
+ const setUserRole = (user, role) => onSetRole(user, role);
+
+ const currentUser = useCurrentUser();
+
+ const [modal, setModal] = React.useState(false);
+
+ const toggle = () => setModal(!modal);
+
+ const [deleting, setDeleting] = React.useState(null);
+
+ const deleteUser = (id) => {
+ onDelete(deleting);
+ setDeleting(null);
+ }
+
+ return (
+
+
+
+
+ | UserId |
+ Name |
+ Email |
+ Role |
+ Delete? |
+
+
+
+ {users.map((user, idx) =>
+
+ | { user.username } |
+ { user.firstName } { user.lastName } |
+ { user.emailAddress } |
+
+
+
+ |
+
+ {currentUser.username !== user.username &&
+
+ }
+ |
+
+ )}
+
+
+
setDeleting(null)}>
+ Delete User?
+
+
+
+ You are requesting to delete a user. If you complete this process the user will be removed. This cannot be undone. Do you wish to continue?
+
+
+
+ {' '}
+
+
+
+
+ );
+}
+
+/*
+
+ | {{ user.username }} |
+ {{ user.firstName }} {{ user.lastName }} |
+ {{ user.emailAddress }} |
+
+
+
+ |
+
+
+ |
+
*/
\ No newline at end of file
diff --git a/ui/src/app/core/components/FormattedDate.js b/ui/src/app/core/components/FormattedDate.js
new file mode 100644
index 000000000..7a18594a2
--- /dev/null
+++ b/ui/src/app/core/components/FormattedDate.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import { format } from 'date-fns';
+
+export default function FormattedDate ({ date }) {
+ const formatted = React.useMemo(() => format(new Date(date), 'MMM Lo, Y'), [date]);
+
+ return (<>{ formatted }>);
+}
\ No newline at end of file
diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js
new file mode 100644
index 000000000..d99e52cc9
--- /dev/null
+++ b/ui/src/app/core/user/UserContext.js
@@ -0,0 +1,38 @@
+import React from "react";
+import useFetch from 'use-http';
+import API_BASE_PATH from '../../App.constant';
+
+const UserContext = React.createContext();
+
+const { Provider, Consumer } = UserContext;
+
+const path = '/admin/users/current';
+
+/*eslint-disable react-hooks/exhaustive-deps*/
+function UserProvider({ children }) {
+
+ const { get, response } = useFetch(`${API_BASE_PATH}`, {
+ cacheLife: 10000,
+ cachePolicy: 'cache-first'
+ });
+
+ React.useEffect(() => { loadUser() }, []);
+
+ async function loadUser() {
+ const user = await get(`${path}`);
+ if (response.ok) setUser(user);
+ }
+
+ const [user, setUser] = React.useState({});
+ return (
+ {children}
+ );
+}
+
+function useCurrentUser() {
+ const context = React.useContext(UserContext);
+ return context;
+}
+
+
+export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser };
\ No newline at end of file
diff --git a/ui/src/app/dashboard/container/ActionsTab.js b/ui/src/app/dashboard/container/ActionsTab.js
new file mode 100644
index 000000000..c89e23585
--- /dev/null
+++ b/ui/src/app/dashboard/container/ActionsTab.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import useFetch from 'use-http';
+import UserManagement from '../../admin/container/UserManagement';
+import API_BASE_PATH from '../../App.constant';
+
+import Translate from '../../i18n/components/translate';
+
+export function ActionsTab() {
+
+ return (
+ <>
+
+
+
+
+
+ Enable Metadata Sources
+
+
+
+
+ {/**/}
+
+
+
+
+
+
+
+
+ User Access Request
+
+
+
+ {/*
*/}
+
+
+ >
+
+ );
+}
+
+export default ActionsTab;
\ No newline at end of file
diff --git a/ui/src/app/dashboard/container/AdminTab.js b/ui/src/app/dashboard/container/AdminTab.js
new file mode 100644
index 000000000..2e04221c0
--- /dev/null
+++ b/ui/src/app/dashboard/container/AdminTab.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import useFetch from 'use-http';
+import UserManagement from '../../admin/container/UserManagement';
+import API_BASE_PATH from '../../App.constant';
+
+import Translate from '../../i18n/components/translate';
+
+export function AdminTab () {
+
+ const [users, setUsers] = React.useState([]);
+
+ const { get, patch, del, response } = useFetch(`${API_BASE_PATH}`, {})
+
+ async function loadUsers() {
+ const users = await get('/admin/users')
+ if (response.ok) {
+ setUsers(users);
+ }
+ }
+ const [roles, setRoles] = React.useState([]);
+
+ async function loadRoles() {
+ const roles = await get('/supportedRoles')
+ if (response.ok) {
+ setRoles(roles);
+ }
+ }
+
+ async function setUserRole (user, role) {
+ const update = await patch(`/admin/users/${user.username}`, {
+ ...user,
+ role
+ });
+ if (response.ok) {
+ loadUsers();
+ }
+ }
+
+ async function deleteUser(id) {
+ const removal = await del(`/admin/users/${id}`);
+ if (response.ok) {
+ loadUsers();
+ }
+ }
+
+ React.useEffect(() => {
+ loadUsers();
+ loadRoles();
+ }, []);
+
+
+ return (
+
+ );
+}
+
+export default AdminTab;
\ No newline at end of file
diff --git a/ui/src/app/dashboard/container/Dashboard.js b/ui/src/app/dashboard/container/Dashboard.js
index 66a4aef51..9580c2857 100644
--- a/ui/src/app/dashboard/container/Dashboard.js
+++ b/ui/src/app/dashboard/container/Dashboard.js
@@ -7,45 +7,51 @@ import { NavLink } from 'react-router-dom';
import Translate from '../../i18n/components/translate';
import './Dashboard.scss';
-import { ResolverList } from './ResolverList';
+import { SourcesTab } from './SourcesTab';
+import { ProvidersTab } from './ProvidersTab';
+import { AdminTab } from './AdminTab';
+import { ActionsTab } from './ActionsTab';
export function Dashboard () {
const actions = 0;
- let { path, url } = useRouteMatch();
+ let { path } = useRouteMatch();
return (
-
-
+
+
-
+
+
+
+
diff --git a/ui/src/app/dashboard/container/ProvidersTab.js b/ui/src/app/dashboard/container/ProvidersTab.js
new file mode 100644
index 000000000..929c0e3e1
--- /dev/null
+++ b/ui/src/app/dashboard/container/ProvidersTab.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import useFetch from 'use-http';
+import Translate from '../../i18n/components/translate';
+import API_BASE_PATH from '../../App.constant';
+
+import ProviderList from '../../metadata/provider/component/ProviderList';
+
+export function ProvidersTab () {
+
+ const [providers, setProviders] = React.useState([]);
+
+ const { get, response } = useFetch(`${API_BASE_PATH}/MetadataResolvers`, {})
+
+ async function loadProviders() {
+ const providers = await get('/')
+ if (response.ok) {
+ setProviders(providers);
+ }
+ }
+
+ React.useEffect(() => { loadProviders() }, []);
+
+ return (
+
+
+
+
+ Current Metadata Providers
+
+
+
+ { /* search goes here */}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard/container/ResolverList.js b/ui/src/app/dashboard/container/SourcesTab.js
similarity index 56%
rename from ui/src/app/dashboard/container/ResolverList.js
rename to ui/src/app/dashboard/container/SourcesTab.js
index 63b189db4..d047d54c6 100644
--- a/ui/src/app/dashboard/container/ResolverList.js
+++ b/ui/src/app/dashboard/container/SourcesTab.js
@@ -5,9 +5,27 @@ import API_BASE_PATH from '../../App.constant';
import SourceList from '../../metadata/source/component/SourceList';
-export function ResolverList () {
+export function SourcesTab () {
- const { data = [] } = useFetch(`${API_BASE_PATH}/EntityDescriptors`, {}, []);
+ const [sources, setSources] = React.useState([]);
+
+ const { get, del, response } = useFetch(`${API_BASE_PATH}/EntityDescriptors`, {})
+
+ async function loadSources() {
+ const sources = await get('/')
+ if (response.ok) {
+ setSources(sources);
+ }
+ }
+
+ async function deleteSource(id) {
+ const removal = await del(`/${id}`);
+ if (response.ok) {
+ loadSources();
+ }
+ }
+
+ React.useEffect(() => { loadSources() }, []);
return (
@@ -19,7 +37,7 @@ export function ResolverList () {
{ /* search goes here */ }
-
+
diff --git a/ui/src/app/i18n/context/I18n.provider.js b/ui/src/app/i18n/context/I18n.provider.js
index 0e5c6d434..e1849eff9 100644
--- a/ui/src/app/i18n/context/I18n.provider.js
+++ b/ui/src/app/i18n/context/I18n.provider.js
@@ -19,8 +19,8 @@ function I18nProvider ({ children }) {
React.useEffect(() => { loadMessages() }, []);
async function loadMessages() {
- const todos = await get(`${path}`);
- if (response.ok) setMessages(todos);
+ const msgs = await get(`${path}`);
+ if (response.ok) setMessages(msgs);
}
const [messages, setMessages] = React.useState({});
diff --git a/ui/src/app/metadata/provider/component/ProviderList.js b/ui/src/app/metadata/provider/component/ProviderList.js
new file mode 100644
index 000000000..cea52f41b
--- /dev/null
+++ b/ui/src/app/metadata/provider/component/ProviderList.js
@@ -0,0 +1,137 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { Badge, UncontrolledPopover, PopoverBody } from 'reactstrap';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons';
+
+import FormattedDate from '../../../core/components/FormattedDate';
+import Translate from '../../../i18n/components/translate';
+
+export default function ProviderList({ entities, onDelete }) {
+ return (
+
+
+
+
+ | Order |
+ Title |
+ Provider Type |
+ Author |
+ Created Date |
+ Enabled |
+
+
+
+ {entities.map((provider, idx) =>
+
+
+
+ { idx + 1 }
+
+
+
+ { /*
+
+ {{ i + 1 }}
+ —
+
+
+
+
+ */ }
+ |
+
+ {provider.name}
+ |
+ { provider['@type'] } |
+ { provider.createdBy } |
+ |
+
+
+
+
+ |
+
+ )}
+
+
+
+
+ );
+}
+
+/*
+
+ |
+ {{ 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/source/component/SourceList.js b/ui/src/app/metadata/source/component/SourceList.js
index 3e13be33f..6ec1b71e3 100644
--- a/ui/src/app/metadata/source/component/SourceList.js
+++ b/ui/src/app/metadata/source/component/SourceList.js
@@ -1,12 +1,29 @@
import React from 'react';
+import { Link } from 'react-router-dom';
+import { Badge, UncontrolledPopover, PopoverBody, Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTrash, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
+
+import FormattedDate from '../../../core/components/FormattedDate';
import Translate from '../../../i18n/components/translate';
-export default function SourceList({ entities }) {
+export default function SourceList({ entities, onDelete }) {
+
+ const [modal, setModal] = React.useState(false);
+
+ const toggle = () => setModal(!modal);
+
+ const [deleting, setDeleting] = React.useState(null);
+ const deleteSource = (id) => {
+ onDelete(deleting);
+ setDeleting(null);
+ }
return (
-
+
@@ -15,11 +32,62 @@ export default function SourceList({ entities }) {
| Author |
Created Date |
Enabled |
- |
+ |
-
+
+ {entities.map((source, idx) =>
+
+ |
+ {source.serviceProviderName }
+ |
+
+ {source.entityId}
+ |
+
+ {source.createdBy }
+ |
+ |
+
+
+
+
+ |
+
+
+ { source.serviceEnabled &&
+
+ A metadata source must be disabled before it can be deleted.
+
+ }
+ |
+
+ ) }
+
+
setDeleting(null)}>
+ Delete Metadata Source?
+
+
+
+ You are deleting a metadata source. This cannot be undone. Continue?
+
+
+
+ {' '}
+
+
+
);
}
diff --git a/ui/src/theme/project/index.scss b/ui/src/theme/project/index.scss
index 550718819..1126af89d 100644
--- a/ui/src/theme/project/index.scss
+++ b/ui/src/theme/project/index.scss
@@ -7,6 +7,7 @@
@import './typography';
@import './list';
@import './tabs';
+@import './table.scss';
@import './utility';
body {
diff --git a/ui/src/theme/project/table.scss b/ui/src/theme/project/table.scss
new file mode 100644
index 000000000..53f8a3abb
--- /dev/null
+++ b/ui/src/theme/project/table.scss
@@ -0,0 +1,18 @@
+.source-list tr td:last-child {
+ width: 1%;
+ white-space: nowrap;
+}
+
+.provider-index {
+ font-size: 1.8rem;
+ line-height: 1.8rem;
+ font-weight: lighter;
+ font-family: monospace;
+ width: 45px;
+}
+
+.table-providers {
+ tr > td {
+ vertical-align: middle;
+ }
+}
\ No newline at end of file
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 850c1611a..3e244f3df 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -3904,6 +3904,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@^2.21.1:
+ version "2.21.1"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.1.tgz#679a4ccaa584c0706ea70b3fa92262ac3009d2b0"
+ integrity sha512-m1WR0xGiC6j6jNFAyW4Nvh4WxAi4JF4w9jRJwSI8nBmNcyZXPcP9VUQG+6gHQXAmqaGEKDKhOqAtENDC941UkA==
+
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"