diff --git a/demo/public/locales/cs/translation.json b/demo/public/locales/cs/translation.json index e94765064..65bf6e1b4 100644 --- a/demo/public/locales/cs/translation.json +++ b/demo/public/locales/cs/translation.json @@ -22,62 +22,6 @@ "HelpButton": { "Help": "Nápověda" }, - "ASABConfig": { - "Nothing has been selected": "Zatím nebylo nic vybráno", - "Please select the configuration from tree menu on the left side of the screen": "Prosím vyberte konfiguraci z nabídky na levé straně obrazovky", - "Save": "Uložit", - "Read only": "Pouze pro čtení", - "Basic": "Základní nastavení", - "Advanced": "Pokročilé nastavení", - "Data updated successfully": "Data úspěšně aktualizována", - "Something went wrong, failed to update data": "Něco je špatně, nepodařilo se aktualizovat data", - "Something went wrong": "Něco je špatně!", - "We are sorry, but the file cannot be found": "Omlouváme se, ale soubor nebylo možné najít :-(", - "Config file does not exist": "Konfigurace nebyla nalezena", - "Something went wrong! Unable to get schema": "Něco je špatně! Nepodařilo se načíst schema {{type}}", - "Unable to get data for tree menu": "Nepodařilo se načíst data pro tree menu", - "Unable to get schema. Try to reload the page": "Nepodařilo se načíst schema {{type}}. Zkuste obnovit stránku", - "Unable to get config data. Try to reload the page": "Nepodařilo se načíst {{config}} data. Zkuste obnovit stránku", - "Configuration name can't be empty!": "Název konfigurace nemůže být prázdný!", - "Remove": "Odstranit", - "Something went wrong, failed to create configuration": "Něco je špatně, nepodařilo se vytvořit konfiguraci", - "Configuration created successfully": "Konfigurace vytvořena úspěšně", - "Name": "Název", - "Configuration name": "Název konfigurace", - "Fill out configuration name": "Vyplňte název konfigurace", - "Create": "Vytvořit", - "New": "Nová", - "Schema title": "Název schema", - "Schema description": "Popis schema", - "Something went wrong! Unable to get configurations": "Něco je špatně! Nepodařilo se získat konfiguraci pro {{config}}", - "Unable to get configurations. Try to reload the page": "Nepodařilo se získat konfiguraci pro {{config}}. Zkuste obnovit stránku", - "Create configuration": "Vytvořit konfiguraci", - "Do you want to remove this configuration?": "Opravdu chcete odstranit tuto konfiguraci?", - "Something went wrong, failed to remove configuration": "Něco je špatně, nepodařilo se odstranit konfiguraci", - "New configuration": "Nová konfigurace", - "Section added": "Sekce přidána", - "Add new section": "Přidat novou sekci", - "Add": "Přidat", - "Type": "Typ", - "Do you want to remove this section?": "Opravdu chcete odstranit tuto sekci?", - "Actions": "Akce", - "Export": "Exportovat", - "Import": "Importovat", - "Back": "Zpět", - "Only tar.gz files are allowed": "Povoleny jsou pouze soubory tar.gz", - "Search": "Hledat", - "Override": "Přepsat", - "Merge": "Spojit", - "Import configuration": "Importovat konfiguraci", - "Choose file": "Vyberte soubor", - "No file chosen": "Nebyl vybrán žádný soubor", - "Failed to import configuration": "Nepodařilo se importovat konfiguraci", - "Configuration has been successfully imported": "Konfigurace byla úspěšně importována" - }, - "ASABConfigModule": { - "Actions": "Akce", - "Search": "Hledat" - }, "ASABConfigService": { "Incorrect/invalid config file downloaded": "Došlo ke stažení neplatné konfigurace", "Error when downloading a config file. The path might be corrupted": "Chyba při stahování kofigurace. Cesta k souboru nemusí být správně" diff --git a/demo/public/locales/en/translation.json b/demo/public/locales/en/translation.json index f57b69df5..574dabf65 100644 --- a/demo/public/locales/en/translation.json +++ b/demo/public/locales/en/translation.json @@ -22,60 +22,6 @@ "HelpButton": { "Help": "Help" }, - "ASABConfig": { - "Nothing has been selected": "Nothing has been selected", - "Please select the configuration from tree menu on the left side of the screen": "Please select the configuration from tree menu on the left side of the screen", - "Save": "Save", - "Read only": "Read only", - "Basic": "Basic", - "Advanced": "Advanced", - "Data updated successfully": "Data updated successfully", - "Something went wrong, failed to update data": "Something went wrong, failed to update data", - "Something went wrong": "Something went wrong!", - "We are sorry, but the file cannot be found": "We are sorry, but the file cannot be found :-(", - "Config file does not exist": "Config file does not exist", - "Something went wrong! Unable to get schema": "Something went wrong! Unable to get schema {{type}}", - "Unable to get data for tree menu": "Unable to get data for tree menu", - "Unable to get schema. Try to reload the page": "Unable to get schema {{type}}. Try to reload the page", - "Unable to get config data. Try to reload the page": "Unable to get config {{config}} data. Try to reload the page", - "Configuration name can't be empty!": "Configuration name can't be empty!", - "Remove": "Remove", - "Something went wrong, failed to create configuration": "Something went wrong, failed to create configuration", - "Configuration created successfully": "Configuration created successfully", - "Name": "Name", - "Configuration name": "Configuration name", - "Fill out configuration name": "Fill out configuration name", - "Create": "Create", - "New": "New", - "Schema title": "Schema title", - "Schema description": "Schema description", - "Something went wrong! Unable to get configurations": "Something went wrong! Unable to get configurations for {{config}}", - "Unable to get configurations. Try to reload the page": "Unable to get configurations for {{config}}. Try to reload the page", - "Create configuration": "Create configuration", - "Do you want to remove this configuration?": "Do you want to remove this configuration?", - "Something went wrong, failed to remove configuration": "Something went wrong, failed to remove configuration", - "New configuration": "New configuration", - "Section added": "Section added", - "Add new section": "Add new section", - "Add": "Add", - "Type": "Type", - "Do you want to remove this section?": "Do you want to remove this section?", - "Actions": "Actions", - "Export": "Export", - "Import": "Import", - "Back": "Back", - "Only tar.gz files are allowed": "Only tar.gz files are allowed", - "Search": "Search", - "Override": "Override", - "Merge": "Merge", - "Import configuration": "Import configuration", - "Failed to import library": "Failed to import library", - "Configuration has been successfully imported": "Configuration has been successfully imported" - }, - "ASABConfigModule": { - "Actions": "Actions", - "Search": "Search" - }, "ASABConfigService": { "Incorrect/invalid config file downloaded": "Incorrect/invalid config file downloaded", "Error when downloading a config file. The path might be corrupted": "Error when downloading a config file. The path might be corrupted" diff --git a/doc/asab-config.md b/doc/asab-config.md deleted file mode 100644 index c7b2a820d..000000000 --- a/doc/asab-config.md +++ /dev/null @@ -1,519 +0,0 @@ -# ASAB Config - -## Setup - -Before using this component in your project, `react-simple-tree-menu` and `react-hook-form - v7` must be installed and added into the project's `package.json` file: - -``` -yarn add react-simple-tree-menu -yarn add react-hook-form -``` - -In `config` file, define ASAB Config as a service: - -``` -module.exports = { - app: { - - ... - - }, - webpackDevServer: { - port: 3000, - proxy: { - '/api/asab_config': { - target: 'http://localhost:8082', - pathRewrite: {'^/api/asab_config' : ''}, - ws: true - }, - } - } -} -``` - -In the top-level `index.js` of your ASAB UI application, load the ASAB config module - -``` -const modules = []; - -... - -import ASABConfigModule from 'asab-webui/modules/maintenance/ConfigModule'; -modules.push(ASABConfigModule); - -... - -ReactDOM.render(( - - - -), document.getElementById('app')); -``` - -The module will be displayed as a subitem of `Maintenance` in the sidebar navigation. - - -## Authorization (resources) - -To provide any of these actions - `Create`, `Remove`, `Save`, `Import`, `Export` - configuration, one needs to have `config:admin` and/or `authz:superuser` resource. - -## Schema and configuration files - -To obtain / save configuration, ASAB config service must be running. - -Configuration and schema has to be saved in Zookeeper. - -Config structure in Zookeeper should be set as following: - -``` -- **main Zookeeper node** - - config - - **type** - - **config** - - type - - **type** - - schema -``` - -where - -- **main Zookeeper node** is the main node in the Zookeeper -- **type** is the name of the section -- **config** is the name of the configuration - -Configuration, which will not fit the schema will fall into adHoc value/section, which is strictly read-only. - - -### Basic config and schema example for Section properties - -Config example: - -``` -{ - "my-source": { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } -} -``` - -Schema example: - -``` -{ - "$id": "Props schema", - "type": "object", - "title": "Props schema", - "description": "My props schema", - "default": {}, - "examples": [ - { - "My:source": { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } - } - ], - "required": ["my-source"], // Required value is optional for properties - "properties": { - "My:source": { - "type": "string", - "title": "Some source", - "description": "My Some source", - "default": {}, - "examples": [ - { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } - ], - "required": [ - "source", - "datetime_field", - "type" - ], - "properties": { - "source": { - "type": "string", - "title": "Data source", - "description": "Value for Data source", - "default": "", - "examples": [ - "my-data-source*" - ] - }, - "datetime_field": { - "type": "string", - "title": "Datetime", - "description": "Datetime value", - "default": "", - "examples": [ - "@timestamp" - ] - }, - "type": { - "type": "string", - "title": "Type", - "description": "Select type", - "default": ["elasticsearch", "api"], - "$defs": { - "select": { "type": "select" } - }, - "examples": [ - "elasticsearch" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false -} -``` - -### Basic schema example for Section pattern properties - -Config example: - -``` -{ - "Some:source:my-source": { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } -} -``` - -Schema example: - -``` -{ - "$id": "Pattern props schema", - "type": "object", - "title": "Pattern props schema", - "description": "My pattern props schema", - "default": {}, - "examples": [ - { - "Some:source:my-source": { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } - } - ], - "required": [], // For pattern properties section, required should be left empty - "patternProperties": { - "^Some:source:.*$": { - "type": "string", - "title": "Some source", - "description": "My Some source", - "default": {}, - "examples": [ - { - "source": "my-data-source*", - "datetime_field": "@timestamp", - "type": "elasticsearch" - } - ], - "required": [ - "source", - "datetime_field", - "type" - ], - "properties": { - "source": { - "type": "string", - "title": "Data source", - "description": "Value for Data source", - "default": "", - "examples": [ - "my-data-source*" - ] - }, - "datetime_field": { - "type": "string", - "title": "Datetime", - "description": "Datetime value", - "default": "", - "examples": [ - "@timestamp" - ] - }, - "type": { - "type": "string", - "title": "Type", - "description": "Select type", - "default": ["elasticsearch", "api"], - "$defs": { - "select": { "type": "select" } - }, - "examples": [ - "elasticsearch" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false -} -``` - -## Supported inputs types - -### String values - -#### Single string - -``` -"properties": { - "name": { - "type": "string", - "title": "Name", - "description": "Fill your name", - "default": "", - "examples": [ - "Your Name" - ] - } -} -``` - -#### Password - -``` -"properties": { - "email": { - "type": "string", - "title": "Password", - "description": "Fill your secret", - "default": "", - "$defs": { - "password": { - "type": "password" - } - }, - "examples": [ - "S0meSecr3t" - ] - } -} -``` - -#### Email - -``` -"properties": { - "email": { - "type": "string", - "title": "Email", - "description": "Fill your email", - "default": "", - "$defs": { - "email": { - "type": "email" - } - }, - "examples": [ - "your@email.ex" - ] - } -} -``` - -#### URL - -``` -"properties": { - "url": { - "type": "string", - "title": "URL", - "description": "Fill the redirect URL", - "default": "", - "$defs": { - "url": { - "type": "url" - } - }, - "examples": [ - "http://my-url.my" - ] - } -} -``` - -#### Text area - -``` -"properties": { - "image": { - "type": "string", - "title": "Base64 image", - "description": "Add base64 image string", - "default": "", - "$defs": { - "textarea": { - "type": "textarea" - } - }, - "examples": [ - "data:image/svg;base64,iVBORw0KG..." - ] - } -} -``` - -#### Select - -``` -"properties": { - "slct": { - "type": "string", - "title": "Select a string", - "description": "Please select a string value", - "default": ["string one", "string two"], - "$defs": { - "select": { "type": "select" } - }, - "examples": [ - "string one" - ] - } -} -``` - -### Number values - -#### Single number - -``` -"properties": { - "nmbr": { - "type": "number", - "title": "Random number", - "description": "Add any number", - "default": "", - "$defs": { - "number": { - "type": "number" - } - }, - "examples": [ - 666 - ] - } -} -``` - -#### Select - -``` -"properties": { - "nmbrslct": { - "type": "number", - "title": "Select the number you like", - "description": "Please select some number", - "default": [1,2,3], - "$defs": { - "select": { - "type": "select" - } - }, - "examples": [ - 1 - ] - } -} -``` - -### Boolean values - -Using checkbox - -``` -"properties": { - "chckbx": { - "type": "boolean", - "title": "Turn on/off my checkbox", - "description": "Turn me on/off", - "default": true, - "$defs": { - "checkbox": { - "type": "checkbox" - } - }, - "examples": [ - true - ] - } -} -``` - -### Array values - -``` -"properties": { - "arr": { - "type": "array", - "title": "Array values", - "description": "Please write string values separated by comma", - "default": "", - "examples": [ - "string one", "string two" - ] - } -} -``` - -## Language localisations - -Language localizations for ASAB-Config configuration can be added to the translation.json files of `public/locales/en` & `public/locales/cs` of the product where ASAB Config module is used. - -Example: - -``` -{ - "ASABConfig": { - "Nothing has been selected": "Nothing has been selected", - "Please select the configuration from tree menu on the left side of the screen": "Please select the configuration from tree menu on the left side of the screen", - "Save": "Save", - "Read only": "Read only", - "Basic": "Basic", - "Advanced": "Advanced", - "Data updated successfully": "Data updated successfully", - "Something went wrong, failed to update data": "Something went wrong, failed to update data", - "Something went wrong": "Something went wrong!", - "We are sorry, but the file cannot be found": "We are sorry, but the file cannot be found :-(", - "Config file does not exist": "Config file does not exist", - "Something went wrong! Unable to get schema": "Something went wrong! Unable to get schema {{type}}", - "Unable to get data for tree menu": "Unable to get data for tree menu", - "Unable to get schema. Try to reload the page": "Unable to get schema {{type}}. Try to reload the page", - "Unable to get config data. Try to reload the page": "Unable to get config {{config}} data. Try to reload the page", - "Configuration name can't be empty!": "Configuration name can't be empty!", - "Remove": "Remove", - "Something went wrong, failed to create configuration": "Something went wrong, failed to create configuration", - "Configuration created successfully": "Configuration created successfully", - "Name": "Name", - "Configuration name": "Configuration name", - "Fill out configuration name": "Fill out configuration name", - "Create": "Create", - "New": "New", - "Something went wrong! Unable to get configurations": "Something went wrong! Unable to get configurations for {{config}}", - "Unable to get configurations. Try to reload the page": "Unable to get configurations for {{config}}. Try to reload the page", - "Create configuration": "Create configuration", - "Do you want to remove this configuration?": "Do you want to remove this configuration?", - "Something went wrong, failed to remove configuration": "Something went wrong, failed to remove configuration", - "New configuration": "New configuration", - "Section added": "Section added", - "Add new section": "Add new section", - "Add": "Add", - "Type": "Type", - "Do you want to remove this section?": "Do you want to remove this section?" - } -} -``` diff --git a/doc/asab-services.md b/doc/asab-services.md deleted file mode 100644 index 7f5226cee..000000000 --- a/doc/asab-services.md +++ /dev/null @@ -1,48 +0,0 @@ -# ASAB Services - -ASAB WebUI Services is a page with a list of available instances. It use a websocket connection, so the data are propagated realtime. - -## Setup - -In `config` file, define ASAB Services as a service: - -``` -module.exports = { - app: { - - ... - - }, - webpackDevServer: { - port: 3000, - proxy: { - '/api/lmio_remote_control': { - target: 'http://localhost:8086', - ws: true, - pathRewrite: {'^/api/lmio_remote_control' : ''} - }, - } - } -} -``` - -In the top-level `index.js` of your ASAB UI application, load the ASAB services module - -``` -const modules = []; - -... - -import ASABServicesModule from 'asab-webui/modules/maintenance/ServicesModule'; -modules.push(ASABServicesModule); - -... - -ReactDOM.render(( - - - -), document.getElementById('app')); -``` - -The module will be displayed as a subitem of `Maintenance` in the sidebar navigation. diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigContainer.js b/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigContainer.js deleted file mode 100644 index 0fc17c23c..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigContainer.js +++ /dev/null @@ -1,239 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useSelector, connect } from "react-redux"; - -import { - Container, Col, Row, - Card, CardBody -} from "reactstrap"; - -import { useTranslation } from 'react-i18next'; - -import { TreeViewComponent } from "./TreeViewComponent"; -import ConfigEditor from "./ConfigEditor"; -import ConfigList from "./ConfigList"; -import ConfigImport from "./ConfigImport"; -import { getBrandImage } from "asab-webui"; - -function ConfigContainer(props) { - - const ASABConfigAPI = props.app.axiosCreate('asab_config'); - const serviceURL = props.app.getServiceURL('asab_config'); - const { t } = useTranslation(); - - const configType = props.match.params.configType; - const configName = props.match.params.configName; - - const theme = useSelector(state => state.theme); - const homeScreenAlt = props.app.Config.get('title'); - - const [ treeData, setTreeData ] = useState({}); // Set complete data for TreeViewComponent - const [ createConfig, setCreateConfig ] = useState(false); // Use for condition to render components - const [ chosenPanel, setChosenPanel ] = useState("configurator"); // Sets the condition for showing the ConfigImport component - const [ typeList, setTypeList ] = useState([]); // Set data name of type for group configuration - const [ treeList, setTreeList ] = useState({}); // Set cleaned data for trigger UseEffect for updating TreeViewComponent, and for render the tree - const [ openNodes, setOpenNodes ] = useState([]); // Set open nodes in the TreeMenu - const [ homeScreenImg, setHomeScreenImg ] = useState({}); // Set open nodes in the TreeMenu - - useEffect(() => { - setHomeScreenImg(getBrandImage(props, theme)); - }, [theme]); - - // To get the full overview on schemas and configs it is needed to update the tree list and data state - useEffect(() => { - getTypes(); - }, []); - - useEffect(() => { - if (typeList.length > 0) { - getTree(); - } - }, [typeList]) - - useEffect(() => { - getChart(); - }, [treeList]); - - // Obtain list of types - // TODO: add Error Card screen when no types are fetched - const getTypes = async () => { - try { - let response = await ASABConfigAPI.get("/type"); - if (response.data.result != 'OK') { - throw new Error("Unable to get data for tree menu"); - } - // Sort data - let sortedData = response.data.data; - sortedData = sortedData.sort(); - setTypeList(sortedData); - // TODO: validate responses which are not 200 - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get data for tree menu")}. ${e?.response?.data?.message}`, 30); - return; - } - } - - // Obtain the list of configs parsed to the type key - const getTree = async () => { - let tree = await Promise.all(typeList.map(t => getConfigs(t))); - setTreeList(tree); - } - - - const getConfigs = async (typeId) => { - let tree = {}; - try { - let response = await ASABConfigAPI.get("/config/" + typeId); - if (response.data.result == 'OK'){ - // Sort data - let sortedData = response.data.data; - if (sortedData != undefined) { - sortedData = sortedData.sort(); - tree[typeId] = sortedData; - } - } - return tree; - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get schema. Try to reload the page", {type: typeId})}. ${e?.response?.data?.message}`, 30); - return; - } - } - - // Handle treeList to obtain the structure to render the tree - const getChart = () => { - let dataChart = []; - Object.values(treeList).map((element, idx) => { - addTreeStructure(element, dataChart); - }); - let nodes = []; - dataChart.map(node => { - nodes.push(node.key) - }); - setOpenNodes(nodes); - setTreeData(dataChart); - } - - - const addTreeStructure = (element, dataChart) => { - if (typeof element === 'object' && element !== null) { - Object.keys(element).map((key) => { - var obj = { - type: "folder", // this is not needed yet, but it might be useful for icons - key: key, - label: key, - nodes: [] - }; - dataChart.push(obj); - - var index = dataChart.indexOf(obj); - if (element[key] != undefined) { - element[key].map((e) => { - if (typeof e === "object" && e !== null) { - addTreeStructure(e, dataChart[index].nodes); - } else if (typeof e === "string" && e !== null) { - var strObj = { - type: "file", // this is not needed yet, but it might be useful for icons - key: e, - label: e - }; - dataChart[index].nodes.push(strObj); - } - }) - } - }) - } else if (element !== undefined) { - dataChart.push( - { - type: "file", // this is not needed yet, but it might be useful for icons - key: element, - label: element, - } - ); - } - } - - - // Render function - return ( - - - - - - - {chosenPanel != 'import' ? - configType != '$' && configName != '$' ? - configName != '!manage' && createConfig == false ? - - : - - : - - - - - - {homeScreenAlt} - - -

{t('ASABConfig|Nothing has been selected')}

-
- -
{t('ASABConfig|Please select the configuration from tree menu on the left side of the screen')}
-
- -
-
-
- : - - } - -
-
- ) -} - -function mapStateToProps(state) { - return { - config_created: state.asab_config.config_created, - config_removed: state.asab_config.config_removed, - config_imported: state.asab_config.config_imported - } -} - -export default connect(mapStateToProps)(ConfigContainer); diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigEditor.js b/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigEditor.js deleted file mode 100644 index d36dbc704..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigEditor.js +++ /dev/null @@ -1,776 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useForm } from "react-hook-form"; -import ReactJson from 'react-json-view'; -import classnames from 'classnames'; -import { useTranslation } from 'react-i18next'; -import { useHistory } from "react-router-dom"; -import { useSelector } from 'react-redux'; - -import { - Card, CardBody, CardHeader, CardFooter, - Form, FormGroup, FormText, Input, Label, - TabContent, TabPane, Nav, NavItem, NavLink, - Dropdown, DropdownToggle, DropdownMenu, DropdownItem, - Row, Col, ButtonGroup -} from "reactstrap"; - -import { - NumberConfigItem, - CheckBoxConfigItem, - ConfigAdHocItem, - StringItems -} from './ConfigFormatItems'; - -import { ButtonWithAuthz, getBrandImage } from 'asab-webui'; - -function ConfigEditor(props) { - const { register, handleSubmit, setValue, getValues, formState: { errors, isSubmitting }, reset, resetField } = useForm(); - const { t, i18n } = useTranslation(); - const ASABConfigAPI = props.app.axiosCreate('asab_config'); - let history = useHistory(); - - const [ adHocValues, setAdHocValues ] = useState({}); - const [ adHocSections, setAdHocSections ] = useState({}); - const [ formStruct, setFormStruct ] = useState({}); - const [ jsonValues, setJsonValues ] = useState({}); - - // States for schema sections - const [ selectPatternSections, setSelectPatternSections ] = useState([]); - const [ patternPropsSchema, setPatternPropsSchema ] = useState({}); - - // Retrieve the asab config url from config file - const homeScreenAlt = props.app.Config.get('title'); - const configType = props.configType; - const configName = props.configName; - - const [ configNotExist, setConfigNotExist ] = useState(false); - const [ activeTab, setActiveTab ] = useState('basic'); - - const resourceManageConfig = "asab:config:edit"; - const resources = useSelector(state => state.auth?.resources); - const theme = useSelector(state => state?.theme); - - // Pattern props dropdown - const [dropdownOpen, setDropdownOpen] = useState(false); - const toggleDropDown = () => setDropdownOpen(!dropdownOpen); - - // Branding - const [ homeScreenImg, setHomeScreenImg ] = useState({}); - - useEffect(() => { - setHomeScreenImg(getBrandImage(props, theme)) - }, [theme]); - - // The container will be re-rendered on configType or configName change - useEffect(() => { - initialLoad(); - }, [ configType, configName ]); - - // Set values based on form struct - useEffect(() => { - if (Object.keys(formStruct).length > 0) { - setValues(); - } - }, [formStruct]) - - // Load data and set up the data for form struct - const initialLoad = async () => { - let values = undefined; - let schema = undefined; - setConfigNotExist(false); - // Set selected pattern sections to empty - setSelectPatternSections([]); - - let prevValues = getValues(); - (prevValues && Object.keys(prevValues).length > 0) && Object.keys(prevValues).map((key, idx) => { - resetField(key); - }) - - try { - let response = await ASABConfigAPI.get(`/type/${configType}`); - // TODO: validate responses which are not 200 - if (response.data.result != 'OK') { - throw new Error("Something went wrong! Unable to get schema") - } - schema = response.data.data; - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get schema. Try to reload the page", {type: configType})}. ${e?.response?.data?.message}`, 30); - return; - } - - try { - let response = await ASABConfigAPI.get(`/config/${configType}/${configName}?format=json`); - if (response.data.result != "OK") { - props.app.addAlert("warning", `${t("ASABConfig|Config file does not exist")}. ${e?.response?.data?.message}`, 30); - setConfigNotExist(true); - return; - } - values = response.data.data; - // TODO: validate responses which are not 200 - } - catch(e) { - // Set the states to initial state on failed response to clear the inputs - setFormStruct({}); - setAdHocValues({}); - setAdHocSections({}); - setJsonValues({}); - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get config data. Try to reload the page", {type: configName})}. ${e?.response?.data?.message}`, 30); - return; - } - - let formStructure = {}; - let schemaProps = {}; - let sectionKeyData = {}; - let ahValues = {}; - let ahSections = values; - // TODO: handle nested patternProperties (in items) - // Check for properties of schema - if (schema.properties) { - schemaProps = schema.properties; - await Promise.all(values && Object.keys(values).map(async (section, idx) => { - await Promise.all(Object.keys(schema.properties).map(async (sectionName, id) => { - if (section == sectionName) { - let arrAHValues = []; - await Promise.all(Object.keys(values[section]).map(async (key, id) => { - sectionKeyData[`${section} ${key}`] = values[section][key]; - - // Check for adHoc values in properties of section - if (schemaProps[`${section}`].properties) { - // Add empty string to keys, which are not present in configuration, but key is present in schema - await Promise.all(Object.keys(schemaProps[`${section}`].properties).map((schemaKey, i) => { - if (Object.keys(values[section]).indexOf(schemaKey) == -1) { - sectionKeyData[`${section} ${schemaKey}`] = ""; - } - })); - // Check if key exist in schema and if not, add it to adHoc values - if (schemaProps[`${section}`].properties[`${key}`] == undefined) { - let v = {}; - v[key] = values[section][key]; - arrAHValues.push(v); - ahValues[`${section}`] = arrAHValues; - } - } - })); - // Mutate adHoc section based on the matching sections - // If section name match with schema, then remove it from adHoc sections (it is also removed for submit) - ahSections = Object.assign({}, ahSections); - delete ahSections[section]; - } - })); - })); - } - - // TODO: handle nested patternProperties (in items) - // Check for pattern properties of schema - if (schema.patternProperties) { - // Handle only pattern properties with data - if (values && Object.keys(values).length > 0) { - await Promise.all(values && Object.keys(values).map(async (section, idx) => { - await Promise.all(Object.keys(schema.patternProperties).map(async (sectionName, id) => { - // Check for matching section name - if (section.match(sectionName) != null) { - // If matched, then add schema to schema props - schemaProps[`${section}`] = schema.patternProperties[sectionName]; - let arrAHValues = []; - await Promise.all(Object.keys(values[section]).map(async (key, i) => { - // Add data for section - sectionKeyData[`${section} ${key}`] = values[section][key]; - - // Check for adHoc values in properties of section - if (schemaProps[`${section}`].properties) { - // Add empty string to keys, which are not present in configuration, but key is present in schema - await Promise.all(Object.keys(schemaProps[`${section}`].properties).map((schemaKey, i) => { - if (Object.keys(values[section]).indexOf(schemaKey) == -1) { - sectionKeyData[`${section} ${schemaKey}`] = ""; - } - })); - // Check if key exist in schema and if not, add it to adHoc values - if (schemaProps[`${section}`].properties[`${key}`] == undefined) { - let v = {}; - v[key] = values[section][key]; - arrAHValues.push(v); - ahValues[`${section}`] = arrAHValues; - } - } - })); - // Mutate adHoc section based on the matching sections - // If section name match with schema, then remove it from adHoc sections (it is also removed for submit) - ahSections = Object.assign({}, ahSections); - delete ahSections[section]; - } - })); - })); - } - - // Trim pattern props section names and push it to array to use it in the selector for adding a new empty config section - let patternSections = []; - await Promise.all(Object.keys(schema.patternProperties).map((sectionName, idx) => { - let sectionTrimmed = sectionName.substring(1).replace(":.*$", ""); - patternSections.push(sectionTrimmed); - })) - setSelectPatternSections(patternSections); - setPatternPropsSchema(schema.patternProperties); - } - - // Set values for JSON view - setJsonValues(values); - // Set data for adHoc sections - setAdHocSections(ahSections); - // Set data for adHoc values - setAdHocValues(ahValues); - - // Assign sectionKeyData to form struct under data key - formStructure["data"] = sectionKeyData; - // Assign schema to form struct under properties key - formStructure["properties"] = schemaProps; - // Set data and properties for form struct - setFormStruct(formStructure); - } - - - // Set values from form struct and adHoc sections - const setValues = () => { - // Reset old values before setting new values to prevent unintended data submitting - reset({}); - - /* - Set empty values for empty configuration (it will remove old values - from forms when config is completelly empty and which reset is not able - to remove) - */ - if (formStruct.properties && formStruct.data && Object.keys(formStruct.data).length == 0) { - Object.keys(formStruct.properties).map((key, idx) => { - if (formStruct.properties[key].properties) { - Object.keys(formStruct.properties[key].properties).map((k, i) => { - setValue(`${key} ${k}`, ""); - }) - } - }) - } - - // Set values from form struct for registration and submitting - if (formStruct.data) { - Object.entries(formStruct.data).map((entry, idx) => { - setValue(`${entry[0]}`, entry[1]); - }); - } - // Set adHoc sections for submitting - if (adHocSections) { - Object.keys(adHocSections).map((section, idx) => { - Object.keys(adHocSections[section]).map((key, id) => { - setValue(`${section} ${key}`, adHocSections[section][key]); - }); - }); - } - } - - - // Parse data to JSON format, stringify it and save to config file - const onSubmit = async (data) => { - // Get parsed sections for submit - let parsedSections = await getParsedSections(data); - - try { - let response = await ASABConfigAPI.put(`/config/${configType}/${configName}`, - JSON.parse(JSON.stringify(parsedSections)), - { headers: { - 'Content-Type': 'application/json' - } - } - ) - if (response.data.result != "OK"){ - throw new Error(t('ASABConfig|Something went wrong, failed to update data')); - } - props.app.addAlert("success", t('ASABConfig|Data updated successfully')); - initialLoad(); // Load the new data after saving - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Something went wrong, failed to update data")}. ${e?.response?.data?.message}`, 30); - initialLoad(); - return; - } - } - - // Swith between the tabs - const toggle = tab => { - if(activeTab !== tab) setActiveTab(tab); - } - - // Function to convert value types from string - function convertValueType(sectionValue, valueType) { - /* - Conversion based on - https://json-schema.org/understanding-json-schema/reference/type.html - */ - let value; - // Check number type values - if (valueType == "number" || - valueType == "integer" || - valueType == "float" || - valueType == "null" || - valueType == "boolean") { - // If value is an empty string, then return undefined (to prevent parsing failures) - if (sectionValue === "") { - value = value; - } else { - value = JSON.parse(sectionValue); - } - } - // Check for array type values - else if (valueType == "array") { - // If value is an empty string, then return undefined (to prevent parsing failures) - if (sectionValue === "") { - value = value; - } else { - value = sectionValue.toString().split(","); - } - } - // Check for object type values - else if (valueType == "object") { - // If value is an empty string, then return undefined (to prevent parsing failures) - if (sectionValue === "") { - value = value; - } else { - value = JSON.parse(JSON.stringify(sectionValue)); - } - } - // If not match any of the types, return default (string). This apply also for adHoc values of sections - else { - value = sectionValue; - } - return value; - } - - // Confirm message form for config section removal - const removeSectionForm = (sectionTitle) => { - var r = confirm(t("ASABConfig|Do you want to remove this section?")); - if (r == true) { - removeSection(sectionTitle); - } - } - - // TODO: Unify the code with onSubmit, that it does not repeat itself - // Remove config section - const removeSection = async (sectionTitle) => { - let data = getValues(); - // Get parsed section for removal section - let parsedSections = await getParsedSections(data); - - // Remove section out of data and save result - delete parsedSections[sectionTitle] - - try { - let response = await ASABConfigAPI.put(`/config/${configType}/${configName}`, - JSON.parse(JSON.stringify(parsedSections)), - { headers: { - 'Content-Type': 'application/json' - } - } - ) - if (response.data.result != "OK"){ - throw new Error(t('ASABConfig|Something went wrong, failed to update data')); - } - props.app.addAlert("success", t('ASABConfig|Data updated successfully')); - initialLoad(); // Load the new data after saving - } - catch(e) { - console.error(e); - props.app.addAlert("warning", t("ASABConfig|Something went wrong, failed to update data", {error: e?.response?.data?.message}), 30); - initialLoad(); - return; - } - } - - // Add new section out of pattern properties section list - const addNewSection = async (selectedSection) => { - let section = selectedSection; - let properties = formStruct.properties; - let cnt = 1; - let selectedProperties = {}; - await Promise.all(Object.keys(properties).map((sectionName, idx) => { - if (sectionName.match(section) != null) { - cnt += 1; - selectedProperties = properties[sectionName]; - } - })) - - let formStructure = formStruct; - if (cnt == 1) { - // If section not present in the configuration, use schema obtained from the service - await Promise.all(Object.keys(patternPropsSchema).map(async (sectionName, id) => { - if (sectionName.match(section) != null) { - formStructure["properties"][`${section}:${cnt}`] = patternPropsSchema[sectionName]; - } - })) - } else { - // If section already present in the configuration, use its schema props - let newSection = `${section}:${cnt}`; - // Check if there is a section of the same name in the configuration - await Promise.all(Object.keys(properties).map(async (sectionName, id) => { - // If there is a section of the same name in the config, add a random string to new section - if (sectionName === newSection) { - let randomString = Math.random().toString(36).substr(2, 1); - newSection = newSection + randomString; - } - })) - formStructure["properties"][newSection] = selectedProperties; - } - - props.app.addAlert("success", t('ASABConfig|Section added')); - // Update form struct and call setValues function to load data - setFormStruct(formStructure); - setValues(); - } - - // Function for obtaining parsed sections - const getParsedSections = async (data) => { - // Get 'type' of the values (if defined) from the schema - let formStructProperties = formStruct.properties; - let sectionTypes = {}; - // Iterate through sections - if (Object.keys(formStructProperties).length > 0) { - await Promise.all(Object.keys(formStructProperties).map(async (sect, idx) => { - let valueTypes = {}; - // Iterate through section keys - await Promise.all(Object.keys(formStructProperties[sect]).length > 0 && Object.keys(formStructProperties[sect]).map(async (key, id) => { - if (key === "properties") { - // Iterate through key properties - await Promise.all(Object.entries(formStructProperties[sect]["properties"]).map((entry, i) => { - // If type of the value is undefined, then default is string - valueTypes[entry[0]] = entry[1].type ? entry[1].type : "string"; - })); - } - })); - sectionTypes[sect] = valueTypes; - })); - } - - let parsedSections = {}; - // TODO: Disable saving output from ReactJSONview component - if (activeTab == 'advanced') { - // If data are being submitted from JSON view, dont parse data to object - parsedSections = jsonValues; - } else { - let splitKey = ""; - let sectionTitle = ""; - let sectionKey = ""; - let sectionValue = ""; - // Parse data to object - await Promise.all(Object.keys(data).map((key, idx) => { - splitKey = key.split(" "); - sectionTitle = splitKey[0]; - sectionKey = splitKey[1]; - sectionValue = data[key]; - // Parsing - let obj = {}; - if (sectionTypes[sectionTitle] == undefined) { - // Values of adHoc sections - obj[sectionKey] = sectionValue; - parsedSections[sectionTitle] = {...parsedSections[sectionTitle], ...obj}; - } else { - let valueType = sectionTypes[sectionTitle][sectionKey]; - obj[sectionKey] = convertValueType(sectionValue, valueType); - parsedSections[sectionTitle] = {...parsedSections[sectionTitle], ...obj}; - } - })); - } - return parsedSections; - } - - // Convert pattern section name for Add button from technical name to Title of the section defined in schema - const sectionNameString = (patternpropsSchema, patternSection) => { - let patternKey = Object.keys(patternPropsSchema) ? Object.keys(patternPropsSchema).filter(key => key.match(patternSection)) : ""; - let returnString = patternSection; - Object.keys(patternPropsSchema) && Object.keys(patternPropsSchema).map((key,idx) => { - if (patternKey[0] == key) { - returnString = patternPropsSchema[key]?.title ? patternPropsSchema[key]?.title : patternSection; - } - }) - return returnString; - } - - if (configNotExist) { - return ( - - ) - } - - // TODO: add Content loader when available as a component in ASAB WebUI - return ( - -
- -
- - {configName ? configType.toString() + ' / ' + configName.toString() : ""} -
- {/* TODO: Replace div.float-right with ButtonGroup */} - - - -
- - - - - {/* List of Sections (it may consist also of AdHocValues) */} - {formStruct && formStruct.properties && Object.keys(formStruct.properties).map((section_name, idx) => - - )} - - {/* List all remaining sections e.g. AdHocSections */} - {Object.keys(adHocSections).length > 0 && Object.keys(adHocSections).map((section_name, idx) => - - )} -
-
-
- -
- { setJsonValues(e.updated_src)} } - enableClipboard={false} - name={false} - /> -
-
-
-
- - - - {t('ASABConfig|Save')} - - - {selectPatternSections.length > 0 && - - - - {t('ASABConfig|Add')} - - - {selectPatternSections.map((patternSection, idx) => { - return( - {addNewSection(patternSection), e.preventDefault()}} - > - {sectionNameString(patternPropsSchema, patternSection)} - - ) - })} - - - - } - -
-
- ); -} - -export default ConfigEditor; - - -function ConfigSection(props) { - const { t, i18n } = useTranslation(); - return ( - -
- - -
- {props.section['title']} -
- - -
- {props.selectPatternSections.length > 0 && - {props.removeSectionForm(props.sectionname)}} - disabled={props.isSubmitting} - resource={props.resourceManageConfig} - resources={props.resources} - > - - - } -
- -
- - {Object.keys(props.section.properties).map((item_name, idx) => - // Decide what type of config item to render based on format - // TODO: Update also other RADIO and SELECT types - {switch(props.section.properties[item_name]['type']){ - case 'string': return() - case 'number': return() - case 'integer': return() - case 'boolean': return() - default: return() - }} - )} - - {/* List all remaining key/values (aka AdHocValues) from a config as simple Config Item */} - {Object.keys(props.adhocvalues).length > 0 && Object.keys(props.adhocvalues).map((value_name, idx) => - {return(props.sectionname == value_name ? - - : null - )} - )} -
- - ); -} - - -function ConfigAdHocSection(props) { - const { t, i18n } = useTranslation(); - let myid = props.sectionname; - return ( - -
-
- {myid} -
- {Object.keys(props.values).length > 0 && Object.keys(props.values).map((key, idx) => - { - return ( - - - - - {t('ASABConfig|Read only')} - - - )} - )} -
- ); -} - -// This component returns a pre-defined messages -function ConfigMessageCard(props) { - return( - - - {props.homeScreenAlt} -

{props.purposeTitle}

-
{props.purposeSubtitle}
-
-
) -} diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigFormatItems.js b/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigFormatItems.js deleted file mode 100644 index a88a299c7..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigFormatItems.js +++ /dev/null @@ -1,378 +0,0 @@ -import React from "react"; - -import { - Form, FormGroup, FormText, Input, Label -} from "reactstrap"; - -import { useTranslation } from 'react-i18next'; - -// TODO: Different types of ConfigItem to cover formats such as "number", "boolean", checkbox, radiobox - -export function StringItems(props) { - if (props.defs) { - let def = Object.keys(props.defs)[0]; - if (def == 'url') { - return ( - ) - } else if (def == 'email') { - return ( - ) - } else if (def == 'password') { - return ( - ) - - } else if (def == 'textarea') { - return ( - ) - } else if (def == 'select') { - return ( - ) - } else { - // If not defined as above or if `text`, then the text input is used - return ( - ) - } - } else { - return ( - ) - } -} - -export function ConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); -} - - -export function NumberConfigItem(props) { - if (props.defs && Object.keys(props.defs)[0] == 'select') { - return ( - ) - } else { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); - } -} - - -export function UrlConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); -} - - -export function EmailConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); -} - - -export function PasswordConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); -} - - -export function CheckBoxConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - -
- -
- - {props.item['description']} - -
- ); -} - -// TODO: Implement and test radio button when there will be a case for it -export function RadioButtonConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - -
- -
- - - - {props.item['description']} - - -
- ); -} - - -export function SelectConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - {props.item['default'].length > 0 ? props.item['default'].map((val, idx) => {return( - - )}) : null} - - - {props.item['description']} - - - ); -} - - -export function TextAreaConfigItem(props) { - let myid = `${props.sectionname} ${props.itemname}`; - const reg = props.register(myid); - return ( - - - - - {props.item['description']} - - - ); -} - -export function ConfigAdHocItem(props) { - const { t, i18n } = useTranslation(); - let myid = props.valuename; - return ( - props.values.length > 1 ? - props.values.map(obj => { - return( - - - - - {t('ASABConfig|Read only')} - - - ) - }) - : - - - - - - {t('ASABConfig|Read only')} - - - - ); -} diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigImport.js b/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigImport.js deleted file mode 100644 index 8be82d628..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigImport.js +++ /dev/null @@ -1,165 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { - CardBody, CardHeader, CardFooter, Row, Col, - Button, Input, Label, - FormGroup, FormText, InputGroup, - InputGroupText, Card, ButtonGroup -} from 'reactstrap'; -import {types} from "./actions/actions"; - - -const ConfigImport = (props) => { - - const ASABConfigAPI = props.app.axiosCreate('asab_config'); - const { t } = useTranslation(); - const [chosenFilename, setChosenFilename] = useState("No file chosen"); - const [type, setType] = useState("merge"); - const [errors, setErrors] = useState(false); - const inputFileRef = useRef(null) - const formRef = useRef(null); - - useEffect(() => { - if (props.configImported) { - props.getTree(); - props.app.Store.dispatch({ - type: types.CONFIG_IMPORTED, - config_imported: false - }); - } - }, [props.configImported]) - - // Choose file, click simulation on reference of the another - const chooseFile = () => { - if (!inputFileRef.current) return; - - inputFileRef.current.click(); - } - // File formatting - const updateFilename = () => { - if (!inputFileRef.current) return; - - // Get filename from input and remove path - const filename = inputFileRef.current.value.replace(/.*[\/\\]/, ''); - setChosenFilename(filename); - - // Check if file is tar - if (!filename.includes(".tar.gz")) { - setErrors(true); - } else { - setErrors(false); - } - } - - const onTypeChange = (e) => setType(e.target.value); - - // Import PUT request - const importConfiguration = async (event) => { - event.preventDefault(); - try { - const data = new FormData(formRef.current); - const response = await ASABConfigAPI.put(`/import?type=${type}`, data, { - headers: { - 'Content-Type': 'multipart/form-data' - } - }) - - if (response.data.result !== "OK") throw new Error(`Response result is ${response.data.result}. File has not been imported`); - props.app.Store.dispatch({ - type: types.CONFIG_IMPORTED, - config_imported: true - }); - props.setChosenPanel("editor"); - props.app.addAlert("success", t("ASABConfig|Configuration has been successfully imported")); - } catch (e) { - console.error("Failed to import configuration\n", e); - props.app.addAlert("warning", `${t("ASABConfig|Failed to import configuration")}. ${e?.response?.data?.message}`, 30); - } - } - - return ( - -
- -
- - {t("ASABConfig|Import configuration")} -
-
- - - - - - - {t("ASABConfig|Choose file")} - - - {t("ASABConfig|Only tar.gz files are allowed")} - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} - -export default ConfigImport; diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigList.js b/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigList.js deleted file mode 100644 index 5c587eb7e..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/ConfigList.js +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { useTranslation } from 'react-i18next'; -import { useHistory, Link } from "react-router-dom"; -import { useSelector } from 'react-redux'; - -import { - Button, - Card, CardBody, CardHeader, CardFooter, - Form, FormGroup, FormText, Input, Label -} from "reactstrap"; - -import {types} from './actions/actions'; - -import { DataTable, ButtonWithAuthz } from 'asab-webui'; - -function ConfigList(props) { - const { register, handleSubmit, getValues, formState: { errors, isSubmitting }, reset } = useForm(); - const { t, i18n } = useTranslation(); - const ASABConfigAPI = props.app.axiosCreate('asab_config'); - let history = useHistory(); - - const [ configList, setConfigList ] = useState([]); - const [ description, setDescription ] = useState(""); - - const resourceManageConfig = "asab:config:edit"; - const resources = useSelector(state => state.auth?.resources); - - const configType = props.configType; - - const regConfigName = register("configName", - { - validate: { - emptyInput: value => (getValues("configName") !== "" || t("ASABConfig|Configuration name can't be empty!")), - } - }); - - // The container will be re-rendered on configType or configName change - useEffect(() => { - initialLoad(); - }, [ configType ]); - - - const headers = [ - { - name: t('ASABConfig|Name'), - key: "name", - link: { pathname: `/config/${configType}/`, key: "name" } - }, - { - name: ' ', - customComponent: { - generate: (obj) => ( -
- {removeConfigForm(obj.name), e.preventDefault()}} - > - - -
- ) - } - } - - ]; - - - // Load data and set up the data for form struct - const initialLoad = async () => { - props.setCreateConfig(false); - let data = []; - let schema = {}; - try { - let response = await ASABConfigAPI.get(`/config/${configType}`); - // TODO: validate responses which are not 200 - if (response.data.result != 'OK') { - throw new Error(t(`ASABConfig|Something went wrong! Unable to get configurations`, {config: configType})); - } - data = response.data.data; - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get configurations. Try to reload the page", {config: configType})}. ${e?.response?.data?.message}`, 30); - } - - try { - let response = await ASABConfigAPI.get(`/type/${configType}`); - // TODO: validate responses which are not 200 - if (response.data.result != 'OK') { - throw new Error(t(`ASABConfig|Something went wrong! Unable to get schema`, {type: configType})); - } - schema = response.data.data; - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Unable to get schema. Try to reload the page", {type: configType})}. ${e?.response?.data?.message}`, 30); - } - - // Create an array of objects with name, schema title and schema description - let cfgList = []; - Promise.all(await data.map(cfg => { - cfgList.push({name: cfg}); - })); - setConfigList(cfgList); - setDescription(schema?.description ? schema.description : "") - } - - // Create configuration button - const createConfigComponent = ( - {props.setCreateConfig(true), e.preventDefault()}} - resource={resourceManageConfig} - resources={resources} - > - {t("ASABConfig|Create")} - - ); - - // Confirm message form for configuration removal - const removeConfigForm = (configName) => { - var r = confirm(t("ASABConfig|Do you want to remove this configuration?")); - if (r == true) { - removeConfig(configName); - } - } - - // Remove configuration - const removeConfig = async (configName) => { - try { - let response = await ASABConfigAPI.delete(`/config/${configType}/${configName}`); - if (response.data.result != "OK"){ - throw new Error(t('ASABConfig|Something went wrong, failed to remove configuration')); - } - props.app.Store.dispatch({ - type: types.CONFIG_REMOVED, - config_removed: true - }); - initialLoad(); - } catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Something went wrong, failed to remove configuration")}. ${e?.response?.data?.message}`, 30); - } - } - - if (props.createConfig) { - return - } - - return ( -
- {`${description}`}
} - title={{ text: t("ASABConfig|Type") + ` ${configType}`, icon: "cil-settings" }} - headers={headers} - data={configList} - limit={99999} - customComponent={createConfigComponent} - /> - - ) -} - -export default ConfigList; - - -function CreateConfigCard(props) { - const { register, handleSubmit, getValues, formState: { errors, isSubmitting }, reset } = useForm(); - const { t, i18n } = useTranslation(); - const ASABConfigAPI = props.app.axiosCreate('asab_config'); - let history = useHistory(); - - const regConfigName = register("configName", - { - validate: { - emptyInput: value => (getValues("configName") !== "" || t("ASABConfig|Configuration name can't be empty!")), - } - }); - - // Parse data to JSON format, stringify it and save to config file - const onSubmit = async (data) => { - let configName = data.configName; - let configNameExtension = configName.split('.').pop(); - - if (configName == configNameExtension) { - configName = `${configName}.json` - } - - try { - let response = await ASABConfigAPI.put(`/config/${props.configType}/${configName}`, - {}, - { headers: { - 'Content-Type': 'application/json' - } - } - ) - if (response.data.result != "OK"){ - throw new Error(t('ASABConfig|Something went wrong, failed to create configuration')); - } - props.setCreateConfig(false); - props.app.addAlert("success", t('ASABConfig|Configuration created successfully')); - props.app.Store.dispatch({ - type: types.CONFIG_CREATED, - config_created: true - }); - history.push({ - pathname: `/config/${props.configType}/${configName}` - }); - } - catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ASABConfig|Something went wrong, failed to create configuration")}. ${e?.response?.data?.message}`, 30); - } - } - - return( -
- - -
- - {t("ASABConfig|Type") + ` ${props.configType.toString()} / ` + t('ASABConfig|New configuration')} -
- -
- - - - - - {errors.configName ? errors.configName.message : t('ASABConfig|Fill out configuration name')} - - - -
-
- ) -} diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/TreeViewComponent.js b/src/modules/maintenance/ConfigModule/ConfigContainers/TreeViewComponent.js deleted file mode 100644 index 2985d4e52..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/TreeViewComponent.js +++ /dev/null @@ -1,133 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useHistory } from "react-router-dom"; -import { useSelector } from 'react-redux'; -import { useTranslation } from 'react-i18next'; - -import { TreeMenu } from 'asab-webui'; -import { types } from './actions/actions'; -import { - Input, - InputGroup, InputGroupText, - ButtonDropdown, DropdownToggle, - DropdownMenu, DropdownItem -} from "reactstrap"; - - -export function TreeViewComponent(props) { - const setChosenPanel = props.setChosenPanel; - - let history = useHistory(); - const { t, i18n } = useTranslation(); - - const [isDropdownMenuOpen, setDropdownMenu] = useState(false); - - // Obtain resources from state (if available) - const resources = useSelector(state => state.auth?.resources); - const resource = "asab:config:edit"; - - useEffect(() => { - if (props.configCreated || props.configRemoved) { - props.getTree(); - if (props.configCreated) { - props.app.Store.dispatch({ - type: types.CONFIG_CREATED, - config_created: false - }); - } - if (props.configRemoved) { - props.app.Store.dispatch({ - type: types.CONFIG_REMOVED, - config_removed: false - }); - } - } - }, [props.configCreated, props.configRemoved]); - - - // Get the configType and configName from the TreeView menu - const onClickItem = ({ key }) => { - // TODO: Update for multilevel tree structure - let splitKey = key.split("/"); - props.setCreateConfig(false); - if (splitKey.length > 1) { - // Push params to the URL - history.push({ - pathname: `/config/${splitKey[0]}/${splitKey[1]}`, - }) - } else { - history.push({ - pathname: `/config/${splitKey[0]}/!manage`, - }) - } - setChosenPanel('configurator') - } - - - const TreeMenuDropdownMenu = ( - - {resources ? resources.indexOf(resource) == -1 && resources.indexOf("authz:superuser") == -1 ? - - - {t("ASABConfig|Export")} - - : - - - - {t("ASABConfig|Export")} - - - : - - - {t("ASABConfig|Export")} - - } - setChosenPanel("import")} - > - - {t("ASABConfig|Import")} - - - ); - - return ( - - ) -} diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/actions/actions.js b/src/modules/maintenance/ConfigModule/ConfigContainers/actions/actions.js deleted file mode 100644 index eb1bb5fa7..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/actions/actions.js +++ /dev/null @@ -1,5 +0,0 @@ -export const types = { - CONFIG_CREATED: "asab/ASAB-CONFIG/CONFIG_CREATED", - CONFIG_REMOVED: "asab/ASAB-CONFIG/CONFIG_REMOVED", - CONFIG_IMPORTED: "asab/ASAB-CONFIG/CONFIG_IMPORTED" -} diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/configuration.scss b/src/modules/maintenance/ConfigModule/ConfigContainers/configuration.scss deleted file mode 100644 index 6972e6e1a..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/configuration.scss +++ /dev/null @@ -1,76 +0,0 @@ -.config-container { - height: 100%; - - .card-editor-layout { - height: 100%; - width: 100%; - - form { - height: 100%; - display: flex; - flex-direction: column; - } - } - - .card-editor-body { - height: 100% !important; - overflow: auto !important; - overflow-x: hidden !important; - } - - /* ConfigList DataTable */ - .config-list-dt { - height: 100% !important; - .row { - height: 100%; - .card, .col { - height: 100% !important; - } - .card-body { - overflow: auto !important; - } - .card-footer { - display: none !important; - } - } - } - - /*Config Editor*/ - .pattern-section-dropdown { - max-height: 400px; - overflow-y: auto; - } - - .hidden-file-input { - display: none; - } - - .file-input { - overflow: hidden; - display: flex; - flex-wrap: wrap; - flex: 1 5; - input { - cursor: pointer; - &:focus { - box-shadow: none; - border-color: #ced4da; - } - } - .input-group-text { - cursor: pointer; - border-radius: 5px 0 0 5px; - border-right: none; - } - } - - .dropdown-export-item, .dropdown-export-item:hover { - text-decoration: none; - } - - .config-editor-cardbody { - display: grid; - align-items: center; - justify-content: center; - } -} \ No newline at end of file diff --git a/src/modules/maintenance/ConfigModule/ConfigContainers/reducer.js b/src/modules/maintenance/ConfigModule/ConfigContainers/reducer.js deleted file mode 100644 index da551130c..000000000 --- a/src/modules/maintenance/ConfigModule/ConfigContainers/reducer.js +++ /dev/null @@ -1,28 +0,0 @@ -// Actions -import {types} from './actions/actions'; - -const initialState = { - config_created: false, - config_removed: false, - config_imported: false -} - -export default function asabConfigReducer(state = initialState, action) { - switch (action.type) { - - case types.CONFIG_CREATED: - return { - config_created: action.config_created - }; - case types.CONFIG_REMOVED: - return { - config_removed: action.config_removed - } - case types.CONFIG_IMPORTED: - return { - config_imported: action.config_imported - } - default: - return state - } -} diff --git a/src/modules/maintenance/ConfigModule/index.js b/src/modules/maintenance/ConfigModule/index.js deleted file mode 100644 index 930259262..000000000 --- a/src/modules/maintenance/ConfigModule/index.js +++ /dev/null @@ -1,56 +0,0 @@ -import { lazy } from 'react'; -import Module from 'asab-webui/abc/Module'; -import { componentLoader } from 'asab-webui'; -const ConfigContainer = lazy(() => componentLoader(() => import("./ConfigContainers/ConfigContainer"))); - -import asabConfigReducer from './ConfigContainers/reducer'; - -import "./ConfigContainers/configuration.scss"; - -export default class ConfigModule extends Module { - constructor(app, name) { - super(app, "ASABConfigModule"); - // Using redux to update items in Coniguration right after the change - app.ReduxService.addReducer("asab_config", asabConfigReducer); - - app.Router.addRoute({ - path: "/config/:configType/:configName", - exact: true, - name: "Configuration", - component: ConfigContainer, - resource: "asab:config:access" - }); - - // Check presence of Maintenance item in sidebar - let items = app.Navigation.getItems()?.items; - let isMaintenancePresent = false; - items.forEach(itm => { - // If Maintenance present, then append Config as a Maintenance subitem - if (itm?.name == "Maintenance") { - itm.children.push({ - name: "Configuration", - url: "/config/$/$", - icon: "cil-settings", - resource: "asab:config:access" - }); - isMaintenancePresent = true; - } - }) - - // If Maintenance not present in sidebar navigation, add a Maintenance item - if (!isMaintenancePresent) { - app.Navigation.addItem({ - name: 'Maintenance', - icon: "cil-apps-settings", - children: [ - { - name: "Configuration", - url: "/config/$/$", - icon: "cil-settings", - resource: "asab:config:access" - } - ] - }); - } - } -} diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js b/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js deleted file mode 100644 index b8a6ca57d..000000000 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/ServicesContainer.js +++ /dev/null @@ -1,549 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import ReactJson from 'react-json-view'; -import { useSelector } from 'react-redux'; - -import { Container, Card, CardBody, CardHeader, Table, - InputGroup, InputGroupText, Input, InputGroupAddon, - ButtonGroup -} from 'reactstrap'; - -import { CellContentLoader } from 'asab-webui'; - -import ActionButton from "./components/ActionButton"; - -export default function ServicesContainer(props) { - - const [fullFrameData, setFullFrameData] = useState({}); - const [wsData, setWSData] = useState({}); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - const [errorMsg, setErrorMsg] = useState(""); - - const [filter, setFilter] = useState(""); - - const theme = useSelector(state => state.theme); - - const { t } = useTranslation(); - - // Set up websocket connection - let wsSubPath = '/ws'; - const serviceName = 'lmio_remote_control'; - let WSUrl = props.app.getWebSocketURL(serviceName, wsSubPath); - let WSClient = null; - - const isMounted = useRef(null); - - // Connect to ws on page initialization, close ws connection on page leave - useEffect(() => { - isMounted.current = true; - - if (WSUrl != undefined) { - reconnect(); - } - - return () => { - if (WSClient != null) { - try { - WSClient.close(); - } catch (e) { - console.log("Ignored exception: ", e) - } - } - - isMounted.current = false; - } - }, []); - - - // Use memo for data rendering (due to expensive caluclations) - const data = useMemo(() => { - let webSocketData = wsData.data; - // Render ws data - if(webSocketData && Object.keys(webSocketData)) { - // Check for delta frame - if (wsData?.frame_type === "df") { - // If key in full frame, update data, otherwise append wsData to fullframe object without mutating the original object - let renderAll = {...fullFrameData, ...{}}; - Object.keys(webSocketData).map((dfd, idx) => { - if (fullFrameData[dfd]) { - // New additions / updates to values of object - const additions = webSocketData[dfd] ? webSocketData[dfd] : {}; - // Append new values / updates to values of object - let updateValues = {...renderAll[dfd], ...additions} - // Create a new key-value pair from deltaFrame key and new/updated values - let newObj = {}; - newObj[dfd] = updateValues; - // Append new object to fullFrame data object without mutation of original one - renderAll = {...renderAll, ...newObj}; - } else { - let newObj = {}; - newObj[dfd] = webSocketData[dfd]; - // Append wsData object to fullFrame object and return it - renderAll = {...renderAll, ...newObj}; - } - }) - // Set fullFrame with update data from delta frame - setFullFrameData(renderAll); - return renderAll; - } else { - // Set full frame with full frame data - setFullFrameData(webSocketData); - return webSocketData; - } - } - // Fallback if wsData will not meet the condition requirements - return fullFrameData; - }, [wsData]) // If websocket data change, then trigger computation of data rendering - - - // Filter state among data - const filteredData = useMemo (() => { - if ((filter != undefined) && (filter.length > 0)) { - const fltr = filter.toLowerCase(); - let filteredObj = {}; - Object.keys(fullFrameData).map((key, idx) => { - if (fullFrameData[key].state) { - if (fullFrameData[key].state.indexOf(fltr) != -1) { - filteredObj[key] = fullFrameData[key]; - } - } - }) - return filteredObj; - } - return undefined; - }, [filter, fullFrameData]) - - // Reconnect ws method - const reconnect = () => { - if (WSClient != null) { - try { - WSClient.close(); - } catch (e) { - console.log("Ignored exception: ", e) - } - } - - if (isMounted.current === false) return; - - WSClient = props.app.createWebSocket(serviceName, wsSubPath); - - // TODO: remove onopen - WSClient.onopen = () => { - console.log('ws connection open'); - } - - WSClient.onmessage = (message) => { - setLoading(false); - if (IsJsonString(message.data) == true) { - let retrievedData = JSON.parse(message.data); - if (retrievedData && Object.keys(retrievedData)) { - // Set websocket data - setWSData(retrievedData); - } - setError(false); - } else { - setErrorMsg(t("ServicesContainer|Can't display data due to parsing error")); - setError(true); - } - }; - - WSClient.onerror = (error) => { - setLoading(false); - setErrorMsg(t("ServicesContainer|Can't establish websocket connection, data can't be loaded")); - setError(true); - setTimeout(() => { - reconnect(); - }, 3000, this); - }; - } - - - return ( - - - -
- {t("ServicesContainer|Services")} -
- -
- - {(loading == true) ? - - : - - - - - - - - - - - - - - - - - - - - - {(error == true) ? - - - - : - - } - -
- - {t("ServicesContainer|Service")} - - {t("ServicesContainer|Node ID")} - - {t("ServicesContainer|Name")} - - {t("ServicesContainer|Version")} - -
{errorMsg}
- } -
-
-
- ) -} - -// Method to render table row with data -const DataRow = ({data, props}) => { - const { t } = useTranslation(); - - // Generate status - /* - Cannot use generateStatus func from separate container, cause it causes - "Rendered more hooks than during the previous render." error - */ - const generateStatus = (status) => { - if (status == undefined) { - return (
); - } - if (typeof status === "string") { - return statusTranslations(status); - } - if (typeof status === "object") { - return statusTranslations(status.name); - } - return status; - } - - // Translate well known statuses - const statusTranslations = (status) => { - - if (status.toLowerCase() === "running") { - return (
); - }; - if (status.toLowerCase() === "starting") { - return (
); - }; - if (status.toLowerCase() === "stopped") { - return (
); - }; - if (status.toLowerCase() === "unknown") { - return (
); - }; - return (
); - } - - return( - data && Object.keys(data).map((objKey) => ( - - )) - ) -} - -// Content of the row in table -const RowContent = ({props, objKey, data, generateStatus}) => { - const { t } = useTranslation(); - const theme = useSelector(state => state.theme); - const [collapseData, setCollapseData] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - if (data[objKey]?.state && ((data[objKey]?.state == "stopped") || (data[objKey]?.state == "starting"))) { - setCollapseData(false); - } else { - setCollapseData(true); - } - },[data[objKey]?.state]) - - // Action to start, stop, restart and up the container - const setAction = async(action, id) => { - let body = {}; - body["command"] = action; - const LMIORemoteControlAPI = props.app.axiosCreate('lmio_remote_control'); - try { - let response = await LMIORemoteControlAPI.post(`/instance/${id}`, body); - if (response.data.result != "Accepted") { - throw new Error(`Something went wrong, failed to ${action} container`); - } - props.app.addAlert("success", t("ServicesContainer|Service action accepted successfully")); - } catch(e) { - console.error(e); - props.app.addAlert("warning", `${t("ServicesContainer|Service action has been rejected")}. ${e?.response?.data?.message}`, 30); - } - setIsSubmitting(false); - } - - return( - <> - - -
- {collapseData ? - {setCollapseData(false)}}> - : - {setCollapseData(true)}}> - } - {generateStatus(data[objKey]?.state ? data[objKey].state : undefined)} -
- - - {data[objKey]?.service} - - - {data[objKey]?.node_id?.toString()} - - - {data[objKey]?.name?.toString()} - - - {data[objKey]?.advertised_data?.version ? data[objKey]?.advertised_data?.version : data[objKey]?.version ? data[objKey]?.version : "N/A"} - - -
- - {setAction("start", data[objKey]?.instance_id), setIsSubmitting(true)}} - disabled={isSubmitting == true} - /> - {setAction("stop", data[objKey]?.instance_id), setIsSubmitting(true)}} - icon="cil-media-stop" - disabled={isSubmitting == true} - /> - {setAction("restart", data[objKey]?.instance_id), setIsSubmitting(true)}} - icon="cil-reload" - disabled={isSubmitting == true} - /> - {setAction("up", data[objKey]?.instance_id), setIsSubmitting(true)}} - icon="cil-media-eject" - disabled={isSubmitting == true} - /> - -
- - - {!collapseData && - - - {data[objKey]?.type ? -
- {t("ServicesContainer|Type")}: {data[objKey]?.type?.toString()} -
- : - null - } - {data[objKey]?.returncode?.toString() ? -
- {t("ServicesContainer|Return code")}: {data[objKey]?.returncode?.toString()} -
- : - null - } - {data[objKey]?.error ? -
- {t("ServicesContainer|Error")}: {data[objKey]?.error?.toString()} -
- : - null - } - {data[objKey]?.exception ? -
- - {t("ServicesContainer|Exception")}: - - -
- : - null - } - {data[objKey]?.console ? -
- - {t("ServicesContainer|Console")}: - - -
- : - null - } - {data[objKey]?.detail ? - - : - null - } - {data[objKey]?.advertised_data ? - - : - null - } - - - } - - ) -} - -// Method to display collapsed table -const CollapsedTable = ({obj, title}) => { - const theme = useSelector(state => state.theme); - - return( - - - - - - - - {Object.keys(obj).length != 0 && Object.entries(obj).map((itms, idx) => { - return( - - - - - ) - })} - -
{title}
- - {itms[0] && itms[0].toString()} - - - {itms[1] ? - (typeof itms[1] == "object") && (Object.keys(itms[1]).length > 0) ? - - : - {itms[1] && itms[1].toString()} - : "-"} -
- ) -} - - -// Search method -const Search = ({ search, filterValue, setFilterValue }) => { - - return ( -
- - {search.icon && - - - - } - setFilterValue(e.target.value)} - placeholder={search.placeholder} - type="text" - bsSize="sm" - /> - -
- ); -} - -// Check if string is valid JSON -function IsJsonString(str) { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; -} diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js b/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js deleted file mode 100644 index cb41d5665..000000000 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/components/ActionButton.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useState } from 'react'; -import { Button, Tooltip } from 'reactstrap'; -import { useSelector } from 'react-redux'; -import { ButtonWithAuthz } from 'asab-webui'; - -const ActionButton = ({ - label, - onClick, - icon, - id, - disabled=false, - className="", - color="", - outline=false -}) => { - const [tooltipOpen, setTooltipOpen] = useState(false); - const resource = "asab:service:manage"; - const resources = useSelector(state => state.auth?.resources); - - const toggle = () => setTooltipOpen(!tooltipOpen); - - const title = () => `${label.split(' ')[0]}`; - - return ( - - - - - - {title()} - - - ) -} - -export default ActionButton; diff --git a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss b/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss deleted file mode 100644 index d7e447a9c..000000000 --- a/src/modules/maintenance/ServicesModule/ServicesContainers/services.scss +++ /dev/null @@ -1,100 +0,0 @@ -@import "~asab-webui/styles/constants/index.scss"; - -/* container styles */ - -.svcs-container { - height: 100%; - -} - -/* card body styles */ - -.services-body { - overflow: auto; - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -/* status indicator styles */ - -.service-status-circle { - margin-top: 0.25rem; - width: 0.8rem; - height: 0.8rem; - background: $secondary; - border-radius: 50% -} - -.service-status-running { - background: $light-green; -} - -.service-status-starting { - background: $warning; -} - -.service-status-stopped { - background: $danger; -} - -/* table styles */ - -.td-style { - text-align: center; - background: $bg-color; -} - -/* collapsed table styles */ - -.collapsed-data { - background-color: $bg-color; -} - -.collapsed-data:hover { - background-color: $bg-color; -} - -.caret-status-div { - display: inline-flex !important; - .caret-icon { - padding-right: 0.75rem; - padding-top: 0.15rem; - color: $primary; - cursor: pointer; - } -} - -.collapsed-table-row { - padding-top: 0px !important; - padding-bottom: 0px !important; - td { - padding-top: 0.25em !important; - padding-bottom: 0.25em !important; - height: fit-content !important; - } - th { - padding-top: 0.25em !important; - padding-bottom: 0.25em !important; - padding-left: 0em !important; - height: fit-content !important; - } -} - -.collapsed-heading { - color: var(--text-secondary-color) !important; - text-align: inherit !important; - font-weight: bold !important; -} - -.collapsed-code-value { - font-size: 100%; - color: var(--text-color); -} - -.collapsed-console { - display: inline-flex; -} - -.collapsed-span { - padding-right: 0.5em; -} diff --git a/src/modules/maintenance/ServicesModule/index.js b/src/modules/maintenance/ServicesModule/index.js deleted file mode 100644 index 79c5ffd29..000000000 --- a/src/modules/maintenance/ServicesModule/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import { lazy } from 'react'; -import Module from 'asab-webui/abc/Module'; -import { componentLoader } from 'asab-webui'; -const ServicesContainer = lazy(() => componentLoader(() => import("./ServicesContainers/ServicesContainer"))); - -import "./ServicesContainers/services.scss"; - -export default class ServicesModule extends Module { - constructor(app, name) { - super(app, "ASABServicesModule"); - - app.Router.addRoute({ - path: "/services", - exact: true, - name: "Services", - component: ServicesContainer, - resource: "asab:service:access" - }); - - // Check presence of Maintenance item in sidebar - let items = app.Navigation.getItems()?.items; - let isMaintenancePresent = false; - items.forEach(itm => { - // If Maintenance present, then append Microservices as a Maintenance subitem - if (itm?.name == "Maintenance") { - itm.children.push({ - name: "Services", - url: "/services", - icon: "cil-list", - resource: "asab:service:access" - }); - isMaintenancePresent = true; - } - }) - - // If Maintenance not present in sidebar navigation, add a Maintenance item - if (!isMaintenancePresent) { - app.Navigation.addItem({ - name: 'Maintenance', - icon: "cil-apps-settings", - children: [ - { - name: "Services", - url: "/services", - icon: "cil-list", - resource: "asab:service:access" - } - ] - }); - } - } -}