mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-10 16:49:41 +00:00
refactor(automation): name lifecycles and outputs the same way everywhere
A lifecycle was written three ways depending on where you were standing: "On Start" in the automation settings, the raw "onStart" in the event editor, and again as a literal in the trigger list. Anyone attaching an automation to an event had to work out that the two lists were the same list. One label map now owns the user facing names, and one helper summarises what an automation sends. Both are shared, so the event editor gains the labels and a Sends column for free: the trigger row says which automation runs and what it will do, instead of only its title. Two things the panel could not say before, now that it can look them up: - A trigger can outlive the automation it points at, say after a partial project import. It rendered as an empty tag; it now says so. - not_contains is in the type and the runtime, but the server validation list omits it, so an automation using it cannot be saved. The operator list moves out of the form and drops it, with a test to stop it coming back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
This commit is contained in:
@@ -31,7 +31,12 @@ export default function TriggersListItem(props: TriggersListItemProps) {
|
||||
<Tag>{cycles.find((cycle) => cycle.value === trigger.trigger)?.label}</Tag>
|
||||
</td>
|
||||
<td>
|
||||
<Tag>{automations?.[trigger.automationId]?.title}</Tag>
|
||||
{/* a trigger can outlive the automation it points at, say after a partial project import */}
|
||||
{automations?.[trigger.automationId] ? (
|
||||
<Tag>{automations[trigger.automationId].title}</Tag>
|
||||
) : (
|
||||
<Tag variant='warning'>Missing automation</Tag>
|
||||
)}
|
||||
</td>
|
||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={handleEdit}>
|
||||
|
||||
+41
-1
@@ -1,6 +1,6 @@
|
||||
import { TimerLifeCycle, Trigger } from 'ontime-types';
|
||||
|
||||
import { checkDuplicates } from '../automationUtils';
|
||||
import { checkDuplicates, cycles, groupTriggersByAutomation, operators } from '../automationUtils';
|
||||
|
||||
describe('checkDuplicates', () => {
|
||||
it('should return undefined if there are no duplicates', () => {
|
||||
@@ -22,3 +22,43 @@ describe('checkDuplicates', () => {
|
||||
expect(checkDuplicates(triggers)).toStrictEqual([2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupTriggersByAutomation', () => {
|
||||
it('returns an empty object when there are no triggers', () => {
|
||||
expect(groupTriggersByAutomation([])).toEqual({});
|
||||
});
|
||||
|
||||
it('collects the lifecycles each automation is bound to', () => {
|
||||
const triggers: Trigger[] = [
|
||||
{ id: '1', title: 'First', trigger: TimerLifeCycle.onStart, automationId: 'a' },
|
||||
{ id: '2', title: 'Second', trigger: TimerLifeCycle.onFinish, automationId: 'a' },
|
||||
{ id: '3', title: 'Third', trigger: TimerLifeCycle.onLoad, automationId: 'b' },
|
||||
];
|
||||
|
||||
expect(groupTriggersByAutomation(triggers)).toEqual({
|
||||
a: [TimerLifeCycle.onStart, TimerLifeCycle.onFinish],
|
||||
b: [TimerLifeCycle.onLoad],
|
||||
});
|
||||
});
|
||||
|
||||
it('collapses duplicates, the runtime only fires an automation once per lifecycle', () => {
|
||||
const triggers: Trigger[] = [
|
||||
{ id: '1', title: 'First', trigger: TimerLifeCycle.onStart, automationId: 'a' },
|
||||
{ id: '2', title: 'Second', trigger: TimerLifeCycle.onStart, automationId: 'a' },
|
||||
];
|
||||
|
||||
expect(groupTriggersByAutomation(triggers)).toEqual({ a: [TimerLifeCycle.onStart] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('operators', () => {
|
||||
it('does not offer not_contains, which the server validation rejects', () => {
|
||||
expect(operators.map(({ value }) => value)).not.toContain('not_contains');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cycles', () => {
|
||||
it('uses the shared user facing labels', () => {
|
||||
expect(cycles.find(({ value }) => value === 'onStart')?.label).toBe('On Start');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Automation, AutomationDTO, CustomFields, TimerLifeCycle, Trigger } from 'ontime-types';
|
||||
import { Automation, AutomationDTO, AutomationFilter, CustomFields, TimerLifeCycle, Trigger } from 'ontime-types';
|
||||
|
||||
import { lifecycleLabels } from '../../../../common/constants/timerLifecycle';
|
||||
|
||||
type CycleLabel = {
|
||||
id: number;
|
||||
@@ -7,15 +9,29 @@ type CycleLabel = {
|
||||
};
|
||||
|
||||
export const cycles: CycleLabel[] = [
|
||||
{ id: 1, label: 'On Load', value: 'onLoad' },
|
||||
{ id: 2, label: 'On Start', value: 'onStart' },
|
||||
{ id: 3, label: 'On Pause', value: 'onPause' },
|
||||
{ id: 4, label: 'On Stop', value: 'onStop' },
|
||||
{ id: 5, label: 'Every second', value: 'onClock' },
|
||||
{ id: 6, label: 'On Timer Update', value: 'onUpdate' },
|
||||
{ id: 7, label: 'On Finish', value: 'onFinish' },
|
||||
{ id: 8, label: 'On Warning', value: 'onWarning' },
|
||||
{ id: 9, label: 'On Danger', value: 'onDanger' },
|
||||
{ id: 1, label: lifecycleLabels.onLoad, value: 'onLoad' },
|
||||
{ id: 2, label: lifecycleLabels.onStart, value: 'onStart' },
|
||||
{ id: 3, label: lifecycleLabels.onPause, value: 'onPause' },
|
||||
{ id: 4, label: lifecycleLabels.onStop, value: 'onStop' },
|
||||
{ id: 5, label: lifecycleLabels.onClock, value: 'onClock' },
|
||||
{ id: 6, label: lifecycleLabels.onUpdate, value: 'onUpdate' },
|
||||
{ id: 7, label: lifecycleLabels.onFinish, value: 'onFinish' },
|
||||
{ id: 8, label: lifecycleLabels.onWarning, value: 'onWarning' },
|
||||
{ id: 9, label: lifecycleLabels.onDanger, value: 'onDanger' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Filter operators offered in the automation form
|
||||
* NOTE: not_contains is supported by the type and by the runtime, but the server
|
||||
* validation list omits it, so an automation using it cannot be saved.
|
||||
* It stays out of the UI until the server accepts it.
|
||||
*/
|
||||
export const operators: Array<{ value: AutomationFilter['operator']; label: string }> = [
|
||||
{ value: 'equals', label: 'equals' },
|
||||
{ value: 'not_equals', label: 'does not equal' },
|
||||
{ value: 'contains', label: 'contains' },
|
||||
{ value: 'greater_than', label: 'is greater than' },
|
||||
{ value: 'less_than', label: 'is less than' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -83,3 +99,23 @@ export function checkDuplicates(triggers: Trigger[]) {
|
||||
}
|
||||
return duplicates.length > 0 ? duplicates : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the lifecycles each automation is bound to
|
||||
* Used to show when an automation runs, and to highlight the ones that never will
|
||||
*/
|
||||
export function groupTriggersByAutomation(triggers: Trigger[]): Record<string, TimerLifeCycle[]> {
|
||||
const grouped: Record<string, TimerLifeCycle[]> = {};
|
||||
|
||||
for (const trigger of triggers) {
|
||||
if (!Object.hasOwn(grouped, trigger.automationId)) {
|
||||
grouped[trigger.automationId] = [];
|
||||
}
|
||||
// the runtime fires an automation once per lifecycle, duplicates would be noise here
|
||||
if (!grouped[trigger.automationId].includes(trigger.trigger)) {
|
||||
grouped[trigger.automationId].push(trigger.trigger);
|
||||
}
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user