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-13024 New Updates UI #761

Merged
merged 2 commits into from
Jul 23, 2024
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 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';
Loading