import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import AppState from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import Icon from 'Components/Icon'; import Label from 'Components/Label'; import SpinnerButton from 'Components/Link/SpinnerButton'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import { icons, kinds } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; import { fetchGeneralSettings } from 'Store/Actions/settingsActions'; import { fetchUpdates } from 'Store/Actions/systemActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import { UpdateMechanism } from 'typings/Settings/General'; import formatDate from 'Utilities/Date/formatDate'; import formatDateTime from 'Utilities/Date/formatDateTime'; import translate from 'Utilities/String/translate'; import UpdateChanges from './UpdateChanges'; import styles from './Updates.css'; const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; function createUpdatesSelector() { return createSelector( (state: AppState) => state.system.updates, (state: AppState) => state.settings.general, (updates, generalSettings) => { const { error: updatesError, items } = updates; const isFetching = updates.isFetching || generalSettings.isFetching; const isPopulated = updates.isPopulated && generalSettings.isPopulated; return { isFetching, isPopulated, updatesError, generalSettingsError: generalSettings.error, items, updateMechanism: generalSettings.item.updateMechanism, }; } ); } function Updates() { const currentVersion = useSelector((state: AppState) => state.app.version); const { packageUpdateMechanismMessage } = useSelector( createSystemStatusSelector() ); const { shortDateFormat, longDateFormat, timeFormat } = useSelector( createUISettingsSelector() ); const isInstallingUpdate = useSelector( createCommandExecutingSelector(commandNames.APPLICATION_UPDATE) ); const { isFetching, isPopulated, updatesError, generalSettingsError, items, updateMechanism, } = useSelector(createUpdatesSelector()); const dispatch = useDispatch(); const [isMajorUpdateModalOpen, setIsMajorUpdateModalOpen] = useState(false); const hasError = !!(updatesError || generalSettingsError); const hasUpdates = isPopulated && !hasError && items.length > 0; const noUpdates = isPopulated && !hasError && !items.length; const externalUpdaterPrefix = translate('UpdateAppDirectlyLoadError'); const externalUpdaterMessages: Partial> = { external: translate('ExternalUpdater'), apt: translate('AptUpdater'), docker: translate('DockerUpdater'), }; const { isMajorUpdate, hasUpdateToInstall } = useMemo(() => { const majorVersion = parseInt( currentVersion.match(VERSION_REGEX)?.[0] ?? '0' ); const latestVersion = items[0]?.version; const latestMajorVersion = parseInt( latestVersion?.match(VERSION_REGEX)?.[0] ?? '0' ); return { isMajorUpdate: latestMajorVersion > majorVersion, hasUpdateToInstall: items.some( (update) => update.installable && update.latest ), }; }, [currentVersion, items]); const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; const handleInstallLatestPress = useCallback(() => { if (isMajorUpdate) { setIsMajorUpdateModalOpen(true); } else { dispatch(executeCommand({ name: commandNames.APPLICATION_UPDATE })); } }, [isMajorUpdate, setIsMajorUpdateModalOpen, dispatch]); const handleInstallLatestMajorVersionPress = useCallback(() => { setIsMajorUpdateModalOpen(false); dispatch( executeCommand({ name: commandNames.APPLICATION_UPDATE, installMajorUpdate: true, }) ); }, [setIsMajorUpdateModalOpen, dispatch]); const handleCancelMajorVersionPress = useCallback(() => { setIsMajorUpdateModalOpen(false); }, [setIsMajorUpdateModalOpen]); useEffect(() => { dispatch(fetchUpdates()); dispatch(fetchGeneralSettings()); }, [dispatch]); return ( {isPopulated || hasError ? null : } {noUpdates ? ( {translate('NoUpdatesAreAvailable')} ) : null} {hasUpdateToInstall ? (
{updateMechanism === 'builtIn' || updateMechanism === 'script' ? ( {translate('InstallLatest')} ) : ( <>
{externalUpdaterPrefix}{' '}
)} {isFetching ? ( ) : null}
) : null} {noUpdateToInstall && (
{translate('OnLatestVersion')}
{isFetching && ( )}
)} {hasUpdates && (
{items.map((update) => { return (
{update.version}
{formatDate(update.releaseDate, shortDateFormat)}
{update.branch === 'master' ? null : ( )} {update.version === currentVersion ? ( ) : null} {update.version !== currentVersion && update.installedOn ? ( ) : null}
{update.changes ? (
) : (
{translate('MaintenanceRelease')}
)}
); })}
)} {updatesError ? ( {translate('FailedToFetchUpdates')} ) : null} {generalSettingsError ? ( {translate('FailedToFetchSettings')} ) : null}
{translate('InstallMajorVersionUpdateMessage')}
} confirmLabel={translate('Install')} onConfirm={handleInstallLatestMajorVersionPress} onCancel={handleCancelMajorVersionPress} />
); } export default Updates;