diff --git a/docs/develop/plugins/configuration.md b/docs/develop/plugins/configuration.md index 15cc22da0..300ba17ba 100644 --- a/docs/develop/plugins/configuration.md +++ b/docs/develop/plugins/configuration.md @@ -33,7 +33,7 @@ plugin.schema = { JSON Schema approach works reasonably well for simple to medium complex configuration data. The server supports also [custom plugin configuration components](../webapps.md), bypassing the automatic configuration format generation. -It should ne noted that some JSON schema constructs are not supported. Refer ([details](https://github.com/peterkelly/react-jsonschema-form-bs4/blob/v1.7.1-bs4/docs/index.md#json-schema-supporting-status)) for details. +It should be noted that some JSON schema constructs are not supported. Refer to the [RJSF documentation](https://rjsf-team.github.io/react-jsonschema-form/docs/) for details. The configuration data is stored by the server under the following path `$SIGNALK_NODE_CONFIG_DIR/plugin-config-data/.json`. _(Default value of SIGNALK_NODE_CONFIG_DIR is $HOME/.signalk.)_ diff --git a/packages/server-admin-ui/package.json b/packages/server-admin-ui/package.json index 34164e59c..0d2ab741f 100644 --- a/packages/server-admin-ui/package.json +++ b/packages/server-admin-ui/package.json @@ -20,6 +20,10 @@ "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@rjsf/bootstrap-4": "^5.24.13", + "@rjsf/core": "^5.24.13", + "@rjsf/utils": "^5.24.13", + "@rjsf/validator-ajv8": "^5.24.13", "@signalk/server-admin-ui-dependencies": "1.0.1", "ansi-to-html": "^0.6.14", "babel-loader": "^8.1.0", @@ -36,13 +40,13 @@ "lodash.set": "^4.3.2", "lodash.uniq": "^4.5.0", "moment": "^2.29.1", - "react": "^16.13.1", + "react": "^16.14.0", + "react-bootstrap": "^1.6.8", "react-copy-to-clipboard": "^5.0.3", - "react-dom": "^16.13.1", + "react-dom": "^16.14.0", "react-html-parser": "^2.0.2", "react-infinite-scroll-component": "^6.1.0", "react-json-tree": "^0.20.0", - "react-jsonschema-form-bs4": "^1.7.1", "react-redux": "^5.1.2", "react-router-dom": "^4.3.1", "react-select": "^3.1.0", diff --git a/packages/server-admin-ui/scss/_custom.scss b/packages/server-admin-ui/scss/_custom.scss index f8c3ac47b..429a1ba1c 100644 --- a/packages/server-admin-ui/scss/_custom.scss +++ b/packages/server-admin-ui/scss/_custom.scss @@ -1,22 +1,98 @@ // Here you can add other styles -form.rjsf label { - margin-bottom: 0; -} +// RJSF Form Styling -form.rjsf div > p.field-description { - font-size: 0.7rem; - font-style: italic; - margin-bottom: 0px; -} +// Mixin for button state variants to reduce duplication +@mixin button-variant( + $bg-color, + $border-color, + $hover-bg, + $hover-border, + $shadow-color +) { + background-color: $bg-color !important; + border-color: $border-color !important; + color: white !important; + + &:hover { + background-color: $hover-bg !important; + border-color: $hover-border !important; + } -form.rjsf input { - width: auto; + &:focus { + background-color: $hover-bg !important; + border-color: $hover-border !important; + box-shadow: 0 0 0 0.2rem $shadow-color !important; + } } -form.rjsf div.row.array-item { - background-color: aliceblue; - margin-bottom: 3px; +form.rjsf { + // General form element styling + label { + margin-bottom: 0; + } + + div > p.field-description { + font-size: 0.7rem; + font-style: italic; + margin-bottom: 0px; + } + + input { + width: auto; + } + + div.row.array-item { + background-color: aliceblue; + margin-bottom: 3px; + } + + // Custom button styling for RJSF buttons to match original appearance + + // Add button styling - cyan color + button.btn-add, + button.array-item-add button { + @include button-variant( + #20a8d8, + // background + #20a8d8, + // border + #1985ac, + // hover background + #1985ac, + // hover border + rgba(32, 168, 216, 0.5) // focus shadow + ); + } + + // Remove button styling - red color + button.array-item-remove { + @include button-variant( + #f86c6b, + // background + #f86c6b, + // border + #f63c3a, + // hover background + #f63c3a, + // hover border + rgba(248, 108, 107, 0.5) // focus shadow + ); + } + + // Array button styling + .array-button-style { + flex: 1 1 0%; + padding-left: 6px; + padding-right: 6px; + font-weight: bold; + } + + // Button group flexbox layout + .btn-group-flex { + display: flex; + justify-content: space-around; + } } @media (max-width: 767px) { diff --git a/packages/server-admin-ui/src/views/ServerConfig/PluginConfigurationForm.js b/packages/server-admin-ui/src/views/ServerConfig/PluginConfigurationForm.js index 1f9f77ed8..b8273189c 100644 --- a/packages/server-admin-ui/src/views/ServerConfig/PluginConfigurationForm.js +++ b/packages/server-admin-ui/src/views/ServerConfig/PluginConfigurationForm.js @@ -1,40 +1,325 @@ import React from 'react' -import Form from 'react-jsonschema-form-bs4' +import { withTheme } from '@rjsf/core' +import { Theme as Bootstrap4Theme } from '@rjsf/bootstrap-4' +import validator from '@rjsf/validator-ajv8' +import { getTemplate, getUiOptions } from '@rjsf/utils' -// eslint-disable-next-line react/display-name -export default ({ plugin, onSubmit }) => { - const schema = JSON.parse(JSON.stringify(plugin.schema)) - var uiSchema = {} +const Form = withTheme(Bootstrap4Theme) - if (typeof plugin.uiSchema !== 'undefined') { - uiSchema['configuration'] = JSON.parse(JSON.stringify(plugin.uiSchema)) - } +const GRID_COLUMNS = { + CONTENT: 'col-9', + TOOLBAR: 'col-3', + ADD_BUTTON_CONTAINER: 'col-3 offset-9' +} - const topSchema = { - type: 'object', - properties: { - configuration: { - type: 'object', - title: ' ', - description: schema.description, - properties: schema.properties - } - } - } +const CSS_CLASSES = { + FORM_CONTROL: 'form-control', + FORM_CHECK: 'form-check', + FORM_CHECK_INPUT: 'form-check-input', + FORM_CHECK_LABEL: 'form-check-label', + BTN_INFO: 'btn btn-info', + BTN_OUTLINE_DARK: 'btn btn-outline-dark', + BTN_DANGER: 'btn btn-danger', + ARRAY_ITEM: 'row array-item', + ARRAY_ITEM_TOOLBOX: 'array-item-toolbox', + ARRAY_ITEM_LIST: 'array-item-list', + ARRAY_ITEM_ADD: 'row', + FIELD_DESCRIPTION: 'field-description', + CHECKBOX: 'checkbox ' +} + +const isArrayItemId = (id) => { + if (!id || typeof id !== 'string') return false + const parts = id.split('_') + return parts.length > 2 && /^\d+$/.test(parts[parts.length - 1]) +} + +const createButton = ( + className, + onClick, + disabled, + style, + icon, + tabIndex = 0 +) => ( + +) + +const ArrayFieldItemTemplate = (props) => { + const { + children, + disabled, + hasToolbar, + hasMoveUp, + hasMoveDown, + hasRemove, + index, + onDropIndexClick, + onReorderClick, + readonly, + registry, + uiSchema + } = props + + const { MoveUpButton, MoveDownButton, RemoveButton } = + registry.templates.ButtonTemplates + + return ( +
+
{children}
+
+ {hasToolbar && ( +
+ {(hasMoveUp || hasMoveDown) && ( + + )} + {(hasMoveUp || hasMoveDown) && ( + + )} + {hasRemove && ( + + )} +
+ )} +
+
+ ) +} + +const FieldTemplate = (props) => { + const { + id, + classNames, + style, + label, + help, + required, + description, + errors, + children, + displayLabel, + schema + } = props + + const isCheckbox = schema.type === 'boolean' + const isObject = schema.type === 'object' + + return ( +
+ {displayLabel && label && !isCheckbox && ( + + )} + {description && !isObject && ( +

+ {description} +

+ )} + {children} + {errors} + {help} +
+ ) +} + +const ObjectFieldTemplate = (props) => { + const { title, description, properties, idSchema } = props + const isArrayItem = isArrayItemId(idSchema.$id) - if (plugin.statusMessage) { - topSchema.description = `Status: ${plugin.statusMessage}` + return ( +
+ {title && !isArrayItem && ( + {title} + )} + {description && ( +

+ {description} +

+ )} + {properties.map((prop) => prop.content)} +
+ ) +} + +const ArrayFieldTemplate = (props) => { + const { + canAdd, + disabled, + idSchema, + uiSchema, + items, + onAddClick, + readonly, + registry, + schema, + title + } = props + + const uiOptions = getUiOptions(uiSchema) + const ResolvedArrayFieldItemTemplate = getTemplate( + 'ArrayFieldItemTemplate', + registry, + uiOptions + ) + const { + ButtonTemplates: { AddButton } + } = registry.templates + + return ( +
+ {(uiOptions.title || title) && ( + + {uiOptions.title || title} + + )} + {(uiOptions.description || schema.description) && ( +
+ {uiOptions.description || schema.description} +
+ )} +
+ {items?.map(({ key, ...itemProps }) => ( + + ))} +
+ {canAdd && ( +
+

+ +

+
+ )} +
+ ) +} + +const customTemplates = { + FieldTemplate, + ObjectFieldTemplate, + ArrayFieldTemplate, + ArrayFieldItemTemplate, + ButtonTemplates: { + AddButton: (props) => + createButton( + `${CSS_CLASSES.BTN_INFO} ${props.className || ''}`, + props.onClick, + props.disabled, + undefined, + , + 0 + ), + MoveUpButton: (props) => + createButton( + `${CSS_CLASSES.BTN_OUTLINE_DARK} ${props.className || ''}`, + props.onClick, + props.disabled, + undefined, + , + -1 + ), + MoveDownButton: (props) => + createButton( + `${CSS_CLASSES.BTN_OUTLINE_DARK} ${props.className || ''}`, + props.onClick, + props.disabled, + undefined, + , + -1 + ), + RemoveButton: (props) => + createButton( + `${CSS_CLASSES.BTN_DANGER} ${props.className || ''}`, + props.onClick, + props.disabled, + undefined, + , + -1 + ), + SubmitButton: (props) => { + const { submitText } = props.uiSchema?.['ui:submitButtonOptions'] || {} + return ( +
+ +
+ ) + } } +} +// eslint-disable-next-line react/display-name +export default ({ plugin, onSubmit }) => { const { enabled, enableLogging, enableDebug } = plugin.data + return (
{ + templates={customTemplates} + onSubmit={({ formData }) => { onSubmit({ - ...submitData.formData, + ...formData, enabled, enableLogging, enableDebug