From cb6b31c9e43643a981a0ff4e7488911adff790da Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 4 Mar 2025 09:57:17 +0100 Subject: [PATCH 01/41] Add release file notifications to toolkit --- packages/toolkit/src/repository/repository.ts | 3 ++- packages/types/src/notifications.ts | 22 +++++++++++++++++++ packages/types/src/pkg.ts | 2 ++ packages/types/src/releaseFiles.ts | 10 ++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/notifications.ts diff --git a/packages/toolkit/src/repository/repository.ts b/packages/toolkit/src/repository/repository.ts index a57c3cf632..7d204faf78 100644 --- a/packages/toolkit/src/repository/repository.ts +++ b/packages/toolkit/src/repository/repository.ts @@ -205,7 +205,8 @@ export class DappnodeRepository extends ApmRepository { disclaimer: await this.getPkgAsset(releaseFilesToDownload.disclaimer, ipfsEntries), gettingStarted: await this.getPkgAsset(releaseFilesToDownload.gettingStarted, ipfsEntries), prometheusTargets: await this.getPkgAsset(releaseFilesToDownload.prometheusTargets, ipfsEntries), - grafanaDashboards: await this.getPkgAsset(releaseFilesToDownload.grafanaDashboards, ipfsEntries) + grafanaDashboards: await this.getPkgAsset(releaseFilesToDownload.grafanaDashboards, ipfsEntries), + notifications: await this.getPkgAsset(releaseFilesToDownload.notifications, ipfsEntries) }; } diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts new file mode 100644 index 0000000000..fdd3a04a79 --- /dev/null +++ b/packages/types/src/notifications.ts @@ -0,0 +1,22 @@ +export interface GatusConfig { + endpoints: Endpoint[]; +} + +interface Endpoint { + name: string; + enabled: boolean; + url: string; + method: string; + conditions: string[]; + interval: string; // e.g., "1m" + group: string; + alerts: Alert[]; +} + +interface Alert { + type: string; + "failure-threshold": number; + "success-threshold": number; + "send-on-resolved": boolean; + description: string; +} diff --git a/packages/types/src/pkg.ts b/packages/types/src/pkg.ts index dd74b29166..16d9d6a973 100644 --- a/packages/types/src/pkg.ts +++ b/packages/types/src/pkg.ts @@ -1,5 +1,6 @@ import { Compose } from "./compose.js"; import { Manifest, PrometheusTarget, GrafanaDashboard } from "./manifest.js"; +import { GatusConfig } from "./notifications.js"; import { SetupWizard } from "./setupWizard.js"; /** @@ -97,6 +98,7 @@ export type DirectoryFiles = { gettingStarted?: string; prometheusTargets?: PrometheusTarget[]; grafanaDashboards?: GrafanaDashboard[]; + notifications?: GatusConfig; }; export interface FileConfig { diff --git a/packages/types/src/releaseFiles.ts b/packages/types/src/releaseFiles.ts index 2c4d6df4b5..074f9183e9 100644 --- a/packages/types/src/releaseFiles.ts +++ b/packages/types/src/releaseFiles.ts @@ -84,6 +84,13 @@ export const releaseFiles = Object.freeze({ maxSize: 10e6, // ~ 10MB required: false as const, multiple: true as const + }), + notifications: Object.freeze({ + regex: /.*notifications.yaml$/, + format: FileFormat.YAML, + maxSize: 10e3, + required: false as const, + multiple: false as const }) } as const); @@ -95,5 +102,6 @@ export const releaseFilesToDownload = { disclaimer: releaseFiles.disclaimer, gettingStarted: releaseFiles.gettingStarted, prometheusTargets: releaseFiles.prometheusTargets, - grafanaDashboards: releaseFiles.grafanaDashboards + grafanaDashboards: releaseFiles.grafanaDashboards, + notifications: releaseFiles.notifications }; From 530a4a9fd82172c6f8906216e399b620592f8a0d Mon Sep 17 00:00:00 2001 From: pablomendezroyo Date: Tue, 4 Mar 2025 10:01:10 +0100 Subject: [PATCH 02/41] join notifications in manifest --- packages/installer/src/dappnodeInstaller.ts | 9 +++++++-- packages/types/src/manifest.ts | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/installer/src/dappnodeInstaller.ts b/packages/installer/src/dappnodeInstaller.ts index 40aa987a35..d2d2ce0ce0 100644 --- a/packages/installer/src/dappnodeInstaller.ts +++ b/packages/installer/src/dappnodeInstaller.ts @@ -23,6 +23,7 @@ import { parseTimeoutSeconds } from "./utils.js"; import { getEthersProvider } from "./ethClient/index.js"; import { omit } from "lodash-es"; import { ethers } from "ethers"; +import { GatusConfig } from "../../types/dist/notifications.js"; /** * Returns the ipfsUrl to initialize the ipfs instance @@ -72,7 +73,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer: pkgRelease.disclaimer, gettingStarted: pkgRelease.gettingStarted, grafanaDashboards: pkgRelease.grafanaDashboards, - prometheusTargets: pkgRelease.prometheusTargets + prometheusTargets: pkgRelease.prometheusTargets, + notifications: pkgRelease.notifications }); // set compose to custom dappnode compose in release @@ -151,7 +153,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer, gettingStarted, prometheusTargets, - grafanaDashboards + grafanaDashboards, + notifications }: { manifest: Manifest; SetupWizard?: SetupWizard; @@ -159,12 +162,14 @@ export class DappnodeInstaller extends DappnodeRepository { gettingStarted?: string; prometheusTargets?: PrometheusTarget[]; grafanaDashboards?: GrafanaDashboard[]; + notifications?: GatusConfig; }): Manifest { if (SetupWizard) manifest.setupWizard = SetupWizard; if (disclaimer) manifest.disclaimer = { message: disclaimer }; if (gettingStarted) manifest.gettingStarted = gettingStarted; if (prometheusTargets) manifest.prometheusTargets = prometheusTargets; if (grafanaDashboards && grafanaDashboards.length > 0) manifest.grafanaDashboards = grafanaDashboards; + if (notifications) manifest.notifications = notifications; return manifest; } diff --git a/packages/types/src/manifest.ts b/packages/types/src/manifest.ts index f81ed8acdb..74e9042299 100644 --- a/packages/types/src/manifest.ts +++ b/packages/types/src/manifest.ts @@ -1,3 +1,4 @@ +import { GatusConfig } from "./notifications.js"; import { SetupSchema, SetupTarget, SetupUiJson, SetupWizard } from "./setupWizard.js"; export interface Manifest { @@ -98,6 +99,9 @@ export interface Manifest { // setupWizard for compacted manifests in core packages setupWizard?: SetupWizard; + + // notifications + notifications?: GatusConfig; } export interface UpstreamItem { From ec6dbc9be833859e3cc75818c4dfc892175fea19 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Tue, 4 Mar 2025 10:30:29 +0100 Subject: [PATCH 03/41] fix unit test --- packages/installer/test/unit/release/findEntries.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/installer/test/unit/release/findEntries.test.ts b/packages/installer/test/unit/release/findEntries.test.ts index c8a27f3b19..649e374acf 100644 --- a/packages/installer/test/unit/release/findEntries.test.ts +++ b/packages/installer/test/unit/release/findEntries.test.ts @@ -54,7 +54,8 @@ describe("validateTarImage", () => { "host-grafana-dashboard.json", "prometheus-targets.json", "setup-wizard.json", - "signature.json" + "signature.json", + "notifications.yaml" ].map((name) => ({ name, path: `Qm-root/${name}`, @@ -70,6 +71,7 @@ describe("validateTarImage", () => { disclaimer: "disclaimer.md", gettingStarted: "getting-started.md", prometheusTargets: "prometheus-targets.json", + notifications: "notifications.yaml", grafanaDashboards: ["docker-grafana-dashboard.json", "host-grafana-dashboard.json"] }; From c44789f0105f5ec86da216cd233c6dec129ab479 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Wed, 5 Mar 2025 09:13:06 +0100 Subject: [PATCH 04/41] use with instead of assert --- packages/common/src/validation/index.ts | 6 +++--- .../src/api/middlewares/ethForward/resolveDomain.ts | 4 ++-- packages/dockerCompose/src/validate.ts | 2 +- packages/schemas/src/validateManifestSchema.ts | 2 +- packages/schemas/src/validateSetupWizardSchema.ts | 2 +- packages/types/src/releaseFiles.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/common/src/validation/index.ts b/packages/common/src/validation/index.ts index c1bbd27c43..fef11b3e0a 100644 --- a/packages/common/src/validation/index.ts +++ b/packages/common/src/validation/index.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; -import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" assert { type: "json" }; -import routesReturnSchema from "./schemas/RoutesReturn.schema.json" assert { type: "json" }; -import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" assert { type: "json" }; +import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" with { type: "json" }; +import routesReturnSchema from "./schemas/RoutesReturn.schema.json" with { type: "json" }; +import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" with { type: "json" }; import { Args } from "typescript-json-schema"; const ajv = new Ajv({ allErrors: true, strict: false }); diff --git a/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts b/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts index ecc1c62f55..ab50670574 100644 --- a/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts +++ b/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts @@ -1,7 +1,7 @@ import { ethers } from "ethers"; import { getEthersProvider, getEthUrl } from "@dappnode/installer"; -import resolverAbi from "./abi/resolverAbi.json" assert { type: "json" }; -import ensAbi from "./abi/ens.json" assert { type: "json" }; +import resolverAbi from "./abi/resolverAbi.json" with { type: "json" }; +import ensAbi from "./abi/ens.json" with { type: "json" }; import { Network, Content, NotFoundError, EnsResolverError } from "./types.js"; import { decodeContentHash, isEmpty, decodeDnsLink, decodeContent } from "./utils/index.js"; import memoize from "memoizee"; diff --git a/packages/dockerCompose/src/validate.ts b/packages/dockerCompose/src/validate.ts index 4abd66771b..640f60615b 100644 --- a/packages/dockerCompose/src/validate.ts +++ b/packages/dockerCompose/src/validate.ts @@ -1,5 +1,5 @@ import { getSchemaValidator } from "@dappnode/utils"; -import compose3xSchema from "./compose_v3x.schema.json" assert { type: "json" }; +import compose3xSchema from "./compose_v3x.schema.json" with { type: "json" }; import { Compose } from "@dappnode/types"; /** diff --git a/packages/schemas/src/validateManifestSchema.ts b/packages/schemas/src/validateManifestSchema.ts index 0e18a052b8..561044d288 100644 --- a/packages/schemas/src/validateManifestSchema.ts +++ b/packages/schemas/src/validateManifestSchema.ts @@ -1,7 +1,7 @@ import { ajv } from "./ajv.js"; import { CliError } from "./error.js"; import { processError } from "./utils.js"; -import manifestSchema from "./schemas/manifest.schema.json" assert { type: "json" }; +import manifestSchema from "./schemas/manifest.schema.json" with { type: "json" }; import { Manifest } from "@dappnode/types"; /** diff --git a/packages/schemas/src/validateSetupWizardSchema.ts b/packages/schemas/src/validateSetupWizardSchema.ts index 9e59a215e3..e1f07147c0 100644 --- a/packages/schemas/src/validateSetupWizardSchema.ts +++ b/packages/schemas/src/validateSetupWizardSchema.ts @@ -1,6 +1,6 @@ import { ajv } from "./ajv.js"; import { processError } from "./utils.js"; -import setupWizardSchema from "./schemas/setup-wizard.schema.json" assert { type: "json" }; +import setupWizardSchema from "./schemas/setup-wizard.schema.json" with { type: "json" }; import { CliError } from "./error.js"; import { SetupWizard } from "@dappnode/types"; diff --git a/packages/types/src/releaseFiles.ts b/packages/types/src/releaseFiles.ts index 074f9183e9..af7df593fd 100644 --- a/packages/types/src/releaseFiles.ts +++ b/packages/types/src/releaseFiles.ts @@ -86,7 +86,7 @@ export const releaseFiles = Object.freeze({ multiple: true as const }), notifications: Object.freeze({ - regex: /.*notifications.yaml$/, + regex: /^.*notifications\.yaml$/, format: FileFormat.YAML, maxSize: 10e3, required: false as const, From 1b3d0d867aec9860c08018c7d4d7a3014426410e Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Wed, 5 Mar 2025 09:14:17 +0100 Subject: [PATCH 05/41] return missing notifications --- packages/installer/src/dappnodeInstaller.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/installer/src/dappnodeInstaller.ts b/packages/installer/src/dappnodeInstaller.ts index d2d2ce0ce0..fd9bd7adb2 100644 --- a/packages/installer/src/dappnodeInstaller.ts +++ b/packages/installer/src/dappnodeInstaller.ts @@ -109,7 +109,8 @@ export class DappnodeInstaller extends DappnodeRepository { disclaimer: pkgRelease.disclaimer, gettingStarted: pkgRelease.gettingStarted, grafanaDashboards: pkgRelease.grafanaDashboards, - prometheusTargets: pkgRelease.prometheusTargets + prometheusTargets: pkgRelease.prometheusTargets, + notifications: pkgRelease.notifications }); }); From 84ff200984590ba1b35c8e392b57b4530e4ab298 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Wed, 5 Mar 2025 09:53:38 +0100 Subject: [PATCH 06/41] move writemanifest to installer --- .../src/installer/writeAndValidateFiles.ts | 13 +++++++++++-- packages/utils/src/index.ts | 1 - packages/utils/src/writeManifest.ts | 6 ------ 3 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 packages/utils/src/writeManifest.ts diff --git a/packages/installer/src/installer/writeAndValidateFiles.ts b/packages/installer/src/installer/writeAndValidateFiles.ts index 8ad61238a9..a69c6688c0 100644 --- a/packages/installer/src/installer/writeAndValidateFiles.ts +++ b/packages/installer/src/installer/writeAndValidateFiles.ts @@ -1,10 +1,10 @@ import fs from "fs"; import { Log } from "@dappnode/logger"; import { validatePath } from "@dappnode/utils"; -import { InstallPackageData } from "@dappnode/types"; +import { InstallPackageData, Manifest } from "@dappnode/types"; import { dockerComposeConfig } from "@dappnode/dockerapi"; import { ComposeEditor } from "@dappnode/dockercompose"; -import { isNotFoundError, writeManifest } from "@dappnode/utils"; +import { isNotFoundError } from "@dappnode/utils"; /** * Write the new compose and test it with config @@ -47,3 +47,12 @@ function copyIfExists(src: string, dest: string): void { if (!isNotFoundError(e)) throw e; } } + +/** + * Util: Write manifest to file + * @param manfiestPath + * @param manifest + */ +function writeManifest(manfiestPath: string, manifest: Manifest): void { + fs.writeFileSync(manfiestPath, JSON.stringify(manifest, null, 2)); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b2625f7596..62361276b9 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -37,6 +37,5 @@ export { shouldUpdate } from "./shouldUpdate.js"; export { getPublicIpFromUrls } from "./getPublicIpFromUrls.js"; export { computeSemverUpdateType } from "./computeSemverUpdateType.js"; export * from "./coreVersionId.js"; -export { writeManifest } from "./writeManifest.js"; export { readManifestIfExists } from "./readManifestIfExists.js"; export { removeCidrSuffix } from "./removeCidrSuffix.js"; diff --git a/packages/utils/src/writeManifest.ts b/packages/utils/src/writeManifest.ts deleted file mode 100644 index 00cc398a1c..0000000000 --- a/packages/utils/src/writeManifest.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from "fs"; -import { Manifest } from "@dappnode/types"; - -export function writeManifest(manfiestPath: string, manifest: Manifest): void { - fs.writeFileSync(manfiestPath, JSON.stringify(manifest, null, 2)); -} From 222bb346b53c4fb6876b721039c7cc4ce229eab2 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Wed, 5 Mar 2025 18:10:59 +0100 Subject: [PATCH 07/41] return notifications in package manifest endpoint --- packages/dappmanager/src/api/routes/packageManifest.ts | 3 ++- packages/types/src/index.ts | 1 + packages/types/src/notifications.ts | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/dappmanager/src/api/routes/packageManifest.ts b/packages/dappmanager/src/api/routes/packageManifest.ts index d8b38b9d59..bbc63afb0c 100644 --- a/packages/dappmanager/src/api/routes/packageManifest.ts +++ b/packages/dappmanager/src/api/routes/packageManifest.ts @@ -54,7 +54,8 @@ export const packageManifest = wrapHandler(async (req, res) => { "links", "repository", "bugs", - "license" + "license", + "notifications" ]); res.status(200).send(filteredManifest); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 61b5aa63dd..15c5bf572b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -10,6 +10,7 @@ export * from "./releaseFiles.js"; export * from "./errors.js"; export * from "./routes.js"; export * from "./subscriptions.js"; +export * from "./notifications.js"; // utils export * from "./utils/index.js"; diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts index fdd3a04a79..440570aaf4 100644 --- a/packages/types/src/notifications.ts +++ b/packages/types/src/notifications.ts @@ -2,7 +2,7 @@ export interface GatusConfig { endpoints: Endpoint[]; } -interface Endpoint { +export interface Endpoint { name: string; enabled: boolean; url: string; @@ -11,6 +11,13 @@ interface Endpoint { interval: string; // e.g., "1m" group: string; alerts: Alert[]; + description: string; // dappnode specific + metric?: { + // dappnode specific + min: number; + max: number; + unit: string; // e.g ºC + }; } interface Alert { From 16ca50425b7911e6c7120652900f5f9eb511c94a Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 07:08:03 +0100 Subject: [PATCH 08/41] use assert instead of with --- packages/common/src/validation/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/common/src/validation/index.ts b/packages/common/src/validation/index.ts index fef11b3e0a..c1bbd27c43 100644 --- a/packages/common/src/validation/index.ts +++ b/packages/common/src/validation/index.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; -import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" with { type: "json" }; -import routesReturnSchema from "./schemas/RoutesReturn.schema.json" with { type: "json" }; -import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" with { type: "json" }; +import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" assert { type: "json" }; +import routesReturnSchema from "./schemas/RoutesReturn.schema.json" assert { type: "json" }; +import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" assert { type: "json" }; import { Args } from "typescript-json-schema"; const ajv = new Ajv({ allErrors: true, strict: false }); From 7d7319ad8e1970c5b166ff38d8cf2ac8a37ebf5c Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 09:25:04 +0100 Subject: [PATCH 09/41] fix import --- packages/installer/src/dappnodeInstaller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/installer/src/dappnodeInstaller.ts b/packages/installer/src/dappnodeInstaller.ts index fd9bd7adb2..42786a7a4b 100644 --- a/packages/installer/src/dappnodeInstaller.ts +++ b/packages/installer/src/dappnodeInstaller.ts @@ -11,7 +11,8 @@ import { PackageRequest, SetupWizard, GrafanaDashboard, - PrometheusTarget + PrometheusTarget, + GatusConfig } from "@dappnode/types"; import { DappGetState, DappgetOptions, dappGet } from "./dappGet/index.js"; import { validateDappnodeCompose, validateManifestSchema } from "@dappnode/schemas"; @@ -23,7 +24,6 @@ import { parseTimeoutSeconds } from "./utils.js"; import { getEthersProvider } from "./ethClient/index.js"; import { omit } from "lodash-es"; import { ethers } from "ethers"; -import { GatusConfig } from "../../types/dist/notifications.js"; /** * Returns the ipfsUrl to initialize the ipfs instance From c02c85751e63e5c49f217ee2b1a4185a17bca820 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 09:47:57 +0100 Subject: [PATCH 10/41] implement first approach backend calls --- .../admin-ui/src/__mock-backend__/index.ts | 4 +- packages/dappmanager/src/calls/gatusConfig.ts | 56 +++++++++++++++++++ packages/dappmanager/src/calls/index.ts | 1 + packages/types/src/routes.ts | 13 +++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/dappmanager/src/calls/gatusConfig.ts diff --git a/packages/admin-ui/src/__mock-backend__/index.ts b/packages/admin-ui/src/__mock-backend__/index.ts index b547e360dc..f47b444496 100644 --- a/packages/admin-ui/src/__mock-backend__/index.ts +++ b/packages/admin-ui/src/__mock-backend__/index.ts @@ -386,7 +386,9 @@ export const otherCalls: Omit = { dockerLatestVersion: "20.10.8" }), getIsConnectedToInternet: async () => false, - getCoreVersion: async () => "0.2.92" + getCoreVersion: async () => "0.2.92", + gatusGetEndpoints: async () => new Map(), + gatusUpdateEndpoint: async () => {} }; export const calls: Routes = { diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts new file mode 100644 index 0000000000..4cb22a8df4 --- /dev/null +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -0,0 +1,56 @@ +import { listPackages } from "@dappnode/dockerapi"; +import { GatusConfig, Endpoint, Manifest } from "@dappnode/types"; +import { getManifestPath } from "@dappnode/utils"; +import fs from "fs"; + +/** + * Get gatus endpoints indexed by dnpName + */ +export async function gatusGetEndpoints(): Promise> { + const packages = await listPackages(); + + // Read all manifests files and retrieve the gatus config + const endpoints = new Map(); + for (const pkg of packages) { + const manifestPath = getManifestPath(pkg.dnpName, pkg.isCore); + const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + if (manifest.notifications) endpoints.set(pkg.dnpName, manifest.notifications); + } + + return endpoints; +} + +/** + * Update endpoint properties + * @param dnpName + * @param updatedEndpoint + */ +export async function gatusUpdateEndpoint({ + dnpName, + updatedEndpoint +}: { + dnpName: string; + updatedEndpoint: Endpoint; +}): Promise { + // Get current endpoint status + const manifest: Manifest = JSON.parse(fs.readFileSync(getManifestPath(dnpName, false), "utf8")); + if (!manifest.notifications) throw new Error("No notifications found in manifest"); + + const endpoint = manifest.notifications.endpoints.find((e) => e.name === updatedEndpoint.name); + if (!endpoint) throw new Error(`Endpoint ${updatedEndpoint.name} not found in manifest`); + + // Update endpoint + Object.assign(endpoint, updatedEndpoint); + + // Save manifest + fs.writeFileSync(getManifestPath(dnpName, false), JSON.stringify(manifest, null, 2)); + + // Update endpoint in gatus + // await fetch(`http://notifier.notifications.dappnode:8082/gatus/endpoints`, { + // method: "POST", + // headers: { + // "Content-Type": "application/json" + // }, + // body: JSON.stringify(endpoint) + // }); +} diff --git a/packages/dappmanager/src/calls/index.ts b/packages/dappmanager/src/calls/index.ts index a61851ced7..e2b1b1f658 100644 --- a/packages/dappmanager/src/calls/index.ts +++ b/packages/dappmanager/src/calls/index.ts @@ -22,6 +22,7 @@ export { getCoreVersion } from "./getCoreVersion.js"; export { getUserActionLogs } from "./getUserActionLogs.js"; export { getHostUptime } from "./getHostUptime.js"; export { getIsConnectedToInternet } from "./getIsConnectedToInternet.js"; +export { gatusGetEndpoints, gatusUpdateEndpoint } from "./gatusConfig.js"; export * from "./httpsPortal.js"; export { ipfsTest } from "./ipfsTest.js"; export { ipfsClientTargetSet } from "./ipfsClientTargetSet.js"; diff --git a/packages/types/src/routes.ts b/packages/types/src/routes.ts index 5173890dc1..cb15f79aef 100644 --- a/packages/types/src/routes.ts +++ b/packages/types/src/routes.ts @@ -45,6 +45,7 @@ import { } from "./calls.js"; import { PackageEnvs } from "./compose.js"; import { PackageBackup } from "./manifest.js"; +import { Endpoint, GatusConfig } from "./notifications.js"; import { TrustedReleaseKey } from "./pkg.js"; import { OptimismConfigSet, OptimismConfigGet } from "./rollups.js"; import { Network, StakerConfigGet, StakerConfigSet } from "./stakers.js"; @@ -261,6 +262,16 @@ export interface Routes { */ fetchDnpRequest: (kwargs: { id: string; version?: string }) => Promise; + /** + * Gatus get endpoints + */ + gatusGetEndpoints(): Promise>; + + /** + * Gatus update endpoint + */ + gatusUpdateEndpoint: (kwargs: { dnpName: string; updatedEndpoint: Endpoint }) => Promise; + /** * Returns the user action logs. This logs are stored in a different * file and format, and are meant to ease user support @@ -690,6 +701,8 @@ export const routesData: { [P in keyof Routes]: RouteData } = { fetchDirectory: {}, fetchRegistry: {}, fetchDnpRequest: {}, + gatusGetEndpoints: { log: true }, + gatusUpdateEndpoint: { log: true }, getUserActionLogs: {}, getHostUptime: {}, httpsPortalMappingAdd: { log: true }, From b7526ee51dfcdb815c105bf833b2366f85c3e19b Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 11:48:06 +0100 Subject: [PATCH 11/41] use node 22 and assert instead of with --- Dockerfile | 2 +- packages/common/src/validation/index.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3c30344886..d7a3c4fa2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Arguments -ARG BASE_IMAGE=node:20.16.0-alpine3.19 +ARG BASE_IMAGE=node:22.14.0-alpine3.21 ARG BUILDPLATFORM # Initial stage to gather git data diff --git a/packages/common/src/validation/index.ts b/packages/common/src/validation/index.ts index c1bbd27c43..fef11b3e0a 100644 --- a/packages/common/src/validation/index.ts +++ b/packages/common/src/validation/index.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; -import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" assert { type: "json" }; -import routesReturnSchema from "./schemas/RoutesReturn.schema.json" assert { type: "json" }; -import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" assert { type: "json" }; +import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" with { type: "json" }; +import routesReturnSchema from "./schemas/RoutesReturn.schema.json" with { type: "json" }; +import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" with { type: "json" }; import { Args } from "typescript-json-schema"; const ajv = new Ajv({ allErrors: true, strict: false }); From e1428027608899df39ca723dd4b93c3dfb076e90 Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 11:49:22 +0100 Subject: [PATCH 12/41] rollback docker image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d7a3c4fa2e..3c30344886 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Arguments -ARG BASE_IMAGE=node:22.14.0-alpine3.21 +ARG BASE_IMAGE=node:20.16.0-alpine3.19 ARG BUILDPLATFORM # Initial stage to gather git data From c09b754218e3a0adf2c5c8d6b6e9ca492eee6baa Mon Sep 17 00:00:00 2001 From: Pablo Mendez Date: Thu, 6 Mar 2025 12:02:30 +0100 Subject: [PATCH 13/41] rollback to assert --- packages/common/src/validation/index.ts | 6 +++--- .../src/api/middlewares/ethForward/resolveDomain.ts | 4 ++-- packages/dockerCompose/src/validate.ts | 2 +- packages/schemas/src/validateManifestSchema.ts | 2 +- packages/schemas/src/validateSetupWizardSchema.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/common/src/validation/index.ts b/packages/common/src/validation/index.ts index fef11b3e0a..c1bbd27c43 100644 --- a/packages/common/src/validation/index.ts +++ b/packages/common/src/validation/index.ts @@ -1,7 +1,7 @@ import Ajv from "ajv"; -import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" with { type: "json" }; -import routesReturnSchema from "./schemas/RoutesReturn.schema.json" with { type: "json" }; -import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" with { type: "json" }; +import routesArgumentsSchema from "./schemas/RoutesArguments.schema.json" assert { type: "json" }; +import routesReturnSchema from "./schemas/RoutesReturn.schema.json" assert { type: "json" }; +import subscriptionsArgumentsSchema from "./schemas/SubscriptionsArguments.schema.json" assert { type: "json" }; import { Args } from "typescript-json-schema"; const ajv = new Ajv({ allErrors: true, strict: false }); diff --git a/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts b/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts index ab50670574..ecc1c62f55 100644 --- a/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts +++ b/packages/dappmanager/src/api/middlewares/ethForward/resolveDomain.ts @@ -1,7 +1,7 @@ import { ethers } from "ethers"; import { getEthersProvider, getEthUrl } from "@dappnode/installer"; -import resolverAbi from "./abi/resolverAbi.json" with { type: "json" }; -import ensAbi from "./abi/ens.json" with { type: "json" }; +import resolverAbi from "./abi/resolverAbi.json" assert { type: "json" }; +import ensAbi from "./abi/ens.json" assert { type: "json" }; import { Network, Content, NotFoundError, EnsResolverError } from "./types.js"; import { decodeContentHash, isEmpty, decodeDnsLink, decodeContent } from "./utils/index.js"; import memoize from "memoizee"; diff --git a/packages/dockerCompose/src/validate.ts b/packages/dockerCompose/src/validate.ts index 640f60615b..4abd66771b 100644 --- a/packages/dockerCompose/src/validate.ts +++ b/packages/dockerCompose/src/validate.ts @@ -1,5 +1,5 @@ import { getSchemaValidator } from "@dappnode/utils"; -import compose3xSchema from "./compose_v3x.schema.json" with { type: "json" }; +import compose3xSchema from "./compose_v3x.schema.json" assert { type: "json" }; import { Compose } from "@dappnode/types"; /** diff --git a/packages/schemas/src/validateManifestSchema.ts b/packages/schemas/src/validateManifestSchema.ts index 561044d288..0e18a052b8 100644 --- a/packages/schemas/src/validateManifestSchema.ts +++ b/packages/schemas/src/validateManifestSchema.ts @@ -1,7 +1,7 @@ import { ajv } from "./ajv.js"; import { CliError } from "./error.js"; import { processError } from "./utils.js"; -import manifestSchema from "./schemas/manifest.schema.json" with { type: "json" }; +import manifestSchema from "./schemas/manifest.schema.json" assert { type: "json" }; import { Manifest } from "@dappnode/types"; /** diff --git a/packages/schemas/src/validateSetupWizardSchema.ts b/packages/schemas/src/validateSetupWizardSchema.ts index e1f07147c0..9e59a215e3 100644 --- a/packages/schemas/src/validateSetupWizardSchema.ts +++ b/packages/schemas/src/validateSetupWizardSchema.ts @@ -1,6 +1,6 @@ import { ajv } from "./ajv.js"; import { processError } from "./utils.js"; -import setupWizardSchema from "./schemas/setup-wizard.schema.json" with { type: "json" }; +import setupWizardSchema from "./schemas/setup-wizard.schema.json" assert { type: "json" }; import { CliError } from "./error.js"; import { SetupWizard } from "@dappnode/types"; From 064859e154ca2047c70427c9c5b570b7a58a49cb Mon Sep 17 00:00:00 2001 From: mateumiralles Date: Thu, 6 Mar 2025 01:08:22 +0100 Subject: [PATCH 14/41] initial commit --- .../src/components/sidebar/navbarItems.ts | 8 +++ packages/admin-ui/src/pages/index.ts | 4 +- .../components/NotificationsRoot.tsx | 59 ++++++++++++++++++ .../components/notifications.scss | 0 .../notifications/components/tabs/Inbox.tsx | 60 +++++++++++++++++++ .../admin-ui/src/pages/notifications/data.ts | 11 ++++ .../admin-ui/src/pages/notifications/index.ts | 4 ++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx create mode 100644 packages/admin-ui/src/pages/notifications/components/notifications.scss create mode 100644 packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx create mode 100644 packages/admin-ui/src/pages/notifications/data.ts create mode 100644 packages/admin-ui/src/pages/notifications/index.ts diff --git a/packages/admin-ui/src/components/sidebar/navbarItems.ts b/packages/admin-ui/src/components/sidebar/navbarItems.ts index 64c2ed1315..5f235ff485 100644 --- a/packages/admin-ui/src/components/sidebar/navbarItems.ts +++ b/packages/admin-ui/src/components/sidebar/navbarItems.ts @@ -15,6 +15,7 @@ import { MdWifi, MdPeople } from "react-icons/md"; +import { FaRegBell } from "react-icons/fa"; import { SiEthereum } from "react-icons/si"; import { BiGitRepoForked } from "react-icons/bi"; import { GiRolledCloth } from "react-icons/gi"; @@ -31,6 +32,7 @@ import { relativePath as communityRelativePath } from "pages/community"; import { relativePath as stakersRelativePath } from "pages/stakers"; import { relativePath as rollupsRelativePath } from "pages/rollups"; import { relativePath as repositoryRelativePath } from "pages/repository"; +import { relativePath as notificationsRelativePath } from "pages/notifications"; export const fundedBy: { logo: string; text: string; link: string }[] = [ { @@ -116,6 +118,12 @@ export const sidenavItems: { icon: MdSettings, show: true }, + { + name: "Notifications", + href: notificationsRelativePath, + icon: FaRegBell, + show: true + }, { name: "Community", href: communityRelativePath, diff --git a/packages/admin-ui/src/pages/index.ts b/packages/admin-ui/src/pages/index.ts index 5534631d35..94e432de06 100644 --- a/packages/admin-ui/src/pages/index.ts +++ b/packages/admin-ui/src/pages/index.ts @@ -10,6 +10,7 @@ import * as community from "./community"; import * as stakers from "./stakers"; import * as rollups from "./rollups"; import * as repository from "./repository"; +import * as notifications from "./notifications"; export const pages = { dashboard, @@ -23,7 +24,8 @@ export const pages = { support, community, system, - repository + repository, + notifications }; export const defaultPage = dashboard; diff --git a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx new file mode 100644 index 0000000000..8a3a1be829 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { Routes, Route, NavLink } from "react-router-dom"; +// Own module +import { title, subPaths } from "../data"; +// Components +import Title from "components/Title"; +import Inbox from "./tabs/Inbox"; +// CSS +import "./notifications.scss"; +import { useApi } from "api"; + +export const NotificationsRoot: React.FC = () => { + const availableRoutes: { + name: string; + subPath: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: () => React.JSX.Element; + }[] = [ + { + name: "Inbox", + subPath: subPaths.inbox, + component: Inbox + }, + { + name: "Settings", + subPath: subPaths.settings, + component: Inbox + } + ]; + + + + const dnpsRequest = useApi.packagesGet(); + console.log('dnpsRequest', dnpsRequest); + + + return ( + <> + + <div className="horizontal-navbar"> + {availableRoutes.map((route) => ( + <button key={route.subPath} className="item-container"> + <NavLink to={route.subPath} className="item no-a-style" style={{ whiteSpace: "nowrap" }}> + {route.name} + </NavLink> + </button> + ))} + </div> + + <div className="section-spacing"> + <Routes> + {availableRoutes.map((route) => ( + <Route key={route.subPath} path={route.subPath} element={<route.component />} /> + ))} + </Routes> + </div> + </> + ); +}; diff --git a/packages/admin-ui/src/pages/notifications/components/notifications.scss b/packages/admin-ui/src/pages/notifications/components/notifications.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx new file mode 100644 index 0000000000..7f25689c6e --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx @@ -0,0 +1,60 @@ +import SubTitle from "components/SubTitle"; +import React from "react"; +import Card from "components/Card"; + +interface Notification { + timestamp: string; + title: string; + dnp: string; + body: string; +} + +export default function Inbox() { + const newNotifications: Notification[] = [ + { + timestamp: "2021-06-01T12:00:00Z", + title: "New Notification", + dnp: "package-name.dnp.dappnode.eth", + body: "This is a new notification" + } + ]; + const seenNotifications: Notification[] = [ + { + timestamp: "2021-06-01T12:00:00Z", + title: "Seen Notification", + dnp: "package-name.dnp.dappnode.eth", + body: "This is a seen notification" + } + ]; + return ( + <> + {newNotifications.length > 0 && ( + <> + <SubTitle>New Notifications</SubTitle> + {newNotifications.map((notification) => ( + <Card key={notification.timestamp}> + <div>{new Date(notification.timestamp).toLocaleString()}</div> + <div>{notification.title}</div> + <div>{notification.dnp}</div> + <div>{notification.body}</div> + </Card> + ))} + </> + )} + + <SubTitle>History</SubTitle> + {seenNotifications.length === 0 ? ( + <Card>No notifications</Card> + ) : ( + seenNotifications.map((notification) => ( + <Card key={notification.timestamp}> + <div>{new Date(notification.timestamp).toUTCString()}</div> + <div>{notification.title}</div> + <div>{notification.dnp}</div> + <div>{notification.body}</div> + </Card> + )) + )} + </> + ); +} diff --git a/packages/admin-ui/src/pages/notifications/data.ts b/packages/admin-ui/src/pages/notifications/data.ts new file mode 100644 index 0000000000..0e09541383 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/data.ts @@ -0,0 +1,11 @@ +export const relativePath = "notifications/inbox"; // default redirect to inbox +export const rootPath = "notifications/*"; +export const title = "Notifications"; + +// Additional data + +// SubPaths +export const subPaths = { + inbox: "inbox", + settings: "settings" +}; diff --git a/packages/admin-ui/src/pages/notifications/index.ts b/packages/admin-ui/src/pages/notifications/index.ts new file mode 100644 index 0000000000..8add77c619 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/index.ts @@ -0,0 +1,4 @@ +import { NotificationsRoot } from "./components/NotificationsRoot"; + +export { rootPath, relativePath } from "./data"; +export const RootComponent = NotificationsRoot; From c92b6b40c312dcf44c246cb19927ce254ca6fd17 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 02:48:46 +0100 Subject: [PATCH 15/41] Notifications card & Install notifications pkg --- .../components/InstallNotifications.tsx | 26 ++++++ .../components/NotificationsRoot.tsx | 51 ++++++----- .../components/notifications.scss | 90 +++++++++++++++++++ .../notifications/components/tabs/Inbox.tsx | 62 ++++++++++--- 4 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx diff --git a/packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx b/packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx new file mode 100644 index 0000000000..e63c0fff7e --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import Button from "components/Button"; +import { getInstallerPath } from "pages/installer/data"; +import SubTitle from "components/SubTitle"; +import Card from "components/Card"; + +import "./notifications.scss"; + +interface InstallNotificationsPkgProps { + pkgName: string; +} + +export const InstallNotificationsPkg: React.FC<InstallNotificationsPkgProps> = ({ pkgName }) => { + const installerPath = getInstallerPath(pkgName); + + return ( + <Card className="install-notifications-card"> + <SubTitle>Install notifications package</SubTitle> + <p>To receive notifications on your Dappnode, you must install the Notifications Dappnode Package.</p> + <NavLink to={installerPath + "/" + pkgName}> + <Button variant="dappnode">Install</Button> + </NavLink> + </Card> + ); +}; diff --git a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx index 8a3a1be829..d7b05e12fa 100644 --- a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx +++ b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx @@ -8,6 +8,8 @@ import Inbox from "./tabs/Inbox"; // CSS import "./notifications.scss"; import { useApi } from "api"; +import { InstallNotificationsPkg } from "./InstallNotifications"; +import Loading from "components/Loading"; export const NotificationsRoot: React.FC = () => { const availableRoutes: { @@ -28,32 +30,41 @@ export const NotificationsRoot: React.FC = () => { } ]; - + const dnpsRequest = useApi.packagesGet(); + const loading = dnpsRequest.isValidating; + const installedDnps = dnpsRequest.data; - const dnpsRequest = useApi.packagesGet(); - console.log('dnpsRequest', dnpsRequest); - + const notificationsDnpName = "notifications.public.dappnode.eth"; + const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === notificationsDnpName); return ( <> <Title title={title} /> - <div className="horizontal-navbar"> - {availableRoutes.map((route) => ( - <button key={route.subPath} className="item-container"> - <NavLink to={route.subPath} className="item no-a-style" style={{ whiteSpace: "nowrap" }}> - {route.name} - </NavLink> - </button> - ))} - </div> + {loading ? ( + <Loading steps={["Loading data"]} /> + ) : !isNotificationsPkgInstalled ? ( + <InstallNotificationsPkg pkgName={notificationsDnpName}/> + ) : ( + <> + <div className="horizontal-navbar"> + {availableRoutes.map((route) => ( + <button key={route.subPath} className="item-container"> + <NavLink to={route.subPath} className="item no-a-style" style={{ whiteSpace: "nowrap" }}> + {route.name} + </NavLink> + </button> + ))} + </div> - <div className="section-spacing"> - <Routes> - {availableRoutes.map((route) => ( - <Route key={route.subPath} path={route.subPath} element={<route.component />} /> - ))} - </Routes> - </div> + <div className="section-spacing"> + <Routes> + {availableRoutes.map((route) => ( + <Route key={route.subPath} path={route.subPath} element={<route.component />} /> + ))} + </Routes> + </div> + </> + )} </> ); }; diff --git a/packages/admin-ui/src/pages/notifications/components/notifications.scss b/packages/admin-ui/src/pages/notifications/components/notifications.scss index e69de29bb2..207fd6f58e 100644 --- a/packages/admin-ui/src/pages/notifications/components/notifications.scss +++ b/packages/admin-ui/src/pages/notifications/components/notifications.scss @@ -0,0 +1,90 @@ +.install-notifications-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.notification-card { + border-radius: 10px; + background-color: #e9ecef; + padding: 20px; + cursor: pointer; + + .notification-header { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; // Keep the original alignment + width: 100%; + + .notification-img { + width: 70px; + height: 70px; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + background-color: #a3a5a7; + } + + .notification-header-data { + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; + + .notification-title { + font-size: 1.4rem; + @media (max-width: 40rem) { + font-size: 1.2rem; + } + } + + .notification-header-details { + display: flex; + justify-content: space-between; + width: 100%; + color: var(--light-text-color); + font-style: italic; + + @media (max-width: 60rem) { + flex-direction: column; + font-size: 0.8rem; + + } + } + } + } + + .notification-body { + padding-top: 10px; + font-size: 1rem; + } +} +.notification-card:hover { + background-color: var(--color-dark-quaternarytext); + transition: all 0.1s ease-in-out; +} + +#dark { + .notification-card { + background-color: var(--color-dark-card); + color: var(--color-dark-maintext); + + .notification-header { + .notification-img { + background-color: var(--color-dark-card-hover); + color: white; + } + + .notification-header-details { + color: var(--light-text-color); + } + } + } + + .notification-card:hover { + background-color: var(--color-dark-card-hover); + } +} diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx index 7f25689c6e..386110abd7 100644 --- a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx @@ -1,6 +1,9 @@ import SubTitle from "components/SubTitle"; -import React from "react"; +import React, { useState } from "react"; import Card from "components/Card"; +import { Accordion } from "react-bootstrap"; + +import "../notifications.scss"; interface Notification { timestamp: string; @@ -16,6 +19,12 @@ export default function Inbox() { title: "New Notification", dnp: "package-name.dnp.dappnode.eth", body: "This is a new notification" + }, + { + timestamp: "2025-06-06T13:00:00Z", + title: "Validator exited sucesfully", + dnp: "lido-csm-mainnet.dnp.dappnode.eth", + body: "Your validator 10802082 has entered the exit queue automatically. No manual action required" } ]; const seenNotifications: Notification[] = [ @@ -24,20 +33,28 @@ export default function Inbox() { title: "Seen Notification", dnp: "package-name.dnp.dappnode.eth", body: "This is a seen notification" + }, + { + timestamp: "2025-06-04T12:00:00Z", + title: "Updated Telegram configuration", + dnp: "lido-csm-mainnet.dnp.dappnode.eth", + body: "Your telegram configuration has been updated successfully" + }, + { + timestamp: "2025-06-06T12:00:00Z", + title: "Validator exit request", + dnp: "lido-csm-mainnet.dnp.dappnode.eth", + body: "Your validator 10802082 has requested to exit the network. Executing automatic exit" } ]; + return ( <> {newNotifications.length > 0 && ( <> <SubTitle>New Notifications</SubTitle> {newNotifications.map((notification) => ( - <Card key={notification.timestamp}> - <div>{new Date(notification.timestamp).toLocaleString()}</div> - <div>{notification.title}</div> - <div>{notification.dnp}</div> - <div>{notification.body}</div> - </Card> + <NotificationCard key={notification.timestamp} notification={notification} /> ))} </> )} @@ -47,14 +64,33 @@ export default function Inbox() { <Card>No notifications</Card> ) : ( seenNotifications.map((notification) => ( - <Card key={notification.timestamp}> - <div>{new Date(notification.timestamp).toUTCString()}</div> - <div>{notification.title}</div> - <div>{notification.dnp}</div> - <div>{notification.body}</div> - </Card> + <NotificationCard key={notification.timestamp} notification={notification} /> )) )} </> ); } + +function NotificationCard({ notification }: { notification: Notification }) { + const [isOpen, setIsOpen] = useState(false); + + return ( + <Accordion defaultActiveKey={isOpen ? "0" : "1"} > + <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> + <div className="notification-header"> + <div className="notification-img"> IMG </div> + <div className="notification-header-data"> + <div className="notification-header-details"> + <div>{new Date(notification.timestamp).toLocaleString()}</div> + <div>{notification.dnp}</div> + </div> + <div className="notification-title">{notification.title}</div> + </div> + </div> + <Accordion.Collapse eventKey="0"> + <div className="notification-body">{notification.body}</div> + </Accordion.Collapse> + </Accordion.Toggle>{" "} + </Accordion> + ); +} From 5ff3e3ee597fb2d12aa39b27ad38d9abcd323b92 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 12:30:10 +0100 Subject: [PATCH 16/41] Notifications Settings tab v1 --- .../components/NotificationsRoot.tsx | 15 +- .../components/notifications.scss | 56 ++++- .../notifications/components/tabs/Inbox.tsx | 4 +- .../components/tabs/Settings.tsx | 230 ++++++++++++++++++ 4 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx diff --git a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx index d7b05e12fa..d29c259fa3 100644 --- a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx +++ b/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx @@ -1,15 +1,16 @@ import React from "react"; import { Routes, Route, NavLink } from "react-router-dom"; +import { useApi } from "api"; // Own module import { title, subPaths } from "../data"; +import { Inbox } from "./tabs/Inbox"; +import { NotificationsSettings } from "./tabs/Settings"; +import { InstallNotificationsPkg } from "./InstallNotifications"; // Components import Title from "components/Title"; -import Inbox from "./tabs/Inbox"; +import Loading from "components/Loading"; // CSS import "./notifications.scss"; -import { useApi } from "api"; -import { InstallNotificationsPkg } from "./InstallNotifications"; -import Loading from "components/Loading"; export const NotificationsRoot: React.FC = () => { const availableRoutes: { @@ -26,7 +27,7 @@ export const NotificationsRoot: React.FC = () => { { name: "Settings", subPath: subPaths.settings, - component: Inbox + component: NotificationsSettings } ]; @@ -36,14 +37,14 @@ export const NotificationsRoot: React.FC = () => { const notificationsDnpName = "notifications.public.dappnode.eth"; const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === notificationsDnpName); - + return ( <> <Title title={title} /> {loading ? ( <Loading steps={["Loading data"]} /> ) : !isNotificationsPkgInstalled ? ( - <InstallNotificationsPkg pkgName={notificationsDnpName}/> + <InstallNotificationsPkg pkgName={notificationsDnpName} /> ) : ( <> <div className="horizontal-navbar"> diff --git a/packages/admin-ui/src/pages/notifications/components/notifications.scss b/packages/admin-ui/src/pages/notifications/components/notifications.scss index 207fd6f58e..493d765d31 100644 --- a/packages/admin-ui/src/pages/notifications/components/notifications.scss +++ b/packages/admin-ui/src/pages/notifications/components/notifications.scss @@ -1,3 +1,4 @@ +// --------------INSTALL NOTIS PKG---------------- .install-notifications-card { display: flex; flex-direction: column; @@ -5,6 +6,7 @@ justify-content: center; } +// ---------------------INBOX--------------------- .notification-card { border-radius: 10px; background-color: #e9ecef; @@ -51,7 +53,6 @@ @media (max-width: 60rem) { flex-direction: column; font-size: 0.8rem; - } } } @@ -88,3 +89,56 @@ background-color: var(--color-dark-card-hover); } } + +// -------------------SETTINGS--------------------- + +.notifications-settings { + .title-switch-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + } + .notifications-section-title { + margin: 0.5rem 0; + } + + .manage-notifications-wrapper { + display: flex; + flex-direction: column; + gap: 10px; + } + + .notifications-pkg-name { + font-size: 1.2rem; + margin: 0.5rem 0; + } + + .endpoints-card { + border-radius: 10px; + background-color: #e9ecef; + padding: 20px 10px; + display: flex; + flex-direction: column; + + .endpoint-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + hr { + width: 100%; + } + } +} + +#dark { + .notifications-settings { + .endpoints-card { + background-color: var(--color-dark-card); + color: var(--color-dark-maintext); + } + } +} diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx index 386110abd7..5267ca9e27 100644 --- a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx @@ -12,7 +12,7 @@ interface Notification { body: string; } -export default function Inbox() { +export function Inbox() { const newNotifications: Notification[] = [ { timestamp: "2021-06-01T12:00:00Z", @@ -81,8 +81,8 @@ function NotificationCard({ notification }: { notification: Notification }) { <div className="notification-img"> IMG </div> <div className="notification-header-data"> <div className="notification-header-details"> - <div>{new Date(notification.timestamp).toLocaleString()}</div> <div>{notification.dnp}</div> + <div>{new Date(notification.timestamp).toLocaleString()}</div> </div> <div className="notification-title">{notification.title}</div> </div> diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx b/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx new file mode 100644 index 0000000000..f6160dc28b --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx @@ -0,0 +1,230 @@ +import SubTitle from "components/SubTitle"; +import React, { useState } from "react"; +import Switch from "components/Switch"; + +import "../notifications.scss"; + +interface GatusConfig { + endpoints: Endpoint[]; +} + +interface Endpoint { + name: string; + enabled: boolean; + url: string; + method: string; + conditions: string[]; + interval: string; // e.g., "1m" + group: string; + description: string; // dappnode specific + metric?: { + // dappnode specific + min: number; + max: number; + unit: string; // e.g ºC + }; +} + +export function NotificationsSettings() { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + + const fakeNotifications: Map<String, GatusConfig> = new Map([ + [ + "Dappmanager", + { + endpoints: [ + { + name: "Installed packages updates", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Core", + description: "Sends a notification whenever a new version of an installed package is released" + } + ] + } + ], + [ + "DMS", + { + endpoints: [ + { + name: "Disk space", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Core", + description: "Sends a notification whenever your storage disk exceeds the specified percentage.", + metric: { + min: 0, + max: 100, + unit: "%" + } + } + ] + } + ], + + [ + "Web3Signer", + { + endpoints: [ + { + name: "Validator offline / back online", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Staking", + description: "Sends a notification whenever one of your validators toggles its state." + }, + { + name: "Missed attestation", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Staking", + description: "Sends a notification whenever one of your validators misses an attestation." + }, + { + name: "Missed proposal", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Staking", + description: "Sends a notification whenever one of your validators misses a block proposal." + }, + { + name: "Submitted Proposal", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Staking", + description: "Sends a notification whenever one of your validators submits a block proposal successfully." + }, + { + name: "Efectiveness", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Staking", + description: + "Sends a notification whenever the effectiveness of one of your validators falls below the specified percentage.", + metric: { + min: 0, + max: 100, + unit: "%" + } + } + ] + } + ], + [ + "Lido CSM", + { + endpoints: [ + { + name: "Validator exited sucesfully", + enabled: true, + url: "", + method: "", + conditions: [""], + interval: "1m", + group: "Core", + description: + "Sends a notification whenever a validator has entered the exit queue automatically. No manual action required" + } + ] + } + ] + ]); + + return ( + <div className="notifications-settings"> + <div> + <div className="title-switch-row"> + <SubTitle className="notifications-section-title">Enable notifications</SubTitle> + <Switch + checked={notificationsEnabled} + onToggle={() => { + setNotificationsEnabled(!notificationsEnabled); + }} + /> + </div> + <div>Enable notifications to retrieve a registry of notifications on your Dappnode.</div> + </div> + <br /> + {notificationsEnabled && ( + <div> + <SubTitle className="notifications-section-title">Manage notifications</SubTitle> + <div>Enable, disable and customize notifications individually.</div> + <br /> + <div className="manage-notifications-wrapper"> + {[...fakeNotifications.entries()].map(([key, value]) => ( + <ManagePackageSection pkg={String(key)} endpoints={value.endpoints} /> + ))} + </div> + </div> + )} + </div> + ); +} + +function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endpoint[] }) { + const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); + + const handlePkgToggle = () => { + // TODO: update "notifications.yaml" file + setPkgNotificationsEnabled(!pkgNotificationsEnabled); + }; + + const handleEndpointToggle = () => {}; + + return ( + <div key={String(pkg)}> + <div className="title-switch-row"> + <SubTitle className="notifications-pkg-name">{pkg}</SubTitle> + <Switch + checked={pkgNotificationsEnabled} + onToggle={() => { + handlePkgToggle(); + }} + /> + </div> + {pkgNotificationsEnabled && ( + <div className="endpoints-card"> + {endpoints.map((endpoint, index) => ( + <> + <div key={index} className="endpoint-row"> + <div> + <strong>{endpoint.name}</strong> + <div>{endpoint.description}</div> + </div> + <Switch + checked={endpoint.enabled} + onToggle={() => { + handleEndpointToggle; + }} + /> + </div> + {index + 1 < endpoints.length && <hr />} + </> + ))} + </div> + )} + </div> + ); +} From 849efc37749e740d5712059fef1cb0a6f7864685 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 13:40:51 +0100 Subject: [PATCH 17/41] Slider component --- packages/admin-ui/src/components/Slider.scss | 39 +++++++++++++++++ packages/admin-ui/src/components/Slider.tsx | 45 ++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 packages/admin-ui/src/components/Slider.scss create mode 100644 packages/admin-ui/src/components/Slider.tsx diff --git a/packages/admin-ui/src/components/Slider.scss b/packages/admin-ui/src/components/Slider.scss new file mode 100644 index 0000000000..fc74ee9c3e --- /dev/null +++ b/packages/admin-ui/src/components/Slider.scss @@ -0,0 +1,39 @@ +.slider-container { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.slider-component { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: #c0c0c0; + border-radius: 2px; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 16px; + height: 16px; + background: var(--dappnode-color); + border-radius: 50%; + cursor: pointer; + } + + &::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--dappnode-color); + border-radius: 50%; + cursor: pointer; + } +} + +.slider-value { + color: #c0c0c0; + font-weight: bold; + min-width: fit-content; +} diff --git a/packages/admin-ui/src/components/Slider.tsx b/packages/admin-ui/src/components/Slider.tsx new file mode 100644 index 0000000000..e90f323074 --- /dev/null +++ b/packages/admin-ui/src/components/Slider.tsx @@ -0,0 +1,45 @@ +import React, { useState } from "react"; +import "./Slider.scss"; + +interface SliderProps { + min?: number; + max?: number; + step?: number; + value?: number; + unit?: string; + onChange?: (value: number) => void; +} + +const Slider: React.FC<SliderProps> = ({ + min = 0, + max = 100, + step = 1, + value = 50, + unit = "%", + onChange, +}) => { + const [sliderValue, setSliderValue] = useState(value); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const newValue = Number(e.target.value); + setSliderValue(newValue); + if (onChange) onChange(newValue); + }; + + return ( + <div className="slider-container"> + <input + type="range" + min={min} + max={max} + step={step} + value={sliderValue} + onChange={handleChange} + className="slider-component" + /> + <span className="slider-value">{sliderValue} {unit && unit}</span> + </div> + ); +}; + +export default Slider; From 6ae9e39feee5400b42e122daf39af2950c370809 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 13:44:18 +0100 Subject: [PATCH 18/41] Notifications Settings tab v2 --- .../components/notifications.scss | 18 ++++-- .../components/tabs/Settings.tsx | 55 ++++++++++++------- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/components/notifications.scss b/packages/admin-ui/src/pages/notifications/components/notifications.scss index 493d765d31..a1b71c0dcd 100644 --- a/packages/admin-ui/src/pages/notifications/components/notifications.scss +++ b/packages/admin-ui/src/pages/notifications/components/notifications.scss @@ -96,7 +96,6 @@ .title-switch-row { display: flex; flex-direction: row; - align-items: center; gap: 10px; } .notifications-section-title { @@ -106,7 +105,7 @@ .manage-notifications-wrapper { display: flex; flex-direction: column; - gap: 10px; + gap: 15px; } .notifications-pkg-name { @@ -114,10 +113,10 @@ margin: 0.5rem 0; } - .endpoints-card { + .endpoint-list-card { border-radius: 10px; background-color: #e9ecef; - padding: 20px 10px; + padding: 20px 15px; display: flex; flex-direction: column; @@ -131,12 +130,21 @@ hr { width: 100%; } + + .slider-wrapper{ + padding-top: 15px; + width: 50%; + + @media (max-width: 60rem) { + width: 100%; + } + } } } #dark { .notifications-settings { - .endpoints-card { + .endpoint-list-card { background-color: var(--color-dark-card); color: var(--color-dark-maintext); } diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx b/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx index f6160dc28b..ffe577c378 100644 --- a/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx +++ b/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import Switch from "components/Switch"; import "../notifications.scss"; +import Slider from "components/Slider"; interface GatusConfig { endpoints: Endpoint[]; @@ -191,8 +192,6 @@ function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endp setPkgNotificationsEnabled(!pkgNotificationsEnabled); }; - const handleEndpointToggle = () => {}; - return ( <div key={String(pkg)}> <div className="title-switch-row"> @@ -205,26 +204,44 @@ function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endp /> </div> {pkgNotificationsEnabled && ( - <div className="endpoints-card"> - {endpoints.map((endpoint, index) => ( - <> - <div key={index} className="endpoint-row"> - <div> - <strong>{endpoint.name}</strong> - <div>{endpoint.description}</div> - </div> - <Switch - checked={endpoint.enabled} - onToggle={() => { - handleEndpointToggle; - }} - /> - </div> - {index + 1 < endpoints.length && <hr />} - </> + <div className="endpoint-list-card"> + {endpoints.map((endpoint, i) => ( + <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> ))} </div> )} </div> ); } + +function EndpointItem({ endpoint, index, numEndpoints }: { endpoint: Endpoint; index: number; numEndpoints: number }) { + const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); + + const handleEndpointToggle = () => { + // TODO: update "notifications.yaml" file + setEndpointEnabled(!endpointEnabled); + }; + + return ( + <> + <div key={index} className="endpoint-row"> + <div> + <strong>{endpoint.name}</strong> + <div>{endpoint.description}</div> + </div> + <Switch + checked={endpointEnabled} + onToggle={() => { + handleEndpointToggle(); + }} + /> + </div> + {endpointEnabled && endpoint.metric && ( + <div className="slider-wrapper"> + <Slider /> + </div> + )} + {index + 1 < numEndpoints && <hr />} + </> + ); +} From 5d5b8df7bf1e9b5871e088962ca98aad4d423f1c Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 15:47:24 +0100 Subject: [PATCH 19/41] code re-structured --- .../{components => }/NotificationsRoot.tsx | 10 +- .../components/notifications.scss | 152 ------------------ .../admin-ui/src/pages/notifications/index.ts | 2 +- .../{components/tabs => tabs/Inbox}/Inbox.tsx | 30 +--- .../Inbox/components/NotificationsCard.tsx | 27 ++++ .../pages/notifications/tabs/Inbox/inbox.scss | 82 ++++++++++ .../InstallNotifications.tsx | 2 +- .../installNotifications.scss | 7 + .../tabs => tabs/Settings}/Settings.tsx | 4 +- .../notifications/tabs/Settings/settings.scss | 58 +++++++ 10 files changed, 186 insertions(+), 188 deletions(-) rename packages/admin-ui/src/pages/notifications/{components => }/NotificationsRoot.tsx (88%) delete mode 100644 packages/admin-ui/src/pages/notifications/components/notifications.scss rename packages/admin-ui/src/pages/notifications/{components/tabs => tabs/Inbox}/Inbox.tsx (63%) create mode 100644 packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx create mode 100644 packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss rename packages/admin-ui/src/pages/notifications/{components => tabs/InstallNotifications}/InstallNotifications.tsx (95%) create mode 100644 packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/installNotifications.scss rename packages/admin-ui/src/pages/notifications/{components/tabs => tabs/Settings}/Settings.tsx (99%) create mode 100644 packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss diff --git a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx similarity index 88% rename from packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx rename to packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx index d29c259fa3..228299b524 100644 --- a/packages/admin-ui/src/pages/notifications/components/NotificationsRoot.tsx +++ b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx @@ -2,15 +2,13 @@ import React from "react"; import { Routes, Route, NavLink } from "react-router-dom"; import { useApi } from "api"; // Own module -import { title, subPaths } from "../data"; -import { Inbox } from "./tabs/Inbox"; -import { NotificationsSettings } from "./tabs/Settings"; -import { InstallNotificationsPkg } from "./InstallNotifications"; +import { title, subPaths } from "./data"; +import { Inbox } from "./tabs/Inbox/Inbox"; +import { NotificationsSettings } from "./tabs/Settings/Settings"; +import { InstallNotificationsPkg } from "./tabs/InstallNotifications/InstallNotifications"; // Components import Title from "components/Title"; import Loading from "components/Loading"; -// CSS -import "./notifications.scss"; export const NotificationsRoot: React.FC = () => { const availableRoutes: { diff --git a/packages/admin-ui/src/pages/notifications/components/notifications.scss b/packages/admin-ui/src/pages/notifications/components/notifications.scss deleted file mode 100644 index a1b71c0dcd..0000000000 --- a/packages/admin-ui/src/pages/notifications/components/notifications.scss +++ /dev/null @@ -1,152 +0,0 @@ -// --------------INSTALL NOTIS PKG---------------- -.install-notifications-card { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -// ---------------------INBOX--------------------- -.notification-card { - border-radius: 10px; - background-color: #e9ecef; - padding: 20px; - cursor: pointer; - - .notification-header { - display: flex; - flex-direction: row; - gap: 10px; - align-items: center; // Keep the original alignment - width: 100%; - - .notification-img { - width: 70px; - height: 70px; - border-radius: 10px; - display: flex; - justify-content: center; - align-items: center; - background-color: #a3a5a7; - } - - .notification-header-data { - display: flex; - flex-direction: column; - justify-content: space-between; - flex-grow: 1; - - .notification-title { - font-size: 1.4rem; - @media (max-width: 40rem) { - font-size: 1.2rem; - } - } - - .notification-header-details { - display: flex; - justify-content: space-between; - width: 100%; - color: var(--light-text-color); - font-style: italic; - - @media (max-width: 60rem) { - flex-direction: column; - font-size: 0.8rem; - } - } - } - } - - .notification-body { - padding-top: 10px; - font-size: 1rem; - } -} -.notification-card:hover { - background-color: var(--color-dark-quaternarytext); - transition: all 0.1s ease-in-out; -} - -#dark { - .notification-card { - background-color: var(--color-dark-card); - color: var(--color-dark-maintext); - - .notification-header { - .notification-img { - background-color: var(--color-dark-card-hover); - color: white; - } - - .notification-header-details { - color: var(--light-text-color); - } - } - } - - .notification-card:hover { - background-color: var(--color-dark-card-hover); - } -} - -// -------------------SETTINGS--------------------- - -.notifications-settings { - .title-switch-row { - display: flex; - flex-direction: row; - gap: 10px; - } - .notifications-section-title { - margin: 0.5rem 0; - } - - .manage-notifications-wrapper { - display: flex; - flex-direction: column; - gap: 15px; - } - - .notifications-pkg-name { - font-size: 1.2rem; - margin: 0.5rem 0; - } - - .endpoint-list-card { - border-radius: 10px; - background-color: #e9ecef; - padding: 20px 15px; - display: flex; - flex-direction: column; - - .endpoint-row { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - } - - hr { - width: 100%; - } - - .slider-wrapper{ - padding-top: 15px; - width: 50%; - - @media (max-width: 60rem) { - width: 100%; - } - } - } -} - -#dark { - .notifications-settings { - .endpoint-list-card { - background-color: var(--color-dark-card); - color: var(--color-dark-maintext); - } - } -} diff --git a/packages/admin-ui/src/pages/notifications/index.ts b/packages/admin-ui/src/pages/notifications/index.ts index 8add77c619..37b01edfcc 100644 --- a/packages/admin-ui/src/pages/notifications/index.ts +++ b/packages/admin-ui/src/pages/notifications/index.ts @@ -1,4 +1,4 @@ -import { NotificationsRoot } from "./components/NotificationsRoot"; +import { NotificationsRoot } from "./NotificationsRoot"; export { rootPath, relativePath } from "./data"; export const RootComponent = NotificationsRoot; diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx similarity index 63% rename from packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx rename to packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx index 5267ca9e27..7c3dae615d 100644 --- a/packages/admin-ui/src/pages/notifications/components/tabs/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx @@ -1,11 +1,11 @@ import SubTitle from "components/SubTitle"; -import React, { useState } from "react"; +import React from "react"; import Card from "components/Card"; -import { Accordion } from "react-bootstrap"; -import "../notifications.scss"; +import "./inbox.scss"; +import { NotificationCard } from "./components/NotificationsCard"; -interface Notification { +export interface Notification { timestamp: string; title: string; dnp: string; @@ -71,26 +71,4 @@ export function Inbox() { ); } -function NotificationCard({ notification }: { notification: Notification }) { - const [isOpen, setIsOpen] = useState(false); - return ( - <Accordion defaultActiveKey={isOpen ? "0" : "1"} > - <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> - <div className="notification-header"> - <div className="notification-img"> IMG </div> - <div className="notification-header-data"> - <div className="notification-header-details"> - <div>{notification.dnp}</div> - <div>{new Date(notification.timestamp).toLocaleString()}</div> - </div> - <div className="notification-title">{notification.title}</div> - </div> - </div> - <Accordion.Collapse eventKey="0"> - <div className="notification-body">{notification.body}</div> - </Accordion.Collapse> - </Accordion.Toggle>{" "} - </Accordion> - ); -} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx new file mode 100644 index 0000000000..89aa672ac9 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -0,0 +1,27 @@ +import React, { useState } from "react"; +import { Accordion } from "react-bootstrap"; +import { Notification } from "../Inbox"; + +export function NotificationCard({ notification }: { notification: Notification }) { + const [isOpen, setIsOpen] = useState(false); + + return ( + <Accordion defaultActiveKey={isOpen ? "0" : "1"} > + <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> + <div className="notification-header"> + <div className="notification-img"> IMG </div> + <div className="notification-header-data"> + <div className="notification-header-details"> + <div>{notification.dnp}</div> + <div>{new Date(notification.timestamp).toLocaleString()}</div> + </div> + <div className="notification-title">{notification.title}</div> + </div> + </div> + <Accordion.Collapse eventKey="0"> + <div className="notification-body">{notification.body}</div> + </Accordion.Collapse> + </Accordion.Toggle>{" "} + </Accordion> + ); + } \ No newline at end of file diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss new file mode 100644 index 0000000000..b12513acfd --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss @@ -0,0 +1,82 @@ +.notification-card { + border-radius: 10px; + background-color: #e9ecef; + padding: 15px; + cursor: pointer; + + .notification-header { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + width: 100%; + + .notification-img { + width: 60px; + height: 60px; + border-radius: 10px; + display: flex; + justify-content: center; + align-items: center; + background-color: #a3a5a7; + } + + .notification-header-data { + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; + + .notification-title { + font-size: 1.2rem; + @media (max-width: 40rem) { + font-size: 1rem; + } + } + + .notification-header-details { + display: flex; + justify-content: space-between; + width: 100%; + color: var(--light-text-color); + font-style: italic; + + @media (max-width: 60rem) { + flex-direction: column; + font-size: 0.8rem; + } + } + } + } + + .notification-body { + padding-top: 10px; + font-size: 1rem; + } + } + .notification-card:hover { + background-color: var(--color-dark-quaternarytext); + transition: all 0.1s ease-in-out; + } + + #dark { + .notification-card { + background-color: var(--color-dark-card); + color: var(--color-dark-maintext); + + .notification-header { + .notification-img { + background-color: var(--color-dark-card-hover); + color: white; + } + + .notification-header-details { + color: var(--light-text-color); + } + } + } + + .notification-card:hover { + background-color: var(--color-dark-card-hover); + } + } \ No newline at end of file diff --git a/packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx b/packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/InstallNotifications.tsx similarity index 95% rename from packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx rename to packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/InstallNotifications.tsx index e63c0fff7e..3cd16c89ac 100644 --- a/packages/admin-ui/src/pages/notifications/components/InstallNotifications.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/InstallNotifications.tsx @@ -5,7 +5,7 @@ import { getInstallerPath } from "pages/installer/data"; import SubTitle from "components/SubTitle"; import Card from "components/Card"; -import "./notifications.scss"; +import "./installNotifications.scss"; interface InstallNotificationsPkgProps { pkgName: string; diff --git a/packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/installNotifications.scss b/packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/installNotifications.scss new file mode 100644 index 0000000000..d84515a4c6 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/InstallNotifications/installNotifications.scss @@ -0,0 +1,7 @@ +.install-notifications-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + \ No newline at end of file diff --git a/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx similarity index 99% rename from packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx rename to packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx index ffe577c378..09a19ae75f 100644 --- a/packages/admin-ui/src/pages/notifications/components/tabs/Settings.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx @@ -1,10 +1,10 @@ import SubTitle from "components/SubTitle"; import React, { useState } from "react"; import Switch from "components/Switch"; - -import "../notifications.scss"; import Slider from "components/Slider"; +import "./settings.scss"; + interface GatusConfig { endpoints: Endpoint[]; } diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss b/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss new file mode 100644 index 0000000000..d93d291bb3 --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss @@ -0,0 +1,58 @@ +.notifications-settings { + .title-switch-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + } + .notifications-section-title { + margin: 0.5rem 0; + } + + .manage-notifications-wrapper { + display: flex; + flex-direction: column; + gap: 15px; + } + + .notifications-pkg-name { + font-size: 1.2rem; + margin: 0.5rem 0; + } + + .endpoint-list-card { + border-radius: 10px; + background-color: #e9ecef; + padding: 20px 15px; + display: flex; + flex-direction: column; + + .endpoint-row { + display: flex; + flex-direction: row; + justify-content: space-between; + } + + hr { + width: 100%; + } + + .slider-wrapper{ + padding-top: 15px; + width: 50%; + + @media (max-width: 60rem) { + width: 100%; + } + } + } +} + +#dark { + .notifications-settings { + .endpoint-list-card { + background-color: var(--color-dark-card); + color: var(--color-dark-maintext); + } + } +} From 89ffe3d4db4146e37995107088f3a0700d974a9c Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 21:28:52 +0100 Subject: [PATCH 20/41] Settings components --- .../notifications/tabs/Settings/Settings.tsx | 66 +------------------ .../tabs/Settings/components/EndpointItem.tsx | 36 ++++++++++ .../components/ManagePackageSection.tsx | 35 ++++++++++ 3 files changed, 73 insertions(+), 64 deletions(-) create mode 100644 packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx create mode 100644 packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx index 09a19ae75f..694329a9c6 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx @@ -1,7 +1,7 @@ import SubTitle from "components/SubTitle"; import React, { useState } from "react"; import Switch from "components/Switch"; -import Slider from "components/Slider"; +import { ManagePackageSection } from "./components/ManagePackageSection"; import "./settings.scss"; @@ -9,7 +9,7 @@ interface GatusConfig { endpoints: Endpoint[]; } -interface Endpoint { +export interface Endpoint { name: string; enabled: boolean; url: string; @@ -183,65 +183,3 @@ export function NotificationsSettings() { </div> ); } - -function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endpoint[] }) { - const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); - - const handlePkgToggle = () => { - // TODO: update "notifications.yaml" file - setPkgNotificationsEnabled(!pkgNotificationsEnabled); - }; - - return ( - <div key={String(pkg)}> - <div className="title-switch-row"> - <SubTitle className="notifications-pkg-name">{pkg}</SubTitle> - <Switch - checked={pkgNotificationsEnabled} - onToggle={() => { - handlePkgToggle(); - }} - /> - </div> - {pkgNotificationsEnabled && ( - <div className="endpoint-list-card"> - {endpoints.map((endpoint, i) => ( - <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> - ))} - </div> - )} - </div> - ); -} - -function EndpointItem({ endpoint, index, numEndpoints }: { endpoint: Endpoint; index: number; numEndpoints: number }) { - const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); - - const handleEndpointToggle = () => { - // TODO: update "notifications.yaml" file - setEndpointEnabled(!endpointEnabled); - }; - - return ( - <> - <div key={index} className="endpoint-row"> - <div> - <strong>{endpoint.name}</strong> - <div>{endpoint.description}</div> - </div> - <Switch - checked={endpointEnabled} - onToggle={() => { - handleEndpointToggle(); - }} - /> - </div> - {endpointEnabled && endpoint.metric && ( - <div className="slider-wrapper"> - <Slider /> - </div> - )} - {index + 1 < numEndpoints && <hr />} - </> - ); -} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx new file mode 100644 index 0000000000..bb91ae169f --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -0,0 +1,36 @@ +import React, { useState } from "react"; +import { Endpoint } from "../Settings"; +import Switch from "components/Switch"; +import Slider from "components/Slider"; + +export function EndpointItem({ endpoint, index, numEndpoints }: { endpoint: Endpoint; index: number; numEndpoints: number }) { + const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); + + const handleEndpointToggle = () => { + // TODO: update "notifications.yaml" file + setEndpointEnabled(!endpointEnabled); + }; + + return ( + <> + <div key={index} className="endpoint-row"> + <div> + <strong>{endpoint.name}</strong> + <div>{endpoint.description}</div> + </div> + <Switch + checked={endpointEnabled} + onToggle={() => { + handleEndpointToggle(); + }} + /> + </div> + {endpointEnabled && endpoint.metric && ( + <div className="slider-wrapper"> + <Slider /> + </div> + )} + {index + 1 < numEndpoints && <hr />} + </> + ); + } \ No newline at end of file diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx new file mode 100644 index 0000000000..a05d6b66cb --- /dev/null +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -0,0 +1,35 @@ +import SubTitle from "components/SubTitle"; +import Switch from "components/Switch"; +import { Endpoint } from "../Settings"; +import React, { useState } from "react"; +import { EndpointItem } from "./EndpointItem"; + +export function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endpoint[] }) { + const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); + + const handlePkgToggle = () => { + // TODO: update "notifications.yaml" file + setPkgNotificationsEnabled(!pkgNotificationsEnabled); + }; + + return ( + <div key={String(pkg)}> + <div className="title-switch-row"> + <SubTitle className="notifications-pkg-name">{pkg}</SubTitle> + <Switch + checked={pkgNotificationsEnabled} + onToggle={() => { + handlePkgToggle(); + }} + /> + </div> + {pkgNotificationsEnabled && ( + <div className="endpoint-list-card"> + {endpoints.map((endpoint, i) => ( + <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> + ))} + </div> + )} + </div> + ); + } \ No newline at end of file From 9f6ec00fcb13fabe6e6c371d0db19bd01be2f5dd Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 21:50:19 +0100 Subject: [PATCH 21/41] Notifications card component updated --- .../Inbox/components/NotificationsCard.tsx | 42 ++--- .../pages/notifications/tabs/Inbox/inbox.scss | 143 +++++++++--------- 2 files changed, 98 insertions(+), 87 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx index 89aa672ac9..34a773724e 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -1,27 +1,31 @@ import React, { useState } from "react"; import { Accordion } from "react-bootstrap"; import { Notification } from "../Inbox"; +import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; export function NotificationCard({ notification }: { notification: Notification }) { - const [isOpen, setIsOpen] = useState(false); - - return ( - <Accordion defaultActiveKey={isOpen ? "0" : "1"} > - <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> - <div className="notification-header"> - <div className="notification-img"> IMG </div> - <div className="notification-header-data"> - <div className="notification-header-details"> - <div>{notification.dnp}</div> - <div>{new Date(notification.timestamp).toLocaleString()}</div> - </div> + const [isOpen, setIsOpen] = useState(false); + + return ( + <Accordion defaultActiveKey={isOpen ? "0" : "1"}> + <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> + <div className="notification-header"> + <div className="notification-img"> IMG </div> + <div className="notification-header-data"> + <div className="notification-header-row secondary-text"> + <div>{notification.dnp}</div> + <div>{new Date(notification.timestamp).toLocaleString()}</div> + </div> + <div className="notification-header-row "> <div className="notification-title">{notification.title}</div> + {isOpen ? <IoIosArrowUp /> : <IoIosArrowDown />} </div> </div> - <Accordion.Collapse eventKey="0"> - <div className="notification-body">{notification.body}</div> - </Accordion.Collapse> - </Accordion.Toggle>{" "} - </Accordion> - ); - } \ No newline at end of file + </div> + <Accordion.Collapse eventKey="0"> + <div className="notification-body">{notification.body}</div> + </Accordion.Collapse> + </Accordion.Toggle>{" "} + </Accordion> + ); +} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss index b12513acfd..6d0a5e3568 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss @@ -1,82 +1,89 @@ .notification-card { - border-radius: 10px; - background-color: #e9ecef; - padding: 15px; - cursor: pointer; - - .notification-header { + border-radius: 10px; + background-color: #e9ecef; + padding: 10px; + cursor: pointer; + + .notification-header { + display: flex; + flex-direction: row; + gap: 10px; + align-items: center; + width: 100%; + + .notification-img { + width: 50px; + height: 50px; + border-radius: 10px; display: flex; - flex-direction: row; - gap: 10px; + justify-content: center; align-items: center; - width: 100%; - - .notification-img { - width: 60px; - height: 60px; - border-radius: 10px; - display: flex; - justify-content: center; - align-items: center; - background-color: #a3a5a7; + background-color: #a3a5a7; + } + + .notification-header-data { + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; + + .notification-title { + font-size: 1.2rem; + @media (max-width: 40rem) { + font-size: 1rem; + } } - - .notification-header-data { + + .notification-header-row { display: flex; - flex-direction: column; justify-content: space-between; - flex-grow: 1; - - .notification-title { - font-size: 1.2rem; - @media (max-width: 40rem) { - font-size: 1rem; - } + align-items: center; + width: 100%; + + @media (max-width: 60rem) { + flex-direction: column; } - - .notification-header-details { - display: flex; - justify-content: space-between; - width: 100%; - color: var(--light-text-color); - font-style: italic; - - @media (max-width: 60rem) { - flex-direction: column; - font-size: 0.8rem; - } + + } + .secondary-text { + color: var(--light-text-color); + font-style: italic; + + @media (max-width: 60rem) { + font-size: 0.8rem; } } } - - .notification-body { - padding-top: 10px; - font-size: 1rem; - } } - .notification-card:hover { - background-color: var(--color-dark-quaternarytext); - transition: all 0.1s ease-in-out; + + .notification-body { + padding-top: 10px; + font-size: 1rem; } - - #dark { - .notification-card { - background-color: var(--color-dark-card); - color: var(--color-dark-maintext); - - .notification-header { - .notification-img { - background-color: var(--color-dark-card-hover); - color: white; - } - - .notification-header-details { - color: var(--light-text-color); - } +} +.notification-card:hover { + background-color: #f1f3f5; + transition: all 0.1s ease-in-out; +} + +#dark { + .notification-card { + background-color: var(--color-dark-card); + color: var(--color-dark-maintext); + + .notification-header { + .notification-img { + background-color: var(--color-dark-card-hover); + color: white; + } + + .notification-header-details { + color: var(--light-text-color); } } - - .notification-card:hover { - background-color: var(--color-dark-card-hover); - } - } \ No newline at end of file + } + + .notification-card:hover { + background-color: var(--color-dark-card-hover); + } +} From 0ddd83031ab17aa401c1db45305cfabc168c277b Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 22:35:45 +0100 Subject: [PATCH 22/41] Avatars in notifications list --- .../pages/notifications/tabs/Inbox/Inbox.tsx | 58 +++++++++++++++---- .../Inbox/components/NotificationsCard.tsx | 13 +++-- .../pages/notifications/tabs/Inbox/inbox.scss | 28 ++++----- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx index 7c3dae615d..b22d73c17a 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx @@ -4,6 +4,10 @@ import Card from "components/Card"; import "./inbox.scss"; import { NotificationCard } from "./components/NotificationsCard"; +import { useApi } from "api"; +import Loading from "components/Loading"; +import defaultAvatar from "img/defaultAvatar.png"; +import dappnodeIcon from "img/dappnode-logo-only.png"; export interface Notification { timestamp: string; @@ -17,44 +21,78 @@ export function Inbox() { { timestamp: "2021-06-01T12:00:00Z", title: "New Notification", - dnp: "package-name.dnp.dappnode.eth", + dnp: "dappmanager.dnp.dappnode.eth", body: "This is a new notification" }, { timestamp: "2025-06-06T13:00:00Z", title: "Validator exited sucesfully", - dnp: "lido-csm-mainnet.dnp.dappnode.eth", - body: "Your validator 10802082 has entered the exit queue automatically. No manual action required" + dnp: "holesky-reth.dnp.dappnode.eth", + body: "Execution client synced" } ]; const seenNotifications: Notification[] = [ + { + timestamp: "2021-06-01T12:00:00Z", + title: "Execution client synced", + dnp: "besu.public.dappnode.eth", + body: "This is a seen notification" + }, + { + timestamp: "2021-06-01T12:00:00Z", + title: "Execution client synced", + dnp: "ipfs.dnp.dappnode.eth", + body: "This is a seen notification" + }, { timestamp: "2021-06-01T12:00:00Z", title: "Seen Notification", - dnp: "package-name.dnp.dappnode.eth", + dnp: "lighthouse-holesky.dnp.dappnode.eth", body: "This is a seen notification" }, { timestamp: "2025-06-04T12:00:00Z", title: "Updated Telegram configuration", - dnp: "lido-csm-mainnet.dnp.dappnode.eth", + dnp: "lido-csm-holesky.dnp.dappnode.eth", body: "Your telegram configuration has been updated successfully" }, + { + timestamp: "2025-06-04T12:00:00Z", + title: "Relays configuration updated", + dnp: "mev-boost.dnp.dappnode.eth", + body: "Your relays configuration in mainnet has been updated successfully" + }, { timestamp: "2025-06-06T12:00:00Z", title: "Validator exit request", - dnp: "lido-csm-mainnet.dnp.dappnode.eth", + dnp: "lido-csm-holesky.dnp.dappnode.eth", body: "Your validator 10802082 has requested to exit the network. Executing automatic exit" } ]; - return ( + const dnpsRequest = useApi.packagesGet(); + const loading = dnpsRequest.isValidating; + const installedDnps = dnpsRequest.data; + const findPkgAvatar = (dnpName: string) => { + const dnp = installedDnps?.find((dnp) => dnp.dnpName === dnpName); + + if (!dnp) { + return defaultAvatar; + } else if (dnp.isCore) { + return dappnodeIcon; + } + return dnp.avatarUrl; + }; + + return loading ? ( + <Loading steps={["Loading data"]} /> + ) : ( <> {newNotifications.length > 0 && ( <> <SubTitle>New Notifications</SubTitle> {newNotifications.map((notification) => ( - <NotificationCard key={notification.timestamp} notification={notification} /> + <NotificationCard key={notification.timestamp} notification={notification} avatarUrl={findPkgAvatar(notification.dnp)} /> ))} </> )} @@ -64,11 +102,9 @@ export function Inbox() { <Card>No notifications</Card> ) : ( seenNotifications.map((notification) => ( - <NotificationCard key={notification.timestamp} notification={notification} /> + <NotificationCard key={notification.timestamp} notification={notification} avatarUrl={findPkgAvatar(notification.dnp)} /> )) )} </> ); } - - diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx index 34a773724e..2a4e3ff814 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -3,18 +3,23 @@ import { Accordion } from "react-bootstrap"; import { Notification } from "../Inbox"; import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; -export function NotificationCard({ notification }: { notification: Notification }) { +interface NotificationCardProps { + notification: Notification; + avatarUrl: string; +} + +export function NotificationCard({ notification, avatarUrl }: NotificationCardProps) { const [isOpen, setIsOpen] = useState(false); return ( <Accordion defaultActiveKey={isOpen ? "0" : "1"}> <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> <div className="notification-header"> - <div className="notification-img"> IMG </div> + <img className="avatar" src={avatarUrl} alt={notification.dnp} /> <div className="notification-header-data"> <div className="notification-header-row secondary-text"> <div>{notification.dnp}</div> - <div>{new Date(notification.timestamp).toLocaleString()}</div> + <i>{new Date(notification.timestamp).toLocaleString()}</i> </div> <div className="notification-header-row "> <div className="notification-title">{notification.title}</div> @@ -25,7 +30,7 @@ export function NotificationCard({ notification }: { notification: Notification <Accordion.Collapse eventKey="0"> <div className="notification-body">{notification.body}</div> </Accordion.Collapse> - </Accordion.Toggle>{" "} + </Accordion.Toggle> </Accordion> ); } diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss index 6d0a5e3568..b82798bb2f 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss @@ -11,20 +11,21 @@ align-items: center; width: 100%; - .notification-img { - width: 50px; - height: 50px; - border-radius: 10px; - display: flex; - justify-content: center; - align-items: center; - background-color: #a3a5a7; + .avatar { + width: 40px; + height: 40px; + + @media (max-width: 60rem) { + width: 35px; + height: 35px; + } } .notification-header-data { display: flex; flex-direction: column; justify-content: space-between; + text-align: left; flex-grow: 1; .notification-title { @@ -37,20 +38,15 @@ .notification-header-row { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; width: 100%; - - @media (max-width: 60rem) { - flex-direction: column; - } - } .secondary-text { color: var(--light-text-color); - font-style: italic; - + font-size: 1rem; @media (max-width: 60rem) { font-size: 0.8rem; + flex-direction: column-reverse; } } } From 54585f40177c9591f4bdaa4a87d795f0d6e9da45 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 6 Mar 2025 22:41:05 +0100 Subject: [PATCH 23/41] slider style fix --- .../src/pages/notifications/tabs/Settings/settings.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss b/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss index d93d291bb3..e826517ea0 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/settings.scss @@ -39,7 +39,7 @@ .slider-wrapper{ padding-top: 15px; - width: 50%; + width: 30%; @media (max-width: 60rem) { width: 100%; From d585c12a080d459bbab7983e39db898cc43c46be Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Fri, 7 Mar 2025 11:18:55 +0100 Subject: [PATCH 24/41] add dappmnode custom type --- packages/types/src/notifications.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts index 440570aaf4..e67a5647fc 100644 --- a/packages/types/src/notifications.ts +++ b/packages/types/src/notifications.ts @@ -11,7 +11,11 @@ export interface Endpoint { interval: string; // e.g., "1m" group: string; alerts: Alert[]; - description: string; // dappnode specific + definition: { + // dappnode specific + title: string; + description: string; + }; metric?: { // dappnode specific min: number; From 05806cd4a88b6eba718ff5f83c864925f2094431 Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Fri, 7 Mar 2025 12:29:34 +0100 Subject: [PATCH 25/41] commit for demo --- .../admin-ui/src/__mock-backend__/index.ts | 3 +- .../pages/notifications/tabs/Inbox/Inbox.tsx | 82 ++------- .../Inbox/components/NotificationsCard.tsx | 6 +- .../notifications/tabs/Settings/Settings.tsx | 155 +----------------- .../tabs/Settings/components/EndpointItem.tsx | 72 ++++---- .../components/ManagePackageSection.tsx | 61 +++---- packages/dappmanager/src/calls/gatusConfig.ts | 17 +- packages/dappmanager/src/calls/index.ts | 2 +- packages/types/src/notifications.ts | 18 ++ packages/types/src/routes.ts | 11 +- 10 files changed, 139 insertions(+), 288 deletions(-) diff --git a/packages/admin-ui/src/__mock-backend__/index.ts b/packages/admin-ui/src/__mock-backend__/index.ts index f47b444496..dd37b7a067 100644 --- a/packages/admin-ui/src/__mock-backend__/index.ts +++ b/packages/admin-ui/src/__mock-backend__/index.ts @@ -388,7 +388,8 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = { getIsConnectedToInternet: async () => false, getCoreVersion: async () => "0.2.92", gatusGetEndpoints: async () => new Map(), - gatusUpdateEndpoint: async () => {} + gatusUpdateEndpoint: async () => {}, + gatuGetAllNotifications: async () => [] }; export const calls: Routes = { diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx index b22d73c17a..3b18138e24 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx @@ -1,7 +1,6 @@ import SubTitle from "components/SubTitle"; import React from "react"; import Card from "components/Card"; - import "./inbox.scss"; import { NotificationCard } from "./components/NotificationsCard"; import { useApi } from "api"; @@ -9,73 +8,16 @@ import Loading from "components/Loading"; import defaultAvatar from "img/defaultAvatar.png"; import dappnodeIcon from "img/dappnode-logo-only.png"; -export interface Notification { - timestamp: string; - title: string; - dnp: string; - body: string; -} - export function Inbox() { - const newNotifications: Notification[] = [ - { - timestamp: "2021-06-01T12:00:00Z", - title: "New Notification", - dnp: "dappmanager.dnp.dappnode.eth", - body: "This is a new notification" - }, - { - timestamp: "2025-06-06T13:00:00Z", - title: "Validator exited sucesfully", - dnp: "holesky-reth.dnp.dappnode.eth", - body: "Execution client synced" - } - ]; - const seenNotifications: Notification[] = [ - { - timestamp: "2021-06-01T12:00:00Z", - title: "Execution client synced", - dnp: "besu.public.dappnode.eth", - body: "This is a seen notification" - }, - { - timestamp: "2021-06-01T12:00:00Z", - title: "Execution client synced", - dnp: "ipfs.dnp.dappnode.eth", - body: "This is a seen notification" - }, - { - timestamp: "2021-06-01T12:00:00Z", - title: "Seen Notification", - dnp: "lighthouse-holesky.dnp.dappnode.eth", - body: "This is a seen notification" - }, - { - timestamp: "2025-06-04T12:00:00Z", - title: "Updated Telegram configuration", - dnp: "lido-csm-holesky.dnp.dappnode.eth", - body: "Your telegram configuration has been updated successfully" - }, - { - timestamp: "2025-06-04T12:00:00Z", - title: "Relays configuration updated", - dnp: "mev-boost.dnp.dappnode.eth", - body: "Your relays configuration in mainnet has been updated successfully" - }, - { - timestamp: "2025-06-06T12:00:00Z", - title: "Validator exit request", - dnp: "lido-csm-holesky.dnp.dappnode.eth", - body: "Your validator 10802082 has requested to exit the network. Executing automatic exit" - } - ]; - const dnpsRequest = useApi.packagesGet(); + const notifications = useApi.gatuGetAllNotifications(); + const newNotifications = notifications.data?.filter((notification) => !notification.seen); + const seenNotifications = notifications.data?.filter((notification) => notification.seen); const loading = dnpsRequest.isValidating; const installedDnps = dnpsRequest.data; const findPkgAvatar = (dnpName: string) => { const dnp = installedDnps?.find((dnp) => dnp.dnpName === dnpName); - + if (!dnp) { return defaultAvatar; } else if (dnp.isCore) { @@ -88,21 +30,29 @@ export function Inbox() { <Loading steps={["Loading data"]} /> ) : ( <> - {newNotifications.length > 0 && ( + {newNotifications && newNotifications.length > 0 && ( <> <SubTitle>New Notifications</SubTitle> {newNotifications.map((notification) => ( - <NotificationCard key={notification.timestamp} notification={notification} avatarUrl={findPkgAvatar(notification.dnp)} /> + <NotificationCard + key={notification.timestamp} + notification={notification} + avatarUrl={findPkgAvatar(notification.dnpName)} + /> ))} </> )} <SubTitle>History</SubTitle> - {seenNotifications.length === 0 ? ( + {!seenNotifications || seenNotifications.length === 0 ? ( <Card>No notifications</Card> ) : ( seenNotifications.map((notification) => ( - <NotificationCard key={notification.timestamp} notification={notification} avatarUrl={findPkgAvatar(notification.dnp)} /> + <NotificationCard + key={notification.timestamp} + notification={notification} + avatarUrl={findPkgAvatar(notification.dnpName)} + /> )) )} </> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx index 2a4e3ff814..365d89961f 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { Accordion } from "react-bootstrap"; -import { Notification } from "../Inbox"; +import { Notification } from "@dappnode/types"; import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; interface NotificationCardProps { @@ -15,10 +15,10 @@ export function NotificationCard({ notification, avatarUrl }: NotificationCardPr <Accordion defaultActiveKey={isOpen ? "0" : "1"}> <Accordion.Toggle as={"div"} eventKey="0" onClick={() => setIsOpen(!isOpen)} className="notification-card"> <div className="notification-header"> - <img className="avatar" src={avatarUrl} alt={notification.dnp} /> + <img className="avatar" src={avatarUrl} alt={notification.dnpName} /> <div className="notification-header-data"> <div className="notification-header-row secondary-text"> - <div>{notification.dnp}</div> + <div>{notification.dnpName}</div> <i>{new Date(notification.timestamp).toLocaleString()}</i> </div> <div className="notification-header-row "> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx index 694329a9c6..f2d71f0182 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx @@ -2,156 +2,12 @@ import SubTitle from "components/SubTitle"; import React, { useState } from "react"; import Switch from "components/Switch"; import { ManagePackageSection } from "./components/ManagePackageSection"; - +import { useApi } from "api"; import "./settings.scss"; -interface GatusConfig { - endpoints: Endpoint[]; -} - -export interface Endpoint { - name: string; - enabled: boolean; - url: string; - method: string; - conditions: string[]; - interval: string; // e.g., "1m" - group: string; - description: string; // dappnode specific - metric?: { - // dappnode specific - min: number; - max: number; - unit: string; // e.g ºC - }; -} - export function NotificationsSettings() { const [notificationsEnabled, setNotificationsEnabled] = useState(true); - - const fakeNotifications: Map<String, GatusConfig> = new Map([ - [ - "Dappmanager", - { - endpoints: [ - { - name: "Installed packages updates", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Core", - description: "Sends a notification whenever a new version of an installed package is released" - } - ] - } - ], - [ - "DMS", - { - endpoints: [ - { - name: "Disk space", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Core", - description: "Sends a notification whenever your storage disk exceeds the specified percentage.", - metric: { - min: 0, - max: 100, - unit: "%" - } - } - ] - } - ], - - [ - "Web3Signer", - { - endpoints: [ - { - name: "Validator offline / back online", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Staking", - description: "Sends a notification whenever one of your validators toggles its state." - }, - { - name: "Missed attestation", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Staking", - description: "Sends a notification whenever one of your validators misses an attestation." - }, - { - name: "Missed proposal", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Staking", - description: "Sends a notification whenever one of your validators misses a block proposal." - }, - { - name: "Submitted Proposal", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Staking", - description: "Sends a notification whenever one of your validators submits a block proposal successfully." - }, - { - name: "Efectiveness", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Staking", - description: - "Sends a notification whenever the effectiveness of one of your validators falls below the specified percentage.", - metric: { - min: 0, - max: 100, - unit: "%" - } - } - ] - } - ], - [ - "Lido CSM", - { - endpoints: [ - { - name: "Validator exited sucesfully", - enabled: true, - url: "", - method: "", - conditions: [""], - interval: "1m", - group: "Core", - description: - "Sends a notification whenever a validator has entered the exit queue automatically. No manual action required" - } - ] - } - ] - ]); + const endpoints = useApi.gatusGetEndpoints(); return ( <div className="notifications-settings"> @@ -174,9 +30,10 @@ export function NotificationsSettings() { <div>Enable, disable and customize notifications individually.</div> <br /> <div className="manage-notifications-wrapper"> - {[...fakeNotifications.entries()].map(([key, value]) => ( - <ManagePackageSection pkg={String(key)} endpoints={value.endpoints} /> - ))} + {endpoints.data && + Array.from(endpoints.data).map(([dnpName, endpoints]) => ( + <ManagePackageSection key={dnpName} dnpName={dnpName} endpoints={endpoints} /> + ))} </div> </div> )} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx index bb91ae169f..f74abff06a 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -1,36 +1,44 @@ -import React, { useState } from "react"; -import { Endpoint } from "../Settings"; +import React, { useState } from "react"; +import { Endpoint } from "@dappnode/types"; import Switch from "components/Switch"; import Slider from "components/Slider"; -export function EndpointItem({ endpoint, index, numEndpoints }: { endpoint: Endpoint; index: number; numEndpoints: number }) { - const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); - - const handleEndpointToggle = () => { - // TODO: update "notifications.yaml" file - setEndpointEnabled(!endpointEnabled); - }; - - return ( - <> - <div key={index} className="endpoint-row"> - <div> - <strong>{endpoint.name}</strong> - <div>{endpoint.description}</div> - </div> - <Switch - checked={endpointEnabled} - onToggle={() => { - handleEndpointToggle(); - }} - /> +export function EndpointItem({ + endpoint, + index, + numEndpoints +}: { + endpoint: Endpoint; + index: number; + numEndpoints: number; +}) { + const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); + + const handleEndpointToggle = () => { + // TODO: update "notifications.yaml" file + setEndpointEnabled(!endpointEnabled); + }; + + return ( + <> + <div key={index} className="endpoint-row"> + <div> + <strong>{endpoint.definition.title}</strong> + <div>{endpoint.definition.description}</div> + </div> + <Switch + checked={endpointEnabled} + onToggle={() => { + handleEndpointToggle(); + }} + /> + </div> + {endpointEnabled && endpoint.metric && ( + <div className="slider-wrapper"> + <Slider /> </div> - {endpointEnabled && endpoint.metric && ( - <div className="slider-wrapper"> - <Slider /> - </div> - )} - {index + 1 < numEndpoints && <hr />} - </> - ); - } \ No newline at end of file + )} + {index + 1 < numEndpoints && <hr />} + </> + ); +} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index a05d6b66cb..3d4ed7d118 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -1,35 +1,36 @@ import SubTitle from "components/SubTitle"; import Switch from "components/Switch"; -import { Endpoint } from "../Settings"; -import React, { useState } from "react"; +import React, { useState } from "react"; import { EndpointItem } from "./EndpointItem"; +import { Endpoint } from "@dappnode/types"; +import { prettyDnpName } from "utils/format"; -export function ManagePackageSection({ pkg, endpoints }: { pkg: string; endpoints: Endpoint[] }) { - const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); - - const handlePkgToggle = () => { - // TODO: update "notifications.yaml" file - setPkgNotificationsEnabled(!pkgNotificationsEnabled); - }; - - return ( - <div key={String(pkg)}> - <div className="title-switch-row"> - <SubTitle className="notifications-pkg-name">{pkg}</SubTitle> - <Switch - checked={pkgNotificationsEnabled} - onToggle={() => { - handlePkgToggle(); - }} - /> - </div> - {pkgNotificationsEnabled && ( - <div className="endpoint-list-card"> - {endpoints.map((endpoint, i) => ( - <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> - ))} - </div> - )} +export function ManagePackageSection({ dnpName, endpoints }: { dnpName: string; endpoints: Endpoint[] }) { + const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); + + const handlePkgToggle = () => { + // TODO: update "notifications.yaml" file + setPkgNotificationsEnabled(!pkgNotificationsEnabled); + }; + + return ( + <div key={String(dnpName)}> + <div className="title-switch-row"> + <SubTitle className="notifications-pkg-name">{prettyDnpName(dnpName)}</SubTitle> + <Switch + checked={pkgNotificationsEnabled} + onToggle={() => { + handlePkgToggle(); + }} + /> </div> - ); - } \ No newline at end of file + {pkgNotificationsEnabled && ( + <div className="endpoint-list-card"> + {endpoints.map((endpoint, i) => ( + <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> + ))} + </div> + )} + </div> + ); +} diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts index 4cb22a8df4..341ba5bb62 100644 --- a/packages/dappmanager/src/calls/gatusConfig.ts +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -1,20 +1,29 @@ import { listPackages } from "@dappnode/dockerapi"; -import { GatusConfig, Endpoint, Manifest } from "@dappnode/types"; +import { Endpoint, Manifest, Notification } from "@dappnode/types"; import { getManifestPath } from "@dappnode/utils"; import fs from "fs"; +/** + * Get all the notifications + * @returns all the notifications + */ +export async function gatuGetAllNotifications(): Promise<Notification[]> { + const response = await fetch(`http://notifier.notifications.dappnode:8080/api/v1/notifications`); + return response.json(); +} + /** * Get gatus endpoints indexed by dnpName */ -export async function gatusGetEndpoints(): Promise<Map<string, GatusConfig>> { +export async function gatusGetEndpoints(): Promise<Map<string, Endpoint[]>> { const packages = await listPackages(); // Read all manifests files and retrieve the gatus config - const endpoints = new Map<string, GatusConfig>(); + const endpoints = new Map<string, Endpoint[]>(); for (const pkg of packages) { const manifestPath = getManifestPath(pkg.dnpName, pkg.isCore); const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - if (manifest.notifications) endpoints.set(pkg.dnpName, manifest.notifications); + if (manifest.notifications) endpoints.set(pkg.dnpName, manifest.notifications.endpoints); } return endpoints; diff --git a/packages/dappmanager/src/calls/index.ts b/packages/dappmanager/src/calls/index.ts index e2b1b1f658..9d430921a3 100644 --- a/packages/dappmanager/src/calls/index.ts +++ b/packages/dappmanager/src/calls/index.ts @@ -22,7 +22,7 @@ export { getCoreVersion } from "./getCoreVersion.js"; export { getUserActionLogs } from "./getUserActionLogs.js"; export { getHostUptime } from "./getHostUptime.js"; export { getIsConnectedToInternet } from "./getIsConnectedToInternet.js"; -export { gatusGetEndpoints, gatusUpdateEndpoint } from "./gatusConfig.js"; +export { gatusGetEndpoints, gatusUpdateEndpoint, gatuGetAllNotifications } from "./gatusConfig.js"; export * from "./httpsPortal.js"; export { ipfsTest } from "./ipfsTest.js"; export { ipfsClientTargetSet } from "./ipfsClientTargetSet.js"; diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts index e67a5647fc..1fa28adcfa 100644 --- a/packages/types/src/notifications.ts +++ b/packages/types/src/notifications.ts @@ -1,3 +1,16 @@ +export interface Notification { + title: string; + body: string; + dnpName: string; + timestamp: string; + category: string; + seen: boolean; + callToAction?: { + title: string; + url: string; + }; +} + export interface GatusConfig { endpoints: Endpoint[]; } @@ -30,4 +43,9 @@ interface Alert { "success-threshold": number; "send-on-resolved": boolean; description: string; + url: string; + method: string; + body: string; + headers: Record<string, string>; + placeholders: Record<string, Record<string, string>>; } diff --git a/packages/types/src/routes.ts b/packages/types/src/routes.ts index cb15f79aef..8f4e0dca61 100644 --- a/packages/types/src/routes.ts +++ b/packages/types/src/routes.ts @@ -45,10 +45,11 @@ import { } from "./calls.js"; import { PackageEnvs } from "./compose.js"; import { PackageBackup } from "./manifest.js"; -import { Endpoint, GatusConfig } from "./notifications.js"; +import { Endpoint } from "./notifications.js"; import { TrustedReleaseKey } from "./pkg.js"; import { OptimismConfigSet, OptimismConfigGet } from "./rollups.js"; import { Network, StakerConfigGet, StakerConfigSet } from "./stakers.js"; +import { Notification } from "./notifications.js"; export interface Routes { /** @@ -262,10 +263,15 @@ export interface Routes { */ fetchDnpRequest: (kwargs: { id: string; version?: string }) => Promise<RequestedDnp>; + /** + * Gatus get all notifications + */ + gatuGetAllNotifications(): Promise<Notification[]>; + /** * Gatus get endpoints */ - gatusGetEndpoints(): Promise<Map<string, GatusConfig>>; + gatusGetEndpoints(): Promise<Map<string, Endpoint[]>>; /** * Gatus update endpoint @@ -701,6 +707,7 @@ export const routesData: { [P in keyof Routes]: RouteData } = { fetchDirectory: {}, fetchRegistry: {}, fetchDnpRequest: {}, + gatuGetAllNotifications: { log: true }, gatusGetEndpoints: { log: true }, gatusUpdateEndpoint: { log: true }, getUserActionLogs: {}, From 5526b32355a7fcb0b8e1f9f4fa9a7df4aa148e12 Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Fri, 7 Mar 2025 12:49:22 +0100 Subject: [PATCH 26/41] et dnp instead of public --- packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx index 228299b524..96b1045b81 100644 --- a/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx +++ b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx @@ -33,7 +33,7 @@ export const NotificationsRoot: React.FC = () => { const loading = dnpsRequest.isValidating; const installedDnps = dnpsRequest.data; - const notificationsDnpName = "notifications.public.dappnode.eth"; + const notificationsDnpName = "notifications.dnp.dappnode.eth"; const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === notificationsDnpName); return ( From 3a179337e9ee50a01a8e105601abeb6604192ec6 Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Fri, 7 Mar 2025 13:11:32 +0100 Subject: [PATCH 27/41] fix commit for demo --- packages/admin-ui/src/__mock-backend__/index.ts | 4 +++- .../src/pages/notifications/tabs/Settings/Settings.tsx | 6 +++--- packages/dappmanager/src/calls/gatusConfig.ts | 8 +++++--- packages/types/src/routes.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/admin-ui/src/__mock-backend__/index.ts b/packages/admin-ui/src/__mock-backend__/index.ts index dd37b7a067..9e68977561 100644 --- a/packages/admin-ui/src/__mock-backend__/index.ts +++ b/packages/admin-ui/src/__mock-backend__/index.ts @@ -387,7 +387,9 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = { }), getIsConnectedToInternet: async () => false, getCoreVersion: async () => "0.2.92", - gatusGetEndpoints: async () => new Map(), + gatusGetEndpoints: async () => { + return { "geth.dnp.dappnode.eth": [] }; + }, gatusUpdateEndpoint: async () => {}, gatuGetAllNotifications: async () => [] }; diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx index f2d71f0182..f67c31e680 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/Settings.tsx @@ -7,7 +7,7 @@ import "./settings.scss"; export function NotificationsSettings() { const [notificationsEnabled, setNotificationsEnabled] = useState(true); - const endpoints = useApi.gatusGetEndpoints(); + const endpointsCall = useApi.gatusGetEndpoints(); return ( <div className="notifications-settings"> @@ -30,8 +30,8 @@ export function NotificationsSettings() { <div>Enable, disable and customize notifications individually.</div> <br /> <div className="manage-notifications-wrapper"> - {endpoints.data && - Array.from(endpoints.data).map(([dnpName, endpoints]) => ( + {endpointsCall.data && + Object.entries(endpointsCall.data).map(([dnpName, endpoints]) => ( <ManagePackageSection key={dnpName} dnpName={dnpName} endpoints={endpoints} /> ))} </div> diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts index 341ba5bb62..610ba5f5df 100644 --- a/packages/dappmanager/src/calls/gatusConfig.ts +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -15,15 +15,17 @@ export async function gatuGetAllNotifications(): Promise<Notification[]> { /** * Get gatus endpoints indexed by dnpName */ -export async function gatusGetEndpoints(): Promise<Map<string, Endpoint[]>> { +export async function gatusGetEndpoints(): Promise<{ [dnpName: string]: Endpoint[] }> { const packages = await listPackages(); // Read all manifests files and retrieve the gatus config - const endpoints = new Map<string, Endpoint[]>(); + const endpoints: { [dnpName: string]: Endpoint[] } = {}; for (const pkg of packages) { const manifestPath = getManifestPath(pkg.dnpName, pkg.isCore); const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); - if (manifest.notifications) endpoints.set(pkg.dnpName, manifest.notifications.endpoints); + if (manifest.notifications) { + endpoints[pkg.dnpName] = manifest.notifications.endpoints; + } } return endpoints; diff --git a/packages/types/src/routes.ts b/packages/types/src/routes.ts index 8f4e0dca61..caa49b2352 100644 --- a/packages/types/src/routes.ts +++ b/packages/types/src/routes.ts @@ -271,7 +271,7 @@ export interface Routes { /** * Gatus get endpoints */ - gatusGetEndpoints(): Promise<Map<string, Endpoint[]>>; + gatusGetEndpoints(): Promise<{ [dnpName: string]: Endpoint[] }>; /** * Gatus update endpoint From 01a2d3192c51b750ec0afae55e24c167689fbb49 Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Fri, 7 Mar 2025 13:56:13 +0100 Subject: [PATCH 28/41] add missing headers --- packages/types/src/notifications.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts index 1fa28adcfa..b3c22036a8 100644 --- a/packages/types/src/notifications.ts +++ b/packages/types/src/notifications.ts @@ -19,6 +19,7 @@ export interface Endpoint { name: string; enabled: boolean; url: string; + headers: Record<string, string>; method: string; conditions: string[]; interval: string; // e.g., "1m" From c6f213af786dc648ec740bc88b7afa3a28cbd075 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 10 Mar 2025 16:04:50 +0100 Subject: [PATCH 29/41] Installer Notifications step --- .../installer/components/InstallDnpView.tsx | 17 ++++++++++++++++- .../components/ManagePackageSection.tsx | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx index 251ccdb55b..9bc43138d8 100644 --- a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx +++ b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { api } from "api"; +import { api, useApi } from "api"; import { useDispatch } from "react-redux"; import { Routes, Route, useNavigate, useLocation, useParams, NavLink } from "react-router-dom"; import { isEmpty, throttle } from "lodash-es"; @@ -28,6 +28,7 @@ import { RequestedDnp, UserSettingsAllDnps } from "@dappnode/types"; import { diff } from "semver"; import Button from "components/Button"; import { pathName as systemPathName, subPaths as systemSubPaths } from "pages/system/data"; +import { ManagePackageSection } from "pages/notifications/tabs/Settings/components/ManagePackageSection"; interface InstallDnpViewProps { dnp: RequestedDnp; @@ -172,6 +173,13 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => } ].filter((option) => option.available); + const dnpsRequest = useApi.packagesGet(); + const installedDnps = dnpsRequest.data; + const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === "notifications.dnp.dappnode.eth"); + + const endpointsCall = useApi.gatusGetEndpoints(); + const dnpNotificationEndpoints = endpointsCall.data && endpointsCall.data[dnpName]; + const disableInstallation = !isEmpty(progressLogs) || requiresCoreUpdate || requiresDockerUpdate || packagesToBeUninstalled.length > 0; @@ -179,6 +187,7 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => const permissionsSubPath = "permissions"; const warningsSubPath = "warnings"; const disclaimerSubPath = "disclaimer"; + const notificationsSubPath = "notifications"; const installSubPath = "install"; const availableRoutes: { @@ -228,6 +237,12 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => render: () => <Disclaimer disclaimers={disclaimers} onAccept={goNext} goBack={goBack} />, available: disclaimers.length > 0 }, + { + name: "Notifications", + subPath: notificationsSubPath, + render: () => <ManagePackageSection dnpName="Enable notifications" endpoints={dnpNotificationEndpoints || []} />, + available: isNotificationsPkgInstalled && dnpNotificationEndpoints && dnpNotificationEndpoints.length > 0 + }, // Placeholder for the final step in the horizontal stepper { name: "Install", diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index 3d4ed7d118..81d0ce2af8 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -14,7 +14,7 @@ export function ManagePackageSection({ dnpName, endpoints }: { dnpName: string; }; return ( - <div key={String(dnpName)}> + <div key={String(dnpName)} className="notifications-settings"> <div className="title-switch-row"> <SubTitle className="notifications-pkg-name">{prettyDnpName(dnpName)}</SubTitle> <Switch From 103cebf158a0fdef127f78259da7d515b6d19aff Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Tue, 11 Mar 2025 17:03:10 +0100 Subject: [PATCH 30/41] step btns --- .../pages/installer/components/InstallDnpView.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx index 9bc43138d8..e80b886498 100644 --- a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx +++ b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx @@ -240,7 +240,17 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => { name: "Notifications", subPath: notificationsSubPath, - render: () => <ManagePackageSection dnpName="Enable notifications" endpoints={dnpNotificationEndpoints || []} />, + render: () => ( + <> + <ManagePackageSection dnpName="Enable notifications" endpoints={dnpNotificationEndpoints || []} /> + <div className="button-group"> + <Button onClick={goBack}>Back</Button> + <Button variant="dappnode" onClick={() => goNext()}> + Next + </Button> + </div> + </> + ), available: isNotificationsPkgInstalled && dnpNotificationEndpoints && dnpNotificationEndpoints.length > 0 }, // Placeholder for the final step in the horizontal stepper From 91f5fbd61b0c3881a8415ddc17232ba6d8cab6d2 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Wed, 12 Mar 2025 12:15:31 +0100 Subject: [PATCH 31/41] NotificationCard component styles --- .../pages/notifications/tabs/Inbox/Inbox.tsx | 8 +++++-- .../Inbox/components/NotificationsCard.tsx | 8 ++++++- .../pages/notifications/tabs/Inbox/inbox.scss | 23 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx index 3b18138e24..a681d32ab6 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/Inbox.tsx @@ -11,8 +11,12 @@ import dappnodeIcon from "img/dappnode-logo-only.png"; export function Inbox() { const dnpsRequest = useApi.packagesGet(); const notifications = useApi.gatuGetAllNotifications(); - const newNotifications = notifications.data?.filter((notification) => !notification.seen); - const seenNotifications = notifications.data?.filter((notification) => notification.seen); + const newNotifications = notifications.data + ?.filter((notification) => !notification.seen) + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + const seenNotifications = notifications.data + ?.filter((notification) => notification.seen) + .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); const loading = dnpsRequest.isValidating; const installedDnps = dnpsRequest.data; const findPkgAvatar = (dnpName: string) => { diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx index 365d89961f..6097f2cebf 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { Accordion } from "react-bootstrap"; import { Notification } from "@dappnode/types"; import { IoIosArrowDown, IoIosArrowUp } from "react-icons/io"; +import { prettyDnpName } from "utils/format"; interface NotificationCardProps { notification: Notification; @@ -18,7 +19,12 @@ export function NotificationCard({ notification, avatarUrl }: NotificationCardPr <img className="avatar" src={avatarUrl} alt={notification.dnpName} /> <div className="notification-header-data"> <div className="notification-header-row secondary-text"> - <div>{notification.dnpName}</div> + <div className="notification-name-row"> + <div>{prettyDnpName(notification.dnpName)}</div> + <div className="group-label">{notification.category}</div> + {notification.body.includes("Resolved: ") && <div className="sucess-label">success</div>} + </div> + <i>{new Date(notification.timestamp).toLocaleString()}</i> </div> <div className="notification-header-row "> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss index b82798bb2f..47b238e8d2 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss @@ -40,6 +40,11 @@ justify-content: space-between; align-items: flex-start; width: 100%; + + .notification-name-row { + display: flex; + gap: 5px; + } } .secondary-text { color: var(--light-text-color); @@ -49,6 +54,20 @@ flex-direction: column-reverse; } } + + .group-label { + display: flex; + align-items: center; + background-color: var(--color-light-border); + padding: 0px 5px; + border-radius: 8px; + font-size: 0.7rem; + } + .sucess-label { + @extend .group-label; + background-color: rgba(0, 109, 29, 0.7) !important; + color: white; + } } } @@ -76,6 +95,10 @@ .notification-header-details { color: var(--light-text-color); } + + .group-label { + background-color: var(--color-dark-border); + } } } From 8b1af173e5cc45af2898c71d92a4c88607722837 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Thu, 13 Mar 2025 12:30:10 +0100 Subject: [PATCH 32/41] Avoid unecessary re-renders --- .../components/ManagePackageSection.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index 81d0ce2af8..514b6c6a51 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -5,11 +5,16 @@ import { EndpointItem } from "./EndpointItem"; import { Endpoint } from "@dappnode/types"; import { prettyDnpName } from "utils/format"; -export function ManagePackageSection({ dnpName, endpoints }: { dnpName: string; endpoints: Endpoint[] }) { - const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(true); +interface ManagePackageSectionProps { + dnpName: string; + endpoints: Endpoint[]; +} +export function ManagePackageSection({ dnpName, endpoints }: ManagePackageSectionProps) { + const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(endpoints.some((ep) => ep.enabled)); + const [pkgEndpoints, setPkgEndpoints] = useState(endpoints); const handlePkgToggle = () => { - // TODO: update "notifications.yaml" file + setPkgEndpoints(pkgEndpoints.map((ep) => ({ ...ep, enabled: !pkgNotificationsEnabled }))); setPkgNotificationsEnabled(!pkgNotificationsEnabled); }; @@ -26,8 +31,13 @@ export function ManagePackageSection({ dnpName, endpoints }: { dnpName: string; </div> {pkgNotificationsEnabled && ( <div className="endpoint-list-card"> - {endpoints.map((endpoint, i) => ( - <EndpointItem endpoint={endpoint} index={i} numEndpoints={endpoints.length} /> + {pkgEndpoints.map((endpoint, i) => ( + <EndpointItem + endpoint={endpoint} + index={i} + numEndpoints={endpoints.length} + setPkgEndpoints={setPkgEndpoints} + /> ))} </div> )} From 8ebec1585655a3593540996451d7818dbc52b6ab Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 09:34:20 +0100 Subject: [PATCH 33/41] Avoid re-rendering --- .../pages/notifications/NotificationsRoot.tsx | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx index 96b1045b81..17eacf8fb0 100644 --- a/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx +++ b/packages/admin-ui/src/pages/notifications/NotificationsRoot.tsx @@ -8,14 +8,14 @@ import { NotificationsSettings } from "./tabs/Settings/Settings"; import { InstallNotificationsPkg } from "./tabs/InstallNotifications/InstallNotifications"; // Components import Title from "components/Title"; -import Loading from "components/Loading"; +import { renderResponse } from "components/SwrRender"; export const NotificationsRoot: React.FC = () => { const availableRoutes: { name: string; subPath: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any - component: () => React.JSX.Element; + component: React.ComponentType; }[] = [ { name: "Inbox", @@ -30,40 +30,38 @@ export const NotificationsRoot: React.FC = () => { ]; const dnpsRequest = useApi.packagesGet(); - const loading = dnpsRequest.isValidating; - const installedDnps = dnpsRequest.data; - const notificationsDnpName = "notifications.dnp.dappnode.eth"; - const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === notificationsDnpName); + return renderResponse(dnpsRequest, ["Loading notifications"], (dnps) => { + const notificationsDnpName = "notifications.dnp.dappnode.eth"; + const isNotificationsPkgInstalled = dnps?.some((dnp) => dnp.dnpName === notificationsDnpName); - return ( - <> - <Title title={title} /> - {loading ? ( - <Loading steps={["Loading data"]} /> - ) : !isNotificationsPkgInstalled ? ( - <InstallNotificationsPkg pkgName={notificationsDnpName} /> - ) : ( - <> - <div className="horizontal-navbar"> - {availableRoutes.map((route) => ( - <button key={route.subPath} className="item-container"> - <NavLink to={route.subPath} className="item no-a-style" style={{ whiteSpace: "nowrap" }}> - {route.name} - </NavLink> - </button> - ))} - </div> - - <div className="section-spacing"> - <Routes> + return ( + <> + <Title title={title} /> + {!isNotificationsPkgInstalled ? ( + <InstallNotificationsPkg pkgName={notificationsDnpName} /> + ) : ( + <> + <div className="horizontal-navbar"> {availableRoutes.map((route) => ( - <Route key={route.subPath} path={route.subPath} element={<route.component />} /> + <button key={route.subPath} className="item-container"> + <NavLink to={route.subPath} className="item no-a-style" style={{ whiteSpace: "nowrap" }}> + {route.name} + </NavLink> + </button> ))} - </Routes> - </div> - </> - )} - </> - ); + </div> + + <div className="section-spacing"> + <Routes> + {availableRoutes.map((route) => ( + <Route key={route.subPath} path={route.subPath} element={<route.component />} /> + ))} + </Routes> + </div> + </> + )} + </> + ); + }); }; From 1e9453db1ef19f3c6590a22005b5ca3ce5ad929b Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 09:59:12 +0100 Subject: [PATCH 34/41] update `gatusUpdateEndpoints` type --- packages/admin-ui/src/__mock-backend__/index.ts | 2 +- packages/dappmanager/src/calls/gatusConfig.ts | 14 +++++++------- packages/dappmanager/src/calls/index.ts | 2 +- packages/types/src/routes.ts | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/admin-ui/src/__mock-backend__/index.ts b/packages/admin-ui/src/__mock-backend__/index.ts index 9e68977561..3191ae8a8e 100644 --- a/packages/admin-ui/src/__mock-backend__/index.ts +++ b/packages/admin-ui/src/__mock-backend__/index.ts @@ -390,7 +390,7 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = { gatusGetEndpoints: async () => { return { "geth.dnp.dappnode.eth": [] }; }, - gatusUpdateEndpoint: async () => {}, + gatusUpdateEndpoints: async () => {}, gatuGetAllNotifications: async () => [] }; diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts index 610ba5f5df..a30b677760 100644 --- a/packages/dappmanager/src/calls/gatusConfig.ts +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -34,24 +34,24 @@ export async function gatusGetEndpoints(): Promise<{ [dnpName: string]: Endpoint /** * Update endpoint properties * @param dnpName - * @param updatedEndpoint + * @param updatedEndpoints */ -export async function gatusUpdateEndpoint({ +export async function gatusUpdateEndpoints({ dnpName, - updatedEndpoint + updatedEndpoints }: { dnpName: string; - updatedEndpoint: Endpoint; + updatedEndpoints: Endpoint[]; }): Promise<void> { // Get current endpoint status const manifest: Manifest = JSON.parse(fs.readFileSync(getManifestPath(dnpName, false), "utf8")); if (!manifest.notifications) throw new Error("No notifications found in manifest"); - const endpoint = manifest.notifications.endpoints.find((e) => e.name === updatedEndpoint.name); - if (!endpoint) throw new Error(`Endpoint ${updatedEndpoint.name} not found in manifest`); + const endpoints = manifest.notifications.endpoints; + if (!endpoints) throw new Error(`No endpoints found in manifest`); // Update endpoint - Object.assign(endpoint, updatedEndpoint); + Object.assign(endpoints, updatedEndpoints); // Save manifest fs.writeFileSync(getManifestPath(dnpName, false), JSON.stringify(manifest, null, 2)); diff --git a/packages/dappmanager/src/calls/index.ts b/packages/dappmanager/src/calls/index.ts index 9d430921a3..8c5254db63 100644 --- a/packages/dappmanager/src/calls/index.ts +++ b/packages/dappmanager/src/calls/index.ts @@ -22,7 +22,7 @@ export { getCoreVersion } from "./getCoreVersion.js"; export { getUserActionLogs } from "./getUserActionLogs.js"; export { getHostUptime } from "./getHostUptime.js"; export { getIsConnectedToInternet } from "./getIsConnectedToInternet.js"; -export { gatusGetEndpoints, gatusUpdateEndpoint, gatuGetAllNotifications } from "./gatusConfig.js"; +export { gatusGetEndpoints, gatusUpdateEndpoints, gatuGetAllNotifications } from "./gatusConfig.js"; export * from "./httpsPortal.js"; export { ipfsTest } from "./ipfsTest.js"; export { ipfsClientTargetSet } from "./ipfsClientTargetSet.js"; diff --git a/packages/types/src/routes.ts b/packages/types/src/routes.ts index caa49b2352..514d9b82f6 100644 --- a/packages/types/src/routes.ts +++ b/packages/types/src/routes.ts @@ -276,7 +276,7 @@ export interface Routes { /** * Gatus update endpoint */ - gatusUpdateEndpoint: (kwargs: { dnpName: string; updatedEndpoint: Endpoint }) => Promise<void>; + gatusUpdateEndpoints: (kwargs: { dnpName: string; updatedEndpoints: Endpoint[] }) => Promise<void>; /** * Returns the user action logs. This logs are stored in a different @@ -709,7 +709,7 @@ export const routesData: { [P in keyof Routes]: RouteData } = { fetchDnpRequest: {}, gatuGetAllNotifications: { log: true }, gatusGetEndpoints: { log: true }, - gatusUpdateEndpoint: { log: true }, + gatusUpdateEndpoints: { log: true }, getUserActionLogs: {}, getHostUptime: {}, httpsPortalMappingAdd: { log: true }, From 6bc6af4234f2a5614ec85e2971da50b288e747ec Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 10:06:43 +0100 Subject: [PATCH 35/41] Notifications settings components update --- .../tabs/Settings/components/EndpointItem.tsx | 41 +++++++++++-------- .../components/ManagePackageSection.tsx | 13 +++++- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx index f74abff06a..0e1bfb1758 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -1,24 +1,33 @@ -import React, { useState } from "react"; +import React from "react"; import { Endpoint } from "@dappnode/types"; import Switch from "components/Switch"; import Slider from "components/Slider"; -export function EndpointItem({ - endpoint, - index, - numEndpoints -}: { +interface EndpointItemProps { endpoint: Endpoint; index: number; numEndpoints: number; -}) { - const [endpointEnabled, setEndpointEnabled] = useState(endpoint.enabled); + setPkgEndpoints: React.Dispatch<React.SetStateAction<Endpoint[]>>; +} + +export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: EndpointItemProps) { + const endpointEnabled = endpoint.enabled; const handleEndpointToggle = () => { - // TODO: update "notifications.yaml" file - setEndpointEnabled(!endpointEnabled); + setPkgEndpoints((prevEndpoints) => + prevEndpoints.map((ep, i) => (i === index ? { ...ep, enabled: !ep.enabled } : ep)) + ); }; + // TODO: Parse conditions, and update its value according to the slider + const handleSliderUpdate = () => { + // setPkgEndpoints((prevEndpoints) => + // prevEndpoints.map((ep, i) => (i === index ? { ...ep, conditions: [''] } : ep)) + // ); + }; + + console.log("EndpointItem", endpoint, index); + return ( <> <div key={index} className="endpoint-row"> @@ -26,16 +35,14 @@ export function EndpointItem({ <strong>{endpoint.definition.title}</strong> <div>{endpoint.definition.description}</div> </div> - <Switch - checked={endpointEnabled} - onToggle={() => { - handleEndpointToggle(); - }} - /> + <Switch checked={endpointEnabled} onToggle={handleEndpointToggle} /> </div> {endpointEnabled && endpoint.metric && ( <div className="slider-wrapper"> - <Slider /> + <Slider + onChange={handleSliderUpdate} + // value={endpoint.conditions[0]} + /> </div> )} {index + 1 < numEndpoints && <hr />} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index 514b6c6a51..c8a81857c4 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -1,9 +1,10 @@ import SubTitle from "components/SubTitle"; import Switch from "components/Switch"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { EndpointItem } from "./EndpointItem"; import { Endpoint } from "@dappnode/types"; import { prettyDnpName } from "utils/format"; +import { useApi } from "api"; interface ManagePackageSectionProps { dnpName: string; @@ -13,6 +14,16 @@ export function ManagePackageSection({ dnpName, endpoints }: ManagePackageSectio const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(endpoints.some((ep) => ep.enabled)); const [pkgEndpoints, setPkgEndpoints] = useState(endpoints); + useEffect(() => { + // TODO: Implement timeOut that waits for more config updates before sending the new Endpoints config + const updateConfig = async () => { + useApi.gatusUpdateEndpoints({ + dnpName, + updatedEndpoints: pkgEndpoints}); + }; + updateConfig(); + }, [pkgEndpoints]); + const handlePkgToggle = () => { setPkgEndpoints(pkgEndpoints.map((ep) => ({ ...ep, enabled: !pkgNotificationsEnabled }))); setPkgNotificationsEnabled(!pkgNotificationsEnabled); From 903551f8442c7241c189d3e95b83bfae4650af31 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 11:40:33 +0100 Subject: [PATCH 36/41] Installer step fixed --- .../installer/components/InstallDnpView.tsx | 19 ++----- .../components/Steps/Notifications.tsx | 49 +++++++++++++++++++ .../tabs/Settings/components/EndpointItem.tsx | 2 - .../components/ManagePackageSection.tsx | 43 +++++++++------- 4 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 packages/admin-ui/src/pages/installer/components/Steps/Notifications.tsx diff --git a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx index e80b886498..141bb82261 100644 --- a/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx +++ b/packages/admin-ui/src/pages/installer/components/InstallDnpView.tsx @@ -28,7 +28,7 @@ import { RequestedDnp, UserSettingsAllDnps } from "@dappnode/types"; import { diff } from "semver"; import Button from "components/Button"; import { pathName as systemPathName, subPaths as systemSubPaths } from "pages/system/data"; -import { ManagePackageSection } from "pages/notifications/tabs/Settings/components/ManagePackageSection"; +import { Notifications } from "./Steps/Notifications"; interface InstallDnpViewProps { dnp: RequestedDnp; @@ -55,6 +55,8 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => const dispatch = useDispatch(); const { dnpName, reqVersion, semVersion, settings, manifest, setupWizard, isInstalled, installedVersion } = dnp; + console.log("manifest", manifest); + const updateType = installedVersion && diff(installedVersion, semVersion); const areUpdateWarnings = manifest.warnings?.onPatchUpdate || manifest.warnings?.onMinorUpdate || manifest.warnings?.onMajorUpdate; @@ -177,8 +179,7 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => const installedDnps = dnpsRequest.data; const isNotificationsPkgInstalled = installedDnps?.some((dnp) => dnp.dnpName === "notifications.dnp.dappnode.eth"); - const endpointsCall = useApi.gatusGetEndpoints(); - const dnpNotificationEndpoints = endpointsCall.data && endpointsCall.data[dnpName]; + const dnpNotificationEndpoints = manifest.notifications?.endpoints || []; const disableInstallation = !isEmpty(progressLogs) || requiresCoreUpdate || requiresDockerUpdate || packagesToBeUninstalled.length > 0; @@ -240,17 +241,7 @@ const InstallDnpView: React.FC<InstallDnpViewProps> = ({ dnp, progressLogs }) => { name: "Notifications", subPath: notificationsSubPath, - render: () => ( - <> - <ManagePackageSection dnpName="Enable notifications" endpoints={dnpNotificationEndpoints || []} /> - <div className="button-group"> - <Button onClick={goBack}>Back</Button> - <Button variant="dappnode" onClick={() => goNext()}> - Next - </Button> - </div> - </> - ), + render: () => <Notifications endpoints={dnpNotificationEndpoints || []} goBack={goBack} goNext={goNext} dnpName={dnpName}/>, available: isNotificationsPkgInstalled && dnpNotificationEndpoints && dnpNotificationEndpoints.length > 0 }, // Placeholder for the final step in the horizontal stepper diff --git a/packages/admin-ui/src/pages/installer/components/Steps/Notifications.tsx b/packages/admin-ui/src/pages/installer/components/Steps/Notifications.tsx new file mode 100644 index 0000000000..22369c4aee --- /dev/null +++ b/packages/admin-ui/src/pages/installer/components/Steps/Notifications.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from "react"; +import Button from "components/Button"; +import { ManagePackageSection } from "pages/notifications/tabs/Settings/components/ManagePackageSection"; +import { Endpoint } from "@dappnode/types"; +import { useApi } from "api"; + +interface NotificationsProps { + endpoints: Endpoint[]; + dnpName: string; + goNext: () => void; + goBack: () => void; +} + +export const Notifications: React.FC<NotificationsProps> = ({ endpoints, goBack, goNext, dnpName }) => { + const endpointsCall = useApi.gatusGetEndpoints(); + const pkgEndpointsData = endpointsCall.data?.[dnpName]; + + // Merge endpoints in order to preserve existing endpoints config when updating a package + const mergedEndpoints = useMemo(() => { + if (!pkgEndpointsData) return endpoints; + + const endpointMap = new Map<string, Endpoint>(); + + endpoints.forEach((endpoint) => { + endpointMap.set(endpoint.name, endpoint); + }); + + pkgEndpointsData.forEach((retrievedEndpoint: Endpoint) => { + endpointMap.set(retrievedEndpoint.name, { + ...endpointMap.get(retrievedEndpoint.name), // Preserve existing config if it exists + ...retrievedEndpoint, // Overwrite with new data + }); + }); + + return Array.from(endpointMap.values()); + }, [endpoints, pkgEndpointsData]); + + return ( + <> + <ManagePackageSection dnpName="Enable notifications" endpoints={mergedEndpoints} /> + <div className="button-group"> + <Button onClick={goBack}>Back</Button> + <Button variant="dappnode" onClick={goNext}> + Next + </Button> + </div> + </> + ); +}; diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx index 0e1bfb1758..6cd0b27115 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -26,8 +26,6 @@ export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: // ); }; - console.log("EndpointItem", endpoint, index); - return ( <> <div key={index} className="endpoint-row"> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index c8a81857c4..275a9f70a0 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -1,6 +1,6 @@ +import React, { useEffect, useState } from "react"; import SubTitle from "components/SubTitle"; import Switch from "components/Switch"; -import React, { useEffect, useState } from "react"; import { EndpointItem } from "./EndpointItem"; import { Endpoint } from "@dappnode/types"; import { prettyDnpName } from "utils/format"; @@ -10,43 +10,50 @@ interface ManagePackageSectionProps { dnpName: string; endpoints: Endpoint[]; } + export function ManagePackageSection({ dnpName, endpoints }: ManagePackageSectionProps) { - const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(endpoints.some((ep) => ep.enabled)); const [pkgEndpoints, setPkgEndpoints] = useState(endpoints); + const [pkgNotificationsEnabled, setPkgNotificationsEnabled] = useState(endpoints.some((ep) => ep.enabled)); + + // Sync state when `endpoints` prop changes, but keep user modifications + useEffect(() => { + setPkgEndpoints((prevPkgEndpoints) => { + const updatedEndpoints = endpoints.map((newEp) => { + const existingEp = prevPkgEndpoints.find((ep) => ep.name === newEp.name); + return existingEp ? { ...existingEp, ...newEp } : newEp; + }); + return updatedEndpoints; + }); + }, [endpoints]); + + // Handle switch toggle to enable/disable all endpoints + const handlePkgToggle = () => { + const newEnabledState = !pkgNotificationsEnabled; + setPkgEndpoints((prevPkgEndpoints) => prevPkgEndpoints.map((ep) => ({ ...ep, enabled: newEnabledState }))); + setPkgNotificationsEnabled(newEnabledState); + }; useEffect(() => { // TODO: Implement timeOut that waits for more config updates before sending the new Endpoints config const updateConfig = async () => { - useApi.gatusUpdateEndpoints({ - dnpName, - updatedEndpoints: pkgEndpoints}); + useApi.gatusUpdateEndpoints({ dnpName, updatedEndpoints: pkgEndpoints }); }; updateConfig(); }, [pkgEndpoints]); - - const handlePkgToggle = () => { - setPkgEndpoints(pkgEndpoints.map((ep) => ({ ...ep, enabled: !pkgNotificationsEnabled }))); - setPkgNotificationsEnabled(!pkgNotificationsEnabled); - }; - return ( <div key={String(dnpName)} className="notifications-settings"> <div className="title-switch-row"> <SubTitle className="notifications-pkg-name">{prettyDnpName(dnpName)}</SubTitle> - <Switch - checked={pkgNotificationsEnabled} - onToggle={() => { - handlePkgToggle(); - }} - /> + <Switch checked={pkgNotificationsEnabled} onToggle={handlePkgToggle} /> </div> {pkgNotificationsEnabled && ( <div className="endpoint-list-card"> {pkgEndpoints.map((endpoint, i) => ( <EndpointItem + key={endpoint.name} endpoint={endpoint} index={i} - numEndpoints={endpoints.length} + numEndpoints={pkgEndpoints.length} setPkgEndpoints={setPkgEndpoints} /> ))} From d3a6b3650865c5b365f109e2f3cfa2afc9867c42 Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Mon, 17 Mar 2025 12:19:03 +0100 Subject: [PATCH 37/41] implement reload --- packages/dappmanager/src/calls/gatusConfig.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/dappmanager/src/calls/gatusConfig.ts b/packages/dappmanager/src/calls/gatusConfig.ts index a30b677760..64726e0a16 100644 --- a/packages/dappmanager/src/calls/gatusConfig.ts +++ b/packages/dappmanager/src/calls/gatusConfig.ts @@ -56,12 +56,11 @@ export async function gatusUpdateEndpoints({ // Save manifest fs.writeFileSync(getManifestPath(dnpName, false), JSON.stringify(manifest, null, 2)); - // Update endpoint in gatus - // await fetch(`http://notifier.notifications.dappnode:8082/gatus/endpoints`, { - // method: "POST", - // headers: { - // "Content-Type": "application/json" - // }, - // body: JSON.stringify(endpoint) - // }); + // Trigger reload. Gatus will execute reload at a minimum interval of x seconds + await fetch(`http://notifier.notifications.dappnode:8082/api/v1/gatus/endpoints/reload`, { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }); } From 2a82114e5c13b4d6e4fb45ed7e09935fcdb9e954 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 13:40:29 +0100 Subject: [PATCH 38/41] parse condition in slider --- .../tabs/Settings/components/EndpointItem.tsx | 48 ++++++++++++++++--- .../components/ManagePackageSection.tsx | 7 +-- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx index 6cd0b27115..3417e8fe03 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Endpoint } from "@dappnode/types"; import Switch from "components/Switch"; import Slider from "components/Slider"; @@ -13,17 +13,48 @@ interface EndpointItemProps { export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: EndpointItemProps) { const endpointEnabled = endpoint.enabled; + const operators = ["<", ">", "==", "!=", ">=", "<="]; + + // Extract the operator and number from the condition string from the 1ST CONDITION. Rn, is only supporting 1 slider (from 1st condition) per endpoint + const conditionString = endpoint.conditions[0]; + const operator = operators.find((op) => conditionString.includes(op)); + const conditionValue = operator + ? conditionString + .split(operator) + .pop() + ?.trim() || "" + : "0"; + + const [sliderValue, setSliderValue] = useState<number>(parseFloat(conditionValue)); + + useEffect(() => { + console.log("sliderValue", endpoint.name, endpoint.metric, sliderValue); + }, [sliderValue]); + const handleEndpointToggle = () => { setPkgEndpoints((prevEndpoints) => prevEndpoints.map((ep, i) => (i === index ? { ...ep, enabled: !ep.enabled } : ep)) ); }; - // TODO: Parse conditions, and update its value according to the slider - const handleSliderUpdate = () => { - // setPkgEndpoints((prevEndpoints) => - // prevEndpoints.map((ep, i) => (i === index ? { ...ep, conditions: [''] } : ep)) - // ); + const handleSliderUpdate = (value: number) => { + setSliderValue(value); + const updatedCondition = operator + ? `${endpoint.conditions[0].split(operator)[0].trim()} ${operator} ${value}` + : endpoint.conditions[0]; + setPkgEndpoints((prevEndpoints) => + prevEndpoints.map((ep, i) => + i === index + ? { + ...ep, + conditions: [ + updatedCondition, // Update ONLY the first condition + ...ep.conditions.slice(1) + ] + } + : ep + ) + ); }; return ( @@ -38,8 +69,11 @@ export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: {endpointEnabled && endpoint.metric && ( <div className="slider-wrapper"> <Slider + value={sliderValue} onChange={handleSliderUpdate} - // value={endpoint.conditions[0]} + min={endpoint.metric.min} + max={endpoint.metric.max} + unit={endpoint.metric.unit} /> </div> )} diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx index 275a9f70a0..466a039a2b 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/ManagePackageSection.tsx @@ -4,7 +4,7 @@ import Switch from "components/Switch"; import { EndpointItem } from "./EndpointItem"; import { Endpoint } from "@dappnode/types"; import { prettyDnpName } from "utils/format"; -import { useApi } from "api"; +import { api } from "api"; interface ManagePackageSectionProps { dnpName: string; @@ -35,10 +35,7 @@ export function ManagePackageSection({ dnpName, endpoints }: ManagePackageSectio useEffect(() => { // TODO: Implement timeOut that waits for more config updates before sending the new Endpoints config - const updateConfig = async () => { - useApi.gatusUpdateEndpoints({ dnpName, updatedEndpoints: pkgEndpoints }); - }; - updateConfig(); + api.gatusUpdateEndpoints({ dnpName, updatedEndpoints: pkgEndpoints }); }, [pkgEndpoints]); return ( <div key={String(dnpName)} className="notifications-settings"> From c49cebfbff84b122c2a693399544b1fb857d87bd Mon Sep 17 00:00:00 2001 From: Pablo Mendez <pablo@dappnode.io> Date: Mon, 17 Mar 2025 14:09:50 +0100 Subject: [PATCH 39/41] deprecate unused types --- packages/types/src/notifications.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/types/src/notifications.ts b/packages/types/src/notifications.ts index b3c22036a8..3c181bfe7c 100644 --- a/packages/types/src/notifications.ts +++ b/packages/types/src/notifications.ts @@ -19,7 +19,6 @@ export interface Endpoint { name: string; enabled: boolean; url: string; - headers: Record<string, string>; method: string; conditions: string[]; interval: string; // e.g., "1m" @@ -44,9 +43,5 @@ interface Alert { "success-threshold": number; "send-on-resolved": boolean; description: string; - url: string; - method: string; - body: string; - headers: Record<string, string>; - placeholders: Record<string, Record<string, string>>; + enabled: boolean; } From 92f80c45d54c33281101ffa7af40af8bf5e9eb73 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Mon, 17 Mar 2025 15:57:32 +0100 Subject: [PATCH 40/41] set endpoints state when slider released --- packages/admin-ui/src/components/Slider.tsx | 12 ++++++++++++ .../tabs/Settings/components/EndpointItem.tsx | 11 ++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/admin-ui/src/components/Slider.tsx b/packages/admin-ui/src/components/Slider.tsx index e90f323074..86ec012bf0 100644 --- a/packages/admin-ui/src/components/Slider.tsx +++ b/packages/admin-ui/src/components/Slider.tsx @@ -8,6 +8,7 @@ interface SliderProps { value?: number; unit?: string; onChange?: (value: number) => void; + onChangeComplete?: (value: number) => void; } const Slider: React.FC<SliderProps> = ({ @@ -17,6 +18,7 @@ const Slider: React.FC<SliderProps> = ({ value = 50, unit = "%", onChange, + onChangeComplete, // In order to trigger an action when the user releases the slider }) => { const [sliderValue, setSliderValue] = useState(value); @@ -26,6 +28,14 @@ const Slider: React.FC<SliderProps> = ({ if (onChange) onChange(newValue); }; + const handleMouseUp = () => { + if (onChangeComplete) onChangeComplete(sliderValue); + }; + + const handleTouchEnd = () => { + if (onChangeComplete) onChangeComplete(sliderValue); + }; + return ( <div className="slider-container"> <input @@ -35,6 +45,8 @@ const Slider: React.FC<SliderProps> = ({ step={step} value={sliderValue} onChange={handleChange} + onMouseUp={handleMouseUp} // For mouse support + onTouchEnd={handleTouchEnd} // For mobile support className="slider-component" /> <span className="slider-value">{sliderValue} {unit && unit}</span> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx index 3417e8fe03..1230b30c75 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Settings/components/EndpointItem.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { Endpoint } from "@dappnode/types"; import Switch from "components/Switch"; import Slider from "components/Slider"; @@ -27,10 +27,6 @@ export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: const [sliderValue, setSliderValue] = useState<number>(parseFloat(conditionValue)); - useEffect(() => { - console.log("sliderValue", endpoint.name, endpoint.metric, sliderValue); - }, [sliderValue]); - const handleEndpointToggle = () => { setPkgEndpoints((prevEndpoints) => prevEndpoints.map((ep, i) => (i === index ? { ...ep, enabled: !ep.enabled } : ep)) @@ -39,9 +35,13 @@ export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: const handleSliderUpdate = (value: number) => { setSliderValue(value); + }; + + const handleSliderUpdateComplete = (value: number) => { const updatedCondition = operator ? `${endpoint.conditions[0].split(operator)[0].trim()} ${operator} ${value}` : endpoint.conditions[0]; + setPkgEndpoints((prevEndpoints) => prevEndpoints.map((ep, i) => i === index @@ -71,6 +71,7 @@ export function EndpointItem({ endpoint, index, numEndpoints, setPkgEndpoints }: <Slider value={sliderValue} onChange={handleSliderUpdate} + onChangeComplete={handleSliderUpdateComplete} min={endpoint.metric.min} max={endpoint.metric.max} unit={endpoint.metric.unit} From 7958da049314db5bd561e087fd042b8746a626f9 Mon Sep 17 00:00:00 2001 From: mateumiralles <mateumiralles714@gmail.com> Date: Tue, 18 Mar 2025 11:30:56 +0100 Subject: [PATCH 41/41] triggrered label in `NotificationCard` component --- .../tabs/Inbox/components/NotificationsCard.tsx | 3 ++- .../admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx index 6097f2cebf..e145155d17 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/components/NotificationsCard.tsx @@ -22,7 +22,8 @@ export function NotificationCard({ notification, avatarUrl }: NotificationCardPr <div className="notification-name-row"> <div>{prettyDnpName(notification.dnpName)}</div> <div className="group-label">{notification.category}</div> - {notification.body.includes("Resolved: ") && <div className="sucess-label">success</div>} + {notification.body.includes("Resolved: ") && <div className="sucess-label">resolved</div>} + {notification.body.includes("Triggered: ") && <div className="trigger-label">triggered</div>} </div> <i>{new Date(notification.timestamp).toLocaleString()}</i> diff --git a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss index 47b238e8d2..c90c129828 100644 --- a/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss +++ b/packages/admin-ui/src/pages/notifications/tabs/Inbox/inbox.scss @@ -68,6 +68,11 @@ background-color: rgba(0, 109, 29, 0.7) !important; color: white; } + .trigger-label { + @extend .group-label; + background-color: rgba(255, 30, 29, 0.5) !important; + color: white; + } } }