@adcops/autocore-react 3.3.59 → 3.3.63
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.
- package/dist/components/ams/AmsProvider.d.ts +45 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -0
- package/dist/components/ams/AmsProvider.js +1 -0
- package/dist/components/ams/AssetDetailView.d.ts +3 -0
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -0
- package/dist/components/ams/AssetDetailView.js +1 -0
- package/dist/components/ams/AssetRegistryTable.d.ts +3 -0
- package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -0
- package/dist/components/ams/AssetRegistryTable.js +1 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts +10 -0
- package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -0
- package/dist/components/ams/CalibrationEntryDialog.js +1 -0
- package/dist/components/ams/SubLocationPicker.d.ts +3 -0
- package/dist/components/ams/SubLocationPicker.d.ts.map +1 -0
- package/dist/components/ams/SubLocationPicker.js +1 -0
- package/dist/components/ams/index.d.ts +6 -0
- package/dist/components/ams/index.d.ts.map +1 -0
- package/dist/components/ams/index.js +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis/ProjectSelector.d.ts +15 -0
- package/dist/components/tis/ProjectSelector.d.ts.map +1 -0
- package/dist/components/tis/ProjectSelector.js +1 -0
- package/dist/components/tis/TestDataView.d.ts +9 -1
- package/dist/components/tis/TestDataView.d.ts.map +1 -1
- package/dist/components/tis/TestDataView.js +1 -1
- package/dist/components/tis/TestSetupForm.d.ts +8 -4
- package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis/TisProvider.d.ts +45 -0
- package/dist/components/tis/TisProvider.d.ts.map +1 -1
- package/dist/components/tis/TisProvider.js +1 -1
- package/dist/core/AutoCoreTagContext.d.ts +16 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/themes/adc-dark/blue/theme.css +67 -37
- package/dist/themes/adc-dark/blue/theme.css.map +1 -1
- package/package.json +1 -1
- package/src/components/ams/AmsProvider.tsx +219 -0
- package/src/components/ams/AssetDetailView.tsx +101 -0
- package/src/components/ams/AssetRegistryTable.tsx +171 -0
- package/src/components/ams/CalibrationEntryDialog.tsx +197 -0
- package/src/components/ams/SubLocationPicker.tsx +146 -0
- package/src/components/ams/index.ts +12 -0
- package/src/components/index.ts +30 -0
- package/src/components/tis/ProjectSelector.tsx +190 -0
- package/src/components/tis/TestDataView.tsx +321 -28
- package/src/components/tis/TestSetupForm.tsx +66 -253
- package/src/components/tis/TisProvider.tsx +192 -1
- package/src/core/AutoCoreTagContext.tsx +114 -16
- package/src/themes/adc-dark/_extensions.scss +15 -0
- package/src/themes/adc-dark/blue/adc_theme.scss +56 -10
|
@@ -90,6 +90,45 @@ export interface TisContextValue {
|
|
|
90
90
|
selection: TisSelection;
|
|
91
91
|
setSelection: (patch: TisSelectionPatch) => void;
|
|
92
92
|
|
|
93
|
+
// -----------------------------------------------------------------
|
|
94
|
+
// Project management — used by both <ProjectSelector> (Project tab)
|
|
95
|
+
// and <TestSetupForm> (Test tab) so they share a single source of
|
|
96
|
+
// truth for "which projects are real" and "what fields does the
|
|
97
|
+
// current one have." Keeping this in the provider rather than in
|
|
98
|
+
// TestSetupForm lets the two components live in different tabs
|
|
99
|
+
// without prop-threading.
|
|
100
|
+
// -----------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/** Project IDs returned by the server's `tis.list_projects`. */
|
|
103
|
+
existingProjects: string[];
|
|
104
|
+
/** True when the project either exists on disk OR was created in
|
|
105
|
+
* this browser session via `<ProjectInfoDialog mode="create">`.
|
|
106
|
+
* This is the gate for staging — typing an unknown name is
|
|
107
|
+
* invalid until + creates the directory. */
|
|
108
|
+
projectKnown: (id: string) => boolean;
|
|
109
|
+
/** Refresh `existingProjects` from the server. Called automatically
|
|
110
|
+
* on `tis.project_created` / `tis.project_updated` broadcasts. */
|
|
111
|
+
refreshProjects: () => Promise<void>;
|
|
112
|
+
/** Add a project ID to the in-session "just created" set so the
|
|
113
|
+
* form is immediately valid for it without round-tripping to
|
|
114
|
+
* list_projects. Idempotent. */
|
|
115
|
+
markProjectJustCreated: (id: string) => void;
|
|
116
|
+
|
|
117
|
+
/** `project_fields` blob for the currently-selected project,
|
|
118
|
+
* fetched from project.json. `{}` when nothing is loaded yet
|
|
119
|
+
* (use `projectFieldsLoaded` to disambiguate "empty project" vs
|
|
120
|
+
* "still fetching"). */
|
|
121
|
+
projectFields: Record<string, any>;
|
|
122
|
+
projectFieldsLoaded: boolean;
|
|
123
|
+
/** Fetch and cache project_fields for one project. Returns the
|
|
124
|
+
* fields on success, or null on error. The current selection's
|
|
125
|
+
* fields are also re-loaded automatically when `selection.projectId`
|
|
126
|
+
* changes. */
|
|
127
|
+
loadProjectFields: (id: string) => Promise<Record<string, any> | null>;
|
|
128
|
+
/** Stash freshly-known project_fields without a round trip — used
|
|
129
|
+
* by the create / edit dialogs after a successful submit. */
|
|
130
|
+
setProjectFields: (id: string, fields: Record<string, any>) => void;
|
|
131
|
+
|
|
93
132
|
/** Fetch the run list for a (project, method?) pair. Method may be
|
|
94
133
|
* omitted to aggregate runs across every method in the project —
|
|
95
134
|
* the History tab uses this. */
|
|
@@ -120,6 +159,14 @@ const TisContext = createContext<TisContextValue>({
|
|
|
120
159
|
state: EMPTY_STATE,
|
|
121
160
|
selection: EMPTY_SELECTION,
|
|
122
161
|
setSelection: () => {},
|
|
162
|
+
existingProjects: [],
|
|
163
|
+
projectKnown: () => false,
|
|
164
|
+
refreshProjects: async () => {},
|
|
165
|
+
markProjectJustCreated: () => {},
|
|
166
|
+
projectFields: {},
|
|
167
|
+
projectFieldsLoaded: false,
|
|
168
|
+
loadProjectFields: async () => null,
|
|
169
|
+
setProjectFields: () => {},
|
|
123
170
|
fetchRuns: async () => [],
|
|
124
171
|
fetchRun: async () => null,
|
|
125
172
|
runCache: {},
|
|
@@ -294,6 +341,33 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
294
341
|
}));
|
|
295
342
|
}, []);
|
|
296
343
|
|
|
344
|
+
// -----------------------------------------------------------------
|
|
345
|
+
// Auto-clear pins on a new active run.
|
|
346
|
+
//
|
|
347
|
+
// The server broadcasts `tis.active_run_id` immediately at
|
|
348
|
+
// `start_test` (before any cycle is added). When that scalar
|
|
349
|
+
// transitions to a new, non-empty value, any pins the operator
|
|
350
|
+
// set — typically by clicking a row in <ResultHistoryTable> —
|
|
351
|
+
// would otherwise keep <TestDataView> stuck on the old run. We
|
|
352
|
+
// drop all four pins here so `selection` falls through to the
|
|
353
|
+
// active broadcast scalars and the Data view follows the live
|
|
354
|
+
// test automatically.
|
|
355
|
+
//
|
|
356
|
+
// Pins set *after* the new run begins (e.g., the operator opens
|
|
357
|
+
// History mid-test and clicks a historical row) are honoured —
|
|
358
|
+
// they're set after this effect runs and they win until the
|
|
359
|
+
// next Start. End-of-test (active flips false) doesn't clear
|
|
360
|
+
// either: active_run_id keeps the just-finished run's value, so
|
|
361
|
+
// the operator can still read its results.
|
|
362
|
+
const prevActiveRunRef = useRef('');
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
const newRunId = state.activeRunId;
|
|
365
|
+
if (newRunId && newRunId !== prevActiveRunRef.current) {
|
|
366
|
+
prevActiveRunRef.current = newRunId;
|
|
367
|
+
setPins({ projectId: null, methodId: null, sampleId: null, runId: null });
|
|
368
|
+
}
|
|
369
|
+
}, [state.activeRunId]);
|
|
370
|
+
|
|
297
371
|
// -----------------------------------------------------------------
|
|
298
372
|
// Fetchers
|
|
299
373
|
// -----------------------------------------------------------------
|
|
@@ -340,11 +414,111 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
340
414
|
|
|
341
415
|
const runCache = useMemo(() => ({ ...cacheRef.current }), [cacheVersion]);
|
|
342
416
|
|
|
417
|
+
// -----------------------------------------------------------------
|
|
418
|
+
// Project management state
|
|
419
|
+
//
|
|
420
|
+
// Mirrors what TestSetupForm used to track locally, but lifted up
|
|
421
|
+
// here so <ProjectSelector> on the Project tab and <TestSetupForm>
|
|
422
|
+
// on the Test tab share a single source of truth. Without this
|
|
423
|
+
// lift, the two tabs would each fire their own list_projects and
|
|
424
|
+
// disagree on which IDs are valid.
|
|
425
|
+
// -----------------------------------------------------------------
|
|
426
|
+
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
427
|
+
const justCreatedRef = useRef<Set<string>>(new Set());
|
|
428
|
+
const [projectsTick, setProjectsTick] = useState(0); // bumps on Set mutation
|
|
429
|
+
const [projectFieldsCache, setProjectFieldsCache] = useState<Record<string, Record<string, any>>>({});
|
|
430
|
+
|
|
431
|
+
const refreshProjects = useCallback(async () => {
|
|
432
|
+
try {
|
|
433
|
+
const resp: any = await invoke('tis.list_projects' as any, MessageType.Request, {} as any);
|
|
434
|
+
if (resp?.success && resp.data?.projects) {
|
|
435
|
+
setExistingProjects(resp.data.projects as string[]);
|
|
436
|
+
}
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error('[TisProvider] tis.list_projects failed:', e);
|
|
439
|
+
}
|
|
440
|
+
}, [invoke]);
|
|
441
|
+
|
|
442
|
+
const markProjectJustCreated = useCallback((id: string) => {
|
|
443
|
+
if (!id) return;
|
|
444
|
+
if (justCreatedRef.current.has(id)) return;
|
|
445
|
+
justCreatedRef.current.add(id);
|
|
446
|
+
setProjectsTick(t => t + 1);
|
|
447
|
+
}, []);
|
|
448
|
+
|
|
449
|
+
const projectKnown = useCallback((id: string) => {
|
|
450
|
+
if (!id) return false;
|
|
451
|
+
if (justCreatedRef.current.has(id)) return true;
|
|
452
|
+
return existingProjects.includes(id);
|
|
453
|
+
// existingProjects + projectsTick are deps but useCallback
|
|
454
|
+
// closes over them; consumers read current values fine.
|
|
455
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
456
|
+
}, [existingProjects, projectsTick]);
|
|
457
|
+
|
|
458
|
+
const setProjectFields = useCallback((id: string, fields: Record<string, any>) => {
|
|
459
|
+
if (!id) return;
|
|
460
|
+
setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
|
|
461
|
+
}, []);
|
|
462
|
+
|
|
463
|
+
const loadProjectFields = useCallback(async (id: string): Promise<Record<string, any> | null> => {
|
|
464
|
+
if (!id) return null;
|
|
465
|
+
try {
|
|
466
|
+
const resp: any = await invoke('tis.read_project' as any, MessageType.Request, { project_id: id } as any);
|
|
467
|
+
if (resp?.success) {
|
|
468
|
+
const fields = (resp.data?.project_fields ?? {}) as Record<string, any>;
|
|
469
|
+
setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
|
|
470
|
+
return fields;
|
|
471
|
+
}
|
|
472
|
+
} catch (e) {
|
|
473
|
+
console.warn('[TisProvider] tis.read_project failed:', e);
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}, [invoke]);
|
|
477
|
+
|
|
478
|
+
// Initial project list load + refresh on server-side mutation
|
|
479
|
+
// broadcasts. Mark-just-created is purely local; the server
|
|
480
|
+
// broadcasts kick the persisted list back into sync if a project
|
|
481
|
+
// was added by another client (or the next time `acctl` writes a
|
|
482
|
+
// new directory).
|
|
483
|
+
useEffect(() => { void refreshProjects(); }, [refreshProjects]);
|
|
484
|
+
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
const onCreated = () => { void refreshProjects(); };
|
|
487
|
+
const onUpdated = (payload: any) => {
|
|
488
|
+
const pid = typeof payload?.project_id === 'string' ? payload.project_id : '';
|
|
489
|
+
if (pid) void loadProjectFields(pid);
|
|
490
|
+
};
|
|
491
|
+
const id1 = subscribe('tis.project_created', onCreated);
|
|
492
|
+
const id2 = subscribe('tis.project_updated', onUpdated);
|
|
493
|
+
return () => { unsubscribe(id1); unsubscribe(id2); };
|
|
494
|
+
}, [subscribe, unsubscribe, refreshProjects, loadProjectFields]);
|
|
495
|
+
|
|
496
|
+
// Auto-fetch project_fields whenever the selection lands on a
|
|
497
|
+
// known project we don't yet have cached. The Test tab's stage
|
|
498
|
+
// payload depends on this.
|
|
499
|
+
useEffect(() => {
|
|
500
|
+
const pid = selection.projectId;
|
|
501
|
+
if (!pid || !projectKnown(pid)) return;
|
|
502
|
+
if (projectFieldsCache[pid] !== undefined) return;
|
|
503
|
+
void loadProjectFields(pid);
|
|
504
|
+
}, [selection.projectId, projectKnown, projectFieldsCache, loadProjectFields]);
|
|
505
|
+
|
|
506
|
+
const projectFields = projectFieldsCache[selection.projectId] ?? {};
|
|
507
|
+
const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
|
|
508
|
+
|
|
343
509
|
const value: TisContextValue = useMemo(() => ({
|
|
344
510
|
schemas, defaultMethodId, schemasLoaded,
|
|
345
511
|
state, selection, setSelection,
|
|
512
|
+
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
513
|
+
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
346
514
|
fetchRuns, fetchRun, runCache,
|
|
347
|
-
}), [
|
|
515
|
+
}), [
|
|
516
|
+
schemas, defaultMethodId, schemasLoaded,
|
|
517
|
+
state, selection, setSelection,
|
|
518
|
+
existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
|
|
519
|
+
projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
|
|
520
|
+
fetchRuns, fetchRun, runCache,
|
|
521
|
+
]);
|
|
348
522
|
|
|
349
523
|
return <TisContext.Provider value={value}>{children}</TisContext.Provider>;
|
|
350
524
|
};
|
|
@@ -356,6 +530,23 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
|
|
|
356
530
|
export const useTis = () => useContext(TisContext);
|
|
357
531
|
export const useTisSchemas = () => useContext(TisContext).schemas;
|
|
358
532
|
export const useTisState = () => useContext(TisContext).state;
|
|
533
|
+
/**
|
|
534
|
+
* Tuple of `[selection, setSelection]`.
|
|
535
|
+
*
|
|
536
|
+
* `selection` has four fields — `projectId`, `methodId`, `sampleId`,
|
|
537
|
+
* `runId`. Each one falls through to the matching `tis.active_*`
|
|
538
|
+
* broadcast scalar when no pin is set. Pins are set by passing a
|
|
539
|
+
* concrete value to `setSelection`; pass `null` to clear a pin and
|
|
540
|
+
* resume auto-following.
|
|
541
|
+
*
|
|
542
|
+
* **Auto-clear on Start:** when a new test becomes active (the
|
|
543
|
+
* `tis.active_run_id` scalar transitions to a new non-empty value),
|
|
544
|
+
* the provider drops all four pins automatically. This is what makes
|
|
545
|
+
* `<TestDataView>` follow the live run after an operator has been
|
|
546
|
+
* browsing history — they don't have to click anything to "switch
|
|
547
|
+
* to the live test." Pins set *after* the new run begins (mid-test
|
|
548
|
+
* History click) are honoured until the next Start.
|
|
549
|
+
*/
|
|
359
550
|
export const useTisSelection = () => {
|
|
360
551
|
const { selection, setSelection } = useContext(TisContext);
|
|
361
552
|
return [selection, setSelection] as const;
|
|
@@ -151,13 +151,29 @@ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
151
151
|
* @param props.tags - List of tag configurations (tagName, fqdn, etc.)
|
|
152
152
|
* @param props.scales - Map of scale definitions (name, factor, label)
|
|
153
153
|
* @param props.eagerRead - If true, automatically fetches initial values on mount
|
|
154
|
+
* @param props.flushIntervalMs - Maximum rate at which broadcast updates trigger
|
|
155
|
+
* React state changes. Broadcasts arriving faster than this are coalesced
|
|
156
|
+
* per-tag (latest-value-wins) and flushed in a single batched render at most
|
|
157
|
+
* once per interval.
|
|
158
|
+
*
|
|
159
|
+
* Default `33` (≈30 FPS) keeps Firefox smooth even when the controller scan
|
|
160
|
+
* rate is 2–4 kHz. The wire still carries every change-of-value broadcast —
|
|
161
|
+
* this only paces the React reconciliation cost, which dominates render
|
|
162
|
+
* budget. To also reduce wire bandwidth (relevant on poor links / Tailscale
|
|
163
|
+
* over slow WiFi), see the connection-level rate cap (server-side).
|
|
164
|
+
*
|
|
165
|
+
* Set to `0` to disable throttling and apply every broadcast immediately
|
|
166
|
+
* (matches pre-throttle behaviour). Initial-load `eagerRead` values are
|
|
167
|
+
* never throttled — they bypass this path entirely so the page renders
|
|
168
|
+
* fully populated on mount.
|
|
154
169
|
*/
|
|
155
170
|
export const AutoCoreTagProvider: React.FC<{
|
|
156
171
|
children: ReactNode;
|
|
157
172
|
tags: readonly TagConfig[];
|
|
158
173
|
scales?: Record<string, ScaleConfig>;
|
|
159
174
|
eagerRead?: boolean;
|
|
160
|
-
|
|
175
|
+
flushIntervalMs?: number;
|
|
176
|
+
}> = ({ children, tags, scales, eagerRead = true, flushIntervalMs = 33 }) => {
|
|
161
177
|
const startedRef = useRef(false);
|
|
162
178
|
|
|
163
179
|
// PERFORMANCE: Memoize default scales to ensure reference stability.
|
|
@@ -281,25 +297,107 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
281
297
|
});
|
|
282
298
|
}, [tags, toDisplay]);
|
|
283
299
|
|
|
300
|
+
// -----------------------------------------------------------------
|
|
301
|
+
// Throttle: broadcast updates are coalesced per tag and flushed in
|
|
302
|
+
// a single batched setState at most once per `flushIntervalMs`. This
|
|
303
|
+
// exists because every broadcast that lands in `setRawValues` triggers
|
|
304
|
+
// a context-wide React reconciliation. With high-rate scan loops
|
|
305
|
+
// (3830 runs at 4 kHz) and ~10 fast-changing tags, that's tens of
|
|
306
|
+
// thousands of reconciliations per second — Firefox in particular
|
|
307
|
+
// stalls visibly. The batched flush keeps reconciliation at the
|
|
308
|
+
// `flushIntervalMs` rate (~30 FPS by default), regardless of how
|
|
309
|
+
// many broadcasts the wire delivers in between.
|
|
310
|
+
//
|
|
311
|
+
// Latest-value-wins: each tag keeps only its most recent raw + display
|
|
312
|
+
// value in the pending maps; intermediate values within a window are
|
|
313
|
+
// dropped. This is the right semantic for status indicators — the
|
|
314
|
+
// operator wants "where is X right now," not the path it took to get
|
|
315
|
+
// there. Event broadcasts (e.g., tis.cycle_added) don't go through
|
|
316
|
+
// this path; they're consumed by their own subscribers.
|
|
317
|
+
//
|
|
318
|
+
// The eagerRead path on mount writes directly to setRawValues (see
|
|
319
|
+
// `eagerPullNonADS` etc. above) and bypasses this throttle, so the
|
|
320
|
+
// initial render is fully populated.
|
|
321
|
+
// -----------------------------------------------------------------
|
|
322
|
+
const pendingRawRef = useRef<Map<string, unknown>>(new Map());
|
|
323
|
+
const pendingDisplayRef = useRef<Map<string, unknown>>(new Map());
|
|
324
|
+
const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
325
|
+
|
|
326
|
+
const flushPending = useCallback(() => {
|
|
327
|
+
flushTimerRef.current = null;
|
|
328
|
+
const r = pendingRawRef.current;
|
|
329
|
+
const d = pendingDisplayRef.current;
|
|
330
|
+
if (r.size === 0 && d.size === 0) return;
|
|
331
|
+
pendingRawRef.current = new Map();
|
|
332
|
+
pendingDisplayRef.current = new Map();
|
|
333
|
+
setRawValues(prev => {
|
|
334
|
+
let next = prev;
|
|
335
|
+
r.forEach((v, k) => {
|
|
336
|
+
if (next[k] !== v) {
|
|
337
|
+
if (next === prev) next = { ...prev };
|
|
338
|
+
next[k] = v;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return next;
|
|
342
|
+
});
|
|
343
|
+
setValues(prev => {
|
|
344
|
+
let next = prev;
|
|
345
|
+
d.forEach((v, k) => {
|
|
346
|
+
if (next[k] !== v) {
|
|
347
|
+
if (next === prev) next = { ...prev };
|
|
348
|
+
next[k] = v;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return next;
|
|
352
|
+
});
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
284
355
|
/**
|
|
285
|
-
* Handles incoming value updates from the server.
|
|
286
|
-
*
|
|
287
|
-
* 1.
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
356
|
+
* Handles incoming broadcast value updates from the server.
|
|
357
|
+
*
|
|
358
|
+
* 1. Stages the raw and display values in per-tag pending maps
|
|
359
|
+
* (latest-value-wins; intermediate values inside a window are
|
|
360
|
+
* dropped).
|
|
361
|
+
* 2. Schedules (or piggybacks on) a flush timer that batches every
|
|
362
|
+
* accumulated tag into a single setRawValues + setValues pass at
|
|
363
|
+
* most once per `flushIntervalMs`.
|
|
364
|
+
* 3. When `flushIntervalMs === 0`, flushes synchronously — same as
|
|
365
|
+
* the pre-throttle path.
|
|
291
366
|
*/
|
|
292
367
|
const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
|
|
293
|
-
// Store the raw controller value (source of truth for scaling)
|
|
294
|
-
setRawValues(prev =>
|
|
295
|
-
prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
|
|
296
|
-
);
|
|
297
|
-
// Compute and store the display value (with scaling/codecs applied)
|
|
298
368
|
const display = toDisplay(tag, raw);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
369
|
+
|
|
370
|
+
if (flushIntervalMs <= 0) {
|
|
371
|
+
// Throttle disabled — preserve the original synchronous shape.
|
|
372
|
+
setRawValues(prev =>
|
|
373
|
+
prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
|
|
374
|
+
);
|
|
375
|
+
setValues(prev =>
|
|
376
|
+
prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
pendingRawRef.current.set(tag.tagName, raw);
|
|
382
|
+
pendingDisplayRef.current.set(tag.tagName, display);
|
|
383
|
+
if (flushTimerRef.current === null) {
|
|
384
|
+
flushTimerRef.current = setTimeout(flushPending, flushIntervalMs);
|
|
385
|
+
}
|
|
386
|
+
}, [toDisplay, flushIntervalMs, flushPending]);
|
|
387
|
+
|
|
388
|
+
// Drain any pending updates on unmount so we don't leak the timer
|
|
389
|
+
// reference and so a late-arriving setState doesn't fire on a
|
|
390
|
+
// dead component during dev-mode StrictMode double-mounts.
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
return () => {
|
|
393
|
+
if (flushTimerRef.current !== null) {
|
|
394
|
+
clearTimeout(flushTimerRef.current);
|
|
395
|
+
flushTimerRef.current = null;
|
|
396
|
+
}
|
|
397
|
+
pendingRawRef.current = new Map();
|
|
398
|
+
pendingDisplayRef.current = new Map();
|
|
399
|
+
};
|
|
400
|
+
}, []);
|
|
303
401
|
|
|
304
402
|
/**
|
|
305
403
|
* Eagerly fetches initial values for non-ADS domains (e.g., MODBUS).
|
|
@@ -175,6 +175,21 @@
|
|
|
175
175
|
gap: 2px;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/* ---- ac-toolbar-tool-list: stacked text buttons in an OverlayPanel ---- */
|
|
179
|
+
|
|
180
|
+
.ac-toolbar-tool-list {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
min-width: 16rem;
|
|
184
|
+
padding: 2mm;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.ac-toolbar-tool-item {
|
|
188
|
+
justify-content: flex-start;
|
|
189
|
+
gap: 0.75rem;
|
|
190
|
+
margin: 2mm;
|
|
191
|
+
}
|
|
192
|
+
|
|
178
193
|
/* ---- ac-form: content page form layout ---- */
|
|
179
194
|
|
|
180
195
|
.ac-form {
|
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
// General Padding & Sizing
|
|
15
15
|
$panelContentPadding: 0.5rem 0.75rem !default; // Affects panels, cards, dialogs, etc.
|
|
16
16
|
$inputPadding: 0.5rem 0.75rem !default; // Affects all input fields
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Buttons run one size class larger than the original lara baseline:
|
|
18
|
+
// the previous "medium" was too small for our 1080p kiosk targets and
|
|
19
|
+
// the workstation operators leaning on touchscreens. Each of small /
|
|
20
|
+
// medium / large was bumped up one tier (+0.125rem on font and on each
|
|
21
|
+
// padding axis), so the new "small" is the size the old "medium" was.
|
|
22
|
+
// `.p-button-sm` and `.p-button-lg` overrides below carry the matching
|
|
23
|
+
// numbers for the other two tiers.
|
|
24
|
+
$buttonPadding: 0.625rem 0.875rem !default; // Affects buttons (medium/default)
|
|
25
|
+
$buttonIconOnlyPadding: 0.625rem 0 !default;
|
|
19
26
|
$inlineSpacing: 0.25rem !default; // Space between items like icons and text
|
|
20
27
|
$borderRadius: 3px !default; // Sharper corners for a more "desktop" feel
|
|
21
28
|
|
|
@@ -24,8 +31,16 @@ $tableHeaderCellPadding: 0.5rem 0.75rem !default;
|
|
|
24
31
|
$tableBodyCellPadding: 0.5rem 0.75rem !default;
|
|
25
32
|
|
|
26
33
|
// Menu Sizing
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
//
|
|
35
|
+
// Bumped past the medium-button padding because menu items are
|
|
36
|
+
// usually selected in a single tap on a touchscreen with imprecise
|
|
37
|
+
// aim — slightly more vertical breathing room than a button gives a
|
|
38
|
+
// touch target close to the 44 px iOS-HIG / WCAG 2.5.5 minimum at
|
|
39
|
+
// our 1.125rem (~18 px) menu-item font-size: 18 + 2×12 = 42 px.
|
|
40
|
+
// `.p-menuitem-link` font-size and `.p-menuitem-icon` size below
|
|
41
|
+
// finish the picture (icons scale up alongside the text).
|
|
42
|
+
$menuitemPadding: 0.75rem 1rem !default;
|
|
43
|
+
$horizontalMenuRootMenuitemPadding: 0.75rem 1rem !default;
|
|
29
44
|
|
|
30
45
|
|
|
31
46
|
// 2. Import the original lara-dark-blue theme
|
|
@@ -49,7 +64,7 @@ textarea {
|
|
|
49
64
|
|
|
50
65
|
.p-button {
|
|
51
66
|
padding: $buttonPadding;
|
|
52
|
-
font-size:
|
|
67
|
+
font-size: 1.125rem;
|
|
53
68
|
font-family: inherit;
|
|
54
69
|
border-radius: $borderRadius;
|
|
55
70
|
cursor: pointer;
|
|
@@ -61,7 +76,7 @@ textarea {
|
|
|
61
76
|
|
|
62
77
|
.p-button.p-button-icon-only {
|
|
63
78
|
padding: $buttonIconOnlyPadding;
|
|
64
|
-
width: 2.
|
|
79
|
+
width: 2.75rem;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
// PrimeReact always renders a `<span class="p-button-label"> </span>`
|
|
@@ -72,14 +87,16 @@ textarea {
|
|
|
72
87
|
display: none;
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
// `.p-button-sm` lands at what used to be the medium baseline.
|
|
75
91
|
.p-button.p-button-sm {
|
|
76
|
-
font-size:
|
|
77
|
-
padding: 0.
|
|
92
|
+
font-size: 1rem;
|
|
93
|
+
padding: 0.5rem 0.75rem;
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
// `.p-button-lg` extends the same +0.125rem step past the new medium.
|
|
80
97
|
.p-button.p-button-lg {
|
|
81
|
-
font-size: 1.
|
|
82
|
-
padding: 0.
|
|
98
|
+
font-size: 1.25rem;
|
|
99
|
+
padding: 0.75rem 1rem;
|
|
83
100
|
}
|
|
84
101
|
|
|
85
102
|
.p-inputtext {
|
|
@@ -133,6 +150,35 @@ textarea {
|
|
|
133
150
|
border-bottom-right-radius: $borderRadius;
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
// Menu items (TieredMenu / Menu / SplitButton dropdown / ContextMenu).
|
|
154
|
+
// Match the new medium-button font-size so SplitButton dropdowns don't
|
|
155
|
+
// look noticeably smaller than the button that opened them, and bump
|
|
156
|
+
// the leading icon a touch larger than the text so radio/check glyphs
|
|
157
|
+
// read clearly on a touchscreen.
|
|
158
|
+
.p-menuitem-link {
|
|
159
|
+
font-size: 1.125rem;
|
|
160
|
+
}
|
|
161
|
+
.p-menuitem-link .p-menuitem-icon {
|
|
162
|
+
font-size: 1.25rem;
|
|
163
|
+
width: 1.25rem;
|
|
164
|
+
height: 1.25rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Input-list popup items (Dropdown / AutoComplete / MultiSelect /
|
|
168
|
+
// CascadeSelect / TreeSelect / picklist filters). Vertical padding
|
|
169
|
+
// for these is already at the touch-friendly lara default
|
|
170
|
+
// (`$inputListItemPadding: 0.75rem 1.25rem`); we just bump the
|
|
171
|
+
// font-size to the medium-button scale so options read at the same
|
|
172
|
+
// visual weight as the button / menu items around them. Net target
|
|
173
|
+
// height ≈ 18 px font + 24 px padding = 42 px, same as the menus.
|
|
174
|
+
.p-dropdown-item,
|
|
175
|
+
.p-autocomplete-item,
|
|
176
|
+
.p-multiselect-item,
|
|
177
|
+
.p-cascadeselect-item,
|
|
178
|
+
.p-treeselect-item {
|
|
179
|
+
font-size: 1.125rem;
|
|
180
|
+
}
|
|
181
|
+
|
|
136
182
|
// TabView: ensure tabs are visibly tab-shaped with padding and spacing,
|
|
137
183
|
// regardless of @layer cascade resolution.
|
|
138
184
|
.p-tabview .p-tabview-nav {
|