@adcops/autocore-react 3.3.106 → 3.3.111
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/assets/JogXLeft.d.ts +4 -0
- package/dist/assets/JogXLeft.d.ts.map +1 -0
- package/dist/assets/JogXLeft.js +1 -0
- package/dist/assets/JogXRight.d.ts +4 -0
- package/dist/assets/JogXRight.d.ts.map +1 -0
- package/dist/assets/JogXRight.js +1 -0
- package/dist/assets/index.d.ts +2 -2
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/components/JogPanel.d.ts +29 -4
- package/dist/components/JogPanel.d.ts.map +1 -1
- package/dist/components/JogPanel.js +1 -1
- package/dist/components/ValueInput.d.ts.map +1 -1
- package/dist/components/ValueInput.js +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/network/StagedChangeBanner.d.ts.map +1 -1
- package/dist/components/network/StagedChangeBanner.js +1 -1
- package/dist/components/tis/ResultHistoryTable.css +12 -0
- package/dist/components/tis/ResultHistoryTable.d.ts +1 -0
- package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -1
- package/dist/components/tis/ResultHistoryTable.js +1 -1
- package/dist/components/tis/SampleSummaryPanel.d.ts +23 -0
- package/dist/components/tis/SampleSummaryPanel.d.ts.map +1 -0
- package/dist/components/tis/SampleSummaryPanel.js +1 -0
- package/dist/components/tis/ScienceTable.css +87 -0
- package/dist/components/tis/ScienceTable.d.ts +35 -0
- package/dist/components/tis/ScienceTable.d.ts.map +1 -0
- package/dist/components/tis/ScienceTable.js +1 -0
- 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.map +1 -1
- package/dist/components/tis/TestSetupForm.js +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/FieldArrayEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +6 -0
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/types.d.ts +15 -0
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.d.ts.map +1 -1
- package/dist/components/tis-editor/validation.js +1 -1
- package/package.json +1 -1
- package/src/assets/{JogXNeg.tsx → JogXLeft.tsx} +6 -3
- package/src/assets/{JogXPos.tsx → JogXRight.tsx} +6 -3
- package/src/assets/index.ts +2 -2
- package/src/components/JogPanel.tsx +64 -59
- package/src/components/ValueInput.tsx +10 -3
- package/src/components/index.ts +3 -0
- package/src/components/network/StagedChangeBanner.tsx +22 -3
- package/src/components/tis/ResultHistoryTable.css +12 -0
- package/src/components/tis/ResultHistoryTable.tsx +76 -4
- package/src/components/tis/SampleSummaryPanel.tsx +171 -0
- package/src/components/tis/ScienceTable.css +87 -0
- package/src/components/tis/ScienceTable.tsx +114 -0
- package/src/components/tis/TestDataView.tsx +92 -51
- package/src/components/tis/TestSetupForm.tsx +18 -2
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +18 -1
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +82 -1
- package/src/components/tis-editor/types.ts +17 -0
- package/src/components/tis-editor/validation.ts +35 -0
- package/dist/assets/JogXNeg.d.ts +0 -4
- package/dist/assets/JogXNeg.d.ts.map +0 -1
- package/dist/assets/JogXNeg.js +0 -1
- package/dist/assets/JogXPos.d.ts +0 -4
- package/dist/assets/JogXPos.d.ts.map +0 -1
- package/dist/assets/JogXPos.js +0 -1
|
@@ -108,10 +108,17 @@ export const ValueInput: React.FC<ValueInputProps> = ({
|
|
|
108
108
|
}
|
|
109
109
|
}, [value, editing]);
|
|
110
110
|
|
|
111
|
+
// Treat the bounds as an unordered interval: an author may write the range
|
|
112
|
+
// in "magnitude" order (e.g. min:-50, max:-1000 for a compressive load),
|
|
113
|
+
// which is numerically inverted. Without this, min > max yields an empty
|
|
114
|
+
// valid set and the field rejects every value, including its own default.
|
|
115
|
+
const lo = min !== undefined && max !== undefined ? Math.min(min, max) : min;
|
|
116
|
+
const hi = min !== undefined && max !== undefined ? Math.max(min, max) : max;
|
|
117
|
+
|
|
111
118
|
const validate = (val: number | null): boolean => {
|
|
112
119
|
if (val === null || Number.isNaN(val)) return false;
|
|
113
|
-
if (
|
|
114
|
-
if (
|
|
120
|
+
if (lo !== undefined && val < lo) return false;
|
|
121
|
+
if (hi !== undefined && val > hi) return false;
|
|
115
122
|
return true;
|
|
116
123
|
};
|
|
117
124
|
|
|
@@ -148,7 +155,7 @@ export const ValueInput: React.FC<ValueInputProps> = ({
|
|
|
148
155
|
};
|
|
149
156
|
|
|
150
157
|
const isLabelDefined = label !== undefined && label !== null && label !== '';
|
|
151
|
-
const allowNegative =
|
|
158
|
+
const allowNegative = lo === undefined || lo < 0;
|
|
152
159
|
|
|
153
160
|
return (
|
|
154
161
|
<div>
|
package/src/components/index.ts
CHANGED
|
@@ -45,6 +45,9 @@ export type { TestMethodDialogProps } from './tis/TestMethodDialog';
|
|
|
45
45
|
export { ResultHistoryTable } from './tis/ResultHistoryTable';
|
|
46
46
|
export type { ResultHistoryTableProps } from './tis/ResultHistoryTable';
|
|
47
47
|
|
|
48
|
+
export { SampleSummaryPanel } from './tis/SampleSummaryPanel';
|
|
49
|
+
export type { SampleSummaryPanelProps } from './tis/SampleSummaryPanel';
|
|
50
|
+
|
|
48
51
|
export { TestDataView } from './tis/TestDataView';
|
|
49
52
|
export type { TestDataViewProps, ChartAxis, ChartSeries, ChartView, RawDataShape } from './tis/TestDataView';
|
|
50
53
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useEffect, useState } from 'react';
|
|
14
|
+
import { createPortal } from 'react-dom';
|
|
14
15
|
import { Button } from 'primereact/button';
|
|
15
16
|
import { useNetwork } from './NetworkProvider';
|
|
16
17
|
|
|
@@ -38,10 +39,23 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
|
|
|
38
39
|
const total = Math.max(1, staged.revert_in_seconds);
|
|
39
40
|
const pctRemaining = Math.max(0, Math.min(100, (remainingSec / total) * 100));
|
|
40
41
|
|
|
42
|
+
// NOTE: the banner is rendered through a portal into <body> (see the
|
|
43
|
+
// createPortal call below) and positioned `fixed`, not `sticky`. That is
|
|
44
|
+
// deliberate: host apps commonly wrap their content in a
|
|
45
|
+
// `position: fixed`/`transform`ed shell (which establishes a stacking
|
|
46
|
+
// context) and open configuration screens as PrimeReact *modal* dialogs.
|
|
47
|
+
// A modal Dialog portals its mask to <body> at z-index ~1100+, so a
|
|
48
|
+
// banner living inside the app shell — no matter how high its own
|
|
49
|
+
// z-index — renders *beneath* the modal and becomes unreachable
|
|
50
|
+
// ("press confirm on a popup that is hidden"). Portaling to <body> puts
|
|
51
|
+
// the banner in the root stacking context alongside the modal, where a
|
|
52
|
+
// z-index above the PrimeReact modal range actually wins.
|
|
41
53
|
const containerStyle: React.CSSProperties = {
|
|
42
|
-
position: '
|
|
54
|
+
position: 'fixed',
|
|
43
55
|
top: 0,
|
|
44
|
-
|
|
56
|
+
left: 0,
|
|
57
|
+
right: 0,
|
|
58
|
+
zIndex: 2000,
|
|
45
59
|
padding: '0.75rem 1rem',
|
|
46
60
|
background: remainingSec <= 10 ? '#7f1d1d' : '#78350f',
|
|
47
61
|
color: 'white',
|
|
@@ -50,7 +64,7 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
|
|
|
50
64
|
...style,
|
|
51
65
|
};
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
const banner = (
|
|
54
68
|
<div style={containerStyle} role="alertdialog" aria-live="polite">
|
|
55
69
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', flexWrap: 'wrap' }}>
|
|
56
70
|
<div style={{ minWidth: 0 }}>
|
|
@@ -96,6 +110,11 @@ export const StagedChangeBanner: React.FC<StagedChangeBannerProps> = ({ style })
|
|
|
96
110
|
</div>
|
|
97
111
|
</div>
|
|
98
112
|
);
|
|
113
|
+
|
|
114
|
+
// Guard for non-DOM environments (SSR/tests); render inline if there is
|
|
115
|
+
// no document to portal into.
|
|
116
|
+
if (typeof document === 'undefined' || !document.body) return banner;
|
|
117
|
+
return createPortal(banner, document.body);
|
|
99
118
|
};
|
|
100
119
|
|
|
101
120
|
export default StagedChangeBanner;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Runs excluded from cross-test aggregation are kept visible but dimmed and
|
|
2
|
+
struck through, so an operator can see at a glance which rows don't feed
|
|
3
|
+
the avg/min/max/stddev/count results for their sample group. */
|
|
4
|
+
.tis-row-excluded > td {
|
|
5
|
+
opacity: 0.55;
|
|
6
|
+
text-decoration: line-through;
|
|
7
|
+
}
|
|
8
|
+
/* Keep the action buttons (Include / Download) legible and clickable. */
|
|
9
|
+
.tis-row-excluded > td .p-button {
|
|
10
|
+
opacity: 1;
|
|
11
|
+
text-decoration: none;
|
|
12
|
+
}
|
|
@@ -2,10 +2,12 @@ import React, { useState, useEffect, useContext } from 'react';
|
|
|
2
2
|
import { DataTable } from 'primereact/datatable';
|
|
3
3
|
import { Column } from 'primereact/column';
|
|
4
4
|
import { Button } from 'primereact/button';
|
|
5
|
-
import {
|
|
5
|
+
import { Badge } from 'primereact/badge';
|
|
6
6
|
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
7
7
|
import { MessageType } from '../../hub/CommandMessage';
|
|
8
8
|
import { useTis } from './TisProvider';
|
|
9
|
+
import { SampleSummaryPanel } from './SampleSummaryPanel';
|
|
10
|
+
import './ResultHistoryTable.css';
|
|
9
11
|
|
|
10
12
|
export interface ResultHistoryTableProps {
|
|
11
13
|
/**
|
|
@@ -72,6 +74,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
72
74
|
const [loading, setLoading] = useState(false);
|
|
73
75
|
const [downloading, setDownloading] = useState<InFlight | null>(null);
|
|
74
76
|
const [projectBusy, setProjectBusy] = useState<ProjectDownloadKind | null>(null);
|
|
77
|
+
const [excludingRunId, setExcludingRunId] = useState<string | null>(null);
|
|
75
78
|
const { invoke } = useContext(EventEmitterContext);
|
|
76
79
|
|
|
77
80
|
const loadTests = async () => {
|
|
@@ -169,6 +172,34 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
169
172
|
}
|
|
170
173
|
};
|
|
171
174
|
|
|
175
|
+
// Toggle a run's inclusion in cross-test aggregation. Excluded runs are
|
|
176
|
+
// kept on disk (not deleted) but dropped from avg/min/max/stddev/count
|
|
177
|
+
// results for their sample group. The server recomputes + rebroadcasts
|
|
178
|
+
// the sample summary on success.
|
|
179
|
+
const handleToggleExclude = async (rowData: any) => {
|
|
180
|
+
const runId = rowData?.run_id;
|
|
181
|
+
const rowMethodId = rowData?.method_id ?? methodId;
|
|
182
|
+
if (!runId || !rowMethodId || !projectId) return;
|
|
183
|
+
const nextExcluded = rowData?.excluded !== true;
|
|
184
|
+
setExcludingRunId(runId);
|
|
185
|
+
try {
|
|
186
|
+
const resp: any = await invoke('tis.set_test_excluded' as any, MessageType.Request, {
|
|
187
|
+
project_id: projectId, method_id: rowMethodId, run_id: runId, excluded: nextExcluded,
|
|
188
|
+
} as any);
|
|
189
|
+
if (!resp?.success) {
|
|
190
|
+
alert(`Failed to ${nextExcluded ? 'exclude' : 'include'} ${runId}` +
|
|
191
|
+
(resp?.error_message ? `: ${resp.error_message}` : ''));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
await loadTests();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error('Failed to toggle exclusion', err);
|
|
197
|
+
alert(`Exclude toggle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
198
|
+
} finally {
|
|
199
|
+
setExcludingRunId(null);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
172
203
|
const handleProjectDownload = async (kind: ProjectDownloadKind) => {
|
|
173
204
|
if (!projectId) return;
|
|
174
205
|
const topic = kind === 'report' ? 'tis.export_project_csv'
|
|
@@ -284,6 +315,11 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
284
315
|
</div>
|
|
285
316
|
</div>
|
|
286
317
|
|
|
318
|
+
{/* Per-sample cross-test rollup. Renders nothing unless a method
|
|
319
|
+
declares aggregate results_fields, so projects without the
|
|
320
|
+
feature see the history table unchanged. */}
|
|
321
|
+
<SampleSummaryPanel projectId={projectId} methodId={methodId} />
|
|
322
|
+
|
|
287
323
|
<DataTable
|
|
288
324
|
value={tests}
|
|
289
325
|
loading={loading}
|
|
@@ -294,6 +330,7 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
294
330
|
scrollHeight="flex"
|
|
295
331
|
tableStyle={{ minWidth: 0 }}
|
|
296
332
|
style={{ width: '100%' }}
|
|
333
|
+
rowClassName={(rowData: any) => (rowData?.excluded === true ? 'tis-row-excluded' : '')}
|
|
297
334
|
selectionMode="single"
|
|
298
335
|
onSelectionChange={(e: any) => {
|
|
299
336
|
const row = e.value;
|
|
@@ -327,13 +364,48 @@ export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = (props) =>
|
|
|
327
364
|
style={{ minWidth: '12rem' }}
|
|
328
365
|
/>
|
|
329
366
|
<Column field="method_id" header="Test Method" sortable style={{ minWidth: '10rem' }} />
|
|
330
|
-
|
|
367
|
+
{/* Run ID dropped from the table — it's an internal handle
|
|
368
|
+
(the row's sample_id + date already identify the run for a
|
|
369
|
+
human), and its 12rem column was the main reason the table
|
|
370
|
+
overflowed in portrait. Still carried on the row data and
|
|
371
|
+
used for selection + downloads. */}
|
|
331
372
|
<Column header="Status" field="status" sortable
|
|
332
373
|
body={(rowData) => {
|
|
333
374
|
const s = statusOf(rowData);
|
|
334
|
-
return <
|
|
375
|
+
return <Badge value={s.label} severity={s.severity} />;
|
|
376
|
+
}}
|
|
377
|
+
style={{ minWidth: '6rem' }}
|
|
378
|
+
/>
|
|
379
|
+
{/* Inclusion shown as a single checkmark / eye-slash toggle so
|
|
380
|
+
the column stays narrow. Green check = counted in cross-test
|
|
381
|
+
results; amber eye-slash = excluded. Click toggles. */}
|
|
382
|
+
<Column
|
|
383
|
+
header="Incl."
|
|
384
|
+
align="center"
|
|
385
|
+
alignHeader="center"
|
|
386
|
+
style={{ width: '4.5rem' }}
|
|
387
|
+
body={(rowData) => {
|
|
388
|
+
const isExcluded = rowData?.excluded === true;
|
|
389
|
+
const isBusy = excludingRunId === rowData.run_id;
|
|
390
|
+
return (
|
|
391
|
+
<Button
|
|
392
|
+
icon={isBusy ? 'pi pi-spin pi-spinner'
|
|
393
|
+
: (isExcluded ? 'pi pi-eye-slash' : 'pi pi-check')}
|
|
394
|
+
rounded
|
|
395
|
+
text
|
|
396
|
+
size="small"
|
|
397
|
+
severity={isExcluded ? 'warning' : 'success'}
|
|
398
|
+
disabled={excludingRunId !== null}
|
|
399
|
+
onClick={() => handleToggleExclude(rowData)}
|
|
400
|
+
aria-label={isExcluded ? 'Excluded — click to include'
|
|
401
|
+
: 'Included — click to exclude'}
|
|
402
|
+
tooltip={isExcluded
|
|
403
|
+
? 'Excluded from cross-test results — click to include'
|
|
404
|
+
: 'Included in cross-test results — click to exclude'}
|
|
405
|
+
tooltipOptions={{ position: 'left' }}
|
|
406
|
+
/>
|
|
407
|
+
);
|
|
335
408
|
}}
|
|
336
|
-
style={{ minWidth: '8rem' }}
|
|
337
409
|
/>
|
|
338
410
|
<Column
|
|
339
411
|
header="Download"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { DataTable } from 'primereact/datatable';
|
|
3
|
+
import { Column } from 'primereact/column';
|
|
4
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
5
|
+
import { EventEmitterContext } from '../../core/EventEmitterContext';
|
|
6
|
+
import { MessageType } from '../../hub/CommandMessage';
|
|
7
|
+
import { useTis } from './TisProvider';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Per-method cross-test rollup, one row per sample_id.
|
|
11
|
+
*
|
|
12
|
+
* Distinct from the per-run results overlay in <TestDataView>: this panel
|
|
13
|
+
* answers "across every run of sample X, what's the avg/min/max/stddev/count?"
|
|
14
|
+
* — the question operators ask when a sample is run repeatedly. Backed by
|
|
15
|
+
* `tis.sample_summaries`; refreshes on the server's `tis.sample_summary_updated`
|
|
16
|
+
* broadcast (a sibling finishing or an exclusion toggle).
|
|
17
|
+
*
|
|
18
|
+
* Renders nothing unless some method in the project declares aggregate
|
|
19
|
+
* results_fields. When `methodId` is omitted and more than one method has
|
|
20
|
+
* aggregates, a small method picker is shown.
|
|
21
|
+
*/
|
|
22
|
+
export interface SampleSummaryPanelProps {
|
|
23
|
+
/** Defaults to the active project from <TisProvider>. */
|
|
24
|
+
projectId?: string;
|
|
25
|
+
/** Lock the panel to one method. When omitted, the user picks among
|
|
26
|
+
* methods that declare aggregate results_fields. */
|
|
27
|
+
methodId?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SummaryRow {
|
|
31
|
+
sample_id: string;
|
|
32
|
+
summary: Record<string, unknown>;
|
|
33
|
+
included_count: number;
|
|
34
|
+
excluded_count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface AggColumn {
|
|
38
|
+
name: string;
|
|
39
|
+
header: string;
|
|
40
|
+
scale?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const SampleSummaryPanel: React.FC<SampleSummaryPanelProps> = (props) => {
|
|
44
|
+
const tis = useTis();
|
|
45
|
+
const { invoke, subscribe, unsubscribe } = useContext(EventEmitterContext);
|
|
46
|
+
const projectId = props.projectId ?? tis.selection.projectId ?? undefined;
|
|
47
|
+
|
|
48
|
+
// Methods that actually declare an aggregate results_field — the only
|
|
49
|
+
// ones worth showing here.
|
|
50
|
+
const methodsWithAggregates = React.useMemo(() => {
|
|
51
|
+
return Object.entries(tis.schemas)
|
|
52
|
+
.filter(([, m]) => ((m?.results_fields ?? []) as any[]).some((f) => f?.aggregate))
|
|
53
|
+
.map(([id]) => id);
|
|
54
|
+
}, [tis.schemas]);
|
|
55
|
+
|
|
56
|
+
const [picked, setPicked] = useState<string | undefined>(undefined);
|
|
57
|
+
const methodId = props.methodId ?? picked ?? methodsWithAggregates[0];
|
|
58
|
+
|
|
59
|
+
// Keep the local pick valid as schemas load / change.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (props.methodId) return;
|
|
62
|
+
if (!picked || !methodsWithAggregates.includes(picked)) {
|
|
63
|
+
setPicked(methodsWithAggregates[0]);
|
|
64
|
+
}
|
|
65
|
+
}, [props.methodId, picked, methodsWithAggregates]);
|
|
66
|
+
|
|
67
|
+
const [rows, setRows] = useState<SummaryRow[]>([]);
|
|
68
|
+
const [loading, setLoading] = useState(false);
|
|
69
|
+
|
|
70
|
+
// Columns come from the schema's aggregate results_fields (label + units),
|
|
71
|
+
// so empty-but-declared columns still show with a blank cell.
|
|
72
|
+
const aggColumns: AggColumn[] = React.useMemo(() => {
|
|
73
|
+
const schema = methodId ? (tis.schemas[methodId] as any) : undefined;
|
|
74
|
+
const fields: any[] = (schema?.results_fields ?? []).filter((f: any) => f?.aggregate);
|
|
75
|
+
return fields.map((f) => ({
|
|
76
|
+
name: f.name,
|
|
77
|
+
header: (f.label || f.name) + (f.units ? ` [${f.units}]` : ''),
|
|
78
|
+
scale: typeof f.scale === 'number' ? f.scale : undefined,
|
|
79
|
+
}));
|
|
80
|
+
}, [tis.schemas, methodId]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!projectId || !methodId) { setRows([]); return; }
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
const load = async () => {
|
|
86
|
+
setLoading(true);
|
|
87
|
+
try {
|
|
88
|
+
const resp: any = await invoke('tis.sample_summaries' as any, MessageType.Request,
|
|
89
|
+
{ project_id: projectId, method_id: methodId } as any);
|
|
90
|
+
if (!cancelled && resp?.success) setRows(resp.data?.summaries ?? []);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('Failed to load sample summaries', err);
|
|
93
|
+
} finally {
|
|
94
|
+
if (!cancelled) setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
load();
|
|
98
|
+
// Any rollup change for this project+method refreshes the whole panel
|
|
99
|
+
// (cheap; also picks up brand-new sample groups the broadcast can't
|
|
100
|
+
// patch in place). Also follow the live run lifecycle like the table.
|
|
101
|
+
const onSummary = (payload: any) => {
|
|
102
|
+
if (payload?.project_id === projectId && payload?.method_id === methodId) load();
|
|
103
|
+
};
|
|
104
|
+
const id = subscribe('tis.sample_summary_updated', onSummary);
|
|
105
|
+
return () => { cancelled = true; unsubscribe(id); };
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [projectId, methodId, tis.state.activeRunId, tis.state.active]);
|
|
108
|
+
|
|
109
|
+
// Feature not configured for this project → render nothing.
|
|
110
|
+
if (methodsWithAggregates.length === 0) return null;
|
|
111
|
+
|
|
112
|
+
const fmt = (v: unknown, scale?: number): string => {
|
|
113
|
+
if (v === null || v === undefined) return '—';
|
|
114
|
+
if (typeof v === 'number') {
|
|
115
|
+
if (!Number.isFinite(v)) return String(v);
|
|
116
|
+
const scaled = typeof scale === 'number' && scale !== 0 ? v * scale : v;
|
|
117
|
+
return Number.parseFloat(scaled.toPrecision(6)).toString();
|
|
118
|
+
}
|
|
119
|
+
return String(v);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div style={{ width: '100%', maxWidth: '100%', overflow: 'hidden', boxSizing: 'border-box', marginBottom: '1rem' }}>
|
|
124
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem', gap: '0.5rem', flexWrap: 'wrap' }}>
|
|
125
|
+
<h3 style={{ margin: 0 }}>Sample Summary</h3>
|
|
126
|
+
{!props.methodId && methodsWithAggregates.length > 1 && (
|
|
127
|
+
<Dropdown
|
|
128
|
+
value={methodId}
|
|
129
|
+
options={methodsWithAggregates.map((id) => ({ label: id, value: id }))}
|
|
130
|
+
onChange={(e) => setPicked(e.value)}
|
|
131
|
+
style={{ minWidth: '12rem' }}
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
<DataTable
|
|
136
|
+
value={rows}
|
|
137
|
+
loading={loading}
|
|
138
|
+
dataKey="sample_id"
|
|
139
|
+
emptyMessage="No samples yet."
|
|
140
|
+
scrollable
|
|
141
|
+
tableStyle={{ minWidth: 0 }}
|
|
142
|
+
style={{ width: '100%' }}
|
|
143
|
+
size="small"
|
|
144
|
+
stripedRows
|
|
145
|
+
>
|
|
146
|
+
<Column field="sample_id" header="Sample ID" sortable style={{ minWidth: '8rem' }} />
|
|
147
|
+
{aggColumns.map((c) => (
|
|
148
|
+
<Column
|
|
149
|
+
key={c.name}
|
|
150
|
+
header={c.header}
|
|
151
|
+
bodyStyle={{ fontVariantNumeric: 'tabular-nums', textAlign: 'right' }}
|
|
152
|
+
style={{ minWidth: '7rem' }}
|
|
153
|
+
body={(row: SummaryRow) => fmt(row.summary?.[c.name], c.scale)}
|
|
154
|
+
/>
|
|
155
|
+
))}
|
|
156
|
+
<Column
|
|
157
|
+
header="# Tests"
|
|
158
|
+
bodyStyle={{ textAlign: 'right' }}
|
|
159
|
+
style={{ width: '6rem' }}
|
|
160
|
+
body={(row: SummaryRow) => row.included_count}
|
|
161
|
+
/>
|
|
162
|
+
<Column
|
|
163
|
+
header="Excluded"
|
|
164
|
+
bodyStyle={{ textAlign: 'right' }}
|
|
165
|
+
style={{ width: '6rem' }}
|
|
166
|
+
body={(row: SummaryRow) => (row.excluded_count > 0 ? row.excluded_count : '—')}
|
|
167
|
+
/>
|
|
168
|
+
</DataTable>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ScienceTable — "instrument report" styling for cycle/results tables.
|
|
3
|
+
*
|
|
4
|
+
* Every rule is scoped under `.p-datatable.ac-science-table` (both classes on
|
|
5
|
+
* the DataTable root) so it out-specifies the PrimeReact theme's
|
|
6
|
+
* `.p-datatable ...` rules and cannot bleed onto any other table. The palette
|
|
7
|
+
* is a fixed light "lab report" look (dark headers, cream cells) independent
|
|
8
|
+
* of the app theme — that IS the requested Bluehill appearance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.ac-science-table-wrap {
|
|
12
|
+
border: 1px solid #2b2b2b;
|
|
13
|
+
border-radius: 4px;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
background: #f7f4e9;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Dark title bar with a centered caption, like "Results 1". */
|
|
19
|
+
.ac-science-table-title {
|
|
20
|
+
background: #2b2b2b;
|
|
21
|
+
color: #ffffff;
|
|
22
|
+
text-align: center;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
letter-spacing: 0.02em;
|
|
25
|
+
padding: 0.4rem 0.75rem;
|
|
26
|
+
font-size: 0.95rem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Horizontal scroll for wide results rows without pushing the page sideways. */
|
|
30
|
+
.ac-science-table-scroll {
|
|
31
|
+
overflow-x: auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ---- Column header band -------------------------------------------------- */
|
|
35
|
+
.p-datatable.ac-science-table .p-datatable-thead > tr > th {
|
|
36
|
+
background: #3d3d3d;
|
|
37
|
+
color: #ffffff;
|
|
38
|
+
text-align: center;
|
|
39
|
+
border: 1px solid #2b2b2b;
|
|
40
|
+
border-top: none;
|
|
41
|
+
padding: 0.35rem 0.6rem;
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
vertical-align: middle;
|
|
44
|
+
}
|
|
45
|
+
.p-datatable.ac-science-table .p-datatable-thead > tr > th .p-column-header-content {
|
|
46
|
+
justify-content: center;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.ac-sci-th {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
align-items: center;
|
|
53
|
+
line-height: 1.15;
|
|
54
|
+
}
|
|
55
|
+
.ac-sci-th-unit {
|
|
56
|
+
font-weight: 400;
|
|
57
|
+
font-size: 0.8em;
|
|
58
|
+
opacity: 0.85;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---- Data cells: cream, dark text, thin gridlines, tabular figures ------- */
|
|
62
|
+
.p-datatable.ac-science-table .p-datatable-tbody > tr {
|
|
63
|
+
background: #f7f4e9;
|
|
64
|
+
color: #1a1a1a;
|
|
65
|
+
}
|
|
66
|
+
.p-datatable.ac-science-table .p-datatable-tbody > tr > td {
|
|
67
|
+
background: #f7f4e9;
|
|
68
|
+
color: #1a1a1a;
|
|
69
|
+
text-align: center;
|
|
70
|
+
border: 1px solid #d9d2b8;
|
|
71
|
+
padding: 0.3rem 0.6rem;
|
|
72
|
+
font-variant-numeric: tabular-nums;
|
|
73
|
+
}
|
|
74
|
+
/* Faint zebra banding keeps long cycle lists readable on the cream base. */
|
|
75
|
+
.p-datatable.ac-science-table .p-datatable-tbody > tr:nth-child(even) > td {
|
|
76
|
+
background: #efe9d4;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ---- Row-index column: dark, matching the header band -------------------- */
|
|
80
|
+
.p-datatable.ac-science-table .p-datatable-thead > tr > th.ac-sci-index,
|
|
81
|
+
.p-datatable.ac-science-table .p-datatable-tbody > tr > td.ac-sci-index {
|
|
82
|
+
background: #3d3d3d;
|
|
83
|
+
color: #ffffff;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
text-align: center;
|
|
86
|
+
border: 1px solid #2b2b2b;
|
|
87
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* ScienceTable — a presentational wrapper around PrimeReact's DataTable that
|
|
5
|
+
* gives cycle-data and results tables the crisp, "instrument report" look of
|
|
6
|
+
* a materials-testing readout (à la Instron Bluehill): a dark title bar, a
|
|
7
|
+
* dark column-header band with the unit in brackets on a second line, a dark
|
|
8
|
+
* row-index column, and pale data cells with thin gridlines and centered,
|
|
9
|
+
* tabular-aligned values.
|
|
10
|
+
*
|
|
11
|
+
* Purely visual — it takes rows + a column spec and renders them; it holds no
|
|
12
|
+
* data-fetching or business logic. Styling lives in ScienceTable.css, scoped
|
|
13
|
+
* to `.p-datatable.ac-science-table` so it never leaks onto other DataTables.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import { DataTable } from 'primereact/datatable';
|
|
18
|
+
import { Column } from 'primereact/column';
|
|
19
|
+
import type { ColumnBodyOptions } from 'primereact/column';
|
|
20
|
+
import './ScienceTable.css';
|
|
21
|
+
|
|
22
|
+
export interface ScienceColumn {
|
|
23
|
+
/** Row property to read. */
|
|
24
|
+
field: string;
|
|
25
|
+
/** Header label (unit rendered separately in brackets). */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Optional unit, shown as `[unit]` on a second header line. */
|
|
28
|
+
units?: string;
|
|
29
|
+
/** Cell text alignment. Default 'center' (report style). */
|
|
30
|
+
align?: 'left' | 'center' | 'right';
|
|
31
|
+
/** Optional custom cell renderer (e.g. numeric formatting). */
|
|
32
|
+
body?: (row: any) => React.ReactNode;
|
|
33
|
+
/** Optional per-column style (e.g. minWidth). */
|
|
34
|
+
style?: React.CSSProperties;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScienceTableProps {
|
|
38
|
+
/** Title shown in the dark bar above the table. Omit for no bar. */
|
|
39
|
+
title?: React.ReactNode;
|
|
40
|
+
columns: ScienceColumn[];
|
|
41
|
+
rows: any[];
|
|
42
|
+
/** Leading 1-based row-index column, like the Bluehill "1". Default true. */
|
|
43
|
+
showIndex?: boolean;
|
|
44
|
+
/** Header for the index column. Default "#". */
|
|
45
|
+
indexHeader?: string;
|
|
46
|
+
emptyMessage?: string;
|
|
47
|
+
/** Enable virtual scrolling; pair with `scrollHeight` for large runs. */
|
|
48
|
+
scrollable?: boolean;
|
|
49
|
+
scrollHeight?: string;
|
|
50
|
+
/** Virtual-scroll row height in px. Default 38. */
|
|
51
|
+
virtualItemSize?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const HeaderCell: React.FC<{ column: ScienceColumn }> = ({ column }) => (
|
|
55
|
+
<div className="ac-sci-th">
|
|
56
|
+
<span className="ac-sci-th-name">{column.label}</span>
|
|
57
|
+
{column.units ? <span className="ac-sci-th-unit">[{column.units}]</span> : null}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const ScienceTable: React.FC<ScienceTableProps> = ({
|
|
62
|
+
title,
|
|
63
|
+
columns,
|
|
64
|
+
rows,
|
|
65
|
+
showIndex = true,
|
|
66
|
+
indexHeader = '#',
|
|
67
|
+
emptyMessage = 'No data.',
|
|
68
|
+
scrollable = false,
|
|
69
|
+
scrollHeight,
|
|
70
|
+
virtualItemSize = 38,
|
|
71
|
+
}) => {
|
|
72
|
+
const virtual = scrollable && !!scrollHeight;
|
|
73
|
+
return (
|
|
74
|
+
<div className="ac-science-table-wrap">
|
|
75
|
+
{title != null && <div className="ac-science-table-title">{title}</div>}
|
|
76
|
+
<div className="ac-science-table-scroll">
|
|
77
|
+
<DataTable
|
|
78
|
+
className="ac-science-table"
|
|
79
|
+
value={rows}
|
|
80
|
+
scrollable={scrollable}
|
|
81
|
+
scrollHeight={scrollHeight}
|
|
82
|
+
virtualScrollerOptions={virtual ? { itemSize: virtualItemSize } : undefined}
|
|
83
|
+
emptyMessage={emptyMessage}
|
|
84
|
+
>
|
|
85
|
+
{showIndex ? (
|
|
86
|
+
<Column
|
|
87
|
+
header={indexHeader}
|
|
88
|
+
headerClassName="ac-sci-index"
|
|
89
|
+
bodyClassName="ac-sci-index"
|
|
90
|
+
align="center"
|
|
91
|
+
alignHeader="center"
|
|
92
|
+
style={{ width: '3.5rem', maxWidth: '3.5rem' }}
|
|
93
|
+
body={(_data: any, options: ColumnBodyOptions) => options.rowIndex + 1}
|
|
94
|
+
/>
|
|
95
|
+
) : null}
|
|
96
|
+
{columns.map((c) => (
|
|
97
|
+
<Column
|
|
98
|
+
key={c.field}
|
|
99
|
+
field={c.field}
|
|
100
|
+
header={<HeaderCell column={c} />}
|
|
101
|
+
align={c.align ?? 'center'}
|
|
102
|
+
alignHeader="center"
|
|
103
|
+
style={c.style}
|
|
104
|
+
bodyClassName="ac-sci-cell"
|
|
105
|
+
body={c.body ? (row: any) => c.body!(row) : undefined}
|
|
106
|
+
/>
|
|
107
|
+
))}
|
|
108
|
+
</DataTable>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default ScienceTable;
|