From 23128607a77e3890c7351bf0afab3726c5e52951 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:43:11 +0100 Subject: [PATCH 01/52] feat(settings): extend types, add updateSettings API and useUpdateSettings hook with query invalidation Made-with: Cursor --- ui/apps/pmm/package.json | 1 + ui/apps/pmm/src/api/settings.ts | 9 + ui/apps/pmm/src/hooks/api/useSettings.ts | 36 +- ui/apps/pmm/src/types/settings.types.ts | 41 ++- ui/yarn.lock | 408 ++++++++++++----------- 5 files changed, 293 insertions(+), 202 deletions(-) diff --git a/ui/apps/pmm/package.json b/ui/apps/pmm/package.json index 0392e21d070..d227ce1a5de 100644 --- a/ui/apps/pmm/package.json +++ b/ui/apps/pmm/package.json @@ -35,6 +35,7 @@ "notistack": "^3.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.71.2", "react-is": "18.3.1", "react-markdown": "^9.0.1", "react-router-dom": "^6.30.2", diff --git a/ui/apps/pmm/src/api/settings.ts b/ui/apps/pmm/src/api/settings.ts index 467561781fb..9ca8326bcb3 100644 --- a/ui/apps/pmm/src/api/settings.ts +++ b/ui/apps/pmm/src/api/settings.ts @@ -1,6 +1,8 @@ import { GetFrontendSettingsResponse, GetSettingsResponse, + Settings, + UpdateSettingsPayload, } from 'types/settings.types'; import { api, grafanaApi } from './api'; @@ -19,3 +21,10 @@ export const getFrontendSettings = async () => { await grafanaApi.get('/frontend/settings'); return res.data; }; + +export const updateSettings = async ( + payload: UpdateSettingsPayload +): Promise => { + const res = await api.put('/server/settings', payload); + return res.data.settings; +}; diff --git a/ui/apps/pmm/src/hooks/api/useSettings.ts b/ui/apps/pmm/src/hooks/api/useSettings.ts index c0b44269d35..68cb830a7f5 100644 --- a/ui/apps/pmm/src/hooks/api/useSettings.ts +++ b/ui/apps/pmm/src/hooks/api/useSettings.ts @@ -1,14 +1,27 @@ -import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { + useMutation, + UseMutationOptions, + useQuery, + useQueryClient, + UseQueryOptions, +} from '@tanstack/react-query'; import { getFrontendSettings, getReadonlySettings, getSettings, + updateSettings, } from 'api/settings'; -import { FrontendSettings, Settings } from 'types/settings.types'; +import { + FrontendSettings, + Settings, + UpdateSettingsPayload, +} from 'types/settings.types'; + +export const SETTINGS_QUERY_KEY = ['settings'] as const; export const useSettings = (options?: Partial>) => useQuery({ - queryKey: ['settings'], + queryKey: SETTINGS_QUERY_KEY, queryFn: () => getSettings(), ...options, }); @@ -30,3 +43,20 @@ export const useFrontendSettings = ( queryFn: () => getFrontendSettings(), ...options, }); + +export const useUpdateSettings = ( + options?: Partial< + UseMutationOptions + > +) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (payload) => updateSettings(payload), + onSuccess: async (data, variables, context) => { + await queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY }); + await options?.onSuccess?.(data, variables, context); + }, + ...options, + }); +}; diff --git a/ui/apps/pmm/src/types/settings.types.ts b/ui/apps/pmm/src/types/settings.types.ts index 772553f659d..c15e0f7918f 100644 --- a/ui/apps/pmm/src/types/settings.types.ts +++ b/ui/apps/pmm/src/types/settings.types.ts @@ -10,12 +10,51 @@ export interface ReadonlySettings { enableAccessControl: boolean; } +export interface MetricsResolutions { + hr: string; + mr: string; + lr: string; +} + +export interface AdvisorRunIntervalsSettings { + rareInterval: string; + standardInterval: string; + frequentInterval: string; +} + export interface GetReadonlySettingsResponse { settings: ReadonlySettings; } export interface Settings extends ReadonlySettings { - updateSnoozeDuration: string; + updateSnoozeDuration?: string; + metricsResolutions?: MetricsResolutions; + dataRetention?: string; + sshKey?: string; + advisorRunIntervals?: AdvisorRunIntervalsSettings; + telemetrySummaries?: string[]; + enableInternalPgQan?: boolean; +} + +/** Payload for PUT /server/settings - partial updates supported */ +export interface UpdateSettingsPayload { + sshKey?: string; + metricsResolutions?: MetricsResolutions; + dataRetention?: string; + pmmPublicAddress?: string; + enableTelemetry?: boolean; + enableAlerting?: boolean; + enableAdvisor?: boolean; + advisorRunIntervals?: { + rareInterval: string; + standardInterval: string; + frequentInterval: string; + }; + enableBackupManagement?: boolean; + enableAzurediscover?: boolean; + enableUpdates?: boolean; + enableAccessControl?: boolean; + enableInternalPgQan?: boolean; } export interface FrontendSettings extends GetFrontendSettingsResponse {} diff --git a/ui/yarn.lock b/ui/yarn.lock index c9dd967e9b7..290002d9aa9 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -484,135 +484,120 @@ esquery "^1.5.0" jsdoc-type-pratt-parser "~4.0.0" -"@esbuild/aix-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c" - integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== - -"@esbuild/android-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752" - integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== - -"@esbuild/android-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a" - integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== - -"@esbuild/android-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16" - integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== - -"@esbuild/darwin-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd" - integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== - -"@esbuild/darwin-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e" - integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== - -"@esbuild/freebsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe" - integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== - -"@esbuild/freebsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3" - integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== - -"@esbuild/linux-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977" - integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== - -"@esbuild/linux-arm@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9" - integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== - -"@esbuild/linux-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0" - integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== - -"@esbuild/linux-loong64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0" - integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== - -"@esbuild/linux-mips64el@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd" - integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== - -"@esbuild/linux-ppc64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869" - integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== - -"@esbuild/linux-riscv64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6" - integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== - -"@esbuild/linux-s390x@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663" - integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== - -"@esbuild/linux-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306" - integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== - -"@esbuild/netbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4" - integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== - -"@esbuild/netbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076" - integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== - -"@esbuild/openbsd-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd" - integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== - -"@esbuild/openbsd-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679" - integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== - -"@esbuild/openharmony-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d" - integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== - -"@esbuild/sunos-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6" - integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== - -"@esbuild/win32-arm64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323" - integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== - -"@esbuild/win32-ia32@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267" - integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== - -"@esbuild/win32-x64@0.25.12": - version "0.25.12" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5" - integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" @@ -1893,10 +1878,15 @@ "@rooks/use-mutation-observer" "^4.11.2" resize-observer-polyfill "^1.5.1" -"@remix-run/router@1.23.0", "@remix-run/router@1.23.1", "@remix-run/router@1.23.2": - version "1.23.2" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.2.tgz#156c4b481c0bee22a19f7924728a67120de06971" - integrity sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w== +"@remix-run/router@1.23.0": + version "1.23.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.0.tgz#35390d0e7779626c026b11376da6789eb8389242" + integrity sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA== + +"@remix-run/router@1.23.1": + version "1.23.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.23.1.tgz#0ce8857b024e24fc427585316383ad9d295b3a7f" + integrity sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ== "@rolldown/pluginutils@1.0.0-beta.27": version "1.0.0-beta.27" @@ -3781,7 +3771,7 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -brace-expansion@^2.0.2, brace-expansion@~1.1.12: +brace-expansion@^1.1.7: version "1.1.12" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== @@ -3789,6 +3779,13 @@ brace-expansion@^2.0.2, brace-expansion@~1.1.12: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1, brace-expansion@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -4213,7 +4210,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6, cross-spawn@~7.0.5: +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -4891,10 +4888,10 @@ domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" -dompurify@3.2.4, dompurify@^3.2.7: - version "3.3.2" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.2.tgz#58c515d0f8508b8749452a028aa589ad80b36325" - integrity sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ== +dompurify@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e" + integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -5164,37 +5161,34 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild@^0.21.3, esbuild@~0.25.0: - version "0.25.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5" - integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.12" - "@esbuild/android-arm" "0.25.12" - "@esbuild/android-arm64" "0.25.12" - "@esbuild/android-x64" "0.25.12" - "@esbuild/darwin-arm64" "0.25.12" - "@esbuild/darwin-x64" "0.25.12" - "@esbuild/freebsd-arm64" "0.25.12" - "@esbuild/freebsd-x64" "0.25.12" - "@esbuild/linux-arm" "0.25.12" - "@esbuild/linux-arm64" "0.25.12" - "@esbuild/linux-ia32" "0.25.12" - "@esbuild/linux-loong64" "0.25.12" - "@esbuild/linux-mips64el" "0.25.12" - "@esbuild/linux-ppc64" "0.25.12" - "@esbuild/linux-riscv64" "0.25.12" - "@esbuild/linux-s390x" "0.25.12" - "@esbuild/linux-x64" "0.25.12" - "@esbuild/netbsd-arm64" "0.25.12" - "@esbuild/netbsd-x64" "0.25.12" - "@esbuild/openbsd-arm64" "0.25.12" - "@esbuild/openbsd-x64" "0.25.12" - "@esbuild/openharmony-arm64" "0.25.12" - "@esbuild/sunos-x64" "0.25.12" - "@esbuild/win32-arm64" "0.25.12" - "@esbuild/win32-ia32" "0.25.12" - "@esbuild/win32-x64" "0.25.12" + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" @@ -6321,10 +6315,15 @@ ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -immutable@5.0.3, immutable@^4.0.0, immutable@^5.1.5: - version "5.1.5" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.5.tgz#93ee4db5c2a9ab42a4a783069f3c5d8847d40165" - integrity sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A== +immutable@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" + integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== + +immutable@^4.0.0: + version "4.3.8" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.8.tgz#02d183c7727fb2bb1d5d0380da0d779dce9296a7" + integrity sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw== import-cwd@^3.0.0: version "3.0.0" @@ -8195,7 +8194,28 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -minimatch@9.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^9.0.4, minimatch@^9.0.5, minimatch@^9.0.6: +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.9" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== @@ -8248,7 +8268,7 @@ nano-css@^5.6.2: stacktrace-js "^2.0.2" stylis "^4.3.0" -nanoid@^3.3.11, nanoid@~3.3.8: +nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -9328,6 +9348,11 @@ react-hook-form@^7.68.0: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.71.1.tgz#6a758958861682cf0eb22131eead684ba3618f66" integrity sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w== +react-hook-form@^7.71.2: + version "7.71.2" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.71.2.tgz#a5f1d2b855be9ecf1af6e74df9b80f54beae7e35" + integrity sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA== + react-i18next@^15.0.0: version "15.7.4" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.7.4.tgz#146e50f220d204b842e22c75d1a3d23c6c589a30" @@ -9350,31 +9375,11 @@ react-inlinesvg@4.2.0: dependencies: react-from-dom "^0.7.5" -react-is@18.2.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - -react-is@18.3.1, react-is@^18.0.0, react-is@^18.2.0, react-is@^18.3.1: +react-is@18.2.0, react-is@18.3.1, react-is@^16.13.1, react-is@^16.7.0, react-is@^17.0.1, react-is@^18.0.0, react-is@^18.2.0, react-is@^18.3.1, react-is@^19.2.3: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react-is@^16.13.1, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -react-is@^19.2.3: - version "19.2.4" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.4.tgz#a080758243c572ccd4a63386537654298c99d135" - integrity sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA== - react-loading-skeleton@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz#da2090355b4dedcad5c53cb3f0ed364e3a76d6ca" @@ -9414,7 +9419,7 @@ react-router-dom-v5-compat@^6.26.1: history "^5.3.0" react-router "6.30.0" -react-router-dom@5.3.4, react-router-dom@6.30.2, react-router-dom@^6.30.2: +react-router-dom@5.3.4, react-router-dom@^6.30.2: version "6.30.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.30.2.tgz#ee8c161bce4890d34484b552f8510f9af0e22b01" integrity sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q== @@ -9422,7 +9427,14 @@ react-router-dom@5.3.4, react-router-dom@6.30.2, react-router-dom@^6.30.2: "@remix-run/router" "1.23.1" react-router "6.30.2" -react-router@6.30.0, react-router@6.30.2: +react-router@6.30.0: + version "6.30.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.0.tgz#9789d775e63bc0df60f39ced77c8c41f1e01ff90" + integrity sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ== + dependencies: + "@remix-run/router" "1.23.0" + +react-router@6.30.2: version "6.30.2" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.30.2.tgz#c78a3b40f7011f49a373b1df89492e7d4ec12359" integrity sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA== From 680823a9532d4de122ea3294f7626c0a1acb8fc3 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:45:32 +0100 Subject: [PATCH 02/52] feat(settings): add Settings page shell with MUI tabs and placeholder forms - Add Settings page with SSH Key, Metrics Resolution, Advanced Settings tabs - Add Settings.messages.ts with all copy from Grafana - Add placeholder form components (to be implemented in next commits) - Fix useUpdateSettings onSuccess signature for react-query v5 - Fix updateSnoozeDuration optional handling in useSnooze Made-with: Cursor --- ui/apps/pmm/src/hooks/api/useSettings.ts | 8 +- ui/apps/pmm/src/hooks/updates.ts | 6 +- .../src/pages/settings/Settings.messages.ts | 86 +++++++++++++++++++ ui/apps/pmm/src/pages/settings/Settings.tsx | 59 +++++++++++++ .../advanced/AdvancedSettingsForm.tsx | 11 +++ .../MetricsResolutionForm.tsx | 11 +++ .../components/ssh-key/SshKeyForm.tsx | 11 +++ ui/apps/pmm/src/pages/settings/index.ts | 1 + 8 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 ui/apps/pmm/src/pages/settings/Settings.messages.ts create mode 100644 ui/apps/pmm/src/pages/settings/Settings.tsx create mode 100644 ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx create mode 100644 ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx create mode 100644 ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx create mode 100644 ui/apps/pmm/src/pages/settings/index.ts diff --git a/ui/apps/pmm/src/hooks/api/useSettings.ts b/ui/apps/pmm/src/hooks/api/useSettings.ts index 68cb830a7f5..45abe2ae133 100644 --- a/ui/apps/pmm/src/hooks/api/useSettings.ts +++ b/ui/apps/pmm/src/hooks/api/useSettings.ts @@ -53,10 +53,10 @@ export const useUpdateSettings = ( return useMutation({ mutationFn: (payload) => updateSettings(payload), - onSuccess: async (data, variables, context) => { - await queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY }); - await options?.onSuccess?.(data, variables, context); - }, ...options, + onSuccess: (data, variables, onMutate, context) => { + void queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY }); + options?.onSuccess?.(data, variables, onMutate, context); + }, }); }; diff --git a/ui/apps/pmm/src/hooks/updates.ts b/ui/apps/pmm/src/hooks/updates.ts index 80cf707ad60..dbc10666bef 100644 --- a/ui/apps/pmm/src/hooks/updates.ts +++ b/ui/apps/pmm/src/hooks/updates.ts @@ -32,10 +32,8 @@ export const useSnooze = () => { return false; } - return ( - diffFromNow(user.info.snoozedAt) <= - parseDuration(settings.updateSnoozeDuration) - ); + const duration = settings.updateSnoozeDuration ?? '10s'; + return diffFromNow(user.info.snoozedAt) <= parseDuration(duration); }, [latest, user, settings]); const snoozeUpdate = useCallback(async () => { diff --git a/ui/apps/pmm/src/pages/settings/Settings.messages.ts b/ui/apps/pmm/src/pages/settings/Settings.messages.ts new file mode 100644 index 00000000000..8451a63971c --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/Settings.messages.ts @@ -0,0 +1,86 @@ +export const Messages = { + title: 'Settings', + tabs: { + ssh: 'SSH Key', + metrics: 'Metrics Resolution', + advanced: 'Advanced Settings', + }, + advanced: { + action: 'Apply changes', + retentionLabel: 'Data retention', + retentionTooltip: 'This is the value for how long data will be stored.', + retentionUnits: 'days', + retentionLink: 'https://per.co.na/data_retention', + telemetryLabel: 'Telemetry', + telemetryLink: 'https://per.co.na/telemetry', + telemetryTooltip: + 'Option to send usage data back to Percona to let us make our product better.', + telemetrySummaryTitle: + 'We gather and send the following information to Percona:', + updatesLabel: 'Check for updates', + updatesLink: 'https://per.co.na/updates', + updatesTooltip: + 'Option to check new versions and ability to update PMM from UI.', + advisorsLabel: 'Advisors', + sttRareIntervalLabel: 'Rare interval', + sttStandardIntervalLabel: 'Standard interval', + sttFrequentIntervalLabel: 'Frequent interval', + sttCheckIntervalsLabel: 'Execution Intervals', + sttCheckIntervalTooltip: 'Interval between check runs', + sttCheckIntervalUnit: 'hours', + advisorsLink: 'https://per.co.na/advisors', + advisorsTooltip: 'Enable Advisors and get updated checks from Percona.', + azureDiscoverLabel: 'Microsoft Azure monitoring', + azureDiscoverTooltip: + 'Option to enable/disable Microsoft Azure DB instances discovery and monitoring', + azureDiscoverLink: 'https://per.co.na/azure_monitoring', + accessControl: 'Access control', + accessControlTooltip: 'Option to enable/disable Access control.', + accessControlLink: 'https://per.co.na/roles_permissions', + publicAddressLabel: 'Public Address', + publicAddressTooltip: 'Public Address to this PMM server.', + publicAddressButton: 'Get from browser', + alertingLabel: 'Percona Alerting', + alertingTooltip: 'Option to enable/disable Percona Alerting features.', + alertingLink: 'https://per.co.na/alerting', + backupLabel: 'Backup Management', + backupTooltip: 'Option to enable/disable Backup Management features.', + backupLink: 'https://per.co.na/backup_management', + enableInternalPgQanLabel: 'QAN for PMM Server', + enableInternalPgQanTooltip: + "Displays queries from PMM Server's internal PostgreSQL database in Query Analytics (QAN). Enable to troubleshoot PMM Server's database performance alongside your monitored instances.", + enableInternalPgQanLink: 'https://per.co.na/qan-pmm-server', + technicalPreviewLegend: 'Technical preview features', + technicalPreviewDescription: + 'These are technical preview features, not recommended to be used in production environments. Read more about feature status', + technicalPreviewLinkText: 'here', + }, + metrics: { + action: 'Apply changes', + label: 'Metrics resolution, sec', + link: 'https://per.co.na/metrics_resolution', + options: { + rare: 'Rare', + standard: 'Standard', + frequent: 'Frequent', + custom: 'Custom', + }, + intervals: { + low: 'Low', + medium: 'Medium', + high: 'High', + }, + tooltip: 'This setting defines how frequently the data will be collected.', + }, + ssh: { + action: 'Apply SSH key', + label: 'SSH key', + link: 'https://per.co.na/ssh_key', + tooltip: 'Public SSH key to let you login into the server using SSH.', + }, + service: { + success: 'Settings updated', + }, + tooltipLinkText: 'Read more', + unauthorized: 'Insufficient access permissions.', +}; diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx new file mode 100644 index 00000000000..87e6033e899 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -0,0 +1,59 @@ +import { Box, CircularProgress, Stack, Tab, Tabs } from '@mui/material'; +import { FC, useState } from 'react'; +import { Page } from 'components/page'; +import { useSettings } from 'hooks/api/useSettings'; +import { SshKeyForm } from './components/ssh-key/SshKeyForm'; +import { MetricsResolutionForm } from './components/metrics-resolution/MetricsResolutionForm'; +import { AdvancedSettingsForm } from './components/advanced/AdvancedSettingsForm'; +import { Messages } from './Settings.messages'; + +type TabValue = 'ssh' | 'metrics' | 'advanced'; + +export const Settings: FC = () => { + const [tab, setTab] = useState('ssh'); + const { data: settings, isLoading } = useSettings(); + + if (isLoading || !settings) { + return ( + + + + + + ); + } + + return ( + + + setTab(value)} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + + + {tab === 'ssh' && } + {tab === 'metrics' && } + {tab === 'advanced' && } + + + + ); +}; diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx new file mode 100644 index 00000000000..1209ec43baf --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx @@ -0,0 +1,11 @@ +import { Typography } from '@mui/material'; +import { FC } from 'react'; +import { Settings } from 'types/settings.types'; + +interface AdvancedSettingsFormProps { + settings: Settings; +} + +export const AdvancedSettingsForm: FC = () => { + return Advanced Settings form placeholder; +}; diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx new file mode 100644 index 00000000000..bb9be395b74 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx @@ -0,0 +1,11 @@ +import { Typography } from '@mui/material'; +import { FC } from 'react'; +import { Settings } from 'types/settings.types'; + +interface MetricsResolutionFormProps { + settings: Settings; +} + +export const MetricsResolutionForm: FC = () => { + return Metrics Resolution form placeholder; +}; diff --git a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx new file mode 100644 index 00000000000..622c34743ae --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx @@ -0,0 +1,11 @@ +import { Typography } from '@mui/material'; +import { FC } from 'react'; +import { Settings } from 'types/settings.types'; + +interface SshKeyFormProps { + settings: Settings; +} + +export const SshKeyForm: FC = () => { + return SSH Key form placeholder; +}; diff --git a/ui/apps/pmm/src/pages/settings/index.ts b/ui/apps/pmm/src/pages/settings/index.ts new file mode 100644 index 00000000000..ec5d9791b6b --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/index.ts @@ -0,0 +1 @@ +export { Settings } from './Settings'; From b19e46dabc1162c39187af1ff91e5d8ec3933b43 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:47:36 +0100 Subject: [PATCH 03/52] feat(settings): implement SSH Key form with react-hook-form and useUpdateSettings Made-with: Cursor --- .../components/ssh-key/SshKeyForm.tsx | 108 +++++++++++++++++- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx index 622c34743ae..b9d5395e6e3 100644 --- a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx @@ -1,11 +1,111 @@ -import { Typography } from '@mui/material'; -import { FC } from 'react'; +import { + Button, + IconButton, + Link, + Stack, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { FC, useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { enqueueSnackbar } from 'notistack'; +import { useUpdateSettings } from 'hooks/api/useSettings'; import { Settings } from 'types/settings.types'; +import { Messages } from '../../Settings.messages'; interface SshKeyFormProps { settings: Settings; } -export const SshKeyForm: FC = () => { - return SSH Key form placeholder; +interface FormValues { + sshKey: string; +} + +export const SshKeyForm: FC = ({ settings }) => { + const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); + const { + register, + handleSubmit, + reset, + formState: { isDirty }, + } = useForm({ + defaultValues: { sshKey: settings.sshKey ?? '' }, + }); + + useEffect(() => { + reset({ sshKey: settings.sshKey ?? '' }); + }, [settings.sshKey, reset]); + + const onSubmit = async (values: FormValues) => { + try { + await updateSettings({ sshKey: values.sshKey }); + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset({ sshKey: values.sshKey }); + } catch (error) { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + } + }; + + const { label, link, tooltip, action } = Messages.ssh; + const { tooltipLinkText } = Messages; + + return ( + + + + {label} + + + {tooltip} + + {tooltipLinkText} + + + } + arrow + > + + + + + + + + + + + ); }; From b6d59231456a845c8026bb64bfcfe3d68a9b3ae8 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:49:02 +0100 Subject: [PATCH 04/52] feat(settings): implement Metrics Resolution form with react-hook-form Made-with: Cursor --- .../MetricsResolution.constants.ts | 21 ++ .../MetricsResolution.utils.ts | 35 +++ .../MetricsResolutionForm.tsx | 246 +++++++++++++++++- 3 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts create mode 100644 ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts new file mode 100644 index 00000000000..3c9528ec2b9 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts @@ -0,0 +1,21 @@ +import { Messages } from '../../Settings.messages'; +import { MetricsResolutions } from 'types/settings.types'; + +export const RESOLUTION_PRESETS = ['rare', 'standard', 'frequent', 'custom'] as const; +export type ResolutionPreset = (typeof RESOLUTION_PRESETS)[number]; + +export const resolutionOptions: { value: ResolutionPreset; label: string }[] = [ + { value: 'rare', label: Messages.metrics.options.rare }, + { value: 'standard', label: Messages.metrics.options.standard }, + { value: 'frequent', label: Messages.metrics.options.frequent }, + { value: 'custom', label: Messages.metrics.options.custom }, +]; + +export const defaultResolutions: MetricsResolutions[] = [ + { hr: '60s', mr: '180s', lr: '300s' }, + { hr: '5s', mr: '10s', lr: '60s' }, + { hr: '1s', mr: '5s', lr: '30s' }, +]; + +export const RESOLUTION_MIN = 1; +export const RESOLUTION_MAX = 1000000000; diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts new file mode 100644 index 00000000000..c013617d68f --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts @@ -0,0 +1,35 @@ +import { MetricsResolutions } from 'types/settings.types'; +import { + defaultResolutions, + resolutionOptions, + ResolutionPreset, +} from './MetricsResolution.constants'; + +const replaceS = (r: string) => r.replace(/s$/, ''); + +export const removeUnits = (r: MetricsResolutions): { lr: string; mr: string; hr: string } => ({ + lr: replaceS(r.lr), + mr: replaceS(r.mr), + hr: replaceS(r.hr), +}); + +export const addUnits = (r: { lr: string; mr: string; hr: string }): MetricsResolutions => ({ + lr: `${r.lr}s`, + mr: `${r.mr}s`, + hr: `${r.hr}s`, +}); + +const resolutionsEqual = (a: MetricsResolutions, b: MetricsResolutions) => + a.hr === b.hr && a.mr === b.mr && a.lr === b.lr; + +export const getResolutionPreset = ( + metricsResolutions: MetricsResolutions | undefined +): ResolutionPreset => { + if (!metricsResolutions) return 'custom'; + const index = defaultResolutions.findIndex((r) => + resolutionsEqual(r, metricsResolutions) + ); + return index !== -1 + ? (resolutionOptions[index].value as ResolutionPreset) + : 'custom'; +}; diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx index bb9be395b74..966a0cc707a 100644 --- a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx @@ -1,11 +1,247 @@ -import { Typography } from '@mui/material'; -import { FC } from 'react'; -import { Settings } from 'types/settings.types'; +import { + Button, + FormControl, + FormControlLabel, + IconButton, + Link, + Radio, + RadioGroup, + Stack, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import { FC, useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { enqueueSnackbar } from 'notistack'; +import { useUpdateSettings } from 'hooks/api/useSettings'; +import { MetricsResolutions, Settings } from 'types/settings.types'; +import { Messages } from '../../Settings.messages'; +import { + defaultResolutions, + resolutionOptions, + RESOLUTION_MAX, + RESOLUTION_MIN, +} from './MetricsResolution.constants'; +import { + addUnits, + getResolutionPreset, + removeUnits, +} from './MetricsResolution.utils'; interface MetricsResolutionFormProps { settings: Settings; } -export const MetricsResolutionForm: FC = () => { - return Metrics Resolution form placeholder; +interface FormValues { + preset: 'rare' | 'standard' | 'frequent' | 'custom'; + lr: string; + mr: string; + hr: string; +} + +export const MetricsResolutionForm: FC = ({ + settings, +}) => { + const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); + const metricsResolutions = settings.metricsResolutions ?? { + hr: '5s', + mr: '10s', + lr: '60s', + }; + const preset = getResolutionPreset(metricsResolutions); + const raw = removeUnits(metricsResolutions); + + const { + control, + handleSubmit, + reset, + watch, + setValue, + formState: { isDirty, errors }, + } = useForm({ + defaultValues: { + preset, + lr: raw.lr, + mr: raw.mr, + hr: raw.hr, + }, + }); + + const currentPreset = watch('preset'); + + useEffect(() => { + reset({ + preset: getResolutionPreset(metricsResolutions), + ...removeUnits(metricsResolutions), + }); + }, [metricsResolutions, reset]); + + useEffect(() => { + if (currentPreset && currentPreset !== 'custom') { + const idx = resolutionOptions.findIndex((o) => o.value === currentPreset); + if (idx >= 0) { + const def = removeUnits(defaultResolutions[idx]); + setValue('lr', def.lr, { shouldDirty: false }); + setValue('mr', def.mr, { shouldDirty: false }); + setValue('hr', def.hr, { shouldDirty: false }); + } + } + }, [currentPreset, setValue]); + + const onSubmit = async (values: FormValues) => { + try { + const payload: MetricsResolutions = addUnits({ + lr: values.lr, + mr: values.mr, + hr: values.hr, + }); + await updateSettings({ metricsResolutions: payload }); + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset({ + preset: values.preset, + lr: values.lr, + mr: values.mr, + hr: values.hr, + }); + } catch (error) { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + } + }; + + const { label, link, tooltip, action, intervals } = Messages.metrics; + const { tooltipLinkText } = Messages; + + const validateNumber = (v: string) => { + const n = parseInt(v, 10); + if (isNaN(n) || v === '') return 'Required'; + if (n < RESOLUTION_MIN || n > RESOLUTION_MAX) + return `Must be between ${RESOLUTION_MIN} and ${RESOLUTION_MAX}`; + return true; + }; + + return ( + + + + {label} + + + {tooltip} + + {tooltipLinkText} + + + } + arrow + > + + + + + + + ( + + + {resolutionOptions.map((opt) => ( + } + label={opt.label} + /> + ))} + + + )} + /> + + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + + + ); }; From 11c2ecfb4182051f2a44eba7e2178fd13e098d1f Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:50:33 +0100 Subject: [PATCH 05/52] feat(settings): implement Advanced Settings form with react-hook-form Made-with: Cursor --- .../components/advanced/Advanced.constants.ts | 19 + .../components/advanced/Advanced.utils.ts | 43 ++ .../advanced/AdvancedSettingsForm.tsx | 391 +++++++++++++++++- 3 files changed, 449 insertions(+), 4 deletions(-) create mode 100644 ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts create mode 100644 ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts new file mode 100644 index 00000000000..e28328b0cb5 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts @@ -0,0 +1,19 @@ +import { Messages } from '../../Settings.messages'; + +export const SECONDS = 60; +export const MINUTES = 60; +export const HOURS = 24; +export const SECONDS_IN_DAY = SECONDS * MINUTES * HOURS; +export const MINUTES_IN_HOUR = MINUTES * HOURS; +export const MIN_DAYS = 1; +export const MAX_DAYS = 3650; +export const MIN_STT_CHECK_INTERVAL = 0.1; +export const STT_CHECK_INTERVAL_STEP = 0.1; + +export const STT_CHECK_INTERVALS = [ + { label: Messages.advanced.sttRareIntervalLabel, name: 'rareInterval' as const }, + { label: Messages.advanced.sttStandardIntervalLabel, name: 'standardInterval' as const }, + { label: Messages.advanced.sttFrequentIntervalLabel, name: 'frequentInterval' as const }, +]; + +export const TECHNICAL_PREVIEW_DOC_URL = 'https://per.co.na/pmm-feature-status'; diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts new file mode 100644 index 00000000000..76df0b994bb --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts @@ -0,0 +1,43 @@ +import { AdvisorRunIntervalsSettings } from 'types/settings.types'; +import { HOURS, MINUTES_IN_HOUR, SECONDS_IN_DAY } from './Advanced.constants'; + +export const convertSecondsToDays = (dataRetention: string): number | '' => { + if (!dataRetention) return ''; + const value = parseFloat(dataRetention.replace(/[^\d.-]/g, '')); + const units = dataRetention.slice(-1).toLowerCase(); + + switch (units) { + case 'h': + return value / HOURS; + case 'm': + return value / MINUTES_IN_HOUR; + case 's': + return value / SECONDS_IN_DAY; + case 'd': + return value; + default: + return ''; + } +}; + +const parseSeconds = (s: string): number => { + const match = String(s).match(/^(\d+)s?$/); + return match ? parseInt(match[1], 10) : 0; +}; + +export const convertSecondsStringToHour = (secondsStr: string): number => + parseSeconds(secondsStr) / 3600; + +export const convertHoursStringToSeconds = (hours: string | number): number => + Math.round(parseFloat(String(hours)) * 3600); + +export const convertCheckIntervalsToHours = ( + sttCheckIntervals: AdvisorRunIntervalsSettings | undefined +) => { + if (!sttCheckIntervals) return { rareInterval: '24', standardInterval: '24', frequentInterval: '24' }; + return { + rareInterval: `${convertSecondsStringToHour(sttCheckIntervals.rareInterval)}`, + standardInterval: `${convertSecondsStringToHour(sttCheckIntervals.standardInterval)}`, + frequentInterval: `${convertSecondsStringToHour(sttCheckIntervals.frequentInterval)}`, + }; +}; diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx index 1209ec43baf..e585308dc90 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx @@ -1,11 +1,394 @@ -import { Typography } from '@mui/material'; -import { FC } from 'react'; +import { + Alert, + Box, + Button, + FormControlLabel, + IconButton, + Link, + Stack, + Switch, + TextField, + Tooltip, + Typography, +} from '@mui/material'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; +import LinkIcon from '@mui/icons-material/Link'; +import { FC, useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { enqueueSnackbar } from 'notistack'; +import { useUpdateSettings } from 'hooks/api/useSettings'; import { Settings } from 'types/settings.types'; +import { Messages } from '../../Settings.messages'; +import { + MAX_DAYS, + MIN_DAYS, + MIN_STT_CHECK_INTERVAL, + SECONDS_IN_DAY, + STT_CHECK_INTERVALS, + TECHNICAL_PREVIEW_DOC_URL, +} from './Advanced.constants'; +import { + convertCheckIntervalsToHours, + convertHoursStringToSeconds, + convertSecondsToDays, +} from './Advanced.utils'; interface AdvancedSettingsFormProps { settings: Settings; } -export const AdvancedSettingsForm: FC = () => { - return Advanced Settings form placeholder; +interface FormValues { + retention: string; + telemetry: boolean; + updates: boolean; + alerting: boolean; + backup: boolean; + enableInternalPgQan: boolean; + publicAddress: string; + stt: boolean; + rareInterval: string; + standardInterval: string; + frequentInterval: string; + azureDiscover: boolean; + accessControl: boolean; +} + +const LabelWithTooltip: FC<{ + label: string; + tooltip: React.ReactNode; + link?: string; +}> = ({ label, tooltip, link }) => ( + + {label} + + + + + + {link && ( + + {Messages.tooltipLinkText} + + )} + +); + +export const AdvancedSettingsForm: FC = ({ + settings, +}) => { + const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); + const intervals = convertCheckIntervalsToHours(settings.advisorRunIntervals); + + const { + control, + handleSubmit, + reset, + watch, + setValue, + formState: { isDirty, errors }, + } = useForm({ + defaultValues: { + retention: String(convertSecondsToDays(settings.dataRetention ?? '86400s') || '1'), + telemetry: settings.telemetryEnabled ?? false, + updates: settings.updatesEnabled ?? false, + alerting: settings.alertingEnabled ?? false, + backup: settings.backupManagementEnabled ?? false, + enableInternalPgQan: settings.enableInternalPgQan ?? false, + publicAddress: settings.pmmPublicAddress ?? '', + stt: settings.advisorEnabled ?? false, + rareInterval: intervals.rareInterval, + standardInterval: intervals.standardInterval, + frequentInterval: intervals.frequentInterval, + azureDiscover: settings.azurediscoverEnabled ?? false, + accessControl: settings.enableAccessControl ?? false, + }, + }); + + const sttEnabled = watch('stt'); + + useEffect(() => { + reset({ + retention: String(convertSecondsToDays(settings.dataRetention ?? '86400s') || '1'), + telemetry: settings.telemetryEnabled ?? false, + updates: settings.updatesEnabled ?? false, + alerting: settings.alertingEnabled ?? false, + backup: settings.backupManagementEnabled ?? false, + enableInternalPgQan: settings.enableInternalPgQan ?? false, + publicAddress: settings.pmmPublicAddress ?? '', + stt: settings.advisorEnabled ?? false, + ...convertCheckIntervalsToHours(settings.advisorRunIntervals), + azureDiscover: settings.azurediscoverEnabled ?? false, + accessControl: settings.enableAccessControl ?? false, + }); + }, [settings, reset]); + + const onSubmit = async (values: FormValues) => { + try { + const retentionNum = parseFloat(values.retention); + const dataRetention = `${Math.round(retentionNum * SECONDS_IN_DAY)}s`; + const sttCheckIntervals = { + rareInterval: `${convertHoursStringToSeconds(values.rareInterval)}s`, + standardInterval: `${convertHoursStringToSeconds(values.standardInterval)}s`, + frequentInterval: `${convertHoursStringToSeconds(values.frequentInterval)}s`, + }; + + await updateSettings({ + dataRetention, + pmmPublicAddress: values.publicAddress, + enableTelemetry: values.telemetry, + enableUpdates: values.updates, + enableAlerting: values.alerting, + enableBackupManagement: values.backup, + enableInternalPgQan: values.enableInternalPgQan, + enableAdvisor: values.stt, + advisorRunIntervals: values.stt ? sttCheckIntervals : undefined, + enableAzurediscover: values.azureDiscover, + enableAccessControl: values.accessControl, + }); + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset(values); + } catch (error) { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + } + }; + + const { action } = Messages.advanced; + const m = Messages.advanced; + + const validateRetention = (v: string) => { + const n = parseFloat(v); + if (isNaN(n) || v === '') return 'Required'; + if (n < MIN_DAYS || n > MAX_DAYS) return `Must be between ${MIN_DAYS} and ${MAX_DAYS}`; + return true; + }; + + const validateInterval = (v: string) => { + const n = parseFloat(v); + if (isNaN(n) || v === '') return 'Required'; + if (n < MIN_STT_CHECK_INTERVAL) return `Min ${MIN_STT_CHECK_INTERVAL}`; + return true; + }; + + return ( + + + + + + ( + + )} + /> + + {m.retentionUnits} + + + + {[ + { + name: 'telemetry' as const, + label: m.telemetryLabel, + tooltip: ( + + {m.telemetryTooltip} + {m.telemetrySummaryTitle} + {(settings.telemetrySummaries ?? []).map((s) => ( + + {s} + + ))} + + ), + link: m.telemetryLink, + }, + { + name: 'updates' as const, + label: m.updatesLabel, + tooltip: m.updatesTooltip, + link: m.updatesLink, + }, + { + name: 'alerting' as const, + label: m.alertingLabel, + tooltip: m.alertingTooltip, + link: m.alertingLink, + }, + { + name: 'backup' as const, + label: m.backupLabel, + tooltip: m.backupTooltip, + link: m.backupLink, + }, + { + name: 'enableInternalPgQan' as const, + label: m.enableInternalPgQanLabel, + tooltip: m.enableInternalPgQanTooltip, + link: m.enableInternalPgQanLink, + }, + ].map(({ name, label, tooltip, link }) => ( + ( + } + label={ + + } + /> + )} + /> + ))} + + + + + + ( + + )} + /> + + + + ( + } + label={} + /> + )} + /> + + + + + + {STT_CHECK_INTERVALS.map(({ name, label }) => ( + ( + + + + {m.sttCheckIntervalUnit} + + + )} + /> + ))} + + + + + {m.technicalPreviewLegend} + + + {m.technicalPreviewDescription}{' '} + + {m.technicalPreviewLinkText} + + + + ( + } + label={ + + } + /> + )} + /> + ( + } + label={ + + } + /> + )} + /> + + + + + + ); }; From a6bba5cfe614df1ca0655d8de96b76a292e40a2c Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 11:51:28 +0100 Subject: [PATCH 06/52] feat(settings): add /settings route, update Configuration menu and PMM_SETTINGS_URL Made-with: Cursor --- .../pmm/src/contexts/navigation/navigation.constants.ts | 8 +++++--- ui/apps/pmm/src/lib/constants.ts | 2 +- ui/apps/pmm/src/router.tsx | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts b/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts index 5480fc056ba..0dcaae662bd 100644 --- a/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts +++ b/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts @@ -616,8 +616,10 @@ export const NAV_CONFIGURATION: NavItem = { id: 'configuration', icon: 'configuration', text: 'Configuration', - url: `${PMM_NEW_NAV_GRAFANA_PATH}/settings`, + url: `${PMM_NEW_NAV_PATH}/settings`, matches: [ + `${PMM_NEW_NAV_PATH}/settings`, + `${PMM_NEW_NAV_PATH}/settings/*`, `${PMM_NEW_NAV_GRAFANA_PATH}/plugins`, `${PMM_NEW_NAV_GRAFANA_PATH}/admin`, `${PMM_NEW_NAV_GRAFANA_PATH}/admin/general`, @@ -630,8 +632,8 @@ export const NAV_CONFIGURATION: NavItem = { { id: 'configuration-settings', text: 'Settings', - url: `${PMM_NEW_NAV_GRAFANA_PATH}/settings/advanced-settings`, - matches: [`${PMM_NEW_NAV_GRAFANA_PATH}/settings/*`], + url: `${PMM_NEW_NAV_PATH}/settings`, + matches: [`${PMM_NEW_NAV_PATH}/settings`, `${PMM_NEW_NAV_PATH}/settings/*`], }, { id: 'updates', diff --git a/ui/apps/pmm/src/lib/constants.ts b/ui/apps/pmm/src/lib/constants.ts index 28d8abcaa5b..cc28370b528 100644 --- a/ui/apps/pmm/src/lib/constants.ts +++ b/ui/apps/pmm/src/lib/constants.ts @@ -9,7 +9,7 @@ export const PMM_BASE_PATH = `/pmm-ui${PMM_NEW_NAV_PATH}`; export const PMM_NEW_NAV_GRAFANA_PATH = `${PMM_NEW_NAV_PATH}${GRAFANA_SUB_PATH}`; export const PMM_HOME_URL = `${GRAFANA_SUB_PATH}/d/pmm-home`; export const PMM_LOGIN_URL = `${GRAFANA_SUB_PATH}/login`; -export const PMM_SETTINGS_URL = `${GRAFANA_SUB_PATH}/settings/advanced-settings`; +export const PMM_SETTINGS_URL = `${PMM_BASE_PATH}/settings`; export const PMM_NEW_NAV_UPDATES_PATH = `${PMM_NEW_NAV_PATH}/updates`; export const PMM_SUPPORT_URL = 'https://per.co.na/pmm_documentation'; export const PMM_DOCS_UPDATES_URL = 'https://per.co.na/pmm-upgrade'; diff --git a/ui/apps/pmm/src/router.tsx b/ui/apps/pmm/src/router.tsx index 1565d6ead58..be3cde38c3a 100644 --- a/ui/apps/pmm/src/router.tsx +++ b/ui/apps/pmm/src/router.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Navigate, createBrowserRouter } from 'react-router-dom'; +import { Settings } from 'pages/settings'; import { Updates } from 'pages/updates'; import { UpdateClients } from 'pages/update-clients/UpdateClients'; import { MainWithNav } from 'components/main/MainWithNav'; @@ -39,6 +40,10 @@ const router = createBrowserRouter( path: 'help', element: , }, + { + path: 'settings', + element: , + }, { path: 'rta', children: [ From 805e281cd298fece44ecefdfb1bc63911bc17ee6 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 12:02:07 +0100 Subject: [PATCH 07/52] fix(settings): resolve useMemo exhaustive-deps lint; add Settings loading test Made-with: Cursor --- .../pmm/src/pages/settings/Settings.test.tsx | 30 +++++++++++++++++++ .../MetricsResolutionForm.tsx | 13 ++++---- 2 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 ui/apps/pmm/src/pages/settings/Settings.test.tsx diff --git a/ui/apps/pmm/src/pages/settings/Settings.test.tsx b/ui/apps/pmm/src/pages/settings/Settings.test.tsx new file mode 100644 index 00000000000..6883f815d57 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/Settings.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react'; +import { Settings } from './Settings'; +import { TestWrapper } from 'utils/testWrapper'; +import { wrapWithQueryProvider } from 'utils/testUtils'; +import { MemoryRouter } from 'react-router-dom'; +import * as settingsApi from 'api/settings'; + +vi.mock('api/settings'); + +const getSettingsMock = vi.mocked(settingsApi.getSettings); + +describe('Settings', () => { + beforeEach(() => { + getSettingsMock.mockImplementation(() => new Promise(() => {})); + }); + + it('shows loading state when settings are not yet loaded', () => { + render( + + {wrapWithQueryProvider( + + + + )} + + ); + + expect(screen.getByTestId('settings-loading')).toBeInTheDocument(); + }); +}); diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx index 966a0cc707a..5a9c084040e 100644 --- a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx @@ -12,7 +12,7 @@ import { Typography, } from '@mui/material'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import { FC, useEffect } from 'react'; +import { FC, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { enqueueSnackbar } from 'notistack'; import { useUpdateSettings } from 'hooks/api/useSettings'; @@ -41,15 +41,16 @@ interface FormValues { hr: string; } +const DEFAULT_METRICS = { hr: '5s', mr: '10s', lr: '60s' } as const; + export const MetricsResolutionForm: FC = ({ settings, }) => { const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); - const metricsResolutions = settings.metricsResolutions ?? { - hr: '5s', - mr: '10s', - lr: '60s', - }; + const metricsResolutions = useMemo( + () => settings.metricsResolutions ?? DEFAULT_METRICS, + [settings.metricsResolutions] + ); const preset = getResolutionPreset(metricsResolutions); const raw = removeUnits(metricsResolutions); From 261a367a3894548ff092a9453649bb4676f16303 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 19 Mar 2026 13:15:42 +0100 Subject: [PATCH 08/52] feat: smaller improvements --- ui/apps/pmm/src/components/page/Page.tsx | 19 ++++-- ui/apps/pmm/src/components/page/Page.types.ts | 1 + ui/apps/pmm/src/pages/settings/Settings.tsx | 16 ++--- .../advanced/AdvancedSettingsForm.tsx | 62 +++++++++++++++---- 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/ui/apps/pmm/src/components/page/Page.tsx b/ui/apps/pmm/src/components/page/Page.tsx index 55e568f1c1e..d70cab8a790 100644 --- a/ui/apps/pmm/src/components/page/Page.tsx +++ b/ui/apps/pmm/src/components/page/Page.tsx @@ -14,27 +14,34 @@ import { PMM_HOME_URL } from 'lib/constants'; import { Footer } from 'components/footer'; import { updateDocumentTitle } from 'utils/document.utils'; -export const Page: FC = ({ title, topBar, footer, children }) => { +export const Page: FC = ({ + title, + topBar, + footer, + children, + fullWidth, +}) => { const { user } = useUser(); updateDocumentTitle(title); return ( ({ + sx={{ flex: 1, - [theme.breakpoints.up('lg')]: { - width: 1000, - }, width: { md: 'auto', + lg: fullWidth ? '100%' : 1000, }, p: { xs: 2, }, + px: { + md: fullWidth ? 4 : undefined, + }, mx: 'auto', gap: 3, mt: 1, - })} + }} > {topBar} {!!title && {title}} diff --git a/ui/apps/pmm/src/components/page/Page.types.ts b/ui/apps/pmm/src/components/page/Page.types.ts index 994f960f30f..3ab8baf6b0b 100644 --- a/ui/apps/pmm/src/components/page/Page.types.ts +++ b/ui/apps/pmm/src/components/page/Page.types.ts @@ -4,4 +4,5 @@ export interface PageProps extends PropsWithChildren { title?: string; footer?: ReactNode; topBar?: ReactNode; + fullWidth?: boolean; } diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index 87e6033e899..43eb34496c4 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -10,7 +10,7 @@ import { Messages } from './Settings.messages'; type TabValue = 'ssh' | 'metrics' | 'advanced'; export const Settings: FC = () => { - const [tab, setTab] = useState('ssh'); + const [tab, setTab] = useState('advanced'); const { data: settings, isLoading } = useSettings(); if (isLoading || !settings) { @@ -24,13 +24,18 @@ export const Settings: FC = () => { } return ( - + setTab(value)} sx={{ borderBottom: 1, borderColor: 'divider' }} > + { value="metrics" label={Messages.tabs.metrics} /> - + {tab === 'advanced' && } {tab === 'ssh' && } {tab === 'metrics' && } - {tab === 'advanced' && } diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx index e585308dc90..91f51020075 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx @@ -66,7 +66,12 @@ const LabelWithTooltip: FC<{ {link && ( - + {Messages.tooltipLinkText} )} @@ -88,7 +93,9 @@ export const AdvancedSettingsForm: FC = ({ formState: { isDirty, errors }, } = useForm({ defaultValues: { - retention: String(convertSecondsToDays(settings.dataRetention ?? '86400s') || '1'), + retention: String( + convertSecondsToDays(settings.dataRetention ?? '86400s') || '1' + ), telemetry: settings.telemetryEnabled ?? false, updates: settings.updatesEnabled ?? false, alerting: settings.alertingEnabled ?? false, @@ -108,7 +115,9 @@ export const AdvancedSettingsForm: FC = ({ useEffect(() => { reset({ - retention: String(convertSecondsToDays(settings.dataRetention ?? '86400s') || '1'), + retention: String( + convertSecondsToDays(settings.dataRetention ?? '86400s') || '1' + ), telemetry: settings.telemetryEnabled ?? false, updates: settings.updatesEnabled ?? false, alerting: settings.alertingEnabled ?? false, @@ -161,7 +170,8 @@ export const AdvancedSettingsForm: FC = ({ const validateRetention = (v: string) => { const n = parseFloat(v); if (isNaN(n) || v === '') return 'Required'; - if (n < MIN_DAYS || n > MAX_DAYS) return `Must be between ${MIN_DAYS} and ${MAX_DAYS}`; + if (n < MIN_DAYS || n > MAX_DAYS) + return `Must be between ${MIN_DAYS} and ${MAX_DAYS}`; return true; }; @@ -177,7 +187,7 @@ export const AdvancedSettingsForm: FC = ({ component="form" onSubmit={handleSubmit(onSubmit)} gap={2} - maxWidth={600} + maxWidth={{ xs: '100%', md: 600 }} > = ({ ))} - + - + ( - + )} /> @@ -291,13 +316,22 @@ export const AdvancedSettingsForm: FC = ({ render={({ field }) => ( } - label={} + label={ + + } /> )} /> - + {STT_CHECK_INTERVALS.map(({ name, label }) => ( @@ -341,7 +375,11 @@ export const AdvancedSettingsForm: FC = ({ {m.technicalPreviewDescription}{' '} - + {m.technicalPreviewLinkText} From 614e2c7be65f96416f91e36752babeb4b15d37de Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Mon, 23 Mar 2026 09:07:06 +0100 Subject: [PATCH 09/52] fix: tooltips on advanced page --- .../advanced/AdvancedSettingsForm.tsx | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx index 91f51020075..08aafc041e6 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx @@ -60,21 +60,31 @@ const LabelWithTooltip: FC<{ }> = ({ label, tooltip, link }) => ( {label} - + + + {tooltip} + {link && ( + + {Messages.tooltipLinkText} + + )} + + + } + arrow + > - {link && ( - - {Messages.tooltipLinkText} - - )} ); @@ -222,14 +232,31 @@ export const AdvancedSettingsForm: FC = ({ name: 'telemetry' as const, label: m.telemetryLabel, tooltip: ( - + {m.telemetryTooltip} {m.telemetrySummaryTitle} - {(settings.telemetrySummaries ?? []).map((s) => ( - - {s} - - ))} + + {(settings.telemetrySummaries ?? []).map((s) => ( + + {s} + + ))} + ), link: m.telemetryLink, From 7ba6546394bbac5cca0062f2d7a5c3612eb72483 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 2 Apr 2026 14:49:17 +0200 Subject: [PATCH 10/52] feat: update imports, use types files, run formatter --- ui/apps/pmm/README.md | 1 + .../ha-badge/HighAvailabilityBadge.styles.ts | 4 +- .../ha-icon/HighAvailabilityIcon.styles.ts | 4 +- .../src/components/main/MainWithNav.test.tsx | 10 +- .../qan-header-actions/QanHeaderActions.tsx | 5 +- .../syntax-highlighter/SyntaxHighlighter.tsx | 4 +- .../navigation/navigation.constants.ts | 5 +- .../navigation/navigation.provider.tsx | 1 - ui/apps/pmm/src/hooks/api/useSettings.ts | 4 +- .../pmm/src/pages/help-center/HelpCenter.tsx | 1 - .../selection-form/RealtimeSelectionForm.tsx | 4 +- .../components/ServiceTags.tsx | 7 +- .../overview/details-pane/BigNumberMetric.tsx | 15 ++- .../details-pane/QueryAndDetails.test.tsx | 5 +- .../overview/details-pane/QueryAndDetails.tsx | 91 ++++++++++---- .../overview/table/query-cell/QueryCell.tsx | 5 +- .../rta/selection/RealtimeSelection.test.tsx | 2 +- ui/apps/pmm/src/pages/settings/Settings.tsx | 9 +- .../pmm/src/pages/settings/Settings.types.ts | 1 + .../components/advanced/Advanced.constants.ts | 15 ++- .../components/advanced/Advanced.utils.ts | 11 +- .../advanced/AdvancedSettingsForm.tsx | 113 ++++++++---------- .../advanced/AdvancedSettingsForm.types.ts | 27 +++++ .../MetricsResolution.constants.ts | 7 +- .../MetricsResolution.utils.ts | 10 +- .../MetricsResolutionForm.tsx | 102 ++++++++-------- .../MetricsResolutionForm.types.ts | 12 ++ .../components/ssh-key/SshKeyForm.tsx | 56 ++++----- .../components/ssh-key/SshKeyForm.types.ts | 9 ++ ui/apps/pmm/src/types/settings.types.ts | 19 +-- ui/apps/pmm/vitest.config.ts | 24 +++- 31 files changed, 368 insertions(+), 215 deletions(-) create mode 100644 ui/apps/pmm/src/pages/settings/Settings.types.ts create mode 100644 ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.types.ts create mode 100644 ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.types.ts create mode 100644 ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.types.ts diff --git a/ui/apps/pmm/README.md b/ui/apps/pmm/README.md index 1912c40897c..15be98de381 100644 --- a/ui/apps/pmm/README.md +++ b/ui/apps/pmm/README.md @@ -12,6 +12,7 @@ See the [PMM Documentation](https://www.percona.com/doc/percona-monitoring-and-m See detailed information about prerequisites and setup [here](../../README.md) # Locally testing @percona/percona-ui + - Checkout code from https://github.com/percona/percona-ui - From the lib folder, run `pnpm build:watch` and `yarn link` - On this repo's `ui/apps/pmm` folder, run `yarn link @percona/percona-ui` and uncomment the `exclude` block from `vite.config.ts` diff --git a/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts b/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts index 09263cabdf4..4dbd5e705a8 100644 --- a/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts +++ b/ui/apps/pmm/src/components/ha-badge/HighAvailabilityBadge.styles.ts @@ -20,8 +20,8 @@ export const getStyles = (theme: Theme) => ({ : theme.palette.error.dark, backgroundColor: theme.palette.mode === 'light' - ? theme.palette.error.surface - : theme.palette.error.dark, + ? theme.palette.error.surface + : theme.palette.error.dark, transition: 'none', }, down: { diff --git a/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts b/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts index eb647ad3bfd..744c8ba14c3 100644 --- a/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts +++ b/ui/apps/pmm/src/components/ha-icon/HighAvailabilityIcon.styles.ts @@ -1,6 +1,8 @@ import { Theme } from '@mui/material'; -export const getStyles = ({ palette: { background, warning, error, info } }: Theme) => ({ +export const getStyles = ({ + palette: { background, warning, error, info }, +}: Theme) => ({ icon: { width: 20, height: 20, diff --git a/ui/apps/pmm/src/components/main/MainWithNav.test.tsx b/ui/apps/pmm/src/components/main/MainWithNav.test.tsx index 1cddbefdb2a..d1c8f1e6bd9 100644 --- a/ui/apps/pmm/src/components/main/MainWithNav.test.tsx +++ b/ui/apps/pmm/src/components/main/MainWithNav.test.tsx @@ -32,7 +32,7 @@ const setup = ({ )} ); -} +}; describe('MainWithNav', () => { it('shows loading', () => { @@ -60,12 +60,16 @@ describe('MainWithNav', () => { }); it('hides sidebar so the renderer gets a minimal layout', () => { - setup({ isLoading: false, isLoggedIn: true, kioskModeActive: false, search: 'render=1' }); + setup({ + isLoading: false, + isLoggedIn: true, + kioskModeActive: false, + search: 'render=1', + }); expect(screen.queryByTestId('pmm-sidebar')).toBeNull(); }); - it('shows sidebar when not in renderer mode', () => { setup({ isLoading: false, isLoggedIn: true, kioskModeActive: false }); diff --git a/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx b/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx index 4f6492fa54e..093d33a2e0e 100644 --- a/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx +++ b/ui/apps/pmm/src/components/main/header/qan-header/qan-header-actions/QanHeaderActions.tsx @@ -12,7 +12,10 @@ export const QanHeaderActions: FC = () => { const handleCopy = async () => { try { - const path = constructUrl(location).replace(/\/pmm-ui\/(next\/)?graph\//, ''); + const path = constructUrl(location).replace( + /\/pmm-ui\/(next\/)?graph\//, + '' + ); const res = location.pathname.includes('/graph') ? await createShortUrl(path) : { url: window.location.href }; diff --git a/ui/apps/pmm/src/components/syntax-highlighter/SyntaxHighlighter.tsx b/ui/apps/pmm/src/components/syntax-highlighter/SyntaxHighlighter.tsx index 3de9a156ea3..f8ee01b9756 100644 --- a/ui/apps/pmm/src/components/syntax-highlighter/SyntaxHighlighter.tsx +++ b/ui/apps/pmm/src/components/syntax-highlighter/SyntaxHighlighter.tsx @@ -38,7 +38,9 @@ const SyntaxHighlighter: FC = ({ await navigator.clipboard.writeText(content); enqueueSnackbar('Query copied to clipboard', { variant: 'success' }); } catch (error) { - enqueueSnackbar('Failed to copy query to clipboard', { variant: 'error' }); + enqueueSnackbar('Failed to copy query to clipboard', { + variant: 'error', + }); } } else { enqueueSnackbar('Clipboard is not available', { variant: 'error' }); diff --git a/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts b/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts index 0dcaae662bd..83234357af0 100644 --- a/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts +++ b/ui/apps/pmm/src/contexts/navigation/navigation.constants.ts @@ -633,7 +633,10 @@ export const NAV_CONFIGURATION: NavItem = { id: 'configuration-settings', text: 'Settings', url: `${PMM_NEW_NAV_PATH}/settings`, - matches: [`${PMM_NEW_NAV_PATH}/settings`, `${PMM_NEW_NAV_PATH}/settings/*`], + matches: [ + `${PMM_NEW_NAV_PATH}/settings`, + `${PMM_NEW_NAV_PATH}/settings/*`, + ], }, { id: 'updates', diff --git a/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx b/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx index aacf59750f9..fc1df0ea70d 100644 --- a/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx +++ b/ui/apps/pmm/src/contexts/navigation/navigation.provider.tsx @@ -70,7 +70,6 @@ export const NavigationProvider: FC = ({ children }) => { items.push(NAV_QAN); if (user && settings) { - if (settings.frontend.exploreEnabled && user.isEditor) { items.push(addExplore(settings.frontend)); } diff --git a/ui/apps/pmm/src/hooks/api/useSettings.ts b/ui/apps/pmm/src/hooks/api/useSettings.ts index 45abe2ae133..3787aa553d0 100644 --- a/ui/apps/pmm/src/hooks/api/useSettings.ts +++ b/ui/apps/pmm/src/hooks/api/useSettings.ts @@ -45,9 +45,7 @@ export const useFrontendSettings = ( }); export const useUpdateSettings = ( - options?: Partial< - UseMutationOptions - > + options?: Partial> ) => { const queryClient = useQueryClient(); diff --git a/ui/apps/pmm/src/pages/help-center/HelpCenter.tsx b/ui/apps/pmm/src/pages/help-center/HelpCenter.tsx index 6ba0c037732..76878fbf4b5 100644 --- a/ui/apps/pmm/src/pages/help-center/HelpCenter.tsx +++ b/ui/apps/pmm/src/pages/help-center/HelpCenter.tsx @@ -1,4 +1,3 @@ - import Box from '@mui/material/Box'; import { Page } from 'components/page'; import { FC, useMemo } from 'react'; diff --git a/ui/apps/pmm/src/pages/rta/components/selection-form/RealtimeSelectionForm.tsx b/ui/apps/pmm/src/pages/rta/components/selection-form/RealtimeSelectionForm.tsx index 362cdd6bf32..d55f41c3b63 100644 --- a/ui/apps/pmm/src/pages/rta/components/selection-form/RealtimeSelectionForm.tsx +++ b/ui/apps/pmm/src/pages/rta/components/selection-form/RealtimeSelectionForm.tsx @@ -47,7 +47,9 @@ const RealtimeSelectionForm: FC = ({ onSuccess }) => { size="large" onClick={handleStart} disabled={ - serviceIds.length === 0 || startSessions.isPending || !user?.isPMMAdmin + serviceIds.length === 0 || + startSessions.isPending || + !user?.isPMMAdmin } sx={{ borderRadius: 999, diff --git a/ui/apps/pmm/src/pages/rta/components/services-autocomplete-input/components/ServiceTags.tsx b/ui/apps/pmm/src/pages/rta/components/services-autocomplete-input/components/ServiceTags.tsx index 774a70157aa..e656d2cba6a 100644 --- a/ui/apps/pmm/src/pages/rta/components/services-autocomplete-input/components/ServiceTags.tsx +++ b/ui/apps/pmm/src/pages/rta/components/services-autocomplete-input/components/ServiceTags.tsx @@ -45,7 +45,12 @@ const ServiceTags: FC = ({ tagPresentation, value, getTagProps }) => { flexWrap="wrap" > {value.slice(0, 2).map((option, index) => ( - + ))} {value.length > 2 && ( +{count - 2} diff --git a/ui/apps/pmm/src/pages/rta/overview/details-pane/BigNumberMetric.tsx b/ui/apps/pmm/src/pages/rta/overview/details-pane/BigNumberMetric.tsx index 06543485292..9849254ea9d 100644 --- a/ui/apps/pmm/src/pages/rta/overview/details-pane/BigNumberMetric.tsx +++ b/ui/apps/pmm/src/pages/rta/overview/details-pane/BigNumberMetric.tsx @@ -10,12 +10,21 @@ type Props = { props?: { mainText?: TypographyProps; subText?: TypographyProps; - } + }; dataTestId?: string; }; -const BigNumberMetric: FC = ({ mainText, subText, size = 'medium', props, dataTestId }) => ( - +const BigNumberMetric: FC = ({ + mainText, + subText, + size = 'medium', + props, + dataTestId, +}) => ( + {mainText ? ( { // 2021-01-01T00:00:00Z in America/New_York (EST, UTC-5) is 2020-12-31 19:00:00 const userWithTimezone = { ...TEST_USER_ADMIN, - preferences: { ...TEST_USER_ADMIN.preferences, timezone: 'America/New_York' }, + preferences: { + ...TEST_USER_ADMIN.preferences, + timezone: 'America/New_York', + }, }; renderComponent(userWithTimezone); diff --git a/ui/apps/pmm/src/pages/rta/overview/details-pane/QueryAndDetails.tsx b/ui/apps/pmm/src/pages/rta/overview/details-pane/QueryAndDetails.tsx index b2cc3c4c2a2..52d587d8688 100644 --- a/ui/apps/pmm/src/pages/rta/overview/details-pane/QueryAndDetails.tsx +++ b/ui/apps/pmm/src/pages/rta/overview/details-pane/QueryAndDetails.tsx @@ -1,7 +1,7 @@ import Grid from '@mui/material/Grid'; import { FC } from 'react'; import { format, formatDuration } from 'date-fns'; -import { tz } from "@date-fns/tz"; +import { tz } from '@date-fns/tz'; import { SyntaxHighlighter } from 'components/syntax-highlighter'; import { QueryData } from 'types/rta.types'; import DetailsMetric from './DetailsMetric'; @@ -45,13 +45,13 @@ const QueryAndDetails: FC = ({ const formattedQueryExecutionDuration = queryExecutionDurationMs ? formatDuration( - { - seconds: queryExecutionDurationMs, - }, - { - format: ['seconds'], - } - ) + { + seconds: queryExecutionDurationMs, + }, + { + format: ['seconds'], + } + ) : ''; const formattedQueryExecutionDurationParts = formattedQueryExecutionDuration @@ -63,7 +63,10 @@ const QueryAndDetails: FC = ({ - + = ({ - + 1 @@ -88,7 +94,10 @@ const QueryAndDetails: FC = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + = ({ - + - + diff --git a/ui/apps/pmm/src/pages/rta/overview/table/query-cell/QueryCell.tsx b/ui/apps/pmm/src/pages/rta/overview/table/query-cell/QueryCell.tsx index 0fd6a231a6d..6fa73b5326b 100644 --- a/ui/apps/pmm/src/pages/rta/overview/table/query-cell/QueryCell.tsx +++ b/ui/apps/pmm/src/pages/rta/overview/table/query-cell/QueryCell.tsx @@ -7,7 +7,10 @@ export interface Props { const QueryCell: FC = ({ query }) => ( { describe('Loading States', () => { it('shows loading indicator while fetching services', () => { vi.mocked(realtimeApi.getAvailableServices).mockImplementation( - () => new Promise(() => { }) + () => new Promise(() => {}) ); renderComponent(); diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index 43eb34496c4..a57532e0a3c 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -1,4 +1,8 @@ -import { Box, CircularProgress, Stack, Tab, Tabs } from '@mui/material'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; import { FC, useState } from 'react'; import { Page } from 'components/page'; import { useSettings } from 'hooks/api/useSettings'; @@ -6,8 +10,7 @@ import { SshKeyForm } from './components/ssh-key/SshKeyForm'; import { MetricsResolutionForm } from './components/metrics-resolution/MetricsResolutionForm'; import { AdvancedSettingsForm } from './components/advanced/AdvancedSettingsForm'; import { Messages } from './Settings.messages'; - -type TabValue = 'ssh' | 'metrics' | 'advanced'; +import { TabValue } from './Settings.types'; export const Settings: FC = () => { const [tab, setTab] = useState('advanced'); diff --git a/ui/apps/pmm/src/pages/settings/Settings.types.ts b/ui/apps/pmm/src/pages/settings/Settings.types.ts new file mode 100644 index 00000000000..57fcc0f0d71 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/Settings.types.ts @@ -0,0 +1 @@ +export type TabValue = 'ssh' | 'metrics' | 'advanced'; diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts index e28328b0cb5..0b8fede1a5c 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts +++ b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.constants.ts @@ -11,9 +11,18 @@ export const MIN_STT_CHECK_INTERVAL = 0.1; export const STT_CHECK_INTERVAL_STEP = 0.1; export const STT_CHECK_INTERVALS = [ - { label: Messages.advanced.sttRareIntervalLabel, name: 'rareInterval' as const }, - { label: Messages.advanced.sttStandardIntervalLabel, name: 'standardInterval' as const }, - { label: Messages.advanced.sttFrequentIntervalLabel, name: 'frequentInterval' as const }, + { + label: Messages.advanced.sttRareIntervalLabel, + name: 'rareInterval' as const, + }, + { + label: Messages.advanced.sttStandardIntervalLabel, + name: 'standardInterval' as const, + }, + { + label: Messages.advanced.sttFrequentIntervalLabel, + name: 'frequentInterval' as const, + }, ]; export const TECHNICAL_PREVIEW_DOC_URL = 'https://per.co.na/pmm-feature-status'; diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts index 76df0b994bb..3c1ea3712c1 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts +++ b/ui/apps/pmm/src/pages/settings/components/advanced/Advanced.utils.ts @@ -1,4 +1,4 @@ -import { AdvisorRunIntervalsSettings } from 'types/settings.types'; +import { AdvisorRunIntervals } from 'types/settings.types'; import { HOURS, MINUTES_IN_HOUR, SECONDS_IN_DAY } from './Advanced.constants'; export const convertSecondsToDays = (dataRetention: string): number | '' => { @@ -32,9 +32,14 @@ export const convertHoursStringToSeconds = (hours: string | number): number => Math.round(parseFloat(String(hours)) * 3600); export const convertCheckIntervalsToHours = ( - sttCheckIntervals: AdvisorRunIntervalsSettings | undefined + sttCheckIntervals: AdvisorRunIntervals | undefined ) => { - if (!sttCheckIntervals) return { rareInterval: '24', standardInterval: '24', frequentInterval: '24' }; + if (!sttCheckIntervals) + return { + rareInterval: '24', + standardInterval: '24', + frequentInterval: '24', + }; return { rareInterval: `${convertSecondsStringToHour(sttCheckIntervals.rareInterval)}`, standardInterval: `${convertSecondsStringToHour(sttCheckIntervals.standardInterval)}`, diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx index 08aafc041e6..377237a9823 100644 --- a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.tsx @@ -1,23 +1,20 @@ -import { - Alert, - Box, - Button, - FormControlLabel, - IconButton, - Link, - Stack, - Switch, - TextField, - Tooltip, - Typography, -} from '@mui/material'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import IconButton from '@mui/material/IconButton'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import LinkIcon from '@mui/icons-material/Link'; import { FC, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { enqueueSnackbar } from 'notistack'; import { useUpdateSettings } from 'hooks/api/useSettings'; -import { Settings } from 'types/settings.types'; import { Messages } from '../../Settings.messages'; import { MAX_DAYS, @@ -32,32 +29,17 @@ import { convertHoursStringToSeconds, convertSecondsToDays, } from './Advanced.utils'; +import { + AdvancedSettingsFormProps, + AdvancedSettingsFormValues, + LabelWithTooltipProps, +} from './AdvancedSettingsForm.types'; -interface AdvancedSettingsFormProps { - settings: Settings; -} - -interface FormValues { - retention: string; - telemetry: boolean; - updates: boolean; - alerting: boolean; - backup: boolean; - enableInternalPgQan: boolean; - publicAddress: string; - stt: boolean; - rareInterval: string; - standardInterval: string; - frequentInterval: string; - azureDiscover: boolean; - accessControl: boolean; -} - -const LabelWithTooltip: FC<{ - label: string; - tooltip: React.ReactNode; - link?: string; -}> = ({ label, tooltip, link }) => ( +const LabelWithTooltip: FC = ({ + label, + tooltip, + link, +}) => ( {label} = ({ watch, setValue, formState: { isDirty, errors }, - } = useForm({ + } = useForm({ defaultValues: { retention: String( convertSecondsToDays(settings.dataRetention ?? '86400s') || '1' @@ -141,17 +123,17 @@ export const AdvancedSettingsForm: FC = ({ }); }, [settings, reset]); - const onSubmit = async (values: FormValues) => { - try { - const retentionNum = parseFloat(values.retention); - const dataRetention = `${Math.round(retentionNum * SECONDS_IN_DAY)}s`; - const sttCheckIntervals = { - rareInterval: `${convertHoursStringToSeconds(values.rareInterval)}s`, - standardInterval: `${convertHoursStringToSeconds(values.standardInterval)}s`, - frequentInterval: `${convertHoursStringToSeconds(values.frequentInterval)}s`, - }; + const onSubmit = async (values: AdvancedSettingsFormValues) => { + const retentionNum = parseFloat(values.retention); + const dataRetention = `${Math.round(retentionNum * SECONDS_IN_DAY)}s`; + const sttCheckIntervals = { + rareInterval: `${convertHoursStringToSeconds(values.rareInterval)}s`, + standardInterval: `${convertHoursStringToSeconds(values.standardInterval)}s`, + frequentInterval: `${convertHoursStringToSeconds(values.frequentInterval)}s`, + }; - await updateSettings({ + await updateSettings( + { dataRetention, pmmPublicAddress: values.publicAddress, enableTelemetry: values.telemetry, @@ -163,15 +145,20 @@ export const AdvancedSettingsForm: FC = ({ advisorRunIntervals: values.stt ? sttCheckIntervals : undefined, enableAzurediscover: values.azureDiscover, enableAccessControl: values.accessControl, - }); - enqueueSnackbar(Messages.service.success, { variant: 'success' }); - reset(values); - } catch (error) { - enqueueSnackbar( - error instanceof Error ? error.message : Messages.unauthorized, - { variant: 'error' } - ); - } + }, + { + onSuccess: () => { + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset(values); + }, + onError: (error) => { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + }, + } + ); }; const { action } = Messages.advanced; @@ -217,7 +204,9 @@ export const AdvancedSettingsForm: FC = ({ type="number" error={!!fieldState.error} helperText={fieldState.error?.message} - inputProps={{ min: MIN_DAYS, max: MAX_DAYS, step: 1 }} + slotProps={{ + htmlInput: { min: MIN_DAYS, max: MAX_DAYS, step: 1 }, + }} sx={{ width: 120 }} /> )} @@ -376,7 +365,9 @@ export const AdvancedSettingsForm: FC = ({ disabled={!sttEnabled} error={!!fieldState.error} helperText={fieldState.error?.message} - inputProps={{ min: MIN_STT_CHECK_INTERVAL, step: 0.1 }} + slotProps={{ + htmlInput: { min: MIN_STT_CHECK_INTERVAL, step: 0.1 }, + }} sx={{ width: 100 }} /> diff --git a/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.types.ts b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.types.ts new file mode 100644 index 00000000000..404f0b5cac9 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/advanced/AdvancedSettingsForm.types.ts @@ -0,0 +1,27 @@ +import { Settings } from 'types/settings.types'; + +export interface AdvancedSettingsFormProps { + settings: Settings; +} + +export interface AdvancedSettingsFormValues { + retention: string; + telemetry: boolean; + updates: boolean; + alerting: boolean; + backup: boolean; + enableInternalPgQan: boolean; + publicAddress: string; + stt: boolean; + rareInterval: string; + standardInterval: string; + frequentInterval: string; + azureDiscover: boolean; + accessControl: boolean; +} + +export interface LabelWithTooltipProps { + label: string; + tooltip: React.ReactNode; + link?: string; +} diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts index 3c9528ec2b9..0103b66f7f0 100644 --- a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.constants.ts @@ -1,7 +1,12 @@ import { Messages } from '../../Settings.messages'; import { MetricsResolutions } from 'types/settings.types'; -export const RESOLUTION_PRESETS = ['rare', 'standard', 'frequent', 'custom'] as const; +export const RESOLUTION_PRESETS = [ + 'rare', + 'standard', + 'frequent', + 'custom', +] as const; export type ResolutionPreset = (typeof RESOLUTION_PRESETS)[number]; export const resolutionOptions: { value: ResolutionPreset; label: string }[] = [ diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts index c013617d68f..2e34af0141f 100644 --- a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolution.utils.ts @@ -7,13 +7,19 @@ import { const replaceS = (r: string) => r.replace(/s$/, ''); -export const removeUnits = (r: MetricsResolutions): { lr: string; mr: string; hr: string } => ({ +export const removeUnits = ( + r: MetricsResolutions +): { lr: string; mr: string; hr: string } => ({ lr: replaceS(r.lr), mr: replaceS(r.mr), hr: replaceS(r.hr), }); -export const addUnits = (r: { lr: string; mr: string; hr: string }): MetricsResolutions => ({ +export const addUnits = (r: { + lr: string; + mr: string; + hr: string; +}): MetricsResolutions => ({ lr: `${r.lr}s`, mr: `${r.mr}s`, hr: `${r.hr}s`, diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx index 5a9c084040e..32c2b2ab7b1 100644 --- a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.tsx @@ -1,22 +1,20 @@ -import { - Button, - FormControl, - FormControlLabel, - IconButton, - Link, - Radio, - RadioGroup, - Stack, - TextField, - Tooltip, - Typography, -} from '@mui/material'; +import Button from '@mui/material/Button'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import IconButton from '@mui/material/IconButton'; +import Link from '@mui/material/Link'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { FC, useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { enqueueSnackbar } from 'notistack'; import { useUpdateSettings } from 'hooks/api/useSettings'; -import { MetricsResolutions, Settings } from 'types/settings.types'; +import { MetricsResolutions } from 'types/settings.types'; import { Messages } from '../../Settings.messages'; import { defaultResolutions, @@ -29,17 +27,10 @@ import { getResolutionPreset, removeUnits, } from './MetricsResolution.utils'; - -interface MetricsResolutionFormProps { - settings: Settings; -} - -interface FormValues { - preset: 'rare' | 'standard' | 'frequent' | 'custom'; - lr: string; - mr: string; - hr: string; -} +import { + MetricsResolutionFormProps, + MetricsResolutionFormValues, +} from './MetricsResolutionForm.types'; const DEFAULT_METRICS = { hr: '5s', mr: '10s', lr: '60s' } as const; @@ -61,7 +52,7 @@ export const MetricsResolutionForm: FC = ({ watch, setValue, formState: { isDirty, errors }, - } = useForm({ + } = useForm({ defaultValues: { preset, lr: raw.lr, @@ -91,27 +82,32 @@ export const MetricsResolutionForm: FC = ({ } }, [currentPreset, setValue]); - const onSubmit = async (values: FormValues) => { - try { - const payload: MetricsResolutions = addUnits({ - lr: values.lr, - mr: values.mr, - hr: values.hr, - }); - await updateSettings({ metricsResolutions: payload }); - enqueueSnackbar(Messages.service.success, { variant: 'success' }); - reset({ - preset: values.preset, - lr: values.lr, - mr: values.mr, - hr: values.hr, - }); - } catch (error) { - enqueueSnackbar( - error instanceof Error ? error.message : Messages.unauthorized, - { variant: 'error' } - ); - } + const onSubmit = async (values: MetricsResolutionFormValues) => { + const payload: MetricsResolutions = addUnits({ + lr: values.lr, + mr: values.mr, + hr: values.hr, + }); + await updateSettings( + { metricsResolutions: payload }, + { + onSuccess: () => { + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset({ + preset: values.preset, + lr: values.lr, + mr: values.mr, + hr: values.hr, + }); + }, + onError: (error) => { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + }, + } + ); }; const { label, link, tooltip, action, intervals } = Messages.metrics; @@ -191,7 +187,9 @@ export const MetricsResolutionForm: FC = ({ disabled={currentPreset !== 'custom'} error={!!fieldState.error} helperText={fieldState.error?.message} - inputProps={{ min: RESOLUTION_MIN, max: RESOLUTION_MAX }} + slotProps={{ + htmlInput: { min: RESOLUTION_MIN, max: RESOLUTION_MAX }, + }} data-testid="metrics-resolution-lr" sx={{ minWidth: 120 }} /> @@ -209,7 +207,9 @@ export const MetricsResolutionForm: FC = ({ disabled={currentPreset !== 'custom'} error={!!fieldState.error} helperText={fieldState.error?.message} - inputProps={{ min: RESOLUTION_MIN, max: RESOLUTION_MAX }} + slotProps={{ + htmlInput: { min: RESOLUTION_MIN, max: RESOLUTION_MAX }, + }} data-testid="metrics-resolution-mr" sx={{ minWidth: 120 }} /> @@ -227,7 +227,9 @@ export const MetricsResolutionForm: FC = ({ disabled={currentPreset !== 'custom'} error={!!fieldState.error} helperText={fieldState.error?.message} - inputProps={{ min: RESOLUTION_MIN, max: RESOLUTION_MAX }} + slotProps={{ + htmlInput: { min: RESOLUTION_MIN, max: RESOLUTION_MAX }, + }} data-testid="metrics-resolution-hr" sx={{ minWidth: 120 }} /> diff --git a/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.types.ts b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.types.ts new file mode 100644 index 00000000000..505659e2e20 --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/metrics-resolution/MetricsResolutionForm.types.ts @@ -0,0 +1,12 @@ +import { Settings } from 'types/settings.types'; + +export interface MetricsResolutionFormProps { + settings: Settings; +} + +export interface MetricsResolutionFormValues { + preset: 'rare' | 'standard' | 'frequent' | 'custom'; + lr: string; + mr: string; + hr: string; +} diff --git a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx index b9d5395e6e3..8f723afd4d7 100644 --- a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx +++ b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.tsx @@ -1,27 +1,17 @@ -import { - Button, - IconButton, - Link, - Stack, - TextField, - Tooltip, - Typography, -} from '@mui/material'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { FC, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { enqueueSnackbar } from 'notistack'; import { useUpdateSettings } from 'hooks/api/useSettings'; -import { Settings } from 'types/settings.types'; import { Messages } from '../../Settings.messages'; - -interface SshKeyFormProps { - settings: Settings; -} - -interface FormValues { - sshKey: string; -} +import { SshKeyFormProps, SshKeyFormValues } from './SshKeyForm.types'; export const SshKeyForm: FC = ({ settings }) => { const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); @@ -30,7 +20,7 @@ export const SshKeyForm: FC = ({ settings }) => { handleSubmit, reset, formState: { isDirty }, - } = useForm({ + } = useForm({ defaultValues: { sshKey: settings.sshKey ?? '' }, }); @@ -38,18 +28,22 @@ export const SshKeyForm: FC = ({ settings }) => { reset({ sshKey: settings.sshKey ?? '' }); }, [settings.sshKey, reset]); - const onSubmit = async (values: FormValues) => { - try { - await updateSettings({ sshKey: values.sshKey }); - enqueueSnackbar(Messages.service.success, { variant: 'success' }); - reset({ sshKey: values.sshKey }); - } catch (error) { - enqueueSnackbar( - error instanceof Error ? error.message : Messages.unauthorized, - { variant: 'error' } - ); - } - }; + const onSubmit = async (values: SshKeyFormValues) => + await updateSettings( + { sshKey: values.sshKey }, + { + onSuccess: () => { + enqueueSnackbar(Messages.service.success, { variant: 'success' }); + reset({ sshKey: values.sshKey }); + }, + onError: (error) => { + enqueueSnackbar( + error instanceof Error ? error.message : Messages.unauthorized, + { variant: 'error' } + ); + }, + } + ); const { label, link, tooltip, action } = Messages.ssh; const { tooltipLinkText } = Messages; diff --git a/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.types.ts b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.types.ts new file mode 100644 index 00000000000..fabe2ac150b --- /dev/null +++ b/ui/apps/pmm/src/pages/settings/components/ssh-key/SshKeyForm.types.ts @@ -0,0 +1,9 @@ +import { Settings } from 'types/settings.types'; + +export interface SshKeyFormProps { + settings: Settings; +} + +export interface SshKeyFormValues { + sshKey: string; +} diff --git a/ui/apps/pmm/src/types/settings.types.ts b/ui/apps/pmm/src/types/settings.types.ts index c15e0f7918f..283dffa7b1b 100644 --- a/ui/apps/pmm/src/types/settings.types.ts +++ b/ui/apps/pmm/src/types/settings.types.ts @@ -1,4 +1,3 @@ -// doesn't yet have the complete response export interface ReadonlySettings { updatesEnabled: boolean; telemetryEnabled: boolean; @@ -16,7 +15,7 @@ export interface MetricsResolutions { lr: string; } -export interface AdvisorRunIntervalsSettings { +export interface AdvisorRunIntervals { rareInterval: string; standardInterval: string; frequentInterval: string; @@ -31,9 +30,11 @@ export interface Settings extends ReadonlySettings { metricsResolutions?: MetricsResolutions; dataRetention?: string; sshKey?: string; - advisorRunIntervals?: AdvisorRunIntervalsSettings; + awsPartitions?: string[]; + advisorRunIntervals?: AdvisorRunIntervals; telemetrySummaries?: string[]; enableInternalPgQan?: boolean; + defaultRoleId?: number; } /** Payload for PUT /server/settings - partial updates supported */ @@ -45,16 +46,14 @@ export interface UpdateSettingsPayload { enableTelemetry?: boolean; enableAlerting?: boolean; enableAdvisor?: boolean; - advisorRunIntervals?: { - rareInterval: string; - standardInterval: string; - frequentInterval: string; - }; + advisorRunIntervals?: AdvisorRunIntervals; enableBackupManagement?: boolean; enableAzurediscover?: boolean; enableUpdates?: boolean; enableAccessControl?: boolean; enableInternalPgQan?: boolean; + awsPartitions?: string[]; + updateSnoozeDuration?: string; } export interface FrontendSettings extends GetFrontendSettingsResponse {} @@ -63,6 +62,10 @@ export interface GetSettingsResponse { settings: Settings; } +export interface ChangeSettingsResponse { + settings: Settings; +} + export interface GetFrontendSettingsResponse { anonymousEnabled: boolean; appSubUrl: string; diff --git a/ui/apps/pmm/vitest.config.ts b/ui/apps/pmm/vitest.config.ts index d889ac4e21e..79b4d442374 100644 --- a/ui/apps/pmm/vitest.config.ts +++ b/ui/apps/pmm/vitest.config.ts @@ -15,16 +15,28 @@ export default defineConfig({ 'react/jsx-runtime', 'react/jsx-dev-runtime', '@emotion/react', - '@emotion/styled' + '@emotion/styled', ], alias: { // Force React to resolve from main project's node_modules - 'react': path.resolve(__dirname, '../../node_modules/react'), + react: path.resolve(__dirname, '../../node_modules/react'), 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom'), - 'react/jsx-runtime': path.resolve(__dirname, '../../node_modules/react/jsx-runtime'), - 'react/jsx-dev-runtime': path.resolve(__dirname, '../../node_modules/react/jsx-dev-runtime'), - '@emotion/react': path.resolve(__dirname, '../../node_modules/@emotion/react'), - '@emotion/styled': path.resolve(__dirname, '../../node_modules/@emotion/styled') + 'react/jsx-runtime': path.resolve( + __dirname, + '../../node_modules/react/jsx-runtime' + ), + 'react/jsx-dev-runtime': path.resolve( + __dirname, + '../../node_modules/react/jsx-dev-runtime' + ), + '@emotion/react': path.resolve( + __dirname, + '../../node_modules/@emotion/react' + ), + '@emotion/styled': path.resolve( + __dirname, + '../../node_modules/@emotion/styled' + ), }, }, optimizeDeps: { From 3b60e9a14101ab0e458dcb7b8fdae4dfa9fed9d1 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Thu, 2 Apr 2026 15:45:44 +0200 Subject: [PATCH 11/52] PMM-14930 Send updated settings event --- ui/apps/pmm-compat/src/compat.ts | 5 +++++ ui/apps/pmm/src/hooks/api/useSettings.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/ui/apps/pmm-compat/src/compat.ts b/ui/apps/pmm-compat/src/compat.ts index 4400d45ae4d..9042cc64379 100644 --- a/ui/apps/pmm-compat/src/compat.ts +++ b/ui/apps/pmm-compat/src/compat.ts @@ -157,6 +157,11 @@ export const initialize = () => { }, }); + messenger.addListener({ + type: 'SETTINGS_CHANGED', + onMessage: () => getAppEvents().publish(new SettingsUpdatedEvent()), + }); + getAppEvents().subscribe(SettingsUpdatedEvent, () => { messenger.sendMessage({ type: 'SETTINGS_CHANGED', diff --git a/ui/apps/pmm/src/hooks/api/useSettings.ts b/ui/apps/pmm/src/hooks/api/useSettings.ts index 3787aa553d0..330a8eeac5f 100644 --- a/ui/apps/pmm/src/hooks/api/useSettings.ts +++ b/ui/apps/pmm/src/hooks/api/useSettings.ts @@ -11,6 +11,7 @@ import { getSettings, updateSettings, } from 'api/settings'; +import messenger from 'lib/messenger'; import { FrontendSettings, Settings, @@ -54,6 +55,9 @@ export const useUpdateSettings = ( ...options, onSuccess: (data, variables, onMutate, context) => { void queryClient.invalidateQueries({ queryKey: SETTINGS_QUERY_KEY }); + messenger.sendMessage({ + type: 'SETTINGS_CHANGED', + }); options?.onSuccess?.(data, variables, onMutate, context); }, }); From a4f770c10e0e0ca55eb8be1101345c7d5799fc66 Mon Sep 17 00:00:00 2001 From: freenandes Date: Thu, 2 Apr 2026 15:12:56 +0100 Subject: [PATCH 12/52] Enhanced layout behaviors to make settings better - Updated esbuild packages in yarn.lock from version 0.21.5 to 0.25.12. - Added scrollbar stability in GlobalStyles for improved UI experience. - Refactored Page component layout to ensure proper flex behavior and added a Divider for better separation of the footer. - Enhanced Sidebar component for better usability on narrow screens, so now it overlaps, instead of pushing, to open it in smaller screens. --- ui/.cursor/rules/percona_ui-styling.mdc | 50 +++++++++++++++++++ ui/apps/pmm/src/Providers.tsx | 3 ++ ui/apps/pmm/src/components/page/Page.tsx | 41 ++++++++------- .../pmm/src/components/sidebar/Sidebar.tsx | 14 +++++- .../src/components/sidebar/drawer/Drawer.tsx | 11 ++++ ui/apps/pmm/src/pages/settings/Settings.tsx | 6 ++- ui/yarn.lock | 33 ++++++++++-- 7 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 ui/.cursor/rules/percona_ui-styling.mdc diff --git a/ui/.cursor/rules/percona_ui-styling.mdc b/ui/.cursor/rules/percona_ui-styling.mdc new file mode 100644 index 00000000000..37313d4dbc0 --- /dev/null +++ b/ui/.cursor/rules/percona_ui-styling.mdc @@ -0,0 +1,50 @@ +--- +description: "Percona UI + PMM UI styling — MUI + @percona/percona-ui, theme tokens, layout conventions, what not to do" +alwaysApply: true +--- + +## Stack order + +1. **MUI** (mostly `@mui/material`, `@mui/icons-material`, `@mui/x-date-pickers` — though other MUI dependencies may also be in use) is the base component and styling API. Use MUI primitives for layout and composition. +2. **`@percona/percona-ui`** is the Percona design layer on top of MUI (theme, branded components, tables, dialogs). Prefer exports from `@percona/percona-ui` when they wrap or standardize behavior (e.g. `Table`, `Dialog`, `ThemeContextProvider`, `pmmThemeOptions`, `NotistackMuiSnackbar`, `primitives`). + +## Do not introduce + +- Do not introduce other CSS/UI frameworks (Tailwind, Bootstrap, styled-components, etc.) or ad-hoc global CSS that fights the theme. +- Do not introduce hard-coded colors, font families, or spacing that bypass the theme. Prefer `sx` with `theme.palette`, `theme.spacing`, breakpoints, and MUI `Typography` variants. +- Do not introduce custom component libraries outside MUI + `@percona/percona-ui` unless explicitly requested. + +## Theming and color mode + +- The app root uses `ThemeContextProvider` with `pmmThemeOptions` from `@percona/percona-ui` — do not replace with a separate `ThemeProvider` or duplicate theme objects in feature code. +- Use `ColorModeContext` / existing hooks (e.g. `useColorMode` in `hooks/theme.ts`) for light/dark; avoid direct `document` or `localStorage` theme hacks. + +## Layout and PMM consistency + +- Follow established layout patterns: MUI `Stack`, `Box`, `Grid`, page shells like `components/page/Page.tsx` (max width, padding, `gap` aligned with existing `sx`). +- Avoid one-off page structures that break alignment with the rest of PMM (arbitrary full-viewport hacks, inconsistent gutters). + +## Styling mechanics + +- Prefer **`sx`** and **`useTheme()`** from `@mui/material/styles` for component-level styling. +- For icons, use **`@mui/icons-material`** (Outlined variants where the codebase already does). + +## When something is missing + +- If a token or component behavior should be shared across products, explain it to them, plan a change, present it as a proposal to extend **`@percona/percona-ui`** (theme / components) rather than embedding one-off design in PMM only. +- If unsure whether a primitive exists in `percona-ui`, check that package or Storybook before inventing a parallel implementation in PMM. + +## Examples + +```tsx +// Good — MUI + theme tokens + + +// Bad — unrelated styling stack +
+ +// Good — branded table from design system +import { Table } from '@percona/percona-ui'; + +// Bad — pulling in another data-table library for the same job +``` diff --git a/ui/apps/pmm/src/Providers.tsx b/ui/apps/pmm/src/Providers.tsx index 2750979fc6a..073c25c4e18 100644 --- a/ui/apps/pmm/src/Providers.tsx +++ b/ui/apps/pmm/src/Providers.tsx @@ -26,6 +26,9 @@ const Providers: FC = () => ( = ({ = ({ > {topBar} {!!title && {title}} - {user?.isAuthorized ? ( - children - ) : ( - - - {Messages.noAcccess} - - - - {Messages.goBack} - {Messages.home} - - - - )} + + {user?.isAuthorized ? ( + children + ) : ( + + + {Messages.noAcccess} + + + + {Messages.goBack} + {Messages.home} + + + + )} + + {footer !== undefined ? footer :