mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-12 09:39:37 +00:00
feat(automation): label event triggers and outputs clearly
This commit is contained in:
@@ -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 }));
|
||||||
|
}
|
||||||
+8
-2
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user