diff --git a/ui/public/assets/schema/groups/group.json b/ui/public/assets/schema/groups/group.json
new file mode 100644
index 000000000..088d861f6
--- /dev/null
+++ b/ui/public/assets/schema/groups/group.json
@@ -0,0 +1,15 @@
+{
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "title": "label.group-name",
+ "description": "tooltip.group-name",
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/App.js b/ui/src/app/App.js
index 097c0095d..58456350d 100644
--- a/ui/src/app/App.js
+++ b/ui/src/app/App.js
@@ -27,6 +27,7 @@ import { Filter } from './metadata/Filter';
import { Contention } from './metadata/contention/ContentionContext';
import { SessionModal } from './core/user/SessionModal';
import Button from 'react-bootstrap/Button';
+import { Groups } from './admin/Groups';
function App() {
@@ -76,8 +77,9 @@ function App() {
-
+
+
diff --git a/ui/src/app/admin/Groups.js b/ui/src/app/admin/Groups.js
new file mode 100644
index 000000000..ec69c4f61
--- /dev/null
+++ b/ui/src/app/admin/Groups.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom';
+import { GroupsProvider } from './hoc/GroupsProvider';
+import { NewGroup } from './container/NewGroup';
+import { EditGroup } from './container/EditGroup';
+import { GroupsList } from './container/GroupsList';
+
+export function Groups() {
+
+ let { path } = useRouteMatch();
+
+ return (
+ <>
+
+
+
+ {(groups, onDelete) =>
+
+ }
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js
new file mode 100644
index 000000000..85a08d587
--- /dev/null
+++ b/ui/src/app/admin/component/GroupForm.js
@@ -0,0 +1,69 @@
+import React from 'react';
+import Button from 'react-bootstrap/Button';
+import Form from '@rjsf/bootstrap-4';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons';
+import Translate from '../../i18n/components/translate';
+
+import { useGroupUiSchema } from '../hooks';
+import { fields, widgets } from '../../form/component';
+import { templates } from '../../form/component';
+
+function ErrorListTemplate() {
+ return (<>>);
+}
+
+export function GroupForm ({schema}) {
+
+ const [errors, setErrors] = React.useState([]);
+ const [loading, setLoading] = React.useState(false);
+ const [metadata, setMetadata] = React.useState({});
+
+ const save = () => { };
+ const cancel = () => { };
+ const onChange = () => { };
+
+ const uiSchema = useGroupUiSchema();
+
+ return (<>
+
+
+
+
+
+
+
+
+
+
+ >)
+}
+/**/
\ No newline at end of file
diff --git a/ui/src/app/admin/container/EditGroup.js b/ui/src/app/admin/container/EditGroup.js
new file mode 100644
index 000000000..355a1d6e2
--- /dev/null
+++ b/ui/src/app/admin/container/EditGroup.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+export function EditGroup () {
+ return (<>Edit Group>);
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/container/GroupsList.js b/ui/src/app/admin/container/GroupsList.js
new file mode 100644
index 000000000..023b86ff5
--- /dev/null
+++ b/ui/src/app/admin/container/GroupsList.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import { faEdit, faPlusCircle, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import Button from 'react-bootstrap/Button';
+import { Link } from 'react-router-dom';
+
+import { Translate } from '../../i18n/components/translate';
+import { useTranslator } from '../../i18n/hooks';
+
+import { DeleteConfirmation } from '../../core/components/DeleteConfirmation';
+
+export function GroupsList({ groups, onDelete }) {
+
+ const translator = useTranslator();
+
+ const remove = (id) => {
+ onDelete(id);
+ }
+
+ return (
+
+ {(block) =>
+
+
+
+
+
+ Groups Management
+
+
+
+
+
+
+ Add new group
+
+
+
+
+
+
+ |
+ Group Name
+ |
+ Actions |
+
+
+
+ {groups?.length && groups.map((group, i) =>
+
+ | {group.name} |
+
+
+
+
+ Edit
+
+
+
+ |
+
+ )}
+
+
+
+
+
+
+
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/container/NewGroup.js b/ui/src/app/admin/container/NewGroup.js
new file mode 100644
index 000000000..ab5e7d3ee
--- /dev/null
+++ b/ui/src/app/admin/container/NewGroup.js
@@ -0,0 +1,59 @@
+import React from 'react';
+
+import { Prompt, useHistory } from 'react-router';
+import Translate from '../../i18n/components/translate';
+import { useGroups } from '../hooks';
+import { Schema } from '../../form/Schema';
+
+import { GroupForm } from '../component/GroupForm';
+
+export function NewGroup() {
+ const history = useHistory();
+
+ const { post, response, loading } = useGroups({});
+
+ const [blocking, setBlocking] = React.useState(false);
+
+ async function save(metadata) {
+ await post(``, metadata);
+ if (response.ok) {
+ gotoDetail({ refresh: true });
+ }
+ };
+
+ const cancel = () => {
+ gotoDetail();
+ };
+
+ const gotoDetail = (state = null) => {
+ setBlocking(false);
+ history.push(`/groups`, state);
+ };
+
+ const [group, setGroup] = React.useState({});
+
+ return (
+
+
+ `message.unsaved-editor`
+ }
+ />
+
+
+
+
+ {(schema) => }
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/hoc/GroupsProvider.js b/ui/src/app/admin/hoc/GroupsProvider.js
new file mode 100644
index 000000000..a9d47131b
--- /dev/null
+++ b/ui/src/app/admin/hoc/GroupsProvider.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { useGroups } from '../hooks';
+
+export function GroupsProvider({ children }) {
+
+ const [groups, setGroups] = React.useState([
+ {
+ name: 'foo'
+ }
+ ]);
+
+
+ const { get, del, response } = useGroups({
+ cachePolicy: 'no-cache'
+ });
+
+ async function loadGroups() {
+ const list = await get(``);
+ if (response.ok) {
+ setGroups(list);
+ }
+ }
+
+ async function removeGroup(id) {
+ await del(`/${id}`);
+ if (response.ok) {
+ loadGroups();
+ }
+ }
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ // React.useEffect(() => { loadGroups() }, []);
+
+ return (<>{children(groups, removeGroup)}>);
+}
\ No newline at end of file
diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js
new file mode 100644
index 000000000..d071fb5e4
--- /dev/null
+++ b/ui/src/app/admin/hooks.js
@@ -0,0 +1,16 @@
+import useFetch from 'use-http';
+import API_BASE_PATH from '../App.constant';
+
+export function useGroups () {
+ return useFetch(`${API_BASE_PATH}/groups`);
+}
+
+export function useGroup() {
+ return useFetch(`${API_BASE_PATH}/group`);
+}
+
+export function useGroupUiSchema () {
+ return {
+
+ };
+}
\ No newline at end of file
diff --git a/ui/src/app/metadata/component/DeleteConfirmation.js b/ui/src/app/core/components/DeleteConfirmation.js
similarity index 100%
rename from ui/src/app/metadata/component/DeleteConfirmation.js
rename to ui/src/app/core/components/DeleteConfirmation.js
diff --git a/ui/src/app/metadata/component/DeleteConfirmation.test.js b/ui/src/app/core/components/DeleteConfirmation.test.js
similarity index 100%
rename from ui/src/app/metadata/component/DeleteConfirmation.test.js
rename to ui/src/app/core/components/DeleteConfirmation.test.js
diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js
index 54d0c402c..ca22fe12f 100644
--- a/ui/src/app/core/components/Header.js
+++ b/ui/src/app/core/components/Header.js
@@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar';
import Dropdown from 'react-bootstrap/Dropdown';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes } from '@fortawesome/free-solid-svg-icons';
+import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog } from '@fortawesome/free-solid-svg-icons';
import Translate from '../../i18n/components/translate';
import { useTranslator } from '../../i18n/hooks';
@@ -41,6 +41,10 @@ export function Header () {
+
+
+
+
}
diff --git a/ui/src/app/form/Schema.js b/ui/src/app/form/Schema.js
new file mode 100644
index 000000000..92aefe325
--- /dev/null
+++ b/ui/src/app/form/Schema.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import useFetch from 'use-http';
+
+export function Schema({ path, children }) {
+
+ const [schema, setSchema] = React.useState({});
+
+
+ const { get, response } = useFetch(path, {
+ cachePolicy: 'no-cache'
+ });
+
+ async function loadSchema() {
+ const list = await get(``);
+ if (response.ok) {
+ setSchema(list);
+ }
+ }
+
+ /*eslint-disable react-hooks/exhaustive-deps*/
+ React.useEffect(() => { loadSchema() }, []);
+
+ return (<>{children(schema)}>);
+}
\ 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
index 13a22b53c..4e5ff8a98 100644
--- a/ui/src/app/metadata/domain/filter/component/MetadataFilters.js
+++ b/ui/src/app/metadata/domain/filter/component/MetadataFilters.js
@@ -1,6 +1,6 @@
import React from 'react';
import { useMetadataFilters } from '../../../hooks/api';
-import { DeleteConfirmation } from '../../../component/DeleteConfirmation';
+import { DeleteConfirmation } from '../../../../core/components/DeleteConfirmation';
export const MetadataFiltersContext = React.createContext();
diff --git a/ui/src/app/metadata/hooks/api.js b/ui/src/app/metadata/hooks/api.js
index d951b66a4..f628e847d 100644
--- a/ui/src/app/metadata/hooks/api.js
+++ b/ui/src/app/metadata/hooks/api.js
@@ -24,7 +24,7 @@ export function getMetadataPath(type) {
}
export function useNonAdminSources() {
- return useFetch(`/${getMetadataPath('source')}/disabledNonAdmin`);
+ return useFetch(`${API_BASE_PATH}${getMetadataPath('source')}/disabledNonAdmin`);
}
export function getMetadataListPath(type) {
diff --git a/ui/src/app/metadata/view/MetadataAttributeList.js b/ui/src/app/metadata/view/MetadataAttributeList.js
index b72c3c0e7..bc912118a 100644
--- a/ui/src/app/metadata/view/MetadataAttributeList.js
+++ b/ui/src/app/metadata/view/MetadataAttributeList.js
@@ -8,7 +8,7 @@ import { Link } from 'react-router-dom';
import { Translate } from '../../i18n/components/translate';
import { useTranslator } from '../../i18n/hooks';
-import { DeleteConfirmation } from '../component/DeleteConfirmation';
+import { DeleteConfirmation } from '../../core/components/DeleteConfirmation';
export function MetadataAttributeList ({entities, onDelete}) {