From 0403a89a019cf3f34b49aea94001160271774a1f Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Fri, 7 Aug 2026 15:26:05 +0200 Subject: [PATCH 1/4] PMM-15302 Remove ssh settings tab in HA mode --- .../pmm/src/pages/settings/Settings.test.tsx | 17 ++++++++++++++++- ui/apps/pmm/src/pages/settings/Settings.tsx | 13 ++++++++----- .../settings/components/ssh-key/SshKeyForm.tsx | 7 +++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/ui/apps/pmm/src/pages/settings/Settings.test.tsx b/ui/apps/pmm/src/pages/settings/Settings.test.tsx index 32b5a2b836c..8438ca5fd71 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.test.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.test.tsx @@ -4,9 +4,14 @@ import { Settings } from './Settings'; import { TestWrapper } from 'utils/testWrapper'; import { wrapWithQueryProvider } from 'utils/testUtils'; import * as settingsApi from 'api/settings'; +import * as haApi from 'api/ha'; import type { Settings as SettingsType } from 'types/settings.types'; vi.mock('api/settings'); +vi.mock('api/ha', () => ({ + getHAStatus: vi.fn(), + getHANodes: vi.fn(), +})); vi.mock('./components/metrics-resolution/MetricsResolutionForm', () => ({ MetricsResolutionForm: () => null, })); @@ -18,6 +23,7 @@ vi.mock('./components/ssh-key/SshKeyForm', () => ({ })); const getSettingsMock = vi.mocked(settingsApi.getSettings); +const getHAStatusMock = vi.mocked(haApi.getHAStatus); const mockSettings = {} as SettingsType; const renderWithRoute = (initialPath: string) => @@ -33,7 +39,8 @@ const renderWithRoute = (initialPath: string) => describe('Settings', () => { beforeEach(() => { - getSettingsMock.mockImplementation(() => new Promise(() => {})); + getSettingsMock.mockImplementation(() => new Promise(() => { })); + getHAStatusMock.mockResolvedValue({ status: 'Disabled' }); }); it('shows loading state when settings are not yet loaded', () => { @@ -86,5 +93,13 @@ describe('Settings', () => { ) ); }); + + it('hides ssh tab when HA is enabled', async () => { + getHAStatusMock.mockResolvedValue({ status: 'Enabled' }); + renderWithRoute('/settings/metrics-resolution'); + await waitFor(() => + expect(screen.queryByTestId('settings-tab-ssh')).not.toBeInTheDocument() + ); + }); }); }); diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index 2632aba112f..f851f943c93 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -14,6 +14,7 @@ import { TabValue } from './Settings.types'; import { useNavigate, useParams } from 'react-router-dom'; import { OrgRole } from 'types/user.types'; import { useUser } from 'contexts/user'; +import { useHAStatus } from 'hooks/api/useHA'; export const Settings: FC = () => { const { user } = useUser(); @@ -25,6 +26,7 @@ export const Settings: FC = () => { } = useSettings({ enabled: !!user && user.isPMMAdmin, }); + const { data: haStatus } = useHAStatus(); const navigate = useNavigate(); if (isLoading || (isEnabled && !settings)) { @@ -65,11 +67,12 @@ export const Settings: FC = () => { value="advanced-settings" label={Messages.tabs.advanced} /> - + {haStatus?.status === 'Disabled' && + < Tab + data-testid="settings-tab-ssh" + value="ssh-key" + label={Messages.tabs.ssh} + />} 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 a34152d2263..3aa0bcf9b1a 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 @@ -12,6 +12,8 @@ import { SettingsFieldLabel } from '../settings-field-label'; import { formControlClasses } from '@mui/material'; import { SettingsSubmitButton } from '../settings-submit-button'; import { helperTextTestId } from 'utils/mui.utils'; +import { useHAStatus } from 'hooks/api/useHA'; +import { Navigate } from 'react-router-dom'; export const SshKeyForm: FC = ({ settings }) => { const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); @@ -19,6 +21,7 @@ export const SshKeyForm: FC = ({ settings }) => { resolver: zodResolver(sshKeySchema), defaultValues: { sshKey: settings.sshKey ?? '' }, }); + const { data: haStatus } = useHAStatus(); useEffect(() => { methods.reset({ sshKey: settings.sshKey ?? '' }); @@ -43,6 +46,10 @@ export const SshKeyForm: FC = ({ settings }) => { const { label, link, tooltip, placeholder } = Messages.ssh; + if (haStatus?.status === 'Enabled') { + return + } + return ( From a1df4f90cc280f52780bff679ccb8c4d91cef34f Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Fri, 7 Aug 2026 15:29:51 +0200 Subject: [PATCH 2/4] PMM-15302 Format code --- ui/apps/pmm/src/pages/settings/Settings.test.tsx | 2 +- ui/apps/pmm/src/pages/settings/Settings.tsx | 7 ++++--- .../src/pages/settings/components/ssh-key/SshKeyForm.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/apps/pmm/src/pages/settings/Settings.test.tsx b/ui/apps/pmm/src/pages/settings/Settings.test.tsx index 8438ca5fd71..92e079fec1d 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.test.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.test.tsx @@ -39,7 +39,7 @@ const renderWithRoute = (initialPath: string) => describe('Settings', () => { beforeEach(() => { - getSettingsMock.mockImplementation(() => new Promise(() => { })); + getSettingsMock.mockImplementation(() => new Promise(() => {})); getHAStatusMock.mockResolvedValue({ status: 'Disabled' }); }); diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index f851f943c93..3d347e80739 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -67,12 +67,13 @@ export const Settings: FC = () => { value="advanced-settings" label={Messages.tabs.advanced} /> - {haStatus?.status === 'Disabled' && - < Tab + {haStatus?.status === 'Disabled' && ( + } + /> + )} 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 3aa0bcf9b1a..8725470ffe9 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 @@ -47,7 +47,7 @@ export const SshKeyForm: FC = ({ settings }) => { const { label, link, tooltip, placeholder } = Messages.ssh; if (haStatus?.status === 'Enabled') { - return + return ; } return ( From ba6fa3832dab69f2938c7b438e278e7dcb14c804 Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Mon, 10 Aug 2026 12:34:13 +0200 Subject: [PATCH 3/4] PMM-15302 Handle ssh tab in settings component --- ui/apps/pmm/src/pages/settings/Settings.tsx | 10 +++++++--- .../pages/settings/components/ssh-key/SshKeyForm.tsx | 7 ------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index 3d347e80739..12eeb554fc6 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -11,7 +11,7 @@ import { MetricsResolutionForm } from './components/metrics-resolution/MetricsRe import { AdvancedSettingsForm } from './components/advanced/AdvancedSettingsForm'; import { Messages } from './Settings.messages'; import { TabValue } from './Settings.types'; -import { useNavigate, useParams } from 'react-router-dom'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { OrgRole } from 'types/user.types'; import { useUser } from 'contexts/user'; import { useHAStatus } from 'hooks/api/useHA'; @@ -26,10 +26,10 @@ export const Settings: FC = () => { } = useSettings({ enabled: !!user && user.isPMMAdmin, }); - const { data: haStatus } = useHAStatus(); + const { data: haStatus, isLoading: isHAStatusLoading } = useHAStatus(); const navigate = useNavigate(); - if (isLoading || (isEnabled && !settings)) { + if (isLoading || isHAStatusLoading || (isEnabled && !settings)) { return ( @@ -41,6 +41,10 @@ export const Settings: FC = () => { const setTab = (value: TabValue) => navigate(`/settings/${value}`); + if (haStatus?.status === 'Enabled' && tab === 'ssh-key') { + return ; + } + return ( = ({ settings }) => { const { mutateAsync: updateSettings, isPending } = useUpdateSettings(); @@ -21,7 +19,6 @@ export const SshKeyForm: FC = ({ settings }) => { resolver: zodResolver(sshKeySchema), defaultValues: { sshKey: settings.sshKey ?? '' }, }); - const { data: haStatus } = useHAStatus(); useEffect(() => { methods.reset({ sshKey: settings.sshKey ?? '' }); @@ -46,10 +43,6 @@ export const SshKeyForm: FC = ({ settings }) => { const { label, link, tooltip, placeholder } = Messages.ssh; - if (haStatus?.status === 'Enabled') { - return ; - } - return ( From 73d10c129b4e01a97686172aa02cccd4f92d49ea Mon Sep 17 00:00:00 2001 From: Matej Kubinec Date: Wed, 12 Aug 2026 10:17:22 +0200 Subject: [PATCH 4/4] PMM-15302 Show ssh key tab only on AMI --- managed/services/server/server.go | 4 +- ui/apps/pmm/src/api/__mocks__/settings.ts | 71 +++++++++++++++++++ ui/apps/pmm/src/api/__mocks__/version.ts | 20 ++++++ ui/apps/pmm/src/api/version.ts | 7 ++ ui/apps/pmm/src/hooks/api/useVersion.ts | 12 ++++ .../pmm/src/pages/settings/Settings.test.tsx | 55 +++++++++----- ui/apps/pmm/src/pages/settings/Settings.tsx | 14 ++-- ui/apps/pmm/src/types/version.types.ts | 21 ++++++ 8 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 ui/apps/pmm/src/api/__mocks__/settings.ts create mode 100644 ui/apps/pmm/src/api/__mocks__/version.ts create mode 100644 ui/apps/pmm/src/api/version.ts create mode 100644 ui/apps/pmm/src/hooks/api/useVersion.ts create mode 100644 ui/apps/pmm/src/types/version.types.ts diff --git a/managed/services/server/server.go b/managed/services/server/server.go index 199ee2bfed9..eb06406a7a4 100644 --- a/managed/services/server/server.go +++ b/managed/services/server/server.go @@ -698,8 +698,8 @@ func (s *Server) writeSSHKey(sshKey string) error { defer s.sshKeyM.Unlock() distributionMethod := s.telemetryService.DistributionMethod() - if distributionMethod != serverv1.DistributionMethod_DISTRIBUTION_METHOD_AMI && distributionMethod != serverv1.DistributionMethod_DISTRIBUTION_METHOD_OVF { - return errors.New("SSH key can be set only on AMI and OVF distributions") + if distributionMethod != serverv1.DistributionMethod_DISTRIBUTION_METHOD_AMI { + return errors.New("SSH key can be set only on AMI distribution") } username := "pmm" diff --git a/ui/apps/pmm/src/api/__mocks__/settings.ts b/ui/apps/pmm/src/api/__mocks__/settings.ts new file mode 100644 index 00000000000..b4e58c16f3e --- /dev/null +++ b/ui/apps/pmm/src/api/__mocks__/settings.ts @@ -0,0 +1,71 @@ +import type { AxiosRequestConfig } from 'axios'; +import { + FrontendSettings, + ReadonlySettings, + Settings, + UpdateSettingsPayload, +} from 'types/settings.types'; + +export const READONLY_SETTINGS_MOCK: ReadonlySettings = { + updatesEnabled: true, + telemetryEnabled: false, + advisorEnabled: true, + alertingEnabled: true, + pmmPublicAddress: '', + backupManagementEnabled: true, + azurediscoverEnabled: false, + enableAccessControl: false, +}; + +export const SETTINGS_MOCK: Settings = { + ...READONLY_SETTINGS_MOCK, + metricsResolutions: { + hr: '5s', + mr: '10s', + lr: '60s', + }, + dataRetention: '2592000s', + awsPartitions: ['aws'], + advisorRunIntervals: { + rareInterval: '280800s', + standardInterval: '86400s', + frequentInterval: '14400s', + }, + enableInternalPgQan: false, + defaultRoleId: 1, +}; + +export const FRONTEND_SETTINGS_MOCK: FrontendSettings = { + anonymousEnabled: false, + appSubUrl: '', + apps: {}, + buildInfo: { + version: '', + versionString: '', + }, + exploreEnabled: true, + featureToggles: { + exploreMetrics: true, + }, + unifiedAlertingEnabled: true, + disableLoginForm: false, + auth: { + disableLogin: false, + }, +}; + +export const getSettings = vi.fn< + (config?: AxiosRequestConfig) => Promise +>(async () => SETTINGS_MOCK); + +export const getReadonlySettings = vi.fn( + async (): Promise => READONLY_SETTINGS_MOCK +); + +export const getFrontendSettings = vi.fn( + async (): Promise => FRONTEND_SETTINGS_MOCK +); + +export const updateSettings = vi.fn< + (payload: UpdateSettingsPayload) => Promise +>(async () => SETTINGS_MOCK); diff --git a/ui/apps/pmm/src/api/__mocks__/version.ts b/ui/apps/pmm/src/api/__mocks__/version.ts new file mode 100644 index 00000000000..cfafe1172e3 --- /dev/null +++ b/ui/apps/pmm/src/api/__mocks__/version.ts @@ -0,0 +1,20 @@ +import { DistributionMethod, VersionResponse } from 'types/version.types'; +import { vi } from 'vitest'; +export const VERSION_MOCK: VersionResponse = { + version: '0.0.0', + server: { + version: '0.0.0', + fullVersion: '0.0.0-00', + timestamp: '2026-01-01T00:00:00Z', + }, + managed: { + version: '0.0.0', + fullVersion: '2cccd1107b56ff924b34dbe77ebaad2d021c30ea', + timestamp: '2026-01-01T00:00:00Z', + }, + distributionMethod: DistributionMethod.unspecified, +}; + +export const getVersion = vi.fn( + async (): Promise => Promise.resolve(VERSION_MOCK) +); diff --git a/ui/apps/pmm/src/api/version.ts b/ui/apps/pmm/src/api/version.ts new file mode 100644 index 00000000000..21229c81b12 --- /dev/null +++ b/ui/apps/pmm/src/api/version.ts @@ -0,0 +1,7 @@ +import { VersionResponse } from 'types/version.types'; +import { api } from './api'; + +export const getVersion = async (): Promise => { + const res = await api.get('/server/version'); + return res.data; +}; diff --git a/ui/apps/pmm/src/hooks/api/useVersion.ts b/ui/apps/pmm/src/hooks/api/useVersion.ts new file mode 100644 index 00000000000..5d555ee659c --- /dev/null +++ b/ui/apps/pmm/src/hooks/api/useVersion.ts @@ -0,0 +1,12 @@ +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; +import { getVersion } from 'api/version'; +import { VersionResponse } from 'types/version.types'; + +export const useVersion = ( + options?: Partial> +) => + useQuery({ + queryKey: ['server:version'], + queryFn: () => getVersion(), + ...options, + }); diff --git a/ui/apps/pmm/src/pages/settings/Settings.test.tsx b/ui/apps/pmm/src/pages/settings/Settings.test.tsx index 92e079fec1d..1db8ffddc7f 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.test.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.test.tsx @@ -4,14 +4,13 @@ import { Settings } from './Settings'; import { TestWrapper } from 'utils/testWrapper'; import { wrapWithQueryProvider } from 'utils/testUtils'; import * as settingsApi from 'api/settings'; -import * as haApi from 'api/ha'; -import type { Settings as SettingsType } from 'types/settings.types'; +import * as versionApi from 'api/version'; +import { SETTINGS_MOCK } from 'api/__mocks__/settings'; +import { VERSION_MOCK } from 'api/__mocks__/version'; +import { DistributionMethod } from 'types/version.types'; vi.mock('api/settings'); -vi.mock('api/ha', () => ({ - getHAStatus: vi.fn(), - getHANodes: vi.fn(), -})); +vi.mock('api/version'); vi.mock('./components/metrics-resolution/MetricsResolutionForm', () => ({ MetricsResolutionForm: () => null, })); @@ -23,8 +22,7 @@ vi.mock('./components/ssh-key/SshKeyForm', () => ({ })); const getSettingsMock = vi.mocked(settingsApi.getSettings); -const getHAStatusMock = vi.mocked(haApi.getHAStatus); -const mockSettings = {} as SettingsType; +const getVersionMock = vi.mocked(versionApi.getVersion); const renderWithRoute = (initialPath: string) => render( @@ -39,19 +37,25 @@ const renderWithRoute = (initialPath: string) => describe('Settings', () => { beforeEach(() => { - getSettingsMock.mockImplementation(() => new Promise(() => {})); - getHAStatusMock.mockResolvedValue({ status: 'Disabled' }); + getSettingsMock.mockResolvedValue(SETTINGS_MOCK); + getVersionMock.mockResolvedValue(VERSION_MOCK); }); it('shows loading state when settings are not yet loaded', () => { + getSettingsMock.mockImplementation(() => new Promise(() => {})); + render({wrapWithQueryProvider()}); expect(screen.getByTestId('settings-loading')).toBeInTheDocument(); }); describe('tab navigation by URL', () => { - beforeEach(() => { - getSettingsMock.mockResolvedValue(mockSettings); + it('ssh tab is not shown when distribution type is not AMI', async () => { + renderWithRoute('/settings/metrics-resolution'); + + await screen.findByTestId('settings-tab-metrics'); + + expect(screen.queryByTestId('settings-tab-ssh')).not.toBeInTheDocument(); }); it('activates metrics tab for /settings/metrics', async () => { @@ -74,7 +78,12 @@ describe('Settings', () => { ); }); - it('activates ssh tab for /settings/ssh', async () => { + it('activates ssh tab for /settings/ssh when distributed as AMI', async () => { + getVersionMock.mockResolvedValueOnce({ + ...VERSION_MOCK, + distributionMethod: DistributionMethod.ami, + }); + renderWithRoute('/settings/ssh-key'); await waitFor(() => expect(screen.getByTestId('settings-tab-ssh')).toHaveAttribute( @@ -84,6 +93,16 @@ describe('Settings', () => { ); }); + it('redirects from ssh tab for /settings/ssh to default', async () => { + renderWithRoute('/settings/ssh-key'); + await waitFor(() => + expect(screen.getByTestId('settings-tab-metrics')).toHaveAttribute( + 'aria-selected', + 'true' + ) + ); + }); + it('defaults to metrics tab when no tab is in the URL', async () => { renderWithRoute('/settings'); await waitFor(() => @@ -94,11 +113,15 @@ describe('Settings', () => { ); }); - it('hides ssh tab when HA is enabled', async () => { - getHAStatusMock.mockResolvedValue({ status: 'Enabled' }); + it('shows ssh tab when distribution type is AMI', async () => { + getVersionMock.mockResolvedValueOnce({ + ...VERSION_MOCK, + distributionMethod: DistributionMethod.ami, + }); + renderWithRoute('/settings/metrics-resolution'); await waitFor(() => - expect(screen.queryByTestId('settings-tab-ssh')).not.toBeInTheDocument() + expect(screen.queryByTestId('settings-tab-ssh')).toBeInTheDocument() ); }); }); diff --git a/ui/apps/pmm/src/pages/settings/Settings.tsx b/ui/apps/pmm/src/pages/settings/Settings.tsx index 12eeb554fc6..12ee0ac3fe0 100644 --- a/ui/apps/pmm/src/pages/settings/Settings.tsx +++ b/ui/apps/pmm/src/pages/settings/Settings.tsx @@ -14,7 +14,8 @@ import { TabValue } from './Settings.types'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; import { OrgRole } from 'types/user.types'; import { useUser } from 'contexts/user'; -import { useHAStatus } from 'hooks/api/useHA'; +import { useVersion } from 'hooks/api/useVersion'; +import { DistributionMethod } from 'types/version.types'; export const Settings: FC = () => { const { user } = useUser(); @@ -26,10 +27,13 @@ export const Settings: FC = () => { } = useSettings({ enabled: !!user && user.isPMMAdmin, }); - const { data: haStatus, isLoading: isHAStatusLoading } = useHAStatus(); + const { data: version, isLoading: isVersionLoading } = useVersion({ + enabled: !!user && user.isPMMAdmin, + }); const navigate = useNavigate(); + const showSshKeyTab = version?.distributionMethod === DistributionMethod.ami; - if (isLoading || isHAStatusLoading || (isEnabled && !settings)) { + if (isLoading || isVersionLoading || (isEnabled && !settings)) { return ( @@ -41,7 +45,7 @@ export const Settings: FC = () => { const setTab = (value: TabValue) => navigate(`/settings/${value}`); - if (haStatus?.status === 'Enabled' && tab === 'ssh-key') { + if (!showSshKeyTab && tab === 'ssh-key') { return ; } @@ -71,7 +75,7 @@ export const Settings: FC = () => { value="advanced-settings" label={Messages.tabs.advanced} /> - {haStatus?.status === 'Disabled' && ( + {showSshKeyTab && (