mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
refactor: show new app version
This commit is contained in:
committed by
Carlos Valente
parent
8ad1f2cb38
commit
de38f9d467
@@ -1,6 +1,7 @@
|
|||||||
// keys in tanstack store
|
// keys in tanstack store
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
|
export const APP_VERSION = ['appVersion'];
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const HTTP_SETTINGS = ['httpSettings'];
|
export const HTTP_SETTINGS = ['httpSettings'];
|
||||||
export const OSC_SETTINGS = ['oscSettings'];
|
export const OSC_SETTINGS = ['oscSettings'];
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -40,12 +40,16 @@ ul {
|
|||||||
background-color: $gray-1100;
|
background-color: $gray-1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unsaved::before {
|
&.highlight {
|
||||||
content: '';
|
color: $red-400;
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
&::before {
|
||||||
border-radius: 3px;
|
content: '';
|
||||||
background-color: $blue-400;
|
width: 0.5em;
|
||||||
|
height: 0.5em;
|
||||||
|
border-radius: 99px;
|
||||||
|
background-color: $red-400;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.split {
|
&.split {
|
||||||
|
|||||||
@@ -1,66 +1,90 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
import { isKeyEnter } from '../../../common/utils/keyEvent';
|
||||||
import { cx } from '../../../common/utils/styleUtils';
|
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 useAppSettingsNavigation from '../useAppSettingsNavigation';
|
||||||
|
|
||||||
import style from './PanelList.module.scss';
|
import style from './PanelList.module.scss';
|
||||||
|
|
||||||
|
export interface PanelBaseProps {
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface PanelListProps extends PanelBaseProps {
|
interface PanelListProps extends PanelBaseProps {
|
||||||
selectedPanel: string;
|
selectedPanel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PanelList({ selectedPanel, location }: PanelListProps) {
|
export default function PanelList({ selectedPanel, location }: PanelListProps) {
|
||||||
const { setLocation } = useAppSettingsNavigation();
|
const { options } = useAppSettingsMenu();
|
||||||
const { hasUnsavedChanges } = useSettingsStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={style.tabs}>
|
<ul className={style.tabs}>
|
||||||
{settingPanels.map((panel) => {
|
{options.map((panel) => {
|
||||||
const unsaved = hasUnsavedChanges(panel.id);
|
const isSelected = selectedPanel === panel.id;
|
||||||
|
if (panel.highlight) {
|
||||||
const classes = cx([
|
return (
|
||||||
style.primary,
|
<Tooltip key={panel.id} label={panel.highlight} openDelay={tooltipDelayFast} shouldWrapChildren>
|
||||||
selectedPanel === panel.id ? style.active : null,
|
<PanelListItem panel={panel} location={location} isSelected={isSelected} />
|
||||||
panel.split ? style.split : null,
|
</Tooltip>
|
||||||
unsaved ? style.unsaved : null,
|
);
|
||||||
]);
|
}
|
||||||
|
return <PanelListItem key={panel.id} panel={panel} location={location} isSelected={isSelected} />;
|
||||||
return (
|
|
||||||
<Fragment key={panel.id}>
|
|
||||||
<li
|
|
||||||
key={panel.id}
|
|
||||||
onClick={() => setLocation(panel.id)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && setLocation(panel.id);
|
|
||||||
}}
|
|
||||||
className={classes}
|
|
||||||
tabIndex={0}
|
|
||||||
role='button'
|
|
||||||
>
|
|
||||||
{panel.label}
|
|
||||||
</li>
|
|
||||||
{panel.secondary?.map((secondary) => {
|
|
||||||
const id = secondary.id.split('__')[1];
|
|
||||||
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={secondary.id}
|
|
||||||
onClick={() => setLocation(secondary.id)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
isKeyEnter(event) && setLocation(secondary.id);
|
|
||||||
}}
|
|
||||||
className={secondaryClasses}
|
|
||||||
role='button'
|
|
||||||
>
|
|
||||||
{secondary.label}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Fragment key={panel.id}>
|
||||||
|
<li
|
||||||
|
key={panel.id}
|
||||||
|
onClick={() => setLocation(panel.id as SettingsOptionId)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && setLocation(panel.id as SettingsOptionId);
|
||||||
|
}}
|
||||||
|
className={classes}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
>
|
||||||
|
{panel.label}
|
||||||
|
</li>
|
||||||
|
{panel.secondary?.map((secondary) => {
|
||||||
|
const id = secondary.id.split('__')[1];
|
||||||
|
const secondaryClasses = cx([style.secondary, location === id ? style.active : null]);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={secondary.id}
|
||||||
|
onClick={() => setLocation(secondary.id as SettingsOptionId)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId);
|
||||||
|
}}
|
||||||
|
className={secondaryClasses}
|
||||||
|
role='button'
|
||||||
|
>
|
||||||
|
{secondary.label}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { version } from '../../../../../package.json';
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals';
|
import { documentationUrl, githubUrl, websiteUrl } from '../../../../externals';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import CheckUpdatesButton from './CheckUpdatesButton';
|
import AppVersion from './AppVersion';
|
||||||
|
|
||||||
export default function AboutPanel() {
|
export default function AboutPanel() {
|
||||||
return (
|
return (
|
||||||
@@ -23,8 +22,7 @@ export default function AboutPanel() {
|
|||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.SubHeader>Current version</Panel.SubHeader>
|
<Panel.SubHeader>Current version</Panel.SubHeader>
|
||||||
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
<AppVersion />
|
||||||
<CheckUpdatesButton version={version} />
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Panel.Paragraph>
|
||||||
|
{`You are currently using Ontime version ${version}`}
|
||||||
|
<Panel.Error>{`Could not fetch version information: ${isError}`}</Panel.Error>
|
||||||
|
</Panel.Paragraph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasUpdates) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel.Paragraph>{`You are currently using Ontime version ${version}.`}</Panel.Paragraph>
|
||||||
|
<Panel.Paragraph>
|
||||||
|
{`A new version ${data.version} is available.`}
|
||||||
|
<ExternalLink href={data.url}> Please visit the release page to download</ExternalLink>
|
||||||
|
</Panel.Paragraph>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Panel.Paragraph>{`You are currently using the latest version of Ontime: ${version}`}</Panel.Paragraph>;
|
||||||
|
}
|
||||||
@@ -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<CheckRemote | null>(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 (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
onClick={versionCheck}
|
|
||||||
variant='ontime-filled'
|
|
||||||
isLoading={isFetching}
|
|
||||||
isDisabled={disableButton}
|
|
||||||
size='sm'
|
|
||||||
maxWidth='max-content'
|
|
||||||
>
|
|
||||||
Check for updates
|
|
||||||
</Button>
|
|
||||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ResolveUpdateMessage(props: { updateMessage: CheckRemote | null }) {
|
|
||||||
const { updateMessage } = props;
|
|
||||||
|
|
||||||
if (updateMessage && 'error' in updateMessage) {
|
|
||||||
return <span className={style.error}>{updateMessage.error}</span>;
|
|
||||||
}
|
|
||||||
if (updateMessage && 'url' in updateMessage) {
|
|
||||||
return <ExternalLink href={updateMessage?.url}>{`New version available: ${updateMessage.version}`}</ExternalLink>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import CustomFields from './custom-fields/CustomFields';
|
import CustomFields from './custom-fields/CustomFields';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
|||||||
|
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import HttpIntegrations from './HttpIntegrations';
|
import HttpIntegrations from './HttpIntegrations';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
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 ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||||
import { PanelBaseProps } from '../../settingsStore';
|
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ManageProjects from './ManageProjects';
|
import ManageProjects from './ManageProjects';
|
||||||
|
|||||||
+19
-27
@@ -1,13 +1,16 @@
|
|||||||
import { create } from 'zustand';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import useAppVersion from '../../common/hooks-query/useAppVersion';
|
||||||
|
|
||||||
export type SettingsOption = {
|
export type SettingsOption = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
secondary?: Readonly<SettingsOption[]>;
|
secondary?: Readonly<SettingsOption[]>;
|
||||||
split?: boolean;
|
split?: boolean;
|
||||||
|
highlight?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const settingPanels: Readonly<SettingsOption[]> = [
|
const staticOptions = [
|
||||||
{
|
{
|
||||||
id: 'project',
|
id: 'project',
|
||||||
label: 'Project',
|
label: 'Project',
|
||||||
@@ -77,30 +80,19 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SettingsOptionId = (typeof settingPanels)[number]['id'];
|
export type SettingsOptionId = (typeof staticOptions)[number]['id'];
|
||||||
|
|
||||||
export interface PanelBaseProps {
|
export function useAppSettingsMenu() {
|
||||||
location?: string;
|
const { data } = useAppVersion();
|
||||||
|
|
||||||
|
const options: Readonly<SettingsOption[]> = useMemo(
|
||||||
|
() =>
|
||||||
|
staticOptions.map((option) => ({
|
||||||
|
...option,
|
||||||
|
highlight: option.id === 'about' && data.hasUpdates ? 'New version available' : undefined,
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { options };
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsStore = {
|
|
||||||
unsavedChanges: Set<SettingsOptionId>;
|
|
||||||
hasUnsavedChanges: (panelId: SettingsOptionId) => boolean;
|
|
||||||
addUnsavedChanges: (panelId: SettingsOptionId) => void;
|
|
||||||
removeUnsavedChanges: (panelId: SettingsOptionId) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsStore>((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) };
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { SettingsOptionId } from './settingsStore';
|
import { SettingsOptionId } from './useAppSettingsMenu';
|
||||||
|
|
||||||
const settingsKey = 'settings';
|
const settingsKey = 'settings';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user