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 4 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.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { get } from 'lodash';
import { humanize } from 'shared/components/helpers/Humanization';
import { Databases } from 'shared/core';

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

enum PageSizes {
low = '25',
medium = '50',
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';
Loading