From 2e0629ad0d01b90cb724e47bf201486b526437ce Mon Sep 17 00:00:00 2001 From: Maxi Date: Mon, 18 Nov 2024 15:28:03 +0100 Subject: [PATCH 01/11] fix --- src/management-system-v2/components/process-list.tsx | 2 +- src/management-system-v2/lib/useColumnWidth.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/components/process-list.tsx b/src/management-system-v2/components/process-list.tsx index 548c6673f..0b14a605f 100644 --- a/src/management-system-v2/components/process-list.tsx +++ b/src/management-system-v2/components/process-list.tsx @@ -410,7 +410,7 @@ const ProcessManagementList: FC = ({ processActions={processActions} tableProps={{ scroll: { - y: `${window?.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, + y: `${window!.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, }, pagination: { position: ['bottomCenter'], pageSize: 20 }, onRow: (item) => ({ diff --git a/src/management-system-v2/lib/useColumnWidth.tsx b/src/management-system-v2/lib/useColumnWidth.tsx index ba044eb2d..068ce2819 100644 --- a/src/management-system-v2/lib/useColumnWidth.tsx +++ b/src/management-system-v2/lib/useColumnWidth.tsx @@ -30,7 +30,7 @@ export const useResizeableColumnWidth = ( const initialisedWithHydratedValues = useRef( false, - ); /* Basically a switch to check whther the state was updated once with the saved values, once hydrated */ + ); /* Basically a switch to check whether the state was updated once with the saved values, once hydrated */ const convertedWidthsToNumbers = useRef(false); /* Similar switch */ const computeNewColumns = useCallback(() => { @@ -58,13 +58,13 @@ export const useResizeableColumnWidth = ( initialisedWithHydratedValues.current = true; // console.debug('Updated columns with hydrated values'); setResizeableColumns(newColumns); - }, [hydrated, computeNewColumns, initialisedWithHydratedValues.current]); + }, [hydrated, computeNewColumns]); /* If the user selects different columns (i.e. columnsInPreferences change) update the state with new columns */ useEffect(() => { if (!hydrated) return; - /* This should onl run if the length of the arrays is dfferent */ + /* This should only run if the length of the arrays is dfferent */ if (columnsInPreferences.length === resizeableColumns.length) return; const newColumns = computeNewColumns(); From 8d22a7e8a8f9f0761877bd3736937d731f8f2ff0 Mon Sep 17 00:00:00 2001 From: Maxi Date: Mon, 18 Nov 2024 18:56:07 +0100 Subject: [PATCH 02/11] revert nullcheck --- src/management-system-v2/components/process-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/components/process-list.tsx b/src/management-system-v2/components/process-list.tsx index 0b14a605f..548c6673f 100644 --- a/src/management-system-v2/components/process-list.tsx +++ b/src/management-system-v2/components/process-list.tsx @@ -410,7 +410,7 @@ const ProcessManagementList: FC = ({ processActions={processActions} tableProps={{ scroll: { - y: `${window!.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, + y: `${window?.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, }, pagination: { position: ['bottomCenter'], pageSize: 20 }, onRow: (item) => ({ From 95f1d1298f7a6c87710fe07c99b4a756e94aa2e4 Mon Sep 17 00:00:00 2001 From: Maxi Date: Mon, 18 Nov 2024 19:24:43 +0100 Subject: [PATCH 03/11] Scroll on client --- .../components/process-list.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/components/process-list.tsx b/src/management-system-v2/components/process-list.tsx index 548c6673f..4862146e3 100644 --- a/src/management-system-v2/components/process-list.tsx +++ b/src/management-system-v2/components/process-list.tsx @@ -9,6 +9,8 @@ import { SetStateAction, Key, ReactElement, + useEffect, + useState, } from 'react'; import { CopyOutlined, @@ -35,6 +37,7 @@ import FavouriteStar from './favouriteStar'; import { contextMenuStore } from './processes/context-menu'; import { DraggableElementGenerator } from './processes/draggable-element'; import classNames from 'classnames'; +import { set } from 'zod'; /** respects sorting function, but always keeps folders at the beginning */ function folderAwareSort(sortFunction: (a: ProcessListProcess, b: ProcessListProcess) => number) { @@ -400,6 +403,14 @@ const ProcessManagementList: FC = ({ const setContextMenuItem = contextMenuStore((store) => store.setSelected); const metaPanelisOpened = useUserPreferences.use['process-meta-data']().open; + const [scrollY, setScrollY] = useState('400px'); + useEffect(() => { + if (window) + setScrollY( + `${window.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, + ); + }, []); + return ( = ({ processActions={processActions} tableProps={{ scroll: { - y: `${window?.innerHeight - 32 /* Footer */ - 64 /* Header */ - 82 /* Table-Search etc */ - 60 /* Table-head */ - 60 /* Table-Footer / Pagination */}px`, + y: scrollY, }, pagination: { position: ['bottomCenter'], pageSize: 20 }, onRow: (item) => ({ From 34ee1e8cb1b3ecd3f27003d10ae89b62591dec2a Mon Sep 17 00:00:00 2001 From: Kai Rohwer Date: Mon, 25 Nov 2024 13:17:22 +0100 Subject: [PATCH 04/11] Refactor ProcessExportModal to use string type for selectedOptions and handleOptionSelectionChange --- src/management-system-v2/components/process-export.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/management-system-v2/components/process-export.tsx b/src/management-system-v2/components/process-export.tsx index dd74ad178..86fa5854f 100644 --- a/src/management-system-v2/components/process-export.tsx +++ b/src/management-system-v2/components/process-export.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import { Modal, Checkbox, Radio, RadioChangeEvent, Space, Flex, Divider, Tooltip } from 'antd'; -import type { CheckboxValueType } from 'antd/es/checkbox/Group'; import { useEnvironment } from './auth-can'; import { exportProcesses } from '@/lib/process-export'; @@ -107,9 +106,7 @@ const ProcessExportModal: React.FC = ({ setSelectedType(preselectedExportType); }, [preselectedExportType]); - const [selectedOptions, setSelectedOptions] = useState( - ['metaData'].concat(pdfOptions), - ); + const [selectedOptions, setSelectedOptions] = useState(['metaData'].concat(pdfOptions)); const [isExporting, setIsExporting] = useState(false); const [pngScalingFactor, setPngScalingFactor] = useState(1.5); @@ -120,7 +117,7 @@ const ProcessExportModal: React.FC = ({ setSelectedType(value); }; - const handleOptionSelectionChange = (checkedValues: CheckboxValueType[]) => { + const handleOptionSelectionChange = (checkedValues: string[]) => { setSelectedOptions(checkedValues); }; From cb7885c1e7f3bfc7be7eb27b365c8324aaa329d4 Mon Sep 17 00:00:00 2001 From: "Felipe Trost H." Date: Thu, 28 Nov 2024 13:17:32 +0100 Subject: [PATCH 05/11] Initial mqtt calls for endpoints (#403) * feat(ms2/engines): initial mqtt endpoints * feat(ms2/engines): endpoints for engines * feat(ms2/engines): mqtt requests * feat(ms2/admin): engine view * refactor(ms2/mqtt) * fix(ms2/engine-endpoints): import * fix(ms2/admin-engines): make page dynamic * feat(ms2/admin): added link to engines in sidebar * chore: format * fix(ms2/env-vars): mqtt env vars shouldn't be required yet * fix: possible undefined * fix: engine name key * fix(ms2/build): unset env-var * fix(ms2/mqtt): correct engine id key * admin-dashboard/engines: better error message * fix: import --- .../executions/[processId]/element-status.tsx | 11 +- .../app/admin/engines/engines-table.tsx | 57 +++++++ .../app/admin/engines/page.tsx | 44 +++++ src/management-system-v2/app/admin/layout.tsx | 7 + .../lib/engines/deployment.ts | 2 +- .../lib/engines/endpoint.ts | 30 ++++ .../lib/engines/endpoints.json | 67 ++++++++ .../{endpoints.ts => http-endpoints.ts} | 0 .../lib/engines/mqtt-endpoints.ts | 126 ++++++++++++++ src/management-system-v2/lib/env-vars.ts | 5 + src/management-system-v2/package.json | 3 +- yarn.lock | 156 +++++++++++++++++- 12 files changed, 497 insertions(+), 11 deletions(-) create mode 100644 src/management-system-v2/app/admin/engines/engines-table.tsx create mode 100644 src/management-system-v2/app/admin/engines/page.tsx create mode 100644 src/management-system-v2/lib/engines/endpoint.ts create mode 100644 src/management-system-v2/lib/engines/endpoints.json rename src/management-system-v2/lib/engines/{endpoints.ts => http-endpoints.ts} (100%) create mode 100644 src/management-system-v2/lib/engines/mqtt-endpoints.ts diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/executions/[processId]/element-status.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/executions/[processId]/element-status.tsx index 65ad7febb..045d50a10 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/executions/[processId]/element-status.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/executions/[processId]/element-status.tsx @@ -4,7 +4,7 @@ import { ClockCircleFilled } from '@ant-design/icons'; import React from 'react'; import { statusToType } from './instance-helpers'; import { convertISODurationToMiliseconds, getMetaDataFromElement } from '@proceed/bpmn-helper'; -import { generateRequestUrl } from '@/lib/engines/endpoints'; +import { endpointBuilder } from '@/lib/engines/endpoint'; import { DisplayTable, RelevantInstanceInfo } from './instance-info-panel'; function transformMilisecondsToTimeFormat(milliseconds: number | undefined) { @@ -47,10 +47,11 @@ export function ElementStatus({ info }: { info: RelevantInstanceInfo }) { }} > , ]); diff --git a/src/management-system-v2/app/admin/engines/engines-table.tsx b/src/management-system-v2/app/admin/engines/engines-table.tsx new file mode 100644 index 000000000..54f159246 --- /dev/null +++ b/src/management-system-v2/app/admin/engines/engines-table.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { Tag } from 'antd'; +import { useState } from 'react'; +import { type TableEngine } from './page'; +import ElementList from '@/components/item-list-view'; +import Bar from '@/components/bar'; +import useFuzySearch from '@/lib/useFuzySearch'; + +export default function EnginesTable({ engines }: { engines: TableEngine[] }) { + const { filteredData, searchQuery, setSearchQuery } = useFuzySearch({ + data: engines, + keys: ['name'], + highlightedKeys: ['name'], + transformData: (matches) => matches.map((match) => match.item), + }); + + const [selectedEngines, setSelectedEngines] = useState([]); + + return ( + <> + setSearchQuery(e.target.value), + onPressEnter: (e) => setSearchQuery(e.currentTarget.value), + placeholder: 'Search spaces ...', + }} + /> + + engine.name.highlighted, + }, + { + title: 'Status', + dataIndex: 'owner', + sorter: (a, b) => +a.running - +b.running, + render: (_, engine) => ( + + {engine.running ? 'Online' : 'Offline'} + + ), + }, + ]} + /> + + ); +} diff --git a/src/management-system-v2/app/admin/engines/page.tsx b/src/management-system-v2/app/admin/engines/page.tsx new file mode 100644 index 000000000..5860a86f1 --- /dev/null +++ b/src/management-system-v2/app/admin/engines/page.tsx @@ -0,0 +1,44 @@ +import { getCurrentUser } from '@/components/auth'; +import Content from '@/components/content'; +import { getEngines } from '@/lib/engines/mqtt-endpoints'; +import { Result, Skeleton } from 'antd'; +import { notFound, redirect } from 'next/navigation'; +import { Suspense } from 'react'; +import { getSystemAdminByUserId } from '@/lib/data/DTOs'; +import EnginesTable from './engines-table'; +import { env } from '@/lib/env-vars'; + +export type TableEngine = Awaited>[number] & { name: string }; + +async function Engines() { + const user = await getCurrentUser(); + if (!user.session) redirect('/'); + const adminData = getSystemAdminByUserId(user.userId); + if (!adminData) redirect('/'); + + try { + const engines = (await getEngines()).map((e) => ({ ...e, name: e.id })); + + return ; + } catch (e) { + console.error(e); + return ; + } +} + +export default function EnginesPage() { + if (!env.NEXT_PUBLIC_ENABLE_EXECUTION) return notFound(); + + if (!env.MQTT_SERVER_ADDRESS) + return ; + + return ( + + }> + + + + ); +} + +export const dynamic = 'force-dynamic'; diff --git a/src/management-system-v2/app/admin/layout.tsx b/src/management-system-v2/app/admin/layout.tsx index d0f034ca4..b8789c0fe 100644 --- a/src/management-system-v2/app/admin/layout.tsx +++ b/src/management-system-v2/app/admin/layout.tsx @@ -1,6 +1,7 @@ import Link from 'next/link'; import Layout from '@/app/(dashboard)/[environmentId]/layout-client'; import { AreaChartOutlined, AppstoreOutlined, FileOutlined } from '@ant-design/icons'; +import { MdOutlineComputer } from 'react-icons/md'; import { FaUsers } from 'react-icons/fa'; import { RiAdminFill } from 'react-icons/ri'; @@ -45,6 +46,12 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) label: Manage admins, icon: , }, + + { + key: 'engines', + label: Engines, + icon: , + }, ], }, ]} diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index c1bd0b569..d5b9039a5 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -9,7 +9,7 @@ import { // @ts-ignore // import decider from '@proceed/decider'; import { Machine, getMachines } from './machines'; -import * as endpoints from './endpoints'; +import * as endpoints from './http-endpoints'; import { prepareExport } from '../process-export/export-preparation'; import { Prettify } from '../typescript-utils'; diff --git a/src/management-system-v2/lib/engines/endpoint.ts b/src/management-system-v2/lib/engines/endpoint.ts new file mode 100644 index 000000000..140529bca --- /dev/null +++ b/src/management-system-v2/lib/engines/endpoint.ts @@ -0,0 +1,30 @@ +type EndpointSchema = typeof import('./endpoints.json'); +type Endpoints = EndpointSchema; +type Methods = 'get' | 'post' | 'put' | 'delete'; + +type GetParamsFromString< + Str extends string, + Count extends unknown[] = [], +> = Str extends `${infer Start}:${string}/${infer Rest}` + ? Str extends `${Start}:${infer Param}/${Rest}` + ? GetParamsFromString + : Count + : Str extends `${string}:${infer End}` + ? [...Count, End] + : Count; + +type EndpointArgsArray = ParamsArray extends [] + ? [] + : [Record]; +type EndpointArgs = EndpointArgsArray>; + +type AvailableEndpoints = keyof Endpoints[Method] extends string + ? keyof Endpoints[Method] + : never; +export function endpointBuilder>( + _: Method, + endpoint: Url, + ...options: EndpointArgs +) { + return endpoint.replace(/:([^/]+)/g, (_, capture_group) => options[0]?.[capture_group] || ''); +} diff --git a/src/management-system-v2/lib/engines/endpoints.json b/src/management-system-v2/lib/engines/endpoints.json new file mode 100644 index 000000000..bfaa4aa11 --- /dev/null +++ b/src/management-system-v2/lib/engines/endpoints.json @@ -0,0 +1,67 @@ +{ + "get": { + "/machine/:properties": { + "params": true + }, + "/machine/": {}, + "/capabilities/": {}, + "/configuration/": {}, + "/configuration/:key": {}, + "/logging": {}, + "/logging/status": {}, + "/logging/standard": {}, + "/logging/process": {}, + "/logging/process/:definitionId": {}, + "/logging/process/:definitionId/instance/:instanceId": {}, + "/monitoring/": {}, + "/tasklist/api/": {}, + "/tasklist/api/userTask": {}, + "/configuration/api/config": {}, + "/logging/api/log": {}, + "/": {}, + "/process/": {}, + "/process/:definitionId": {}, + "/process/:definitionId/versions": {}, + "/process/:definitionId/versions/:version": {}, + "/process/:definitionId/instance": {}, + "/process/:definitionId/instance/:instanceID": {}, + "/process/:definitionId/user-tasks/:fileName": {}, + "/process/:definitionId/user-tasks": {}, + "/status/": {}, + "/resources/process/:definitionId/images/:fileName": {}, + "/resources/process/:definitionId/images/": {} + }, + "post": { + "/capabilities/execute": {}, + "/capabilities/return": {}, + "/evaluation/": {}, + "/tasklist/api/userTask": {}, + "/configuration/api/config": {}, + "/process/": {}, + "/process/:definitionId/versions/:version/instance": {}, + "/process/:definitionId/instance/:instanceId/tokens": {}, + "/process/:definitionId/instance/:instanceId/variables": {}, + "/process/:definitionId/versions/:version/instance/migration": {} + }, + "put": { + "/configuration/": {}, + "/tasklist/api/variable": {}, + "/tasklist/api/milestone": {}, + "/process/:definitionId/instance/:instanceID": {}, + "/process/:definitionId/instance/:instanceID/instanceState": {}, + "/process/:definitionId/instance/:instanceId/tokens/:tokenId": {}, + "/process/:definitionId/instance/:instanceId/tokens/:tokenId/currentFlowNodeState": {}, + "/process/:definitionId/user-tasks/:fileName": {}, + "/resources/process/:definitionId/images/:fileName": {} + }, + "delete": { + "/configuration/": {}, + "/logging": {}, + "/logging/standard": {}, + "/logging/process": {}, + "/logging/process/:definitionId": {}, + "/logging/process/:definitionId/instance/:instanceId": {}, + "/process/:definitionId": {}, + "/process/:definitionId/instance/:instanceId/tokens/:tokenId": {} + } +} diff --git a/src/management-system-v2/lib/engines/endpoints.ts b/src/management-system-v2/lib/engines/http-endpoints.ts similarity index 100% rename from src/management-system-v2/lib/engines/endpoints.ts rename to src/management-system-v2/lib/engines/http-endpoints.ts diff --git a/src/management-system-v2/lib/engines/mqtt-endpoints.ts b/src/management-system-v2/lib/engines/mqtt-endpoints.ts new file mode 100644 index 000000000..06d45f6b0 --- /dev/null +++ b/src/management-system-v2/lib/engines/mqtt-endpoints.ts @@ -0,0 +1,126 @@ +import mqtt from 'mqtt'; +import { env } from '@/lib/env-vars'; + +const mqttTimeout = 1000; + +const mqttCredentials = { + password: env.MQTT_PASSWORD, + username: env.MQTT_USERNAME, +}; + +const baseTopicPrefix = env.MQTT_BASETOPIC ? env.MQTT_BASETOPIC + '/' : ''; + +export function getClient(options?: mqtt.IClientOptions): Promise { + const address = env.MQTT_SERVER_ADDRESS || ''; + + return new Promise((res, rej) => { + const client = mqtt.connect(address, { + ...mqttCredentials, + ...options, + }); + client.on('connect', () => res(client)); + client.on('error', (err) => rej(err)); + }); +} + +function subscribeToTopic(client: mqtt.MqttClient, topic: string) { + return new Promise((res, rej) => { + setTimeout(rej, mqttTimeout); // Timeout if the subscription takes too long + client.subscribe(topic, (err) => { + if (err) rej(err); + res(); + }); + }); +} + +function getEnginePrefix(engineId: string) { + return `${baseTopicPrefix}proceed-pms/engine/${engineId}`; +} + +export async function getEngines() { + const client = await getClient({ + connectTimeout: mqttTimeout, + }); + + const engines: { id: string; running: boolean; version: string }[] = []; + + await subscribeToTopic(client, `${getEnginePrefix('+')}/status`); + + // All retained messages are sent at once + // The broker should bundle them in one tcp packet, + // after it is parsed all messages are in the queue, and handled before close + // is handled, as the packets where pushed to the queue before the close event was emitted. + // This is of course subject to the implementation of the broker, + // however for a small amount of engines it should be fine. + await new Promise((res) => { + setTimeout(res, mqttTimeout); // Timeout in case we receive no messages + + client.on('message', (topic, message) => { + const match = topic.match(new RegExp(`^${getEnginePrefix('')}([^\/]+)\/status`)); + if (match) { + const id = match[1]; + const status = JSON.parse(message.toString()); + engines.push({ id, ...status }); + res(); + } + }); + }); + + await client.endAsync(); + + return engines; +} + +const requestClient = getClient(); + +export async function mqttRequest( + engineId: string, + url: string, + message: { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body: Record; + query: Record; + page?: number; + }, +) { + const client = await requestClient; + + const requestId = crypto.randomUUID(); + const requestTopic = getEnginePrefix(engineId) + '/api' + url; + await subscribeToTopic(client, requestTopic); + + // handler for the response + let res: (res: any) => void, rej: (Err: any) => void; + const receivedAnswer = new Promise((_res, _rej) => { + res = _res; + rej = _rej; + }); + function handler(topic: string, _message: any) { + const message = JSON.parse(_message.toString()); + if (topic !== requestTopic) return; + if ( + !message || + typeof message !== 'object' || + !('type' in message) || + message.type !== 'response' || + !('id' in message) || + message.id !== requestId + ) + return; + + res(JSON.parse(message.body)); + } + client.on('message', handler); + + // send request + client.publish(requestTopic, JSON.stringify({ ...message, type: 'request', id: requestId })); + + // await for response or timeout + setTimeout(rej!, mqttTimeout); + const response = await receivedAnswer; + + // cleanup + client.removeListener('message', handler); + + return response; +} diff --git a/src/management-system-v2/lib/env-vars.ts b/src/management-system-v2/lib/env-vars.ts index 8face3c3b..70eb4f3ba 100644 --- a/src/management-system-v2/lib/env-vars.ts +++ b/src/management-system-v2/lib/env-vars.ts @@ -26,6 +26,11 @@ const environmentVariables = { } }) .optional(), + + MQTT_SERVER_ADDRESS: z.string().url().optional(), + MQTT_USERNAME: z.string().optional(), + MQTT_PASSWORD: z.string().optional(), + MQTT_BASETOPIC: z.string().optional(), }, production: { NEXTAUTH_SECRET: z.string(), diff --git a/src/management-system-v2/package.json b/src/management-system-v2/package.json index 1d66a484a..f3821877f 100644 --- a/src/management-system-v2/package.json +++ b/src/management-system-v2/package.json @@ -67,7 +67,8 @@ "yup": "^0.32.9", "zod": "3.22.4", "zustand": "4.5.2", - "react-resizable": "^3.0.5" + "react-resizable": "^3.0.5", + "mqtt": "^5.10.1" }, "devDependencies": { "@tanstack/eslint-plugin-query": "5.28.11", diff --git a/yarn.lock b/yarn.lock index 6eb048b8c..76183a7da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1303,6 +1303,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.23.8", "@babel/runtime@^7.24.5": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz" @@ -3285,6 +3292,14 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/readable-stream@^4.0.0", "@types/readable-stream@^4.0.5": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.18.tgz#5d8d15d26c776500ce573cae580787d149823bfc" + integrity sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + "@types/responselike@^1.0.0": version "1.0.3" resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz" @@ -3392,6 +3407,13 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/ws@^8.5.9": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" @@ -5705,6 +5727,16 @@ bl@^4.0.2, bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bl@^6.0.8: + version "6.0.16" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.0.16.tgz#29b190f1a754e2d168de3dc8c74ed8d12bf78e6e" + integrity sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + blob@0.0.5: version "0.0.5" resolved "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz" @@ -6097,6 +6129,14 @@ buffer@^5.1.0, buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builder-util-runtime@8.9.2: version "8.9.2" resolved "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz" @@ -7061,6 +7101,11 @@ commist@^1.0.0: leven "^2.1.0" minimist "^1.1.0" +commist@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/commist/-/commist-3.2.0.tgz#da9c8e5f245ac21510badc4b10c46b5bcc9b56cd" + integrity sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" @@ -9917,7 +9962,7 @@ eventemitter3@^5.0.1: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@^3.0.0, events@^3.2.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -10279,6 +10324,14 @@ fast-text-encoding@^1.0.0: resolved "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== +fast-unique-numbers@^8.0.13: + version "8.0.13" + resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz#3c87232061ff5f408a216e1f0121232f76f695d7" + integrity sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g== + dependencies: + "@babel/runtime" "^7.23.8" + tslib "^2.6.2" + fast-xml-parser@3.15.0: version "3.15.0" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.15.0.tgz" @@ -11609,6 +11662,11 @@ help-me@^3.0.0: glob "^7.1.6" readable-stream "^3.6.0" +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz" @@ -14833,6 +14891,11 @@ lowercase-keys@^2.0.0: resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.0.1: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^10.1.0, lru-cache@^10.2.0: version "10.2.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz" @@ -15300,7 +15363,7 @@ minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -15509,6 +15572,15 @@ mqtt-packet@^6.8.0: debug "^4.1.1" process-nextick-args "^2.0.1" +mqtt-packet@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-9.0.0.tgz#fd841854d8c0f1f5211b00de388c4ced45b59216" + integrity sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w== + dependencies: + bl "^6.0.8" + debug "^4.3.4" + process-nextick-args "^2.0.1" + mqtt@^4.3.7: version "4.3.8" resolved "https://registry.npmjs.org/mqtt/-/mqtt-4.3.8.tgz" @@ -15532,6 +15604,28 @@ mqtt@^4.3.7: ws "^7.5.5" xtend "^4.0.2" +mqtt@^5.10.1: + version "5.10.1" + resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-5.10.1.tgz#d4f45ffdd825bad331c18f08796a744dabbe16de" + integrity sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw== + dependencies: + "@types/readable-stream" "^4.0.5" + "@types/ws" "^8.5.9" + commist "^3.2.0" + concat-stream "^2.0.0" + debug "^4.3.4" + help-me "^5.0.0" + lru-cache "^10.0.1" + minimist "^1.2.8" + mqtt-packet "^9.0.0" + number-allocator "^1.0.14" + readable-stream "^4.4.2" + reinterval "^1.1.0" + rfdc "^1.3.0" + split2 "^4.2.0" + worker-timers "^7.1.4" + ws "^8.17.1" + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -16010,7 +16104,7 @@ num2fraction@^1.2.2: resolved "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz" integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== -number-allocator@^1.0.9: +number-allocator@^1.0.14, number-allocator@^1.0.9: version "1.0.14" resolved "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz" integrity sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA== @@ -18421,6 +18515,17 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.2.0, readable-stream@^4.4.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readable-web-to-node-stream@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz" @@ -19841,6 +19946,11 @@ split2@^3.0.0, split2@^3.1.0: dependencies: readable-stream "^3.0.0" +split2@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@^1.1.2: version "1.1.3" resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz" @@ -20166,7 +20276,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -21021,6 +21131,11 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz" @@ -22288,6 +22403,34 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-timers-broker@^6.1.8: + version "6.1.8" + resolved "https://registry.yarnpkg.com/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz#08f64e5931b77fadc55f0c7388c077a7dd17e4c7" + integrity sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ== + dependencies: + "@babel/runtime" "^7.24.5" + fast-unique-numbers "^8.0.13" + tslib "^2.6.2" + worker-timers-worker "^7.0.71" + +worker-timers-worker@^7.0.71: + version "7.0.71" + resolved "https://registry.yarnpkg.com/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz#f96138bafbcfaabea116603ce23956e05e76db6a" + integrity sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + +worker-timers@^7.1.4: + version "7.1.8" + resolved "https://registry.yarnpkg.com/worker-timers/-/worker-timers-7.1.8.tgz#f53072c396ac4264fd3027914f4ab793c92d90be" + integrity sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + worker-timers-broker "^6.1.8" + worker-timers-worker "^7.0.71" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -22411,6 +22554,11 @@ ws@^7.5.5: resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.17.1: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + ws@~7.4.2: version "7.4.6" resolved "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz" From bf6fcc756822f24c27289087dc52a42b8887a883 Mon Sep 17 00:00:00 2001 From: "Felipe Trost H." Date: Thu, 28 Nov 2024 20:47:08 +0100 Subject: [PATCH 06/11] engine: monitoring and logging (#409) * feat(engine/network): loopback interface * feat(engine): publish monitoring data every engine.loadInterval * feat(engine/logger): register callbacks * fix(engine): setup continous mqtt messaging * feat(engine): publish logs with mqtt * refactor * feat(engine): mqtt log level setting * refactor * fix: wrong variable * refactor(engine): no undefined in mqtt messages * refactor(engine): use machine information module * Revert "feat(engine/network): loopback interface" This reverts commit 38fba12646b30f1ceae0968651bf6dd7edf6889c. * better warning * rename variable --- .../native-config/src/config_default.json | 3 +- .../universal/core/src/messaging-setup.js | 64 +++++++++++++++++++ src/engine/universal/core/src/module.js | 3 +- .../universal/machine/logging/logging.js | 23 ++++++- .../logging/src/loggerHelpers/logger.js | 11 ++-- 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/engine/native/node/native-config/src/config_default.json b/src/engine/native/node/native-config/src/config_default.json index 23e33fc40..7bad87cd3 100644 --- a/src/engine/native/node/native-config/src/config_default.json +++ b/src/engine/native/node/native-config/src/config_default.json @@ -9,7 +9,8 @@ "maxProcessLogEntries": 500, "maxProcessLogTables": 5, "rotationInterval": 600, - "maxStandardLogEntries": 1000 + "maxStandardLogEntries": 1000, + "mqttLevel": "info" }, "processes": { "acceptUserTasks": false, diff --git a/src/engine/universal/core/src/messaging-setup.js b/src/engine/universal/core/src/messaging-setup.js index b8f201d49..d98632a21 100644 --- a/src/engine/universal/core/src/messaging-setup.js +++ b/src/engine/universal/core/src/messaging-setup.js @@ -1,4 +1,5 @@ const { version: proceedVersion } = require('../../../native/node/package.json'); +const { logging } = require('@proceed/machine'); /** * This file contains functionality that handles setup and interactions of the messaging interface with other modules of the engine @@ -58,4 +59,67 @@ module.exports = { } } }, + async setupMonitoringAndLogging(messaging, configModule, machineModule, logger) { + let { serverAddress, baseTopic } = await configModule.readConfig('messaging'); + if (!serverAddress) return; + + if (baseTopic && !baseTopic.endsWith('/')) baseTopic += '/'; + baseTopic += 'proceed-pms'; + + // Monitoring data + const [{ id: machineId }, loadInterval] = await Promise.all([ + machineModule.getMachineInformation(['id']), + configModule.readConfig('engine.loadInterval'), + ]); + + setInterval(async () => { + try { + const machineData = await machineModule.getMachineInformation(); + await messaging.publish(`${baseTopic}/engine/${machineId}/machine/monitoring`, machineData); + } catch (e) { + logger.error('Failed to publish monitoring data'); + } + }, loadInterval * 1000); + + // Logging data + const mqttLevel = await configModule.readConfig('logs.mqttLevel'); + const orderedLevels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; + const mqttLevelIdx = orderedLevels.indexOf(mqttLevel); + const logGuard = (level) => orderedLevels.indexOf(level) >= mqttLevelIdx; + + logging.registerCallback(async (obj, log) => { + try { + if (!logGuard(log?.level)) return; + + const sentMessages = []; + const print = `[${log.level.toUpperCase()}] ${log.moduleName}${obj.definitionId || ''} ${log.msg}`; + + sentMessages.push(messaging.publish(`${baseTopic}/engine/${machineId}/logging`, print)); + + if (obj.definitionId) { + sentMessages.push( + messaging.publish(`${baseTopic}/engine/${machineId}/logging/process`, print), + ); + + if (log.instanceId) { + sentMessages.push( + messaging.publish( + `${baseTopic}/engine/process/${obj.definitionId}/instance/${log.instanceId}/logging`, + print, + ), + ); + } + } else { + sentMessages.push( + messaging.publish(`${baseTopic}/engine/${machineId}/logging/standard`, print), + ); + } + + await Promise.all(sentMessages); + } catch (e) { + // NOTE: using logger.error could cause an infinite loop if publish keeps failing + console.error(e); + } + }); + }, }; diff --git a/src/engine/universal/core/src/module.js b/src/engine/universal/core/src/module.js index 3e44b6f8d..4f7f27482 100644 --- a/src/engine/universal/core/src/module.js +++ b/src/engine/universal/core/src/module.js @@ -10,7 +10,7 @@ const monitoring = require('@proceed/monitoring'); const management = require('./management.js'); const { setup5thIndustryEndpoints } = require('./engine/5thIndustry.js'); const { enableInterruptedInstanceRecovery } = require('../../../../../FeatureFlags.js'); -const { setupMessaging } = require('./messaging-setup.js'); +const { setupMessaging, setupMonitoringAndLogging } = require('./messaging-setup.js'); const { enableMessaging, enable5thIndustryIntegration } = require('../../../../../FeatureFlags.js'); const configObject = { @@ -53,6 +53,7 @@ module.exports = { if (enableMessaging) { await setupMessaging(system.messaging, config, machineInformation, logger); + await setupMonitoringAndLogging(system.messaging, config, machineInformation, logger); } if (!options.silentMode) { diff --git a/src/engine/universal/machine/logging/logging.js b/src/engine/universal/machine/logging/logging.js index d5ae98eb9..a38e4f0ff 100644 --- a/src/engine/universal/machine/logging/logging.js +++ b/src/engine/universal/machine/logging/logging.js @@ -6,6 +6,8 @@ const rotationUtils = require('./src/utils/logRotationUtils'); const startRotation = require('./src/rotation/rotation'); const routes = require('./src/routes/logRoutes'); +/** @typedef {{moduleName:string; definitionId: string; consoleOnly: boolean;}} LoggerConfObject */ + let singletonInstance; /** @@ -22,6 +24,7 @@ class Logging { this.doneInitializing = undefined; // See @proceed/system.console console.constructor._setLoggingModule(this); + this.logCallbacks = new Set(); } /** @@ -65,13 +68,29 @@ class Logging { routes(this); } + /** @param {(obj: LoggerConfObject, log:any)=>void} callback */ + registerCallback(callback) { + return this.logCallbacks.add(callback); + } + + /** @param {(obj: LoggerConfObject, log:string)=>void} callback */ + unregisterCallback(callback) { + return this.logCallbacks.delete(callback); + } + /** * Factory method creating a logger - * @param {object} confObject An object containing all configuration parameters for the logger + * @param {LoggerConfObject } confObject An object containing all configuration parameters for the logger * @returns a logger */ getLogger(confObject) { - const logger = loggerLoader(confObject, this.init.bind(this)); + const logger = loggerLoader(confObject, this.init.bind(this), [ + (msg) => { + for (const callback of this.logCallbacks) { + callback(confObject, msg); + } + }, + ]); return logger; } diff --git a/src/engine/universal/machine/logging/src/loggerHelpers/logger.js b/src/engine/universal/machine/logging/src/loggerHelpers/logger.js index cc2a5bb89..414416401 100644 --- a/src/engine/universal/machine/logging/src/loggerHelpers/logger.js +++ b/src/engine/universal/machine/logging/src/loggerHelpers/logger.js @@ -15,16 +15,17 @@ const writerLoader = require('./writers.js'); * * Class for logger instances * Instantiates a new logger - * @param {object} confObject The configuration for the logger + * @param {{moduleName:string; definitionId: string; consoleOnly: boolean;}} confObject The configuration for the logger * @param {promise} loggingInitializedPromise a promise indicating that the logger has + * @param {((...args: any[])=>void)[]} [customWriters] Custom writer functions to be used by the logger * finished being asynchronously initialized */ class Logger { - constructor(confObject, loggingInitializer) { + constructor(confObject, loggingInitializer, customWriters) { this.instanceInitialized = false; this.confObject = confObject; this.loggingInitializer = loggingInitializer; - this.functionsForWriter = []; + this.functionsForWriter = customWriters ? [...customWriters] : []; this.moduleName = confObject.moduleName; } @@ -184,7 +185,9 @@ Logger.initialization = new Promise((resolve) => { /** * @param {object} confObject The configuration for the logger * @param {promise} loggingInitializedPromise a promise indicating that the logger has + * @param {((...args: any[])=>void)[]} [customWriters] Custom writer functions to be used by the logger * finished being asynchronously initialized * @returns a configured instance of the Logger class */ -module.exports = (confObject, loggingInitializer) => new Logger(confObject, loggingInitializer); +module.exports = (confObject, loggingInitializer, customWriters) => + new Logger(confObject, loggingInitializer, customWriters); From d4bf95ac4fdc5045ae1fed14cab727f5b2988e1c Mon Sep 17 00:00:00 2001 From: "Felipe Trost H." Date: Thu, 28 Nov 2024 20:48:16 +0100 Subject: [PATCH 07/11] ms2: creation modals bug (#378) * refactor(ms2): extract creation modals out to sepparate components * fix(ms2/process-list): creation modals support arrow movements * fix(ms2/process-list): removed unnecessary button wrapper * fix(ms2/process-list): add user controls * removed unused imports * fix(ms2/folder-creation): createFolder function * refactor(ms2): removed folder creation button * refactor(ms2/folder-creation): don't parse input twice * fix(ms2/process-creation-button): open modal on meta+enter --------- Co-authored-by: Kai Rohwer --- .../components/folder-creation-button.tsx | 71 -------------- .../components/folder-creation.tsx | 48 ++++++++++ .../components/process-creation-button.tsx | 96 ++++++++++--------- .../components/processes/index.tsx | 60 ++++++------ 4 files changed, 129 insertions(+), 146 deletions(-) delete mode 100644 src/management-system-v2/components/folder-creation-button.tsx create mode 100644 src/management-system-v2/components/folder-creation.tsx diff --git a/src/management-system-v2/components/folder-creation-button.tsx b/src/management-system-v2/components/folder-creation-button.tsx deleted file mode 100644 index 014bf48e3..000000000 --- a/src/management-system-v2/components/folder-creation-button.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client'; - -import { FC, ReactNode, useState, useTransition } from 'react'; -import { App, Button } from 'antd'; -import type { ButtonProps } from 'antd'; -import { useParams, useRouter } from 'next/navigation'; -import { useEnvironment } from './auth-can'; -import { FolderUserInput, FolderUserInputSchema } from '@/lib/data/folder-schema'; -import { createFolder as serverCreateFolder } from '@/lib/data/folders'; -import FolderModal from './folder-modal'; -import useParseZodErrors from '@/lib/useParseZodErrors'; -import { wrapServerCall } from '@/lib/wrap-server-call'; - -type FolderCreationButtonProps = ButtonProps & { - wrapperElement?: ReactNode; -}; - -const FolderCreationButton: FC = ({ wrapperElement, ...props }) => { - const app = App.useApp(); - const router = useRouter(); - const spaceId = useEnvironment().spaceId; - const folderId = useParams<{ folderId: string }>().folderId ?? ''; - const [isLoading, startTransition] = useTransition(); - const [errors, parseInput] = useParseZodErrors(FolderUserInputSchema); - const [modalOpen, setModalOpen] = useState(false); - - const createFolder = (values: FolderUserInput) => { - startTransition(async () => { - await wrapServerCall({ - fn: () => { - const folderInput = parseInput({ ...values, parentId: folderId, environmentId: spaceId }); - if (!folderInput) throw new Error(); - - return serverCreateFolder(folderInput); - }, - onSuccess: () => { - router.refresh(); - app.message.open({ type: 'success', content: 'Folder Created' }); - setModalOpen(false); - }, - app, - }); - }); - }; - - return ( - <> - {wrapperElement ? ( -
setModalOpen(true)}>{wrapperElement}
- ) : ( - - )} - - setModalOpen(false)} - spaceId={spaceId} - parentId={folderId} - onSubmit={createFolder} - modalProps={{ - title: 'Create Folder', - okButtonProps: { loading: isLoading }, - }} - /> - - ); -}; - -export default FolderCreationButton; diff --git a/src/management-system-v2/components/folder-creation.tsx b/src/management-system-v2/components/folder-creation.tsx new file mode 100644 index 000000000..854a1e85b --- /dev/null +++ b/src/management-system-v2/components/folder-creation.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { ComponentProps, FC, useTransition } from 'react'; +import { App } from 'antd'; +import { useParams, useRouter } from 'next/navigation'; +import { useEnvironment } from './auth-can'; +import { FolderUserInput } from '@/lib/data/folder-schema'; +import { createFolder as serverCreateFolder } from '@/lib/data/folders'; +import FolderModal from './folder-modal'; +import { wrapServerCall } from '@/lib/wrap-server-call'; + +export const FolderCreationModal: FC< + Partial> & { open: boolean; close: () => void } +> = (props) => { + const { message } = App.useApp(); + const router = useRouter(); + const spaceId = useEnvironment().spaceId; + const folderId = useParams<{ folderId: string }>().folderId ?? ''; + const [isLoading, startTransition] = useTransition(); + + const createFolder = (values: FolderUserInput) => { + startTransition(async () => { + await wrapServerCall({ + fn: async () => serverCreateFolder(values), + onSuccess() { + router.refresh(); + message.open({ type: 'success', content: 'Folder Created' }); + props.close(); + }, + }); + }); + }; + + return ( + + ); +}; diff --git a/src/management-system-v2/components/process-creation-button.tsx b/src/management-system-v2/components/process-creation-button.tsx index f188de3e2..09f2b6781 100644 --- a/src/management-system-v2/components/process-creation-button.tsx +++ b/src/management-system-v2/components/process-creation-button.tsx @@ -1,35 +1,20 @@ -'use client'; - -import React, { ReactNode, useState } from 'react'; +import React, { ComponentProps, ReactNode, useState } from 'react'; import { Button } from 'antd'; import type { ButtonProps, ModalProps } from 'antd'; import ProcessModal from './process-modal'; -import { createProcess } from '@/lib/helpers/processHelpers'; import { addProcesses } from '@/lib/data/processes'; -import { useParams, useRouter, useSelectedLayoutSegments } from 'next/navigation'; +import { useParams, useRouter } from 'next/navigation'; import { useEnvironment } from './auth-can'; import { useAddControlCallback } from '@/lib/controls-store'; import { spaceURL } from '@/lib/utils'; -type ProcessCreationButtonProps = ButtonProps & { - customAction?: (values: { name: string; description: string }) => Promise; - wrapperElement?: ReactNode; - defaultOpen?: boolean; - modalProps?: ModalProps; -}; - -/** - * - * Button to create Processes including a Modal for inserting needed values. Alternatively, a custom wrapper element can be used instead of a button. - */ -const ProcessCreationButton: React.FC = ({ - wrapperElement, - customAction, - defaultOpen = false, - modalProps, - ...props -}) => { - const [isProcessModalOpen, setIsProcessModalOpen] = useState(defaultOpen); +export const ProcessCreationModal: React.FC< + Partial> & { + open: boolean; + setOpen: (open: boolean) => void; + customAction?: (values: { name: string; description: string }) => Promise; + } +> = ({ open, setOpen, customAction, ...props }) => { const router = useRouter(); const environment = useEnvironment(); const folderId = useParams<{ folderId: string }>().folderId ?? ''; @@ -41,10 +26,12 @@ const ProcessCreationButton: React.FC = ({ values.map((value) => ({ ...value, folderId })), environment.spaceId, ).then((res) => (Array.isArray(res) ? res[0] : res))); + if (process && 'error' in process) { return process; } - setIsProcessModalOpen(false); + + setOpen(false); if (process && 'id' in process) { router.push(spaceURL(environment, `/processes/${process.id}`)); @@ -57,8 +44,8 @@ const ProcessCreationButton: React.FC = ({ 'process-list', ['control+enter', 'new'], () => { - if (!isProcessModalOpen) { - setIsProcessModalOpen(true); + if (!open) { + setOpen(true); } }, { @@ -67,30 +54,49 @@ const ProcessCreationButton: React.FC = ({ }, ); + return ( + setOpen(false)} + onSubmit={createNewProcess} + /> + ); +}; + +type ProcessCreationButtonProps = ButtonProps & { + customAction?: (values: { name: string; description: string }) => Promise; + wrapperElement?: ReactNode; + defaultOpen?: boolean; + modalProps?: ModalProps; +}; + +/** + * + * Button to create Processes including a Modal for inserting needed values. Alternatively, a custom wrapper element can be used instead of a button. + */ +const ProcessCreationButton: React.FC = ({ + wrapperElement, + customAction, + defaultOpen = false, + modalProps, + ...props +}) => { + const [isProcessModalOpen, setIsProcessModalOpen] = useState(defaultOpen); + return ( <> {wrapperElement ? ( -
{ - setIsProcessModalOpen(true); - }} - > - {wrapperElement} -
+
setIsProcessModalOpen(true)}>{wrapperElement}
) : ( - +