mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
refactor: WS (#1622)
* refactor: WS * refactor refetch extract refetch keys to package/types auto invalidata all keys use refetch keys for project data no need for constant update of info, just fetch when looking at it split refetch and query keys rename types * refactor viewSettings * fixup! refactor: WS * rearange files and make types for api calls * cleanup viewsettings * switch exhaustiveCheck * combine send socket function * exhaustive check * rename type to tag * fixup! fixup! refactor: WS * remove custom etag solution * get errors from socket * lint * small change --------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
c8fe0bbcba
commit
31d47ed1e5
@@ -1,3 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { serverURL } from '../../externals';
|
||||
|
||||
// keys in tanstack store
|
||||
@@ -23,3 +25,7 @@ const cssOverridePath = 'styles/override.css';
|
||||
|
||||
export const overrideStylesURL = `${serverURL}/${userAssetsPath}/${cssOverridePath}`;
|
||||
export const projectLogoPath = `${serverURL}/${userAssetsPath}/logo`;
|
||||
|
||||
axios.defaults.validateStatus = (status) => {
|
||||
return (status >= 200 && status < 300) || status === 304;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import axios from 'axios';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
import type { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { apiEntryUrl } from './constants';
|
||||
const viewSettingsPath = apiEntryUrl + '/view-settings';
|
||||
|
||||
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve view settings
|
||||
*/
|
||||
export async function getView(): Promise<ViewSettings> {
|
||||
export async function getViewSettings() {
|
||||
const res = await axios.get(viewSettingsPath);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to mutate view settings
|
||||
*/
|
||||
export async function postViewSettings(data: ViewSettings) {
|
||||
return axios.post(viewSettingsPath, data);
|
||||
const res = await axios.post(viewSettingsPath, data);
|
||||
return res.data as ViewSettings;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { getViewSettings, postViewSettings } from '../../common/api/viewSettings';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
import { VIEW_SETTINGS } from '../api/constants';
|
||||
import { getView } from '../api/viewSettings';
|
||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
|
||||
export default function useViewSettings() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: VIEW_SETTINGS,
|
||||
queryFn: getView,
|
||||
queryFn: getViewSettings,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||
return { data: data ?? viewsSettingsPlaceholder, status, isError, refetch, isFetching };
|
||||
const { mutateAsync } = useMutation({
|
||||
mutationFn: postViewSettings,
|
||||
onMutate: () => {
|
||||
ontimeQueryClient.cancelQueries({ queryKey: VIEW_SETTINGS });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
ontimeQueryClient.setQueryData(VIEW_SETTINGS, data);
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data ?? viewsSettingsPlaceholder, mutateAsync, isPending };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { MessageTag } from 'ontime-types';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
import { useClientStore } from '../stores/clientStore';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
import { sendSocket } from '../utils/socket';
|
||||
|
||||
import { useIsOnline } from './useSocket';
|
||||
|
||||
@@ -22,7 +23,7 @@ export const useClientPath = () => {
|
||||
useEffect(() => {
|
||||
if (!isOnline) return;
|
||||
|
||||
socketSendJson('set-client-path', pathname + search);
|
||||
sendSocket(MessageTag.ClientSetPath, pathname + search);
|
||||
}, [pathname, search, isOnline]);
|
||||
|
||||
// navigate to new path when received from server
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OffsetMode, RuntimeStore, SimpleDirection, SimplePlayback, TimerMessage } from 'ontime-types';
|
||||
|
||||
import { useRuntimeStore } from '../stores/runtime';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
import { sendSocket } from '../utils/socket';
|
||||
|
||||
const createSelector =
|
||||
<T>(selector: (state: RuntimeStore) => T) =>
|
||||
@@ -9,9 +9,9 @@ const createSelector =
|
||||
useRuntimeStore(selector);
|
||||
|
||||
export const setClientRemote = {
|
||||
setIdentify: (payload: { target: string; identify: boolean }) => socketSendJson('client', payload),
|
||||
setRedirect: (payload: { target: string; redirect: string }) => socketSendJson('client', payload),
|
||||
setClientName: (payload: { target: string; rename: string }) => socketSendJson('client', payload),
|
||||
setIdentify: (payload: { target: string; identify: boolean }) => sendSocket('client', payload),
|
||||
setRedirect: (payload: { target: string; redirect: string }) => sendSocket('client', payload),
|
||||
setClientName: (payload: { target: string; rename: string }) => sendSocket('client', payload),
|
||||
};
|
||||
|
||||
export const useRundownEditor = createSelector((state: RuntimeStore) => ({
|
||||
@@ -53,13 +53,13 @@ export const useMessagePreview = createSelector((state: RuntimeStore) => ({
|
||||
}));
|
||||
|
||||
export const setMessage = {
|
||||
timerText: (payload: string) => socketSendJson('message', { timer: { text: payload } }),
|
||||
timerVisible: (payload: boolean) => socketSendJson('message', { timer: { visible: payload } }),
|
||||
externalText: (payload: string) => socketSendJson('message', { external: payload }),
|
||||
timerBlink: (payload: boolean) => socketSendJson('message', { timer: { blink: payload } }),
|
||||
timerBlackout: (payload: boolean) => socketSendJson('message', { timer: { blackout: payload } }),
|
||||
timerText: (payload: string) => sendSocket('message', { timer: { text: payload } }),
|
||||
timerVisible: (payload: boolean) => sendSocket('message', { timer: { visible: payload } }),
|
||||
externalText: (payload: string) => sendSocket('message', { external: payload }),
|
||||
timerBlink: (payload: boolean) => sendSocket('message', { timer: { blink: payload } }),
|
||||
timerBlackout: (payload: boolean) => sendSocket('message', { timer: { blackout: payload } }),
|
||||
timerSecondary: (payload: TimerMessage['secondarySource']) =>
|
||||
socketSendJson('message', { timer: { secondarySource: payload } }),
|
||||
sendSocket('message', { timer: { secondarySource: payload } }),
|
||||
};
|
||||
|
||||
export const usePlaybackControl = createSelector((state: RuntimeStore) => ({
|
||||
@@ -70,24 +70,24 @@ export const usePlaybackControl = createSelector((state: RuntimeStore) => ({
|
||||
}));
|
||||
|
||||
export const setPlayback = {
|
||||
start: () => socketSendJson('start'),
|
||||
pause: () => socketSendJson('pause'),
|
||||
roll: () => socketSendJson('roll'),
|
||||
startNext: () => socketSendJson('start', 'next'),
|
||||
start: () => sendSocket('start', undefined),
|
||||
pause: () => sendSocket('pause', undefined),
|
||||
roll: () => sendSocket('roll', undefined),
|
||||
startNext: () => sendSocket('start', 'next'),
|
||||
previous: () => {
|
||||
socketSendJson('load', 'previous');
|
||||
sendSocket('load', 'previous');
|
||||
},
|
||||
next: () => {
|
||||
socketSendJson('load', 'next');
|
||||
sendSocket('load', 'next');
|
||||
},
|
||||
stop: () => {
|
||||
socketSendJson('stop');
|
||||
sendSocket('stop', undefined);
|
||||
},
|
||||
reload: () => {
|
||||
socketSendJson('reload');
|
||||
sendSocket('reload', undefined);
|
||||
},
|
||||
addTime: (amount: number) => {
|
||||
socketSendJson('addtime', amount);
|
||||
sendSocket('addtime', amount);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -99,11 +99,11 @@ export const useAuxTimerControl = createSelector((state: RuntimeStore) => ({
|
||||
}));
|
||||
|
||||
export const setAuxTimer = {
|
||||
start: () => socketSendJson('auxtimer', { '1': SimplePlayback.Start }),
|
||||
pause: () => socketSendJson('auxtimer', { '1': SimplePlayback.Pause }),
|
||||
stop: () => socketSendJson('auxtimer', { '1': SimplePlayback.Stop }),
|
||||
setDirection: (direction: SimpleDirection) => socketSendJson('auxtimer', { '1': { direction } }),
|
||||
setDuration: (time: number) => socketSendJson('auxtimer', { '1': { duration: time } }),
|
||||
start: () => sendSocket('auxtimer', { '1': SimplePlayback.Start }),
|
||||
pause: () => sendSocket('auxtimer', { '1': SimplePlayback.Pause }),
|
||||
stop: () => sendSocket('auxtimer', { '1': SimplePlayback.Stop }),
|
||||
setDirection: (direction: SimpleDirection) => sendSocket('auxtimer', { '1': { direction } }),
|
||||
setDuration: (time: number) => sendSocket('auxtimer', { '1': { duration: time } }),
|
||||
};
|
||||
|
||||
export const useSelectedEventId = createSelector((state: RuntimeStore) => ({
|
||||
@@ -115,10 +115,10 @@ export const useCurrentBlockId = createSelector((state: RuntimeStore) => ({
|
||||
}));
|
||||
|
||||
export const setEventPlayback = {
|
||||
loadEvent: (id: string) => socketSendJson('load', { id }),
|
||||
startEvent: (id: string) => socketSendJson('start', { id }),
|
||||
start: () => socketSendJson('start'),
|
||||
pause: () => socketSendJson('pause'),
|
||||
loadEvent: (id: string) => sendSocket('load', { id }),
|
||||
startEvent: (id: string) => sendSocket('start', { id }),
|
||||
start: () => sendSocket('start', undefined),
|
||||
pause: () => sendSocket('pause', undefined),
|
||||
};
|
||||
|
||||
export const useTimer = createSelector((state: RuntimeStore) => ({
|
||||
@@ -190,7 +190,7 @@ export const useOffsetMode = createSelector((state: RuntimeStore) => ({
|
||||
offsetMode: state.runtime.offsetMode,
|
||||
}));
|
||||
|
||||
export const setOffsetMode = (payload: OffsetMode) => socketSendJson('offsetmode', payload);
|
||||
export const setOffsetMode = (payload: OffsetMode) => sendSocket('offsetmode', payload);
|
||||
|
||||
export const usePlayback = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { isOntimeCloud } from '../externals';
|
||||
|
||||
export const ontimeQueryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 60 * 10, // 10 min
|
||||
gcTime: 10 * MILLIS_PER_MINUTE,
|
||||
// staleTime: MILLIS_PER_HOUR, //TODO: when all routes have implemented refetch signal from server, we can the assume that the data is not stale until we get the signal
|
||||
networkMode: 'always',
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
},
|
||||
mutations: {
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Log, LogLevel, LogOrigin } from 'ontime-types';
|
||||
import { Log, LogLevel, LogOrigin, MessageTag } from 'ontime-types';
|
||||
import { generateId, millisToString } from 'ontime-utils';
|
||||
import { useStore } from 'zustand';
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
import { sendSocket } from '../utils/socket';
|
||||
import { nowInMillis } from '../utils/time';
|
||||
|
||||
type LogStore = {
|
||||
@@ -40,7 +40,7 @@ export function useEmitLog() {
|
||||
text,
|
||||
};
|
||||
|
||||
socketSendJson('ontime-log', log);
|
||||
sendSocket(MessageTag.Log, log);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Log, Rundown, RuntimeStore } from 'ontime-types';
|
||||
import {
|
||||
ApiAction,
|
||||
Log,
|
||||
MessageTag,
|
||||
RefetchKey,
|
||||
Rundown,
|
||||
RuntimeStore,
|
||||
WsPacketToClient,
|
||||
WsPacketToServer,
|
||||
} from 'ontime-types';
|
||||
|
||||
import { isProduction, websocketUrl } from '../../externals';
|
||||
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
|
||||
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME, VIEW_SETTINGS } from '../api/constants';
|
||||
import { invalidateAllCaches } from '../api/utils';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
import {
|
||||
@@ -33,16 +42,14 @@ export const connectSocket = () => {
|
||||
hasConnected = true;
|
||||
reconnectAttempts = 0;
|
||||
|
||||
socketSendJson('set-client-patch', {
|
||||
sendSocket(MessageTag.ClientSet, {
|
||||
type: 'ontime',
|
||||
origin: window.location.origin,
|
||||
path: window.location.pathname + window.location.search,
|
||||
name: preferredClientName,
|
||||
});
|
||||
invalidateAllCaches(); // assume all data to be stale after a reconnect
|
||||
setOnlineStatus(true);
|
||||
|
||||
if (preferredClientName) {
|
||||
socketSendJson('set-client-name', preferredClientName);
|
||||
}
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
@@ -65,56 +72,48 @@ export const connectSocket = () => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
websocket.onmessage = (event) => {
|
||||
websocket.onmessage = async (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
const data = JSON.parse(event.data) as WsPacketToClient;
|
||||
|
||||
const { type, payload } = data;
|
||||
const { tag, payload } = data;
|
||||
|
||||
if (!type) {
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'pong': {
|
||||
switch (tag) {
|
||||
case MessageTag.Pong: {
|
||||
const offset = (new Date().getTime() - new Date(payload).getTime()) * 0.5;
|
||||
patchRuntimeProperty('ping', offset);
|
||||
updateDevTools({ ping: offset });
|
||||
break;
|
||||
}
|
||||
case 'client': {
|
||||
if (typeof payload === 'object' || payload !== null) {
|
||||
if (payload.clientId && payload.clientName) {
|
||||
setClientId(payload.clientId);
|
||||
if (!preferredClientName) {
|
||||
setClientName(payload.clientName);
|
||||
}
|
||||
}
|
||||
case MessageTag.ClientInit: {
|
||||
setClientId(payload.clientId);
|
||||
if (!preferredClientName) {
|
||||
setClientName(payload.clientName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-rename': {
|
||||
if (typeof payload === 'object') {
|
||||
const id = getClientId();
|
||||
if (payload.target && payload.target === id) {
|
||||
setClientName(payload.name);
|
||||
}
|
||||
case MessageTag.ClientRename: {
|
||||
const id = getClientId();
|
||||
if (payload.target === id) {
|
||||
setClientName(payload.name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-redirect': {
|
||||
if (typeof payload === 'object') {
|
||||
const id = getClientId();
|
||||
if (payload.target && payload.target === id) {
|
||||
setClientRedirect(payload.path);
|
||||
}
|
||||
case MessageTag.ClientRedirect: {
|
||||
const id = getClientId();
|
||||
if (payload.target === id) {
|
||||
setClientRedirect(payload.path);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-list': {
|
||||
case MessageTag.ClientList: {
|
||||
setClients(payload);
|
||||
if (!isProduction) {
|
||||
ontimeQueryClient.setQueryData(CLIENT_LIST, payload);
|
||||
@@ -122,50 +121,57 @@ export const connectSocket = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dialog': {
|
||||
case MessageTag.Dialog: {
|
||||
if (payload.dialog === 'welcome') {
|
||||
addDialog('welcome');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ontime-log': {
|
||||
case MessageTag.Log: {
|
||||
addLog(payload as Log);
|
||||
break;
|
||||
}
|
||||
case 'ontime': {
|
||||
case MessageTag.RuntimeData: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- removing the key from the payload
|
||||
const { ping, ...serverPayload } = payload as Partial<RuntimeStore>;
|
||||
|
||||
const { ping, ...serverPayload } = payload;
|
||||
patchRuntime(serverPayload);
|
||||
updateDevTools(serverPayload);
|
||||
break;
|
||||
}
|
||||
case 'ontime-patch': {
|
||||
const patch = payload as Partial<RuntimeStore>;
|
||||
case MessageTag.RuntimePatch: {
|
||||
const patch = payload;
|
||||
patchRuntime(patch);
|
||||
updateDevTools(patch);
|
||||
break;
|
||||
}
|
||||
case 'ontime-refetch': {
|
||||
case MessageTag.Refetch: {
|
||||
// the refetch message signals that the rundown has changed in the server side
|
||||
const { reload, target } = payload;
|
||||
if (reload) {
|
||||
invalidateAllCaches();
|
||||
} else if (target === 'RUNDOWN') {
|
||||
const { revision } = payload;
|
||||
const currentRevision = ontimeQueryClient.getQueryData<Rundown>(RUNDOWN)?.revision ?? -1;
|
||||
if (revision > currentRevision) {
|
||||
const { target, revision } = payload;
|
||||
switch (target) {
|
||||
case RefetchKey.All:
|
||||
invalidateAllCaches();
|
||||
break;
|
||||
case RefetchKey.Rundown:
|
||||
if (revision === (ontimeQueryClient.getQueryData(RUNDOWN) as Rundown).revision) break;
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
|
||||
break;
|
||||
case RefetchKey.ViewSettings:
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: VIEW_SETTINGS });
|
||||
break;
|
||||
case RefetchKey.Report:
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||
break;
|
||||
default: {
|
||||
target satisfies never;
|
||||
break;
|
||||
}
|
||||
} else if (target === 'REPORT') {
|
||||
ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log('unknown WS message', type);
|
||||
tag satisfies never;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -175,20 +181,14 @@ export const connectSocket = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const socketSend = (message: any) => {
|
||||
export function sendSocket<T extends MessageTag | ApiAction>(
|
||||
tag: T,
|
||||
payload: T extends MessageTag ? Pick<WsPacketToServer & { tag: T }, 'payload'>['payload'] : unknown,
|
||||
): void {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(message);
|
||||
websocket.send(JSON.stringify({ tag, payload }));
|
||||
}
|
||||
};
|
||||
|
||||
export const socketSendJson = (type: string, payload?: unknown) => {
|
||||
socketSend(
|
||||
JSON.stringify({
|
||||
type,
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function updateDevTools(newData: Partial<RuntimeStore>) {
|
||||
if (!isProduction) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button, Input, Switch, useDisclosure } from '@chakra-ui/react';
|
||||
import { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import { postViewSettings } from '../../../../common/api/viewSettings';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||
@@ -19,17 +18,17 @@ import CodeEditorModal from './StyleEditorModal';
|
||||
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
|
||||
|
||||
export default function ViewSettingsForm() {
|
||||
const { data, status, refetch } = useViewSettings();
|
||||
const { data, isPending, mutateAsync } = useViewSettings();
|
||||
const { data: info, status: infoStatus } = useInfo();
|
||||
const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setError,
|
||||
register,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isSubmitting, isDirty },
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
} = useForm<ViewSettings>({
|
||||
defaultValues: data,
|
||||
values: data,
|
||||
@@ -46,17 +45,11 @@ export default function ViewSettingsForm() {
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = async (formData: ViewSettings) => {
|
||||
const newData = {
|
||||
...formData,
|
||||
};
|
||||
|
||||
try {
|
||||
await postViewSettings(newData);
|
||||
mutateAsync(formData);
|
||||
} catch (error) {
|
||||
const message = maybeAxiosError(error);
|
||||
setError('root', { message });
|
||||
} finally {
|
||||
await refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,7 +61,7 @@ export default function ViewSettingsForm() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isLoading = status === 'pending' || infoStatus === 'pending';
|
||||
const isLoading = isPending || infoStatus === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Section
|
||||
@@ -105,6 +98,7 @@ export default function ViewSettingsForm() {
|
||||
</Info>
|
||||
<Panel.Section>
|
||||
<Panel.Loader isLoading={isLoading} />
|
||||
<Panel.Error>{errors.root?.message}</Panel.Error>
|
||||
<Panel.ListGroup>
|
||||
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} />
|
||||
<Panel.ListItem>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { MessageTag } from 'ontime-types';
|
||||
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { usePing } from '../../../../common/hooks/useSocket';
|
||||
import { socketSendJson } from '../../../../common/utils/socket';
|
||||
import { sendSocket } from '../../../../common/utils/socket';
|
||||
import { isDockerImage, isOntimeCloud } from '../../../../externals';
|
||||
import type { PanelBaseProps } from '../../panel-list/PanelList';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
@@ -57,10 +58,10 @@ function OntimeCloudStats() {
|
||||
* Send immediate ping request, and keep sending on an interval
|
||||
*/
|
||||
useEffect(() => {
|
||||
socketSendJson('ping', new Date());
|
||||
sendSocket(MessageTag.Ping, new Date());
|
||||
|
||||
const doPing = setInterval(() => {
|
||||
socketSendJson('ping', new Date());
|
||||
sendSocket(MessageTag.Ping, new Date());
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -158,8 +158,10 @@ export default function RundownEntry(props: RundownEntryProps) {
|
||||
|
||||
return emitError(`Unknown field: ${field}`);
|
||||
}
|
||||
default:
|
||||
default: {
|
||||
action satisfies never;
|
||||
throw new Error(`Unhandled event ${action}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ export function getTimerByType(
|
||||
return timerObject.clock;
|
||||
case TimerType.None:
|
||||
return null;
|
||||
default:
|
||||
default: {
|
||||
viewTimerType satisfies never;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user