feat(automation): label event triggers and outputs clearly

This commit is contained in:
Carlos Valente
2026-09-11 19:56:56 +02:00
parent 4601770d1d
commit ab61d3434b
5 changed files with 109 additions and 3 deletions
@@ -0,0 +1,25 @@
import { TimerLifeCycle } from 'ontime-types';
/**
* User facing labels for the timer lifecycle
* Shared between the automation settings and the rundown event editor
* so that a lifecycle is named the same everywhere it is shown
*/
export const lifecycleLabels: Record<TimerLifeCycle, string> = {
[TimerLifeCycle.onLoad]: 'On Load',
[TimerLifeCycle.onStart]: 'On Start',
[TimerLifeCycle.onPause]: 'On Pause',
[TimerLifeCycle.onStop]: 'On Stop',
[TimerLifeCycle.onClock]: 'Every second',
[TimerLifeCycle.onUpdate]: 'On Timer Update',
[TimerLifeCycle.onFinish]: 'On Finish',
[TimerLifeCycle.onWarning]: 'On Warning',
[TimerLifeCycle.onDanger]: 'On Danger',
};
/**
* Resolves a lifecycle to its user facing label, falling back to the raw value
*/
export function getLifecycleLabel(cycle: TimerLifeCycle | string): string {
return lifecycleLabels[cycle as TimerLifeCycle] ?? cycle;
}
@@ -0,0 +1,32 @@
import type { AutomationOutput } from 'ontime-types';
import { summariseOutputs } from '../automationOutputs';
describe('summariseOutputs', () => {
it('returns an empty list when there are no outputs', () => {
expect(summariseOutputs([])).toEqual([]);
});
it('counts repeated output kinds', () => {
const outputs: AutomationOutput[] = [
{ type: 'osc', targetIP: '127.0.0.1', targetPort: 8000, address: '/go', args: '' },
{ type: 'osc', targetIP: '127.0.0.1', targetPort: 8000, address: '/stop', args: '' },
{ type: 'http', url: 'http://127.0.0.1/start' },
];
expect(summariseOutputs(outputs)).toEqual([
{ type: 'osc', label: 'OSC', count: 2 },
{ type: 'http', label: 'HTTP', count: 1 },
]);
});
it('presents kinds in a stable order regardless of insertion order', () => {
const outputs: AutomationOutput[] = [
{ type: 'ontime', action: 'aux1-start' },
{ type: 'http', url: 'http://127.0.0.1/start' },
{ type: 'osc', targetIP: '127.0.0.1', targetPort: 8000, address: '/go', args: '' },
];
expect(summariseOutputs(outputs).map(({ type }) => type)).toEqual(['osc', 'http', 'ontime']);
});
});
@@ -0,0 +1,32 @@
import type { AutomationOutput } from 'ontime-types';
const outputLabels: Record<AutomationOutput['type'], string> = {
osc: 'OSC',
http: 'HTTP',
ontime: 'Ontime',
};
export type OutputSummary = {
type: AutomationOutput['type'];
label: string;
count: number;
};
/**
* Summarises an automation's outputs by kind so that a list row can say what the
* automation does without the user having to open the form.
* Shared between the automation settings panel and the rundown event editor.
*/
export function summariseOutputs(outputs: AutomationOutput[]): OutputSummary[] {
const counts = new Map<AutomationOutput['type'], number>();
for (const output of outputs) {
counts.set(output.type, (counts.get(output.type) ?? 0) + 1);
}
// keep a stable presentation order regardless of the order the user added outputs
const order: AutomationOutput['type'][] = ['osc', 'http', 'ontime'];
return order
.filter((type) => counts.has(type))
.map((type) => ({ type, label: outputLabels[type], count: counts.get(type) as number }));
}
@@ -15,7 +15,7 @@
.triggerHeader { .triggerHeader {
display: grid; display: grid;
grid-template-columns: 8rem 1fr 2rem; grid-template-columns: 8rem 1fr auto 2rem;
gap: 0.5rem; gap: 0.5rem;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
font-size: $aux-text-size; font-size: $aux-text-size;
@@ -25,7 +25,7 @@
.trigger { .trigger {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
display: grid; display: grid;
grid-template-columns: 8rem 1fr 2rem; grid-template-columns: 8rem 1fr auto 2rem;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
min-height: 2.5rem; min-height: 2.5rem;
@@ -41,6 +41,12 @@
} }
} }
.outputTags {
display: flex;
gap: 0.25rem;
justify-content: flex-end;
}
.duplicateMessage { .duplicateMessage {
padding-left: 0.75rem; padding-left: 0.75rem;
font-size: $aux-text-size; font-size: $aux-text-size;
@@ -6,8 +6,11 @@ import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton'; import IconButton from '../../../../common/components/buttons/IconButton';
import Info from '../../../../common/components/info/Info'; import Info from '../../../../common/components/info/Info';
import Select from '../../../../common/components/select/Select'; import Select from '../../../../common/components/select/Select';
import Tag from '../../../../common/components/tag/Tag';
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext'; import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings'; import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
import { summariseOutputs } from '../../../../common/utils/automationOutputs';
import { eventTriggerOptions } from './eventTrigger.constants'; import { eventTriggerOptions } from './eventTrigger.constants';
import style from './EventEditorTriggers.module.scss'; import style from './EventEditorTriggers.module.scss';
@@ -27,7 +30,7 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
label: title, label: title,
})); }));
const hasAutomationOptions = allAutomationOptions.length > 0; const hasAutomationOptions = allAutomationOptions.length > 0;
const triggerOptions = eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle })); const triggerOptions = eventTriggerOptions.map((cycle) => ({ value: cycle, label: getLifecycleLabel(cycle) }));
const duplicateIds = new Set<string>(); const duplicateIds = new Set<string>();
const seen = new Map<string, string>(); const seen = new Map<string, string>();
@@ -76,6 +79,7 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
<div className={style.triggerHeader}> <div className={style.triggerHeader}>
<span>Lifecycle</span> <span>Lifecycle</span>
<span>Automation</span> <span>Automation</span>
<span>Sends</span>
</div> </div>
{triggers.map((trigger) => { {triggers.map((trigger) => {
const isDuplicate = duplicateIds.has(trigger.id); const isDuplicate = duplicateIds.has(trigger.id);
@@ -103,6 +107,13 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
}} }}
options={automationOptions} options={automationOptions}
/> />
<div className={style.outputTags}>
{summariseOutputs(automationSettings.automations[trigger.automationId]?.outputs ?? []).map(
({ type, label, count }) => (
<Tag key={type}>{count > 1 ? `${label} ×${count}` : label}</Tag>
),
)}
</div>
<IconButton variant='ghosted-destructive' onClick={() => handleDelete(trigger.id)}> <IconButton variant='ghosted-destructive' onClick={() => handleDelete(trigger.id)}>
<IoTrash /> <IoTrash />
</IconButton> </IconButton>