import { Banner, Variant } from 'design-system';
import { groupBy, isEmpty, mapValues } from 'lodash';
import * as React from 'react';
-import { getSystemUpgrades } from '../../../api/system';
import DismissableAlert from '../../../components/ui/DismissableAlert';
import SystemUpgradeButton from '../../../components/upgrade/SystemUpgradeButton';
-import { UpdateUseCase, sortUpgrades } from '../../../components/upgrade/utils';
+import { UpdateUseCase } from '../../../components/upgrade/utils';
import { translate } from '../../../helpers/l10n';
import { hasGlobalPermission } from '../../../helpers/users';
+import { useSystemUpgrades } from '../../../queries/system';
import { AppState } from '../../../types/appstate';
import { Permissions } from '../../../types/permissions';
-import { SystemUpgrade } from '../../../types/system';
import { Dict } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users';
import withAppStateContext from '../app-state/withAppStateContext';
import withCurrentUserContext from '../current-user/withCurrentUserContext';
-
-const MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION = 6;
-
-type GroupedSystemUpdate = {
- [x: string]: Dict<SystemUpgrade[]>;
-};
+import { isMinorUpdate, isPatchUpdate, isPreLTSUpdate, isPreviousLTSUpdate } from './helpers';
const MAP_VARIANT: Dict<Variant> = {
[UpdateUseCase.NewMinorVersion]: 'info',
currentUser: CurrentUser;
}
-interface State {
- dismissKey: string;
- useCase: UpdateUseCase;
- latestLTS: string;
- systemUpgrades: SystemUpgrade[];
- canSeeNotification: boolean;
-}
-
-export class UpdateNotification extends React.PureComponent<Props, State> {
- mounted = false;
- versionParser = /^(\d+)\.(\d+)(\.(\d+))?/;
-
- constructor(props: Props) {
- super(props);
- this.state = {
- dismissKey: '',
- systemUpgrades: [],
- latestLTS: '',
- canSeeNotification: false,
- useCase: UpdateUseCase.NewMinorVersion,
- };
- }
-
- componentDidMount() {
- this.mounted = true;
-
- this.fetchSystemUpgradeInformation();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- isPreLTSUpdate(parsedVersion: number[], latestLTS: string) {
- const [currentMajor, currentMinor] = parsedVersion;
- const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number);
- return currentMajor < ltsMajor || (currentMajor === ltsMajor && currentMinor < ltsMinor);
- }
-
- isPreviousLTSUpdate(
- parsedVersion: number[],
- latestLTS: string,
- systemUpgrades: GroupedSystemUpdate,
+const VERSION_PARSER = /^(\d+)\.(\d+)(\.(\d+))?/;
+
+export function UpdateNotification({ dismissable, appState, currentUser }: Readonly<Props>) {
+ const canUserSeeNotification =
+ isLoggedIn(currentUser) && hasGlobalPermission(currentUser, Permissions.Admin);
+ const regExpParsedVersion = VERSION_PARSER.exec(appState.version);
+ const { data } = useSystemUpgrades({
+ enabled: canUserSeeNotification && regExpParsedVersion !== null,
+ });
+
+ if (
+ !canUserSeeNotification ||
+ regExpParsedVersion === null ||
+ data === undefined ||
+ isEmpty(data.upgrades)
) {
- const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number);
- let ltsOlderThan6Month = false;
- const beforeLts = this.isPreLTSUpdate(parsedVersion, latestLTS);
- if (beforeLts) {
- const allLTS = sortUpgrades(systemUpgrades[ltsMajor][ltsMinor]);
- const ltsReleaseDate = new Date(allLTS[allLTS.length - 1]?.releaseDate || '');
- if (isNaN(ltsReleaseDate.getTime())) {
- // We can not parse the LTS date.
- // It is unlikly that this could happen but consider LTS to be old.
- return true;
- }
- ltsOlderThan6Month =
- ltsReleaseDate.setMonth(ltsReleaseDate.getMonth() + MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION) -
- Date.now() <
- 0;
- }
- return ltsOlderThan6Month && beforeLts;
+ return null;
}
- isMinorUpdate(parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) {
- const [currentMajor, currentMinor] = parsedVersion;
- const allMinor = systemUpgrades[currentMajor];
- return Object.keys(allMinor)
- .map(Number)
- .some((minor) => minor > currentMinor);
- }
-
- isPatchUpdate(parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) {
- const [currentMajor, currentMinor, currentPatch] = parsedVersion;
- const allMinor = systemUpgrades[currentMajor];
- const allPatch = sortUpgrades(allMinor[currentMinor] || []);
-
- if (!isEmpty(allPatch)) {
- const [, , latestPatch] = allPatch[0].version.split('.').map(Number);
- const effectiveCurrentPatch = isNaN(currentPatch) ? 0 : currentPatch;
- const effectiveLatestPatch = isNaN(latestPatch) ? 0 : latestPatch;
- return effectiveCurrentPatch < effectiveLatestPatch;
- }
- return false;
- }
-
- async fetchSystemUpgradeInformation() {
- if (
- !isLoggedIn(this.props.currentUser) ||
- !hasGlobalPermission(this.props.currentUser, Permissions.Admin)
- ) {
- this.noPromptToShow();
- return;
- }
-
- const regExpParsedVersion = this.versionParser.exec(this.props.appState.version);
- if (regExpParsedVersion === null) {
- this.noPromptToShow();
- return;
- }
- regExpParsedVersion.shift();
- const parsedVersion = regExpParsedVersion.map(Number).map((n) => (isNaN(n) ? 0 : n));
-
- const { upgrades, latestLTS } = await getSystemUpgrades();
-
- if (isEmpty(upgrades)) {
- // No new upgrades
- this.noPromptToShow();
- return;
- }
- const systemUpgrades = mapValues(
+ const { upgrades, latestLTS } = data;
+ const parsedVersion = regExpParsedVersion
+ .slice(1)
+ .map(Number)
+ .map((n) => (isNaN(n) ? 0 : n));
+
+ const systemUpgrades = mapValues(
+ groupBy(upgrades, (upgrade) => {
+ const [major] = upgrade.version.split('.');
+ return major;
+ }),
+ (upgrades) =>
groupBy(upgrades, (upgrade) => {
- const [major] = upgrade.version.split('.');
- return major;
+ const [, minor] = upgrade.version.split('.');
+ return minor;
}),
- (upgrades) =>
- groupBy(upgrades, (upgrade) => {
- const [, minor] = upgrade.version.split('.');
- return minor;
- }),
- );
-
- let useCase = UpdateUseCase.NewMinorVersion;
-
- if (this.isPreviousLTSUpdate(parsedVersion, latestLTS, systemUpgrades)) {
- useCase = UpdateUseCase.PreviousLTS;
- } else if (this.isPreLTSUpdate(parsedVersion, latestLTS)) {
- useCase = UpdateUseCase.PreLTS;
- } else if (this.isPatchUpdate(parsedVersion, systemUpgrades)) {
- useCase = UpdateUseCase.NewPatch;
- } else if (this.isMinorUpdate(parsedVersion, systemUpgrades)) {
- useCase = UpdateUseCase.NewMinorVersion;
- }
-
- const latest = [...upgrades].sort(
- (upgrade1, upgrade2) =>
- new Date(upgrade2.releaseDate ?? '').getTime() -
- new Date(upgrade1.releaseDate ?? '').getTime(),
- )[0];
-
- const dismissKey = useCase + latest.version;
-
- if (this.mounted) {
- this.setState({
- latestLTS,
- useCase,
- dismissKey,
- systemUpgrades: upgrades,
- canSeeNotification: true,
- });
- }
+ );
+
+ let useCase = UpdateUseCase.NewMinorVersion;
+
+ if (isPreviousLTSUpdate(parsedVersion, latestLTS, systemUpgrades)) {
+ useCase = UpdateUseCase.PreviousLTS;
+ } else if (isPreLTSUpdate(parsedVersion, latestLTS)) {
+ useCase = UpdateUseCase.PreLTS;
+ } else if (isPatchUpdate(parsedVersion, systemUpgrades)) {
+ useCase = UpdateUseCase.NewPatch;
+ } else if (isMinorUpdate(parsedVersion, systemUpgrades)) {
+ useCase = UpdateUseCase.NewMinorVersion;
}
- noPromptToShow() {
- if (this.mounted) {
- this.setState({ canSeeNotification: false });
- }
- }
-
- render() {
- const { dismissable } = this.props;
- const { latestLTS, systemUpgrades, canSeeNotification, useCase, dismissKey } = this.state;
- if (!canSeeNotification) {
- return null;
- }
- return dismissable ? (
- <DismissableAlert
- alertKey={dismissKey}
- variant={MAP_VARIANT[useCase]}
- className={`it__promote-update-notification it__upgrade-prompt-${useCase}`}
- >
- {translate('admin_notification.update', useCase)}
- <SystemUpgradeButton
- systemUpgrades={systemUpgrades}
- updateUseCase={useCase}
- latestLTS={latestLTS}
- />
- </DismissableAlert>
- ) : (
- <Banner variant={MAP_VARIANT[useCase]} className={`it__upgrade-prompt-${useCase}`}>
- {translate('admin_notification.update', useCase)}
- <SystemUpgradeButton
- systemUpgrades={systemUpgrades}
- updateUseCase={useCase}
- latestLTS={latestLTS}
- />
- </Banner>
- );
- }
+ const latest = [...upgrades].sort(
+ (upgrade1, upgrade2) =>
+ new Date(upgrade2.releaseDate ?? '').getTime() -
+ new Date(upgrade1.releaseDate ?? '').getTime(),
+ )[0];
+
+ const dismissKey = useCase + latest.version;
+
+ return dismissable ? (
+ <DismissableAlert
+ alertKey={dismissKey}
+ variant={MAP_VARIANT[useCase]}
+ className={`it__promote-update-notification it__upgrade-prompt-${useCase}`}
+ >
+ {translate('admin_notification.update', useCase)}
+ <SystemUpgradeButton
+ systemUpgrades={upgrades}
+ updateUseCase={useCase}
+ latestLTS={latestLTS}
+ />
+ </DismissableAlert>
+ ) : (
+ <Banner variant={MAP_VARIANT[useCase]} className={`it__upgrade-prompt-${useCase}`}>
+ {translate('admin_notification.update', useCase)}
+ <SystemUpgradeButton
+ systemUpgrades={upgrades}
+ updateUseCase={useCase}
+ latestLTS={latestLTS}
+ />
+ </Banner>
+ );
}
export default withCurrentUserContext(withAppStateContext(UpdateNotification));
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { isEmpty } from 'lodash';
+import { sortUpgrades } from '../../../components/upgrade/utils';
+import { SystemUpgrade } from '../../../types/system';
+
+const MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION = 6;
+
+type GroupedSystemUpdate = {
+ [x: string]: Record<string, SystemUpgrade[]>;
+};
+
+export const isPreLTSUpdate = (parsedVersion: number[], latestLTS: string) => {
+ const [currentMajor, currentMinor] = parsedVersion;
+ const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number);
+ return currentMajor < ltsMajor || (currentMajor === ltsMajor && currentMinor < ltsMinor);
+};
+
+export const isPreviousLTSUpdate = (
+ parsedVersion: number[],
+ latestLTS: string,
+ systemUpgrades: GroupedSystemUpdate,
+) => {
+ const [ltsMajor, ltsMinor] = latestLTS.split('.').map(Number);
+ let ltsOlderThan6Month = false;
+ const beforeLts = isPreLTSUpdate(parsedVersion, latestLTS);
+ if (beforeLts) {
+ const allLTS = sortUpgrades(systemUpgrades[ltsMajor][ltsMinor]);
+ const ltsReleaseDate = new Date(allLTS[allLTS.length - 1]?.releaseDate ?? '');
+ if (isNaN(ltsReleaseDate.getTime())) {
+ // We can not parse the LTS date.
+ // It is unlikly that this could happen but consider LTS to be old.
+ return true;
+ }
+ ltsOlderThan6Month =
+ ltsReleaseDate.setMonth(ltsReleaseDate.getMonth() + MONTH_BEFOR_PREVIOUS_LTS_NOTIFICATION) -
+ Date.now() <
+ 0;
+ }
+ return ltsOlderThan6Month && beforeLts;
+};
+
+export const isMinorUpdate = (parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) => {
+ const [currentMajor, currentMinor] = parsedVersion;
+ const allMinor = systemUpgrades[currentMajor];
+ return Object.keys(allMinor)
+ .map(Number)
+ .some((minor) => minor > currentMinor);
+};
+
+export const isPatchUpdate = (parsedVersion: number[], systemUpgrades: GroupedSystemUpdate) => {
+ const [currentMajor, currentMinor, currentPatch] = parsedVersion;
+ const allMinor = systemUpgrades[currentMajor];
+ const allPatch = sortUpgrades(allMinor[currentMinor] || []);
+
+ if (!isEmpty(allPatch)) {
+ const [, , latestPatch] = allPatch[0].version.split('.').map(Number);
+ const effectiveCurrentPatch = isNaN(currentPatch) ? 0 : currentPatch;
+ const effectiveLatestPatch = isNaN(latestPatch) ? 0 : latestPatch;
+ return effectiveCurrentPatch < effectiveLatestPatch;
+ }
+ return false;
+};