Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PMM-12912 Add indicator for updates #723

Merged
6 changes: 6 additions & 0 deletions .betterer.results.json
Original file line number Diff line number Diff line change
Expand Up @@ -5366,6 +5366,12 @@
"count": 1
}
],
"/public/app/percona/integrated-alerting/components/TemplateForm/EvaluateEvery/EvaluateEvery.styles.ts": [
{
"message": "Styles should be written using objects.",
"count": 1
}
],
"/public/app/plugins/datasource/alertmanager/DataSource.ts": [
{
"message": "Unexpected any. Specify a different type.",
Expand Down
1 change: 1 addition & 0 deletions packages/grafana-data/src/types/navModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface NavModelItem extends NavLinkDTO {
isDivider?: boolean;
isHeading?: boolean;
showChildren?: boolean;
showDot?: boolean;
}

/**
Expand Down
26 changes: 20 additions & 6 deletions public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useLocalStorage } from 'react-use';
import { GrafanaTheme2, NavModelItem, toIconName } from '@grafana/data';
import { useStyles2, Text, IconButton, Icon } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { Dot } from 'app/percona/shared/components/Elements/Dot';

import { Indent } from '../../Indent/Indent';

Expand Down Expand Up @@ -100,14 +101,23 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
{/* @PERCONA - show icons for inner items */}
{level <= 1 && link.icon && (
<FeatureHighlightWrapper>
<Icon
className={styles.icon}
name={toIconName(link.icon) ?? 'link'}
size={level === 0 ? 'lg' : 'md'}
/>
<>
<Icon
className={styles.icon}
name={toIconName(link.icon) ?? 'link'}
size={level === 0 ? 'lg' : 'md'}
/>
{/* @PERCONA */}
{!!link.showDot && <Dot left={23} top={0} />}
</>
</FeatureHighlightWrapper>
)}
<Text truncate>{link.text}</Text>
{/* @PERCONA */}
<div className={styles.relativeText}>
<Text truncate>{link.text}</Text>
{/* @PERCONA */}
{!!link.showDot && !link.icon && <Dot right={-8} top={2} />}
</div>
</div>
</MegaMenuItemText>
</div>
Expand Down Expand Up @@ -208,6 +218,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
fontStyle: 'italic',
padding: theme.spacing(1, 1.5, 1, 7),
}),
// @PERCONA
relativeText: css({
position: 'relative',
}),
});

function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } {
Expand Down
17 changes: 17 additions & 0 deletions public/app/percona/shared/components/Elements/Dot/Dot.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { css } from '@emotion/css';

import { GrafanaTheme2 } from '@grafana/data';

export const getStyles = (theme: GrafanaTheme2, top?: number, bottom?: number, right?: number, left?: number) => ({
dot: css({
position: 'absolute',
width: 6,
height: 6,
top,
bottom,
right,
left,
borderRadius: theme.shape.radius.circle,
backgroundColor: theme.colors.error.main,
}),
});
13 changes: 13 additions & 0 deletions public/app/percona/shared/components/Elements/Dot/Dot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import classNames from 'classnames';
import React, { FC } from 'react';

import { useStyles2 } from '@grafana/ui';

import { getStyles } from './Dot.styles';
import { DotProps } from './Dot.types';

export const Dot: FC<DotProps> = ({ top, bottom, right, left }) => {
const styles = useStyles2(getStyles, top, bottom, right, left);

return <div className={classNames(styles.dot)} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface DotProps {
top?: number;
bottom?: number;
right?: number;
left?: number;
}
1 change: 1 addition & 0 deletions public/app/percona/shared/components/Elements/Dot/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Dot } from './Dot';
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useAppDispatch } from 'app/store/store';

import { Telemetry } from '../../../ui-events/components/Telemetry';
import usePerconaTour from '../../core/hooks/tour';
import { checkUpdatesAction } from '../../core/reducers/updates';
import { isPmmAdmin } from '../../helpers/permissions';

import { Messages } from './PerconaBootstrapper.messages';
Expand Down Expand Up @@ -79,6 +80,7 @@ export const PerconaBootstrapper = ({ onReady }: PerconaBootstrapperProps) => {
await getSettings();
await dispatch(fetchUserStatusAction());
await dispatch(fetchAdvisors({ disableNotifications: true }));
await dispatch(checkUpdatesAction());
}

await getUserDetails();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ export const PMM_INVENTORY_PAGE: NavModelItem = {
children: [PMM_SERVICES_PAGE, PMM_NODES_PAGE],
};

export const PMM_UPDATES_LINK: NavModelItem = {
id: 'pmm-updates',
text: 'Updates',
url: '/pmm-ui/updates',
hideFromTabs: true,
target: '_self',
showDot: false,
};

