Skip to content

Commit a8ffb1a

Browse files
committed
WIP recorder UI
1 parent ed4899f commit a8ffb1a

File tree

4 files changed

+344
-1
lines changed

4 files changed

+344
-1
lines changed

demo/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import defaultConfig from './fixtures/config';
1414

1515
import '@carbon/styles/css/styles.min.css';
1616
import './style.css';
17+
import RecorderBar from './RecorderBar';
1718
import { RPALink, RPATab } from './plugins/RPA';
1819
import {
1920
FooLinkDynamic,
@@ -144,6 +145,7 @@ function App() {
144145

145146
return (
146147
<>
148+
<RecorderBar injector={ injector } />
147149
<div className="modeler" ref={ modelerRef }>
148150
<div id="canvas" className="canvas"></div>
149151
<ResizablePanel className="properties-panel" defaultWidth={ 300 } minWidth={ 200 } maxWidth={ 600 }>

demo/RecorderBar.jsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useState, useCallback, useEffect } from 'react';
2+
3+
import { getProcessId } from '../lib/utils/element';
4+
5+
function generateDefaultName() {
6+
const now = new Date();
7+
const pad = (n, len = 2) => String(n).padStart(len, '0');
8+
return `recording-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
9+
}
10+
11+
/**
12+
* RecorderBar — toolbar for starting/stopping API response recordings
13+
* from the demo UI. Communicates with the server's recorder endpoints.
14+
*
15+
* @param {Object} props
16+
* @param {Object} [props.injector] — modeler injector for selection tracking
17+
*/
18+
export default function RecorderBar({ injector }) {
19+
const [ recording, setRecording ] = useState(false);
20+
const [ name, setName ] = useState(() => generateDefaultName());
21+
const [ elementId, setElementId ] = useState('');
22+
const [ processId, setProcessId ] = useState('');
23+
const [ description, setDescription ] = useState('');
24+
const [ message, setMessage ] = useState(null);
25+
26+
// Track the currently selected element
27+
useEffect(() => {
28+
if (!injector || recording) return;
29+
30+
const selection = injector.get('selection');
31+
const eventBus = injector.get('eventBus');
32+
33+
const update = () => {
34+
const selected = selection.get();
35+
36+
if (selected.length === 1) {
37+
setElementId(selected[0].id);
38+
39+
const pid = getProcessId(selected[0]);
40+
if (pid) {
41+
setProcessId(pid);
42+
}
43+
}
44+
};
45+
46+
update();
47+
48+
eventBus.on('selection.changed', update);
49+
50+
return () => {
51+
eventBus.off('selection.changed', update);
52+
};
53+
}, [ injector, recording ]);
54+
55+
// Auto-fill process ID from the first process in the diagram
56+
useEffect(() => {
57+
if (!injector || processId || recording) return;
58+
59+
try {
60+
const canvas = injector.get('canvas');
61+
const rootElement = canvas.getRootElement();
62+
63+
if (rootElement?.businessObject?.id) {
64+
setProcessId(rootElement.businessObject.id);
65+
}
66+
} catch (e) {
67+
68+
// ignore
69+
}
70+
}, [ injector, processId, recording ]);
71+
72+
useEffect(() => {
73+
fetch('/api/recorder/status')
74+
.then(res => res.json())
75+
.then(({ recording, name, elementId }) => {
76+
setRecording(recording);
77+
78+
if (recording) {
79+
setName(name || '');
80+
setElementId(elementId || '');
81+
}
82+
})
83+
.catch(() => {});
84+
}, []);
85+
86+
const start = useCallback(async () => {
87+
if (!name.trim() || !elementId.trim()) {
88+
setMessage('Name and element ID are required');
89+
return;
90+
}
91+
92+
const response = await fetch('/api/recorder/start', {
93+
method: 'POST',
94+
headers: { 'Content-Type': 'application/json' },
95+
body: JSON.stringify({
96+
name: name.trim(),
97+
elementId: elementId.trim(),
98+
processId: processId.trim() || undefined
99+
})
100+
});
101+
102+
const result = await response.json();
103+
104+
if (result.success) {
105+
setRecording(true);
106+
setMessage(null);
107+
} else {
108+
setMessage(result.error || 'Failed to start recording');
109+
}
110+
}, [ name, elementId, processId ]);
111+
112+
const save = useCallback(async () => {
113+
const response = await fetch('/api/recorder/save', {
114+
method: 'POST',
115+
headers: { 'Content-Type': 'application/json' },
116+
body: JSON.stringify({
117+
description: description.trim() || undefined
118+
})
119+
});
120+
121+
const result = await response.json();
122+
123+
if (result.success) {
124+
setRecording(false);
125+
setMessage(`Saved to ${result.filePath}`);
126+
setName(generateDefaultName());
127+
setDescription('');
128+
} else {
129+
setMessage(result.error || 'Failed to save recording');
130+
}
131+
}, [ description ]);
132+
133+
const discard = useCallback(async () => {
134+
await fetch('/api/recorder/discard', { method: 'POST' });
135+
setRecording(false);
136+
setMessage('Recording discarded');
137+
setName(generateDefaultName());
138+
setDescription('');
139+
}, []);
140+
141+
return (
142+
<div className="recorder-bar">
143+
<span className="recorder-bar__icon">{ recording ? '⏺' : '📼' }</span>
144+
145+
{ !recording ? (
146+
<div className="recorder-bar__form">
147+
<span className="recorder-bar__input-wrapper">
148+
<input
149+
className="recorder-bar__input"
150+
type="text"
151+
placeholder="Scenario name"
152+
value={ name }
153+
onChange={ e => setName(e.target.value) }
154+
/>
155+
{ name && (
156+
<button
157+
className="recorder-bar__clear"
158+
onClick={ () => setName('') }
159+
title="Clear name"
160+
>
161+
×
162+
</button>
163+
) }
164+
</span>
165+
<input
166+
className="recorder-bar__input"
167+
type="text"
168+
placeholder="Element ID"
169+
value={ elementId }
170+
onChange={ e => setElementId(e.target.value) }
171+
/>
172+
<input
173+
className="recorder-bar__input recorder-bar__input--narrow"
174+
type="text"
175+
placeholder="Process ID"
176+
value={ processId }
177+
readOnly
178+
/>
179+
<button className="recorder-bar__button recorder-bar__button--start" onClick={ start }>
180+
Start recording
181+
</button>
182+
</div>
183+
) : (
184+
<div className="recorder-bar__form">
185+
<span className="recorder-bar__status">
186+
Recording: <strong>{ name }</strong> ({ elementId })
187+
</span>
188+
<input
189+
className="recorder-bar__input"
190+
type="text"
191+
placeholder="Description (optional)"
192+
value={ description }
193+
onChange={ e => setDescription(e.target.value) }
194+
/>
195+
<button className="recorder-bar__button recorder-bar__button--save" onClick={ save }>
196+
Save
197+
</button>
198+
<button className="recorder-bar__button recorder-bar__button--discard" onClick={ discard }>
199+
Discard
200+
</button>
201+
</div>
202+
) }
203+
204+
{ message && (
205+
<span className="recorder-bar__message">{ message }</span>
206+
) }
207+
</div>
208+
);
209+
}

demo/server.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ app.post('/api/recorder/discard', (req, res) => {
7878
});
7979

8080
app.get('/api/recorder/status', (req, res) => {
81-
res.json({ recording: recorder.isRecording });
81+
res.json({
82+
recording: recorder.isRecording,
83+
name: recorder._name,
84+
elementId: recorder._elementId
85+
});
8286
});
8387

8488
// ---------------------------------------------------------------------------

demo/style.css

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,132 @@ body {
4949

5050
.djs-minimap {
5151
display: none;
52+
}
53+
54+
/* Recorder bar */
55+
56+
.recorder-bar {
57+
display: flex;
58+
align-items: center;
59+
gap: 8px;
60+
padding: 6px 12px;
61+
background: hsl(225, 10%, 95%);
62+
border-bottom: 1px solid hsl(225, 10%, 75%);
63+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
64+
font-size: 13px;
65+
flex-shrink: 0;
66+
}
67+
68+
.recorder-bar__icon {
69+
font-size: 16px;
70+
flex-shrink: 0;
71+
}
72+
73+
.recorder-bar__form {
74+
display: flex;
75+
align-items: center;
76+
gap: 6px;
77+
flex-wrap: wrap;
78+
}
79+
80+
.recorder-bar__input-wrapper {
81+
position: relative;
82+
display: inline-flex;
83+
}
84+
85+
.recorder-bar__input-wrapper .recorder-bar__input {
86+
padding-right: 22px;
87+
}
88+
89+
.recorder-bar__clear {
90+
position: absolute;
91+
right: 2px;
92+
top: 50%;
93+
transform: translateY(-50%);
94+
background: none;
95+
border: none;
96+
cursor: pointer;
97+
font-size: 14px;
98+
line-height: 1;
99+
color: hsl(225, 10%, 50%);
100+
padding: 0 3px;
101+
}
102+
103+
.recorder-bar__clear:hover {
104+
color: hsl(225, 10%, 20%);
105+
}
106+
107+
.recorder-bar__input {
108+
padding: 3px 8px;
109+
border: 1px solid hsl(225, 10%, 75%);
110+
border-radius: 3px;
111+
font-size: 12px;
112+
font-family: inherit;
113+
min-width: 120px;
114+
}
115+
116+
.recorder-bar__input--narrow {
117+
min-width: 100px;
118+
}
119+
120+
.recorder-bar__input[readonly] {
121+
background: hsl(225, 10%, 92%);
122+
color: hsl(225, 10%, 40%);
123+
}
124+
125+
.recorder-bar__input:focus {
126+
outline: 2px solid #0f62fe;
127+
outline-offset: -1px;
128+
border-color: #0f62fe;
129+
}
130+
131+
.recorder-bar__button {
132+
padding: 3px 10px;
133+
border: none;
134+
border-radius: 3px;
135+
font-size: 12px;
136+
font-family: inherit;
137+
cursor: pointer;
138+
white-space: nowrap;
139+
}
140+
141+
.recorder-bar__button--start {
142+
background: #0f62fe;
143+
color: white;
144+
}
145+
146+
.recorder-bar__button--start:hover {
147+
background: #0043ce;
148+
}
149+
150+
.recorder-bar__button--save {
151+
background: #198038;
152+
color: white;
153+
}
154+
155+
.recorder-bar__button--save:hover {
156+
background: #0e6027;
157+
}
158+
159+
.recorder-bar__button--discard {
160+
background: hsl(225, 10%, 80%);
161+
color: hsl(225, 10%, 20%);
162+
}
163+
164+
.recorder-bar__button--discard:hover {
165+
background: hsl(225, 10%, 70%);
166+
}
167+
168+
.recorder-bar__status {
169+
color: #da1e28;
170+
white-space: nowrap;
171+
}
172+
173+
.recorder-bar__message {
174+
color: hsl(225, 10%, 40%);
175+
font-size: 12px;
176+
margin-left: 4px;
177+
white-space: nowrap;
178+
overflow: hidden;
179+
text-overflow: ellipsis;
52180
}

0 commit comments

Comments
 (0)