diff --git a/apps/client/src/common/api/sync.ts b/apps/client/src/common/api/sync.ts new file mode 100644 index 0000000000..12690aed28 --- /dev/null +++ b/apps/client/src/common/api/sync.ts @@ -0,0 +1,18 @@ +import axios from 'axios'; +import { SyncClientList, SyncHostConnectionRequest } from 'ontime-types'; + +import { apiEntryUrl } from './constants'; + +const syncPath = `${apiEntryUrl}/sync`; + +/** + * HTTP request to retrieve application info + */ +export async function getSyncList(): Promise { + const res = await axios.get(`${syncPath}/list`); + return res.data; +} + +export async function connectToHost(settings: SyncHostConnectionRequest) { + await axios.post(`${syncPath}/connect`, settings); +} diff --git a/apps/client/src/common/hooks-query/useAppVersion.ts b/apps/client/src/common/hooks-query/useAppVersion.ts index 399380b8d8..c002944330 100644 --- a/apps/client/src/common/hooks-query/useAppVersion.ts +++ b/apps/client/src/common/hooks-query/useAppVersion.ts @@ -1,34 +1,34 @@ -import { useQuery } from '@tanstack/react-query'; -import { dayInMs } from 'ontime-utils'; - -import { version } from '../../../../../package.json'; -import { isLocalhost } from '../../externals'; -import { APP_VERSION } 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 }; -} +import { useQuery } from '@tanstack/react-query'; +import { dayInMs } from 'ontime-utils'; + +import { version } from '../../../../../package.json'; +import { isLocalhost } from '../../externals'; +import { APP_VERSION } 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/common/hooks-query/useSync.ts b/apps/client/src/common/hooks-query/useSync.ts new file mode 100644 index 0000000000..73393f16ce --- /dev/null +++ b/apps/client/src/common/hooks-query/useSync.ts @@ -0,0 +1,13 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getSyncList } from '../api/sync'; + +export function useSync() { + const { data, status, isError, refetch, isLoading } = useQuery({ + queryKey: ['sync-client-list'], + queryFn: getSyncList, + placeholderData: (previousData, _previousQuery) => previousData, + }); + + return { list: data ?? [], status, isError, refetch, isLoading }; +} diff --git a/apps/client/src/common/utils/socket.ts b/apps/client/src/common/utils/socket.ts index c655443ceb..ce070385c0 100644 --- a/apps/client/src/common/utils/socket.ts +++ b/apps/client/src/common/utils/socket.ts @@ -182,7 +182,7 @@ export const connectSocket = () => { ontimeQueryClient.invalidateQueries({ queryKey: APP_SETTINGS }); break; default: { - target satisfies never; + target satisfies never | RefetchKey.RestorePoint; break; } } diff --git a/apps/client/src/features/app-settings/AppSettings.tsx b/apps/client/src/features/app-settings/AppSettings.tsx index 90e4931cd0..262db0d735 100644 --- a/apps/client/src/features/app-settings/AppSettings.tsx +++ b/apps/client/src/features/app-settings/AppSettings.tsx @@ -10,6 +10,7 @@ import NetworkLogPanel from './panel/network-panel/NetworkLogPanel'; import ProjectPanel from './panel/project-panel/ProjectPanel'; import SettingsPanel from './panel/settings-panel/SettingsPanel'; import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel'; +import Sync from './panel/sync-panel/SyncPanel'; import PanelContent from './panel-content/PanelContent'; import PanelList from './panel-list/PanelList'; import useAppSettingsNavigation from './useAppSettingsNavigation'; @@ -33,6 +34,7 @@ export default function AppSettings() { {panel === 'network' && } {panel === 'about' && } {panel === 'shutdown' && } + {panel === 'sync' && } diff --git a/apps/client/src/features/app-settings/panel/sync-panel/SyncPanel.tsx b/apps/client/src/features/app-settings/panel/sync-panel/SyncPanel.tsx new file mode 100644 index 0000000000..e8c7206efe --- /dev/null +++ b/apps/client/src/features/app-settings/panel/sync-panel/SyncPanel.tsx @@ -0,0 +1,180 @@ +import { useForm } from 'react-hook-form'; +import { SyncHostConnectionRequest, SyncRoll } from 'ontime-types'; + +import { connectToHost } from '../../../../common/api/sync'; +import { maybeAxiosError } from '../../../../common/api/utils'; +import Button from '../../../../common/components/buttons/Button'; +import Input from '../../../../common/components/input/input/Input'; +import Select from '../../../../common/components/select/Select'; +import { useSync } from '../../../../common/hooks-query/useSync'; +import { preventEscape } from '../../../../common/utils/keyEvent'; +import * as Panel from '../../panel-utils/PanelUtils'; + +export default function Sync() { + const { list, refetch, isLoading } = useSync(); + const { + handleSubmit, + register, + reset, + setError, + watch, + setValue, + formState: { isSubmitting, isDirty, isValid, errors }, + } = useForm({ + mode: 'onChange', + defaultValues: { host: 'http://127.0.0.1:4003', roll: SyncRoll.Listener }, + resetOptions: { + keepDirtyValues: true, + }, + }); + + const onSubmit = async (formData: SyncHostConnectionRequest) => { + try { + await connectToHost(formData); + } catch (error) { + const message = maybeAxiosError(error); + setError('root', { message }); + } finally { + await refetch(); + } + }; + + const submitError = ''; + const disableSubmit = isSubmitting || !isValid; + + const onReset = () => { + reset(); + }; + + return ( + <> + Machine Synchronization + + {list.length === 0 && ( + preventEscape(event, onReset)} + id='sync-settings' + > + + + Connect to a host + + + + + + {submitError && {submitError}} + + + + + + + + + + +