mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-12 17:49:37 +00:00
feat(automation): clarify trigger lifecycle and outputs
Use consistent lifecycle labels and output summaries in the automation and event editor views.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
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 }));
|
||||
}
|
||||
+8
-2
@@ -15,7 +15,7 @@
|
||||
|
||||
.triggerHeader {
|
||||
display: grid;
|
||||
grid-template-columns: 8rem 1fr 2rem;
|
||||
grid-template-columns: 8rem 1fr auto 2rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: $aux-text-size;
|
||||
@@ -25,7 +25,7 @@
|
||||
.trigger {
|
||||
padding: 0.5rem 0.75rem;
|
||||
display: grid;
|
||||
grid-template-columns: 8rem 1fr 2rem;
|
||||
grid-template-columns: 8rem 1fr auto 2rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 2.5rem;
|
||||
@@ -41,6 +41,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.outputTags {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.duplicateMessage {
|
||||
padding-left: 0.75rem;
|
||||
font-size: $aux-text-size;
|
||||
|
||||
@@ -6,8 +6,11 @@ import Button from '../../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import Info from '../../../../common/components/info/Info';
|
||||
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 useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import { summariseOutputs } from '../../../../common/utils/automationOutputs';
|
||||
import { eventTriggerOptions } from './eventTrigger.constants';
|
||||
|
||||
import style from './EventEditorTriggers.module.scss';
|
||||
@@ -27,7 +30,7 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
|
||||
label: title,
|
||||
}));
|
||||
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 seen = new Map<string, string>();
|
||||
@@ -76,8 +79,10 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
|
||||
<div className={style.triggerHeader}>
|
||||
<span>Lifecycle</span>
|
||||
<span>Automation</span>
|
||||
<span>Sends</span>
|
||||
</div>
|
||||
{triggers.map((trigger) => {
|
||||
const automation = automationSettings.automations[trigger.automationId];
|
||||
const isDuplicate = duplicateIds.has(trigger.id);
|
||||
const lifecycleOptions = isDuplicate
|
||||
? triggerOptions.map((opt) => (opt.value === trigger.trigger ? { ...opt, label: `${opt.label} *` } : opt))
|
||||
@@ -103,6 +108,15 @@ export default function EventEditorTriggers({ triggers, eventId }: EventEditorTr
|
||||
}}
|
||||
options={automationOptions}
|
||||
/>
|
||||
<div className={style.outputTags}>
|
||||
{automation ? (
|
||||
summariseOutputs(automation.outputs).map(({ type, label, count }) => (
|
||||
<Tag key={type}>{count > 1 ? `${label} ×${count}` : label}</Tag>
|
||||
))
|
||||
) : (
|
||||
<Tag variant='warning'>Missing automation</Tag>
|
||||
)}
|
||||
</div>
|
||||
<IconButton variant='ghosted-destructive' onClick={() => handleDelete(trigger.id)}>
|
||||
<IoTrash />
|
||||
</IconButton>
|
||||
|
||||
Reference in New Issue
Block a user