Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report #1469

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions apps/client/src/common/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const SHEET_STATE = ['sheetState'];
export const URL_PRESETS = ['urlpresets'];
export const VIEW_SETTINGS = ['viewSettings'];
export const CLIENT_LIST = ['clientList'];
export const REPORT = ['report'];

// API URLs
export const apiEntryUrl = `${serverURL}/data`;
Expand Down
26 changes: 26 additions & 0 deletions apps/client/src/common/api/report.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from 'axios';
import { OntimeReport } from 'ontime-types';

import { ontimeQueryClient } from '../../common/queryClient';

import { apiEntryUrl, REPORT } from './constants';

export const reportUrl = `${apiEntryUrl}/report`;

/**
* HTTP request to fetch all events
*/
export async function fetchReport(): Promise<OntimeReport> {
const res = await axios.get(`${reportUrl}/`);
return res.data;
}

export async function deleteReport(id: string) {
await axios.delete(`${reportUrl}/${id}`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}

export async function deleteAllReport() {
await axios.delete(`${reportUrl}/all`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}
20 changes: 20 additions & 0 deletions apps/client/src/common/hooks-query/useReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { OntimeReport } from 'ontime-types';

import { REPORT } from '../api/constants';
import { fetchReport } from '../api/report';

export default function useReport() {
const { data } = useQuery<OntimeReport>({
queryKey: REPORT,
queryFn: fetchReport,
placeholderData: (previousData, _previousQuery) => previousData,
retry: 5,
retryDelay: (attempt) => attempt * 2500,
networkMode: 'always',
refetchOnMount: false,
enabled: true,
});

return { data: data ?? {} };
}
21 changes: 13 additions & 8 deletions apps/client/src/common/utils/socket.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Log, RundownCached, RuntimeStore } from 'ontime-types';

import { isProduction, websocketUrl } from '../../externals';
import { CLIENT_LIST, CUSTOM_FIELDS, RUNDOWN, RUNTIME } from '../api/constants';
import { CLIENT_LIST, CUSTOM_FIELDS, REPORT, RUNDOWN, RUNTIME } from '../api/constants';
import { invalidateAllCaches } from '../api/utils';
import { ontimeQueryClient } from '../queryClient';
import {
Expand Down Expand Up @@ -196,19 +196,24 @@ export const connectSocket = () => {
}
case 'ontime-refetch': {
// the refetch message signals that the rundown has changed in the server side
const { revision, reload } = payload;
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;

const { reload, target } = payload;
if (reload) {
invalidateAllCaches();
} else if (revision > currentRevision) {
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
} else if (target === 'RUNDOWN') {
const { revision } = payload;
const currentRevision = ontimeQueryClient.getQueryData<RundownCached>(RUNDOWN)?.revision ?? -1;
if (revision > currentRevision) {
ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
ontimeQueryClient.invalidateQueries({ queryKey: CUSTOM_FIELDS });
}
} else if (target === 'REPORT') {
ontimeQueryClient.refetchQueries({ queryKey: REPORT });
//we need to use refech and not invalidate
}
break;
}
case 'ontime-flush': {
flushBatchUpdates()
flushBatchUpdates();
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const staticOptions = [
secondary: [
{ id: 'feature_settings__custom', label: 'Custom fields' },
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
{ id: 'feature_settings__report', label: 'Reporter' },
],
},
{
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/features/overview/Overview.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
}

.ahead {
color: $green-500;
color: $playback-ahead;
}

.behind {
Expand Down
12 changes: 11 additions & 1 deletion apps/client/src/features/rundown/RundownEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { OntimeEvent, OntimeRundownEntry, Playback, SupportedEvent } from 'ontime-types';

import { deleteReport } from '../../common/api/report';
import { useEventAction } from '../../common/hooks/useEventAction';
import useMemoisedFn from '../../common/hooks/useMemoisedFn';
import { useEmitLog } from '../../common/stores/logger';
Expand All @@ -22,7 +23,8 @@ export type EventItemActions =
| 'delete'
| 'clone'
| 'update'
| 'swap';
| 'swap'
| 'clear-report';

interface RundownEntryProps {
type: SupportedEvent;
Expand Down Expand Up @@ -142,6 +144,14 @@ export default function RundownEntry(props: RundownEntryProps) {

return emitError(`Unknown field: ${field}`);
}
case 'clear-report': {
const { field, value } = payload as FieldValue;
if (field === undefined || field !== 'id' || value === undefined || typeof value !== 'string') {
return;
}
deleteReport(value);
break;
}
default:
throw new Error(`Unhandled event ${action}`);
}
Expand Down
9 changes: 8 additions & 1 deletion apps/client/src/features/rundown/event-block/EventBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MouseEvent, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
import { IoCloseCircle } from '@react-icons/all-files/io5/IoCloseCircle';
import { IoDuplicateOutline } from '@react-icons/all-files/io5/IoDuplicateOutline';
import { IoLink } from '@react-icons/all-files/io5/IoLink';
import { IoPeople } from '@react-icons/all-files/io5/IoPeople';
Expand Down Expand Up @@ -175,7 +176,13 @@ export default function EventBlock(props: EventBlockProps) {
},
isDisabled: selectedEventId == null || selectedEventId === eventId,
},
{ withDivider: true, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
{ withDivider: false, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
{
withDivider: true,
label: 'Clear report',
icon: IoCloseCircle,
onClick: () => actionHandler('clear-report', { field: 'id', value: eventId }),
},
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function EventBlockInner(props: EventBlockInnerProps) {
isLoaded={loaded}
totalGap={totalGap}
isLinkedAndNext={isNext && linkStart !== null}
duration={duration}
/>
)}
<div className={style.statusElements} id='block-status' data-ispublic={isPublic}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
border-radius: 2px;

&.over {
color: $playback-negative;
color: $ontime-delay-text;
}

&.under {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useMemo } from 'react';
import { Tooltip } from '@chakra-ui/react';
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
import type { OntimeEventReport } from 'ontime-types';
import { isPlaybackActive, MILLIS_PER_MINUTE, MILLIS_PER_SECOND } from 'ontime-utils';

import { usePlayback, useTimelineStatus } from '../../../../common/hooks/useSocket';
import useReport from '../../../../common/hooks-query/useReport';
import { cx } from '../../../../common/utils/styleUtils';
import { formatDuration } from '../../../../common/utils/time';
import { formatDuration, formatTime } from '../../../../common/utils/time';
import { tooltipDelayFast } from '../../../../ontimeConfig';

import style from './EventBlockChip.module.scss';
Expand All @@ -17,10 +20,11 @@ interface EventBlockChipProps {
className: string;
totalGap: number;
isLinkedAndNext: boolean;
duration: number;
}

export default function EventBlockChip(props: EventBlockChipProps) {
const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext } = props;
const { trueTimeStart, isPast, isLoaded, className, totalGap, isLinkedAndNext, id, duration } = props;
const { playback } = usePlayback();

if (isLoaded) {
Expand All @@ -30,7 +34,7 @@ export default function EventBlockChip(props: EventBlockChipProps) {
const playbackActive = isPlaybackActive(playback);

if (!playbackActive || isPast) {
return null; //TODO: Event report will go here
return <EventReport className={className} id={id} duration={duration} />;
}

if (playbackActive) {
Expand Down Expand Up @@ -75,3 +79,55 @@ function EventUntil(props: EventUntilProps) {
</Tooltip>
);
}

interface EventReportProps {
className: string;
id: string;
duration: number;
}

function EventReport(props: EventReportProps) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can make this more efficient.

We know that, once an event has run, the report will never change until it runs again
Do you agree? if so, we can find a way together, to only run this once when the event finishes

const { className, id, duration } = props;
const { data } = useReport();
const currentReport: OntimeEventReport | undefined = data[id];

const [value, overUnderStyle, tooltip] = useMemo(() => {
if (!currentReport) {
return [null, 'none', ''];
}

const { startedAt, endedAt } = currentReport;
if (!startedAt || !endedAt) {
return [null, 'none', ''];
}

const actualDuration = endedAt - startedAt;
const difference = actualDuration - duration;
const absDifference = Math.abs(difference);

if (absDifference < MILLIS_PER_SECOND) {
return ['ontime', 'ontime', 'Event finished ontime'];
}

const isOver = difference > 0;

const fullTimeValue = formatTime(absDifference);

const tooltip = `Event ran ${isOver ? 'over' : 'under'} time by ${fullTimeValue}`;

const value = `${isOver ? '+' : '-'}${formatDuration(absDifference, absDifference > 2 * MILLIS_PER_MINUTE)}`;
return [value, isOver ? 'over' : 'under', tooltip];
}, [currentReport, duration]);

if (!value) {
return null;
}

return (
<Tooltip label={tooltip} openDelay={tooltipDelayFast}>
<div className={cx([style.chip, style[overUnderStyle], className])}>
{value === 'ontime' ? <IoCheckmarkCircle size='1.1rem' /> : value}
</div>
</Tooltip>
);
}
2 changes: 2 additions & 0 deletions apps/server/src/api-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { router as sheetsRouter } from './sheets/sheets.router.js';
import { router as excelRouter } from './excel/excel.router.js';
import { router as sessionRouter } from './session/session.router.js';
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
import { router as reportRouter } from './report/report.router.js';

export const appRouter = express.Router();

Expand All @@ -25,6 +26,7 @@ appRouter.use('/excel', excelRouter);
appRouter.use('/url-presets', urlPresetsRouter);
appRouter.use('/session', sessionRouter);
appRouter.use('/view-settings', viewSettingsRouter);
appRouter.use('/report', reportRouter);

//we don't want to redirect to react index when using api routes
appRouter.all('/*', (_req, res) => {
Expand Down
18 changes: 18 additions & 0 deletions apps/server/src/api-data/report/report.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Request, Response } from 'express';
import type { OntimeReport } from 'ontime-types';
import * as report from './report.service.js';

export async function getAll(_req: Request, res: Response<OntimeReport>) {
res.json(report.generate());
}

export async function deleteAll(_req: Request, res: Response<OntimeReport>) {
report.clear();
res.status(200).send();
}

export async function deleteWithId(req: Request, res: Response<OntimeReport>) {
const { eventId } = req.params;
report.clear(eventId);
res.status(200).send();
}
10 changes: 10 additions & 0 deletions apps/server/src/api-data/report/report.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import express from 'express';
import { getAll, deleteWithId, deleteAll } from './report.controller.js';
import { paramsMustHaveEventId } from '../rundown/rundown.validation.js';

export const router = express.Router();

router.get('/', getAll);

router.delete('/all', deleteAll);
router.delete('/:eventId', paramsMustHaveEventId, deleteWithId);
Loading