mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
feat: improve preset building
This commit is contained in:
committed by
Carlos Valente
parent
5bc286145d
commit
a1fab10bfd
@@ -6,7 +6,7 @@ import { apiEntryUrl } from './constants';
|
||||
const urlPresetsPath = `${apiEntryUrl}/url-presets`;
|
||||
|
||||
/**
|
||||
* HTTP request to retrieve aliases
|
||||
* HTTP request to retrieve all presets
|
||||
*/
|
||||
export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||
const res = await axios.get(urlPresetsPath);
|
||||
@@ -14,8 +14,22 @@ export async function getUrlPresets(): Promise<URLPreset[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to mutate aliases
|
||||
* HTTP request to add a preset
|
||||
*/
|
||||
export async function postUrlPresets(data: URLPreset[]): Promise<URLPreset[]> {
|
||||
return axios.post(urlPresetsPath, data);
|
||||
export async function postUrlPreset(data: URLPreset): Promise<URLPreset[]> {
|
||||
return (await axios.post(urlPresetsPath, data)).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to edit a preset
|
||||
*/
|
||||
export async function putUrlPreset(alias: string, data: URLPreset): Promise<URLPreset[]> {
|
||||
return (await axios.put(`${urlPresetsPath}/${alias}`, data)).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to delete a preset
|
||||
*/
|
||||
export async function deleteUrlPreset(alias: string): Promise<URLPreset[]> {
|
||||
return (await axios.delete(`${urlPresetsPath}/${alias}`)).data;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,20 @@ export function maybeAxiosError(error: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility unwrap a an instance of Error
|
||||
*/
|
||||
export function unwrapError(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
} else {
|
||||
if (typeof error !== 'string') {
|
||||
return JSON.stringify(error);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility unwraps a potential axios error and sends to logger
|
||||
* @param prepend
|
||||
|
||||
@@ -45,8 +45,8 @@ export function RedirectClientModal({ id, isOpen, name, currentPath, origin, onC
|
||||
label: view.label,
|
||||
})),
|
||||
...enabledPresets.map((preset) => ({
|
||||
value: preset.pathAndParams,
|
||||
label: `Preset: ${preset.alias}`,
|
||||
value: preset.search,
|
||||
label: `URL Preset: ${preset.alias}`,
|
||||
})),
|
||||
];
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
&:focus:not(:read-only) {
|
||||
background-color: $gray-1000;
|
||||
border: 1px solid $blue-500;
|
||||
outline: 2px solid $blue-500;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { URL_PRESETS } from '../api/constants';
|
||||
import { getUrlPresets } from '../api/urlPresets';
|
||||
import { deleteUrlPreset, getUrlPresets, postUrlPreset, putUrlPreset } from '../api/urlPresets';
|
||||
|
||||
interface FetchProps {
|
||||
skip?: boolean;
|
||||
@@ -13,12 +14,42 @@ export default function useUrlPresets({ skip = false }: FetchProps = {}) {
|
||||
queryKey: URL_PRESETS,
|
||||
queryFn: getUrlPresets,
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
refetchInterval: queryRefetchIntervalSlow,
|
||||
networkMode: 'always',
|
||||
enabled: !skip,
|
||||
});
|
||||
|
||||
return { data: data ?? [], status, isError, refetch };
|
||||
}
|
||||
|
||||
export function useUpdateUrlPreset() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const addFn = useMutation({
|
||||
mutationFn: postUrlPreset,
|
||||
onSuccess: (newPresets) => {
|
||||
queryClient.setQueryData(URL_PRESETS, newPresets);
|
||||
},
|
||||
});
|
||||
|
||||
const updateFn = useMutation({
|
||||
mutationFn: ({ alias, data }: { alias: string; data: URLPreset }) => putUrlPreset(alias, data),
|
||||
onSuccess: (newPresets) => {
|
||||
queryClient.setQueryData(URL_PRESETS, newPresets);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteFn = useMutation({
|
||||
mutationFn: deleteUrlPreset,
|
||||
onSuccess: (newPresets) => {
|
||||
queryClient.setQueryData(URL_PRESETS, newPresets);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
addPreset: addFn.mutateAsync,
|
||||
updatePreset: (alias: string, data: URLPreset) => updateFn.mutateAsync({ alias, data }),
|
||||
deletePreset: deleteFn.mutateAsync,
|
||||
isMutating: addFn.isPending || updateFn.isPending || deleteFn.isPending,
|
||||
isMutationError: addFn.isError || updateFn.isError || deleteFn.isError,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { resolvePath } from 'react-router-dom';
|
||||
|
||||
import { arePathsEquivalent, generatePathFromPreset, getRouteFromPreset, validateUrlPresetPath } from '../urlPresets';
|
||||
import {
|
||||
arePathsEquivalent,
|
||||
generatePathFromPreset,
|
||||
generateUrlPresetOptions,
|
||||
getRouteFromPreset,
|
||||
validateUrlPresetPath,
|
||||
} from '../urlPresets';
|
||||
|
||||
describe('validateUrlPresetPaths()', () => {
|
||||
test.each([
|
||||
@@ -27,7 +33,8 @@ describe('getRouteFromPreset()', () => {
|
||||
{
|
||||
enabled: true,
|
||||
alias: 'demopage',
|
||||
pathAndParams: '/timer?user=guest',
|
||||
target: 'timer',
|
||||
search: 'user=guest',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -76,14 +83,14 @@ describe('getRouteFromPreset()', () => {
|
||||
|
||||
describe('generatePathFromPreset()', () => {
|
||||
test.each([
|
||||
['timer?user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
|
||||
['timer?user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
|
||||
])('generates a path from a preset: %s', (path, alias, expected) => {
|
||||
expect(generatePathFromPreset(path, alias, null, null)).toEqual(expected);
|
||||
['timer', 'user=guest', 'demopage', 'timer?user=guest&alias=demopage'],
|
||||
['timer', 'user=admin', 'demopage', 'timer?user=admin&alias=demopage'],
|
||||
])('generates a path from a preset: %s', (target, path, alias, expected) => {
|
||||
expect(generatePathFromPreset(target, path, alias, null, null)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('appends the feature params to the alias', () => {
|
||||
expect(generatePathFromPreset('timer?user=guest', 'demopage', 'true', '123')).toBe(
|
||||
expect(generatePathFromPreset('timer', 'user=guest', 'demopage', 'true', '123')).toBe(
|
||||
'timer?user=guest&alias=demopage&locked=true&token=123',
|
||||
);
|
||||
});
|
||||
@@ -106,3 +113,62 @@ describe('arePathsEquivalent()', () => {
|
||||
expect(arePathsEquivalent('timer?test=a&locked=true=token=123', 'timer?test=a')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateUrlPresetOptions', () => {
|
||||
it.each([
|
||||
[
|
||||
'cloud URL without protocol',
|
||||
'test',
|
||||
'www.getontime.no/timer?param1=value1¶m2=value2',
|
||||
{
|
||||
alias: 'test',
|
||||
target: 'timer',
|
||||
search: 'param1=value1¶m2=value2',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'cloud URL',
|
||||
'test',
|
||||
'https://cloud.getontime.no/timer?param1=value1¶m2=value2',
|
||||
{
|
||||
alias: 'test',
|
||||
target: 'timer',
|
||||
search: 'param1=value1¶m2=value2',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'local URL',
|
||||
'test',
|
||||
'http://localhost:4001/timer?param1=value1¶m2=value2',
|
||||
{
|
||||
alias: 'test',
|
||||
target: 'timer',
|
||||
search: 'param1=value1¶m2=value2',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
'IP-based URL',
|
||||
'test',
|
||||
'http://192.168.0.1:4001/timer?param1=value1¶m2=value2',
|
||||
{
|
||||
alias: 'test',
|
||||
target: 'timer',
|
||||
search: 'param1=value1¶m2=value2',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
])('should generate URL preset options for %s', (_description, alias, url, expected) => {
|
||||
expect(generateUrlPresetOptions(alias, url)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('throws on invalid URL', () => {
|
||||
expect(() => generateUrlPresetOptions('test', 'invalid-url')).toThrow();
|
||||
});
|
||||
|
||||
it('throws on on invalid route', () => {
|
||||
expect(() => generateUrlPresetOptions('test', 'www.getontime.no/somethingelse/')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Path, resolvePath } from 'react-router-dom';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
import { OntimeView, URLPreset } from 'ontime-types';
|
||||
import { checkRegex } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* Validates a preset against defined parameters
|
||||
@@ -49,7 +50,7 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
|
||||
const foundPreset = urlPresets.find((preset) => preset.alias === removeTrailingSlash(currentURL) && preset.enabled);
|
||||
if (foundPreset) {
|
||||
// if so, we can redirect to the preset path
|
||||
return generatePathFromPreset(foundPreset.pathAndParams, foundPreset.alias, locked, token);
|
||||
return generatePathFromPreset(foundPreset.target, foundPreset.search, foundPreset.alias, locked, token);
|
||||
}
|
||||
|
||||
// if the current url is not an alias, we check if the alias is in the search parameters
|
||||
@@ -63,7 +64,7 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
|
||||
for (const preset of urlPresets) {
|
||||
// if the page has a known enabled alias, we check if we need to redirect
|
||||
if (preset.alias === presetOnPage && preset.enabled) {
|
||||
const newPath = generatePathFromPreset(preset.pathAndParams, preset.alias, locked, token);
|
||||
const newPath = generatePathFromPreset(preset.target, preset.search, preset.alias, locked, token);
|
||||
if (!arePathsEquivalent(currentPath, newPath)) {
|
||||
// if current path is out of date
|
||||
// return new path so we can redirect
|
||||
@@ -77,8 +78,14 @@ export function getRouteFromPreset(location: Path, urlPresets: URLPreset[]): str
|
||||
/**
|
||||
* Handles generating a path and search parameters from a preset
|
||||
*/
|
||||
export function generatePathFromPreset(pathAndParams: string, alias: string, locked: string | null, token: string | null ): string {
|
||||
const path = resolvePath(pathAndParams);
|
||||
export function generatePathFromPreset(
|
||||
target: Omit<OntimeView, 'editor'>,
|
||||
search: string,
|
||||
alias: string,
|
||||
locked: string | null,
|
||||
token: string | null,
|
||||
): string {
|
||||
const path = resolvePath(`${target}?${search}`);
|
||||
const searchParams = new URLSearchParams(path.search);
|
||||
|
||||
// save the alias so we have a reference to it being a preset and can update if necessary
|
||||
@@ -106,16 +113,16 @@ export function generatePathFromPreset(pathAndParams: string, alias: string, loc
|
||||
export function arePathsEquivalent(currentPath: string, newPath: string): boolean {
|
||||
const currentUrl = new URL(currentPath, document.location.origin);
|
||||
const newUrl = new URL(newPath, document.location.origin);
|
||||
|
||||
|
||||
// check path
|
||||
if (currentUrl.pathname !== newUrl.pathname) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// check search params
|
||||
// if the params match, we dont need further checks
|
||||
if (currentUrl.searchParams.toString() === newUrl.searchParams.toString()) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there is no match, we check the edge cases for the url sharing feature
|
||||
@@ -124,3 +131,38 @@ export function arePathsEquivalent(currentPath: string, newPath: string): boolea
|
||||
|
||||
return currentUrl.searchParams.toString() === newUrl.searchParams.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL preset from a user given alias and URL.
|
||||
*/
|
||||
export function generateUrlPresetOptions(alias: string, userUrl: string): URLPreset {
|
||||
let sanitisedUrl = userUrl.toLowerCase();
|
||||
// we need to ensure the URL has a protocol, but it doesnt matter which
|
||||
if (!checkRegex.startsWithHttp(sanitisedUrl)) {
|
||||
sanitisedUrl = `http://${sanitisedUrl}`;
|
||||
}
|
||||
|
||||
const url = new URL(sanitisedUrl);
|
||||
const target = extractLastSegment(url.pathname);
|
||||
|
||||
if (target === 'editor' || !Object.values(OntimeView).includes(target as OntimeView)) {
|
||||
throw new Error(`Invalid target view: ${target}`);
|
||||
}
|
||||
|
||||
return {
|
||||
alias,
|
||||
target,
|
||||
search: url.searchParams.toString(),
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* the path can contain the stage hash
|
||||
* "/team-hash/timer" or "/timer"
|
||||
* we need to extract ontime view it targets
|
||||
*/
|
||||
function extractLastSegment(pathname: string): string {
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
return segments[segments.length - 1] || '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user