mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-01 13:38:03 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7c48f38a9 | |||
| dcb6bfa62d | |||
| 0abc4de3cd | |||
| f750ed3cdc | |||
| f026c1c1e3 | |||
| ce176b01a2 | |||
| 337dd7892a | |||
| b7daf2926a | |||
| 81a07099ae | |||
| 8f88ca5053 | |||
| 82a885dc44 | |||
| c4be814de5 | |||
| 9f3186e249 | |||
| 5159554697 | |||
| cf690aff08 | |||
| 48c4e5f429 | |||
| ad0da6def4 | |||
| a15eb0db4c | |||
| 70e8fbf327 | |||
| 1044dfac01 | |||
| b5e5c113e2 | |||
| cbcff5e7ab | |||
| 83cb2f82f5 | |||
| 837fd53dfd | |||
| 4962288a53 | |||
| 75a35e8537 | |||
| 6e839fcefb | |||
| 052a8e4ccb | |||
| 969c2bad14 | |||
| 9717222491 | |||
| 081c0bdac4 | |||
| 8b99e91883 | |||
| 4af2e688d1 |
@@ -0,0 +1,53 @@
|
||||
name: Ontime CLI build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [ "*" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_cli:
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.18.2
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build project packages
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
run: pnpm build
|
||||
|
||||
- name: Copy server
|
||||
run: mkdir -p apps/cli/server && cp apps/server/dist/index.cjs apps/cli/server/index.cjs
|
||||
|
||||
- name: Copy client
|
||||
run: cp -R apps/client/build apps/cli/client
|
||||
|
||||
- name: Copy external
|
||||
run: cp -R apps/server/src/external apps/cli/external
|
||||
|
||||
- name: Publish to NPM
|
||||
run: pnpm publish --access public --no-git-checks
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
working-directory: ./apps/cli
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"plugins": [],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
server
|
||||
external
|
||||
client
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Ontime CLI
|
||||
|
||||

|
||||
> Ontime is an application for managing rundown and event timers.
|
||||
|
||||
Congratulations! You got this far into Ontime's rabbit hole and want to manage your installation.
|
||||
|
||||
The CLI tool is a fast and lightweight way of installing Ontime and is perfect for self-hosting and headless installs.
|
||||
|
||||
## Getting started
|
||||
|
||||
### Prerequisites
|
||||
You will need to have installed the correct version of node.js
|
||||
Please check [the app engines declaration](https://github.com/cpvalente/ontime/blob/master/package.json) for the correct version.
|
||||
|
||||
### Running Ontime
|
||||
To get Ontime running, all you need to do is install it in your system using your package manager of choice:
|
||||
|
||||
Install globally in your system
|
||||
```bash
|
||||
npm install -g @getontime/cli
|
||||
```
|
||||
|
||||
... and run using the installed script
|
||||
```bash
|
||||
ontime
|
||||
```
|
||||
|
||||
Or install and run ontime (the installation here is temporary for the duration of the session)
|
||||
```bash
|
||||
npx install @getontime/cli
|
||||
```
|
||||
|
||||
## Links
|
||||
- [Ontime's repository](https://github.com/cpvalente/ontime)
|
||||
- [Ontime's documentation](https://docs.getontime.no/)
|
||||
- [Ontime's website](https://getontime.no/)
|
||||
|
||||
## Sponsoring
|
||||
You can help the development of this project or say thank you with a one time donation. \
|
||||
See the [terms of donations](https://github.com/cpvalente/ontime/blob/master/SPONSOR.md)
|
||||
|
||||
[](https://github.com/sponsors/cpvalente)
|
||||
[](https://www.buymeacoffee.com/cpvalente)
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// NOTE: for now the following needs to be in place: ./server/index.cjs, ./client, ./external
|
||||
|
||||
const ontimeServer = require('./server/index.cjs');
|
||||
const { initAssets, startServer, startIntegrations, shutdown } = ontimeServer;
|
||||
|
||||
async function startOntime() {
|
||||
await initAssets();
|
||||
|
||||
await startServer();
|
||||
|
||||
await startIntegrations();
|
||||
}
|
||||
|
||||
startOntime();
|
||||
|
||||
process.on(['SIGHUP', 'SIGINT', 'SIGTERM'], () => {
|
||||
shutdown();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@getontime/cli",
|
||||
"version": "3.3.2",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
"keywords": [
|
||||
"lighdev",
|
||||
"ontime",
|
||||
"timer"
|
||||
],
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "main.js",
|
||||
"bin": {
|
||||
"ontime": "main.js"
|
||||
},
|
||||
"files": [
|
||||
"client",
|
||||
"external",
|
||||
"server"
|
||||
],
|
||||
"devDependencies": {
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"prettier": "^3.0.3"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime-ui",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,16 +4,15 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import ErrorBoundary from './common/components/error-boundary/ErrorBoundary';
|
||||
import IdentifyOverlay from './common/components/identify-overlay/IdentifyOverlay';
|
||||
import { AppContextProvider } from './common/context/AppContext';
|
||||
import { ontimeQueryClient } from './common/queryClient';
|
||||
import { socketClientName } from './common/stores/connectionName';
|
||||
import { connectSocket } from './common/utils/socket';
|
||||
import theme from './theme/theme';
|
||||
import { TranslationProvider } from './translation/TranslationProvider';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
const preferredClientName = socketClientName.getState().name;
|
||||
connectSocket(preferredClientName);
|
||||
connectSocket();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -24,11 +23,15 @@ function App() {
|
||||
<div className='App'>
|
||||
<ErrorBoundary>
|
||||
<TranslationProvider>
|
||||
<IdentifyOverlay />
|
||||
<AppRouter />
|
||||
</TranslationProvider>
|
||||
</ErrorBoundary>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</div>
|
||||
<ErrorBoundary>
|
||||
<div id='identify-portal' />
|
||||
</ErrorBoundary>
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { useClientPath } from './common/hooks/useClientPath';
|
||||
import Log from './features/log/Log';
|
||||
import withPreset from './features/PresetWrapper';
|
||||
import withData from './features/viewers/ViewWrapper';
|
||||
@@ -34,6 +35,9 @@ const TimerControl = lazy(() => import('./features/control/playback/TimerControl
|
||||
const MessageControl = lazy(() => import('./features/control/message/MessageControlExport'));
|
||||
|
||||
export default function AppRouter() {
|
||||
// handle client path changes
|
||||
useClientPath();
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const RUNTIME = ['runtimeStore'];
|
||||
export const SHEET_STATE = ['sheetState'];
|
||||
export const URL_PRESETS = ['urlpresets'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
export const CLIENT_LIST = ['clientList'];
|
||||
|
||||
// resolve location
|
||||
const location = window.location;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.buttonSection {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $element-spacing;
|
||||
margin-top: -$element-spacing;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftAddon,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
|
||||
import style from './ClientModal.module.scss';
|
||||
|
||||
interface RedirectClientModalProps {
|
||||
id: string;
|
||||
name?: string;
|
||||
path?: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RedirectClientModal(props: RedirectClientModalProps) {
|
||||
const { id, isOpen, name = '', path: currentPath = '', onClose } = props;
|
||||
const [path, setPath] = useState(currentPath);
|
||||
|
||||
const { setRedirect } = setClientRemote;
|
||||
|
||||
const handleRedirect = () => {
|
||||
if (path !== currentPath && path !== '') {
|
||||
setRedirect({ target: id, redirect: path });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const host = `${window.location.origin}/`;
|
||||
const canSubmit = path !== currentPath && path !== '';
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Redirect: {name}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<InputGroup variant='ontime-filled' size='md'>
|
||||
<InputLeftAddon>{host}</InputLeftAddon>
|
||||
<Input placeholder='minimal?key=0000ffff' value={path} onChange={(event) => setPath(event.target.value)} />
|
||||
</InputGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className={style.buttonSection}>
|
||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size='md' variant='ontime-filled' onClick={handleRedirect} isDisabled={!canSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
|
||||
import style from './ClientModal.module.scss';
|
||||
|
||||
interface RenameClientModalProps {
|
||||
id: string;
|
||||
name?: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RenameClientModal(props: RenameClientModalProps) {
|
||||
const { id, name: currentName = '', isOpen, onClose } = props;
|
||||
const [name, setName] = useState(currentName);
|
||||
|
||||
const { setClientName } = setClientRemote;
|
||||
|
||||
const handleRename = () => {
|
||||
if (name !== currentName && name !== '') {
|
||||
setClientName({ target: id, rename: name });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const canSubmit = name !== currentName && name !== '';
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} variant='ontime'>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Rename: {currentName}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
size='md'
|
||||
placeholder='new name'
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className={style.buttonSection}>
|
||||
<Button size='md' variant='ontime-subtle' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size='md' variant='ontime-filled' onClick={handleRename} isDisabled={!canSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: 10px solid $ui-white;
|
||||
background-color: $ui-black;
|
||||
|
||||
color: $ui-white;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
margin-top: -10vh;
|
||||
line-height: 1em;
|
||||
font-size: 12vw;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: $label-gray;
|
||||
line-height: 2em;
|
||||
font-size: 3vw;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { MILLIS_PER_MINUTE } from 'ontime-utils';
|
||||
|
||||
import { setClientRemote } from '../../hooks/useSocket';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
|
||||
import style from './IdentifyOverlay.module.scss';
|
||||
|
||||
export default function IdentifyOverlay() {
|
||||
const clients = useClientStore((store) => store.clients);
|
||||
const id = useClientStore((store) => store.id);
|
||||
const showOverlay = clients[id]?.identify;
|
||||
|
||||
if (!showOverlay) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const portalRoot = document.getElementById('identify-portal');
|
||||
|
||||
if (!portalRoot) {
|
||||
return null;
|
||||
}
|
||||
return createPortal(<Overlay />, portalRoot);
|
||||
}
|
||||
|
||||
function Overlay() {
|
||||
const clients = useClientStore((store) => store.clients);
|
||||
const id = useClientStore((store) => store.id);
|
||||
const name = useClientStore((store) => store.name);
|
||||
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const { setIdentify } = setClientRemote;
|
||||
const showOverlay = clients[id]?.identify;
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
setIdentify({ target: id, identify: false });
|
||||
}, [id, setIdentify]);
|
||||
|
||||
// start a timer that will close the overlay after some time
|
||||
useEffect(() => {
|
||||
if (showOverlay) {
|
||||
timerRef.current = setTimeout(handleClose, MILLIS_PER_MINUTE);
|
||||
}
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, [showOverlay, id, setIdentify, handleClose]);
|
||||
|
||||
console.log('here2');
|
||||
return (
|
||||
<div className={style.overlay} data-testid='identify-overlay' onClick={handleClose}>
|
||||
<div className={style.name}>{name}</div>
|
||||
<div className={style.message}>Click to close</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,10 +19,10 @@ import { IoSwapVertical } from '@react-icons/all-files/io5/IoSwapVertical';
|
||||
|
||||
import { navigatorConstants } from '../../../viewerConfig';
|
||||
import useClickOutside from '../../hooks/useClickOutside';
|
||||
import { useClientStore } from '../../stores/clientStore';
|
||||
import { useViewOptionsStore } from '../../stores/viewOptions';
|
||||
import { isKeyEnter } from '../../utils/keyEvent';
|
||||
|
||||
import RenameClientModal from './rename-client-modal/RenameClientModal';
|
||||
import { RenameClientModal } from '../client-modal/RenameClientModal';
|
||||
|
||||
import style from './NavigationMenu.module.scss';
|
||||
|
||||
@@ -34,8 +34,10 @@ interface NavigationMenuProps {
|
||||
function NavigationMenu(props: NavigationMenuProps) {
|
||||
const { isOpen, onClose } = props;
|
||||
|
||||
const { isOpen: isRenameOpen, onOpen: onRenameOpen, onClose: onRenameClose } = useDisclosure();
|
||||
const id = useClientStore((store) => store.id);
|
||||
const name = useClientStore((store) => store.name);
|
||||
|
||||
const { isOpen: isOpenRename, onOpen: onRenameOpen, onClose: onCloseRename } = useDisclosure();
|
||||
const { fullscreen, toggle } = useFullscreen();
|
||||
const { toggleMirror } = useViewOptionsStore();
|
||||
|
||||
@@ -45,7 +47,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
|
||||
return createPortal(
|
||||
<div id='navigation-menu-portal' ref={menuRef}>
|
||||
<RenameClientModal isOpen={isRenameOpen} onClose={onRenameClose} />
|
||||
<RenameClientModal id={id} name={name} isOpen={isOpenRename} onClose={onCloseRename} />
|
||||
<Drawer placement='left' onClose={onClose} isOpen={isOpen} variant='ontime' data-testid='navigation__menu'>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent>
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { setClientName } from '../../../hooks/useSocket';
|
||||
import { useSocketClientName } from '../../../stores/connectionName';
|
||||
|
||||
interface RenameClientModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function RenameClientModal({ isOpen, onClose }: RenameClientModalProps) {
|
||||
const { name: clientName, persistName } = useSocketClientName();
|
||||
const [newName, setNewName] = useState(clientName);
|
||||
|
||||
useEffect(() => {
|
||||
setNewName(clientName);
|
||||
}, [isOpen, clientName]);
|
||||
|
||||
const handleRename = async () => {
|
||||
if (newName) {
|
||||
setClientName(newName);
|
||||
persistName(newName);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size='sm'
|
||||
closeOnOverlayClick={false}
|
||||
motionPreset='slideInBottom'
|
||||
scrollBehavior='inside'
|
||||
preserveScrollBarGap
|
||||
variant='ontime'
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Rename client</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Input
|
||||
placeholder='Connection must have a name'
|
||||
defaultValue={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
variant='ontime-filled'
|
||||
/>
|
||||
<Button
|
||||
isDisabled={newName === clientName || !newName}
|
||||
onClick={handleRename}
|
||||
width='100%'
|
||||
variant='ontime-filled'
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useClientStore } from '../stores/clientStore';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
|
||||
export const useClientPath = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pathname, search } = useLocation();
|
||||
const redirect = useClientStore((store) => store.redirect);
|
||||
const setRedirect = useClientStore((store) => store.setRedirect);
|
||||
|
||||
// notify of client path changes
|
||||
useEffect(() => {
|
||||
//remove leading '/' from path
|
||||
const fullPath = (pathname.startsWith('/') ? pathname.slice(1) : pathname) + search;
|
||||
socketSendJson('set-client-path', fullPath);
|
||||
}, [pathname, search]);
|
||||
|
||||
// navigate to new path when received from server
|
||||
useEffect(() => {
|
||||
if (redirect === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// clear redirect
|
||||
setRedirect('');
|
||||
|
||||
// navigate if there is a path change
|
||||
if (redirect !== pathname + search) {
|
||||
navigate(redirect);
|
||||
}
|
||||
}, [navigate, pathname, redirect, search, setRedirect]);
|
||||
};
|
||||
@@ -3,6 +3,12 @@ import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
|
||||
import { useRuntimeStore } from '../stores/runtime';
|
||||
import { socketSendJson } from '../utils/socket';
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
export const useRundownEditor = () => {
|
||||
const featureSelector = (state: RuntimeStore) => ({
|
||||
playback: state.timer.playback,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ClientList } from 'ontime-types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ClientStore {
|
||||
name?: string;
|
||||
setName: (newValue: string) => void;
|
||||
|
||||
id: string;
|
||||
setId: (newValue: string) => void;
|
||||
|
||||
redirect: string;
|
||||
setRedirect: (newValue: string) => void;
|
||||
|
||||
clients: ClientList;
|
||||
setClients: (clients: ClientList) => void;
|
||||
}
|
||||
|
||||
const clientNameKey = 'ontime-client-name';
|
||||
|
||||
function persistNameInStorage(newValue: string) {
|
||||
localStorage.setItem(clientNameKey, newValue);
|
||||
}
|
||||
|
||||
export const useClientStore = create<ClientStore>((set) => ({
|
||||
name: localStorage.getItem(clientNameKey) ?? undefined,
|
||||
setName: (name: string) =>
|
||||
set(() => {
|
||||
persistNameInStorage(name);
|
||||
return { name };
|
||||
}),
|
||||
|
||||
id: '',
|
||||
setId: (id) => set({ id }),
|
||||
|
||||
redirect: '',
|
||||
setRedirect: (redirect: string) => set({ redirect }),
|
||||
|
||||
clients: {},
|
||||
setClients: (clients: ClientList) => set({ clients }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* Allows getting client name (outside react)
|
||||
*/
|
||||
export function getClientName(): string | undefined {
|
||||
return useClientStore.getState().name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows updating current client name (outside react)
|
||||
*/
|
||||
export function setClientName(name: string): void {
|
||||
useClientStore.getState().setName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows updating current client name (outside react)
|
||||
*/
|
||||
export function setClientId(id: string): void {
|
||||
useClientStore.getState().setId(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows getting client name (outside react)
|
||||
*/
|
||||
export function getClientId(): string | undefined {
|
||||
return useClientStore.getState().id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows updating redirect (outside react)
|
||||
*/
|
||||
export function setClientRedirect(path: string): void {
|
||||
useClientStore.getState().setRedirect(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows setting clients (outside react)
|
||||
*/
|
||||
export function setClients(clients: ClientList): void {
|
||||
return useClientStore.getState().setClients(clients);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useStore } from 'zustand';
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
|
||||
interface SocketClientNameState {
|
||||
name?: string;
|
||||
setName: (newValue: string) => void;
|
||||
persistName: (newValue: string) => void;
|
||||
}
|
||||
|
||||
const clientNameKey = 'ontime-client-name';
|
||||
|
||||
function persistKeyToStorage(newValue: string) {
|
||||
localStorage.setItem(clientNameKey, newValue);
|
||||
}
|
||||
|
||||
export const socketClientName = createStore<SocketClientNameState>((set) => ({
|
||||
name: localStorage.getItem(clientNameKey) ?? undefined,
|
||||
setName: (newValue: string) => set(() => ({ name: newValue })),
|
||||
persistName: (newValue: string) =>
|
||||
set(() => {
|
||||
persistKeyToStorage(newValue);
|
||||
return { name: newValue };
|
||||
}),
|
||||
}));
|
||||
|
||||
export const useSocketClientName = () => useStore(socketClientName);
|
||||
@@ -1,21 +1,31 @@
|
||||
import { Log, RuntimeStore } from 'ontime-types';
|
||||
|
||||
import { isProduction, RUNTIME, websocketUrl } from '../api/constants';
|
||||
import { CLIENT_LIST, isProduction, RUNTIME, websocketUrl } from '../api/constants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
import { socketClientName } from '../stores/connectionName';
|
||||
import {
|
||||
getClientId,
|
||||
getClientName,
|
||||
setClientId,
|
||||
setClientName,
|
||||
setClientRedirect,
|
||||
setClients,
|
||||
} from '../stores/clientStore';
|
||||
import { addLog } from '../stores/logger';
|
||||
import { patchRuntime, runtimeStore } from '../stores/runtime';
|
||||
|
||||
export let websocket: WebSocket | null = null;
|
||||
let reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
const reconnectInterval = 1000;
|
||||
|
||||
export let shouldReconnect = true;
|
||||
export let hasConnected = false;
|
||||
export let reconnectAttempts = 0;
|
||||
|
||||
export const connectSocket = (preferredClientName?: string) => {
|
||||
export const connectSocket = () => {
|
||||
websocket = new WebSocket(websocketUrl);
|
||||
|
||||
const preferredClientName = getClientName();
|
||||
|
||||
websocket.onopen = () => {
|
||||
clearTimeout(reconnectTimeout as NodeJS.Timeout);
|
||||
hasConnected = true;
|
||||
@@ -24,6 +34,9 @@ export const connectSocket = (preferredClientName?: string) => {
|
||||
if (preferredClientName) {
|
||||
socketSendJson('set-client-name', preferredClientName);
|
||||
}
|
||||
|
||||
socketSendJson('set-client-type', 'ontime');
|
||||
socketSendJson('set-client-path', location.pathname);
|
||||
};
|
||||
|
||||
websocket.onclose = () => {
|
||||
@@ -54,10 +67,48 @@ export const connectSocket = (preferredClientName?: string) => {
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'client-name': {
|
||||
socketClientName.getState().setName(payload);
|
||||
case 'client-id': {
|
||||
if (typeof payload === 'string') {
|
||||
setClientId(payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-name': {
|
||||
if (typeof payload === 'string') {
|
||||
setClientName(payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-rename': {
|
||||
if (typeof payload === 'object') {
|
||||
const id = getClientId();
|
||||
if (payload.target && 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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'client-list': {
|
||||
setClients(payload);
|
||||
if (!isProduction) {
|
||||
ontimeQueryClient.setQueryData(CLIENT_LIST, payload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ontime-log': {
|
||||
addLog(payload as Log);
|
||||
break;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { ErrorBoundary } from '@sentry/react';
|
||||
import { useKeyDown } from '../../common/hooks/useKeyDown';
|
||||
|
||||
import AboutPanel from './panel/about-panel/AboutPanel';
|
||||
import ClientControlPanel from './panel/client-control-panel/ClientControlPanel';
|
||||
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
|
||||
import GeneralPanel from './panel/general-panel/GeneralPanel';
|
||||
import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||
import LogPanel from './panel/log-panel/LogPanel';
|
||||
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
|
||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
|
||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||
@@ -30,8 +31,9 @@ export default function AppSettings() {
|
||||
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
|
||||
{panel === 'sources' && <SourcesPanel />}
|
||||
{panel === 'integrations' && <IntegrationsPanel location={location} />}
|
||||
{panel === 'client_control' && <ClientControlPanel />}
|
||||
{panel === 'about' && <AboutPanel />}
|
||||
{panel === 'log' && <LogPanel />}
|
||||
{panel === 'network' && <NetworkLogPanel location={location} />}
|
||||
{panel === 'shutdown' && <ShutdownPanel />}
|
||||
</PanelContent>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -73,12 +73,12 @@ $inner-padding: 1rem;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
box-shadow: 0 1px $white-10;
|
||||
background-color: $gray-1350;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 400;
|
||||
color: $gray-400;
|
||||
background-color: $gray-1350;
|
||||
white-space: nowrap;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
gap: $element-spacing;
|
||||
}
|
||||
|
||||
.halfWidth {
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pathList {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.badgeList {
|
||||
width: 100%;
|
||||
* {
|
||||
margin-right: $element-spacing;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import ClientList from './ClientList';
|
||||
|
||||
export default function ClientControlPanel() {
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Manage clients</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<ClientList />
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useState } from 'react';
|
||||
import { Badge, Button, useDisclosure } from '@chakra-ui/react';
|
||||
|
||||
import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal';
|
||||
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
|
||||
import { setClientRemote } from '../../../../common/hooks/useSocket';
|
||||
import { useClientStore } from '../../../../common/stores/clientStore';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import style from './ClientControlPanel.module.scss';
|
||||
|
||||
export default function ClientList() {
|
||||
const id = useClientStore((store) => store.id);
|
||||
const clients = useClientStore((store) => store.clients);
|
||||
const { isOpen: isOpenRedirect, onOpen: onOpenRedirect, onClose: onCloseRedirect } = useDisclosure();
|
||||
const { isOpen: isOpenRename, onOpen: onOpenRename, onClose: onCloseRename } = useDisclosure();
|
||||
const { setIdentify } = setClientRemote;
|
||||
|
||||
const [targetId, setTargetId] = useState('');
|
||||
|
||||
const openRename = (targetId: string) => {
|
||||
setTargetId(targetId);
|
||||
onOpenRename();
|
||||
};
|
||||
|
||||
const openRedirect = (targetId: string) => {
|
||||
setTargetId(targetId);
|
||||
onOpenRedirect();
|
||||
};
|
||||
|
||||
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
|
||||
const otherClients = Object.entries(clients).filter(([_, { type }]) => type !== 'ontime');
|
||||
|
||||
const targetClient = clients[targetId];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpenRedirect && (
|
||||
<RedirectClientModal
|
||||
id={targetId}
|
||||
name={targetClient?.name}
|
||||
path={targetClient?.path}
|
||||
isOpen={isOpenRedirect}
|
||||
onClose={onCloseRedirect}
|
||||
/>
|
||||
)}
|
||||
{isOpenRename && (
|
||||
<RenameClientModal id={targetId} name={targetClient?.name} isOpen={isOpenRename} onClose={onCloseRename} />
|
||||
)}
|
||||
<Panel.Section>
|
||||
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className={style.fullWidth}>Client Name (Connection ID)</td>
|
||||
<td className={style.fullWidth}>Path</td>
|
||||
<td />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ontimeClients.map(([key, client]) => {
|
||||
const { identify, name, path } = client;
|
||||
const isCurrent = id === key;
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className={style.badgeList}>
|
||||
<Badge variant='outline' size='xs'>
|
||||
{key}
|
||||
</Badge>
|
||||
{isCurrent && (
|
||||
<Badge variant='outline' colorScheme='yellow' size='xs'>
|
||||
self
|
||||
</Badge>
|
||||
)}
|
||||
{name}
|
||||
</td>
|
||||
{isCurrent ? <td /> : <td className={style.pathList}>{path}</td>}
|
||||
<td className={style.actionButtons}>
|
||||
<Button
|
||||
size='xs'
|
||||
className={`${identify ? style.blink : ''}`}
|
||||
isDisabled={isCurrent}
|
||||
variant={identify ? 'ontime-filled' : 'ontime-subtle'}
|
||||
data-testid={isCurrent ? '' : 'not-self-identify'}
|
||||
onClick={() => {
|
||||
setIdentify({ target: key, identify: !identify });
|
||||
}}
|
||||
>
|
||||
Identify
|
||||
</Button>
|
||||
<Button
|
||||
size='xs'
|
||||
variant='ontime-subtle'
|
||||
data-testid={isCurrent ? '' : 'not-self-rename'}
|
||||
onClick={() => openRename(key)}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size='xs'
|
||||
variant='ontime-subtle'
|
||||
isDisabled={isCurrent}
|
||||
data-testid={isCurrent ? '' : 'not-self-redirect'}
|
||||
onClick={() => openRedirect(key)}
|
||||
>
|
||||
Redirect
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
</Panel.Section>
|
||||
<Panel.Divider />
|
||||
<Panel.Section>
|
||||
<Panel.Title>Other Clients ({otherClients.length})</Panel.Title>
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className={style.halfWidth}>Client Name (Connection ID)</td>
|
||||
<td className={style.halfWidth}>Client type</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{otherClients.map(([key, client]) => {
|
||||
const { name, type } = client;
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td className={style.badgeList}>
|
||||
<Badge variant='outline' size='sx'>
|
||||
{key}
|
||||
</Badge>
|
||||
{name}
|
||||
</td>
|
||||
<td>{type}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
</Panel.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -153,6 +153,7 @@ export default function GeneralPanelForm() {
|
||||
<option value='pt'>Portuguese</option>
|
||||
<option value='es'>Spanish</option>
|
||||
<option value='sv'>Swedish</option>
|
||||
<option value='pl'>Polish</option>
|
||||
</Select>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
|
||||
@@ -14,4 +14,6 @@ export const cycles: CycleLabel[] = [
|
||||
{ id: 5, label: 'Every second', value: 'onClock' },
|
||||
{ id: 5, label: 'On Timer Update', value: 'onUpdate' },
|
||||
{ id: 6, label: 'On Finish', value: 'onFinish' },
|
||||
{ id: 7, label: 'On Warning', value: 'onWarning' },
|
||||
{ id: 8, label: 'On Danger', value: 'onDanger' },
|
||||
];
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import LogExport from './LogExport';
|
||||
import InfoNif from './NetworkInterfaces';
|
||||
|
||||
export default function LogPanel() {
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Log</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||
</Panel.Section>
|
||||
<InfoNif />
|
||||
<LogExport />
|
||||
</>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { handleLinks } from '../../../../common/utils/linkUtils';
|
||||
import Log from '../../../log/Log';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import style from './LogExport.module.scss';
|
||||
import style from './NetworkLogExport.module.scss';
|
||||
|
||||
export default function LogExport() {
|
||||
const extract = (event: MouseEvent) => {
|
||||
@@ -0,0 +1,28 @@
|
||||
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
|
||||
import { PanelBaseProps } from '../../settingsStore';
|
||||
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import InfoNif from './NetworkInterfaces';
|
||||
import LogExport from './NetworkLogExport';
|
||||
|
||||
export default function NetworkLogPanel({ location }: PanelBaseProps) {
|
||||
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
|
||||
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Network</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
|
||||
</Panel.Section>
|
||||
<InfoNif />
|
||||
<div ref={logRef}>
|
||||
<LogExport />
|
||||
</div>
|
||||
<div ref={clientsRef}>
|
||||
<ClientControlPanel />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,21 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
||||
{ id: 'integrations__http', label: 'HTTP settings' },
|
||||
],
|
||||
},
|
||||
{ id: 'log', label: 'Log', split: true },
|
||||
{
|
||||
id: 'network',
|
||||
label: 'Network',
|
||||
split: true,
|
||||
secondary: [
|
||||
{
|
||||
id: 'network__log',
|
||||
label: 'Application log',
|
||||
},
|
||||
{
|
||||
id: 'network__clients',
|
||||
label: 'Manage clients',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'about',
|
||||
label: 'About',
|
||||
|
||||
@@ -47,11 +47,13 @@ $table-header-font-size: calc(1rem - 3px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: $gray-1300;
|
||||
|
||||
font-size: $table-header-font-size;
|
||||
color: $gray-700;
|
||||
font-weight: 400;
|
||||
|
||||
th {
|
||||
background-color: $gray-1300;
|
||||
}
|
||||
}
|
||||
|
||||
.eventRow {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import Color from 'color';
|
||||
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeRundown, OntimeRundownEntry } from 'ontime-types';
|
||||
@@ -13,7 +12,7 @@ import DelayRow from './cuesheet-table-elements/DelayRow';
|
||||
import EventRow from './cuesheet-table-elements/EventRow';
|
||||
import CuesheetTableSettings from './cuesheet-table-settings/CuesheetTableSettings';
|
||||
import { useCuesheetSettings } from './store/CuesheetSettings';
|
||||
import { initialColumnOrder } from './cuesheetCols';
|
||||
import useColumnManager from './useColumnManager';
|
||||
|
||||
import style from './Cuesheet.module.scss';
|
||||
|
||||
@@ -27,23 +26,20 @@ interface CuesheetProps {
|
||||
export default function Cuesheet({ data, columns, handleUpdate, selectedId }: CuesheetProps) {
|
||||
const { followSelected, showSettings, showDelayBlock, showPrevious, showIndexColumn } = useCuesheetSettings();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||
key: 'table-order',
|
||||
defaultValue: initialColumnOrder,
|
||||
});
|
||||
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
|
||||
const {
|
||||
columnVisibility,
|
||||
columnOrder,
|
||||
columnSizing,
|
||||
resetColumnOrder,
|
||||
setColumnVisibility,
|
||||
saveColumnOrder,
|
||||
setColumnSizing,
|
||||
} = useColumnManager(columns);
|
||||
|
||||
const selectedRef = useRef<HTMLTableRowElement | null>(null);
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
useFollowComponent({ followRef: selectedRef, scrollRef: tableContainerRef, doFollow: followSelected });
|
||||
|
||||
// keep column order in sync with columns
|
||||
useEffect(() => {
|
||||
const order = columns.map((col) => col.id as string);
|
||||
saveColumnOrder(order);
|
||||
}, [columns, saveColumnOrder]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@@ -61,10 +57,6 @@ export default function Cuesheet({ data, columns, handleUpdate, selectedId }: Cu
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const resetColumnOrder = () => {
|
||||
saveColumnOrder(initialColumnOrder);
|
||||
};
|
||||
|
||||
const setAllVisible = () => {
|
||||
table.toggleAllColumnsVisible(true);
|
||||
};
|
||||
|
||||
@@ -117,5 +117,3 @@ export function makeCuesheetColumns(customFields: CustomFields): ColumnDef<Ontim
|
||||
...dynamicCustomFields,
|
||||
];
|
||||
}
|
||||
|
||||
export const initialColumnOrder: string[] = makeCuesheetColumns({}).map((column) => column.id as string);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { OntimeRundownEntry } from 'ontime-types';
|
||||
|
||||
export default function useColumnManager(columns: ColumnDef<OntimeRundownEntry>[]) {
|
||||
const [columnVisibility, setColumnVisibility] = useLocalStorage({ key: 'table-hidden', defaultValue: {} });
|
||||
const [columnOrder, saveColumnOrder] = useLocalStorage<string[]>({
|
||||
key: 'table-order',
|
||||
defaultValue: columns.map((col) => col.id as string),
|
||||
});
|
||||
const [columnSizing, setColumnSizing] = useLocalStorage({ key: 'table-sizes', defaultValue: {} });
|
||||
|
||||
// if the columns change, we update the dataset
|
||||
useEffect(() => {
|
||||
let shouldReplace = false;
|
||||
const newColumns: string[] = [];
|
||||
|
||||
// iterate through columns to see if there are new ids
|
||||
columns.forEach((column) => {
|
||||
const columnnId = column.id as string;
|
||||
if (!shouldReplace && !columnOrder.includes(columnnId)) {
|
||||
shouldReplace = true;
|
||||
}
|
||||
newColumns.push(columnnId);
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
saveColumnOrder(newColumns);
|
||||
}
|
||||
}, [columnOrder, columns, saveColumnOrder]);
|
||||
|
||||
const resetColumnOrder = useCallback(() => {
|
||||
saveColumnOrder(columns.map((col) => col.id as string));
|
||||
}, [columns, saveColumnOrder]);
|
||||
|
||||
return {
|
||||
columnVisibility,
|
||||
columnOrder,
|
||||
columnSizing,
|
||||
resetColumnOrder,
|
||||
setColumnVisibility,
|
||||
saveColumnOrder,
|
||||
setColumnSizing,
|
||||
};
|
||||
}
|
||||
@@ -37,7 +37,6 @@ $bg-container-onlight: $gray-100;
|
||||
$box-shadow-l1: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
|
||||
$box-shadow-l2: rgba(0, 0, 0, 0.15) 0 3px 3px 0;
|
||||
$box-shadow-right: rgba(0, 0, 0, 0.15) 3px 0 3px 0;
|
||||
$large-bottom-drawer-shadow: rgba(0, 0, 0, 0.35) 0 3px 6px 6px;
|
||||
$large-top-drawer-shadow: rgba(0, 0, 0, 0.35) 0 1px 6px 3px;
|
||||
|
||||
$modal-note-color: $gray-700;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { langEs } from './languages/es';
|
||||
import { langFr } from './languages/fr';
|
||||
import { langIt } from './languages/it';
|
||||
import { langNo } from './languages/no';
|
||||
import { langPl } from './languages/pl';
|
||||
import { langPt } from './languages/pt';
|
||||
import { langSv } from './languages/sv';
|
||||
|
||||
@@ -20,6 +21,7 @@ const translationsList = {
|
||||
no: langNo,
|
||||
pt: langPt,
|
||||
sv: langSv,
|
||||
pl: langPl,
|
||||
};
|
||||
|
||||
interface TranslationContextValue {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { TranslationObject } from './en';
|
||||
|
||||
export const langPl: TranslationObject = {
|
||||
'common.end_time': 'Czas zakończenia',
|
||||
'common.expected_finish': 'Zakładany czas zakończenia',
|
||||
'common.minutes': 'min',
|
||||
'common.now': 'Teraz',
|
||||
'common.next': 'Następnie',
|
||||
'common.public_message': 'Wiadomość publiczna',
|
||||
'common.start_time': 'Czas rozpoczęcia',
|
||||
'common.stage_timer': 'Timer Scena',
|
||||
'common.started_at': 'Rozpoczęte o',
|
||||
'common.time_now': 'Aktualny czas',
|
||||
'countdown.ended': 'Zakończone o',
|
||||
'countdown.running': 'Trwa',
|
||||
'countdown.select_event': 'Wybierz event który chcesz śledzić',
|
||||
'countdown.to_start': 'Do rozpoczęcia',
|
||||
'countdown.waiting': 'Oczekiwanie na start',
|
||||
'countdown.overtime': 'ponad czasem',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.2",
|
||||
"author": "Carlos Valente",
|
||||
"description": "Time keeping for live events",
|
||||
"repository": "https://github.com/cpvalente/ontime",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "ontime-server",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.2",
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@googleapis/sheets": "^5.0.5",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* Payload: adds necessary payload for the request to be completed
|
||||
*/
|
||||
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
import { Client, LogOrigin } from 'ontime-types';
|
||||
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
import type { Server } from 'http';
|
||||
@@ -24,6 +24,7 @@ import { IAdapter } from './IAdapter.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||
import { generateId } from 'ontime-utils';
|
||||
|
||||
let instance: SocketServer | null = null;
|
||||
|
||||
@@ -31,7 +32,7 @@ export class SocketServer implements IAdapter {
|
||||
private readonly MAX_PAYLOAD = 1024 * 256; // 256Kb
|
||||
|
||||
private wss: WebSocketServer | null;
|
||||
private readonly clientIds: Set<string>;
|
||||
private readonly clients: Map<string, Client>;
|
||||
|
||||
constructor() {
|
||||
if (instance) {
|
||||
@@ -40,7 +41,7 @@ export class SocketServer implements IAdapter {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias -- this logic is used to ensure singleton
|
||||
instance = this;
|
||||
this.clientIds = new Set<string>();
|
||||
this.clients = new Map<string, Client>();
|
||||
this.wss = null;
|
||||
}
|
||||
|
||||
@@ -48,9 +49,32 @@ export class SocketServer implements IAdapter {
|
||||
this.wss = new WebSocketServer({ path: '/ws', server, maxPayload: this.MAX_PAYLOAD });
|
||||
|
||||
this.wss.on('connection', (ws) => {
|
||||
let clientId = getRandomName();
|
||||
this.clientIds.add(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clientIds.size} Connections with new: ${clientId}`);
|
||||
const clientId = generateId();
|
||||
|
||||
this.clients.set(clientId, {
|
||||
type: 'unknown',
|
||||
identify: false,
|
||||
name: getRandomName(),
|
||||
path: '',
|
||||
});
|
||||
|
||||
logger.info(LogOrigin.Client, `${this.clients.size} Connections with new: ${clientId}`);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-id',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
|
||||
this.sendClientList();
|
||||
|
||||
// send store payload on connect
|
||||
ws.send(
|
||||
@@ -60,18 +84,12 @@ export class SocketServer implements IAdapter {
|
||||
}),
|
||||
);
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
|
||||
ws.on('error', console.error);
|
||||
|
||||
ws.on('close', () => {
|
||||
this.clientIds.delete(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clientIds.size} Connections with disconnected: ${clientId}`);
|
||||
this.clients.delete(clientId);
|
||||
logger.info(LogOrigin.Client, `${this.clients.size} Connections with disconnected: ${clientId}`);
|
||||
this.sendClientList();
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
@@ -84,26 +102,44 @@ export class SocketServer implements IAdapter {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-type') {
|
||||
if (payload && typeof payload == 'string') {
|
||||
const previousData = this.clients.get(clientId);
|
||||
this.clients.set(clientId, { ...previousData, type: payload });
|
||||
}
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-path') {
|
||||
if (payload && typeof payload == 'string') {
|
||||
const previousData = this.clients.get(clientId);
|
||||
previousData.path = payload;
|
||||
this.clients.set(clientId, previousData);
|
||||
}
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'set-client-name') {
|
||||
if (payload) {
|
||||
const previousName = clientId;
|
||||
clientId = payload;
|
||||
this.clientIds.delete(previousName);
|
||||
this.clientIds.add(clientId);
|
||||
logger.info(LogOrigin.Client, `Client ${previousName} renamed to ${clientId}`);
|
||||
const previousData = this.clients.get(clientId);
|
||||
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${payload}`);
|
||||
this.clients.set(clientId, { ...previousData, name: payload });
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: this.clients.get(clientId).name,
|
||||
}),
|
||||
);
|
||||
}
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: 'client-name',
|
||||
payload: clientId,
|
||||
}),
|
||||
);
|
||||
this.sendClientList();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,17 +171,58 @@ export class SocketServer implements IAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
// message is any serializable value
|
||||
sendAsJson(message: unknown) {
|
||||
this.wss?.clients.forEach((client) => {
|
||||
try {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
private sendClientList(): void {
|
||||
const payload = Object.fromEntries(this.clients.entries());
|
||||
this.sendAsJson({ type: 'client-list', payload });
|
||||
}
|
||||
|
||||
public getClientList(): string[] {
|
||||
return Array.from(this.clients.keys());
|
||||
}
|
||||
|
||||
public renameClient(target: string, name: string) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
logger.info(LogOrigin.Client, `Client ${previousData.name} renamed to ${name}`);
|
||||
this.clients.set(target, { ...previousData, name });
|
||||
this.sendAsJson({
|
||||
type: 'client-rename',
|
||||
payload: { name, target },
|
||||
});
|
||||
this.sendClientList();
|
||||
}
|
||||
|
||||
public redirectClient(target: string, path: string) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
this.sendAsJson({ type: 'client-redirect', payload: { target, path } });
|
||||
}
|
||||
|
||||
public identifyClient(target: string, identify: boolean) {
|
||||
const previousData = this.clients.get(target);
|
||||
if (!previousData) {
|
||||
throw new Error(`Client "${target}" not found`);
|
||||
}
|
||||
this.clients.set(target, { ...previousData, identify });
|
||||
this.sendClientList();
|
||||
}
|
||||
|
||||
// message is any serializable value
|
||||
public sendAsJson(message: unknown) {
|
||||
try {
|
||||
const stringifiedMessage = JSON.stringify(message);
|
||||
this.wss?.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(stringifiedMessage);
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
/** We do not handle this error */
|
||||
}
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { eventStore } from '../stores/EventStore.js';
|
||||
import * as assert from '../utils/assert.js';
|
||||
import { isEmptyObject } from '../utils/parserUtils.js';
|
||||
import { parseProperty, updateEvent } from './integration.utils.js';
|
||||
import { socket } from '../adapters/WebsocketAdapter.js';
|
||||
import { throttle } from '../utils/throttle.js';
|
||||
import { willCauseRegeneration } from '../services/rundown-service/rundownCacheUtils.js';
|
||||
|
||||
@@ -240,6 +241,33 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
}
|
||||
throw new Error('No matching method provided');
|
||||
},
|
||||
/* Client */
|
||||
client: (payload) => {
|
||||
assert.isObject(payload);
|
||||
if (!('target' in payload) || typeof payload.target != 'string') {
|
||||
throw new Error('No or invalid client target');
|
||||
}
|
||||
|
||||
if ('rename' in payload && typeof payload.rename == 'string') {
|
||||
const { target, rename } = payload;
|
||||
socket.renameClient(target, rename);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
if ('redirect' in payload && typeof payload.redirect == 'string') {
|
||||
const { target, redirect } = payload;
|
||||
socket.redirectClient(target, redirect);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
if ('identify' in payload && typeof payload.identify == 'boolean') {
|
||||
const { target, identify } = payload;
|
||||
socket.identifyClient(target, identify);
|
||||
return { payload: 'success' };
|
||||
}
|
||||
|
||||
throw new Error('No matching method provided');
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -208,8 +208,9 @@ export const startServer = async (
|
||||
expressServer.listen(serverPort, '0.0.0.0', () => {
|
||||
const nif = getNetworkInterfaces();
|
||||
consoleSuccess(`Local: http://localhost:${serverPort}/editor`);
|
||||
for (const key of Object.keys(nif)) {
|
||||
consoleSuccess(`Network: http://${nif[key].address}:${serverPort}/editor`);
|
||||
for (const key in nif) {
|
||||
const address = nif[key].address;
|
||||
consoleSuccess(`Network: http://${address}:${serverPort}/editor`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { consoleSubdued, consoleRed } from '../utils/console.js';
|
||||
|
||||
class Logger {
|
||||
private queue: Log[];
|
||||
private escalateErrorFn: (error: string) => void | null;
|
||||
private escalateErrorFn: ((error: string) => void) | null;
|
||||
private canLog = false;
|
||||
|
||||
constructor() {
|
||||
@@ -20,7 +20,7 @@ class Logger {
|
||||
/**
|
||||
* Enabling setup logger after init
|
||||
*/
|
||||
init(escalateErrorFn: (error: string) => void) {
|
||||
init(escalateErrorFn?: (error: string) => void) {
|
||||
// flush logs from queue
|
||||
this.queue.forEach((log) => {
|
||||
this._push(log);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseModel, OntimeRundown, Settings, URLPreset } from 'ontime-types';
|
||||
import { DatabaseModel, OntimeRundown, Settings, URLPreset, ViewSettings } from 'ontime-types';
|
||||
import { safeMerge } from '../DataProvider.utils.js';
|
||||
|
||||
describe('safeMerge', () => {
|
||||
@@ -49,7 +49,7 @@ describe('safeMerge', () => {
|
||||
} as DatabaseModel;
|
||||
|
||||
it('returns existing data if new data is not provided', () => {
|
||||
const mergedData = safeMerge(existing, undefined);
|
||||
const mergedData = safeMerge(existing, {});
|
||||
expect(mergedData).toEqual(existing);
|
||||
});
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('safeMerge', () => {
|
||||
rundown: [],
|
||||
project: {
|
||||
title: '',
|
||||
description: '',
|
||||
publicUrl: '',
|
||||
publicInfo: '',
|
||||
backstageUrl: '',
|
||||
@@ -153,8 +154,9 @@ describe('safeMerge', () => {
|
||||
viewSettings: {
|
||||
overrideStyles: false,
|
||||
endMessage: '',
|
||||
},
|
||||
} as ViewSettings,
|
||||
urlPresets: [],
|
||||
customFields: {},
|
||||
osc: {
|
||||
portIn: 8888,
|
||||
portOut: 9999,
|
||||
@@ -163,6 +165,10 @@ describe('safeMerge', () => {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
http: {
|
||||
enabledOut: false,
|
||||
subscriptions: [],
|
||||
},
|
||||
} as DatabaseModel;
|
||||
|
||||
const newData = {
|
||||
|
||||
@@ -361,7 +361,7 @@ describe('remove() mutation', () => {
|
||||
];
|
||||
const { newRundown } = remove({ eventIds: ['1', '2', '3'], persistedRundown: testRundown });
|
||||
expect(newRundown.length).toBe(3);
|
||||
expect(newRundown.at(0).id).toBe('4');
|
||||
expect(newRundown.at(0)?.id).toBe('4');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -885,7 +885,7 @@ describe('custom fields', () => {
|
||||
};
|
||||
|
||||
const customField = await editCustomField('sound', { label: 'Sound', type: 'string', colour: 'green' });
|
||||
expect(customFieldChangelog).toStrictEqual({});
|
||||
expect(customFieldChangelog).toStrictEqual(new Map());
|
||||
|
||||
expect(customField).toStrictEqual(expected);
|
||||
});
|
||||
@@ -931,9 +931,15 @@ describe('custom fields', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// We need to flush all scheduled tasks for the generate function to settle
|
||||
vi.useFakeTimers();
|
||||
const customField = await editCustomField('video', { label: 'AV', type: 'string', colour: 'red' });
|
||||
expect(customField).toStrictEqual(expectedAfter);
|
||||
expect(customFieldChangelog).toStrictEqual({ video: 'av' });
|
||||
expect(customFieldChangelog).toStrictEqual(new Map([['video', 'av']]));
|
||||
await editCustomField('av', { label: 'video' });
|
||||
vi.runAllTimers();
|
||||
expect(customFieldChangelog).toStrictEqual(new Map());
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -945,8 +951,8 @@ describe('custom fields', () => {
|
||||
type: 'string',
|
||||
colour: 'blue',
|
||||
},
|
||||
av: {
|
||||
label: 'AV',
|
||||
video: {
|
||||
label: 'video',
|
||||
type: 'string',
|
||||
colour: 'red',
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('handleCustomField()', () => {
|
||||
label: 'sound',
|
||||
},
|
||||
} as CustomFields;
|
||||
const customFieldChangelog = {};
|
||||
const customFieldChangelog = new Map<string, string>();
|
||||
|
||||
// @ts-expect-error -- partial event for testing
|
||||
const event: OntimeEvent = {
|
||||
@@ -132,9 +132,7 @@ describe('handleCustomField()', () => {
|
||||
},
|
||||
} as CustomFields;
|
||||
|
||||
const customFieldChangelog = {
|
||||
sound: 'video',
|
||||
};
|
||||
const customFieldChangelog = new Map([['sound', 'video']]);
|
||||
|
||||
// @ts-expect-error -- partial event for testing
|
||||
const event: OntimeEvent = {
|
||||
@@ -170,9 +168,7 @@ describe('handleCustomField()', () => {
|
||||
},
|
||||
} as CustomFields;
|
||||
|
||||
const customFieldChangelog = {
|
||||
field1: 'newField1',
|
||||
};
|
||||
const customFieldChangelog = new Map([['field1', 'newField1']]);
|
||||
|
||||
// @ts-expect-error -- partial event for testing
|
||||
const mutableEvent: OntimeEvent = {
|
||||
|
||||
@@ -47,7 +47,7 @@ let links: Record<EventID, EventID> = {};
|
||||
* lighting: lx
|
||||
* }
|
||||
*/
|
||||
export const customFieldChangelog = {};
|
||||
export const customFieldChangelog = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Keep track of which custom fields are used.
|
||||
@@ -141,6 +141,7 @@ export function generate(
|
||||
}
|
||||
|
||||
isStale = false;
|
||||
customFieldChangelog.clear();
|
||||
totalDelay = accumulatedDelay;
|
||||
if (lastEnd !== null && firstStart !== null) {
|
||||
totalDuration = getTotalDuration(firstStart, lastEnd, daySpan);
|
||||
@@ -411,6 +412,7 @@ function invalidateIfUsed(label: CustomFieldLabel) {
|
||||
// schedule a non priority cache update
|
||||
setImmediate(() => {
|
||||
generate();
|
||||
DataProvider.setRundown(persistedRundown);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -459,7 +461,7 @@ export const editCustomField = async (key: string, newField: Partial<CustomField
|
||||
}
|
||||
|
||||
const existingField = persistedCustomFields[key];
|
||||
if (existingField.type !== newField.type) {
|
||||
if (newField.type !== undefined && existingField.type !== newField.type) {
|
||||
throw new Error('Change of field type is not allowed');
|
||||
}
|
||||
|
||||
@@ -468,7 +470,7 @@ export const editCustomField = async (key: string, newField: Partial<CustomField
|
||||
|
||||
if (key !== newKey) {
|
||||
delete persistedCustomFields[key];
|
||||
customFieldChangelog[key] = newKey;
|
||||
customFieldChangelog.set(key, newKey);
|
||||
}
|
||||
|
||||
scheduleCustomFieldPersist(persistedCustomFields);
|
||||
|
||||
@@ -73,15 +73,15 @@ export function addToCustomAssignment(
|
||||
*/
|
||||
export function handleCustomField(
|
||||
customFields: CustomFields,
|
||||
customFieldChangelog: Record<string, string>,
|
||||
customFieldChangelog: Map<string, string>,
|
||||
mutableEvent: OntimeEvent,
|
||||
assignedCustomFields: Record<string, string[]>,
|
||||
) {
|
||||
for (const field in mutableEvent.custom) {
|
||||
// rename the property if it is in the changelog
|
||||
if (field in customFieldChangelog) {
|
||||
if (customFieldChangelog.has(field)) {
|
||||
const oldData = mutableEvent.custom[field];
|
||||
const newLabel = customFieldChangelog[field];
|
||||
const newLabel = customFieldChangelog.get(field);
|
||||
|
||||
mutableEvent.custom[newLabel] = oldData;
|
||||
delete mutableEvent.custom[field];
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { EndAction, LogOrigin, MaybeNumber, OntimeEvent, Playback, RuntimeStore, TimerLifeCycle } from 'ontime-types';
|
||||
import {
|
||||
EndAction,
|
||||
LogOrigin,
|
||||
MaybeNumber,
|
||||
OntimeEvent,
|
||||
Playback,
|
||||
RuntimeStore,
|
||||
TimerLifeCycle,
|
||||
TimerPhase,
|
||||
} from 'ontime-types';
|
||||
import { millisToString, validatePlayback } from 'ontime-utils';
|
||||
|
||||
import { deepEqual } from 'fast-equals';
|
||||
@@ -76,7 +85,7 @@ class RuntimeService {
|
||||
integrationService.dispatch(TimerLifeCycle.onUpdate);
|
||||
});
|
||||
|
||||
this.lastIntegrationTimerValue = newState.timer.current;
|
||||
this.lastIntegrationTimerValue = newState.timer.current ?? -1;
|
||||
}
|
||||
|
||||
const shouldUpdateClock = getShouldClockUpdate(this.lastIntegrationClockUpdate, newState.clock);
|
||||
@@ -93,6 +102,25 @@ class RuntimeService {
|
||||
const rundown = getPlayableEvents();
|
||||
this.eventTimer.roll(rundown);
|
||||
}
|
||||
|
||||
const timerPhaseChanged = RuntimeService.previousState.timer?.phase !== newState.timer.phase;
|
||||
|
||||
if (timerPhaseChanged) {
|
||||
switch (newState.timer.phase) {
|
||||
case TimerPhase.Warning:
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onWarning);
|
||||
});
|
||||
break;
|
||||
case TimerPhase.Danger:
|
||||
process.nextTick(() => {
|
||||
integrationService.dispatch(TimerLifeCycle.onDanger);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** delay initialisation until we have a restore point */
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { AuthenticationStatus, CustomFields, LogOrigin, MaybeString, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap, getErrorMessage } from 'ontime-utils';
|
||||
|
||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||
import { Credentials, OAuth2Client } from 'google-auth-library';
|
||||
@@ -13,7 +14,6 @@ import got from 'got';
|
||||
import { resolveSheetsDirectory } from '../../setup/index.js';
|
||||
import { ensureDirectory } from '../../utils/fileManagement.js';
|
||||
import { cellRequestFromEvent, type ClientSecret, getA1Notation, validateClientSecret } from './sheetUtils.js';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
import { parseExcel } from '../../utils/parser.js';
|
||||
import { logger } from '../../classes/Logger.js';
|
||||
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
|
||||
@@ -208,7 +208,8 @@ async function verifySheet(
|
||||
});
|
||||
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to verify sheet: ${error.message}`);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
throw new Error(`Failed to verify sheet: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('cellRequestFromEvent()', () => {
|
||||
timeDanger: { row: 1, col: 41 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(5)?.userEnteredValue?.stringValue).toStrictEqual(event.note);
|
||||
});
|
||||
|
||||
test('number to timer', () => {
|
||||
@@ -103,9 +103,10 @@ describe('cellRequestFromEvent()', () => {
|
||||
timeWarning: { row: 1, col: 40 },
|
||||
timeDanger: { row: 1, col: 41 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata).updateCells.rows[0].values[10].userEnteredValue
|
||||
.stringValue;
|
||||
expect(result).toStrictEqual(millisToString(event.duration));
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(10)?.userEnteredValue?.stringValue).toStrictEqual(
|
||||
millisToString(event.duration),
|
||||
);
|
||||
});
|
||||
|
||||
test('boolean to TRUE', () => {
|
||||
@@ -149,8 +150,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
timeDanger: { row: 1, col: 41 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[11].userEnteredValue.boolValue).toStrictEqual(true);
|
||||
expect(result.updateCells.rows[0].values[12].userEnteredValue.boolValue).toStrictEqual(false);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(11)?.userEnteredValue?.boolValue).toStrictEqual(true);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(12)?.userEnteredValue?.boolValue).toStrictEqual(false);
|
||||
});
|
||||
|
||||
test('spacing in metadata', () => {
|
||||
@@ -180,8 +181,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
title: { row: 1, col: 6 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[6].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(0)?.userEnteredValue?.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(6)?.userEnteredValue?.stringValue).toStrictEqual(event.title);
|
||||
});
|
||||
|
||||
test('metadata offset from zero', () => {
|
||||
@@ -212,8 +213,8 @@ describe('cellRequestFromEvent()', () => {
|
||||
user0: { row: 1, col: 16 },
|
||||
};
|
||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells.rows[0].values[1].userEnteredValue.stringValue).toStrictEqual(event.title);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(0)?.userEnteredValue?.stringValue).toStrictEqual(event.cue);
|
||||
expect(result.updateCells?.rows?.at(0)?.values?.at(1)?.userEnteredValue?.stringValue).toStrictEqual(event.title);
|
||||
});
|
||||
|
||||
test('sheet setup', () => {
|
||||
@@ -243,10 +244,10 @@ describe('cellRequestFromEvent()', () => {
|
||||
title: { row: 10, col: 6 },
|
||||
};
|
||||
const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||
expect(result1.updateCells?.start?.sheetId).toStrictEqual(1234);
|
||||
const result2 = cellRequestFromEvent(event, 10, 1234, metadata);
|
||||
expect(result2.updateCells.start.rowIndex).toStrictEqual(21);
|
||||
expect(result2.updateCells.start.columnIndex).toStrictEqual(5);
|
||||
expect(result2.updateCells.fields).toStrictEqual('userEnteredValue');
|
||||
expect(result2.updateCells?.start?.rowIndex).toStrictEqual(21);
|
||||
expect(result2.updateCells?.start?.columnIndex).toStrictEqual(5);
|
||||
expect(result2.updateCells?.fields).toStrictEqual('userEnteredValue');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,6 +160,10 @@ describe('mutation on runtimeState', () => {
|
||||
start();
|
||||
newState = getState();
|
||||
const firstStart = newState.clock;
|
||||
if (newState.runtime.offset === null) {
|
||||
throw new Error('Value cannot be null at this stage');
|
||||
}
|
||||
|
||||
expect(newState.runtime.actualStart).toBe(newState.clock);
|
||||
expect(newState.runtime.offset).toBe(event1.timeStart - newState.clock);
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset);
|
||||
@@ -167,7 +171,11 @@ describe('mutation on runtimeState', () => {
|
||||
// 3. Next event
|
||||
load(event2, [event1, event2]);
|
||||
start();
|
||||
|
||||
newState = getState();
|
||||
if (newState.runtime.actualStart === null || newState.runtime.offset === null) {
|
||||
throw new Error('Value cannot be null at this stage');
|
||||
}
|
||||
|
||||
// there is a case where the calculation time overflows the millisecond which makes
|
||||
// tests fail
|
||||
@@ -182,6 +190,10 @@ describe('mutation on runtimeState', () => {
|
||||
// 4. Add time
|
||||
addTime(10);
|
||||
newState = getState();
|
||||
if (newState.runtime.offset === null) {
|
||||
throw new Error('Value cannot be null at this stage');
|
||||
}
|
||||
|
||||
expect(newState.runtime.offset).toBe(delayBefore - 10);
|
||||
expect(newState.runtime.expectedEnd).toBe(event2.timeEnd - newState.runtime.offset);
|
||||
|
||||
|
||||
@@ -645,6 +645,8 @@ describe('test import of v2 datamodel', () => {
|
||||
onStop: [],
|
||||
onUpdate: [],
|
||||
onFinish: [],
|
||||
onWarning: [],
|
||||
onDanger: [],
|
||||
},
|
||||
},
|
||||
http: {
|
||||
|
||||
@@ -33,6 +33,8 @@ describe('sanitiseOscSubscriptions()', () => {
|
||||
{ id: '4', cycle: 'onStop', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', address: '/test', payload: 'test', enabled: true },
|
||||
{ id: '6', cycle: 'onFinish', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '7', cycle: 'onWarning', address: '/test', payload: 'test', enabled: false },
|
||||
{ id: '8', cycle: 'onDanger', address: '/test', payload: 'test', enabled: false },
|
||||
];
|
||||
const sanitationResult = sanitiseOscSubscriptions(oscSubscriptions);
|
||||
expect(sanitationResult).toStrictEqual(oscSubscriptions);
|
||||
@@ -69,6 +71,8 @@ describe('sanitiseHttpSubscriptions()', () => {
|
||||
{ id: '4', cycle: 'onStop', message: 'http://test', enabled: false },
|
||||
{ id: '5', cycle: 'onUpdate', message: 'http://test', enabled: true },
|
||||
{ id: '6', cycle: 'onFinish', message: 'http://test', enabled: false },
|
||||
{ id: '7', cycle: 'onWarning', message: 'http://test', enabled: false },
|
||||
{ id: '8', cycle: 'onDanger', message: 'http://test', enabled: false },
|
||||
];
|
||||
const sanitationResult = sanitiseHttpSubscriptions(httpSubscription);
|
||||
expect(sanitationResult).toStrictEqual(httpSubscription);
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('mergeObject()', () => {
|
||||
test('it handles falsy values', () => {
|
||||
const a = {
|
||||
first: 'yes',
|
||||
second: 'yes',
|
||||
second: 'yes' as string | null,
|
||||
third: 'yes',
|
||||
};
|
||||
const b = {
|
||||
|
||||
@@ -9,7 +9,11 @@ export function getNetworkInterfaces(): { name: string; address: string }[] {
|
||||
const results: { name: string; address: string }[] = [];
|
||||
|
||||
for (const name of Object.keys(nets)) {
|
||||
for (const net of nets[name]) {
|
||||
const netObjects = nets[name];
|
||||
if (!netObjects) {
|
||||
continue;
|
||||
}
|
||||
for (const net of netObjects) {
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
results.push({
|
||||
|
||||
@@ -53,7 +53,7 @@ export function mergeObject<T extends object>(a: T, b: Partial<T>): T {
|
||||
}
|
||||
|
||||
if (typeof bValue === 'object' && bValue !== null && typeof aValue === 'object' && aValue !== null) {
|
||||
// @ts-expect-error -- library side, ignore for now
|
||||
// @ts-expect-error -- not sure how to type this
|
||||
merged[key] = deepmerge(aValue, bValue);
|
||||
} else if (bValue !== undefined) {
|
||||
merged[key] = bValue;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('redirect', async ({ context }) => {
|
||||
const controllerPage = await context.newPage();
|
||||
const remotePage = await context.newPage();
|
||||
|
||||
await controllerPage.goto('http://localhost:4001/editor?settings=network__clients');
|
||||
await remotePage.goto('http://localhost:4001/timer');
|
||||
|
||||
await controllerPage.getByTestId('not-self-redirect').click();
|
||||
await controllerPage.getByPlaceholder('minimal?key=0000ffff').click();
|
||||
await controllerPage.getByPlaceholder('minimal?key=0000ffff').fill('clock');
|
||||
await controllerPage.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await expect(remotePage.getByTestId('clock-view')).toBeVisible();
|
||||
});
|
||||
|
||||
test('identify', async ({ context }) => {
|
||||
const controllerPage = await context.newPage();
|
||||
const remotePage = await context.newPage();
|
||||
|
||||
await controllerPage.goto('http://localhost:4001/editor?settings=network__clients');
|
||||
await remotePage.goto('http://localhost:4001/timer');
|
||||
|
||||
await controllerPage.getByTestId('not-self-identify').click();
|
||||
|
||||
await expect(remotePage.getByTestId('identify-overlay')).toBeVisible();
|
||||
});
|
||||
|
||||
test('rename', async ({ context }) => {
|
||||
const controllerPage = await context.newPage();
|
||||
const remotePage = await context.newPage();
|
||||
|
||||
await controllerPage.goto('http://localhost:4001/editor?settings=network__clients');
|
||||
await remotePage.goto('http://localhost:4001/timer');
|
||||
|
||||
await controllerPage.getByTestId('not-self-rename').click();
|
||||
await controllerPage.getByPlaceholder('new name').click();
|
||||
await controllerPage.getByPlaceholder('new name').fill('test');
|
||||
await controllerPage.getByRole('button', { name: 'Submit' }).click();
|
||||
await controllerPage.getByTestId('not-self-identify').click();
|
||||
|
||||
await expect(remotePage.getByTestId('identify-overlay')).toContainText('test');
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ontime",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.2",
|
||||
"description": "Time keeping for live events",
|
||||
"keywords": [
|
||||
"ontime",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export type ClientType = 'unknown' | 'ontime' | string;
|
||||
|
||||
export type Client = {
|
||||
name: string;
|
||||
type: ClientType;
|
||||
identify: boolean;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type ClientList = Record<string, Client>;
|
||||
@@ -6,6 +6,8 @@ export enum TimerLifeCycle {
|
||||
onClock = 'onClock',
|
||||
onUpdate = 'onUpdate',
|
||||
onFinish = 'onFinish',
|
||||
onWarning = 'onWarning',
|
||||
onDanger = 'onDanger',
|
||||
}
|
||||
|
||||
export type TimerLifeCycleKey = keyof typeof TimerLifeCycle;
|
||||
|
||||
@@ -66,6 +66,7 @@ export { type TimerState, TimerPhase } from './definitions/runtime/TimerState.ty
|
||||
export { type SimpleTimerState, SimplePlayback, SimpleDirection } from './definitions/runtime/AuxTimer.type.js';
|
||||
|
||||
// CLIENT
|
||||
export type { Client, ClientList, ClientType } from './definitions/Clients.type.js';
|
||||
|
||||
// TYPE UTILITIES
|
||||
export { isOntimeBlock, isOntimeDelay, isOntimeEvent, isOntimeCycle, isKeyOfType } from './utils/guards.js';
|
||||
|
||||
Generated
+135
-57
@@ -16,10 +16,10 @@ importers:
|
||||
version: 18.11.18
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^v7.12.0
|
||||
version: 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.12.0
|
||||
version: 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
cross-env:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
@@ -48,6 +48,18 @@ importers:
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
|
||||
apps/cli:
|
||||
devDependencies:
|
||||
eslint:
|
||||
specifier: ^8.53.0
|
||||
version: 8.56.0
|
||||
eslint-config-prettier:
|
||||
specifier: ^9.0.0
|
||||
version: 9.1.0(eslint@8.56.0)
|
||||
prettier:
|
||||
specifier: ^3.0.3
|
||||
version: 3.3.1
|
||||
|
||||
apps/client:
|
||||
dependencies:
|
||||
'@chakra-ui/react':
|
||||
@@ -161,10 +173,10 @@ importers:
|
||||
version: 5.14.5
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^v7.12.0
|
||||
version: 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.12.0
|
||||
version: 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(vite@5.2.11)
|
||||
@@ -176,7 +188,7 @@ importers:
|
||||
version: 9.1.0(eslint@8.56.0)
|
||||
eslint-plugin-jest:
|
||||
specifier: ^28.6.0
|
||||
version: 28.6.0(@typescript-eslint/eslint-plugin@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 28.6.0(@typescript-eslint/eslint-plugin@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.3.1)
|
||||
@@ -324,10 +336,10 @@ importers:
|
||||
version: 8.5.10
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^v7.12.0
|
||||
version: 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.12.0
|
||||
version: 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
esbuild:
|
||||
specifier: ^0.19.10
|
||||
version: 0.19.10
|
||||
@@ -366,10 +378,10 @@ importers:
|
||||
devDependencies:
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^v7.12.0
|
||||
version: 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.12.0
|
||||
version: 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
eslint:
|
||||
specifier: ^8.56.0
|
||||
version: 8.56.0
|
||||
@@ -388,10 +400,10 @@ importers:
|
||||
devDependencies:
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^v7.12.0
|
||||
version: 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.12.0
|
||||
version: 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
version: 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
eslint:
|
||||
specifier: ^8.56.0
|
||||
version: 8.56.0
|
||||
@@ -2515,6 +2527,7 @@ packages:
|
||||
/@humanwhocodes/config-array@0.11.13:
|
||||
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
deprecated: Use @eslint/config-array instead
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.1
|
||||
debug: 4.3.4
|
||||
@@ -2530,6 +2543,7 @@ packages:
|
||||
|
||||
/@humanwhocodes/object-schema@2.0.1:
|
||||
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
dev: true
|
||||
|
||||
/@jest/expect-utils@29.3.1:
|
||||
@@ -3597,8 +3611,8 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==}
|
||||
/@typescript-eslint/eslint-plugin@7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-FX1X6AF0w8MdVFLSdqwqN/me2hyhuQg4ykN6ZpVhh1ij/80pTvDKclX1sZB9iqex8SjQfVhwMKs3JtnnMLzG9w==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^7.0.0
|
||||
@@ -3609,11 +3623,11 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/scope-manager': 7.12.0
|
||||
'@typescript-eslint/type-utils': 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 7.12.0
|
||||
'@typescript-eslint/parser': 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/scope-manager': 7.13.0
|
||||
'@typescript-eslint/type-utils': 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 7.13.0
|
||||
eslint: 8.56.0
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.3.1
|
||||
@@ -3624,8 +3638,8 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@7.12.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==}
|
||||
/@typescript-eslint/parser@7.13.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-EjMfl69KOS9awXXe83iRN7oIEXy9yYdqWfqdrFAYAAr6syP8eLEFI7ZE4939antx2mNgPRW/o1ybm2SFYkbTVA==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
@@ -3634,10 +3648,10 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.12.0
|
||||
'@typescript-eslint/types': 7.12.0
|
||||
'@typescript-eslint/typescript-estree': 7.12.0(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 7.12.0
|
||||
'@typescript-eslint/scope-manager': 7.13.0
|
||||
'@typescript-eslint/types': 7.13.0
|
||||
'@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 7.13.0
|
||||
debug: 4.3.4
|
||||
eslint: 8.56.0
|
||||
typescript: 5.4.3
|
||||
@@ -3661,16 +3675,24 @@ packages:
|
||||
'@typescript-eslint/visitor-keys': 5.62.0
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/scope-manager@7.12.0:
|
||||
resolution: {integrity: sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
/@typescript-eslint/scope-manager@6.21.0:
|
||||
resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.12.0
|
||||
'@typescript-eslint/visitor-keys': 7.12.0
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@7.12.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==}
|
||||
/@typescript-eslint/scope-manager@7.13.0:
|
||||
resolution: {integrity: sha512-ZrMCe1R6a01T94ilV13egvcnvVJ1pxShkE0+NDjDzH4nvG1wXpwsVI5bZCvE7AEDH1mXEx5tJSVR68bLgG7Dng==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.13.0
|
||||
'@typescript-eslint/visitor-keys': 7.13.0
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@7.13.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-xMEtMzxq9eRkZy48XuxlBFzpVMDurUAfDu5Rz16GouAtXm0TaAoTFzqWUFPPuQYXI/CDaH/Bgx/fk/84t/Bc9A==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
@@ -3679,8 +3701,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.12.0(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 7.13.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
debug: 4.3.4
|
||||
eslint: 8.56.0
|
||||
ts-api-utils: 1.3.0(typescript@5.4.3)
|
||||
@@ -3699,8 +3721,13 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types@7.12.0:
|
||||
resolution: {integrity: sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==}
|
||||
/@typescript-eslint/types@6.21.0:
|
||||
resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/types@7.13.0:
|
||||
resolution: {integrity: sha512-QWuwm9wcGMAuTsxP+qz6LBBd3Uq8I5Nv8xb0mk54jmNoCyDspnMvVsOxI6IsMmway5d1S9Su2+sCKv1st2l6eA==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dev: true
|
||||
|
||||
@@ -3746,8 +3773,30 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@7.12.0(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==}
|
||||
/@typescript-eslint/typescript-estree@6.21.0(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/visitor-keys': 6.21.0
|
||||
debug: 4.3.4
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.3
|
||||
semver: 7.6.2
|
||||
ts-api-utils: 1.3.0(typescript@5.4.3)
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@7.13.0(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-cAvBvUoobaoIcoqox1YatXOnSl3gx92rCZoMRPzMNisDiM12siGilSM4+dJAekuuHTibI2hVC2fYK79iSFvWjw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
@@ -3755,8 +3804,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.12.0
|
||||
'@typescript-eslint/visitor-keys': 7.12.0
|
||||
'@typescript-eslint/types': 7.13.0
|
||||
'@typescript-eslint/visitor-keys': 7.13.0
|
||||
debug: 4.3.4
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
@@ -3808,16 +3857,35 @@ packages:
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@7.12.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==}
|
||||
/@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.5
|
||||
'@typescript-eslint/scope-manager': 6.21.0
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
'@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.3)
|
||||
eslint: 8.56.0
|
||||
semver: 7.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@7.13.0(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-jceD8RgdKORVnB4Y6BqasfIkFhl4pajB1wVxrF4akxD2QPM8GNYjgGwEzYS+437ewlqqrg7Dw+6dhdpjMpeBFQ==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^8.56.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
|
||||
'@typescript-eslint/scope-manager': 7.12.0
|
||||
'@typescript-eslint/types': 7.12.0
|
||||
'@typescript-eslint/typescript-estree': 7.12.0(typescript@5.4.3)
|
||||
'@typescript-eslint/scope-manager': 7.13.0
|
||||
'@typescript-eslint/types': 7.13.0
|
||||
'@typescript-eslint/typescript-estree': 7.13.0(typescript@5.4.3)
|
||||
eslint: 8.56.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3840,11 +3908,19 @@ packages:
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys@7.12.0:
|
||||
resolution: {integrity: sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==}
|
||||
/@typescript-eslint/visitor-keys@6.21.0:
|
||||
resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 6.21.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/visitor-keys@7.13.0:
|
||||
resolution: {integrity: sha512-nxn+dozQx+MK61nn/JP+M4eCkHDSxSLDpgE3WcQo0+fkjEolnaB5jswvIKC4K56By8MMgIho7f1PVxERHEo8rw==}
|
||||
engines: {node: ^18.18.0 || >=20.0.0}
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.12.0
|
||||
'@typescript-eslint/types': 7.13.0
|
||||
eslint-visitor-keys: 3.4.3
|
||||
dev: true
|
||||
|
||||
@@ -5525,7 +5601,7 @@ packages:
|
||||
eslint: 8.56.0
|
||||
dev: true
|
||||
|
||||
/eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.12.0)(eslint@8.56.0)(typescript@5.4.3):
|
||||
/eslint-plugin-jest@28.6.0(@typescript-eslint/eslint-plugin@7.13.0)(eslint@8.56.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-YG28E1/MIKwnz+e2H7VwYPzHUYU4aMa19w0yGcwXnnmJH6EfgHahTJ2un3IyraUxNfnz/KUhJAFXNNwWPo12tg==}
|
||||
engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
@@ -5538,8 +5614,8 @@ packages:
|
||||
jest:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.12.0(@typescript-eslint/parser@7.12.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 7.12.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/eslint-plugin': 7.13.0(@typescript-eslint/parser@7.13.0)(eslint@8.56.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 6.21.0(eslint@8.56.0)(typescript@5.4.3)
|
||||
eslint: 8.56.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -6279,7 +6355,7 @@ packages:
|
||||
es6-error: 4.1.1
|
||||
matcher: 3.0.0
|
||||
roarr: 2.15.4
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
serialize-error: 7.0.1
|
||||
dev: true
|
||||
optional: true
|
||||
@@ -6310,7 +6386,7 @@ packages:
|
||||
array-union: 2.1.0
|
||||
dir-glob: 3.0.1
|
||||
fast-glob: 3.3.2
|
||||
ignore: 5.2.4
|
||||
ignore: 5.3.1
|
||||
merge2: 1.4.1
|
||||
slash: 3.0.0
|
||||
dev: true
|
||||
@@ -6580,11 +6656,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
|
||||
dev: true
|
||||
|
||||
/ignore@5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/ignore@5.3.1:
|
||||
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -7374,6 +7445,13 @@ packages:
|
||||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/minimatch@9.0.3:
|
||||
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/minimatch@9.0.4:
|
||||
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
Reference in New Issue
Block a user