export const PMM_HEADING_LINK: NavModelItem = {
id: 'settings-pmm',
text: 'PMM',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { fetchActiveServiceTypesAction } from 'app/percona/shared/core/reducers/
import { useAppDispatch } from 'app/store/store';
import { FolderDTO, useSelector } from 'app/types';

import { getCategorizedAdvisors, getPerconaSettings, getPerconaUser, getServices } from '../../../core/selectors';
import {
getCategorizedAdvisors,
getPerconaSettings,
getPerconaUser,
getServices,
getUpdatesInfo,
} from '../../../core/selectors';

import {
ACTIVE_SERVICE_TYPES_CHECK_INTERVAL_MS,
Expand Down Expand Up @@ -50,6 +56,7 @@ const PerconaNavigation: FC = () => {
const dispatch = useAppDispatch();
const { activeTypes } = useSelector(getServices);
const advisorsPage = buildAdvisorsNavItem(categorizedAdvisors);
const { updateAvailable } = useSelector(getUpdatesInfo);

dispatch(updateNavIndex(getPmmSettingsPage(alertingEnabled)));
dispatch(updateNavIndex(PMM_DUMP_PAGE));
Expand Down Expand Up @@ -113,7 +120,7 @@ const PerconaNavigation: FC = () => {
}
}

buildInventoryAndSettings(updatedNavTree, result);
buildInventoryAndSettings(updatedNavTree, result, updateAvailable);

const iaMenuItem = alertingEnabled
? buildIntegratedAlertingMenuItem(updatedNavTree)
Expand All @@ -140,7 +147,7 @@ const PerconaNavigation: FC = () => {

dispatch(updateNavTree(filterByServices(updatedNavTree, activeTypes)));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [result, folders, activeTypes, isAuthorized, isPlatformUser, advisorsPage]);
}, [result, folders, activeTypes, isAuthorized, isPlatformUser, advisorsPage, updateAvailable]);

return null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PMM_ADD_INSTANCE_CREATE_PAGE,
getPmmSettingsPage,
PMM_INVENTORY_PAGE,
PMM_UPDATES_LINK,
} from './PerconaNavigation.constants';

