From 2fa397548a5d3323debd87f7cd5dcc473a92c19a Mon Sep 17 00:00:00 2001
From: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
Date: Tue, 4 Jan 2022 22:12:59 +0100
Subject: [PATCH] Feat/navigate (#81)
* feat/navigate upgrade react router
* feat/navigate migrate to react router 6
* feat/navigate style modal
* feat/navigate create endpoints for app settings
* feat/navigate protect editor with pin
* feat/navigate apply dynamic routing draft
* feat/navigate upgrade relevant packages
* feat/navigate restructure directory and add tests
* feat/navigate test aliases validation
* feat/navigate validate aliases before sending
* feat/navigate create data endpoint
* feat/navigate config: prettier
* feat/navigate invalidate empty strings
* feat/navigate create endpoints
* feat/navigate parse on import
* feat/navigate navigate to alias
* feat/navigate user help and sample data
* feat/navigate refact aliases modal
* feat/navigate refact settings style
* feat/navigate update sample db
* feat/navigate link is relative to hostname
* feat/navigate navigate to first match
* feat/navigate update readme and version bump
* feat/navigate config: create shared module
* feat/navigate config: cheat module install
* feat/navigate fix tests
* feat/navigate run tests in pull request
* Update ontime_cy.yml
---
.github/workflows/ontime_cy.yml | 64 ++
.prettierrc | 7 +
README.md | 13 +-
client/package.json | 15 +-
client/src/App.jsx | 90 +--
client/src/_main.scss | 15 -
client/src/app/api/apiConstants.js | 2 +
client/src/app/api/ontimeApi.js | 30 +
client/src/app/appConstants.js | 14 +
client/src/app/context/AppContext.js | 37 ++
client/src/app/context/LoggingContext.js | 4 +-
client/src/app/userConfig.js | 7 -
client/src/app/utils.js | 2 -
.../src/app/utils/__tests__/aliases.test.js | 27 +
client/src/app/utils/__tests__/math.test.js | 21 +
client/src/app/utils/aliases.js | 29 +
client/src/app/utils/math.js | 9 +
.../eventTimes/EventTimesVertical.jsx | 2 +-
.../myProgressBar/MyProgressBar.jsx | 2 +-
client/src/common/components/nav/NavLogo.jsx | 8 +-
.../components/protectRoute/ProtectRoute.jsx | 67 ++
.../protectRoute/ProtectRoute.module.scss | 47 ++
.../src/common/components/views/TodayItem.jsx | 2 +-
client/src/common/input/EditableTimer.jsx | 2 +-
client/src/common/utils/eventsManager.js | 2 +-
client/src/common/utils/handleLink.js | 13 +
client/src/features/control/PlaybackTimer.jsx | 2 +-
client/src/features/editors/Editor.jsx | 6 +-
client/src/features/info/Info.module.scss | 3 +-
.../src/features/info/InfoLogger.module.scss | 3 +-
client/src/features/info/InfoNif.jsx | 9 +-
client/src/features/menu/MenuBar.jsx | 29 +-
client/src/features/menu/MenuBar.module.css | 3 -
client/src/features/menu/MenuBar.module.scss | 9 +
client/src/features/modals/AliasesModal.jsx | 441 ++++++++-----
.../src/features/modals/AppSettingsModal.jsx | 275 ++++----
.../features/modals/EventSettingsModal.jsx | 273 ++++----
.../modals/IntegrationSettingsModal.jsx | 588 +++++++++---------
client/src/features/modals/ModalManager.jsx | 18 +-
client/src/features/modals/Modals.module.scss | 149 +++--
.../src/features/modals/OscSettingsModal.jsx | 168 +++++
.../src/features/modals/SubmitContainer.jsx | 35 ++
client/src/features/modals/modalHelper.js | 11 +
client/src/features/viewers/ViewWrapper.jsx | 2 +-
.../viewers/production/lower/LowerWrapper.jsx | 24 +-
client/src/index.js | 16 +-
client/src/styles/_main.scss | 45 +-
client/src/styles/_mixins.scss | 9 +
client/src/styles/_variables.scss | 4 -
client/yarn.lock | 514 +++++++--------
server/data/db.json | 23 +-
server/package.json | 18 +-
server/src/app.js | 16 +-
server/src/classes/EventTimer.js | 23 +-
server/src/classes/Timer.js | 2 +-
.../src/classes/__tests__/eventtimer.test.js | 43 +-
server/src/controllers/eventsController.js | 2 +-
server/src/controllers/ontimeController.js | 76 +++
server/src/models/aliasDefinition.js | 5 +
server/src/models/dataModel.js | 10 +-
server/src/package.json | 9 +-
server/src/routes/ontimeRouter.js | 17 +
.../src/utils/__tests__/generate_id.test.js | 20 -
.../src/utils/__tests__/getRandomName.test.js | 16 +-
server/src/utils/__tests__/parser.tests.js | 63 +-
server/src/utils/parser.js | 338 ++++++----
server/src/yarn.lock | 50 +-
server/utils/__tests__/generate_id.test.js | 28 +
.../{src => }/utils/__tests__/time.tests.js | 4 +-
server/{src => }/utils/generate_id.js | 0
server/utils/package.json | 11 +
server/utils/time.js | 67 ++
server/utils/yarn.lock | 8 +
server/yarn.lock | 4 +-
74 files changed, 2592 insertions(+), 1428 deletions(-)
create mode 100644 .github/workflows/ontime_cy.yml
create mode 100644 .prettierrc
delete mode 100644 client/src/_main.scss
create mode 100644 client/src/app/appConstants.js
create mode 100644 client/src/app/context/AppContext.js
delete mode 100644 client/src/app/userConfig.js
delete mode 100644 client/src/app/utils.js
create mode 100644 client/src/app/utils/__tests__/aliases.test.js
create mode 100644 client/src/app/utils/__tests__/math.test.js
create mode 100644 client/src/app/utils/aliases.js
create mode 100644 client/src/app/utils/math.js
create mode 100644 client/src/common/components/protectRoute/ProtectRoute.jsx
create mode 100644 client/src/common/components/protectRoute/ProtectRoute.module.scss
create mode 100644 client/src/common/utils/handleLink.js
delete mode 100644 client/src/features/menu/MenuBar.module.css
create mode 100644 client/src/features/menu/MenuBar.module.scss
create mode 100644 client/src/features/modals/OscSettingsModal.jsx
create mode 100644 client/src/features/modals/SubmitContainer.jsx
create mode 100644 client/src/features/modals/modalHelper.js
create mode 100644 client/src/styles/_mixins.scss
delete mode 100644 client/src/styles/_variables.scss
create mode 100644 server/src/models/aliasDefinition.js
delete mode 100644 server/src/utils/__tests__/generate_id.test.js
create mode 100644 server/utils/__tests__/generate_id.test.js
rename server/{src => }/utils/__tests__/time.tests.js (95%)
rename server/{src => }/utils/generate_id.js (100%)
create mode 100644 server/utils/package.json
create mode 100644 server/utils/time.js
create mode 100644 server/utils/yarn.lock
diff --git a/.github/workflows/ontime_cy.yml b/.github/workflows/ontime_cy.yml
new file mode 100644
index 000000000..3eb4a7cf1
--- /dev/null
+++ b/.github/workflows/ontime_cy.yml
@@ -0,0 +1,64 @@
+# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: ontime_test_CI
+
+on: [push, pull_request]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ env:
+ CI: ''
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Use Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '14.x'
+
+ # Utils server
+ - name: Utils - Install dependencies
+ run: yarn install
+ working-directory: ./server/utils
+
+ - name: Utils - run link command
+ run: yarn link
+ working-directory: ./server/utils
+
+ # React
+ - name: React - Link to utils
+ run: yarn link ontime-utils
+ working-directory: ./client
+
+ - name: React - Install dependencies
+ run: yarn install
+ working-directory: ./client
+
+ - name: React - Run tests
+ run: yarn test:pipeline
+ working-directory: ./client
+
+ - name: React - Build project
+ run: yarn build
+ working-directory: ./client
+
+ # Node server
+ - name: React - Link to utils
+ run: yarn link ontime-utils
+ working-directory: ./server/src
+
+ - name: Server - Install dependencies
+ run: yarn install
+ working-directory: ./server/src
+
+ # App
+ - name: Electron - Install dependencies
+ run: yarn install
+ working-directory: ./server
+
+ - name: Electron - Run tests
+ run: yarn test
+ working-directory: ./server
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..7dbcef92e
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": true,
+ "jsxSingleQuote": true
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 1c5e7667f..06cb2c3b4 100644
--- a/README.md
+++ b/README.md
@@ -21,8 +21,11 @@ From here, any device in the same network with a browser is able to render the v
You can then use the ontime logo in the top right corner to select the desired view (event in the lower thirds view, where it is hidden).
In case of unattended machines or automations, it is possible to use different URL to recall individual views
+and extend with using the URL aliases feature
```
+For the presentation views...
+-------------------------------------------------------------
IP.ADDRESS:4001 > Web server default to presenter timer view
IP.ADDRESS:4001/preseter > Presenter / Stage timer view
IP.ADDRESS:4001/sm > Stage Manager / Backstage view
@@ -30,6 +33,11 @@ IP.ADDRESS:4001/public > Public / Foyer view
IP.ADDRESS:4001/pip > Picture in Picture view
IP.ADDRESS:4001/lower > Lower Thirds
IP.ADDRESS:4001/studio > Studio Clock
+
+...and for the editor (the control interface, same as the app)
+-------------------------------------------------------------
+IP.ADDRESS:4001/studio > Studio Clock
+
```
More documentation available [here](https://cpvalente.gitbook.io/ontime/)
@@ -47,6 +55,9 @@ More documentation available [here](https://cpvalente.gitbook.io/ontime/)
- [x] Open Sound Control (OSC) Control and Feedback
- [x] Roll mode: run independently using the system clock
- [x] Import event list from Excel
+- [x] URL Aliases (define configurable aliases to ease onsite setup)
+- [x] Logging view
+- [x] Edit anywhere: run ontime in your local network and use any machine to reach the editor page (same as app)
## Unopinionated
We are not interested in forcing workflows and have made ontime, so it is flexible to whichever way you would like to work.
@@ -77,8 +88,6 @@ These will be implemented in a development friendly order unless there is user d
- [ ] Companion module
- [ ] Lower Third Manager
- [ ] Note only event
-- [ ] URL Aliases (define configurable aliases to ease onsite setup)
-- [ ] Logging view
- [ ] Reach Schedule: way to speedup timer to meet a deadline
- [ ] vMix integration
diff --git a/client/package.json b/client/package.json
index 15eaa764b..e64adc4bc 100644
--- a/client/package.json
+++ b/client/package.json
@@ -3,26 +3,26 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@chakra-ui/react": "^1.7.1",
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
+ "@chakra-ui/react": "^1.7.3",
+ "@emotion/react": "^11.7.1",
+ "@emotion/styled": "^11.6.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"axios": "^0.24.0",
"framer-motion": "^4.1.6",
"jotai": "^0.16.5",
+ "ontime-utils": "link: ../server/utils/",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.1",
"react-fast-compare": "^3.2.0",
"react-icons": "^4.3.1",
- "react-qr-code": "^2.0.2",
+ "react-qr-code": "2.0.3",
"react-query": "^3.34.5",
- "react-router": "^5.2.0",
- "react-router-dom": "^5.2.0",
+ "react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
- "socket.io-client": "^4.3.2",
+ "socket.io-client": "4.4.0",
"typeface-open-sans": "^1.1.13",
"use-fit-text": "^2.4.0",
"web-vitals": "^1.0.1"
@@ -31,6 +31,7 @@
"start": "set BROWSER=none&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
+ "test:pipeline": "react-scripts test --watchAll=false --runInBand --detectOpenHandles --forceExit ",
"eject": "react-scripts eject"
},
"browserslist": {
diff --git a/client/src/App.jsx b/client/src/App.jsx
index fef3e7a31..ac14536c4 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,18 +1,17 @@
import { lazy, Suspense, useCallback, useEffect } from 'react';
-import { Route, Switch } from 'react-router-dom';
+import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import './App.scss';
-import { QueryClient, QueryClientProvider } from 'react-query';
-import SocketProvider from 'app/context/socketContext';
import withSocket from 'features/viewers/ViewWrapper';
import ErrorBoundary from 'common/components/errorBoundary/ErrorBoundary';
+import ProtectRoute from './common/components/protectRoute/ProtectRoute';
+import { useFetch } from './app/hooks/useFetch';
+import { ALIASES } from './app/api/apiConstants';
+import { getAliases } from './app/api/ontimeApi';
const Editor = lazy(() => import('features/editors/Editor'));
const PresenterView = lazy(() =>
import('features/viewers/presenter/PresenterView')
);
-const PresenterSimple = lazy(() =>
- import('features/viewers/presenter/PresenterSimple')
-);
const StageManager = lazy(() =>
import('features/viewers/backstage/StageManager')
);
@@ -21,18 +20,9 @@ const Lower = lazy(() =>
import('features/viewers/production/lower/LowerWrapper')
);
const Pip = lazy(() => import('features/viewers/production/Pip'));
-
const StudioClock = lazy(() => import('features/viewers/studio/StudioClock'));
-const queryClient = new QueryClient();
-// Seemed to cause issues
-// broadcastQueryClient({
-// queryClient,
-// broadcastChannel: 'ontime',
-// });
-
const SPresenter = withSocket(PresenterView);
-const SPresenterSimple = withSocket(PresenterSimple);
const SStageManager = withSocket(StageManager);
const SPublic = withSocket(Public);
const SLowerThird = withSocket(Lower);
@@ -40,6 +30,10 @@ const SPip = withSocket(Pip);
const SStudio = withSocket(StudioClock);
function App() {
+ const { data } = useFetch(ALIASES, getAliases);
+ const location = useLocation();
+ const navigate = useNavigate();
+
// Handle keyboard shortcuts
const handleKeyPress = useCallback((e) => {
// check if the alt key is pressed
@@ -65,33 +59,47 @@ function App() {
};
}, [handleKeyPress]);
+ // navigate if is alias route
+ useEffect(() => {
+ if (data == null) return;
+ for (const d of data) {
+ if (`/${d.alias}` === location.pathname && d.enabled) {
+ navigate(`/${d.pathAndParams}`);
+ break;
+ }
+ }
+ }, [data, location, navigate]);
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Lower cannot have fallback */}
-
- {/* Send to default if nothing found */}
-
-
-
-
-
-
-
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/*/!* Lower cannot have fallback *!/*/}
+ } />
+ {/*/!* Protected Routes *!/*/}
+
+
+
+ }
+ />
+ {/* Send to default if nothing found */}
+ } />
+
+
+
+
);
}
diff --git a/client/src/_main.scss b/client/src/_main.scss
deleted file mode 100644
index 83da3237e..000000000
--- a/client/src/_main.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-$ontime-accent: #4bffabcc;
-$ontime-pink: #ff7597;
-$ontime-roll: #2b6cb0;
-
-$notes-color: #d69e2e;
-
-$header-gray: #ccc;
-$label-gray: #aaa;
-
-@mixin container-bg {
- background-color: rgba(0, 0, 0, 0.13);
- border-radius: 2px;
- padding: 0 0.5em;
- margin: 0 0.5em;
-}
\ No newline at end of file
diff --git a/client/src/app/api/apiConstants.js b/client/src/app/api/apiConstants.js
index 2fbdadb91..0ca3d29c4 100644
--- a/client/src/app/api/apiConstants.js
+++ b/client/src/app/api/apiConstants.js
@@ -1,8 +1,10 @@
export const NODE_PORT = 4001;
export const EVENT_TABLE = 'event';
+export const ALIASES = 'aliases';
export const EVENTS_TABLE = 'events';
export const APP_TABLE = 'appinfo';
export const OSC_SETTINGS = 'oscSettings';
+export const APP_SETTINGS = 'appSettings';
const calculateServer = () => {
return window.location.origin.replace(window.location.port, `${NODE_PORT}/`);
diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js
index 91fdce1f5..26331631f 100644
--- a/client/src/app/api/ontimeApi.js
+++ b/client/src/app/api/ontimeApi.js
@@ -9,6 +9,18 @@ export const ontimePlaceholderInfo = {
},
};
+export const ontimePlaceholderSettings = {
+ pinCode: null,
+};
+
+export const eventPlaceholderSettings = {
+ title: '',
+ url: '',
+ publicInfo: '',
+ backstageInfo: '',
+ endMessage: '',
+};
+
export const oscPlaceholderSettings = {
port: '',
portOut: '',
@@ -74,6 +86,15 @@ export const ontimeVars = [
},
];
+export const getSettings = async () => {
+ const res = await axios.get(`${ontimeURL}/settings`);
+ return res.data;
+};
+
+export const postSettings = async (data) => {
+ return await axios.post(`${ontimeURL}/settings`, data);
+};
+
export const getInfo = async () => {
const res = await axios.get(`${ontimeURL}/info`);
return res.data;
@@ -83,6 +104,15 @@ export const postInfo = async (data) => {
return await axios.post(`${ontimeURL}/info`, data);
};
+export const getAliases = async () => {
+ const res = await axios.get(`${ontimeURL}/aliases`);
+ return res.data;
+};
+
+export const postAliases = async (data) => {
+ return await axios.post(`${ontimeURL}/aliases`, data);
+};
+
export const getOSC = async () => {
const res = await axios.get(`${ontimeURL}/osc`);
return res.data;
diff --git a/client/src/app/appConstants.js b/client/src/app/appConstants.js
new file mode 100644
index 000000000..b74f6778a
--- /dev/null
+++ b/client/src/app/appConstants.js
@@ -0,0 +1,14 @@
+// Exported viewer links
+const speakerLink = 'http://localhost:4001/speaker';
+const smLink = 'http://localhost:4001/sm';
+const publicLink = 'http://localhost:4001/public';
+const pipLink = 'http://localhost:4001/pip';
+const studioLink = 'http://localhost:4001/studio';
+
+export const viewerLinks = [
+ { link: speakerLink, label: 'Speaker Screen' },
+ { link: smLink, label: 'Backstage Screen' },
+ { link: publicLink, label: 'Public Screen' },
+ { link: pipLink, label: 'Picture in Picture' },
+ { link: studioLink, label: 'Studio Clock' }
+];
\ No newline at end of file
diff --git a/client/src/app/context/AppContext.js b/client/src/app/context/AppContext.js
new file mode 100644
index 000000000..a2d39bc96
--- /dev/null
+++ b/client/src/app/context/AppContext.js
@@ -0,0 +1,37 @@
+import { createContext, useCallback, useEffect, useState } from 'react';
+import { useFetch } from '../hooks/useFetch';
+import { APP_SETTINGS } from '../api/apiConstants';
+import { getSettings } from '../api/ontimeApi';
+
+export const AppContext = createContext({
+ auth: false,
+ data: {
+ pinCode: null
+ }
+});
+
+export const AppContextProvider = (props) => {
+ const [auth, setAuth] = useState(true);
+ const { data } = useFetch(APP_SETTINGS, getSettings);
+
+ useEffect(() => {
+ if (data == null) return;
+ if (data?.pinCode === null || data?.pinCode === '') {
+ setAuth(true);
+ } else {
+ setAuth(false);
+ }
+ },[data])
+
+ const validate = useCallback((pin) => {
+ const correct = pin === data.pinCode;
+ setAuth(correct);
+ return correct;
+ }, [data]);
+
+ return (
+
+ {props.children}
+
+ );
+};
\ No newline at end of file
diff --git a/client/src/app/context/LoggingContext.js b/client/src/app/context/LoggingContext.js
index 6f85ee39c..9e616ae27 100644
--- a/client/src/app/context/LoggingContext.js
+++ b/client/src/app/context/LoggingContext.js
@@ -1,7 +1,7 @@
import { useSocket } from './socketContext';
import { createContext, useCallback, useEffect, useState } from 'react';
-import { generateId } from 'ontime-server/utils/generate_id';
-import { nowInMillis, stringFromMillis } from 'ontime-server/utils/time';
+import { generateId } from 'ontime-utils/generate_id';
+import { nowInMillis, stringFromMillis } from 'ontime-utils/time';
export const LoggingContext = createContext({
logData: [],
diff --git a/client/src/app/userConfig.js b/client/src/app/userConfig.js
deleted file mode 100644
index ed0daa965..000000000
--- a/client/src/app/userConfig.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const userConfig = {
- timerColorOnPause: '#555',
- timerColorOnRunning: '#FFF',
- timerColorOnMessage: '#CCC',
- timerColorOnTimeOver: '#F00',
- overTimeText: '',
-}
\ No newline at end of file
diff --git a/client/src/app/utils.js b/client/src/app/utils.js
deleted file mode 100644
index 6eb270605..000000000
--- a/client/src/app/utils.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const clamp = (num, a, b) =>
- Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b));
diff --git a/client/src/app/utils/__tests__/aliases.test.js b/client/src/app/utils/__tests__/aliases.test.js
new file mode 100644
index 000000000..618aab7b1
--- /dev/null
+++ b/client/src/app/utils/__tests__/aliases.test.js
@@ -0,0 +1,27 @@
+import { validateAlias } from '../aliases';
+
+describe('An alias fails if incorrect', () => {
+
+ const testsToFail = [
+ // no empty
+ '',
+ // no https, http or www
+ 'https://www.test.com',
+ 'http://www.test.com',
+ 'www.test.com',
+ // no hostname
+ 'localhost/test',
+ '127.0.0.1/test',
+ '0.0.0.0/test',
+ // no editor
+ 'editor',
+ 'editor?test'
+ ];
+
+ testsToFail.forEach((t) => (
+ test(`${t}`, () => {
+ expect(validateAlias(t).status).toBeFalsy();
+ })
+ )
+ );
+});
\ No newline at end of file
diff --git a/client/src/app/utils/__tests__/math.test.js b/client/src/app/utils/__tests__/math.test.js
new file mode 100644
index 000000000..9b1ee536f
--- /dev/null
+++ b/client/src/app/utils/__tests__/math.test.js
@@ -0,0 +1,21 @@
+import { clamp } from '../math';
+
+test('Clamps a set of numbers correctly', () => {
+ const testCases = [
+ { num: 10, min: 0, max: 20, result: 10 },
+ { num: 0, min: 0, max: 20, result: 0 },
+ { num: 20, min: 0, max: 20, result: 20 },
+ { num: 20, min: 0, max: 20, result: 20 },
+ { num: -20, min: 0, max: 20, result: 0 },
+ { num: -0, min: 0, max: 20, result: 0 },
+ { num: -50, min: -30, max: -20, result: -30 },
+ { num: -50, min: 0, max: 0, result: 0 },
+ { num: 50.5, min: 0, max: 100, result: 50.5 },
+ { num: 50, min: 0, max: 20.32, result: 20.32 },
+ { num: 10, min: 20.32, max: 40, result: 20.32 }
+ ];
+
+ testCases.forEach((t) => (
+ expect(clamp(t.num, t.min, t.max)).toBe(t.result)
+ ));
+});
\ No newline at end of file
diff --git a/client/src/app/utils/aliases.js b/client/src/app/utils/aliases.js
new file mode 100644
index 000000000..a7b737291
--- /dev/null
+++ b/client/src/app/utils/aliases.js
@@ -0,0 +1,29 @@
+/**
+ * Validates an alias against defined parameters
+ * @param {string} alias
+ * @returns {{message: string, status: boolean}}
+ */
+export const validateAlias = (alias) => {
+
+ const valid = { status: true, message: 'ok' };
+
+ if (alias === '' || alias == null) {
+ // cannot be empty
+ valid.status = false;
+ valid.message = 'should not be empty';
+ } else if (alias.includes('http') || alias.includes('https') || alias.includes('www')) {
+ // cannot contain http, https or www
+ valid.status = false;
+ valid.message = 'should not include http, https, www';
+ } else if (alias.includes('127.0.0.1') || alias.includes('localhost') || alias.includes('0.0.0.0')) {
+ // aliases cannot contain hostname
+ valid.status = false;
+ valid.message = 'should not include hostname';
+ } else if (alias.includes('editor')) {
+ // no editor
+ valid.status = false;
+ valid.message = 'No aliases to editor page allowed';
+ }
+
+ return valid;
+};
\ No newline at end of file
diff --git a/client/src/app/utils/math.js b/client/src/app/utils/math.js
new file mode 100644
index 000000000..c7a6bc5f8
--- /dev/null
+++ b/client/src/app/utils/math.js
@@ -0,0 +1,9 @@
+/**
+ * Clamps a value between a min and a max
+ * @param {number} num - Value to clamp
+ * @param {number} min - min value
+ * @param {number} max - max value
+ * @returns {number}
+ */
+export const clamp = (num, min, max) =>
+ Math.max(Math.min(num, Math.max(min, max)), Math.min(min, max));
diff --git a/client/src/common/components/eventTimes/EventTimesVertical.jsx b/client/src/common/components/eventTimes/EventTimesVertical.jsx
index 7a796c294..5081b7aee 100644
--- a/client/src/common/components/eventTimes/EventTimesVertical.jsx
+++ b/client/src/common/components/eventTimes/EventTimesVertical.jsx
@@ -1,5 +1,5 @@
import EditableTimer from 'common/input/EditableTimer';
-import { stringFromMillis } from 'ontime-server/utils/time';
+import { stringFromMillis } from 'ontime-utils/time';
import { useContext } from 'react';
import { LoggingContext } from '../../../app/context/LoggingContext';
diff --git a/client/src/common/components/myProgressBar/MyProgressBar.jsx b/client/src/common/components/myProgressBar/MyProgressBar.jsx
index e7006cdaa..6a2422e02 100644
--- a/client/src/common/components/myProgressBar/MyProgressBar.jsx
+++ b/client/src/common/components/myProgressBar/MyProgressBar.jsx
@@ -1,4 +1,4 @@
-import { clamp } from 'app/utils';
+import { clamp } from 'app/utils/math';
import styles from './MyProgressBar.module.css';
export default function MyProgressBar(props) {
diff --git a/client/src/common/components/nav/NavLogo.jsx b/client/src/common/components/nav/NavLogo.jsx
index 4497624ef..14264ee28 100644
--- a/client/src/common/components/nav/NavLogo.jsx
+++ b/client/src/common/components/nav/NavLogo.jsx
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
-import { Link, Redirect } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { Image } from '@chakra-ui/react';
import { AnimatePresence, motion } from 'framer-motion';
import { useState, useEffect, useCallback } from 'react';
@@ -57,7 +57,6 @@ export default function NavLogo(props) {
to='/presenter'
className={style.navItem}
tabIndex={1}
- onKeyDownCapture={() => }
>
Presenter
@@ -65,7 +64,6 @@ export default function NavLogo(props) {
to='/sm'
className={style.navItem}
tabIndex={2}
- onKeyDownCapture={() => }
>
Backstage
@@ -73,7 +71,6 @@ export default function NavLogo(props) {
to='/public'
className={style.navItem}
tabIndex={3}
- onKeyDownCapture={() => }
>
Public
@@ -81,7 +78,6 @@ export default function NavLogo(props) {
to='/lower'
className={style.navItem}
tabIndex={4}
- onKeyDownCapture={() => }
>
Lower Thirds
@@ -89,7 +85,6 @@ export default function NavLogo(props) {
to='/pip'
className={style.navItem}
tabIndex={4}
- onKeyDownCapture={() => }
>
PIP
@@ -97,7 +92,6 @@ export default function NavLogo(props) {
to='/studio'
className={style.navItem}
tabIndex={5}
- onKeyDownCapture={() => }
>
Studio Clock
diff --git a/client/src/common/components/protectRoute/ProtectRoute.jsx b/client/src/common/components/protectRoute/ProtectRoute.jsx
new file mode 100644
index 000000000..fcad4e854
--- /dev/null
+++ b/client/src/common/components/protectRoute/ProtectRoute.jsx
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import style from './ProtectRoute.module.scss';
+import { PinInput, PinInputField } from '@chakra-ui/react';
+import { IconButton } from '@chakra-ui/button';
+import { FiCheck } from 'react-icons/fi';
+import { AppContext } from '../../../app/context/AppContext';
+import { useContext, useEffect, useState } from 'react';
+
+
+export default function ProtectRoute(props) {
+ const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
+ const [pin, setPin] = useState('');
+ const [failed, setFailed] = useState(false);
+ const { auth, validate } = useContext(AppContext);
+
+ // Set window title
+ useEffect(() => {
+ document.title = 'ontime';
+ }, []);
+
+ const handleValidation = () => {
+ const r = validate(pin);
+ if (!r) {
+ setFailed(true);
+ }
+ }
+
+ return (
+ <>
+ {!isLocal && !auth ? (
+
+ ontime
+
+
{
+ setFailed(false);
+ setPin(value);
+ }}
+ >
+
+
+
+
+
+
}
+ style={{ fontSize: '1.5em' }}
+ onClick={() => handleValidation()}
+ />
+
+
+ ) : (
+ props.children
+ )}
+ >
+ );
+}
+
+ProtectRoute.propTypes = {
+ children: PropTypes.node.isRequired
+};
diff --git a/client/src/common/components/protectRoute/ProtectRoute.module.scss b/client/src/common/components/protectRoute/ProtectRoute.module.scss
new file mode 100644
index 000000000..0b9118d31
--- /dev/null
+++ b/client/src/common/components/protectRoute/ProtectRoute.module.scss
@@ -0,0 +1,47 @@
+@use '../../../styles/main' as *;
+@use '../../../styles/mixins' as *;
+
+.container {
+ background: #222;
+
+ display: grid;
+ place-content: center;
+ height: 100vh;
+ padding-bottom: 30vh;
+
+ color: $ontime-pink;
+ font-family: 'Open Sans', sans-serif;
+ font-weight: 200;
+ text-align: center;
+ font-size: 3vw;
+}
+
+.pin,
+.pin__failed {
+ display: flex;
+ gap: 10px;
+ padding: 20px;
+
+ input {
+ border-radius: 50%;
+ }
+
+ button {
+ margin-left: 20px;
+ }
+}
+
+.pin__failed {
+ input {
+ animation: colourFade 1.5s ease;
+ }
+}
+
+@keyframes colourFade {
+ from {
+ background: $ontime-pink;
+ }
+ to {
+ background: rgba($ontime-pink, 0);
+ }
+}
diff --git a/client/src/common/components/views/TodayItem.jsx b/client/src/common/components/views/TodayItem.jsx
index 5a7ca28df..c1a4d2413 100644
--- a/client/src/common/components/views/TodayItem.jsx
+++ b/client/src/common/components/views/TodayItem.jsx
@@ -1,4 +1,4 @@
-import { stringFromMillis } from 'ontime-server/utils/time';
+import { stringFromMillis } from 'ontime-utils/time';
import style from './Paginator.module.css';
export default function TodayItem(props) {
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
diff --git a/client/src/common/input/EditableTimer.jsx b/client/src/common/input/EditableTimer.jsx
index c7f6c5c14..8de5dbbc7 100644
--- a/client/src/common/input/EditableTimer.jsx
+++ b/client/src/common/input/EditableTimer.jsx
@@ -4,7 +4,7 @@ import {
isTimeString,
timeStringToMillis,
} from '../utils/dateConfig';
-import { stringFromMillis } from 'ontime-server/utils/time';
+import { stringFromMillis } from 'ontime-utils/time';
import style from './EditableTimer.module.css';
import { LoggingContext } from '../../app/context/LoggingContext';
diff --git a/client/src/common/utils/eventsManager.js b/client/src/common/utils/eventsManager.js
index 5e861e9ff..b22c45f85 100644
--- a/client/src/common/utils/eventsManager.js
+++ b/client/src/common/utils/eventsManager.js
@@ -1,4 +1,4 @@
-import { stringFromMillis } from 'ontime-server/utils/time';
+import { stringFromMillis } from 'ontime-utils/time';
/**
* @description From a list of events, returns only events of type event with calculated delays
diff --git a/client/src/common/utils/handleLink.js b/client/src/common/utils/handleLink.js
new file mode 100644
index 000000000..dcceb3e23
--- /dev/null
+++ b/client/src/common/utils/handleLink.js
@@ -0,0 +1,13 @@
+/**
+ * Handles link to external URLs: specifically for a electron / browser case
+ * If electron: ask main process to call a new browser window
+ * If browser: open in new tab
+ * @param url
+ */
+export default function handleLink(url) {
+ if (window.process?.type === 'renderer') {
+ window.ipcRenderer.send('send-to-link', url);
+ } else {
+ window.open(url);
+ }
+}
\ No newline at end of file
diff --git a/client/src/features/control/PlaybackTimer.jsx b/client/src/features/control/PlaybackTimer.jsx
index 635646ecb..fe5118187 100644
--- a/client/src/features/control/PlaybackTimer.jsx
+++ b/client/src/features/control/PlaybackTimer.jsx
@@ -1,6 +1,6 @@
import style from './PlaybackControl.module.scss';
import Countdown from 'common/components/countdown/Countdown';
-import { stringFromMillis } from 'ontime-server/utils/time';
+import { stringFromMillis } from 'ontime-utils/time';
import {Tooltip} from '@chakra-ui/react';
import {Button} from '@chakra-ui/button';
import {memo} from 'react';
diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx
index 8ce7224f4..d2c20f0ea 100644
--- a/client/src/features/editors/Editor.jsx
+++ b/client/src/features/editors/Editor.jsx
@@ -24,12 +24,14 @@ export default function Editor() {
return (
-
+
+
+
-
+
diff --git a/client/src/features/info/Info.module.scss b/client/src/features/info/Info.module.scss
index 6d5d2e3da..337c053f9 100644
--- a/client/src/features/info/Info.module.scss
+++ b/client/src/features/info/Info.module.scss
@@ -1,4 +1,5 @@
-@use '../../main' as *;
+@use '../../styles/main' as *;
+@use '../../styles/mixins' as *;
@mixin container {
margin-top: 1em;
diff --git a/client/src/features/info/InfoLogger.module.scss b/client/src/features/info/InfoLogger.module.scss
index 51087c484..0ec7ec283 100644
--- a/client/src/features/info/InfoLogger.module.scss
+++ b/client/src/features/info/InfoLogger.module.scss
@@ -1,5 +1,6 @@
@use 'Info.module' as *;
-@use '../../main' as *;
+@use '../../styles/main' as *;
+@use '../../styles/mixins' as *;
.container,
.container__expanded{
diff --git a/client/src/features/info/InfoNif.jsx b/client/src/features/info/InfoNif.jsx
index 647c35176..5548bcb1a 100644
--- a/client/src/features/info/InfoNif.jsx
+++ b/client/src/features/info/InfoNif.jsx
@@ -4,6 +4,7 @@ import { getInfo, ontimePlaceholderInfo } from 'app/api/ontimeApi';
import { useFetch } from 'app/hooks/useFetch';
import style from './Info.module.scss';
import CollapseBar from '../../common/components/collapseBar/CollapseBar';
+import handleLink from '../../common/utils/handleLink';
export default function InfoNif() {
const { data, status } = useFetch(APP_TABLE, getInfo, {
@@ -12,14 +13,6 @@ export default function InfoNif() {
const [collapsed, setCollapsed] = useState(false);
const baseURL = 'http://__IP__:4001';
- const handleLink = (url) => {
- if (window.process?.type === 'renderer') {
- window.ipcRenderer.send('send-to-link', url);
- } else {
- window.open(url);
- }
- };
-
return (
setCollapsed((c) => !c)}/>
diff --git a/client/src/features/menu/MenuBar.jsx b/client/src/features/menu/MenuBar.jsx
index b902bebff..9027773fb 100644
--- a/client/src/features/menu/MenuBar.jsx
+++ b/client/src/features/menu/MenuBar.jsx
@@ -6,14 +6,15 @@ import SettingsIconBtn from './buttons/SettingsIconBtn';
import MaxIconBtn from './buttons/MaxIconBtn';
import MinIconBtn from './buttons/MinIconBtn';
import QuitIconBtn from './buttons/QuitIconBtn';
-import style from './MenuBar.module.css';
+import style from './MenuBar.module.scss';
import HelpIconBtn from './buttons/HelpIconBtn';
import UploadIconBtn from './buttons/UploadIconBtn';
import { useContext, useRef } from 'react';
import { LoggingContext } from '../../app/context/LoggingContext';
+import PropTypes from 'prop-types';
export default function MenuBar(props) {
- const { onOpen } = props;
+ const { isOpen, onOpen } = props;
const { emitError } = useContext(LoggingContext);
const hiddenFileInput = useRef(null);
const queryClient = useQueryClient();
@@ -33,6 +34,10 @@ export default function MenuBar(props) {
}
};
+ const buttonStyle = {
+ fontSize: '1.5em'
+ };
+
const handleUpload = (event) => {
const fileUploaded = event.target.files[0];
if (fileUploaded == null) return;
@@ -93,25 +98,27 @@ export default function MenuBar(props) {
<>
handleIPC('shutdown')} />
handleIPC('max')}
/>
handleIPC('min')}
/>
handleIPC('help')}
/>
>
);
}
+
+MenuBar.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onOpen: PropTypes.func.isRequired,
+};
+
diff --git a/client/src/features/menu/MenuBar.module.css b/client/src/features/menu/MenuBar.module.css
deleted file mode 100644
index 00d82cfe7..000000000
--- a/client/src/features/menu/MenuBar.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.gap {
- height: 1em;
-}
diff --git a/client/src/features/menu/MenuBar.module.scss b/client/src/features/menu/MenuBar.module.scss
new file mode 100644
index 000000000..a11bf57e7
--- /dev/null
+++ b/client/src/features/menu/MenuBar.module.scss
@@ -0,0 +1,9 @@
+@use '../../styles/main' as *;
+
+.gap {
+ height: 1em;
+}
+
+.open {
+ background: $light-bg;
+}
\ No newline at end of file
diff --git a/client/src/features/modals/AliasesModal.jsx b/client/src/features/modals/AliasesModal.jsx
index 8a9663ee1..9ee5d77c7 100644
--- a/client/src/features/modals/AliasesModal.jsx
+++ b/client/src/features/modals/AliasesModal.jsx
@@ -1,174 +1,321 @@
import { Button, IconButton } from '@chakra-ui/button';
-import { FiPlus, FiMinus } from 'react-icons/fi';
+import { FiInfo, FiMinus, FiSun } from 'react-icons/fi';
import { ModalBody } from '@chakra-ui/modal';
import { Input } from '@chakra-ui/react';
-import { fetchEvent } from 'app/api/eventApi';
-import { useState } from 'react';
+import { getAliases, postAliases } from '../../app/api/ontimeApi';
+import { useContext, useEffect, useState } from 'react';
import { useFetch } from 'app/hooks/useFetch';
-import { EVENT_TABLE } from 'app/api/apiConstants';
+import { ALIASES } from 'app/api/apiConstants';
import style from './Modals.module.scss';
+import { viewerLinks } from '../../app/appConstants';
+import { LoggingContext } from '../../app/context/LoggingContext';
+import { validateAlias } from '../../app/utils/aliases';
+import { Tooltip } from '@chakra-ui/tooltip';
+import SubmitContainer from './SubmitContainer';
+import handleLink from '../../common/utils/handleLink';
export default function AliasesModal() {
- const { data, status, isError } = useFetch(EVENT_TABLE, fetchEvent);
+ const { data, status, refetch } = useFetch(ALIASES, getAliases);
+ const { emitError } = useContext(LoggingContext);
+ const [changed, setChanged] = useState(false);
const [submitting, setSubmitting] = useState(false);
+ const [aliases, setAliases] = useState([]);
+ const host = window.location.host;
+ /**
+ * Set formdata from server state
+ */
+ useEffect(() => {
+ if (data == null) return;
+ if (changed) return;
+ setAliases([...data]);
+ }, [changed, data]);
+
+ /**
+ * Validate and submit data
+ */
const submitHandler = async (event) => {
event.preventDefault();
- // NOTHING HERE YET
+ setSubmitting(true);
+
+ const validatedAliases = [...aliases];
+ let errors = false;
+ for (const alias of validatedAliases) {
+ // validate url
+ const isURLValid = validateAlias(alias.pathAndParams);
+ if (!isURLValid.status) {
+ alias.urlError = isURLValid.message;
+ errors = true;
+ } else {
+ alias.urlError = undefined;
+ }
+ // validate alias
+ const isAliasValid = validateAlias(alias.alias);
+ if (!isAliasValid.status) {
+ alias.aliasError = isAliasValid.message;
+ errors = true;
+ } else {
+ alias.aliasError = undefined;
+ }
+ }
+ setAliases(validatedAliases);
+
+ if (!errors) {
+ await postAliases(aliases);
+ await refetch();
+ setChanged(false);
+ }
+
+ setSubmitting(false);
};
- // Hardcoded links for now
- // it will need dynamic PORT assignment
- const speakerLink = 'http://localhost:4001/speaker';
- const smLink = 'http://localhost:4001/sm';
- const publicLink = 'http://localhost:4001/public';
- const pipLink = 'http://localhost:4001/pip';
- const studioLink = 'http://localhost:4001/studio';
+ /**
+ * Creates a new alias in state with a temporary id
+ */
+ const addNew = () => {
+ if (aliases.length > 20) {
+ emitError('Maximum amount of aliases reacted (20)');
+ return;
+ }
+
+ const emptyAlias = {
+ id: Math.floor(Math.random() * 1000),
+ enabled: false,
+ alias: '',
+ pathAndParams: '',
+ };
+ setAliases((prevState) => [...prevState, emptyAlias]);
+ setChanged(true);
+ };
+
+ /**
+ * Deletes an alias by a given id
+ * @param {string} id - id of alias to delete
+ */
+ const deleteAlias = (id) => {
+ setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]);
+ setChanged(true);
+ };
+
+ /**
+ * Sets enabled flag to true / false
+ * @param {string} id - object id
+ * @param {boolean} isEnabled - whether to enable / disable flag
+ */
+ const setEnabled = (id, isEnabled) => {
+ const aliasesState = [...aliases];
+ for (const a of aliasesState) {
+ if (a.id === id) {
+ if (isEnabled) {
+ if (a.alias === '' || a.pathAndParams === '') {
+ emitError('Alias incomplete');
+ break;
+ }
+
+ const isRepeated = aliases.some(
+ (r) => a.alias === r.alias && r.enabled
+ );
+ if (isRepeated) {
+ emitError('There is already an alias with this name');
+ break;
+ }
+ }
+ a.enabled = isEnabled;
+ break;
+ }
+ }
+ setChanged(true);
+ setAliases(aliasesState);
+ };
+
+ /**
+ * Reverts local state equals to server state
+ */
+ const revert = async () => {
+ setChanged(false);
+ await refetch();
+ };
+
+ /**
+ * Handles change of input field in local state
+ * @param {number} index - index of item in array
+ * @param {string} field - object parameter to update
+ * @param {string} value - new object parameter value
+ */
+ const handleChange = (index, field, value) => {
+ const temp = [...aliases];
+ temp[index][field] = value;
+ setAliases(temp);
+ setChanged(true);
+ };
return (
- <>
+
+
+ Configure easy to use URL Aliases
+
+ 🔥 Changes take effect on save 🔥
+