From de38f9d467ba9e3a05c164ae7e9d2fb8d6333347 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Mon, 28 Oct 2024 21:02:51 +0100 Subject: [PATCH] refactor: show new app version --- apps/client/src/common/api/constants.ts | 1 + .../src/common/hooks-query/useAppVersion.ts | 33 +++++ .../panel-list/PanelList.module.scss | 16 ++- .../app-settings/panel-list/PanelList.tsx | 116 +++++++++++------- .../panel/about-panel/AboutPanel.tsx | 6 +- .../panel/about-panel/AppVersion.tsx | 31 +++++ .../panel/about-panel/CheckUpdatesButton.tsx | 80 ------------ .../FeatureSettingsPanel.tsx | 2 +- .../panel/general-panel/GeneralPanel.tsx | 2 +- .../integrations-panel/IntegrationsPanel.tsx | 2 +- .../panel/network-panel/NetworkLogPanel.tsx | 2 +- .../panel/project-panel/ProjectPanel.tsx | 2 +- ...ettingsStore.ts => useAppSettingsMenu.tsx} | 46 +++---- .../app-settings/useAppSettingsNavigation.tsx | 2 +- 14 files changed, 172 insertions(+), 169 deletions(-) create mode 100644 apps/client/src/common/hooks-query/useAppVersion.ts create mode 100644 apps/client/src/features/app-settings/panel/about-panel/AppVersion.tsx delete mode 100644 apps/client/src/features/app-settings/panel/about-panel/CheckUpdatesButton.tsx rename apps/client/src/features/app-settings/{settingsStore.ts => useAppSettingsMenu.tsx} (61%) diff --git a/apps/client/src/common/api/constants.ts b/apps/client/src/common/api/constants.ts index dafbf1a97..4751a3711 100644 --- a/apps/client/src/common/api/constants.ts +++ b/apps/client/src/common/api/constants.ts @@ -1,6 +1,7 @@ // keys in tanstack store export const APP_INFO = ['appinfo']; export const APP_SETTINGS = ['appSettings']; +export const APP_VERSION = ['appVersion']; export const CUSTOM_FIELDS = ['customFields']; export const HTTP_SETTINGS = ['httpSettings']; export const OSC_SETTINGS = ['oscSettings']; diff --git a/apps/client/src/common/hooks-query/useAppVersion.ts b/apps/client/src/common/hooks-query/useAppVersion.ts new file mode 100644 index 000000000..65136c58c --- /dev/null +++ b/apps/client/src/common/hooks-query/useAppVersion.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { dayInMs } from 'ontime-utils'; + +import { version } from '../../../../../package.json'; +import { APP_VERSION, isLocalhost } from '../api/constants'; +import { getLatestVersion, HasUpdate } from '../api/external'; + +const placeholder: HasUpdate & { hasUpdates: boolean } = { url: '', version: '', hasUpdates: false }; + +export default function useAppVersion() { + const { + data: fetchData, + status, + isFetching, + isError, + refetch, + } = useQuery({ + queryKey: APP_VERSION, + queryFn: getLatestVersion, + placeholderData: (previousData, _previousQuery) => previousData, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: false, + staleTime: dayInMs, + enabled: isLocalhost, + }); + + const hasUpdates = fetchData?.version && !fetchData.version.includes(version); + + const data = fetchData ? { ...fetchData, hasUpdates } : placeholder; + + return { data, placeholder, status, isFetching, isError, refetch }; +} diff --git a/apps/client/src/features/app-settings/panel-list/PanelList.module.scss b/apps/client/src/features/app-settings/panel-list/PanelList.module.scss index a0faf09e2..327c0a735 100644 --- a/apps/client/src/features/app-settings/panel-list/PanelList.module.scss +++ b/apps/client/src/features/app-settings/panel-list/PanelList.module.scss @@ -40,12 +40,16 @@ ul { background-color: $gray-1100; } - &.unsaved::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 3px; - background-color: $blue-400; + &.highlight { + color: $red-400; + + &::before { + content: ''; + width: 0.5em; + height: 0.5em; + border-radius: 99px; + background-color: $red-400; + } } &.split { diff --git a/apps/client/src/features/app-settings/panel-list/PanelList.tsx b/apps/client/src/features/app-settings/panel-list/PanelList.tsx index 34c939227..f0f6e7dbe 100644 --- a/apps/client/src/features/app-settings/panel-list/PanelList.tsx +++ b/apps/client/src/features/app-settings/panel-list/PanelList.tsx @@ -1,66 +1,90 @@ import { Fragment } from 'react'; +import { Tooltip } from '@chakra-ui/react'; import { isKeyEnter } from '../../../common/utils/keyEvent'; import { cx } from '../../../common/utils/styleUtils'; -import { PanelBaseProps, settingPanels, useSettingsStore } from '../settingsStore'; +import { tooltipDelayFast } from '../../../ontimeConfig'; +import { SettingsOption, SettingsOptionId, useAppSettingsMenu } from '../useAppSettingsMenu'; import useAppSettingsNavigation from '../useAppSettingsNavigation'; import style from './PanelList.module.scss'; +export interface PanelBaseProps { + location?: string; +} + interface PanelListProps extends PanelBaseProps { selectedPanel: string; } export default function PanelList({ selectedPanel, location }: PanelListProps) { - const { setLocation } = useAppSettingsNavigation(); - const { hasUnsavedChanges } = useSettingsStore(); + const { options } = useAppSettingsMenu(); return ( ); } + +interface PanelListItemProps { + panel: SettingsOption; + isSelected: boolean; + location?: string; +} + +function PanelListItem(props: PanelListItemProps) { + const { panel, isSelected, location } = props; + const { setLocation } = useAppSettingsNavigation(); + + const classes = cx([ + style.primary, + isSelected && style.active, + panel.split && style.split, + panel.highlight && style.highlight, + ]); + + return ( + +
  • setLocation(panel.id as SettingsOptionId)} + onKeyDown={(event) => { + isKeyEnter(event) && setLocation(panel.id as SettingsOptionId); + }} + className={classes} + tabIndex={0} + role='button' + > + {panel.label} +
  • + {panel.secondary?.map((secondary) => { + const id = secondary.id.split('__')[1]; + const secondaryClasses = cx([style.secondary, location === id ? style.active : null]); + return ( +
  • setLocation(secondary.id as SettingsOptionId)} + onKeyDown={(event) => { + isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId); + }} + className={secondaryClasses} + role='button' + > + {secondary.label} +
  • + ); + })} +
    + ); +} diff --git a/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx b/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx index ab47622b4..2fca9de49 100644 --- a/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx +++ b/apps/client/src/features/app-settings/panel/about-panel/AboutPanel.tsx @@ -1,9 +1,8 @@ -import { version } from '../../../../../package.json'; import ExternalLink from '../../../../common/components/external-link/ExternalLink'; import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals'; import * as Panel from '../PanelUtils'; -import CheckUpdatesButton from './CheckUpdatesButton'; +import AppVersion from './AppVersion'; export default function AboutPanel() { return ( @@ -23,8 +22,7 @@ export default function AboutPanel() { Current version - {`You are currently using Ontime ${version}`} - + ); diff --git a/apps/client/src/features/app-settings/panel/about-panel/AppVersion.tsx b/apps/client/src/features/app-settings/panel/about-panel/AppVersion.tsx new file mode 100644 index 000000000..4938434b3 --- /dev/null +++ b/apps/client/src/features/app-settings/panel/about-panel/AppVersion.tsx @@ -0,0 +1,31 @@ +import { version } from '../../../../../package.json'; +import ExternalLink from '../../../../common/components/external-link/ExternalLink'; +import useAppVersion from '../../../../common/hooks-query/useAppVersion'; +import * as Panel from '../PanelUtils'; + +export default function AppVersion() { + const { data, isError } = useAppVersion(); + + if (isError) { + return ( + + {`You are currently using Ontime version ${version}`} + {`Could not fetch version information: ${isError}`} + + ); + } + + if (data.hasUpdates) { + return ( + <> + {`You are currently using Ontime version ${version}.`} + + {`A new version ${data.version} is available.`} + Please visit the release page to download + + + ); + } + + return {`You are currently using the latest version of Ontime: ${version}`}; +} diff --git a/apps/client/src/features/app-settings/panel/about-panel/CheckUpdatesButton.tsx b/apps/client/src/features/app-settings/panel/about-panel/CheckUpdatesButton.tsx deleted file mode 100644 index d5bdf47bb..000000000 --- a/apps/client/src/features/app-settings/panel/about-panel/CheckUpdatesButton.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useState } from 'react'; -import { Button } from '@chakra-ui/react'; - -import { getLatestVersion, HasUpdate } from '../../../../common/api/external'; -import ExternalLink from '../../../../common/components/external-link/ExternalLink'; - -import style from '../Panel.module.scss'; - -type CheckFail = { - error: string; -}; - -type CheckIsLatest = { - latest: true; -}; - -type CheckRemote = CheckFail | CheckIsLatest | HasUpdate; - -interface CheckUpdatesButtonProps { - version: string; -} - -export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) { - const { version } = props; - - const [updateMessage, setUpdateMessage] = useState(null); - const [isFetching, setIsFetching] = useState(false); - - /** - * Handles version comparison and returns component with message - */ - const versionCheck = async () => { - setIsFetching(true); - - try { - const latest = await getLatestVersion(); - - if (!latest.version.includes(version)) { - // new version, pass data to component - setUpdateMessage(latest); - } else { - setUpdateMessage({ latest: true }); - } - } catch { - setUpdateMessage({ error: 'Error reaching server' }); - } finally { - setIsFetching(false); - } - }; - - const disableButton = Boolean(updateMessage && 'version' in updateMessage); - - return ( - <> - - - - ); -} - -function ResolveUpdateMessage(props: { updateMessage: CheckRemote | null }) { - const { updateMessage } = props; - - if (updateMessage && 'error' in updateMessage) { - return {updateMessage.error}; - } - if (updateMessage && 'url' in updateMessage) { - return {`New version available: ${updateMessage.version}`}; - } - return null; -} diff --git a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx index af4756096..079978a10 100644 --- a/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/feature-settings-panel/FeatureSettingsPanel.tsx @@ -1,5 +1,5 @@ import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; -import { PanelBaseProps } from '../../settingsStore'; +import type { PanelBaseProps } from '../../panel-list/PanelList'; import * as Panel from '../PanelUtils'; import CustomFields from './custom-fields/CustomFields'; diff --git a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx index 0c87d4eaf..2e11e30ae 100644 --- a/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx +++ b/apps/client/src/features/app-settings/panel/general-panel/GeneralPanel.tsx @@ -1,5 +1,5 @@ import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; -import { PanelBaseProps } from '../../settingsStore'; +import type { PanelBaseProps } from '../../panel-list/PanelList'; import EditorSettingsForm from '../interface-panel/EditorSettingsForm'; import * as Panel from '../PanelUtils'; diff --git a/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx b/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx index ea3cfe151..23d3a0b4f 100644 --- a/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx +++ b/apps/client/src/features/app-settings/panel/integrations-panel/IntegrationsPanel.tsx @@ -2,7 +2,7 @@ import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react'; import ExternalLink from '../../../../common/components/external-link/ExternalLink'; import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; -import { PanelBaseProps } from '../../settingsStore'; +import type { PanelBaseProps } from '../../panel-list/PanelList'; import * as Panel from '../PanelUtils'; import HttpIntegrations from './HttpIntegrations'; diff --git a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx index 0de76a1cf..e9224ba43 100644 --- a/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx +++ b/apps/client/src/features/app-settings/panel/network-panel/NetworkLogPanel.tsx @@ -1,5 +1,5 @@ import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; -import { PanelBaseProps } from '../../settingsStore'; +import type { PanelBaseProps } from '../../panel-list/PanelList'; import ClientControlPanel from '../client-control-panel/ClientControlPanel'; import * as Panel from '../PanelUtils'; diff --git a/apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.tsx b/apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.tsx index ffd90aee3..83b25cf61 100644 --- a/apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.tsx +++ b/apps/client/src/features/app-settings/panel/project-panel/ProjectPanel.tsx @@ -1,5 +1,5 @@ import useScrollIntoView from '../../../../common/hooks/useScrollIntoView'; -import { PanelBaseProps } from '../../settingsStore'; +import type { PanelBaseProps } from '../../panel-list/PanelList'; import * as Panel from '../PanelUtils'; import ManageProjects from './ManageProjects'; diff --git a/apps/client/src/features/app-settings/settingsStore.ts b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx similarity index 61% rename from apps/client/src/features/app-settings/settingsStore.ts rename to apps/client/src/features/app-settings/useAppSettingsMenu.tsx index b93f78719..1badd22eb 100644 --- a/apps/client/src/features/app-settings/settingsStore.ts +++ b/apps/client/src/features/app-settings/useAppSettingsMenu.tsx @@ -1,13 +1,16 @@ -import { create } from 'zustand'; +import { useMemo } from 'react'; + +import useAppVersion from '../../common/hooks-query/useAppVersion'; export type SettingsOption = { id: string; label: string; secondary?: Readonly; split?: boolean; + highlight?: string; }; -export const settingPanels: Readonly = [ +const staticOptions = [ { id: 'project', label: 'Project', @@ -77,30 +80,19 @@ export const settingPanels: Readonly = [ }, ] as const; -export type SettingsOptionId = (typeof settingPanels)[number]['id']; +export type SettingsOptionId = (typeof staticOptions)[number]['id']; -export interface PanelBaseProps { - location?: string; +export function useAppSettingsMenu() { + const { data } = useAppVersion(); + + const options: Readonly = useMemo( + () => + staticOptions.map((option) => ({ + ...option, + highlight: option.id === 'about' && data.hasUpdates ? 'New version available' : undefined, + })), + [data], + ); + + return { options }; } - -type SettingsStore = { - unsavedChanges: Set; - hasUnsavedChanges: (panelId: SettingsOptionId) => boolean; - addUnsavedChanges: (panelId: SettingsOptionId) => void; - removeUnsavedChanges: (panelId: SettingsOptionId) => void; -}; - -export const useSettingsStore = create((set, get) => ({ - unsavedChanges: new Set(), - hasUnsavedChanges: (panelId: SettingsOptionId) => get().unsavedChanges.has(panelId), - addUnsavedChanges: (panelId: SettingsOptionId) => - set((state) => { - state.unsavedChanges.add(panelId); - return { unsavedChanges: new Set(state.unsavedChanges) }; - }), - removeUnsavedChanges: (panelId: SettingsOptionId) => - set((state) => { - state.unsavedChanges.delete(panelId); - return { unsavedChanges: new Set(state.unsavedChanges) }; - }), -})); diff --git a/apps/client/src/features/app-settings/useAppSettingsNavigation.tsx b/apps/client/src/features/app-settings/useAppSettingsNavigation.tsx index 70867f3fc..abe59a3da 100644 --- a/apps/client/src/features/app-settings/useAppSettingsNavigation.tsx +++ b/apps/client/src/features/app-settings/useAppSettingsNavigation.tsx @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { SettingsOptionId } from './settingsStore'; +import { SettingsOptionId } from './useAppSettingsMenu'; const settingsKey = 'settings';