@adcops/autocore-react 3.3.73 → 3.3.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/assets/HomeMotor.d.ts +4 -0
  2. package/dist/assets/HomeMotor.d.ts.map +1 -0
  3. package/dist/assets/HomeMotor.js +1 -0
  4. package/dist/assets/svg/home_motor.svg +57 -0
  5. package/dist/components/Indicator.d.ts +29 -52
  6. package/dist/components/Indicator.d.ts.map +1 -1
  7. package/dist/components/Indicator.js +1 -1
  8. package/dist/components/ValueInput.d.ts +1 -1
  9. package/dist/components/ValueInput.d.ts.map +1 -1
  10. package/dist/components/ams/AmsProvider.d.ts +7 -0
  11. package/dist/components/ams/AmsProvider.d.ts.map +1 -1
  12. package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
  13. package/dist/components/ams/AssetDetailView.js +1 -1
  14. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -1
  15. package/dist/components/ams/AssetRegistryTable.js +1 -1
  16. package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -1
  17. package/dist/components/ams/CalibrationEntryDialog.js +1 -1
  18. package/dist/components/ams/MissingAssetsBanner.d.ts +11 -0
  19. package/dist/components/ams/MissingAssetsBanner.d.ts.map +1 -0
  20. package/dist/components/ams/MissingAssetsBanner.js +1 -0
  21. package/dist/components/ams/PlaceholderHealthPanel.d.ts +3 -0
  22. package/dist/components/ams/PlaceholderHealthPanel.d.ts.map +1 -0
  23. package/dist/components/ams/PlaceholderHealthPanel.js +1 -0
  24. package/dist/components/ams/index.d.ts +2 -0
  25. package/dist/components/ams/index.d.ts.map +1 -1
  26. package/dist/components/ams/index.js +1 -1
  27. package/dist/components/index.d.ts +8 -0
  28. package/dist/components/index.d.ts.map +1 -1
  29. package/dist/components/index.js +1 -1
  30. package/dist/components/network/NetworkPanel.d.ts +8 -0
  31. package/dist/components/network/NetworkPanel.d.ts.map +1 -0
  32. package/dist/components/network/NetworkPanel.js +1 -0
  33. package/dist/components/network/NetworkProvider.d.ts +72 -0
  34. package/dist/components/network/NetworkProvider.d.ts.map +1 -0
  35. package/dist/components/network/NetworkProvider.js +1 -0
  36. package/dist/components/network/StagedChangeBanner.d.ts +8 -0
  37. package/dist/components/network/StagedChangeBanner.d.ts.map +1 -0
  38. package/dist/components/network/StagedChangeBanner.js +1 -0
  39. package/dist/components/network/index.d.ts +7 -0
  40. package/dist/components/network/index.d.ts.map +1 -0
  41. package/dist/components/network/index.js +1 -0
  42. package/dist/components/tis/ProjectManager.d.ts +7 -0
  43. package/dist/components/tis/ProjectManager.d.ts.map +1 -0
  44. package/dist/components/tis/ProjectManager.js +1 -0
  45. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
  46. package/dist/components/tis/ResultHistoryTable.js +1 -1
  47. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  48. package/dist/components/tis/TestDataView.js +1 -1
  49. package/dist/components/tis/TestRawDataView.d.ts.map +1 -1
  50. package/dist/components/tis/TestRawDataView.js +1 -1
  51. package/dist/components/tis/TestSetupForm.d.ts +7 -0
  52. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  53. package/dist/components/tis/TestSetupForm.js +1 -1
  54. package/package.json +5 -1
  55. package/src/assets/HomeMotor.tsx +37 -0
  56. package/src/assets/svg/home_motor.svg +57 -0
  57. package/src/components/Indicator.tsx +166 -162
  58. package/src/components/ValueInput.tsx +2 -2
  59. package/src/components/ams/AmsProvider.tsx +7 -0
  60. package/src/components/ams/AssetDetailView.tsx +287 -4
  61. package/src/components/ams/AssetRegistryTable.tsx +325 -21
  62. package/src/components/ams/CalibrationEntryDialog.tsx +163 -30
  63. package/src/components/ams/MissingAssetsBanner.tsx +124 -0
  64. package/src/components/ams/PlaceholderHealthPanel.tsx +188 -0
  65. package/src/components/ams/index.ts +2 -0
  66. package/src/components/index.ts +26 -0
  67. package/src/components/network/NetworkPanel.tsx +363 -0
  68. package/src/components/network/NetworkProvider.tsx +349 -0
  69. package/src/components/network/StagedChangeBanner.tsx +101 -0
  70. package/src/components/network/index.ts +17 -0
  71. package/src/components/tis/ProjectManager.tsx +392 -0
  72. package/src/components/tis/ResultHistoryTable.tsx +125 -74
  73. package/src/components/tis/TestDataView.tsx +160 -14
  74. package/src/components/tis/TestRawDataView.tsx +118 -8
  75. package/src/components/tis/TestSetupForm.tsx +42 -1
