Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pmm-app/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,6 @@
"no-unused-expressions": ["off"],
"class-methods-use-this": ["off"],
"react/require-default-props": ["off"],
"no-nested-ternary": ["off"],
Comment thread
tiagomotasantos marked this conversation as resolved.
}
}
2 changes: 2 additions & 0 deletions pmm-app/src/pmm-qan/panel/QueryAnalytics.constants.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { METRIC_CATALOGUE } from './QueryAnalytics.constants';

jest.mock('shared/components/helpers/notification-manager');

const metrics = {
num_queries: {
sum: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { mount } from 'enzyme';
import Example from './Example';
import { DatabasesType } from '../Details.types';

jest.mock('shared/components/helpers/notification-manager');
jest.mock('react-highlight.js', () => ({ children }) => <div className="sql">{children}</div>);
jest.mock('react-json-view', () => ({ src = {} }) => <div className="json" data-src={JSON.stringify(src)} />);

Expand Down
35 changes: 27 additions & 8 deletions pmm-app/src/pmm-update/UpdatePanel.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount, shallow } from 'enzyme';
import { Button, Spinner } from '@grafana/ui';

Expand All @@ -7,6 +8,7 @@ import { usePerformUpdate, useVersionDetails } from 'pmm-update/hooks';
import { UpdatePanel } from 'pmm-update/UpdatePanel';

jest.mock('shared/components/helpers/notification-manager');
jest.mock('shared/core/Settings.service');

// NOTE (nicolalamacchia): these mocks are here because some test cases alter them
jest.mock('./hooks/useVersionDetails', () => ({
Expand Down Expand Up @@ -55,16 +57,22 @@ describe('UpdatePanel::', () => {
mockedUseVersionDetails.mockClear();
});

it('shows a box telling that no upgrades are available by default', () => {
const wrapper = shallow(<UpdatePanel />);
it('shows a box telling that no updates are available by default', async () => {
let wrapper;

await act(async () => {
wrapper = await mount(<UpdatePanel />);
});

wrapper.update();

expect(wrapper.find(InfoBox).length).toEqual(1);
expect(wrapper.find(InfoBox).props()).toHaveProperty('upToDate', false);

wrapper.unmount();
});

it('should return the correct values if the upgrade initialization was successful', () => {
it('should return the correct values if the update initialization was successful', () => {
const wrapper = shallow(<UpdatePanel />);

expect(mockedUseVersionDetails).toBeCalledTimes(1);
Expand All @@ -83,7 +91,7 @@ describe('UpdatePanel::', () => {
wrapper.unmount();
});

it('should launch the upgrade if the upgrade button is clicked', () => {
it('should launch the update if the update button is clicked', async () => {
mockedUseVersionDetails.mockImplementation(() => [
{
installedVersionDetails, lastCheckDate, nextVersionDetails, isUpdateAvailable: true,
Expand All @@ -94,16 +102,21 @@ describe('UpdatePanel::', () => {
fakeGetCurrentVersionDetails,
]);

const wrapper = shallow(<UpdatePanel />);
let wrapper;

wrapper?.find(Button).simulate('click');
await act(async () => {
wrapper = await mount(<UpdatePanel />);
});

wrapper.update();
wrapper.find('button').at(0).simulate('click');

expect(fakeLaunchUpdate).toBeCalledTimes(1);

wrapper?.unmount();
});

it('should show InfoBox with the upToDate prop if !isUpdateAvailable && !isDefaultView', () => {
it('should show InfoBox with the upToDate prop if !isUpdateAvailable && !isDefaultView', async () => {
mockedUseVersionDetails.mockImplementation(() => [
{
installedVersionDetails, lastCheckDate, nextVersionDetails, isUpdateAvailable: false,
Expand All @@ -114,7 +127,13 @@ describe('UpdatePanel::', () => {
fakeGetCurrentVersionDetails,
]);

const wrapper = shallow(<UpdatePanel />);
let wrapper;

await act(async () => {
wrapper = await mount(<UpdatePanel />);
});

wrapper.update();

expect(wrapper.find(InfoBox).length).toEqual(1);
expect(wrapper.find(InfoBox).props()).toHaveProperty('upToDate');
Expand Down
44 changes: 39 additions & 5 deletions pmm-app/src/pmm-update/UpdatePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,52 @@ import React, {
useEffect, useState, FC, MouseEvent,
} from 'react';
import { Button, Spinner } from '@grafana/ui';
import { logger } from '@percona/platform-core';
import {
AvailableUpdate, CurrentVersion, InfoBox, LastCheck, ProgressModal,
} from 'pmm-update/components';
import { useVersionDetails, usePerformUpdate } from 'pmm-update/hooks';

import { SettingsService } from 'shared/core';
import * as styles from './UpdatePanel.styles';

export const UpdatePanel: FC<{}> = () => {
const isOnline = navigator.onLine;
const [forceUpdate, setForceUpdate] = useState(false);
const [showModal, setShowModal] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [updatesDisabled, setUpdatesDisabled] = useState(false);
const [isLoadingSettings, setLoadingSettings] = useState(true);
const [hasNoAccess, setHasNoAccess] = useState(false);
const [
{
installedVersionDetails, lastCheckDate, nextVersionDetails, isUpdateAvailable,
},
fetchVersionErrorMessage,
isLoading,
isLoadingVersionDetails,
isDefaultView,
getCurrentVersionDetails,
] = useVersionDetails();
const [output, updateErrorMessage, isUpdated, updateFailed, launchUpdate] = usePerformUpdate();
const isLoading = isLoadingVersionDetails || isLoadingSettings;

const getSettings = async () => {
setLoadingSettings(true);

try {
const { updatesDisabled } = await SettingsService.getSettings(true);

setUpdatesDisabled(!!updatesDisabled);
} catch (e) {
if (e.response?.status === 401) {
setHasNoAccess(true);
}

logger.error(e);
}

setLoadingSettings(false);
};

const handleCheckForUpdates = (e: MouseEvent) => {
if (e.altKey) {
Expand All @@ -32,6 +57,10 @@ export const UpdatePanel: FC<{}> = () => {
getCurrentVersionDetails({ force: true });
};

useEffect(() => {
getSettings();
}, []);

useEffect(() => {
setErrorMessage(fetchVersionErrorMessage || updateErrorMessage);

Expand All @@ -53,7 +82,7 @@ export const UpdatePanel: FC<{}> = () => {
<>
<div className={styles.panel}>
<CurrentVersion installedVersionDetails={installedVersionDetails} />
{isUpdateAvailable && !isDefaultView ? (
{isUpdateAvailable && !isDefaultView && !updatesDisabled && !hasNoAccess && !isLoading && isOnline ? (
<AvailableUpdate nextVersionDetails={nextVersionDetails} />
) : null}
{isLoading ? (
Expand All @@ -62,7 +91,7 @@ export const UpdatePanel: FC<{}> = () => {
</div>
) : (
<>
{isUpdateAvailable || forceUpdate ? (
{(isUpdateAvailable || forceUpdate) && !updatesDisabled && !hasNoAccess && isOnline ? (
<div className={styles.middleSectionWrapper}>
<Button onClick={handleUpdate} icon={'fa fa-download' as any} variant="secondary">
Upgrade to
Expand All @@ -71,12 +100,17 @@ export const UpdatePanel: FC<{}> = () => {
</Button>
</div>
) : (
<InfoBox upToDate={!isDefaultView && !forceUpdate} />
<InfoBox
upToDate={!isDefaultView && !forceUpdate}
hasNoAccess={hasNoAccess}
updatesDisabled={updatesDisabled}
isOnline={isOnline}
/>
)}
</>
)}
<LastCheck
disabled={isLoading}
disabled={isLoading || updatesDisabled || !isOnline}
onCheckForUpdates={handleCheckForUpdates}
lastCheckDate={lastCheckDate}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PMM_ADVANCED_SETTINGS_URL = '/graph/settings/advanced-settings';
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ export const Messages = {
noUpdates: 'No updates are available',
updatesNotice: 'PMM checks for updates once a day',
upToDate: 'You are up to date',
noAccess: 'Insufficient access permissions',
updatesDisabled: 'Updates are disabled. You can enable them in ',
pmmSettings: 'PMM Settings.',
notOnline: 'PMM cannot check for updates',
};
31 changes: 21 additions & 10 deletions pmm-app/src/pmm-update/components/InfoBox/InfoBox.styles.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';

export const infoBox = css`
margin: 10px 0;
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: 2px solid #292929;
`;
export const getStyles = ({ colors, spacing }: GrafanaTheme) => ({
infoBox: css`
margin: 10px 0;
display: flex;
flex-direction: column;
flex: 1;
justify-content: center;
align-items: center;
box-sizing: border-box;
border: 2px solid #292929;
text-align: center;
padding: ${spacing.xs};
`,
link: css`
color: ${colors.linkExternal};
&:hover {
color: ${colors.textBlue};
}
`,
});
27 changes: 27 additions & 0 deletions pmm-app/src/pmm-update/components/InfoBox/InfoBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,31 @@ describe('InfoBox::', () => {

wrapper.unmount();
});

it('should show an insufficient access message', () => {
const wrapper = shallow(<InfoBox hasNoAccess />);

expect(wrapper.find('section > p').length).toEqual(1);
expect(wrapper.text()).toEqual(Messages.noAccess);

wrapper.unmount();
});

it('should show updates disabled messages', () => {
const wrapper = shallow(<InfoBox updatesDisabled />);

expect(wrapper.find('section > p').length).toEqual(1);
expect(wrapper.text()).toEqual(`${Messages.updatesDisabled}${Messages.pmmSettings}`);

wrapper.unmount();
});

it('should show not online messages', () => {
const wrapper = shallow(<InfoBox isOnline={false} />);

expect(wrapper.find('section > p').length).toEqual(1);
expect(wrapper.text()).toEqual(Messages.notOnline);

wrapper.unmount();
});
});
49 changes: 35 additions & 14 deletions pmm-app/src/pmm-update/components/InfoBox/InfoBox.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
import React, { FC } from 'react';

import { useStyles } from '@grafana/ui';
import { InfoBoxProps } from 'pmm-update/types';
import { Messages } from './InfoBox.messages';
import * as styles from './InfoBox.styles';
import { getStyles } from './InfoBox.styles';
import { PMM_ADVANCED_SETTINGS_URL } from './InfoBox.constants';

export const InfoBox: FC<InfoBoxProps> = ({
upToDate = false,
hasNoAccess,
updatesDisabled,
isOnline = true,
}) => {
const styles = useStyles(getStyles);

export const InfoBox: FC<InfoBoxProps> = ({ upToDate = false }) => (
<section className={styles.infoBox}>
{upToDate ? (
<p>{Messages.upToDate}</p>
) : (
<>
<p>{Messages.noUpdates}</p>
<p>{Messages.updatesNotice}</p>
</>
)}
</section>
);
return (
<section data-qa="updates-info" className={styles.infoBox}>
{hasNoAccess ? (
<p>{Messages.noAccess}</p>
) : !isOnline ? (
<p>{Messages.notOnline}</p>
) : updatesDisabled ? (
<p>
{Messages.updatesDisabled}
<a className={styles.link} href={PMM_ADVANCED_SETTINGS_URL}>
{Messages.pmmSettings}
</a>
</p>
) : upToDate ? (
<p>{Messages.upToDate}</p>
) : (
<>
<p>{Messages.noUpdates}</p>
<p>{Messages.updatesNotice}</p>
</>
)}
</section>
);
};
3 changes: 3 additions & 0 deletions pmm-app/src/pmm-update/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export interface ProgressModalHeaderProps {

export interface InfoBoxProps {
upToDate?: boolean;
updatesDisabled?: boolean;
hasNoAccess?: boolean;
isOnline?: boolean;
}

export interface AvailableUpdateProps {
Expand Down
15 changes: 15 additions & 0 deletions pmm-app/src/shared/core/Settings.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { apiRequest } from 'shared/components/helpers/api';
import { API } from './constants';
import { Settings, SettingsAPIResponse, SettingsPayload } from './types';

export const SettingsService = {
async getSettings(disableNotifications = false): Promise<Settings> {
const { settings } = await apiRequest.post(API.SETTINGS, {}, disableNotifications) as SettingsAPIResponse;

return toModel(settings);
},
};

const toModel = (response: SettingsPayload): Settings => ({
updatesDisabled: response.updates_disabled,
});
7 changes: 7 additions & 0 deletions pmm-app/src/shared/core/__mocks__/Settings.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Settings } from '../types';

export const SettingsService = {
async getSettings(): Promise<Settings> {
return Promise.resolve({ updatesDisabled: false });
},
};
1 change: 1 addition & 0 deletions pmm-app/src/shared/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './constants';
export * from './types';
export * from './Settings.service';
12 changes: 12 additions & 0 deletions pmm-app/src/shared/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ export enum Databases {
proxysql = 'proxysql',
haproxy = 'haproxy',
}

export interface Settings {
updatesDisabled?: boolean;
}

export interface SettingsAPIResponse {
settings: SettingsPayload;
}

export interface SettingsPayload {
updates_disabled: boolean;
}