From 099ccb8fb788e38b6a2c7f76015decfc4dce11bc Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 4 Aug 2022 14:24:22 -0700 Subject: [PATCH] Added initial UI for property list --- ui/public/assets/data/properties.json | 1 + .../assets/schema/properties/property.json | 30 ++++++ ui/src/app/App.js | 6 ++ ui/src/app/admin/Properties.js | 34 +++++++ ui/src/app/admin/component/PropertyForm.js | 56 +++++++++++ ui/src/app/admin/container/EditProperty.js | 92 +++++++++++++++++++ ui/src/app/admin/container/NewProperty.js | 80 ++++++++++++++++ ui/src/app/admin/container/PropertyList.js | 80 ++++++++++++++++ ui/src/app/admin/hoc/PropertiesProvider.js | 42 +++++++++ ui/src/app/admin/hoc/PropertyProvider.js | 20 ++++ ui/src/app/admin/hooks.js | 16 ++++ ui/src/app/core/components/Header.js | 6 +- 12 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 ui/public/assets/data/properties.json create mode 100644 ui/public/assets/schema/properties/property.json create mode 100644 ui/src/app/admin/Properties.js create mode 100644 ui/src/app/admin/component/PropertyForm.js create mode 100644 ui/src/app/admin/container/EditProperty.js create mode 100644 ui/src/app/admin/container/NewProperty.js create mode 100644 ui/src/app/admin/container/PropertyList.js create mode 100644 ui/src/app/admin/hoc/PropertiesProvider.js create mode 100644 ui/src/app/admin/hoc/PropertyProvider.js diff --git a/ui/public/assets/data/properties.json b/ui/public/assets/data/properties.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/ui/public/assets/data/properties.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/ui/public/assets/schema/properties/property.json b/ui/public/assets/schema/properties/property.json new file mode 100644 index 000000000..f0e90ff49 --- /dev/null +++ b/ui/public/assets/schema/properties/property.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "required": [ + "property", + "value" + ], + "properties": { + "property": { + "title": "label.property-key", + "description": "tooltip.property-key", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "description": { + "title": "label.property-descr", + "description": "tooltip.property-descr", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "value": { + "title": "label.property-value", + "description": "tooltip.property-value", + "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 546241f10..9c4e00422 100644 --- a/ui/src/app/App.js +++ b/ui/src/app/App.js @@ -34,6 +34,7 @@ import { Roles } from './admin/Roles'; import { Groups } from './admin/Groups'; import { BASE_PATH } from './App.constant'; import { ProtectRoute } from './core/components/ProtectRoute'; +import { Properties } from './admin/Properties'; function App() { @@ -108,6 +109,11 @@ function App() { } /> + + + + + } /> diff --git a/ui/src/app/admin/Properties.js b/ui/src/app/admin/Properties.js new file mode 100644 index 000000000..b81e0af48 --- /dev/null +++ b/ui/src/app/admin/Properties.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom'; +import { PropertiesProvider } from './hoc/PropertiesProvider'; +import { NewProperty } from './container/NewProperty'; +import { EditProperty } from './container/EditProperty'; +import { PropertyList } from './container/PropertyList'; + +export function Properties() { + + let { path, url } = useRouteMatch(); + + return ( + <> + + + + {(properties, onDelete) => + + } + + } /> + + + } /> + + + } /> + + + } /> + + + ); +} \ No newline at end of file diff --git a/ui/src/app/admin/component/PropertyForm.js b/ui/src/app/admin/component/PropertyForm.js new file mode 100644 index 000000000..54a0800ea --- /dev/null +++ b/ui/src/app/admin/component/PropertyForm.js @@ -0,0 +1,56 @@ +import React from 'react'; +import Button from 'react-bootstrap/Button'; +import Form from '../../form/Form'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; +import Translate from '../../i18n/components/translate'; + +import { usePropertyUiSchema } from '../hooks'; +import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; + +export function PropertyForm({ property = {}, errors = [], loading = false, schema, onSave, onCancel }) { + + const { dispatch } = React.useContext(FormContext); + const onChange = ({ formData, errors }) => { + dispatch(setFormDataAction(formData)); + dispatch(setFormErrorAction(errors)); + }; + + const uiSchema = usePropertyUiSchema(); + + return (<> +
+
+ + + + +
+
+
+
+
onChange(form)} + schema={schema} + uiSchema={uiSchema} + liveValidate={true}> + <> +
+
+
+
+ ) +} +/**/ \ No newline at end of file diff --git a/ui/src/app/admin/container/EditProperty.js b/ui/src/app/admin/container/EditProperty.js new file mode 100644 index 000000000..beac8c5f8 --- /dev/null +++ b/ui/src/app/admin/container/EditProperty.js @@ -0,0 +1,92 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; +import Translate from '../../i18n/components/translate'; +import { useProperties } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; + +import { PropertyForm } from '../component/PropertyForm'; +import { PropertyProvider } from '../hoc/PropertyProvider'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; + +export function EditProperty() { + + const { id } = useParams(); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const history = useHistory(); + + const { put, response, loading } = useProperties(); + + const [blocking, setBlocking] = React.useState(false); + + async function save(property) { + let toast; + const resp = await put(`/${property.resourceId}`, property); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Updated property successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/properties`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Edit property +
+
+
+
+ + {(property) => + + {(schema) => + <>{property && + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + + }} + + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/NewProperty.js b/ui/src/app/admin/container/NewProperty.js new file mode 100644 index 000000000..911a10bc8 --- /dev/null +++ b/ui/src/app/admin/container/NewProperty.js @@ -0,0 +1,80 @@ +import React from 'react'; + +import { Prompt, useHistory } from 'react-router-dom'; +import Translate from '../../i18n/components/translate'; +import { useProperties } from '../hooks'; +import { Schema } from '../../form/Schema'; +import { FormManager } from '../../form/FormManager'; +import { PropertyForm } from '../component/PropertyForm'; + +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; +import { BASE_PATH } from '../../App.constant'; + +export function NewProperty() { + const history = useHistory(); + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { post, response, loading } = useProperties({}); + + const [blocking, setBlocking] = React.useState(false); + + async function save(property) { + let toast; + const resp = await post(``, property); + if (response.ok) { + gotoDetail({ refresh: true }); + toast = createNotificationAction(`Added property successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + }; + + const cancel = () => { + gotoDetail(); + }; + + const gotoDetail = (state = null) => { + setBlocking(false); + history.push(`/properties`, state); + }; + + return ( +
+ + `message.unsaved-editor` + } + /> +
+
+
+
+ Add a new property +
+
+
+
+ + {(schema) => + + {(data, errors) => + save(data)} + onCancel={() => cancel()} />} + } + +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/container/PropertyList.js b/ui/src/app/admin/container/PropertyList.js new file mode 100644 index 000000000..2312cc1d2 --- /dev/null +++ b/ui/src/app/admin/container/PropertyList.js @@ -0,0 +1,80 @@ +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 { DeleteConfirmation } from '../../core/components/DeleteConfirmation'; + +export function PropertyList({ properties, onDelete }) { + + const remove = (id) => { + onDelete(id); + } + + return ( + + {(block) => +
+
+
+
+ + Roles Management + +
+
+
+ +   + Add new property + +
+
+ + + + + + + + + {(properties?.length > 0) ? properties.map((property, i) => + + + + + ) : + + } + +
+ Role Name + Actions
{property.name} + + + + + Edit + + + + +
No properties defined.
+
+
+
+
+
+ } +
+ ); +} \ No newline at end of file diff --git a/ui/src/app/admin/hoc/PropertiesProvider.js b/ui/src/app/admin/hoc/PropertiesProvider.js new file mode 100644 index 000000000..341d7736f --- /dev/null +++ b/ui/src/app/admin/hoc/PropertiesProvider.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { useProperties } from '../hooks'; +import { createNotificationAction, NotificationTypes, useNotificationDispatcher } from '../../notifications/hoc/Notifications'; +import { useTranslator } from '../../i18n/hooks'; + +export function PropertiesProvider({ children, cache = 'no-cache' }) { + + const [properties, setProperties] = React.useState([]); + + const notifier = useNotificationDispatcher(); + const translator = useTranslator(); + + const { get, del, response, loading } = useProperties({ + cachePolicy: cache + }); + + async function loadProperties() { + const list = await get(`assets/data/properties.json`); + if (response.ok) { + setProperties(list); + } + } + + async function removeProperty(id) { + let toast; + const resp = await del(`/${id}`); + if (response.ok) { + loadProperties(); + toast = createNotificationAction(`Deleted property successfully.`, NotificationTypes.SUCCESS); + } else { + toast = createNotificationAction(`${resp.errorCode} - ${translator(resp.errorMessage)}`, NotificationTypes.ERROR); + } + if (toast) { + notifier(toast); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadProperties() }, []); + + return (<>{children(properties, removeProperty, loading)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hoc/PropertyProvider.js b/ui/src/app/admin/hoc/PropertyProvider.js new file mode 100644 index 000000000..119f3d26d --- /dev/null +++ b/ui/src/app/admin/hoc/PropertyProvider.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useProperty } from '../hooks'; + +export function PropertyProvider({ id, children }) { + + const [property, setProperty] = React.useState(); + const { get, response } = useProperty(id); + + async function loadProperty() { + const r = await get(``); + if (response.ok) { + setProperty(r); + } + } + + /*eslint-disable react-hooks/exhaustive-deps*/ + React.useEffect(() => { loadProperty() }, []); + + return (<>{children(property)}); +} \ No newline at end of file diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index b2c63a7c3..955c510a6 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -46,3 +46,19 @@ export function useGroupUiValidator() { export function useRoleUiSchema() { return {}; } + +export function useProperties (opts = { cachePolicy: 'no-cache' }) { + return useFetch(`${API_BASE_PATH}/admin/properties`, opts); +} + +export function useProperty (id, opts = { cachePolicy: 'no-cache' }) { + return useFetch(`${API_BASE_PATH}/admin/property/${id}`, opts); +} + +export function usePropertyUiSchema () { + return { + description: { + 'ui:widget': 'textarea' + } + }; +} diff --git a/ui/src/app/core/components/Header.js b/ui/src/app/core/components/Header.js index ff979056b..d8773a709 100644 --- a/ui/src/app/core/components/Header.js +++ b/ui/src/app/core/components/Header.js @@ -7,7 +7,7 @@ import Dropdown from 'react-bootstrap/Dropdown'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faBoxOpen, faTags, faIdBadge } from '@fortawesome/free-solid-svg-icons'; +import { faTh, faSignOutAlt, faPlusCircle, faCube, faCubes, faUsersCog, faSpinner, faUserCircle, faCog, faBoxOpen, faTags, faIdBadge, faFileLines } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; import { useTranslator } from '../../i18n/hooks'; @@ -88,6 +88,10 @@ export function Header () { + + + + }