diff --git a/public/app/percona/inventory/Inventory.service.ts b/public/app/percona/inventory/Inventory.service.ts index 22f0a061ed169..fd3b26d124add 100644 --- a/public/app/percona/inventory/Inventory.service.ts +++ b/public/app/percona/inventory/Inventory.service.ts @@ -11,6 +11,7 @@ import { NodeListDBPayload, RemoveNodeBody, ServiceAgentListPayload, + UpdateAgentBody, } from './Inventory.types'; const BASE_URL = `/v1/inventory`; @@ -25,6 +26,9 @@ export const InventoryService = { }, }); }, + updateAgent(agentId: string, payload: UpdateAgentBody, token?: CancelToken) { + return api.put(`${BASE_URL}/agents/${agentId}`, payload, false, token); + }, removeAgent(agentId: string, forceMode = false, token?: CancelToken) { // todo: address forceMode return api.delete(`${BASE_URL}/agents/${agentId}`, false, token, { force: forceMode }); diff --git a/public/app/percona/inventory/Inventory.types.ts b/public/app/percona/inventory/Inventory.types.ts index 1e7c41350b410..51f4a8b0d8631 100644 --- a/public/app/percona/inventory/Inventory.types.ts +++ b/public/app/percona/inventory/Inventory.types.ts @@ -1,3 +1,4 @@ +import { MetricsResolutions } from '../settings/Settings.types'; import { Databases } from '../shared/core'; import { DbNode, NodeType } from '../shared/services/nodes/Nodes.types'; import { @@ -188,3 +189,30 @@ export interface AgentsOption { value: string; label: string; } + +export interface UpdateAgentItem { + enable?: boolean; + custom_labels?: Record; + enable_push_metrics?: boolean; + metrics_resolutions?: MetricsResolutions; +} + +export interface UpdateAgentBody { + node_exporter?: UpdateAgentItem; + mysqld_exporter?: UpdateAgentItem; + mongodb_exporter?: UpdateAgentItem; + postgres_exporter?: UpdateAgentItem; + proxysql_exporter?: UpdateAgentItem; + external_exporter?: UpdateAgentItem; + rds_exporter?: UpdateAgentItem; + azure_database_exporter?: UpdateAgentItem; + qan_mysql_perfschema_agent?: UpdateAgentItem; + qan_mysql_slowlog_agent?: UpdateAgentItem; + qan_mongodb_profiler_agent?: UpdateAgentItem; + qan_mongodb_mongolog_agent?: UpdateAgentItem; + qan_postgresql_pgstatements_agent?: UpdateAgentItem; + qan_postgresql_pgstatmonitor_agent?: UpdateAgentItem; + nomad_agent?: { + enable?: boolean; + }; +} diff --git a/public/app/percona/settings/Settings.messages.ts b/public/app/percona/settings/Settings.messages.ts index 9ec8d27ed2ee0..77c04d0f6ed08 100644 --- a/public/app/percona/settings/Settings.messages.ts +++ b/public/app/percona/settings/Settings.messages.ts @@ -38,6 +38,10 @@ export const Messages = { 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\n' + diff --git a/public/app/percona/settings/Settings.service.ts b/public/app/percona/settings/Settings.service.ts index 4df4d5dec0585..a999e48e14e43 100644 --- a/public/app/percona/settings/Settings.service.ts +++ b/public/app/percona/settings/Settings.service.ts @@ -78,6 +78,7 @@ const toModel = (response: SettingsPayload): Settings => ({ isConnectedToPortal: response.connected_to_platform, defaultRoleId: response.default_role_id, enableAccessControl: response.enable_access_control, + enableInternalPgQan: response.enable_internal_pg_qan, }); const toReadonlyModel = (response: ReadonlySettingsPayload): Settings => ({ @@ -116,4 +117,5 @@ const toReadonlyModel = (response: ReadonlySettingsPayload): Settings => ({ isConnectedToPortal: false, defaultRoleId: -1, enableAccessControl: response.enable_access_control, + enableInternalPgQan: false, }); diff --git a/public/app/percona/settings/Settings.types.ts b/public/app/percona/settings/Settings.types.ts index b1556bf5e90cb..e749c6c198fb4 100644 --- a/public/app/percona/settings/Settings.types.ts +++ b/public/app/percona/settings/Settings.types.ts @@ -63,6 +63,7 @@ export interface AdvancedChangePayload extends AdvancedPayload { enable_azurediscover?: boolean; enable_updates?: boolean; enable_access_control?: boolean; + enable_internal_pg_qan?: boolean; } export interface MetricsResolutionsPayload { @@ -122,6 +123,7 @@ export interface SettingsPayload telemetry_summaries: string[]; default_role_id: number; enable_access_control: boolean; + enable_internal_pg_qan: boolean; } export interface SettingsPayload @@ -180,6 +182,7 @@ export interface Settings extends ReadonlySettings { isConnectedToPortal?: boolean; telemetrySummaries: string[]; defaultRoleId: number; + enableInternalPgQan: boolean; } export interface MetricsResolutions { diff --git a/public/app/percona/settings/__mocks__/Settings.service.ts b/public/app/percona/settings/__mocks__/Settings.service.ts index 7deef0502a503..ac7412a5a50c4 100644 --- a/public/app/percona/settings/__mocks__/Settings.service.ts +++ b/public/app/percona/settings/__mocks__/Settings.service.ts @@ -37,6 +37,7 @@ export const stub: Settings = { frequentInterval: '10s', }, defaultRoleId: 1, + enableInternalPgQan: false, }; SettingsService.getSettings = () => Promise.resolve(stub); diff --git a/public/app/percona/settings/components/Advanced/Advanced.test.tsx b/public/app/percona/settings/components/Advanced/Advanced.test.tsx index d1a2775103289..0a95152f23512 100644 --- a/public/app/percona/settings/components/Advanced/Advanced.test.tsx +++ b/public/app/percona/settings/components/Advanced/Advanced.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; +import { render, screen, fireEvent, waitForElementToBeRemoved, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import * as reducers from 'app/percona/shared/core/reducers'; @@ -10,8 +10,48 @@ import { Advanced } from './Advanced'; jest.mock('app/percona/settings/Settings.service'); +const updateSettingsSpy = jest.spyOn(reducers, 'updateSettingsAction'); + +const setup = (pmmMonitoringEnabled = true) => + render( + + {wrapWithGrafanaContextMock()} + + ); + describe('Advanced::', () => { - it('Renders correctly with props', () => { + beforeEach(() => { + updateSettingsSpy.mockClear(); + }); + + it('renders correctly with props', async () => { render( { }, }, }, - } as StoreState)} + } as unknown as StoreState)} > {wrapWithGrafanaContextMock()} ); - expect(screen.getByTestId('retention-number-input')).toHaveValue(30); - expect(screen.getByTestId('publicAddress-text-input')).toHaveValue('localhost'); + await waitFor(() => expect(screen.getByTestId('retention-number-input')).toHaveValue(30)); + await waitFor(() => expect(screen.getByTestId('publicAddress-text-input')).toHaveValue('localhost')); }); - it('Calls apply changes', async () => { - const spy = jest.spyOn(reducers, 'updateSettingsAction'); + it('calls apply changes', async () => { render( { {wrapWithGrafanaContextMock()} ); + fireEvent.change(screen.getByTestId('retention-number-input'), { target: { value: 70 } }); fireEvent.submit(screen.getByTestId('advanced-button')); + await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); - expect(spy).toHaveBeenCalled(); + expect(updateSettingsSpy).toHaveBeenCalled(); }); - it('Sets correct URL from browser', async () => { + it('sets correct URL from browser', async () => { const location = { ...window.location, host: 'pmmtest.percona.com', @@ -129,12 +170,11 @@ describe('Advanced::', () => { ); fireEvent.click(screen.getByTestId('public-address-button')); - expect(screen.getByTestId('publicAddress-text-input')).toHaveValue('pmmtest.percona.com'); - }); - it('Does not include STT check intervals in the change request if STT checks are disabled', async () => { - const spy = jest.spyOn(reducers, 'updateSettingsAction'); + await waitFor(() => expect(screen.getByTestId('publicAddress-text-input')).toHaveValue('pmmtest.percona.com')); + }); + it('does not include STT check intervals in the change request if STT checks are disabled', async () => { render( { fireEvent.change(screen.getByTestId('retention-number-input'), { target: { value: 70 } }); fireEvent.submit(screen.getByTestId('advanced-button')); + await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); - // expect(spy.calls.mostRecent().args[0].body.stt_check_intervals).toBeUndefined(); - expect(spy).toHaveBeenLastCalledWith( + expect(updateSettingsSpy).toHaveBeenLastCalledWith( expect.objectContaining({ body: expect.objectContaining({ advisor_run_intervals: undefined, @@ -181,8 +221,6 @@ describe('Advanced::', () => { }); it('Includes STT check intervals in the change request if STT checks are enabled', async () => { - const spy = jest.spyOn(reducers, 'updateSettingsAction'); - render( { await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); // expect(spy.calls.mostRecent().args[0].body.stt_check_intervals).toBeDefined(); - expect(spy).toHaveBeenLastCalledWith( + expect(updateSettingsSpy).toHaveBeenLastCalledWith( expect.objectContaining({ body: expect.objectContaining({ advisor_run_intervals: { @@ -232,4 +270,68 @@ describe('Advanced::', () => { }) ); }); + + it('updates internal monitoring when pmm server monitoring is turned on', async () => { + const { container } = setup(); + + const monitoringSwitch = container.querySelector( + '[data-testid="enable-internal-pg-qan"] [name="enableInternalPgQan"]' + ); + + expect(monitoringSwitch).toBeInTheDocument(); + + fireEvent.click(monitoringSwitch!); + + fireEvent.submit(screen.getByTestId('advanced-button')); + + await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); + + expect(updateSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + enable_internal_pg_qan: false, + }), + }) + ); + }); + + it('updates internal monitoring when pmm server monitoring is turned off', async () => { + const { container } = setup(false); + + const monitoringSwitch = container.querySelector( + '[data-testid="enable-internal-pg-qan"] [name="enableInternalPgQan"]' + ); + + expect(monitoringSwitch).toBeInTheDocument(); + + fireEvent.click(monitoringSwitch!); + + fireEvent.submit(screen.getByTestId('advanced-button')); + + await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); + + expect(updateSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + enable_internal_pg_qan: true, + }), + }) + ); + }); + + it("doesn't update internal monitoring when pmm server monitoring doesn't change", async () => { + setup(); + + fireEvent.submit(screen.getByTestId('advanced-button')); + + await waitForElementToBeRemoved(() => screen.getByTestId('Spinner')); + + expect(updateSettingsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.objectContaining({ + enable_internal_pg_qan: true, + }), + }) + ); + }); }); diff --git a/public/app/percona/settings/components/Advanced/Advanced.tsx b/public/app/percona/settings/components/Advanced/Advanced.tsx index 071231c1fd589..450fe355b5f1f 100644 --- a/public/app/percona/settings/components/Advanced/Advanced.tsx +++ b/public/app/percona/settings/components/Advanced/Advanced.tsx @@ -35,7 +35,48 @@ import { convertCheckIntervalsToHours, convertHoursStringToSeconds, convertSecon import { SwitchRow } from './SwitchRow'; const { - advanced: { sttCheckIntervalsLabel, sttCheckIntervalTooltip, sttCheckIntervalUnit }, + tooltipLinkText, + advanced: { + action, + retentionLabel, + retentionTooltip, + retentionUnits, + telemetryLabel, + telemetryLink, + telemetryTooltip, + telemetrySummaryTitle, + telemetryDisclaimer, + updatesLabel, + updatesLink, + updatesTooltip, + advisorsLabel, + advisorsLink, + advisorsTooltip, + publicAddressLabel, + publicAddressTooltip, + publicAddressButton, + accessControl, + accessControlTooltip, + accessControlLink, + alertingLabel, + alertingTooltip, + alertingLink, + azureDiscoverLabel, + azureDiscoverTooltip, + azureDiscoverLink, + technicalPreviewLegend, + technicalPreviewDescription, + technicalPreviewLinkText, + backupLabel, + backupLink, + backupTooltip, + enableInternalPgQanLabel, + enableInternalPgQanLink, + enableInternalPgQanTooltip, + sttCheckIntervalsLabel, + sttCheckIntervalTooltip, + sttCheckIntervalUnit, + }, } = Messages; export const Advanced: FC = () => { @@ -55,47 +96,10 @@ export const Advanced: FC = () => { alertingEnabled, telemetrySummaries, enableAccessControl, + enableInternalPgQan, } = settings!; const settingsStyles = useStyles2(getSettingsStyles); const { rareInterval, standardInterval, frequentInterval } = convertCheckIntervalsToHours(sttCheckIntervals); - const { - advanced: { - action, - retentionLabel, - retentionTooltip, - retentionUnits, - telemetryLabel, - telemetryLink, - telemetryTooltip, - telemetrySummaryTitle, - telemetryDisclaimer, - updatesLabel, - updatesLink, - updatesTooltip, - advisorsLabel, - advisorsLink, - advisorsTooltip, - publicAddressLabel, - publicAddressTooltip, - publicAddressButton, - accessControl, - accessControlTooltip, - accessControlLink, - alertingLabel, - alertingTooltip, - alertingLink, - azureDiscoverLabel, - azureDiscoverTooltip, - azureDiscoverLink, - technicalPreviewLegend, - technicalPreviewDescription, - technicalPreviewLinkText, - backupLabel, - backupLink, - backupTooltip, - }, - tooltipLinkText, - } = Messages; const initialValues: AdvancedFormProps = { retention: convertSecondsToDays(dataRetention), @@ -111,6 +115,7 @@ export const Advanced: FC = () => { frequentInterval, telemetrySummaries, accessControl: enableAccessControl, + enableInternalPgQan, }; const [loading, setLoading] = useState(false); @@ -128,6 +133,7 @@ export const Advanced: FC = () => { frequentInterval, updates, accessControl, + enableInternalPgQan, } = values; const sttCheckIntervals = { rare_interval: `${convertHoursStringToSeconds(rareInterval)}s`, @@ -146,6 +152,7 @@ export const Advanced: FC = () => { enable_backup_management: backup, enable_updates: updates, enable_access_control: accessControl, + enable_internal_pg_qan: enableInternalPgQan, }; setLoading(true); @@ -256,6 +263,16 @@ export const Advanced: FC = () => { dataTestId="advanced-backup" component={SwitchRow} /> +
diff --git a/public/app/percona/settings/components/Advanced/Advanced.types.ts b/public/app/percona/settings/components/Advanced/Advanced.types.ts index 7007e91dd9040..5df711ea6e25f 100644 --- a/public/app/percona/settings/components/Advanced/Advanced.types.ts +++ b/public/app/percona/settings/components/Advanced/Advanced.types.ts @@ -12,4 +12,5 @@ export interface AdvancedFormProps { frequentInterval: string; telemetrySummaries: string[]; accessControl?: boolean; + enableInternalPgQan?: boolean; } diff --git a/public/app/percona/shared/core/reducers/index.ts b/public/app/percona/shared/core/reducers/index.ts index b88b28dc82004..cb3673500648f 100644 --- a/public/app/percona/shared/core/reducers/index.ts +++ b/public/app/percona/shared/core/reducers/index.ts @@ -63,6 +63,7 @@ const initialSettingsState: Settings = { isConnectedToPortal: false, defaultRoleId: 1, enableAccessControl: false, + enableInternalPgQan: false, }; export const fetchSettingsAction = createAsyncThunk( diff --git a/public/app/percona/shared/services/services/__mocks__/Services.service.ts b/public/app/percona/shared/services/services/__mocks__/Services.service.ts index de3e38be28ddd..89c268b241d30 100644 --- a/public/app/percona/shared/services/services/__mocks__/Services.service.ts +++ b/public/app/percona/shared/services/services/__mocks__/Services.service.ts @@ -9,4 +9,9 @@ ServicesService.getActive = () => service_types: [], }); +ServicesService.getServices = () => + Promise.resolve({ + services: [], + }); + ServicesService.removeService = () => Promise.resolve({});