From ff9d5fbbc2d90c02bbd32efefcdb056338c46382 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Thu, 24 Apr 2025 13:56:59 +0200 Subject: [PATCH 01/10] Bump version to 0.8.0 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfe27f0..93739ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@qoretechnologies/reqraft", - "version": "0.7.4", + "version": "0.8.0", "description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.", "main": "dist/index.js", "types": "dist/index.d.ts", From 4bbadd07e2cd07bf834cd8e30c5259af9fa2bc85 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 28 Apr 2025 16:43:44 +0200 Subject: [PATCH 02/10] Services --- package.json | 2 + src/features/api.ts | 35 ++ src/features/constants.ts | 15 + src/features/qogs/Qogs.stories.tsx | 40 ++ src/features/qogs/QogsStore.tsx | 21 + src/features/qogs/QogsTable.tsx | 0 src/features/qogs/constants.ts | 1 + src/features/qogs/useQogs.tsx | 0 src/features/services/ServicesStore.tsx | 16 + src/features/services/constants.ts | 1 + src/features/utils.ts | 29 ++ src/utils/fetch.ts | 68 ++-- yarn.lock | 488 +++++++++++++++++++++++- 13 files changed, 689 insertions(+), 27 deletions(-) create mode 100644 src/features/api.ts create mode 100644 src/features/constants.ts create mode 100644 src/features/qogs/Qogs.stories.tsx create mode 100644 src/features/qogs/QogsStore.tsx create mode 100644 src/features/qogs/QogsTable.tsx create mode 100644 src/features/qogs/constants.ts create mode 100644 src/features/qogs/useQogs.tsx create mode 100644 src/features/services/ServicesStore.tsx create mode 100644 src/features/services/constants.ts create mode 100644 src/features/utils.ts diff --git a/package.json b/package.json index 93739ff..73a33af 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "start": "yarn storybook", "storybook": "storybook dev -p 6008", "update-reqore": "yarn add -D @qoretechnologies/reqore@beta", + "update-toolkit": "yarn add -D @qoretechnologies/ts-toolkit@beta", "test-storybook": "DEBUG_PRINT_LIMIT=300 test-storybook --url http://localhost:6008", "install-playwright": "npx playwright@latest install --with-deps", "build-storybook": "storybook build", @@ -55,6 +56,7 @@ "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", "@qoretechnologies/reqore": "^0.52.3", + "@qoretechnologies/ts-toolkit": "^0.5.29", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", diff --git a/src/features/api.ts b/src/features/api.ts new file mode 100644 index 0000000..3352e48 --- /dev/null +++ b/src/features/api.ts @@ -0,0 +1,35 @@ +import { + IReqraftFetchErrorResponse, + IReqraftFetchOkResponse, + isError, + query, +} from '../utils/fetch'; +import { FEATURES_API_URL } from './constants'; + +export interface QorusFeatureLoadOptions { + type?: keyof typeof FEATURES_API_URL; + onBefore?: () => void; + onSuccess?: (data: IReqraftFetchOkResponse) => void; + onError?: (data: IReqraftFetchErrorResponse) => void; +} + +export const load = async ({ + type, + onBefore, + onSuccess, + onError, +}: QorusFeatureLoadOptions) => { + onBefore?.(); + + const result = await query({ url: FEATURES_API_URL[type], cache: false }); + + if (isError(result)) { + onError?.(result); + + return Promise.reject(result); + } + + onSuccess?.(result); + + return result; +}; diff --git a/src/features/constants.ts b/src/features/constants.ts new file mode 100644 index 0000000..b908322 --- /dev/null +++ b/src/features/constants.ts @@ -0,0 +1,15 @@ +import { QOGS_API_URL } from './qogs/constants'; +import { SERVICES_API_URL } from './services/constants'; + +export interface QorusFeatureStore { + loading: boolean; + data: T; + error?: Error; + errorData?: string; + load: () => Promise; +} + +export const FEATURES_API_URL = { + qogs: QOGS_API_URL, + services: SERVICES_API_URL, +}; diff --git a/src/features/qogs/Qogs.stories.tsx b/src/features/qogs/Qogs.stories.tsx new file mode 100644 index 0000000..4f0bbf9 --- /dev/null +++ b/src/features/qogs/Qogs.stories.tsx @@ -0,0 +1,40 @@ +import { ReqoreMessage, ReqorePanel, ReqoreSpinner, ReqoreTree } from '@qoretechnologies/reqore'; +import { StoryObj } from '@storybook/react'; +import { fireEvent } from '@storybook/test'; +import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils'; +import { StoryMeta } from '../../types'; +import { QorusQogsStore } from './QogsStore'; + +const meta = { + title: 'Features/Qogs', + render: () => { + const { load, loading, data, error, errorData } = QorusQogsStore(); + + return loading ? ( + + ) : ( + + {error ? ( + + {errorData} + + ) : ( + + )} + + ); + }, +} as StoryMeta; + +export default meta; +export type Story = StoryObj; + +export const QogsCanBeLoaded: Story = { + play: async () => { + await testsClickButton({ label: 'Refetch' }); + await testsWaitForText('0:'); + await sleep(1000); + await fireEvent.click(document.querySelector('.reqore-tree-toggle') as HTMLElement); + await testsWaitForText('"fsm3"'); + }, +}; diff --git a/src/features/qogs/QogsStore.tsx b/src/features/qogs/QogsStore.tsx new file mode 100644 index 0000000..9a478e9 --- /dev/null +++ b/src/features/qogs/QogsStore.tsx @@ -0,0 +1,21 @@ +import { QorusQog } from '@qoretechnologies/ts-toolkit'; +import { create } from 'zustand'; +import { isError, query } from '../../utils/fetch'; +import { QorusFeatureStore } from '../constants'; +import { createFeatureStore } from '../utils'; +import { QOGS_API_URL } from './constants'; + +export interface IQorusQogsStore extends QorusFeatureStore {} + +export const QorusQogsStore = create((set, get) => ({ + ...createFeatureStore('qogs', set, get), + update: async (idOrName: QorusQog['fsmid'] | QorusQog['name'], data: Partial) => { + const update = await query({ url: `${QOGS_API_URL}/${idOrName}`, method: 'PUT', body: data }); + + if (isError(update)) { + return Promise.reject(update); + } + + return update; + }, +})); diff --git a/src/features/qogs/QogsTable.tsx b/src/features/qogs/QogsTable.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/features/qogs/constants.ts b/src/features/qogs/constants.ts new file mode 100644 index 0000000..83e2d17 --- /dev/null +++ b/src/features/qogs/constants.ts @@ -0,0 +1 @@ +export const QOGS_API_URL = 'fsms/'; diff --git a/src/features/qogs/useQogs.tsx b/src/features/qogs/useQogs.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/features/services/ServicesStore.tsx b/src/features/services/ServicesStore.tsx new file mode 100644 index 0000000..e593b2f --- /dev/null +++ b/src/features/services/ServicesStore.tsx @@ -0,0 +1,16 @@ +import { create } from 'zustand'; +import { QorusFeatureStore } from '../constants'; +import { createFeatureStore } from '../utils'; + +export interface QorusService { + type: 'user' | 'system'; + name: string; + version: string; + desc: string; + serviceid: number; +} +export interface QorusServicesStore extends QorusFeatureStore {} + +export const QorusServicesStore = create((set, get) => ({ + ...createFeatureStore('services', set, get), +})); diff --git a/src/features/services/constants.ts b/src/features/services/constants.ts new file mode 100644 index 0000000..5526655 --- /dev/null +++ b/src/features/services/constants.ts @@ -0,0 +1 @@ +export const SERVICES_API_URL = 'services/'; diff --git a/src/features/utils.ts b/src/features/utils.ts new file mode 100644 index 0000000..ec65eeb --- /dev/null +++ b/src/features/utils.ts @@ -0,0 +1,29 @@ +import { load } from './api'; +import { FEATURES_API_URL, QorusFeatureStore } from './constants'; + +export const createFeatureStore = ( + type: keyof typeof FEATURES_API_URL, + set, + get +): QorusFeatureStore => { + return { + loading: false, + data: [] as Data, + error: undefined, + errorData: undefined, + load: async () => { + const result = await load({ + type, + onBefore: () => set({ loading: true, error: undefined }), + onSuccess: (data) => { + set({ loading: false, data: data.data, error: undefined }); + }, + onError: (data) => { + set({ loading: false, error: data.error, errorData: data.data }); + }, + }); + + return result.data; + }, + }; +}; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 7ea355d..302ab2b 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -8,14 +8,28 @@ export interface IReqraftFetchConfig { unauthorizedRedirect?: (pathname: string) => string; } -export interface IReqraftFetchResponse { +export interface IReqraftFetchOkResponse { + ok: true; data: T; - ok: boolean; + + code?: number; + error?: any; + response: Response; +} + +export interface IReqraftFetchErrorResponse { + ok: false; + data: E; + code?: number; error?: any; response: Response; } +export type TReqraftFetchResponse = + | IReqraftFetchOkResponse + | IReqraftFetchErrorResponse; + export const fetchConfig: IReqraftFetchConfig = { instance: window.location.origin + '/', instanceToken: '', @@ -76,19 +90,25 @@ export interface IReqraftQueryConfig { queryClient?: QueryClient; } +export function isError( + res: TReqraftFetchResponse +): res is IReqraftFetchErrorResponse { + return res.ok === false; +} + export async function query({ url, method = 'GET', body, cache = true, queryClient = ReqraftQueryClient, -}: IReqraftQueryConfig): Promise> { +}: IReqraftQueryConfig): Promise> { const shouldCache = method === 'DELETE' || method === 'POST' ? false : cache; const cacheKey = `${url}:${method}:${JSON.stringify(body || {})}`; - const requestData = await queryClient.fetchQuery({ + const requestData = await queryClient.fetchQuery>({ queryKey: [cacheKey], - queryFn: async () => { + queryFn: async (): Promise> => { const response = await doFetchData(url, method, body); if ( @@ -100,19 +120,28 @@ export async function query({ } const clone = response.clone(); - let data: any; + let parsed: unknown; try { - data = await clone.json(); + parsed = await clone.json(); } catch (error) { - data = {}; + parsed = {}; + } + + if (!response.ok) { + return { + data: typeof parsed === 'string' ? parsed : JSON.stringify(parsed), + ok: false as const, + code: response.status, + error: response.statusText, + response, + }; } return { - data, - ok: response.ok, - status: response.status, - statusText: response.statusText, + data: parsed as T, + ok: true as const, + code: response.status, response, }; }, @@ -121,20 +150,7 @@ export async function query({ if (!requestData.ok) { queryClient.invalidateQueries({ queryKey: [cacheKey] }); - - return { - data: requestData.data, - ok: false, - code: requestData.status, - error: requestData.statusText, - response: requestData.response, - }; } - return { - data: requestData.data, - ok: true, - code: requestData.status, - response: requestData.response, - }; + return requestData; } diff --git a/yarn.lock b/yarn.lock index db4c374..343b8fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1715,6 +1715,43 @@ yaml "^2.3.1" zustand "^5.0.3" +"@qoretechnologies/reqraft@^0.7.0": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqraft/-/reqraft-0.7.4.tgz#b6ea43deabcba8a3eccf204d5e5df3ab6f7eb499" + integrity sha512-68nbXLwQc9ZnZowhtWvuxbMI0dvErmpqXa9F4q9sNvYCDCbsPTWDJpYvVYgNV5bFe0Lfqi0IP8MjZYoyu2TEmw== + dependencies: + "@tanstack/react-query" "4" + classnames "^2.2.6" + cronstrue "^2.50.0" + epoch-timeago "^1.1.9" + filesize "^10.1.6" + js-yaml "^4.1.0" + lodash "^4.17.21" + polished "^4.2.2" + react-color "^2.19.3" + react-dropzone "^14.3.5" + react-markdown "^9.0.1" + react-use "^17.4.0" + scheduler "^0.23.0" + shortid "^2.2.16" + styled-components "^5.3.6" + thenby "^1.3.4" + use-context-selector "^1.4.1" + zustand "^5.0.3" + +"@qoretechnologies/ts-toolkit@^0.5.29": + version "0.5.29" + resolved "https://registry.yarnpkg.com/@qoretechnologies/ts-toolkit/-/ts-toolkit-0.5.29.tgz#63b13c42c42b5e8e302efc72885556e93e06378d" + integrity sha512-X4zB4cu4BfvzBr2PQvJwBJ8Nf8xJviBU1j+tzrJ4Y72y08wexrujMPaykPCZYM5iHCPfVzo9TuoDI6iOgLJ9DA== + dependencies: + "@qoretechnologies/reqraft" "^0.7.0" + async "^3.2.4" + cron-validator "^1.3.1" + js-yaml "^4.1.0" + lodash "^4.17.21" + openapi-types "^12.1.3" + react-markdown "^8.0.4" + "@radix-ui/number@1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz" @@ -3855,6 +3892,13 @@ dependencies: "@types/node" "*" +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + "@types/hast@^3.0.0": version "3.0.4" resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz" @@ -3946,6 +3990,13 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz" integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== + dependencies: + "@types/unist" "^2" + "@types/mdast@^4.0.0": version "4.0.3" resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz" @@ -3997,6 +4048,11 @@ resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/prop-types@^15.0.0": + version "15.7.14" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" + integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== + "@types/qs@*": version "6.9.15" resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz" @@ -4093,6 +4149,11 @@ resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz" integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== +"@types/unist@^2": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + "@types/unist@^2.0.0": version "2.0.10" resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz" @@ -4778,6 +4839,11 @@ ast-types@^0.16.1: dependencies: tslib "^2.0.1" +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -5502,6 +5568,11 @@ create-require@^1.1.0: resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron-validator@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cron-validator/-/cron-validator-1.3.1.tgz#8f2fe430f92140df77f91178ae31fc1e3a48a20e" + integrity sha512-C1HsxuPCY/5opR55G5/WNzyEGDWFVG+6GLrA+fW/sCTcP6A6NTjUP2AK7B8n2PyFs90kDG2qzwm8LMheADku6A== + cronstrue@^2.50.0: version "2.50.0" resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz" @@ -5858,6 +5929,11 @@ diff@^4.0.1: resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + diffable-html@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/diffable-html/-/diffable-html-4.1.0.tgz" @@ -7206,6 +7282,11 @@ hast-util-to-string@^3.0.0: dependencies: "@types/hast" "^3.0.0" +hast-util-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" + integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== + hast-util-whitespace@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz" @@ -7435,6 +7516,11 @@ ini@^1.3.4: resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + inline-style-parser@0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz" @@ -7540,6 +7626,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" @@ -8533,6 +8624,11 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kleur@^4.0.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + lazy-universal-dotenv@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz" @@ -8749,6 +8845,33 @@ material-colors@^1.2.1: resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz" integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== +mdast-util-definitions@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" + integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +mdast-util-from-markdown@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + mdast-util-from-markdown@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz" @@ -8818,6 +8941,20 @@ mdast-util-phrasing@^4.0.0: "@types/mdast" "^4.0.0" unist-util-is "^6.0.0" +mdast-util-to-hast@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" + integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-definitions "^5.0.0" + micromark-util-sanitize-uri "^1.1.0" + trim-lines "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + mdast-util-to-hast@^13.0.0: version "13.1.0" resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz" @@ -8847,6 +8984,13 @@ mdast-util-to-markdown@^2.0.0: unist-util-visit "^5.0.0" zwitch "^2.0.0" +mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-string@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz" @@ -8903,6 +9047,28 @@ methods@~1.1.2: resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== +micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromark-core-commonmark@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz" @@ -8925,6 +9091,15 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-destination@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz" @@ -8934,6 +9109,16 @@ micromark-factory-destination@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-factory-label@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz" @@ -8944,6 +9129,14 @@ micromark-factory-label@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-space@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz" @@ -8952,6 +9145,16 @@ micromark-factory-space@^2.0.0: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-title@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz" @@ -8962,6 +9165,16 @@ micromark-factory-title@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-whitespace@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz" @@ -8972,6 +9185,14 @@ micromark-factory-whitespace@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-character@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz" @@ -8980,6 +9201,13 @@ micromark-util-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-chunked@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz" @@ -8987,6 +9215,15 @@ micromark-util-chunked@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-classify-character@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz" @@ -8996,6 +9233,14 @@ micromark-util-classify-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + micromark-util-combine-extensions@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz" @@ -9004,6 +9249,13 @@ micromark-util-combine-extensions@^2.0.0: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-decode-numeric-character-reference@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz" @@ -9011,6 +9263,16 @@ micromark-util-decode-numeric-character-reference@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-decode-string@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz" @@ -9021,16 +9283,33 @@ micromark-util-decode-string@^2.0.0: micromark-util-decode-numeric-character-reference "^2.0.0" micromark-util-symbol "^2.0.0" +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + micromark-util-encode@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz" integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + micromark-util-html-tag-name@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz" integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-normalize-identifier@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz" @@ -9038,6 +9317,13 @@ micromark-util-normalize-identifier@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + micromark-util-resolve-all@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz" @@ -9045,6 +9331,15 @@ micromark-util-resolve-all@^2.0.0: dependencies: micromark-util-types "^2.0.0" +micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-sanitize-uri@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz" @@ -9054,6 +9349,16 @@ micromark-util-sanitize-uri@^2.0.0: micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + micromark-util-subtokenize@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz" @@ -9064,16 +9369,49 @@ micromark-util-subtokenize@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + micromark-util-symbol@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz" integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + micromark-util-types@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz" integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromark@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz" @@ -9181,6 +9519,11 @@ mockdate@^3.0.5: resolved "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz" integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -9513,6 +9856,11 @@ open@^8.0.4: is-docker "^2.1.1" is-wsl "^2.2.0" +openapi-types@^12.1.3: + version "12.1.3" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" @@ -9957,7 +10305,7 @@ prompts@^2.0.1, prompts@^2.4.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -10249,6 +10597,27 @@ react-is@^17.0.1: resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-markdown@^8.0.4: + version "8.0.7" + resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b" + integrity sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ== + dependencies: + "@types/hast" "^2.0.0" + "@types/prop-types" "^15.0.0" + "@types/unist" "^2.0.0" + comma-separated-tokens "^2.0.0" + hast-util-whitespace "^2.0.0" + prop-types "^15.0.0" + property-information "^6.0.0" + react-is "^18.0.0" + remark-parse "^10.0.0" + remark-rehype "^10.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.0" + unified "^10.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + react-markdown@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz" @@ -10551,6 +10920,15 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" +remark-parse@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + remark-parse@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" @@ -10561,6 +10939,16 @@ remark-parse@^11.0.0: micromark-util-types "^2.0.0" unified "^11.0.0" +remark-rehype@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + remark-rehype@^11.0.0: version "11.1.0" resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz" @@ -10737,6 +11125,13 @@ rxjs@^7.8.1: dependencies: tslib "^2.1.0" +sade@^1.7.3: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + safe-array-concat@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz" @@ -11391,6 +11786,13 @@ style-loader@^3.3.1: resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz" integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== +style-to-object@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== + dependencies: + inline-style-parser "0.1.1" + style-to-object@^1.0.0: version "1.0.6" resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz" @@ -11883,6 +12285,19 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== +unified@^10.0.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + unified@^11.0.0: version "11.0.4" resolved "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz" @@ -11903,6 +12318,18 @@ unique-string@^2.0.0: dependencies: crypto-random-string "^2.0.0" +unist-util-generated@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" + integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" @@ -11910,6 +12337,13 @@ unist-util-is@^6.0.0: dependencies: "@types/unist" "^3.0.0" +unist-util-position@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" + integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-position@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz" @@ -11925,6 +12359,13 @@ unist-util-remove-position@^5.0.0: "@types/unist" "^3.0.0" unist-util-visit "^5.0.0" +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" @@ -11932,6 +12373,14 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" +unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" @@ -11940,6 +12389,15 @@ unist-util-visit-parents@^6.0.0: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" +unist-util-visit@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + unist-util-visit@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz" @@ -12062,6 +12520,16 @@ uuid@^9.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + v8-to-istanbul@^9.0.1: version "9.2.0" resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz" @@ -12100,6 +12568,14 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message@^4.0.0: version "4.0.2" resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz" @@ -12108,6 +12584,16 @@ vfile-message@^4.0.0: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" +vfile@^5.0.0: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + vfile@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz" From 204a48f165b414344939ca308dc5f15a8c286205 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Thu, 1 May 2025 10:04:26 +0200 Subject: [PATCH 03/10] feat: add QorusServicesTable component and related hooks for service management --- package.json | 2 +- src/features/services/Services.stories.tsx | 25 ++++++++ .../services/{ServicesStore.tsx => store.tsx} | 0 src/features/services/table.tsx | 62 +++++++++++++++++++ src/features/services/useServices.ts | 26 ++++++++ yarn.lock | 8 +-- 6 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 src/features/services/Services.stories.tsx rename src/features/services/{ServicesStore.tsx => store.tsx} (100%) create mode 100644 src/features/services/table.tsx create mode 100644 src/features/services/useServices.ts diff --git a/package.json b/package.json index 73a33af..1ea1c23 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@babel/preset-typescript": "^7.12.7", "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", - "@qoretechnologies/reqore": "^0.52.3", + "@qoretechnologies/reqore": "^0.53.5", "@qoretechnologies/ts-toolkit": "^0.5.29", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", diff --git a/src/features/services/Services.stories.tsx b/src/features/services/Services.stories.tsx new file mode 100644 index 0000000..4b5e75b --- /dev/null +++ b/src/features/services/Services.stories.tsx @@ -0,0 +1,25 @@ +import { StoryObj } from '@storybook/react'; +import { fireEvent } from '@storybook/test'; +import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils'; +import { StoryMeta } from '../../types'; +import { QorusServicesTable } from './table'; + +const meta = { + title: 'Features/Services', + render: () => { + return ; + }, +} as StoryMeta; + +export default meta; +export type Story = StoryObj; + +export const ServicesCanBeLoaded: Story = { + play: async () => { + await testsClickButton({ label: 'Refetch' }); + await testsWaitForText('0:'); + await sleep(1000); + await fireEvent.click(document.querySelector('.reqore-tree-toggle') as HTMLElement); + await testsWaitForText('"fsm3"'); + }, +}; diff --git a/src/features/services/ServicesStore.tsx b/src/features/services/store.tsx similarity index 100% rename from src/features/services/ServicesStore.tsx rename to src/features/services/store.tsx diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx new file mode 100644 index 0000000..95dc766 --- /dev/null +++ b/src/features/services/table.tsx @@ -0,0 +1,62 @@ +import { ReqoreIcon, ReqoreTable } from '@qoretechnologies/reqore'; +import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel'; +import { IReqoreTableColumn } from '@qoretechnologies/reqore/dist/components/Table'; +import { useMemo } from 'react'; +import { useQorusServices } from './useServices'; + +export interface QorusServiceTableProps extends IReqorePanelProps {} + +export const QorusServicesTable = ({}: QorusServiceTableProps) => { + const services = useQorusServices({ loadOnMount: true }); + const columns = useMemo( + (): IReqoreTableColumn[] => [ + { + dataId: 'serviceid', + header: { + label: 'ID', + }, + sortable: true, + pin: 'left', + align: 'center', + }, + { + filterable: true, + dataId: 'name', + header: { + label: 'Name', + }, + sortable: true, + grow: 1, + }, + { + dataId: 'type', + align: 'center', + header: { + label: 'Type', + }, + cell: { + content: (data) => + data.type === 'user' ? ( + + ) : ( + + ), + }, + sortable: true, + }, + ], + [] + ); + return ( + + ); +}; diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts new file mode 100644 index 0000000..4e7b8cb --- /dev/null +++ b/src/features/services/useServices.ts @@ -0,0 +1,26 @@ +import { useMemo } from 'react'; +import { useEffectOnce } from 'react-use'; +import { QorusServicesStore } from './store'; + +export interface UseQorusServicesConfig { + loadOnMount?: boolean; +} + +export const useQorusServices = ({ loadOnMount }: UseQorusServicesConfig): QorusServicesStore => { + const { data, load, loading } = QorusServicesStore(); + + useEffectOnce(() => { + if (loadOnMount) { + load(); + } + }); + + const items = useMemo(() => { + return data.map((item) => ({ + ...item, + _selectId: item.serviceid, + })); + }, [data]); + + return useMemo(() => ({ data: items, load, loading }), [items, load, loading]); +}; diff --git a/yarn.lock b/yarn.lock index 343b8fb..41faab0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,10 +1682,10 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@qoretechnologies/reqore@^0.52.3": - version "0.52.3" - resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.52.3.tgz#14437f444992b542536aebb1cd1b9557d99d8136" - integrity sha512-VgaQ1+ndHZJIyl2yZa/vCnWtY5Uf1soU0jZNrthpgpMRry49gGpPJn9X9nK/yVg4ezJzsCa4V/uSz6z2NqXfMA== +"@qoretechnologies/reqore@^0.53.5": + version "0.53.5" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.5.tgz#62349ecbe539d3f72ada879de90267a742017ba6" + integrity sha512-GnQOZtxskzTyVWyTFwMSJjal7ueI8tj0wOOK2zm/YTGPLh6SS39ICpxund/V2tIYscE1b969pO/RkLhh+dxrdQ== dependencies: "@internationalized/date" "^3.5.3" "@popperjs/core" "^2.11.6" From c45eb7aa2b6ef0fe88bed5cfd3f3575099be3d2d Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Mon, 5 May 2025 16:44:48 +0200 Subject: [PATCH 04/10] feat: enhance service management with new features and permissions handling --- package.json | 2 +- src/features/api.ts | 48 ++++---- src/features/constants.ts | 13 +- src/features/services/constants.ts | 13 ++ src/features/services/events.ts | 11 ++ src/features/services/store.tsx | 93 +++++++++++++- src/features/services/table.tsx | 164 ++++++++++++++++++++++--- src/features/services/useServices.ts | 67 +++++++++- src/features/utils.ts | 34 ++++- src/hooks/useWebSocket/useWebSocket.ts | 2 +- src/stores/currentUser/currentUser.tsx | 14 +++ src/utils/fetch.ts | 22 +++- src/utils/websocket.ts | 15 +++ yarn.lock | 8 +- 14 files changed, 448 insertions(+), 58 deletions(-) create mode 100644 src/features/services/events.ts diff --git a/package.json b/package.json index 1ea1c23..35839ba 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@babel/preset-typescript": "^7.12.7", "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", - "@qoretechnologies/reqore": "^0.53.5", + "@qoretechnologies/reqore": "^0.53.8", "@qoretechnologies/ts-toolkit": "^0.5.29", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", diff --git a/src/features/api.ts b/src/features/api.ts index 3352e48..4b8ae8c 100644 --- a/src/features/api.ts +++ b/src/features/api.ts @@ -1,35 +1,41 @@ -import { - IReqraftFetchErrorResponse, - IReqraftFetchOkResponse, - isError, - query, -} from '../utils/fetch'; +import { IReqraftQueryConfig, isError, query } from '../utils/fetch'; import { FEATURES_API_URL } from './constants'; -export interface QorusFeatureLoadOptions { +export interface QorusFeatureLoadOptions extends Partial> { type?: keyof typeof FEATURES_API_URL; - onBefore?: () => void; - onSuccess?: (data: IReqraftFetchOkResponse) => void; - onError?: (data: IReqraftFetchErrorResponse) => void; } -export const load = async ({ - type, - onBefore, - onSuccess, - onError, -}: QorusFeatureLoadOptions) => { - onBefore?.(); +export interface QorusFeatureEnableOptions extends QorusFeatureLoadOptions { + id?: string | number; + enable?: boolean; +} - const result = await query({ url: FEATURES_API_URL[type], cache: false }); +export const load = async ({ type, ...options }: QorusFeatureLoadOptions) => { + const result = await query({ ...options, url: FEATURES_API_URL[type], cache: false }); if (isError(result)) { - onError?.(result); - return Promise.reject(result); } - onSuccess?.(result); + return result; +}; + +export const toggleEnabled = async ({ + type, + id, + enable, + ...options +}: QorusFeatureEnableOptions) => { + const result = await query({ + ...options, + method: 'PUT', + url: `${FEATURES_API_URL[type]}/${id}?action=${enable ? 'enable' : 'disable'}`, + cache: false, + }); + + if (isError(result)) { + return Promise.reject(result); + } return result; }; diff --git a/src/features/constants.ts b/src/features/constants.ts index b908322..8af7643 100644 --- a/src/features/constants.ts +++ b/src/features/constants.ts @@ -1,15 +1,26 @@ +import { IReqoreIconName } from '@qoretechnologies/reqore/dist/types/icons'; import { QOGS_API_URL } from './qogs/constants'; import { SERVICES_API_URL } from './services/constants'; export interface QorusFeatureStore { loading: boolean; - data: T; + data: T[]; error?: Error; errorData?: string; load: () => Promise; + + itemById: (id: string | number) => T | undefined; + idKey?: string; + updateItem: (id: string | number, data: Partial) => void; + + hasPermissions: (permissions: string[]) => boolean; } export const FEATURES_API_URL = { qogs: QOGS_API_URL, services: SERVICES_API_URL, }; + +export const FEATURES_ICONS: Record = { + services: 'ServerLine', +}; diff --git a/src/features/services/constants.ts b/src/features/services/constants.ts index 5526655..5582a08 100644 --- a/src/features/services/constants.ts +++ b/src/features/services/constants.ts @@ -1 +1,14 @@ export const SERVICES_API_URL = 'services/'; +export const SERVICES_ACTIONS_PERMISSIONS = { + toggleEnabled: ['SERVICE-CONTROL', 'GROUP-CONTROL', 'MODIFY-GROUP', 'MODIFY-GROUP-STATUS'], + toggleAutostart: ['SERVICE-CONTROL', 'SET-SERVICE-AUTOSTART'], + load: [ + 'CALL-SYSTEM-SERVICES-RW', + 'CALL-SYSTEM-SERVICES-RO', + 'CALL-USER-SERVICES-RW', + 'CALL-USER-SERVICES-RO', + ], + unload: ['SERVICE-CONTROL', 'UNLOAD-SERVICE'], + setRemote: ['SERVICE-CONTROL'], + reset: ['SERVICE-CONTROL', 'RESET-SERVICE'], +}; diff --git a/src/features/services/events.ts b/src/features/services/events.ts new file mode 100644 index 0000000..a6abf8e --- /dev/null +++ b/src/features/services/events.ts @@ -0,0 +1,11 @@ +export interface QorusServiceEnableEventInfo { + id: number; + enabled: boolean; + name: string; + synthetic: boolean; + type: 'service'; +} + +export type QorusServiceEvent = QorusServiceEnableEventInfo; + +export const SERVICE_ENABLE_TOGGLE_EVENT = 'GROUP_STATUS_CHANGED'; diff --git a/src/features/services/store.tsx b/src/features/services/store.tsx index e593b2f..340cdad 100644 --- a/src/features/services/store.tsx +++ b/src/features/services/store.tsx @@ -1,6 +1,9 @@ import { create } from 'zustand'; -import { QorusFeatureStore } from '../constants'; +import { query } from '../../utils/fetch'; +import { toggleEnabled } from '../api'; +import { FEATURES_API_URL, QorusFeatureStore } from '../constants'; import { createFeatureStore } from '../utils'; +import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; export interface QorusService { type: 'user' | 'system'; @@ -8,9 +11,93 @@ export interface QorusService { version: string; desc: string; serviceid: number; + enabled: boolean; + autostart?: boolean; + loaded?: string; + remote?: boolean; + lastUpdated?: number; +} +export interface QorusServicesStore extends QorusFeatureStore { + toggleEnabled: (id: string | number) => Promise; + toggleAutostart: (id: string | number) => Promise; + toggleLoaded: (id: string | number) => Promise; + toggleRemote: (id: string | number) => Promise; + reset: (id: string | number) => Promise; } -export interface QorusServicesStore extends QorusFeatureStore {} export const QorusServicesStore = create((set, get) => ({ - ...createFeatureStore('services', set, get), + ...createFeatureStore('services', set, get), + idKey: 'serviceid', + + toggleEnabled: async (id: string | number) => { + const service = get().itemById(id); + const permissions = SERVICES_ACTIONS_PERMISSIONS.toggleEnabled; + + if (!service || !get().hasPermissions(permissions)) { + return; + } + + toggleEnabled({ type: 'services', id, enable: !service?.enabled }); + }, + toggleAutostart: async (id: string | number) => { + if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart)) { + const service = get().itemById(id); + + if (!service) { + return; + } + + await query({ + method: 'PUT', + url: `${FEATURES_API_URL.services}/${id}?action=setAutostart`, + body: { autostart: !service?.autostart }, + cache: false, + }); + } + }, + toggleLoaded: async (id: string | number) => { + const service = get().itemById(id); + const permissions = service?.loaded + ? SERVICES_ACTIONS_PERMISSIONS.unload + : SERVICES_ACTIONS_PERMISSIONS.load; + + if (!service || !get().hasPermissions(permissions)) { + return; + } + + await query({ + method: 'PUT', + url: `${FEATURES_API_URL.services}/${id}?action=${service.loaded ? 'unload' : 'load'}`, + cache: false, + }); + }, + toggleRemote: async (id: string | number) => { + const service = get().itemById(id); + const permissions = SERVICES_ACTIONS_PERMISSIONS.setRemote; + + if (!service || !get().hasPermissions(permissions)) { + return; + } + + await query({ + method: 'PUT', + url: `${FEATURES_API_URL.services}/${id}?action=setRemote`, + body: { remote: !service?.remote }, + cache: false, + }); + }, + reset: async (id: string | number) => { + const service = get().itemById(id); + const permissions = SERVICES_ACTIONS_PERMISSIONS.reset; + + if (!service || !get().hasPermissions(permissions)) { + return; + } + + await query({ + method: 'PUT', + url: `${FEATURES_API_URL.services}/${id}?action=reset`, + cache: false, + }); + }, })); diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 95dc766..4c55308 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -1,24 +1,34 @@ -import { ReqoreIcon, ReqoreTable } from '@qoretechnologies/reqore'; -import { IReqorePanelProps } from '@qoretechnologies/reqore/dist/components/Panel'; +import { ReqoreButton, ReqoreIcon, ReqoreTable } from '@qoretechnologies/reqore'; +import { + IReqorePanelAction, + IReqorePanelProps, +} from '@qoretechnologies/reqore/dist/components/Panel'; import { IReqoreTableColumn } from '@qoretechnologies/reqore/dist/components/Table'; import { useMemo } from 'react'; +import { FEATURES_ICONS } from '../constants'; +import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; import { useQorusServices } from './useServices'; export interface QorusServiceTableProps extends IReqorePanelProps {} export const QorusServicesTable = ({}: QorusServiceTableProps) => { const services = useQorusServices({ loadOnMount: true }); - const columns = useMemo( - (): IReqoreTableColumn[] => [ + const actions = useMemo( + (): IReqorePanelAction[] => [ { - dataId: 'serviceid', - header: { - label: 'ID', + icon: 'RefreshLine', + tooltip: 'Refresh', + loading: services.loading, + loadingIconType: 4, + onClick: () => { + services.load(); }, - sortable: true, - pin: 'left', - align: 'center', }, + ], + [services.loading] + ); + const columns = useMemo( + (): IReqoreTableColumn[] => [ { filterable: true, dataId: 'name', @@ -26,7 +36,27 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { label: 'Name', }, sortable: true, - grow: 1, + width: 400, + grow: 2, + cell: { + padded: 'none', + content: ({ display_name, name, serviceid, short_desc, isSelected }) => ( + + {display_name || name} + + ), + }, }, { dataId: 'type', @@ -34,29 +64,127 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { header: { label: 'Type', }, + resizable: false, + cell: { + tooltip: (type) => (type === 'user' ? 'User' : 'System'), + content: (data) => ( + + ), + }, + sortable: true, + }, + { + dataId: 'lastUpdated', + align: 'center', + width: 100, + header: { + label: 'Updated', + }, + resizable: true, cell: { - content: (data) => - data.type === 'user' ? ( - - ) : ( - - ), + tooltip: (lastUpdated) => `Last updated: ${lastUpdated}`, + content: 'time-ago', }, sortable: true, }, + { + dataId: 'actions', + header: { + icon: 'SettingsLine', + }, + pin: 'right', + width: 190, + resizable: false, + cell: { + padded: 'none', + actions: ({ enabled, autostart, loaded, remote, serviceid }) => [ + { + icon: enabled ? 'ToggleFill' : 'ToggleLine', + compact: true, + intent: enabled ? 'info' : undefined, + minimal: true, + tooltip: enabled ? 'Enabled, click to disable' : 'Disabled, click to enable', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), + onClick: async () => { + services.toggleEnabled(serviceid); + }, + }, + { + icon: autostart ? 'PauseLine' : 'PlayLine', + compact: true, + intent: autostart ? 'info' : undefined, + minimal: true, + tooltip: autostart + ? 'Autostart is enabled, click to disable' + : 'Autostart is disabled, click to enable', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart), + onClick: async () => { + services.toggleAutostart(serviceid); + }, + }, + { + icon: 'ArrowUpLine', + compact: true, + intent: loaded ? 'info' : undefined, + minimal: true, + tooltip: loaded + ? 'Service is loaded, click to unload' + : 'Service is unloaded, click to load', + onClick: async () => { + services.toggleLoaded(serviceid); + }, + disabled: loaded + ? !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.unload) + : !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.load), + }, + { + icon: 'GlobeLine', + compact: true, + intent: remote ? 'info' : undefined, + minimal: true, + tooltip: remote + ? 'Remote service, click to change to local' + : 'Local service, click to change to remote', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.setRemote), + onClick: async () => { + services.toggleRemote(serviceid); + }, + }, + { + icon: 'HistoryLine', + compact: true, + minimal: true, + tooltip: 'Reset service', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.reset), + onClick: async () => { + services.reset(serviceid); + }, + }, + ], + }, + }, ], [] ); + return ( ); }; diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index 4e7b8cb..be52257 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,13 +1,29 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import { useEffectOnce } from 'react-use'; +import { useReqraftWebSocket } from '../../hooks/useWebSocket/useWebSocket'; +import { QorusApiEvent } from '../../utils/websocket'; +import { QorusServiceEvent, SERVICE_ENABLE_TOGGLE_EVENT } from './events'; import { QorusServicesStore } from './store'; export interface UseQorusServicesConfig { loadOnMount?: boolean; } -export const useQorusServices = ({ loadOnMount }: UseQorusServicesConfig): QorusServicesStore => { - const { data, load, loading } = QorusServicesStore(); +export const useQorusServices = ({ + loadOnMount, +}: UseQorusServicesConfig): Partial => { + const { + data, + load, + toggleEnabled, + toggleAutostart, + toggleLoaded, + toggleRemote, + reset, + hasPermissions, + loading, + updateItem, + } = QorusServicesStore(); useEffectOnce(() => { if (loadOnMount) { @@ -18,9 +34,52 @@ export const useQorusServices = ({ loadOnMount }: UseQorusServicesConfig): Qorus const items = useMemo(() => { return data.map((item) => ({ ...item, + lastUpdated: item.lastUpdated || 0, _selectId: item.serviceid, })); }, [data]); - return useMemo(() => ({ data: items, load, loading }), [items, load, loading]); + const handleMessage = useCallback( + (e: MessageEvent) => { + const data: QorusApiEvent[] = JSON.parse(e.data); + + data.forEach((event: QorusApiEvent) => { + if (event.eventstr === SERVICE_ENABLE_TOGGLE_EVENT) { + updateItem(event.info.id, { enabled: event.info.enabled }); + } + }); + }, + [updateItem] + ); + + useReqraftWebSocket({ + url: 'apievents', + openOnMount: true, + onMessage: handleMessage, + }); + + return useMemo( + () => ({ + data: items, + load, + toggleEnabled, + toggleAutostart, + toggleLoaded, + hasPermissions, + toggleRemote, + reset, + loading, + }), + [ + items, + load, + loading, + toggleEnabled, + toggleAutostart, + toggleLoaded, + toggleRemote, + reset, + hasPermissions, + ] + ); }; diff --git a/src/features/utils.ts b/src/features/utils.ts index ec65eeb..7d27313 100644 --- a/src/features/utils.ts +++ b/src/features/utils.ts @@ -1,3 +1,4 @@ +import { currentUserStore } from '../stores/currentUser/currentUser'; import { load } from './api'; import { FEATURES_API_URL, QorusFeatureStore } from './constants'; @@ -8,9 +9,40 @@ export const createFeatureStore = ( ): QorusFeatureStore => { return { loading: false, - data: [] as Data, + idKey: 'id', + itemById: (id: string | number): Data | undefined => { + const { data, idKey } = get(); + + if (!data) { + return undefined; + } + + return data.find((item: Data) => item[idKey] === id); + }, + data: [] as Data[], error: undefined, errorData: undefined, + updateItem: (id: string | number, data: Partial) => { + const { data: currentData, idKey } = get(); + + if (!currentData) { + return; + } + + const itemIndex = currentData.findIndex((item: Data) => item[idKey] === id); + + if (itemIndex === -1) { + return; + } + + const updatedData = [...currentData]; + updatedData[itemIndex] = { ...updatedData[itemIndex], ...data, lastUpdated: Date.now() }; + + set({ data: updatedData }); + }, + hasPermissions: (permissions: string[]): boolean => { + return currentUserStore.getState().hasAnyPermission(permissions); + }, load: async () => { const result = await load({ type, diff --git a/src/hooks/useWebSocket/useWebSocket.ts b/src/hooks/useWebSocket/useWebSocket.ts index d1f5953..580ad6b 100644 --- a/src/hooks/useWebSocket/useWebSocket.ts +++ b/src/hooks/useWebSocket/useWebSocket.ts @@ -4,7 +4,7 @@ import { getCurrentTimeWithMilliseconds } from '../../utils/datetime'; import { IReqraftWebSocketConfig, ReqraftWebSocket } from '../../utils/websocket'; export interface IUseReqraftWebSocketOptions extends IReqraftWebSocketConfig { - onMessage?: (ev: MessageEvent) => void; + onMessage?: (ev: MessageEvent) => void; useState?: boolean; includeSentMessagesInState?: boolean; includeLogMessagesInState?: boolean; diff --git a/src/stores/currentUser/currentUser.tsx b/src/stores/currentUser/currentUser.tsx index a184b05..642da97 100644 --- a/src/stores/currentUser/currentUser.tsx +++ b/src/stores/currentUser/currentUser.tsx @@ -1,5 +1,6 @@ import { create } from 'zustand'; import { query } from '../../utils/fetch'; +import { ReqraftWebSocket } from '../../utils/websocket'; export interface ICurrentUser { provider: string; @@ -29,6 +30,9 @@ export interface ICurrentUserStore { hasAnyPermission: (permissions: string[]) => boolean; updateStorage: (storage: Record) => void; + + apiEvents?: ReqraftWebSocket; + connectToApiEvents: () => void; } export const currentUserStore = create((set, get) => ({ @@ -53,8 +57,18 @@ export const currentUserStore = create((set, get) => ({ set({ currentUser: response.data, loading: false, errorData: undefined, error: undefined }); + // Connect to API events + get().connectToApiEvents(); + return response.data; }, + connectToApiEvents: () => { + const socket = new ReqraftWebSocket({ + url: `apievents`, + }); + + set({ apiEvents: socket }); + }, hasAnyPermission: (permissions) => { if (!get().currentUser) { return false; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 302ab2b..43958e2 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -82,12 +82,15 @@ async function doFetchData( }); } -export interface IReqraftQueryConfig { +export interface IReqraftQueryConfig { url: string; method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: Record; cache?: boolean; queryClient?: QueryClient; + onBefore?: () => void; + onSuccess?: (data: IReqraftFetchOkResponse) => void; + onError?: (data: IReqraftFetchErrorResponse) => void; } export function isError( @@ -102,10 +105,15 @@ export async function query({ body, cache = true, queryClient = ReqraftQueryClient, -}: IReqraftQueryConfig): Promise> { + onBefore, + onSuccess, + onError, +}: IReqraftQueryConfig): Promise> { const shouldCache = method === 'DELETE' || method === 'POST' ? false : cache; const cacheKey = `${url}:${method}:${JSON.stringify(body || {})}`; + onBefore?.(); + const requestData = await queryClient.fetchQuery>({ queryKey: [cacheKey], queryFn: async (): Promise> => { @@ -129,21 +137,27 @@ export async function query({ } if (!response.ok) { - return { + const result = { data: typeof parsed === 'string' ? parsed : JSON.stringify(parsed), ok: false as const, code: response.status, error: response.statusText, response, }; + + onError?.(result); + return result; } - return { + const result = { data: parsed as T, ok: true as const, code: response.status, response, }; + + onSuccess?.(result); + return result; }, staleTime: shouldCache ? CACHE_EXPIRATION_TIME : 0, }); diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts index 97c5616..c1163d6 100644 --- a/src/utils/websocket.ts +++ b/src/utils/websocket.ts @@ -2,6 +2,21 @@ import { forEach } from 'lodash'; import shortid from 'shortid'; import { fetchConfig, query } from './fetch'; +export interface QorusApiEvent { + class: number; + classstr: string; + compositeseverity: number; + compositeseveritystr: string; + event: number; + eventstr: string; + id: number; + info: Info; + severity: number; + severitystr: string; + time: string; + timeus: number; +} + export interface IReqraftWebSocketConfig { url: string; reconnect?: boolean; diff --git a/yarn.lock b/yarn.lock index 41faab0..ea93be1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,10 +1682,10 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@qoretechnologies/reqore@^0.53.5": - version "0.53.5" - resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.5.tgz#62349ecbe539d3f72ada879de90267a742017ba6" - integrity sha512-GnQOZtxskzTyVWyTFwMSJjal7ueI8tj0wOOK2zm/YTGPLh6SS39ICpxund/V2tIYscE1b969pO/RkLhh+dxrdQ== +"@qoretechnologies/reqore@^0.53.8": + version "0.53.8" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.8.tgz#02f11acf15411c14e2ce394b7b26d35f068cb19b" + integrity sha512-OnMbONZcOos+2dG6WXMB5lwGCUNCC+VfcVTU85poL9RqBxcJmwrXUS/LqXOMk4MW/EWWncYSGEGMCzjG0iPjEg== dependencies: "@internationalized/date" "^3.5.3" "@popperjs/core" "^2.11.6" From 9f6591c98f079636365b38b3040d3150504ad6b4 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Tue, 6 May 2025 21:29:18 +0200 Subject: [PATCH 05/10] feat: update dependencies and enhance service event handling with new API events --- package.json | 4 +- src/features/constants.ts | 5 ++- src/features/events.ts | 34 +++++++++++++++++ src/features/services/events.ts | 57 ++++++++++++++++++++++++---- src/features/services/store.tsx | 50 ++++++++++++++++++------ src/features/services/table.tsx | 13 +++++-- src/features/services/useServices.ts | 29 ++------------ src/features/utils.ts | 6 +++ src/utils/websocket.ts | 45 +++++++++------------- yarn.lock | 16 ++++---- 10 files changed, 173 insertions(+), 86 deletions(-) create mode 100644 src/features/events.ts diff --git a/package.json b/package.json index 35839ba..700558f 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,8 @@ "@babel/preset-typescript": "^7.12.7", "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", - "@qoretechnologies/reqore": "^0.53.8", - "@qoretechnologies/ts-toolkit": "^0.5.29", + "@qoretechnologies/reqore": "^0.53.9", + "@qoretechnologies/ts-toolkit": "^0.5.30", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", diff --git a/src/features/constants.ts b/src/features/constants.ts index 8af7643..3a4dcb1 100644 --- a/src/features/constants.ts +++ b/src/features/constants.ts @@ -4,7 +4,7 @@ import { SERVICES_API_URL } from './services/constants'; export interface QorusFeatureStore { loading: boolean; - data: T[]; + data: (T & { lastUpdated?: number })[]; error?: Error; errorData?: string; load: () => Promise; @@ -13,6 +13,9 @@ export interface QorusFeatureStore { idKey?: string; updateItem: (id: string | number, data: Partial) => void; + registerApiEvents?: () => void; + hasRegisteredApiEvents?: boolean; + hasPermissions: (permissions: string[]) => boolean; } diff --git a/src/features/events.ts b/src/features/events.ts new file mode 100644 index 0000000..da42182 --- /dev/null +++ b/src/features/events.ts @@ -0,0 +1,34 @@ +import { QorusAlert } from '@qoretechnologies/ts-toolkit'; +import { QorusServiceApiEvent } from './services/events'; + +export interface QorusBaseApiEvent { + class: number; + classstr: string; + compositeseverity: number; + compositeseveritystr: string; + event: number; + id: number; + severity: number; + severitystr: string; + time: string; + timeus: number; +} + +export interface QorusGlobalAlertRaisedEvent extends QorusBaseApiEvent { + eventstr: typeof QorusGlobalEvents.AlertRaised; + info: QorusAlert; +} + +export interface QorusGlobalAlertClearedEvent extends QorusBaseApiEvent { + eventstr: typeof QorusGlobalEvents.AlertCleared; + info: QorusAlert; +} + +export type QorusAlertApiEvent = QorusGlobalAlertRaisedEvent | QorusGlobalAlertClearedEvent; + +export type QorusApiEvent = QorusServiceApiEvent | QorusAlertApiEvent; + +export const QorusGlobalEvents = { + AlertRaised: 'ALERT_ONGOING_RAISED', + AlertCleared: 'ALERT_ONGOING_CLEARED', +} as const; diff --git a/src/features/services/events.ts b/src/features/services/events.ts index a6abf8e..b3857e4 100644 --- a/src/features/services/events.ts +++ b/src/features/services/events.ts @@ -1,11 +1,52 @@ -export interface QorusServiceEnableEventInfo { - id: number; - enabled: boolean; - name: string; - synthetic: boolean; - type: 'service'; +import { QorusService } from '@qoretechnologies/ts-toolkit'; +import { QorusBaseApiEvent, QorusGlobalAlertClearedEvent } from '../events'; + +export const QorusServiceEvents = { + ENABLE_TOGGLE: 'GROUP_STATUS_CHANGED', + UPDATED: 'SERVICE_UPDATED', + START: 'SERVICE_START', + STOP: 'SERVICE_STOP', +} as const; + +export interface QorusServiceEnableEventInfo extends QorusBaseApiEvent { + eventstr: typeof QorusServiceEvents.ENABLE_TOGGLE; + info: { id: number; enabled: boolean; name: string; synthetic: boolean; type: 'service' }; +} + +export interface QorusServiceUpdatedEventInfo extends QorusBaseApiEvent { + eventstr: typeof QorusServiceEvents.UPDATED; + info: { + name: string; + version: string; + type: 'system' | 'user'; + serviceid: number; + info: QorusService; + }; } -export type QorusServiceEvent = QorusServiceEnableEventInfo; +export interface QorusServiceStartEventInfo extends QorusBaseApiEvent { + eventstr: typeof QorusServiceEvents.START; + info: { + name: string; + version: string; + type: 'system' | 'user'; + serviceid: number; + }; +} + +export interface QorusServiceStopEventInfo extends QorusBaseApiEvent { + eventstr: typeof QorusServiceEvents.STOP; + info: { + name: string; + version: string; + type: 'system' | 'user'; + serviceid: number; + }; +} -export const SERVICE_ENABLE_TOGGLE_EVENT = 'GROUP_STATUS_CHANGED'; +export type QorusServiceApiEvent = + | QorusServiceEnableEventInfo + | QorusServiceUpdatedEventInfo + | QorusServiceStartEventInfo + | QorusServiceStopEventInfo + | QorusGlobalAlertClearedEvent; diff --git a/src/features/services/store.tsx b/src/features/services/store.tsx index 340cdad..91f309f 100644 --- a/src/features/services/store.tsx +++ b/src/features/services/store.tsx @@ -1,22 +1,15 @@ +import { QorusService } from '@qoretechnologies/ts-toolkit'; import { create } from 'zustand'; +import { currentUserStore } from '../../stores/currentUser/currentUser'; import { query } from '../../utils/fetch'; +import {} from '../../utils/websocket'; import { toggleEnabled } from '../api'; import { FEATURES_API_URL, QorusFeatureStore } from '../constants'; +import { QorusApiEvent, QorusGlobalEvents } from '../events'; import { createFeatureStore } from '../utils'; import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; +import { QorusServiceEvents } from './events'; -export interface QorusService { - type: 'user' | 'system'; - name: string; - version: string; - desc: string; - serviceid: number; - enabled: boolean; - autostart?: boolean; - loaded?: string; - remote?: boolean; - lastUpdated?: number; -} export interface QorusServicesStore extends QorusFeatureStore { toggleEnabled: (id: string | number) => Promise; toggleAutostart: (id: string | number) => Promise; @@ -29,6 +22,39 @@ export const QorusServicesStore = create((set, get) => ({ ...createFeatureStore('services', set, get), idKey: 'serviceid', + registerApiEvents: () => { + currentUserStore.getState().apiEvents.addHandler('message', (e) => { + if (e.data === 'pong') { + return; + } + + const data: QorusApiEvent[] = JSON.parse(e.data); + + data.forEach((event: QorusApiEvent) => { + if (event.eventstr === QorusServiceEvents.ENABLE_TOGGLE) { + get().updateItem(event.info.id, { enabled: event.info.enabled }); + } + + if (event.eventstr === QorusServiceEvents.UPDATED) { + get().updateItem(event.info.serviceid, { ...event.info.info }); + } + + if (event.eventstr === QorusServiceEvents.START) { + get().updateItem(event.info.serviceid, { loaded: event.time }); + } + + if (event.eventstr === QorusServiceEvents.STOP) { + get().updateItem(event.info.serviceid, { loaded: undefined }); + } + + if (event.eventstr === QorusGlobalEvents.AlertCleared) { + const service = get().itemById(event.info.id); + get().updateItem(event.info.id, { enabled: event.info.enabled }); + } + }); + }); + }, + toggleEnabled: async (id: string | number) => { const service = get().itemById(id); const permissions = SERVICES_ACTIONS_PERMISSIONS.toggleEnabled; diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 4c55308..00e4f7b 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -1,9 +1,10 @@ -import { ReqoreButton, ReqoreIcon, ReqoreTable } from '@qoretechnologies/reqore'; +import { ReqoreButton, ReqoreIcon, ReqoreTable, ReqoreTimeAgo } from '@qoretechnologies/reqore'; import { IReqorePanelAction, IReqorePanelProps, } from '@qoretechnologies/reqore/dist/components/Panel'; import { IReqoreTableColumn } from '@qoretechnologies/reqore/dist/components/Table'; +import { size } from 'lodash'; import { useMemo } from 'react'; import { FEATURES_ICONS } from '../constants'; import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; @@ -40,7 +41,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { grow: 2, cell: { padded: 'none', - content: ({ display_name, name, serviceid, short_desc, isSelected }) => ( + content: ({ display_name, name, serviceid, short_desc, isSelected, alerts }) => ( { intent={isSelected ? 'info' : undefined} shrink={1} tooltip={short_desc} + rightIcon={size(alerts) > 0 ? 'AlertLine' : undefined} + labelEffect={{ + underline: true, + }} > {display_name || name} @@ -83,7 +88,9 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { resizable: true, cell: { tooltip: (lastUpdated) => `Last updated: ${lastUpdated}`, - content: 'time-ago', + content: ({ lastUpdated }) => { + return ; + }, }, sortable: true, }, diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index be52257..1db6c7a 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,8 +1,6 @@ -import { useCallback, useMemo } from 'react'; +import { size } from 'lodash'; +import { useMemo } from 'react'; import { useEffectOnce } from 'react-use'; -import { useReqraftWebSocket } from '../../hooks/useWebSocket/useWebSocket'; -import { QorusApiEvent } from '../../utils/websocket'; -import { QorusServiceEvent, SERVICE_ENABLE_TOGGLE_EVENT } from './events'; import { QorusServicesStore } from './store'; export interface UseQorusServicesConfig { @@ -22,7 +20,6 @@ export const useQorusServices = ({ reset, hasPermissions, loading, - updateItem, } = QorusServicesStore(); useEffectOnce(() => { @@ -34,30 +31,12 @@ export const useQorusServices = ({ const items = useMemo(() => { return data.map((item) => ({ ...item, - lastUpdated: item.lastUpdated || 0, + lastUpdated: item.lastUpdated, _selectId: item.serviceid, + _intent: size(item.alerts) > 0 ? 'danger' : undefined, })); }, [data]); - const handleMessage = useCallback( - (e: MessageEvent) => { - const data: QorusApiEvent[] = JSON.parse(e.data); - - data.forEach((event: QorusApiEvent) => { - if (event.eventstr === SERVICE_ENABLE_TOGGLE_EVENT) { - updateItem(event.info.id, { enabled: event.info.enabled }); - } - }); - }, - [updateItem] - ); - - useReqraftWebSocket({ - url: 'apievents', - openOnMount: true, - onMessage: handleMessage, - }); - return useMemo( () => ({ data: items, diff --git a/src/features/utils.ts b/src/features/utils.ts index 7d27313..ea3c005 100644 --- a/src/features/utils.ts +++ b/src/features/utils.ts @@ -49,6 +49,12 @@ export const createFeatureStore = ( onBefore: () => set({ loading: true, error: undefined }), onSuccess: (data) => { set({ loading: false, data: data.data, error: undefined }); + + // Register API events if not already registered + if (!get().hasRegisteredApiEvents) { + get().registerApiEvents?.(); + set({ hasRegisteredApiEvents: true }); + } }, onError: (data) => { set({ loading: false, error: data.error, errorData: data.data }); diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts index c1163d6..3fc3ff6 100644 --- a/src/utils/websocket.ts +++ b/src/utils/websocket.ts @@ -2,21 +2,6 @@ import { forEach } from 'lodash'; import shortid from 'shortid'; import { fetchConfig, query } from './fetch'; -export interface QorusApiEvent { - class: number; - classstr: string; - compositeseverity: number; - compositeseveritystr: string; - event: number; - eventstr: string; - id: number; - info: Info; - severity: number; - severitystr: string; - time: string; - timeus: number; -} - export interface IReqraftWebSocketConfig { url: string; reconnect?: boolean; @@ -51,10 +36,10 @@ export class ReqraftWebSocketsManager { }); } - public static addHandler( + public static addHandler( url: string, - event: keyof WebSocketEventMap, - handler: (ev: Event | MessageEvent | CloseEvent) => void + event: E, + handler: (ev: WebSocketEventMap[E]) => void ) { this.connections[url]?.socket.addEventListener(event, handler); @@ -63,10 +48,10 @@ export class ReqraftWebSocketsManager { }; } - public static removeHandler( + public static removeHandler( url: string, - event: keyof WebSocketEventMap, - handler: (ev: Event | MessageEvent | CloseEvent) => void + event: E, + handler: (ev: WebSocketEventMap[E]) => void ) { this.connections[url]?.socket.removeEventListener(event, handler); } @@ -79,7 +64,10 @@ export class ReqraftWebSocket { public options: IReqraftWebSocketConfig; public readonly handlers: Record< string, - { type: keyof WebSocketEventMap; event: (ev: Event) => void } + { + type: keyof WebSocketEventMap; + event: (ev: WebSocketEventMap[keyof WebSocketEventMap]) => void; + } > = {}; public socket: WebSocket; @@ -92,13 +80,16 @@ export class ReqraftWebSocket { this.connect(); } - public addHandler( - event: keyof WebSocketEventMap, - handler: (ev: Event | MessageEvent | CloseEvent) => void - ) { + public addHandler( + event: E, + handler: (ev: WebSocketEventMap[E]) => void + ): string { const id = shortid.generate(); - this.handlers[id] = { type: event, event: handler }; + this.handlers[id] = { + type: event, + event: handler as (e: WebSocketEventMap[keyof WebSocketEventMap]) => void, + }; ReqraftWebSocketsManager.addHandler(this.options.url, event, handler); diff --git a/yarn.lock b/yarn.lock index ea93be1..1e6e312 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,10 +1682,10 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@qoretechnologies/reqore@^0.53.8": - version "0.53.8" - resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.8.tgz#02f11acf15411c14e2ce394b7b26d35f068cb19b" - integrity sha512-OnMbONZcOos+2dG6WXMB5lwGCUNCC+VfcVTU85poL9RqBxcJmwrXUS/LqXOMk4MW/EWWncYSGEGMCzjG0iPjEg== +"@qoretechnologies/reqore@^0.53.9": + version "0.53.9" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.9.tgz#bc4f519d57d571c72bdeae10d5bb7a455ae6652f" + integrity sha512-vg1O7nvqxGfXITTSGQ0RQkMdoG9LHFPhhzCBpQ4hgKowIUY/jHs6JYQuDWifTh8+taW3Ljzkp8CcUrspFu5sSQ== dependencies: "@internationalized/date" "^3.5.3" "@popperjs/core" "^2.11.6" @@ -1739,10 +1739,10 @@ use-context-selector "^1.4.1" zustand "^5.0.3" -"@qoretechnologies/ts-toolkit@^0.5.29": - version "0.5.29" - resolved "https://registry.yarnpkg.com/@qoretechnologies/ts-toolkit/-/ts-toolkit-0.5.29.tgz#63b13c42c42b5e8e302efc72885556e93e06378d" - integrity sha512-X4zB4cu4BfvzBr2PQvJwBJ8Nf8xJviBU1j+tzrJ4Y72y08wexrujMPaykPCZYM5iHCPfVzo9TuoDI6iOgLJ9DA== +"@qoretechnologies/ts-toolkit@^0.5.30": + version "0.5.30" + resolved "https://registry.yarnpkg.com/@qoretechnologies/ts-toolkit/-/ts-toolkit-0.5.30.tgz#cf9bfa1cf906f29502724261634ac98db974e320" + integrity sha512-uKLZc34cW+MBAtL3HNVM1wbws/jc7cWkf7U47dqB12RcMLg9d46FsyjFlX87TwDhGWuADA3xm1F/BDP5q7tYkA== dependencies: "@qoretechnologies/reqraft" "^0.7.0" async "^3.2.4" From 1294d9513ad13533837e7c29d91795d68c503107 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Tue, 6 May 2025 23:56:49 +0200 Subject: [PATCH 06/10] feat: update ts-toolkit dependency to version 0.5.31 and refactor service store methods for improved functionality --- package.json | 2 +- src/features/services/store.tsx | 67 ++++++++++++++------------- src/features/services/table.tsx | 68 +++++++++++++++++++++------- src/features/services/useServices.ts | 20 ++++---- yarn.lock | 8 ++-- 5 files changed, 102 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 700558f..257d60e 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", "@qoretechnologies/reqore": "^0.53.9", - "@qoretechnologies/ts-toolkit": "^0.5.30", + "@qoretechnologies/ts-toolkit": "^0.5.31", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", diff --git a/src/features/services/store.tsx b/src/features/services/store.tsx index 91f309f..fc7a56a 100644 --- a/src/features/services/store.tsx +++ b/src/features/services/store.tsx @@ -3,7 +3,6 @@ import { create } from 'zustand'; import { currentUserStore } from '../../stores/currentUser/currentUser'; import { query } from '../../utils/fetch'; import {} from '../../utils/websocket'; -import { toggleEnabled } from '../api'; import { FEATURES_API_URL, QorusFeatureStore } from '../constants'; import { QorusApiEvent, QorusGlobalEvents } from '../events'; import { createFeatureStore } from '../utils'; @@ -11,9 +10,10 @@ import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; import { QorusServiceEvents } from './events'; export interface QorusServicesStore extends QorusFeatureStore { - toggleEnabled: (id: string | number) => Promise; - toggleAutostart: (id: string | number) => Promise; - toggleLoaded: (id: string | number) => Promise; + toggleEnabledCall: (ids: number[], enabled: true | false) => void; + toggleAutostartCall: (ids: number[], autostart: true | false) => Promise; + toggleLoadedCall: (ids: number[], loaded: true | false) => Promise; + toggleRemote: (id: string | number) => Promise; reset: (id: string | number) => Promise; } @@ -47,53 +47,58 @@ export const QorusServicesStore = create((set, get) => ({ get().updateItem(event.info.serviceid, { loaded: undefined }); } - if (event.eventstr === QorusGlobalEvents.AlertCleared) { + if (event.eventstr === QorusGlobalEvents.AlertRaised && event.info.type === 'SERVICE') { const service = get().itemById(event.info.id); - get().updateItem(event.info.id, { enabled: event.info.enabled }); + const alerts = [...(service?.alerts || []), event.info]; + + get().updateItem(event.info.id, { alerts }); + } + + if (event.eventstr === QorusGlobalEvents.AlertCleared && event.info.type === 'SERVICE') { + const service = get().itemById(event.info.id); + const alerts = service?.alerts?.filter((alert) => alert.alertid !== event.info.alertid); + + get().updateItem(event.info.id, { alerts }); } }); }); }, - toggleEnabled: async (id: string | number) => { - const service = get().itemById(id); - const permissions = SERVICES_ACTIONS_PERMISSIONS.toggleEnabled; - - if (!service || !get().hasPermissions(permissions)) { - return; + toggleEnabledCall: (ids: number[], enabled: true | false) => { + if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled)) { + query({ + method: 'PUT', + url: `${FEATURES_API_URL.services}?action=${enabled ? 'enable' : 'disable'}`, + body: { enabled, ids: ids.join(',') }, + cache: false, + }); } - - toggleEnabled({ type: 'services', id, enable: !service?.enabled }); }, - toggleAutostart: async (id: string | number) => { - if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart)) { - const service = get().itemById(id); - if (!service) { - return; - } - - await query({ + toggleAutostartCall: async (ids: number[], autostart: boolean) => { + if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart)) { + query({ method: 'PUT', - url: `${FEATURES_API_URL.services}/${id}?action=setAutostart`, - body: { autostart: !service?.autostart }, + url: `${FEATURES_API_URL.services}?action=setAutostart`, + body: { autostart, ids: ids.join(',') }, cache: false, }); } }, - toggleLoaded: async (id: string | number) => { - const service = get().itemById(id); - const permissions = service?.loaded - ? SERVICES_ACTIONS_PERMISSIONS.unload - : SERVICES_ACTIONS_PERMISSIONS.load; - if (!service || !get().hasPermissions(permissions)) { + toggleLoadedCall: async (ids: number[], loaded: boolean) => { + if ( + !get().hasPermissions( + loaded ? SERVICES_ACTIONS_PERMISSIONS.load : SERVICES_ACTIONS_PERMISSIONS.unload + ) + ) { return; } await query({ method: 'PUT', - url: `${FEATURES_API_URL.services}/${id}?action=${service.loaded ? 'unload' : 'load'}`, + url: `${FEATURES_API_URL.services}?action=${loaded ? 'load' : 'unload'}`, + body: { loaded, ids: ids.join(',') }, cache: false, }); }, diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 00e4f7b..423571d 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -1,11 +1,11 @@ -import { ReqoreButton, ReqoreIcon, ReqoreTable, ReqoreTimeAgo } from '@qoretechnologies/reqore'; +import { ReqoreIcon, ReqoreTable, ReqoreTag, ReqoreTimeAgo } from '@qoretechnologies/reqore'; import { IReqorePanelAction, IReqorePanelProps, } from '@qoretechnologies/reqore/dist/components/Panel'; import { IReqoreTableColumn } from '@qoretechnologies/reqore/dist/components/Table'; import { size } from 'lodash'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { FEATURES_ICONS } from '../constants'; import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; import { useQorusServices } from './useServices'; @@ -13,9 +13,41 @@ import { useQorusServices } from './useServices'; export interface QorusServiceTableProps extends IReqorePanelProps {} export const QorusServicesTable = ({}: QorusServiceTableProps) => { + const [selected, setSelected] = useState([]); + const services = useQorusServices({ loadOnMount: true }); const actions = useMemo( (): IReqorePanelAction[] => [ + { + label: 'With Selected', + minimal: true, + intent: 'info', + badge: selected.length, + show: selected.length > 0, + tooltip: 'Manage Selected Items', + loading: services.loading, + loadingIconType: 4, + actions: [ + { + icon: 'ToggleFill', + label: 'Enable', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), + onClick: async () => { + services.toggleEnabledCall(selected, true); + setSelected([]); + }, + }, + { + icon: 'ToggleLine', + label: 'Disable', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), + onClick: async () => { + services.toggleEnabledCall(selected, false); + setSelected([]); + }, + }, + ], + }, { icon: 'RefreshLine', tooltip: 'Refresh', @@ -26,8 +58,9 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { }, }, ], - [services.loading] + [services.loading, selected] ); + const columns = useMemo( (): IReqoreTableColumn[] => [ { @@ -40,26 +73,27 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { width: 400, grow: 2, cell: { - padded: 'none', content: ({ display_name, name, serviceid, short_desc, isSelected, alerts }) => ( - 0 + ? 'warning:lighten:1:0.5' + : isSelected + ? 'info:lighten:1:0.5' + : undefined, + }} shrink={1} tooltip={short_desc} rightIcon={size(alerts) > 0 ? 'AlertLine' : undefined} - labelEffect={{ - underline: true, - }} - > - {display_name || name} - + label={display_name || name} + /> ), }, }, @@ -113,7 +147,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { tooltip: enabled ? 'Enabled, click to disable' : 'Disabled, click to enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), onClick: async () => { - services.toggleEnabled(serviceid); + services.toggleEnabledCall([serviceid], !enabled); }, }, { @@ -126,7 +160,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { : 'Autostart is disabled, click to enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart), onClick: async () => { - services.toggleAutostart(serviceid); + services.toggleAutostartCall([serviceid], !autostart); }, }, { @@ -138,7 +172,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { ? 'Service is loaded, click to unload' : 'Service is unloaded, click to load', onClick: async () => { - services.toggleLoaded(serviceid); + services.toggleLoadedCall([serviceid], !loaded); }, disabled: loaded ? !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.unload) @@ -192,6 +226,8 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { striped exportable actions={actions} + onSelectedChange={setSelected} + selected={selected} /> ); }; diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index 1db6c7a..9a8b35b 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,4 +1,3 @@ -import { size } from 'lodash'; import { useMemo } from 'react'; import { useEffectOnce } from 'react-use'; import { QorusServicesStore } from './store'; @@ -13,9 +12,9 @@ export const useQorusServices = ({ const { data, load, - toggleEnabled, - toggleAutostart, - toggleLoaded, + toggleEnabledCall, + toggleAutostartCall, + toggleLoadedCall, toggleRemote, reset, hasPermissions, @@ -33,7 +32,6 @@ export const useQorusServices = ({ ...item, lastUpdated: item.lastUpdated, _selectId: item.serviceid, - _intent: size(item.alerts) > 0 ? 'danger' : undefined, })); }, [data]); @@ -41,9 +39,9 @@ export const useQorusServices = ({ () => ({ data: items, load, - toggleEnabled, - toggleAutostart, - toggleLoaded, + toggleEnabledCall, + toggleAutostartCall, + toggleLoadedCall, hasPermissions, toggleRemote, reset, @@ -53,9 +51,9 @@ export const useQorusServices = ({ items, load, loading, - toggleEnabled, - toggleAutostart, - toggleLoaded, + toggleEnabledCall, + toggleAutostartCall, + toggleLoadedCall, toggleRemote, reset, hasPermissions, diff --git a/yarn.lock b/yarn.lock index 1e6e312..3d2efdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1739,10 +1739,10 @@ use-context-selector "^1.4.1" zustand "^5.0.3" -"@qoretechnologies/ts-toolkit@^0.5.30": - version "0.5.30" - resolved "https://registry.yarnpkg.com/@qoretechnologies/ts-toolkit/-/ts-toolkit-0.5.30.tgz#cf9bfa1cf906f29502724261634ac98db974e320" - integrity sha512-uKLZc34cW+MBAtL3HNVM1wbws/jc7cWkf7U47dqB12RcMLg9d46FsyjFlX87TwDhGWuADA3xm1F/BDP5q7tYkA== +"@qoretechnologies/ts-toolkit@^0.5.31": + version "0.5.31" + resolved "https://registry.yarnpkg.com/@qoretechnologies/ts-toolkit/-/ts-toolkit-0.5.31.tgz#fa3fa8787591fcd358c03636f783036e31ae5f8c" + integrity sha512-fy0aHwv+SW/+zRUHlwmkyhXlBrYVEnSmK8piMngesGLW3X//Ex6uHs7iY6KuMtUB9jEzN5hA3pTLZ/Kl68Z4mg== dependencies: "@qoretechnologies/reqraft" "^0.7.0" async "^3.2.4" From b89336dbc561108138d4b62d1303adea63388367 Mon Sep 17 00:00:00 2001 From: Foxhoundn Date: Thu, 8 May 2025 16:59:17 +0200 Subject: [PATCH 07/10] feat: enhance service management with new API interactions, mock data, and UI updates --- .storybook/preview.tsx | 4 + __tests__/services/api.ts | 64 +++++++++++++++ __tests__/services/data.ts | 91 ++++++++++++++++++++++ package.json | 2 +- src/features/services/Services.stories.tsx | 79 +++++++++++++++++-- src/features/services/api.ts | 12 +++ src/features/services/constants.ts | 1 + src/features/services/store.tsx | 91 +++++++++++++++------- src/features/services/table.tsx | 56 ++++++++++++- src/features/services/useServices.ts | 84 ++++++++++++-------- src/hooks/useFetch/useFetch.tsx | 2 +- src/providers/StorageProvider.tsx | 6 +- src/stores/currentUser/currentUser.tsx | 6 +- yarn.lock | 8 +- 14 files changed, 427 insertions(+), 79 deletions(-) create mode 100644 __tests__/services/api.ts create mode 100644 __tests__/services/data.ts create mode 100644 src/features/services/api.ts diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index d9ecdc9..50e0b54 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,6 +3,10 @@ import { ReqoreContent, ReqoreLayoutContent, ReqoreUIProvider } from '@qoretechn import { initializeReqraft } from '../src'; export const parameters = { + mockAddonConfigs: { + ignoreQueryParams: true, + globalMockData: [], + }, actions: { argTypesRegex: '^on[A-Z].*' }, layout: 'fullscreen', options: { diff --git a/__tests__/services/api.ts b/__tests__/services/api.ts new file mode 100644 index 0000000..5ebf7ef --- /dev/null +++ b/__tests__/services/api.ts @@ -0,0 +1,64 @@ +import { QorusService } from '@qoretechnologies/ts-toolkit'; +import { size } from 'lodash'; +import { QorusServiceEnableCallResponse } from '../../src/features/services/api'; +import { QorusServiceEnableEventInfo } from '../../src/features/services/events'; +import { ServicesSocket } from '../../src/features/services/Services.stories'; +import { MockServicesData } from './data'; + +export const GetServices = { + url: 'https://hq.qoretechnologies.com:8092/api/latest/services/', + method: 'GET', + status: 200, + response: MockServicesData, +}; + +export const ToggleEnableServices = { + url: 'https://hq.qoretechnologies.com:8092/api/latest/services/', + method: 'PUT', + status: 200, + response: (request) => { + const { searchParams, body } = request; + const { ids } = JSON.parse(body); + const affectedServices = MockServicesData.filter((service) => ids.includes(service.serviceid)); + + const action = searchParams.action; + const key = action === 'enable' ? 'enabled' : 'disabled'; + + const getResponseData = (service: QorusService): Partial => + action === 'enable' + ? { + info: size(service.alerts) + ? `Service ${service.serviceid} was NOT enabled` + : `Service ${service.serviceid} enabled`, + [key]: size(service.alerts) ? false : true, + } + : { + info: `Service ${service.serviceid} was disabled`, + [key]: true, + }; + + const responseEvents: Partial[] = affectedServices + .filter((service) => service.serviceid !== 3) + .map((service) => ({ + eventstr: 'GROUP_STATUS_CHANGED', + info: { + id: service.serviceid, + enabled: action === 'enable' ? (size(service.alerts) ? false : true) : false, + name: service.name, + synthetic: false, + type: 'service', + }, + })); + + ServicesSocket.send(JSON.stringify(responseEvents)); + + return affectedServices.map((service) => ({ + arg: 'any', + serviceid: service.serviceid, + name: service.name, + type: service.type, + version: service.version, + ...getResponseData(service), + })) as QorusServiceEnableCallResponse[]; + }, +}; diff --git a/__tests__/services/data.ts b/__tests__/services/data.ts new file mode 100644 index 0000000..fe87936 --- /dev/null +++ b/__tests__/services/data.ts @@ -0,0 +1,91 @@ +import { QorusService } from '@qoretechnologies/ts-toolkit'; + +export const MockServicesData = [ + { + autostart: true, + enabled: true, + loaded: '2023-10-03T12:00:00Z', + name: 'Enabled_Service', + display_name: 'Enabled Service', + serviceid: 1, + type: 'user', + version: '1.0', + manual_autostart: false, + methods: [], + remote: false, + stateless: false, + status: 'whatever', + }, + { + autostart: false, + enabled: false, + name: 'Disabled_Service', + display_name: 'Disabled Service', + serviceid: 2, + type: 'user', + version: '1.0', + manual_autostart: false, + methods: [], + remote: false, + stateless: false, + status: 'whatever', + }, + { + autostart: false, + enabled: false, + name: 'service_3', + alerts: [ + { + alert: 'alert_1', + alertid: 1, + alerttype: 'ONGOING', + id: 3, + instance: 'mock_instance', + local: false, + name: 'Alert 1', + object: 'service_3', + reason: 'This is a test', + source: 'service_3', + type: 'SERVICE', + when: '2023-10-03T12:00:00Z', + who: 'mock_user', + }, + ], + display_name: 'Service With Alerts', + serviceid: 3, + type: 'user', + version: '1.0', + manual_autostart: false, + methods: [], + remote: false, + stateless: false, + status: 'whatever', + }, + { + autostart: false, + enabled: true, + name: 'service_4', + display_name: 'Service With No Alerts', + serviceid: 4, + type: 'user', + version: '1.0', + manual_autostart: false, + methods: [], + remote: true, + stateless: false, + status: 'whatever', + }, + { + autostart: true, + enabled: true, + name: 'service_5', + display_name: 'System Service', + serviceid: 5, + type: 'system', + version: '1.0', + manual_autostart: false, + methods: [], + stateless: false, + status: 'whatever', + }, +] as QorusService[]; diff --git a/package.json b/package.json index 257d60e..e8a761b 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@babel/preset-typescript": "^7.12.7", "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", - "@qoretechnologies/reqore": "^0.53.9", + "@qoretechnologies/reqore": "^0.53.10", "@qoretechnologies/ts-toolkit": "^0.5.31", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", diff --git a/src/features/services/Services.stories.tsx b/src/features/services/Services.stories.tsx index 4b5e75b..6fbd6b5 100644 --- a/src/features/services/Services.stories.tsx +++ b/src/features/services/Services.stories.tsx @@ -1,25 +1,94 @@ import { StoryObj } from '@storybook/react'; -import { fireEvent } from '@storybook/test'; +import { expect, fireEvent } from '@storybook/test'; +import { Client, Server } from 'mock-socket'; +import { GetServices, ToggleEnableServices } from '../../../__tests__/services/api'; import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils'; import { StoryMeta } from '../../types'; import { QorusServicesTable } from './table'; +export let ServicesSocket: Client; + const meta = { title: 'Features/Services', + excludeStories: ['ServicesSocket'], render: () => { return ; }, + async beforeEach() { + const url = `wss://hq.qoretechnologies.com:8092/apievents?token=${process.env.REACT_APP_QORUS_TOKEN}`; + let server = new Server(url); + let killTimeout: NodeJS.Timeout; + + server.on('connection', (socket) => { + ServicesSocket = socket; + + if (killTimeout) { + server.close(); + return; + } + + socket.on('message', (data) => { + if (data === 'ping') { + socket.send('pong'); + return; + } + + if (data === 'kill') { + server.close(); + + killTimeout = setTimeout(() => { + server = new Server(url); + killTimeout = null; + }, 3000); + + return; + } + + socket.send(`Received message: ${data}`); + }); + }); + + return () => { + killTimeout && clearTimeout(killTimeout); + killTimeout = null; + server.close({ + code: 4999, + reason: 'Test ended', + wasClean: true, + }); + }; + }, } as StoryMeta; export default meta; export type Story = StoryObj; export const ServicesCanBeLoaded: Story = { + parameters: { + mockData: [GetServices], + }, play: async () => { - await testsClickButton({ label: 'Refetch' }); - await testsWaitForText('0:'); + await testsWaitForText('Enabled Service'); + await sleep(1000); + await expect(document.querySelectorAll('.reqore-table-body .reqore-table-row')).toHaveLength( + 15 + ); + }, +}; + +export const ServicesCanBeSelected: Story = { + ...ServicesCanBeLoaded, + parameters: { + mockData: [...ServicesCanBeLoaded.parameters.mockData, ToggleEnableServices], + }, + play: async (args) => { + await ServicesCanBeLoaded.play(args); + // ??? No idea why this is needed, but it is + await fireEvent.click(document.querySelectorAll('.reqore-table-header-cell')[0]); + await fireEvent.click(document.querySelectorAll('.reqore-table-header-cell')[0]); await sleep(1000); - await fireEvent.click(document.querySelector('.reqore-tree-toggle') as HTMLElement); - await testsWaitForText('"fsm3"'); + await testsWaitForText('With Selected'); + await testsClickButton({ label: 'With Selected' }); + await testsWaitForText('Reset'); }, }; diff --git a/src/features/services/api.ts b/src/features/services/api.ts new file mode 100644 index 0000000..8127d76 --- /dev/null +++ b/src/features/services/api.ts @@ -0,0 +1,12 @@ +import { QorusService } from '@qoretechnologies/ts-toolkit'; + +export interface QorusServiceEnableCallResponse { + arg: string; + enabled?: boolean; + disabled?: boolean; + info: string; + name: string; + serviceid: number; + type: QorusService['type']; + version: string; +} diff --git a/src/features/services/constants.ts b/src/features/services/constants.ts index 5582a08..55874b4 100644 --- a/src/features/services/constants.ts +++ b/src/features/services/constants.ts @@ -11,4 +11,5 @@ export const SERVICES_ACTIONS_PERMISSIONS = { unload: ['SERVICE-CONTROL', 'UNLOAD-SERVICE'], setRemote: ['SERVICE-CONTROL'], reset: ['SERVICE-CONTROL', 'RESET-SERVICE'], + manageSystemService: ['SERVER-CONTROL', 'SYSTEM-SERVICE-CONTROL'], }; diff --git a/src/features/services/store.tsx b/src/features/services/store.tsx index fc7a56a..df0e6fa 100644 --- a/src/features/services/store.tsx +++ b/src/features/services/store.tsx @@ -1,21 +1,30 @@ import { QorusService } from '@qoretechnologies/ts-toolkit'; import { create } from 'zustand'; import { currentUserStore } from '../../stores/currentUser/currentUser'; -import { query } from '../../utils/fetch'; +import { query, TReqraftFetchResponse } from '../../utils/fetch'; import {} from '../../utils/websocket'; import { FEATURES_API_URL, QorusFeatureStore } from '../constants'; import { QorusApiEvent, QorusGlobalEvents } from '../events'; import { createFeatureStore } from '../utils'; +import { QorusServiceEnableCallResponse } from './api'; import { SERVICES_ACTIONS_PERMISSIONS } from './constants'; import { QorusServiceEvents } from './events'; export interface QorusServicesStore extends QorusFeatureStore { - toggleEnabledCall: (ids: number[], enabled: true | false) => void; - toggleAutostartCall: (ids: number[], autostart: true | false) => Promise; - toggleLoadedCall: (ids: number[], loaded: true | false) => Promise; - - toggleRemote: (id: string | number) => Promise; - reset: (id: string | number) => Promise; + toggleEnabledCall: ( + ids: number[], + enabled: true | false + ) => Promise>; + toggleAutostartCall: ( + ids: number, + autostart: true | false + ) => Promise>; + toggleLoadedCall: ( + ids: number[], + loaded: true | false + ) => Promise>; + resetCall: (id: number[]) => Promise>; + toggleRemoteCall: (id: string | number) => Promise>; } export const QorusServicesStore = create((set, get) => ({ @@ -64,70 +73,100 @@ export const QorusServicesStore = create((set, get) => ({ }); }, - toggleEnabledCall: (ids: number[], enabled: true | false) => { + toggleEnabledCall: ( + ids: number[], + enabled: true | false + ): Promise> => { if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled)) { - query({ + console.log({ ids, enabled }); + return query({ method: 'PUT', url: `${FEATURES_API_URL.services}?action=${enabled ? 'enable' : 'disable'}`, - body: { enabled, ids: ids.join(',') }, + body: { ids: ids.join(',') }, cache: false, }); } + + return Promise.reject({ + ok: false, + error: 'Permission denied', + data: 'Insufficient permissions to perform the action', + }); }, - toggleAutostartCall: async (ids: number[], autostart: boolean) => { + toggleAutostartCall: (id: number, autostart: boolean) => { if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart)) { - query({ + return query({ method: 'PUT', - url: `${FEATURES_API_URL.services}?action=setAutostart`, - body: { autostart, ids: ids.join(',') }, + url: `${FEATURES_API_URL.services}/${id}?action=setAutostart`, + body: { autostart }, cache: false, }); } + + return Promise.reject({ + ok: false, + error: 'Permission denied', + data: 'Insufficient permissions to perform the action', + } as TReqraftFetchResponse); }, - toggleLoadedCall: async (ids: number[], loaded: boolean) => { + toggleLoadedCall: (ids: number[], loaded: boolean) => { if ( !get().hasPermissions( loaded ? SERVICES_ACTIONS_PERMISSIONS.load : SERVICES_ACTIONS_PERMISSIONS.unload ) ) { - return; + return Promise.reject({ + ok: false, + error: 'Permission denied', + data: 'Insufficient permissions to perform the action', + } as TReqraftFetchResponse); } - await query({ + return query({ method: 'PUT', url: `${FEATURES_API_URL.services}?action=${loaded ? 'load' : 'unload'}`, body: { loaded, ids: ids.join(',') }, cache: false, }); }, - toggleRemote: async (id: string | number) => { + + toggleRemoteCall: (id: string | number) => { const service = get().itemById(id); const permissions = SERVICES_ACTIONS_PERMISSIONS.setRemote; if (!service || !get().hasPermissions(permissions)) { - return; + return Promise.reject({ + ok: false, + error: 'Permission denied', + data: 'Insufficient permissions to perform the action', + } as TReqraftFetchResponse); } - await query({ + return query({ method: 'PUT', url: `${FEATURES_API_URL.services}/${id}?action=setRemote`, body: { remote: !service?.remote }, cache: false, }); }, - reset: async (id: string | number) => { - const service = get().itemById(id); + + resetCall: (ids: number[]) => { const permissions = SERVICES_ACTIONS_PERMISSIONS.reset; - if (!service || !get().hasPermissions(permissions)) { - return; + if (!get().hasPermissions(permissions)) { + return Promise.reject({ + ok: false, + error: 'Permission denied', + data: 'Insufficient permissions to perform the action', + } as TReqraftFetchResponse); } - await query({ + return query({ method: 'PUT', - url: `${FEATURES_API_URL.services}/${id}?action=reset`, + url: `${FEATURES_API_URL.services}?action=reset`, + body: { ids: ids.join(',') }, cache: false, }); }, diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 423571d..31a8659 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -46,6 +46,41 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { setSelected([]); }, }, + { + divider: true, + dividerPadded: 'none', + }, + { + icon: 'ArrowUpLine', + label: 'Load', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.load), + onClick: async () => { + services.toggleLoadedCall(selected, true); + setSelected([]); + }, + }, + { + icon: 'ArrowDownLine', + label: 'Unload', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.unload), + onClick: async () => { + services.toggleLoadedCall(selected, false); + setSelected([]); + }, + }, + { + divider: true, + dividerPadded: 'none', + }, + { + icon: 'HistoryLine', + label: 'Reset', + disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.reset), + onClick: async () => { + services.resetCall(selected); + setSelected([]); + }, + }, ], }, { @@ -97,6 +132,19 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { ), }, }, + { + dataId: 'version', + align: 'center', + header: { + label: 'V', + tooltip: 'Version', + }, + resizable: true, + cell: { + content: 'number', + }, + sortable: true, + }, { dataId: 'type', align: 'center', @@ -117,7 +165,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { align: 'center', width: 100, header: { - label: 'Updated', + label: 'Last Update', }, resizable: true, cell: { @@ -160,7 +208,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { : 'Autostart is disabled, click to enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart), onClick: async () => { - services.toggleAutostartCall([serviceid], !autostart); + services.toggleAutostartCall(serviceid, !autostart); }, }, { @@ -188,7 +236,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { : 'Local service, click to change to remote', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.setRemote), onClick: async () => { - services.toggleRemote(serviceid); + services.toggleRemoteCall(serviceid); }, }, { @@ -198,7 +246,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { tooltip: 'Reset service', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.reset), onClick: async () => { - services.reset(serviceid); + services.resetCall(serviceid); }, }, ], diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index 9a8b35b..1d09083 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,5 +1,7 @@ -import { useMemo } from 'react'; +import { useReqoreProperty } from '@qoretechnologies/reqore'; +import { useCallback, useMemo } from 'react'; import { useEffectOnce } from 'react-use'; +import { isError } from '../../utils/fetch'; import { QorusServicesStore } from './store'; export interface UseQorusServicesConfig { @@ -9,54 +11,70 @@ export interface UseQorusServicesConfig { export const useQorusServices = ({ loadOnMount, }: UseQorusServicesConfig): Partial => { - const { - data, - load, - toggleEnabledCall, - toggleAutostartCall, - toggleLoadedCall, - toggleRemote, - reset, - hasPermissions, - loading, - } = QorusServicesStore(); + const addNotification = useReqoreProperty('addNotification'); + const services = QorusServicesStore(); useEffectOnce(() => { if (loadOnMount) { - load(); + services.load(); } }); const items = useMemo(() => { - return data.map((item) => ({ + return services.data.map((item) => ({ ...item, lastUpdated: item.lastUpdated, _selectId: item.serviceid, })); - }, [data]); + }, [services.data]); + + const toggleEnabledCall: QorusServicesStore['toggleEnabledCall'] = useCallback( + async (ids, enabled) => { + const result = await services.toggleEnabledCall(ids, enabled); + + console.log({ result }); + + if (isError(result)) { + // Check if the call resulted in an error + addNotification({ + type: 'danger', + content: result.data, + title: result.error, + }); + } else { + // Check if the call was successful but some items were not enabled + result.data?.forEach((resultItem) => { + if (enabled && !resultItem.enabled) { + addNotification({ + size: 'small', + type: 'danger', + content: resultItem.info, + title: 'Error enabling service(s)', + }); + } + + if (!enabled && !resultItem.disabled) { + addNotification({ + size: 'small', + type: 'danger', + content: resultItem.info, + title: 'Error disabling service(s)', + }); + } + }); + } + + return result; + }, + [services.toggleEnabledCall] + ); return useMemo( () => ({ + ...services, data: items, - load, toggleEnabledCall, - toggleAutostartCall, - toggleLoadedCall, - hasPermissions, - toggleRemote, - reset, - loading, }), - [ - items, - load, - loading, - toggleEnabledCall, - toggleAutostartCall, - toggleLoadedCall, - toggleRemote, - reset, - hasPermissions, - ] + [services, items, toggleEnabledCall] ); }; diff --git a/src/hooks/useFetch/useFetch.tsx b/src/hooks/useFetch/useFetch.tsx index 7293f60..75d5da6 100644 --- a/src/hooks/useFetch/useFetch.tsx +++ b/src/hooks/useFetch/useFetch.tsx @@ -13,7 +13,7 @@ export interface IReqraftUseFetch { errorData?: any; } -export interface IReqraftUseFetchOptions extends IReqraftQueryConfig { +export interface IReqraftUseFetchOptions extends IReqraftQueryConfig { defaultData?: T; loadOnMount?: boolean; } diff --git a/src/providers/StorageProvider.tsx b/src/providers/StorageProvider.tsx index aa93e90..c371fb7 100644 --- a/src/providers/StorageProvider.tsx +++ b/src/providers/StorageProvider.tsx @@ -14,7 +14,7 @@ export interface IReqraftStorageProviderProps children: ReactNode; } -export const ReqraftUserProvider = ({ children, waitForStorage }: IReqraftStorageProviderProps) => { +export const ReqraftUserProvider = ({ children }: IReqraftStorageProviderProps) => { const appName = useReqraftProperty('appName'); const { currentUser, @@ -80,7 +80,9 @@ export const ReqraftUserProvider = ({ children, waitForStorage }: IReqraftStorag [currentUser?.storage, getStorage, updateStorage, removeStorageValue] ); - if (loading && waitForStorage) { + console.log(contextValue, loading); + + if (loading) { return null; } diff --git a/src/stores/currentUser/currentUser.tsx b/src/stores/currentUser/currentUser.tsx index 642da97..184661d 100644 --- a/src/stores/currentUser/currentUser.tsx +++ b/src/stores/currentUser/currentUser.tsx @@ -37,7 +37,7 @@ export interface ICurrentUserStore { export const currentUserStore = create((set, get) => ({ currentUser: undefined, - loading: false, + loading: true, error: undefined, load: async () => { set({ loading: true }); @@ -55,7 +55,7 @@ export const currentUserStore = create((set, get) => ({ return Promise.reject(response.error); } - set({ currentUser: response.data, loading: false, errorData: undefined, error: undefined }); + set({ currentUser: response.data, errorData: undefined, error: undefined }); // Connect to API events get().connectToApiEvents(); @@ -67,7 +67,7 @@ export const currentUserStore = create((set, get) => ({ url: `apievents`, }); - set({ apiEvents: socket }); + set({ apiEvents: socket, loading: false }); }, hasAnyPermission: (permissions) => { if (!get().currentUser) { diff --git a/yarn.lock b/yarn.lock index 3d2efdc..18dde41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,10 +1682,10 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@qoretechnologies/reqore@^0.53.9": - version "0.53.9" - resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.9.tgz#bc4f519d57d571c72bdeae10d5bb7a455ae6652f" - integrity sha512-vg1O7nvqxGfXITTSGQ0RQkMdoG9LHFPhhzCBpQ4hgKowIUY/jHs6JYQuDWifTh8+taW3Ljzkp8CcUrspFu5sSQ== +"@qoretechnologies/reqore@^0.53.10": + version "0.53.10" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.10.tgz#ef70a8b5f2ff65d18ea4ea6e3b5f8e347ae1feb6" + integrity sha512-QO9Lw0mfryb8YxtGiRGAL/HZjOw09dxpUdM+YdrPCfGVncZbNqFhUG9+yQh3D94Q+lFGKflG2nTgQuUPV2YTgQ== dependencies: "@internationalized/date" "^3.5.3" "@popperjs/core" "^2.11.6" From cf841a695b5644f9b68d77dfe5a1ddc1ce16a662 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Thu, 8 May 2025 23:48:41 +0200 Subject: [PATCH 08/10] Move the mock server to the preview, so there is always just one apievents mock socket available at all times --- .storybook/preview.tsx | 17 ++++++++ __tests__/services/api.ts | 4 +- src/features/services/Services.stories.tsx | 47 ---------------------- 3 files changed, 19 insertions(+), 49 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 50e0b54..cbb209f 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,5 +1,6 @@ import withMockdate from '@netsells/storybook-mockdate'; import { ReqoreContent, ReqoreLayoutContent, ReqoreUIProvider } from '@qoretechnologies/reqore'; +import { Client, Server } from 'mock-socket'; import { initializeReqraft } from '../src'; export const parameters = { @@ -43,6 +44,22 @@ export const argTypes = { }, }; +export let ApiEventsWebSocket: Client; + +const url = `wss://hq.qoretechnologies.com:8092/apievents?token=${process.env.REACT_APP_QORUS_TOKEN}`; +let server = new Server(url); + +server.on('connection', (socket) => { + ApiEventsWebSocket = socket; + + socket.on('message', (data) => { + if (data === 'ping') { + socket.send('pong'); + return; + } + }); +}); + export const decorators = [ withMockdate, (Story, context) => { diff --git a/__tests__/services/api.ts b/__tests__/services/api.ts index 5ebf7ef..c16eb43 100644 --- a/__tests__/services/api.ts +++ b/__tests__/services/api.ts @@ -1,8 +1,8 @@ import { QorusService } from '@qoretechnologies/ts-toolkit'; import { size } from 'lodash'; +import { ApiEventsWebSocket } from '../../.storybook/preview'; import { QorusServiceEnableCallResponse } from '../../src/features/services/api'; import { QorusServiceEnableEventInfo } from '../../src/features/services/events'; -import { ServicesSocket } from '../../src/features/services/Services.stories'; import { MockServicesData } from './data'; export const GetServices = { @@ -50,7 +50,7 @@ export const ToggleEnableServices = { }, })); - ServicesSocket.send(JSON.stringify(responseEvents)); + ApiEventsWebSocket.send(JSON.stringify(responseEvents)); return affectedServices.map((service) => ({ arg: 'any', diff --git a/src/features/services/Services.stories.tsx b/src/features/services/Services.stories.tsx index 6fbd6b5..18a6095 100644 --- a/src/features/services/Services.stories.tsx +++ b/src/features/services/Services.stories.tsx @@ -1,63 +1,16 @@ import { StoryObj } from '@storybook/react'; import { expect, fireEvent } from '@storybook/test'; -import { Client, Server } from 'mock-socket'; import { GetServices, ToggleEnableServices } from '../../../__tests__/services/api'; import { sleep, testsClickButton, testsWaitForText } from '../../../__tests__/utils'; import { StoryMeta } from '../../types'; import { QorusServicesTable } from './table'; -export let ServicesSocket: Client; - const meta = { title: 'Features/Services', excludeStories: ['ServicesSocket'], render: () => { return ; }, - async beforeEach() { - const url = `wss://hq.qoretechnologies.com:8092/apievents?token=${process.env.REACT_APP_QORUS_TOKEN}`; - let server = new Server(url); - let killTimeout: NodeJS.Timeout; - - server.on('connection', (socket) => { - ServicesSocket = socket; - - if (killTimeout) { - server.close(); - return; - } - - socket.on('message', (data) => { - if (data === 'ping') { - socket.send('pong'); - return; - } - - if (data === 'kill') { - server.close(); - - killTimeout = setTimeout(() => { - server = new Server(url); - killTimeout = null; - }, 3000); - - return; - } - - socket.send(`Received message: ${data}`); - }); - }); - - return () => { - killTimeout && clearTimeout(killTimeout); - killTimeout = null; - server.close({ - code: 4999, - reason: 'Test ended', - wasClean: true, - }); - }; - }, } as StoryMeta; export default meta; From a7fd594f8736e1c9077ca4207ce5e50a1c6888f6 Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Fri, 9 May 2025 09:17:51 +0200 Subject: [PATCH 09/10] feat: enhance service management with error handling, notification support, and WebSocket improvements --- .storybook/preview.tsx | 22 ++++--- src/features/services/store.tsx | 40 ++++++++---- src/features/services/table.tsx | 6 +- src/features/services/useServices.ts | 95 +++++++++++++++------------- src/providers/StorageProvider.tsx | 2 - src/utils/fetch.ts | 2 +- 6 files changed, 95 insertions(+), 72 deletions(-) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index cbb209f..fb5350e 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -47,18 +47,22 @@ export const argTypes = { export let ApiEventsWebSocket: Client; const url = `wss://hq.qoretechnologies.com:8092/apievents?token=${process.env.REACT_APP_QORUS_TOKEN}`; -let server = new Server(url); +try { + let server = new Server(url); -server.on('connection', (socket) => { - ApiEventsWebSocket = socket; + server.on('connection', (socket) => { + ApiEventsWebSocket = socket; - socket.on('message', (data) => { - if (data === 'ping') { - socket.send('pong'); - return; - } + socket.on('message', (data) => { + if (data === 'ping') { + socket.send('pong'); + return; + } + }); }); -}); +} catch (e) { + console.warn('WebSocket server threw:', e.message); +} export const decorators = [ withMockdate, diff --git a/src/features/services/store.tsx b/src/features/services/store.tsx index df0e6fa..3ed0523 100644 --- a/src/features/services/store.tsx +++ b/src/features/services/store.tsx @@ -1,7 +1,7 @@ import { QorusService } from '@qoretechnologies/ts-toolkit'; import { create } from 'zustand'; import { currentUserStore } from '../../stores/currentUser/currentUser'; -import { query, TReqraftFetchResponse } from '../../utils/fetch'; +import { IReqraftQueryConfig, query, TReqraftFetchResponse } from '../../utils/fetch'; import {} from '../../utils/websocket'; import { FEATURES_API_URL, QorusFeatureStore } from '../constants'; import { QorusApiEvent, QorusGlobalEvents } from '../events'; @@ -13,18 +13,27 @@ import { QorusServiceEvents } from './events'; export interface QorusServicesStore extends QorusFeatureStore { toggleEnabledCall: ( ids: number[], - enabled: true | false + enabled: true | false, + options?: Partial> ) => Promise>; toggleAutostartCall: ( ids: number, - autostart: true | false + autostart: true | false, + options?: Partial> ) => Promise>; toggleLoadedCall: ( ids: number[], - loaded: true | false + loaded: true | false, + options?: Partial> + ) => Promise>; + resetCall: ( + id: number[], + options?: Partial> + ) => Promise>; + toggleRemoteCall: ( + id: string | number, + options?: Partial> ) => Promise>; - resetCall: (id: number[]) => Promise>; - toggleRemoteCall: (id: string | number) => Promise>; } export const QorusServicesStore = create((set, get) => ({ @@ -74,12 +83,13 @@ export const QorusServicesStore = create((set, get) => ({ }, toggleEnabledCall: ( - ids: number[], - enabled: true | false + ids, + enabled, + options ): Promise> => { if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled)) { - console.log({ ids, enabled }); return query({ + ...options, method: 'PUT', url: `${FEATURES_API_URL.services}?action=${enabled ? 'enable' : 'disable'}`, body: { ids: ids.join(',') }, @@ -94,9 +104,10 @@ export const QorusServicesStore = create((set, get) => ({ }); }, - toggleAutostartCall: (id: number, autostart: boolean) => { + toggleAutostartCall: (id, autostart, options) => { if (get().hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleAutostart)) { return query({ + ...options, method: 'PUT', url: `${FEATURES_API_URL.services}/${id}?action=setAutostart`, body: { autostart }, @@ -111,7 +122,7 @@ export const QorusServicesStore = create((set, get) => ({ } as TReqraftFetchResponse); }, - toggleLoadedCall: (ids: number[], loaded: boolean) => { + toggleLoadedCall: (ids, loaded, options) => { if ( !get().hasPermissions( loaded ? SERVICES_ACTIONS_PERMISSIONS.load : SERVICES_ACTIONS_PERMISSIONS.unload @@ -125,6 +136,7 @@ export const QorusServicesStore = create((set, get) => ({ } return query({ + ...options, method: 'PUT', url: `${FEATURES_API_URL.services}?action=${loaded ? 'load' : 'unload'}`, body: { loaded, ids: ids.join(',') }, @@ -132,7 +144,7 @@ export const QorusServicesStore = create((set, get) => ({ }); }, - toggleRemoteCall: (id: string | number) => { + toggleRemoteCall: (id, options) => { const service = get().itemById(id); const permissions = SERVICES_ACTIONS_PERMISSIONS.setRemote; @@ -145,6 +157,7 @@ export const QorusServicesStore = create((set, get) => ({ } return query({ + ...options, method: 'PUT', url: `${FEATURES_API_URL.services}/${id}?action=setRemote`, body: { remote: !service?.remote }, @@ -152,7 +165,7 @@ export const QorusServicesStore = create((set, get) => ({ }); }, - resetCall: (ids: number[]) => { + resetCall: (ids, options) => { const permissions = SERVICES_ACTIONS_PERMISSIONS.reset; if (!get().hasPermissions(permissions)) { @@ -164,6 +177,7 @@ export const QorusServicesStore = create((set, get) => ({ } return query({ + ...options, method: 'PUT', url: `${FEATURES_API_URL.services}?action=reset`, body: { ids: ids.join(',') }, diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 31a8659..0769a26 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -33,7 +33,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { label: 'Enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), onClick: async () => { - services.toggleEnabledCall(selected, true); + services.toggleEnabledWithNotification(selected, true); setSelected([]); }, }, @@ -42,7 +42,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { label: 'Disable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), onClick: async () => { - services.toggleEnabledCall(selected, false); + services.toggleEnabledWithNotification(selected, false); setSelected([]); }, }, @@ -195,7 +195,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { tooltip: enabled ? 'Enabled, click to disable' : 'Disabled, click to enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), onClick: async () => { - services.toggleEnabledCall([serviceid], !enabled); + services.toggleEnabledWithNotification([serviceid], !enabled); }, }, { diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index 1d09083..a34d9d3 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,80 +1,87 @@ import { useReqoreProperty } from '@qoretechnologies/reqore'; import { useCallback, useMemo } from 'react'; import { useEffectOnce } from 'react-use'; -import { isError } from '../../utils/fetch'; +import { IReqraftFetchErrorResponse, isError } from '../../utils/fetch'; import { QorusServicesStore } from './store'; export interface UseQorusServicesConfig { loadOnMount?: boolean; } +export interface UseQorusServicesResult extends Partial { + toggleEnabledWithNotification: QorusServicesStore['toggleEnabledCall']; +} + export const useQorusServices = ({ loadOnMount, -}: UseQorusServicesConfig): Partial => { +}: UseQorusServicesConfig): UseQorusServicesResult => { const addNotification = useReqoreProperty('addNotification'); - const services = QorusServicesStore(); + const { toggleEnabledCall, load, data, ...rest } = QorusServicesStore(); useEffectOnce(() => { if (loadOnMount) { - services.load(); + load(); } }); const items = useMemo(() => { - return services.data.map((item) => ({ + return data.map((item) => ({ ...item, lastUpdated: item.lastUpdated, _selectId: item.serviceid, })); - }, [services.data]); + }, [data]); - const toggleEnabledCall: QorusServicesStore['toggleEnabledCall'] = useCallback( - async (ids, enabled) => { - const result = await services.toggleEnabledCall(ids, enabled); + const handleCallError = useCallback((result: IReqraftFetchErrorResponse) => { + if (isError(result)) { + addNotification({ + type: 'danger', + content: result.data, + title: result.error, + opaque: false, + }); + } + }, []); - console.log({ result }); + const toggleEnabledWithNotification: UseQorusServicesResult['toggleEnabledWithNotification'] = + useCallback( + async (ids, enabled) => { + const result = await toggleEnabledCall(ids, enabled, { + onError: handleCallError, + onSuccess: ({ data }) => { + data?.forEach((resultItem) => { + if (enabled && !resultItem.enabled) { + addNotification({ + size: 'small', + type: 'danger', + content: resultItem.info, + title: 'Error enabling service(s)', + }); + } - if (isError(result)) { - // Check if the call resulted in an error - addNotification({ - type: 'danger', - content: result.data, - title: result.error, - }); - } else { - // Check if the call was successful but some items were not enabled - result.data?.forEach((resultItem) => { - if (enabled && !resultItem.enabled) { - addNotification({ - size: 'small', - type: 'danger', - content: resultItem.info, - title: 'Error enabling service(s)', + if (!enabled && !resultItem.disabled) { + addNotification({ + size: 'small', + type: 'danger', + content: resultItem.info, + title: 'Error disabling service(s)', + }); + } }); - } - - if (!enabled && !resultItem.disabled) { - addNotification({ - size: 'small', - type: 'danger', - content: resultItem.info, - title: 'Error disabling service(s)', - }); - } + }, }); - } - return result; - }, - [services.toggleEnabledCall] - ); + return result; + }, + [toggleEnabledCall] + ); return useMemo( () => ({ - ...services, + ...rest, data: items, - toggleEnabledCall, + toggleEnabledWithNotification, }), - [services, items, toggleEnabledCall] + [rest, items, toggleEnabledWithNotification] ); }; diff --git a/src/providers/StorageProvider.tsx b/src/providers/StorageProvider.tsx index c371fb7..a82485b 100644 --- a/src/providers/StorageProvider.tsx +++ b/src/providers/StorageProvider.tsx @@ -80,8 +80,6 @@ export const ReqraftUserProvider = ({ children }: IReqraftStorageProviderProps) [currentUser?.storage, getStorage, updateStorage, removeStorageValue] ); - console.log(contextValue, loading); - if (loading) { return null; } diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 43958e2..055cece 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -17,7 +17,7 @@ export interface IReqraftFetchOkResponse { response: Response; } -export interface IReqraftFetchErrorResponse { +export interface IReqraftFetchErrorResponse { ok: false; data: E; From d49afb77b555b591f0829c517037a3a22dea897c Mon Sep 17 00:00:00 2001 From: foxhoundn Date: Mon, 19 May 2025 14:56:33 +0200 Subject: [PATCH 10/10] feat: enhance service toggle functionality with notifications and refactor event handling --- __tests__/services/api.ts | 74 +++++++++++++--------- package.json | 2 +- src/features/services/Services.stories.tsx | 13 ++++ src/features/services/table.tsx | 2 +- src/features/services/useServices.ts | 39 +++++++++++- yarn.lock | 8 +-- 6 files changed, 100 insertions(+), 38 deletions(-) diff --git a/__tests__/services/api.ts b/__tests__/services/api.ts index c16eb43..1554d2e 100644 --- a/__tests__/services/api.ts +++ b/__tests__/services/api.ts @@ -20,45 +20,61 @@ export const ToggleEnableServices = { const { searchParams, body } = request; const { ids } = JSON.parse(body); const affectedServices = MockServicesData.filter((service) => ids.includes(service.serviceid)); - const action = searchParams.action; - const key = action === 'enable' ? 'enabled' : 'disabled'; - const getResponseData = (service: QorusService): Partial => - action === 'enable' - ? { + const getResponseData = (service: QorusService) => { + switch (action) { + case 'enable': + return { info: size(service.alerts) ? `Service ${service.serviceid} was NOT enabled` : `Service ${service.serviceid} enabled`, - [key]: size(service.alerts) ? false : true, - } - : { + enabled: size(service.alerts) ? false : true, + } satisfies Partial; + case 'disable': + return { info: `Service ${service.serviceid} was disabled`, - [key]: true, - }; + disabled: true, + } satisfies Partial; + } + }; + + let responseEvents: unknown; + let result: unknown; - const responseEvents: Partial[] = affectedServices - .filter((service) => service.serviceid !== 3) - .map((service) => ({ - eventstr: 'GROUP_STATUS_CHANGED', - info: { - id: service.serviceid, - enabled: action === 'enable' ? (size(service.alerts) ? false : true) : false, + switch (action) { + case 'enable': + case 'disable': { + responseEvents = affectedServices + .filter((service) => (action === 'enable' ? !size(service.alerts) : true)) + .map((service) => ({ + eventstr: 'GROUP_STATUS_CHANGED', + info: { + id: service.serviceid, + enabled: action === 'enable' ? (size(service.alerts) ? false : true) : false, + name: service.name, + synthetic: false, + type: 'service', + }, + })) satisfies Partial[]; + + result = affectedServices.map((service) => ({ + arg: 'any', + serviceid: service.serviceid, name: service.name, - synthetic: false, - type: 'service', - }, - })); + type: service.type, + version: service.version, + ...getResponseData(service), + })) satisfies QorusServiceEnableCallResponse[]; + + break; + } + } + // Send events to the WebSocket ApiEventsWebSocket.send(JSON.stringify(responseEvents)); - return affectedServices.map((service) => ({ - arg: 'any', - serviceid: service.serviceid, - name: service.name, - type: service.type, - version: service.version, - ...getResponseData(service), - })) as QorusServiceEnableCallResponse[]; + // Return the result (this is a 200 response with the data) + return result; }, }; diff --git a/package.json b/package.json index e8a761b..fcc5ed0 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@babel/preset-typescript": "^7.12.7", "@chromatic-com/storybook": "^2.0.2", "@netsells/storybook-mockdate": "^0.3.3", - "@qoretechnologies/reqore": "^0.53.10", + "@qoretechnologies/reqore": "^0.53.12", "@qoretechnologies/ts-toolkit": "^0.5.31", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", diff --git a/src/features/services/Services.stories.tsx b/src/features/services/Services.stories.tsx index 18a6095..a4809ea 100644 --- a/src/features/services/Services.stories.tsx +++ b/src/features/services/Services.stories.tsx @@ -45,3 +45,16 @@ export const ServicesCanBeSelected: Story = { await testsWaitForText('Reset'); }, }; + +export const ServicesCanBeEnabledAndDisabled: Story = { + ...ServicesCanBeSelected, + play: async (args) => { + await ServicesCanBeSelected.play(args); + await testsClickButton({ label: 'Disable' }); + await testsWaitForText('Operation completed successfully!'); + await fireEvent.click(document.querySelectorAll('.qorus-service-enable-toggle')[2]); + await testsWaitForText('Operation completed successfully!'); + await fireEvent.click(document.querySelectorAll('.qorus-service-enable-toggle')[1]); + await testsWaitForText('Operation completed successfully!'); + }, +}; diff --git a/src/features/services/table.tsx b/src/features/services/table.tsx index 0769a26..14d5d31 100644 --- a/src/features/services/table.tsx +++ b/src/features/services/table.tsx @@ -139,7 +139,6 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { label: 'V', tooltip: 'Version', }, - resizable: true, cell: { content: 'number', }, @@ -192,6 +191,7 @@ export const QorusServicesTable = ({}: QorusServiceTableProps) => { compact: true, intent: enabled ? 'info' : undefined, minimal: true, + className: 'qorus-service-enable-toggle', tooltip: enabled ? 'Enabled, click to disable' : 'Disabled, click to enable', disabled: !services.hasPermissions(SERVICES_ACTIONS_PERMISSIONS.toggleEnabled), onClick: async () => { diff --git a/src/features/services/useServices.ts b/src/features/services/useServices.ts index a34d9d3..25542da 100644 --- a/src/features/services/useServices.ts +++ b/src/features/services/useServices.ts @@ -1,6 +1,7 @@ import { useReqoreProperty } from '@qoretechnologies/reqore'; import { useCallback, useMemo } from 'react'; import { useEffectOnce } from 'react-use'; +import shortid from 'shortid'; import { IReqraftFetchErrorResponse, isError } from '../../utils/fetch'; import { QorusServicesStore } from './store'; @@ -32,23 +33,45 @@ export const useQorusServices = ({ })); }, [data]); - const handleCallError = useCallback((result: IReqraftFetchErrorResponse) => { + const handleCallError = useCallback((result: IReqraftFetchErrorResponse, callId?: string) => { if (isError(result)) { addNotification({ type: 'danger', content: result.data, title: result.error, - opaque: false, + id: callId, }); } }, []); + const handleCallBefore = useCallback((callId?: string) => { + addNotification({ + type: 'pending', + content: 'Working on it...', + duration: 10000, + id: callId, + }); + }, []); + + const handleCallSuccess = useCallback((callId?: string) => { + addNotification({ + type: 'success', + content: 'Operation completed successfully!', + id: callId, + duration: 2000, + }); + }, []); + const toggleEnabledWithNotification: UseQorusServicesResult['toggleEnabledWithNotification'] = useCallback( async (ids, enabled) => { + const id = shortid.generate(); const result = await toggleEnabledCall(ids, enabled, { - onError: handleCallError, + onBefore: () => handleCallBefore(id), + onError: (result) => handleCallError(result, id), onSuccess: ({ data }) => { + let success = true; + data?.forEach((resultItem) => { if (enabled && !resultItem.enabled) { addNotification({ @@ -56,7 +79,10 @@ export const useQorusServices = ({ type: 'danger', content: resultItem.info, title: 'Error enabling service(s)', + id, }); + + success = false; } if (!enabled && !resultItem.disabled) { @@ -65,9 +91,16 @@ export const useQorusServices = ({ type: 'danger', content: resultItem.info, title: 'Error disabling service(s)', + id, }); + + success = false; } }); + + if (success) { + handleCallSuccess(id); + } }, }); diff --git a/yarn.lock b/yarn.lock index 18dde41..3816890 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1682,10 +1682,10 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@qoretechnologies/reqore@^0.53.10": - version "0.53.10" - resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.10.tgz#ef70a8b5f2ff65d18ea4ea6e3b5f8e347ae1feb6" - integrity sha512-QO9Lw0mfryb8YxtGiRGAL/HZjOw09dxpUdM+YdrPCfGVncZbNqFhUG9+yQh3D94Q+lFGKflG2nTgQuUPV2YTgQ== +"@qoretechnologies/reqore@^0.53.12": + version "0.53.12" + resolved "https://registry.yarnpkg.com/@qoretechnologies/reqore/-/reqore-0.53.12.tgz#220f08325b803cafe5414bc0788df9bd43d40070" + integrity sha512-YIqAAKqCXtU76FXE6HnQL+1hkyR0GN7aaH8pQfnV1rqUS+V3rf7lXIm1OuWDtAoZAQflqFEaVY5OFmEYfw2Ylw== dependencies: "@internationalized/date" "^3.5.3" "@popperjs/core" "^2.11.6"