@adcops/autocore-react 3.3.48 → 3.3.54

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 (38) hide show
  1. package/dist/components/index.d.ts +10 -2
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/tis/ResultHistoryTable.d.ts +19 -0
  5. package/dist/components/tis/ResultHistoryTable.d.ts.map +1 -0
  6. package/dist/components/tis/ResultHistoryTable.js +1 -0
  7. package/dist/components/{TestDataView.d.ts → tis/TestDataView.d.ts} +9 -5
  8. package/dist/components/tis/TestDataView.d.ts.map +1 -0
  9. package/dist/components/tis/TestDataView.js +1 -0
  10. package/dist/components/tis/TestRawDataView.d.ts +18 -0
  11. package/dist/components/tis/TestRawDataView.d.ts.map +1 -0
  12. package/dist/components/tis/TestRawDataView.js +1 -0
  13. package/dist/components/tis/TestSetupForm.d.ts +35 -0
  14. package/dist/components/tis/TestSetupForm.d.ts.map +1 -0
  15. package/dist/components/tis/TestSetupForm.js +1 -0
  16. package/dist/components/tis/TisProvider.d.ts +93 -0
  17. package/dist/components/tis/TisProvider.d.ts.map +1 -0
  18. package/dist/components/tis/TisProvider.js +1 -0
  19. package/package.json +1 -1
  20. package/src/components/index.ts +40 -2
  21. package/src/components/tis/ResultHistoryTable.tsx +277 -0
  22. package/src/components/{TestDataView.tsx → tis/TestDataView.tsx} +72 -38
  23. package/src/components/{TestRawDataView.tsx → tis/TestRawDataView.tsx} +41 -24
  24. package/src/components/tis/TestSetupForm.tsx +314 -0
  25. package/src/components/tis/TisProvider.tsx +404 -0
  26. package/dist/components/ResultHistoryTable.d.ts +0 -7
  27. package/dist/components/ResultHistoryTable.d.ts.map +0 -1
  28. package/dist/components/ResultHistoryTable.js +0 -1
  29. package/dist/components/TestDataView.d.ts.map +0 -1
  30. package/dist/components/TestDataView.js +0 -1
  31. package/dist/components/TestRawDataView.d.ts +0 -14
  32. package/dist/components/TestRawDataView.d.ts.map +0 -1
  33. package/dist/components/TestRawDataView.js +0 -1
  34. package/dist/components/TestSetupForm.d.ts +0 -24
  35. package/dist/components/TestSetupForm.d.ts.map +0 -1
  36. package/dist/components/TestSetupForm.js +0 -1
  37. package/src/components/ResultHistoryTable.tsx +0 -162
  38. package/src/components/TestSetupForm.tsx +0 -255