export const buildIntegratedAlertingMenuItem = (mainLinks: NavModelItem[]): NavModelItem | undefined => {
Expand Down Expand Up @@ -53,7 +54,11 @@ export const removeAlertingMenuItem = (mainLinks: NavModelItem[]) => {
return alertingItem;
};

export const buildInventoryAndSettings = (mainLinks: NavModelItem[], settings?: Settings): NavModelItem[] => {
export const buildInventoryAndSettings = (
mainLinks: NavModelItem[],
settings?: Settings,
updateAvailable?: boolean
): NavModelItem[] => {
const inventoryLink: NavModelItem = PMM_INVENTORY_PAGE;
const orgLink: NavModelItem = {
id: 'main-organization',
Expand All @@ -64,15 +69,18 @@ export const buildInventoryAndSettings = (mainLinks: NavModelItem[], settings?:
const configNode = mainLinks.find((link) => link.id === 'cfg');
const pmmConfigNode = mainLinks.find((link) => link.id === 'pmmcfg');

PMM_UPDATES_LINK.showDot = updateAvailable;

if (!pmmConfigNode) {
const pmmcfgNode: NavModelItem = {
id: 'pmmcfg',
text: 'PMM Configuration',
icon: 'percona-nav-logo',
url: `${config.appSubUrl}/inventory`,
subTitle: 'Configuration',
children: [PMM_ADD_INSTANCE_PAGE, PMM_ADD_INSTANCE_CREATE_PAGE, inventoryLink, settingsLink],
children: [PMM_ADD_INSTANCE_PAGE, PMM_ADD_INSTANCE_CREATE_PAGE, inventoryLink, settingsLink, PMM_UPDATES_LINK],
sortWeight: -800,
showDot: updateAvailable,
};
mainLinks.push(pmmcfgNode);
}
Expand Down
2 changes: 2 additions & 0 deletions public/app/percona/shared/core/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import pmmDumpsReducers from './pmmDump/pmmDump';
import rolesReducers from './roles/roles';
import servicesReducer from './services';
import tourReducer from './tour/tour';
import updatesReducers from './updates';
import perconaUserReducers from './user/user';
import usersReducers from './users/users';

Expand Down Expand Up @@ -216,5 +217,6 @@ export default {
users: usersReducers,
advisors: advisorsReducers,
pmmDumps: pmmDumpsReducers,
updates: updatesReducers,
}),
};
6 changes: 6 additions & 0 deletions public/app/percona/shared/core/reducers/updates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import updatesReducer from './updates';

export * from './updates';
export * from './updates.types';

export default updatesReducer;
45 changes: 45 additions & 0 deletions public/app/percona/shared/core/reducers/updates/updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

import { UpdatesService } from 'app/percona/shared/services/updates';

import { CheckUpdatesPayload, UpdatesState } from './updates.types';
import { responseToPayload } from './updates.utils';

const initialState: UpdatesState = {
isLoading: false,
};

export const updatesSlice = createSlice({
name: 'updates',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(checkUpdatesAction.pending, () => ({
...initialState,
isLoading: true,
}));

builder.addCase(checkUpdatesAction.fulfilled, (state, { payload }) => ({
...state,
...payload,
isLoading: false,
}));

builder.addCase(checkUpdatesAction.rejected, () => ({
...initialState,
isLoading: false,
}));
},
});

export const checkUpdatesAction = createAsyncThunk('percona/checkUpdates', async (): Promise<CheckUpdatesPayload> => {
try {
const res = await UpdatesService.getCurrentVersion({ force: true });
return responseToPayload(res);
} catch (error) {
const res = await UpdatesService.getCurrentVersion({ force: true, only_installed_version: true });
return responseToPayload(res);
}
});

export default updatesSlice.reducer;
28 changes: 28 additions & 0 deletions public/app/percona/shared/core/reducers/updates/updates.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface CurrentInformation {
version?: string;
fullVersion?: string;
timestamp?: string;
}

export interface LatestInformation {
version?: string;
tag?: string;
timestamp?: string;
}

export interface UpdatesState {
isLoading: boolean;
updateAvailable?: boolean;
installed?: CurrentInformation;
latest?: LatestInformation;
latestNewsUrl?: string;
lastChecked?: string;
}

export interface CheckUpdatesPayload {
installed?: CurrentInformation;
latest?: LatestInformation;
latestNewsUrl?: string;
lastChecked?: string;
updateAvailable: boolean;
}
23 changes: 23 additions & 0 deletions public/app/percona/shared/core/reducers/updates/updates.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CheckUpdatesResponse } from 'app/percona/shared/services/updates/Updates.types';

import { CheckUpdatesPayload } from './updates.types';

export const responseToPayload = (response: CheckUpdatesResponse): CheckUpdatesPayload => ({
installed: response.installed
? {
version: response.installed.version,
fullVersion: response.installed.full_version,
timestamp: response.installed.timestamp,
}
: undefined,
latest: response.latest
? {
version: response.latest.version,
tag: response.latest.tag,
timestamp: response.latest.timestamp,
}
: undefined,
lastChecked: response.last_check,
latestNewsUrl: response.latest_news_url,
updateAvailable: !!response.update_available,
});
1 change: 1 addition & 0 deletions public/app/percona/shared/core/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export const getCategorizedAdvisors = createSelector([getAdvisors], (advisors) =
groupAdvisorsIntoCategories(advisors.result || [])
);
export const getDumps = (state: StoreState) => state.percona.pmmDumps;
export const getUpdatesInfo = (state: StoreState) => state.percona.updates;
8 changes: 8 additions & 0 deletions public/app/percona/shared/services/updates/Updates.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { api } from '../../helpers/api';

import { CheckUpdatesBody, CheckUpdatesResponse } from './Updates.types';

export const UpdatesService = {
getCurrentVersion: (body: CheckUpdatesBody = { force: false }) =>
api.post<CheckUpdatesResponse, CheckUpdatesBody>('/v1/Updates/Check', body, true),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will impact the "API breaking changes" , https://github.com/percona/pmm/blob/PMM-12913-migrate-api-endpoints-to-restful/api/MIGRATION_TO_V3.md?plain=1

POST /v1/updates/Check was replaced by GET /v1/server/updates
Depending on what branch will be merged first, we need to make changes. To keep that in mind.

};
24 changes: 24 additions & 0 deletions public/app/percona/shared/services/updates/Updates.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface CheckUpdatesBody {
force: boolean;
only_installed_version?: boolean;
}

export interface CurrentInfo {
version?: string;
full_version?: string;
timestamp?: string;
}

export interface LatestInfo {
version?: string;
tag?: string;
timestamp?: string;
}

export interface CheckUpdatesResponse {
installed?: CurrentInfo;
latest?: LatestInfo;
update_available?: boolean;
latest_news_url?: string;
last_check?: string;
}
1 change: 1 addition & 0 deletions public/app/percona/shared/services/updates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UpdatesService } from './Updates.service';
Loading