@@ -0,0 +1,363 @@
1
+ /*
2
+ * Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
3
+ *
4
+ * <NetworkPanel> — operator-facing network configuration view.
5
+ *
6
+ * 1. Status row: WiFi radio toggle + active connection summary.
7
+ * 2. WiFi: scan button, table of nearby SSIDs with Connect action.
8
+ * 3. Wired (read-only): one row per ethernet interface with its
9
+ * runtime IP / gateway / DNS, sourced from the netplan-rendered
10
+ * NetworkManager state.
11
+ *
12
+ * Pair with <StagedChangeBanner> at the top of the page so the
13
+ * countdown is visible regardless of which tab is foregrounded.
14
+ */
15
+
16
+ import React, { useEffect, useState } from 'react';
17
+ import { Button } from 'primereact/button';
18
+ import { DataTable } from 'primereact/datatable';
19
+ import { Column } from 'primereact/column';
20
+ import { Dialog } from 'primereact/dialog';
21
+ import { Password } from 'primereact/password';
22
+ import { InputSwitch } from 'primereact/inputswitch';
23
+ import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
24
+ import { useNetwork, type WifiAp, type NetworkInterface } from './NetworkProvider';
25
+
26
+ const signalBars = (signal: number): string => {
27
+ if (signal >= 75) return '▰▰▰▰';
28
+ if (signal >= 50) return '▰▰▰▱';
29
+ if (signal >= 25) return '▰▰▱▱';
30
+ if (signal > 0) return '▰▱▱▱';
31
+ return '▱▱▱▱';
32
+ };
33
+
34
+ const isSecured = (security: string): boolean => {
35
+ const s = (security ?? '').trim();
36
+ return s.length > 0 && s !== '--';
37
+ };
38
+
39
+ export interface NetworkPanelProps {
40
+ /** Optional CSS class on the outer container. */
41
+ className?: string;
42
+ }
43
+
44
+ export const NetworkPanel: React.FC<NetworkPanelProps> = ({ className }) => {
45
+ const net = useNetwork();
46
+ const [connectTarget, setConnectTarget] = useState<WifiAp | null>(null);
47
+ const [password, setPassword] = useState('');
48
+ const [submitting, setSubmitting] = useState(false);
49
+
50
+ // Kick off an initial scan when the panel mounts so the WiFi table
51
+ // isn't empty on first paint. Skipped when a scan is already in
52
+ // flight to avoid stomping a fresh result.
53
+ useEffect(() => {
54
+ if (net.aps.length === 0 && !net.scanning) {
55
+ void net.scanWifi();
56
+ }
57
+ // eslint-disable-next-line react-hooks/exhaustive-deps
58
+ }, []);
59
+
60
+ const wired: NetworkInterface[] = net.status.interfaces.filter(i => i.type === 'ethernet');
61
+ const wifiDevices: NetworkInterface[] = net.status.interfaces.filter(i => i.type === 'wifi');
62
+ const activeWifi = wifiDevices.find(d => d.state === 'connected') ?? wifiDevices[0];
63
+
64
+ const openConnect = (ap: WifiAp) => {
65
+ setConnectTarget(ap);
66
+ setPassword('');
67
+ };
68
+
69
+ const submitConnect = async () => {
70
+ if (!connectTarget) return;
71
+ setSubmitting(true);
72
+ try {
73
+ const pw = isSecured(connectTarget.security) ? password : undefined;
74
+ const result = await net.wifiConnect(connectTarget.ssid, pw);
75
+ if (result) {
76
+ setConnectTarget(null);
77
+ setPassword('');
78
+ }
79
+ } finally {
80
+ setSubmitting(false);
81
+ }
82
+ };
83
+
84
+ const confirmForget = (conn: { name: string; uuid: string }) => {
85
+ confirmDialog({
86
+ header: 'Forget network',
87
+ icon: 'pi pi-exclamation-triangle',
88
+ acceptLabel: 'Forget',
89
+ rejectLabel: 'Cancel',
90
+ acceptClassName: 'p-button-danger',
91
+ message: (
92
+ <div>
93
+ <p style={{ marginTop: 0 }}>
94
+ Remove saved profile <code>{conn.name}</code>?
95
+ </p>
96
+ <p style={{ marginBottom: 0, fontSize: '0.875rem', color: '#9ca3af' }}>
97
+ The password is deleted. You'll need to re-enter it next
98
+ time you connect to this network.
99
+ </p>
100
+ </div>
101
+ ),
102
+ accept: () => { void net.forgetConnection(conn.uuid); },
103
+ });
104
+ };
105
+
106
+ return (
107
+ <div className={className} style={{ width: '100%', maxWidth: '100%', boxSizing: 'border-box' }}>
108
+ <ConfirmDialog />
109
+
110
+ {/* ----------------------------------------------------- Status */}
111
+ <div style={{
112
+ display: 'flex',
113
+ justifyContent: 'space-between',
114
+ alignItems: 'center',
115
+ marginBottom: '1rem',
116
+ gap: '0.5rem',
117
+ flexWrap: 'wrap',
118
+ }}>
119
+ <div>
120
+ <h3 style={{ margin: 0 }}>Network</h3>
121
+ {activeWifi ? (
122
+ <div style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '0.25rem' }}>
123
+ {activeWifi.state === 'connected'
124
+ ? <>Connected to <code>{activeWifi.connection || '(unnamed)'}</code> on <code>{activeWifi.device}</code></>
125
+ : <>WiFi: {activeWifi.state}</>}
126
+ {activeWifi.ip4.addresses.length > 0 && (
127
+ <> &middot; {activeWifi.ip4.addresses[0]}</>
128
+ )}
129
+ </div>
130
+ ) : (
131
+ <div style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '0.25rem' }}>
132
+ No WiFi device detected.
133
+ </div>
134
+ )}
135
+ </div>
136
+ <div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center', flexWrap: 'wrap' }}>
137
+ <label style={{ display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
138
+ <span style={{ fontSize: '0.875rem' }}>WiFi radio</span>
139
+ <InputSwitch
140
+ checked={!!net.status.wifi_radio}
141
+ onChange={(e) => { void net.setRadio(!!e.value); }}
142
+ disabled={net.status.wifi_radio === null}
143
+ />
144
+ </label>
145
+ <Button
146
+ icon="pi pi-refresh"
147
+ label="Refresh"
148
+ size="small"
149
+ onClick={() => { void net.refreshStatus(); }}
150
+ />
151
+ </div>
152
+ </div>
153
+
154
+ {net.lastError && (
155
+ <div style={{
156
+ background: '#7f1d1d',
157
+ color: 'white',
158
+ padding: '0.5rem 0.75rem',
159
+ borderRadius: 4,
160
+ marginBottom: '1rem',
161
+ fontSize: '0.875rem',
162
+ }}>
163
+ {net.lastError}
164
+ </div>
165
+ )}
166
+
167
+ {/* ----------------------------------------------------- WiFi */}
168
+ <div style={{
169
+ marginBottom: '1.5rem',
170
+ padding: '0.75rem 1rem',
171
+ border: '1px solid #2a2a2a',
172
+ borderRadius: 4,
173
+ background: '#161616',
174
+ }}>
175
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.75rem' }}>
176
+ <strong>WiFi Networks</strong>
177
+ <Button
178
+ icon={net.scanning ? 'pi pi-spin pi-spinner' : 'pi pi-search'}
179
+ label={net.scanning ? 'Scanning…' : 'Scan'}
180
+ size="small"
181
+ outlined
182
+ disabled={net.scanning || net.status.wifi_radio === false}
183
+ onClick={() => { void net.scanWifi(); }}
184
+ />
185
+ </div>
186
+ <DataTable
187
+ value={net.aps}
188
+ emptyMessage={net.status.wifi_radio === false
189
+ ? 'WiFi radio is off.'
190
+ : 'No networks found yet. Click Scan to refresh.'}
191
+ scrollable
192
+ scrollHeight="20rem"
193
+ tableStyle={{ minWidth: 0 }}
194
+ style={{ width: '100%' }}
195
+ >
196
+ <Column header="SSID" body={(ap: WifiAp) => (
197
+ <span>
198
+ {ap.in_use && <i className="pi pi-check" style={{ marginRight: '0.4rem', color: '#22c55e' }} />}
199
+ {ap.ssid}
200
+ </span>
201
+ )} style={{ minWidth: '10rem' }} />
202
+ <Column header="Signal" body={(ap: WifiAp) => (
203
+ <span style={{ fontFamily: 'monospace' }}>
204
+ {signalBars(ap.signal)} <span style={{ color: '#9ca3af' }}>{ap.signal}%</span>
205
+ </span>
206
+ )} style={{ width: '10rem' }}
207
+ sortable sortField="signal" />
208
+ <Column header="Security" body={(ap: WifiAp) => (
209
+ <span>{isSecured(ap.security) ? ap.security : 'Open'}</span>
210
+ )} style={{ width: '8rem' }} />
211
+ <Column
212
+ header="Action"
213
+ style={{ width: '9rem' }}
214
+ body={(ap: WifiAp) => (
215
+ <Button
216
+ icon="pi pi-link"
217
+ label={ap.in_use ? 'Reconnect' : 'Connect'}
218
+ size="small"
219
+ outlined
220
+ disabled={!!net.staged}
221
+ onClick={() => openConnect(ap)}
222
+ />
223
+ )}
224
+ />
225
+ </DataTable>
226
+ </div>
227
+
228
+ {/* ----------------------------------------------------- Saved profiles */}
229
+ {net.status.connections.length > 0 && (
230
+ <div style={{
231
+ marginBottom: '1.5rem',
232
+ padding: '0.75rem 1rem',
233
+ border: '1px solid #2a2a2a',
234
+ borderRadius: 4,
235
+ background: '#161616',
236
+ }}>
237
+ <strong style={{ display: 'block', marginBottom: '0.75rem' }}>Saved Connections</strong>
238
+ <DataTable
239
+ value={net.status.connections}
240
+ emptyMessage="No saved profiles."
241
+ tableStyle={{ minWidth: 0 }}
242
+ style={{ width: '100%' }}
243
+ >
244
+ <Column field="name" header="Name" style={{ minWidth: '10rem' }} />
245
+ <Column field="type" header="Type" style={{ width: '8rem' }} />
246
+ <Column field="device" header="Device" style={{ minWidth: '8rem' }} />
247
+ <Column header="Active" style={{ width: '6rem' }}
248
+ body={(c) => c.active ? <i className="pi pi-check" style={{ color: '#22c55e' }} /> : null} />
249
+ <Column
250
+ header="Action"
251
+ style={{ width: '8rem' }}
252
+ body={(c) => c.type === 'wifi' ? (
253
+ <Button
254
+ icon="pi pi-trash"
255
+ label="Forget"
256
+ size="small"
257
+ severity="danger"
258
+ outlined
259
+ onClick={() => confirmForget(c)}
260
+ />
261
+ ) : null}
262
+ />
263
+ </DataTable>
264
+ </div>
265
+ )}
266
+
267
+ {/* ----------------------------------------------------- Wired (read-only) */}
268
+ <div style={{
269
+ padding: '0.75rem 1rem',
270
+ border: '1px solid #2a2a2a',
271
+ borderRadius: 4,
272
+ background: '#161616',
273
+ }}>
274
+ <strong style={{ display: 'block', marginBottom: '0.25rem' }}>Wired Interfaces</strong>
275
+ <div style={{ fontSize: '0.75rem', color: '#6b7280', marginBottom: '0.75rem' }}>
276
+ Wired configuration is managed by netplan and is read-only here.
277
+ </div>
278
+ <DataTable
279
+ value={wired}
280
+ emptyMessage="No wired interfaces."
281
+ tableStyle={{ minWidth: 0 }}
282
+ style={{ width: '100%' }}
283
+ >
284
+ <Column field="device" header="Device" style={{ minWidth: '8rem' }} />
285
+ <Column field="state" header="State" style={{ width: '8rem' }} />
286
+ <Column header="IP Address" body={(d: NetworkInterface) =>
287
+ d.ip4.addresses.length > 0 ? d.ip4.addresses.join(', ') : <span style={{ color: '#6b7280' }}>—</span>
288
+ } style={{ minWidth: '10rem' }} />
289
+ <Column header="Gateway" body={(d: NetworkInterface) =>
290
+ d.ip4.gateway || <span style={{ color: '#6b7280' }}>—</span>
291
+ } style={{ minWidth: '8rem' }} />
292
+ <Column header="DNS" body={(d: NetworkInterface) =>
293
+ d.ip4.dns.length > 0 ? d.ip4.dns.join(', ') : <span style={{ color: '#6b7280' }}>—</span>
294
+ } style={{ minWidth: '8rem' }} />
295
+ </DataTable>
296
+ </div>
297
+
298
+ {/* ----------------------------------------------------- Connect dialog */}
299
+ <Dialog
300
+ header={connectTarget ? `Connect to ${connectTarget.ssid}` : 'Connect'}
301
+ visible={!!connectTarget}
302
+ style={{ width: '24rem' }}
303
+ onHide={() => { if (!submitting) setConnectTarget(null); }}
304
+ modal
305
+ draggable={false}
306
+ >
307
+ {connectTarget && (
308
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
309
+ <div style={{ fontSize: '0.875rem', color: '#9ca3af' }}>
310
+ <div>Signal: {connectTarget.signal}%</div>
311
+ <div>Security: {isSecured(connectTarget.security) ? connectTarget.security : 'Open'}</div>
312
+ </div>
313
+ {isSecured(connectTarget.security) ? (
314
+ <>
315
+ <label htmlFor="nw-pw" style={{ fontSize: '0.875rem' }}>Password</label>
316
+ <Password
317
+ inputId="nw-pw"
318
+ value={password}
319
+ onChange={(e) => setPassword(e.target.value)}
320
+ feedback={false}
321
+ toggleMask
322
+ autoFocus
323
+ />
324
+ </>
325
+ ) : (
326
+ <div style={{ fontSize: '0.875rem' }}>
327
+ This is an open network. No password required.
328
+ </div>
329
+ )}
330
+ <div style={{
331
+ background: '#78350f',
332
+ color: 'white',
333
+ padding: '0.5rem 0.75rem',
334
+ borderRadius: 4,
335
+ fontSize: '0.875rem',
336
+ }}>
337
+ After connecting, you have 60 seconds to confirm the
338
+ change. If you don't, the server will auto-revert
339
+ to the previous network — that way a bad password
340
+ from a remote session can't lock you out.
341
+ </div>
342
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.4rem' }}>
343
+ <Button
344
+ label="Cancel"
345
+ outlined
346
+ onClick={() => setConnectTarget(null)}
347
+ disabled={submitting}
348
+ />
349
+ <Button
350
+ label={submitting ? 'Connecting…' : 'Connect'}
351
+ icon={submitting ? 'pi pi-spin pi-spinner' : 'pi pi-link'}
352
+ onClick={() => { void submitConnect(); }}
353
+ disabled={submitting || (isSecured(connectTarget.security) && password.length === 0)}
354
+ />
355
+ </div>
356
+ </div>
357
+ )}
358
+ </Dialog>
359
+ </div>
360
+ );
361
+ };
362
+
363
+ export default NetworkPanel;