Skip to content

Commit

Permalink
PMM-12912 Add indicator for updates (#723)
Browse files Browse the repository at this point in the history
* PMM-12912 Add indicator for updates

* PMM-12912 Update MegaMenuItem

* PMM-12912 Update pmm updates url

* PMM-12912 Replace badge with dot

* PMM-12912 Show dot based on the update availability

* PMM-12912 Adjust dot

* PMM-12912 update betterrer

* PMM-12912 Fix dot styling

* PMM-12912 Reorder comments

* PMM-12912 Update api response
  • Loading branch information
matejkubinec authored Jul 23, 2024
1 parent fe75a73 commit a63d331
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 11 deletions.
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 { logger } from '../../helpers/logger';
import { isPmmAdmin } from '../../helpers/permissions';

Expand Down Expand Up @@ -82,6 +83,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),
};
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';

0 comments on commit a63d331

Please sign in to comment.