@@ -1,162 +0,0 @@
1
- import React, { useState, useEffect, useContext } from 'react';
2
- import { DataTable } from 'primereact/datatable';
3
- import { Column } from 'primereact/column';
4
- import { Button } from 'primereact/button';
5
- import { EventEmitterContext } from '../core/EventEmitterContext';
6
- import { MessageType } from '../hub/CommandMessage';
7
-
8
- export interface ResultHistoryTableProps {
9
- projectId: string;
10
- definitionId: string;
11
- }
12
-
13
- /**
14
- * Convert a results `raw_data` blob (`{ colA: [...], colB: [...], ... }`) into
15
- * a CSV string. Keys with scalar (non-array) values are skipped; array lengths
16
- * are truncated to the shortest column so the grid is rectangular.
17
- *
18
- * Column order: `t` first if present (it's the canonical x-axis in every
19
- * current test schema), then the remaining keys in their JSON order. Each
20
- * cell is quoted only when it contains a comma, quote, or newline — matching
21
- * RFC 4180.
22
- */
23
- const rawBlobToCsv = (blob: any): string => {
24
- if (!blob || typeof blob !== 'object') return '';
25
-
26
- const entries: Array<[string, any[]]> = Object.entries(blob)
27
- .filter(([, v]) => Array.isArray(v)) as Array<[string, any[]]>;
28
- if (entries.length === 0) return '';
29
-
30
- entries.sort(([a], [b]) => (a === 't' ? -1 : b === 't' ? 1 : 0));
31
-
32
- const columns = entries.map(([k]) => k);
33
- const nRows = entries.reduce((min, [, arr]) => Math.min(min, arr.length), Infinity);
34
-
35
- const escape = (v: any): string => {
36
- if (v === null || v === undefined) return '';
37
- const s = String(v);
38
- return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
39
- };
40
-
41
- const lines: string[] = [columns.join(',')];
42
- for (let i = 0; i < nRows; i++) {
43
- lines.push(entries.map(([, arr]) => escape(arr[i])).join(','));
44
- }
45
- return lines.join('\n');
46
- };
47
-
48
- /**
49
- * Browser download shim: turn a string into a transient blob URL, click it,
50
- * and clean up. Works without any extra libraries.
51
- */
52
- const downloadCsv = (filename: string, csv: string) => {
53
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
54
- const url = URL.createObjectURL(blob);
55
- const a = document.createElement('a');
56
- a.href = url;
57
- a.download = filename;
58
- document.body.appendChild(a);
59
- a.click();
60
- a.remove();
61
- URL.revokeObjectURL(url);
62
- };
63
-
64
- export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectId, definitionId }) => {
65
- const [tests, setTests] = useState<any[]>([]);
66
- const [loading, setLoading] = useState(false);
67
- const [downloadingRunId, setDownloadingRunId] = useState<string | null>(null);
68
- const { invoke } = useContext(EventEmitterContext);
69
-
70
- const loadTests = async () => {
71
- setLoading(true);
72
- try {
73
- const resp: any = await invoke('results.list_tests' as any, MessageType.Request, {
74
- project_id: projectId,
75
- definition_id: definitionId
76
- } as any);
77
- if (resp.success && resp.data && resp.data.tests) {
78
- setTests(resp.data.tests);
79
- }
80
- } catch (err) {
81
- console.error("Failed to load tests", err);
82
- }
83
- setLoading(false);
84
- };
85
-
86
- useEffect(() => {
87
- loadTests();
88
- }, [projectId, definitionId]);
89
-
90
- const formatDate = (dateStr: string) => {
91
- if (!dateStr) return '';
92
- return new Date(dateStr).toLocaleString();
93
- };
94
-
95
- const handleDownload = async (rowData: any) => {
96
- const runId = rowData?.run_id;
97
- if (!runId) return;
98
- setDownloadingRunId(runId);
99
- try {
100
- const resp: any = await invoke('results.read_raw' as any, MessageType.Request, {
101
- project_id: projectId,
102
- definition_id: definitionId,
103
- run_id: runId,
104
- name: 'trace',
105
- } as any);
106
-
107
- if (!resp?.success || !resp.data) {
108
- console.warn('results.read_raw returned no data for', runId, resp?.error_message);
109
- alert(`No raw trace available for ${runId}${resp?.error_message ? `: ${resp.error_message}` : ''}`);
110
- return;
111
- }
112
-
113
- const csv = rawBlobToCsv(resp.data);
114
- if (!csv) {
115
- alert(`Raw trace for ${runId} is empty or has no array columns.`);
116
- return;
117
- }
118
- downloadCsv(`${projectId}_${definitionId}_${runId}.csv`, csv);
119
- } catch (err) {
120
- console.error('Failed to download raw trace', err);
121
- alert(`Download failed: ${err instanceof Error ? err.message : String(err)}`);
122
- } finally {
123
- setDownloadingRunId(null);
124
- }
125
- };
126
-
127
- return (
128
- <div>
129
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
130
- <h3>Test History: {definitionId}</h3>
131
- <Button icon="pi pi-refresh" label="Refresh" onClick={loadTests} disabled={loading} />
132
- </div>
133
-
134
- <DataTable value={tests} loading={loading} paginator rows={10} emptyMessage="No tests found.">
135
- <Column field="run_id" header="Run ID" sortable />
136
- <Column field="start_time" header="Date/Time" body={(rowData) => formatDate(rowData.start_time)} sortable />
137
- <Column header="Config / Results" body={(rowData) => (
138
- <div style={{ fontSize: '0.85em', color: 'var(--text-secondary-color)' }}>
139
- <div><strong>Config:</strong> {JSON.stringify(rowData.config)}</div>
140
- <div><strong>Results:</strong> {JSON.stringify(rowData.results)}</div>
141
- </div>
142
- )} />
143
- <Column
144
- header="Raw Data"
145
- style={{ width: '8rem' }}
146
- body={(rowData) => (
147
- <Button
148
- icon={downloadingRunId === rowData.run_id ? 'pi pi-spin pi-spinner' : 'pi pi-download'}
149
- label="CSV"
150
- size="small"
151
- outlined
152
- disabled={downloadingRunId !== null}
153
- onClick={() => handleDownload(rowData)}
154
- tooltip={`Download raw_data/trace.json as CSV`}
155
- tooltipOptions={{ position: 'left' }}
156
- />
157
- )}
158
- />
159
- </DataTable>
160
- </div>
161
- );
162
- };
@@ -1,255 +0,0 @@
1
- import React, { useState, useEffect, useContext } from 'react';
2
- import { AutoComplete } from 'primereact/autocomplete';
3
- import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
4
- import { EventEmitterContext } from '../core/EventEmitterContext';
5
- import { AutoCoreTagContext } from '../core/AutoCoreTagContext';
6
- import { MessageType } from '../hub/CommandMessage';
7
- import { ValueInput } from './ValueInput';
8
- import { TextInput } from './TextInput';
9
- import { InputText } from 'primereact/inputtext';
10
-
11
- export interface TestFieldDef {
12
- name: string;
13
- type: string;
14
- units?: string;
15
- required?: boolean;
16
- source?: string;
17
- }
18
-
19
- export interface TestDefinition {
20
- project_fields: TestFieldDef[];
21
- config_fields: TestFieldDef[];
22
- cycle_fields: TestFieldDef[];
23
- results_fields: TestFieldDef[];
24
- }
25
-
26
- export interface TestSetupFormProps {
27
- schema: TestDefinition;
28
- defaultProjectId?: string;
29
- defaultDefinitionId?: string;
30
- onProjectChange?: (projectId: string) => void;
31
- onDefinitionChange?: (definitionId: string) => void;
32
- onValidationChange?: (isValid: boolean, config: any) => void;
33
- }
34
-
35
- export const TestSetupForm: React.FC<TestSetupFormProps> = ({
36
- schema,
37
- defaultProjectId = "",
38
- defaultDefinitionId = "default",
39
- onProjectChange,
40
- onDefinitionChange,
41
- onValidationChange
42
- }) => {
43
- const [config, setConfig] = useState<any>({});
44
- const [projectId, setProjectId] = useState<string>(defaultProjectId);
45
- const [definitionId, setDefinitionId] = useState<string>(defaultDefinitionId);
46
-
47
- // Notify parent when projectId or definitionId changes
48
- useEffect(() => {
49
- if (onProjectChange) onProjectChange(projectId);
50
- }, [projectId, onProjectChange]);
51
-
52
- useEffect(() => {
53
- if (onDefinitionChange) onDefinitionChange(definitionId);
54
- }, [definitionId, onDefinitionChange]);
55
-
56
- const [existingProjects, setExistingProjects] = useState<string[]>([]);
57
- const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
58
-
59
- const [isValid, setIsValid] = useState(false);
60
- const { invoke, write } = useContext(EventEmitterContext);
61
- const { rawValues, findTagByFqdn } = useContext(AutoCoreTagContext);
62
-
63
- // Seed and live-update fields that declare a `source`.
64
- //
65
- // The schema's `source` is the remote FQDN (e.g. "gm.x_cycle_move_speed"),
66
- // but the event bus dispatches on the local `tagName` that the
67
- // AutoCoreTagProvider registered. We resolve FQDN → tagName via
68
- // `findTagByFqdn` and then read straight from the provider's `rawValues`,
69
- // which the provider already eager-reads on mount and keeps current via
70
- // subscriptions. That single source covers both the initial population
71
- // (page reload / definition change) and live updates — no separate
72
- // subscribe or invoke needed here.
73
- useEffect(() => {
74
- if (!schema) return;
75
-
76
- const allFields = [...schema.project_fields, ...schema.config_fields];
77
-
78
- setConfig((prev: any) => {
79
- let next = prev;
80
- for (const field of allFields) {
81
- if (!field.source) continue;
82
-
83
- const tag = findTagByFqdn(field.source);
84
- if (!tag) {
85
- console.warn(
86
- `TestSetupForm: no tag registered for source "${field.source}" ` +
87
- `(field "${field.name}"). Add it to acTagSpec with "ux": true ` +
88
- `in project.json, or this field won't populate or update.`
89
- );
90
- continue;
91
- }
92
-
93
- const rawVal = rawValues[tag.tagName];
94
- if (rawVal === undefined || rawVal === null) continue;
95
- if (next[field.name] === rawVal) continue;
96
-
97
- if (next === prev) next = { ...prev };
98
- next[field.name] = rawVal;
99
- }
100
- return next;
101
- });
102
- }, [schema, rawValues, findTagByFqdn]);
103
-
104
- useEffect(() => {
105
- const fetchProjects = async () => {
106
- try {
107
- const resp: any = await invoke('results.list_projects' as any, MessageType.Request as any, {} as any);
108
- if (resp.success && resp.data && resp.data.projects) {
109
- setExistingProjects(resp.data.projects);
110
- }
111
- } catch (err) {
112
- console.error("Failed to list projects", err);
113
- }
114
- };
115
- fetchProjects();
116
- }, [invoke]);
117
-
118
- const searchProjects = (event: AutoCompleteCompleteEvent) => {
119
- const query = event.query.toLowerCase();
120
- setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
121
- };
122
-
123
- const handleDefinitionIdChange = (value: string) => {
124
- const sanitized = value.replace(/[^a-zA-Z0-9_]/g, '');
125
- setDefinitionId(sanitized);
126
- };
127
-
128
- useEffect(() => {
129
- if (!schema) return;
130
- let valid = true;
131
- if (!projectId || projectId.trim() === '') valid = false;
132
- if (!definitionId || definitionId.trim() === '') valid = false;
133
-
134
- const allFields = [...schema.project_fields, ...schema.config_fields];
135
-
136
- for (const field of allFields) {
137
- if (field.required) {
138
- if (config[field.name] === undefined || config[field.name] === '' || config[field.name] === null) {
139
- valid = false;
140
- break;
141
- }
142
- }
143
- }
144
- setIsValid(valid);
145
- if (onValidationChange) {
146
- onValidationChange(valid, config);
147
- }
148
- }, [config, schema, projectId, definitionId, onValidationChange]);
149
-
150
- const isFieldValid = (field: TestFieldDef) => {
151
- if (!field.required) return true;
152
- return config[field.name] !== undefined && config[field.name] !== '' && config[field.name] !== null;
153
- };
154
-
155
- const handleProjectIdChange = (value: string | null | undefined) => {
156
- const val = value || '';
157
- const sanitized = val.replace(/[^a-zA-Z0-9_]/g, '');
158
- setProjectId(sanitized);
159
- };
160
-
161
- const handleFieldChange = async (field: TestFieldDef, val: any) => {
162
- setConfig({...config, [field.name]: val});
163
- if (field.source) {
164
- try {
165
- await write(field.source, val);
166
- } catch(e) {
167
- console.error("Failed to write to source:", e);
168
- }
169
- }
170
- };
171
-
172
- const renderField = (field: TestFieldDef) => {
173
- const valid = isFieldValid(field);
174
- const isNum = field.type !== 'string' && field.type !== 'bool';
175
-
176
- return (
177
- <React.Fragment key={field.name}>
178
- <span className="ac-form-label">{field.name}</span>
179
- {isNum ? (
180
- <ValueInput
181
- label={undefined}
182
- value={config[field.name] != null ? Number(config[field.name]) : null}
183
- onValueChanged={(val) => handleFieldChange(field, val)}
184
- className={!valid ? 'p-invalid' : ''}
185
- />
186
- ) : (
187
- <TextInput
188
- label={undefined}
189
- value={config[field.name] != null ? String(config[field.name]) : ''}
190
- onValueChanged={(val) => handleFieldChange(field, val)}
191
- className={!valid ? 'p-invalid' : ''}
192
- />
193
- )}
194
-
195
- <span className="ac-form-units">{field.units ?? ''}</span>
196
-
197
- <span style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
198
- <i className={valid ? "pi pi-check" : "pi pi-times"}></i>
199
- </span>
200
- </React.Fragment>
201
- );
202
- };
203
-
204
- if (!schema) {
205
- return (
206
- <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
207
- <h3 className="ac-form-section">No Test Definition Selected</h3>
208
- </div>
209
- );
210
- }
211
-
212
- return (
213
- <div className="ac-form-grid" style={{ padding: '1.25rem' }}>
214
- <h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
215
- Project & Definition
216
- <span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
217
- <i className={isValid ? "pi pi-check-circle" : "pi pi-exclamation-circle"}></i>
218
- </span>
219
- </h3>
220
-
221
- <span className="ac-form-label">Project ID</span>
222
- <AutoComplete
223
- value={projectId}
224
- suggestions={filteredProjects}
225
- completeMethod={searchProjects}
226
- onChange={(e) => handleProjectIdChange(e.value)}
227
- dropdown
228
- placeholder="Enter or select Project ID"
229
- className={!projectId || projectId.trim() === '' ? 'p-invalid' : ''}
230
- />
231
- <span />
232
- <span style={{ color: projectId && projectId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
233
- <i className={projectId && projectId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
234
- </span>
235
-
236
- <span className="ac-form-label">Definition ID</span>
237
- <InputText
238
- value={definitionId}
239
- onChange={(e) => handleDefinitionIdChange(e.target.value)}
240
- placeholder="Enter Definition ID"
241
- className={!definitionId || definitionId.trim() === '' ? 'p-invalid' : ''}
242
- />
243
- <span />
244
- <span style={{ color: definitionId && definitionId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)', display: 'flex', alignItems: 'center' }}>
245
- <i className={definitionId && definitionId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
246
- </span>
247
-
248
- <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
249
- {schema.project_fields.map(renderField)}
250
-
251
- <h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
252
- {schema.config_fields.map(renderField)}
253
- </div>
254
- );
255
- };