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 ( +
+ + + + + + + + + + + + {users.map((user, idx) => + + + + + + + + )} + +
UserIdNameEmailRoleDelete?
{ 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 ( +
+
+
+
+
+ User Maintenance +
+
+
+
+ +
+
+
+ ); +} + +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 ( +
+ + + + + + + + + + + + + {entities.map((provider, idx) => + + + + + + + + + )} + +
OrderTitleProvider TypeAuthorCreated DateEnabled
+
+
{ 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 }) { - + - + + {entities.map((source, idx) => + + + + + + + + + ) } +
Author Created Date Enabled
+ {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"