@adcops/autocore-react 3.5.9 → 3.5.11
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/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/tis-editor/TisConfigEditor.css +11 -0
- package/dist/components/tis-editor/TisConfigEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/TisConfigEditor.js +1 -1
- package/dist/components/tis-editor/editor/AuthoringContext.d.ts +37 -0
- package/dist/components/tis-editor/editor/AuthoringContext.d.ts.map +1 -0
- package/dist/components/tis-editor/editor/AuthoringContext.js +1 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts +11 -0
- package/dist/components/tis-editor/editor/ChartViewDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ChartViewDialog.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/RawDataEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/RawDataEditor.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts +7 -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/editor/ViewsEditor.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/ViewsEditor.js +1 -1
- package/dist/components/varpick/VariableInput.d.ts +41 -0
- package/dist/components/varpick/VariableInput.d.ts.map +1 -0
- package/dist/components/varpick/VariableInput.js +1 -0
- package/dist/components/varpick/VariablePicker.d.ts +45 -0
- package/dist/components/varpick/VariablePicker.d.ts.map +1 -0
- package/dist/components/varpick/VariablePicker.js +1 -0
- package/dist/components/varpick/index.d.ts +16 -0
- package/dist/components/varpick/index.d.ts.map +1 -0
- package/dist/components/varpick/index.js +1 -0
- package/dist/components/varpick/tisCatalogs.d.ts +85 -0
- package/dist/components/varpick/tisCatalogs.d.ts.map +1 -0
- package/dist/components/varpick/tisCatalogs.js +1 -0
- package/dist/components/varpick/varpick.css +167 -0
- package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
- package/dist/core/AutoCoreTagContext.js +1 -1
- package/dist/core/AutoCoreTagTypes.d.ts +13 -0
- package/dist/core/AutoCoreTagTypes.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useGmVariables.d.ts +57 -0
- package/dist/hooks/useGmVariables.d.ts.map +1 -0
- package/dist/hooks/useGmVariables.js +1 -0
- package/dist/hooks/useSequenceProducers.d.ts +38 -0
- package/dist/hooks/useSequenceProducers.d.ts.map +1 -0
- package/dist/hooks/useSequenceProducers.js +1 -0
- package/package.json +5 -2
- package/src/components/index.ts +17 -0
- package/src/components/tis-editor/TisConfigEditor.css +11 -0
- package/src/components/tis-editor/TisConfigEditor.tsx +31 -0
- package/src/components/tis-editor/editor/AuthoringContext.tsx +43 -0
- package/src/components/tis-editor/editor/ChartViewDialog.tsx +24 -7
- package/src/components/tis-editor/editor/FieldArrayEditor.tsx +1 -0
- package/src/components/tis-editor/editor/RawDataEditor.tsx +7 -2
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +66 -12
- package/src/components/tis-editor/editor/ViewsEditor.tsx +25 -0
- package/src/components/varpick/VariableInput.tsx +126 -0
- package/src/components/varpick/VariablePicker.tsx +168 -0
- package/src/components/varpick/index.ts +22 -0
- package/src/components/varpick/tisCatalogs.ts +235 -0
- package/src/components/varpick/varpick.css +167 -0
- package/src/core/AutoCoreTagContext.tsx +3 -1
- package/src/core/AutoCoreTagTypes.ts +14 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useGmVariables.ts +142 -0
- package/src/hooks/useSequenceProducers.ts +104 -0
- package/tools/tests/dist/TestFieldDialog-C4mKVh7q.js +32817 -0
- package/tools/tests/dist/autocore-react.css +257 -0
- package/tools/tests/dist/tis-editor.entry.js +8431 -40567
- package/tools/tests/dist/varpick.entry.js +113 -0
- package/tools/tests/live-server.probe.mjs +172 -0
- package/tools/tests/varpick.entry.tsx +81 -0
- package/tools/tests/varpick.test.mjs +208 -0
- package/tools/tests/vite.config.mjs +7 -2
- package/tools/tests/dist/tis-editor.entry.css +0 -90
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The catalogs the TIS method editor's pickers offer.
|
|
3
|
+
*
|
|
4
|
+
* Each field wants a different list, spelled differently, and getting either
|
|
5
|
+
* wrong fails SILENTLY:
|
|
6
|
+
*
|
|
7
|
+
* - a `config_fields` **Source** is written by the Test Setup form, so a
|
|
8
|
+
* hardware-owned variable there would have the operator's entry overwritten
|
|
9
|
+
* by the module on the next scan;
|
|
10
|
+
* - a **bound** is `${gm.…}` and an unresolvable one means NO bound — the
|
|
11
|
+
* field is simply unlimited, with nothing on screen to say so;
|
|
12
|
+
* - a `cycle_fields` **Source** has two possible producers and the editor used
|
|
13
|
+
* to show neither.
|
|
14
|
+
*
|
|
15
|
+
* Which is why the spelling and the filter live here, next to each other, rather
|
|
16
|
+
* than at each call site.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
gmCatalog,
|
|
21
|
+
type Catalog,
|
|
22
|
+
type CatalogGroup,
|
|
23
|
+
type GmVariable,
|
|
24
|
+
type VariableEntry,
|
|
25
|
+
} from '@adcops/autocore-varpick';
|
|
26
|
+
|
|
27
|
+
export interface TisCatalogContext {
|
|
28
|
+
/** From `gm.get_variables` (or the tag-spec fallback). */
|
|
29
|
+
variables: GmVariable[];
|
|
30
|
+
/** Unit label for a quantity, from the machine's resolved units.json. */
|
|
31
|
+
unitFor?: (quantity: string) => string | undefined;
|
|
32
|
+
/** Current value, formatted, by FQDN. */
|
|
33
|
+
liveFor?: (fqdn: string) => string | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Variables a form field may WRITE — a `config_fields` source.
|
|
38
|
+
*
|
|
39
|
+
* `TestSetupForm` writes the operator's entry straight to this tag, so a
|
|
40
|
+
* variable produced by a module is not a candidate: the write would land and
|
|
41
|
+
* then be overwritten before anything read it.
|
|
42
|
+
*/
|
|
43
|
+
export function configSourceCatalog(ctx: TisCatalogContext): Catalog {
|
|
44
|
+
return gmCatalog(ctx.variables, {
|
|
45
|
+
spelling: 'fqdn',
|
|
46
|
+
writableOnly: true,
|
|
47
|
+
unitFor: ctx.unitFor,
|
|
48
|
+
liveFor: ctx.liveFor,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Variables a bound (`min`/`max`) or a token default may READ. */
|
|
53
|
+
export function boundCatalog(ctx: TisCatalogContext): Catalog {
|
|
54
|
+
return gmCatalog(ctx.variables, {
|
|
55
|
+
spelling: 'token',
|
|
56
|
+
unitFor: ctx.unitFor,
|
|
57
|
+
liveFor: ctx.liveFor,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Where a **cycle field's** value comes from.
|
|
63
|
+
*
|
|
64
|
+
* The one catalog that is not just a variable list, because a cycle field has
|
|
65
|
+
* two possible producers and picking the wrong kind is invisible:
|
|
66
|
+
*
|
|
67
|
+
* 1. `source: "gm.<var>"` — the generated `add_cycle()` reads the tag.
|
|
68
|
+
* 2. `source: null` — whoever records the row supplies it by NAME. On gen4_ac
|
|
69
|
+
* that is the sequencer: a `store_value` step fills a row buffer keyed by
|
|
70
|
+
* name, and `tis.add_cycle` writes `cycle_data` verbatim without checking it
|
|
71
|
+
* against `cycle_fields`. So a declared column with no producer is blank on
|
|
72
|
+
* every row, forever, and nothing says why.
|
|
73
|
+
*
|
|
74
|
+
* Offering only FQDNs — which is what a plain variable picker would do — lets an
|
|
75
|
+
* author wire a binding that never fills. So the sequence's own `store_value`
|
|
76
|
+
* names are offered as a first-class group, and choosing one CLEARS the source
|
|
77
|
+
* (its `insert` is the empty string) rather than setting a binding.
|
|
78
|
+
*/
|
|
79
|
+
export function cycleSourceCatalog(
|
|
80
|
+
ctx: TisCatalogContext,
|
|
81
|
+
opts: {
|
|
82
|
+
/** `store_value` names in this method's `.seq.json`, if the host read it. */
|
|
83
|
+
producedNames?: string[];
|
|
84
|
+
/** The cycle field being edited, so its own name can be highlighted. */
|
|
85
|
+
fieldName?: string;
|
|
86
|
+
},
|
|
87
|
+
): Catalog {
|
|
88
|
+
const groups: CatalogGroup[] = [];
|
|
89
|
+
|
|
90
|
+
const produced = opts.producedNames ?? [];
|
|
91
|
+
if (produced.length > 0) {
|
|
92
|
+
const entries: VariableEntry[] = produced.map(name => ({
|
|
93
|
+
id: `seq:${name}`,
|
|
94
|
+
// Choosing a sequence-produced value means there is NO source: the
|
|
95
|
+
// row arrives carrying this name.
|
|
96
|
+
insert: '',
|
|
97
|
+
name,
|
|
98
|
+
kind: 'column',
|
|
99
|
+
description:
|
|
100
|
+
name === opts.fieldName
|
|
101
|
+
? 'The sequence stores this name every cycle, and it matches this column. Nothing else is needed — leave Source empty.'
|
|
102
|
+
: `The sequence stores this name every cycle. It only lands in this column if the column is called "${name}" too.`,
|
|
103
|
+
note:
|
|
104
|
+
name === opts.fieldName
|
|
105
|
+
? undefined
|
|
106
|
+
: `Rename this field to "${name}", or the value is recorded with no column to show it in.`,
|
|
107
|
+
keywords: ['sequence', 'store_value'],
|
|
108
|
+
}));
|
|
109
|
+
groups.push({
|
|
110
|
+
id: 'sequence',
|
|
111
|
+
title: 'Recorded by the sequence',
|
|
112
|
+
hint: 'Matched by NAME, not by binding. Picking one clears Source — that is correct, and is how gen4_ac records every column.',
|
|
113
|
+
entries,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Reading a GM tag at record time is the other legitimate producer, so the
|
|
118
|
+
// variables are still offered — below the sequence, because on a machine
|
|
119
|
+
// with a sequencer they are the less likely answer.
|
|
120
|
+
groups.push(
|
|
121
|
+
...gmCatalog(ctx.variables, {
|
|
122
|
+
spelling: 'fqdn',
|
|
123
|
+
unitFor: ctx.unitFor,
|
|
124
|
+
liveFor: ctx.liveFor,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return groups;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Columns a chart axis or series may plot.
|
|
133
|
+
*
|
|
134
|
+
* A chart naming a column that does not exist renders an empty plot with no
|
|
135
|
+
* error, which reads as "the machine recorded nothing".
|
|
136
|
+
*/
|
|
137
|
+
export function chartFieldCatalog(opts: {
|
|
138
|
+
cycleFields?: string[];
|
|
139
|
+
resultsFields?: string[];
|
|
140
|
+
rawColumns?: string[];
|
|
141
|
+
/** Project/config fields — one value for the whole test. */
|
|
142
|
+
testFields?: string[];
|
|
143
|
+
/** `raw_trace` views plot raw columns; `cycle_scatter` plots cycle fields. */
|
|
144
|
+
viewType?: string;
|
|
145
|
+
}): Catalog {
|
|
146
|
+
const groups: CatalogGroup[] = [];
|
|
147
|
+
const toEntries = (names: string[], kind: string): VariableEntry[] =>
|
|
148
|
+
names.map(name => ({ id: `${kind}:${name}`, insert: name, name, kind: 'column' as const }));
|
|
149
|
+
|
|
150
|
+
const raw = opts.rawColumns ?? [];
|
|
151
|
+
const cycle = opts.cycleFields ?? [];
|
|
152
|
+
const results = opts.resultsFields ?? [];
|
|
153
|
+
|
|
154
|
+
// The view's own type decides which list is the likely answer, so it goes
|
|
155
|
+
// first — but neither list is hidden, because a method can legitimately
|
|
156
|
+
// chart across both.
|
|
157
|
+
const rawFirst = opts.viewType === 'raw_trace';
|
|
158
|
+
const rawGroup: CatalogGroup = {
|
|
159
|
+
id: 'raw',
|
|
160
|
+
title: 'Raw trace columns',
|
|
161
|
+
hint: 'Per-cycle trace columns declared under Raw Data.',
|
|
162
|
+
entries: toEntries(raw, 'raw'),
|
|
163
|
+
};
|
|
164
|
+
const cycleGroup: CatalogGroup = {
|
|
165
|
+
id: 'cycle',
|
|
166
|
+
title: 'Cycle Data columns',
|
|
167
|
+
hint: 'One value per cycle.',
|
|
168
|
+
entries: toEntries(cycle, 'cycle'),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
if (rawFirst) groups.push(rawGroup, cycleGroup);
|
|
172
|
+
else groups.push(cycleGroup, rawGroup);
|
|
173
|
+
|
|
174
|
+
groups.push({
|
|
175
|
+
id: 'results',
|
|
176
|
+
title: 'Results',
|
|
177
|
+
hint: 'One value per test.',
|
|
178
|
+
entries: toEntries(results, 'result'),
|
|
179
|
+
});
|
|
180
|
+
groups.push({
|
|
181
|
+
id: 'test',
|
|
182
|
+
title: 'Test setup values',
|
|
183
|
+
hint: 'Entered once per test, so they plot as a constant across cycles.',
|
|
184
|
+
entries: toEntries(opts.testFields ?? [], 'test'),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return groups.filter(g => g.entries.length > 0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sources a raw-data column may read.
|
|
192
|
+
*
|
|
193
|
+
* `time` and `derived` are the two the server always understands; everything
|
|
194
|
+
* else is a module channel FQDN. The channel list is not modelled here — the
|
|
195
|
+
* editor has no NI catalog — so the two known values are offered by name and
|
|
196
|
+
* the field stays free text for the rest.
|
|
197
|
+
*/
|
|
198
|
+
export function rawSourceCatalog(channels?: string[]): Catalog {
|
|
199
|
+
const groups: CatalogGroup[] = [
|
|
200
|
+
{
|
|
201
|
+
id: 'builtin',
|
|
202
|
+
title: 'Built-in',
|
|
203
|
+
entries: [
|
|
204
|
+
{
|
|
205
|
+
id: 'src:time',
|
|
206
|
+
insert: 'time',
|
|
207
|
+
name: 'time',
|
|
208
|
+
kind: 'column',
|
|
209
|
+
description: 'Sample timestamp, seconds from the start of the cycle.',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'src:derived',
|
|
213
|
+
insert: 'derived',
|
|
214
|
+
name: 'derived',
|
|
215
|
+
kind: 'column',
|
|
216
|
+
description: 'Computed from the other columns by this column’s Formula.',
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
if (channels && channels.length > 0) {
|
|
222
|
+
groups.push({
|
|
223
|
+
id: 'channels',
|
|
224
|
+
title: 'DAQ channels',
|
|
225
|
+
hint: 'Published by the acquisition module.',
|
|
226
|
+
entries: channels.map(c => ({
|
|
227
|
+
id: `chan:${c}`,
|
|
228
|
+
insert: c,
|
|
229
|
+
name: c,
|
|
230
|
+
kind: 'column' as const,
|
|
231
|
+
})),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return groups;
|
|
235
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Variable picker.
|
|
3
|
+
*
|
|
4
|
+
* Colours come from the PrimeReact theme's variables so the dialog matches
|
|
5
|
+
* whatever theme the machine is running, light or dark. Every value has a
|
|
6
|
+
* literal fallback: an un-themed host (the playground, a test harness) must
|
|
7
|
+
* still render legible text rather than black on black.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.ac-varpick__input {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: stretch;
|
|
13
|
+
gap: 0;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.ac-varpick__input .p-inputtext {
|
|
18
|
+
flex: 1 1 auto;
|
|
19
|
+
min-width: 0;
|
|
20
|
+
border-top-right-radius: 0;
|
|
21
|
+
border-bottom-right-radius: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Sized to the full control height, not shrunk to fit: these screens are
|
|
25
|
+
* touchscreens, and a button an operator has to aim at is one they mis-tap. */
|
|
26
|
+
.ac-varpick__input > .p-button {
|
|
27
|
+
flex: 0 0 auto;
|
|
28
|
+
border-top-left-radius: 0;
|
|
29
|
+
border-bottom-left-radius: 0;
|
|
30
|
+
margin-left: -1px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.ac-varpick__hint {
|
|
34
|
+
font-size: 0.8125rem;
|
|
35
|
+
color: var(--text-color-secondary, #6b7280);
|
|
36
|
+
margin-bottom: 0.75rem;
|
|
37
|
+
line-height: 1.4;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ac-varpick__search { width: 100%; }
|
|
41
|
+
|
|
42
|
+
.ac-varpick__empty {
|
|
43
|
+
padding: 1.5rem 0.25rem;
|
|
44
|
+
color: var(--text-color-secondary, #6b7280);
|
|
45
|
+
font-size: 0.875rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.ac-varpick__list {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 1rem;
|
|
52
|
+
margin-top: 0.75rem;
|
|
53
|
+
/* Bounded so the search box and the footer stay put while the list moves. */
|
|
54
|
+
max-height: 55vh;
|
|
55
|
+
overflow-y: auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ac-varpick__group { display: flex; flex-direction: column; gap: 2px; }
|
|
59
|
+
|
|
60
|
+
.ac-varpick__grouphead {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
align-items: baseline;
|
|
64
|
+
gap: 0.5rem;
|
|
65
|
+
padding-bottom: 0.25rem;
|
|
66
|
+
margin-bottom: 0.25rem;
|
|
67
|
+
border-bottom: 1px solid var(--surface-border, rgba(127, 127, 127, 0.25));
|
|
68
|
+
position: sticky;
|
|
69
|
+
top: 0;
|
|
70
|
+
background: var(--surface-overlay, #ffffff);
|
|
71
|
+
z-index: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.ac-varpick__grouptitle {
|
|
75
|
+
font-size: 0.75rem;
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
letter-spacing: 0.04em;
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
color: var(--text-color-secondary, #6b7280);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.ac-varpick__grouphint {
|
|
83
|
+
font-size: 0.75rem;
|
|
84
|
+
color: var(--text-color-secondary, #8b95a1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Two lines by design — the identifier, then what it MEANS. A one-line list of
|
|
89
|
+
* identifiers is the problem this dialog exists to solve, so the description is
|
|
90
|
+
* never truncated and never hidden behind a hover: there is no hover on a
|
|
91
|
+
* touchscreen.
|
|
92
|
+
*/
|
|
93
|
+
.ac-varpick__row {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
align-items: flex-start;
|
|
97
|
+
gap: 2px;
|
|
98
|
+
width: 100%;
|
|
99
|
+
text-align: left;
|
|
100
|
+
min-height: 2.25rem;
|
|
101
|
+
padding: 0.375rem 0.75rem;
|
|
102
|
+
border: 1px solid transparent;
|
|
103
|
+
border-radius: 3px;
|
|
104
|
+
background: transparent;
|
|
105
|
+
color: var(--text-color, #111827);
|
|
106
|
+
font: inherit;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.ac-varpick__row:hover { background: var(--surface-hover, rgba(127, 127, 127, 0.08)); }
|
|
111
|
+
|
|
112
|
+
.ac-varpick__row:focus-visible {
|
|
113
|
+
outline: 2px solid var(--primary-color, #2563eb);
|
|
114
|
+
outline-offset: -2px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ac-varpick__row--on {
|
|
118
|
+
background: var(--highlight-bg, rgba(37, 99, 235, 0.14));
|
|
119
|
+
border-color: var(--primary-color, #2563eb);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ac-varpick__row--off { opacity: 0.55; cursor: default; }
|
|
123
|
+
|
|
124
|
+
.ac-varpick__line {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-wrap: wrap;
|
|
127
|
+
align-items: baseline;
|
|
128
|
+
gap: 0.5rem;
|
|
129
|
+
min-width: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ac-varpick__name {
|
|
133
|
+
font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ac-varpick__label { font-size: 0.875rem; }
|
|
138
|
+
|
|
139
|
+
.ac-varpick__chip {
|
|
140
|
+
font-size: 0.75rem;
|
|
141
|
+
padding: 0 6px;
|
|
142
|
+
border-radius: 999px;
|
|
143
|
+
background: var(--surface-100, rgba(127, 127, 127, 0.12));
|
|
144
|
+
border: 1px solid var(--surface-border, rgba(127, 127, 127, 0.25));
|
|
145
|
+
color: var(--text-color-secondary, #6b7280);
|
|
146
|
+
/* A unit symbol's case is meaningful: "mm/s", "N", "MPa". */
|
|
147
|
+
text-transform: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* The current reading. Nothing turns an unfamiliar name into an obvious one
|
|
151
|
+
* faster than seeing what it says right now. */
|
|
152
|
+
.ac-varpick__live {
|
|
153
|
+
font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
154
|
+
font-size: 0.75rem;
|
|
155
|
+
color: var(--green-600, #15803d);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.ac-varpick__desc {
|
|
159
|
+
font-size: 0.8125rem;
|
|
160
|
+
color: var(--text-color-secondary, #6b7280);
|
|
161
|
+
line-height: 1.35;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.ac-varpick__why { font-size: 0.8125rem; color: var(--orange-600, #b45309); }
|
|
165
|
+
.ac-varpick__note { font-size: 0.8125rem; color: var(--blue-600, #0369a1); }
|
|
166
|
+
|
|
167
|
+
.ac-varpick__foot { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
@@ -112,6 +112,7 @@ export const AutoCoreTagContext = createContext<BaseContextValue<VMapRuntime>>({
|
|
|
112
112
|
scales: {},
|
|
113
113
|
updateScale: async () => { },
|
|
114
114
|
findTagByFqdn: () => undefined,
|
|
115
|
+
tags: [],
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
/**
|
|
@@ -912,12 +913,13 @@ export const AutoCoreTagProvider: React.FC<{
|
|
|
912
913
|
scales: scaleValues,
|
|
913
914
|
updateScale,
|
|
914
915
|
findTagByFqdn,
|
|
916
|
+
tags,
|
|
915
917
|
units,
|
|
916
918
|
activeSystem,
|
|
917
919
|
setUnitSystem,
|
|
918
920
|
formatQuantity,
|
|
919
921
|
}), [values, rawValues, isLoading, write, tap, press, release, scaleValues,
|
|
920
|
-
updateScale, findTagByFqdn, units, activeSystem, setUnitSystem, formatQuantity]);
|
|
922
|
+
updateScale, findTagByFqdn, tags, units, activeSystem, setUnitSystem, formatQuantity]);
|
|
921
923
|
|
|
922
924
|
|
|
923
925
|
return (
|
|
@@ -435,4 +435,18 @@ export interface BaseContextValue<VMap extends Record<string, any>> {
|
|
|
435
435
|
* if no tag is registered for the given FQDN.
|
|
436
436
|
*/
|
|
437
437
|
findTagByFqdn: (fqdn: string) => TagConfig | undefined;
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* The tag spec this provider was built with.
|
|
441
|
+
*
|
|
442
|
+
* Exposed so a consumer can enumerate what the HMI knows about rather than
|
|
443
|
+
* having every host thread `acTagSpec` down by hand. The variable picker
|
|
444
|
+
* uses it as its fallback catalog when the server is an older build with no
|
|
445
|
+
* `gm.get_variables` — names and types, no descriptions.
|
|
446
|
+
*
|
|
447
|
+
* It is NOT the machine's full variable table: `acctl codegen-tags` emits
|
|
448
|
+
* only variables marked `ux: true`. Treat it as a floor, never as the
|
|
449
|
+
* authoritative list.
|
|
450
|
+
*/
|
|
451
|
+
tags: readonly TagConfig[];
|
|
438
452
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -11,3 +11,14 @@
|
|
|
11
11
|
export {useAdsRegisterSymbols, useAdsWriteValue, useAdsTapValue} from "./adsHooks";
|
|
12
12
|
export {useScaledValue, kMillimeters2Inches } from './useScaledValue';
|
|
13
13
|
export * from './useServeletData';
|
|
14
|
+
|
|
15
|
+
// The machine's GM variable table, described — the catalog behind the
|
|
16
|
+
// variable picker. Degrades to the generated tag spec on a server too old to
|
|
17
|
+
// answer `gm.get_variables`.
|
|
18
|
+
export { useGmVariables } from './useGmVariables';
|
|
19
|
+
export type { UseGmVariablesResult, UseGmVariablesOptions, GmIpcInvoker } from './useGmVariables';
|
|
20
|
+
|
|
21
|
+
// The `store_value` names a method's sequence records — the other producer of
|
|
22
|
+
// a TIS cycle column, and the one nothing used to check.
|
|
23
|
+
export { useSequenceProducers, collectStoredNames } from './useSequenceProducers';
|
|
24
|
+
export type { UseSequenceProducersResult, UseSequenceProducersOptions } from './useSequenceProducers';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useGmVariables — the machine's GM variable table, for the variable picker.
|
|
3
|
+
*
|
|
4
|
+
* Reads `gm.get_variables`: `gm.get_catalog` with the metadata attached — type,
|
|
5
|
+
* description, quantity, and whether a UI may write the variable. Everything it
|
|
6
|
+
* returns already lives in project.json; the verb exists because a catalog of
|
|
7
|
+
* bare FQDN strings cannot tell an author what a name means, which is why every
|
|
8
|
+
* field that asks for one has been a text box.
|
|
9
|
+
*
|
|
10
|
+
* ## Degrading on an older server
|
|
11
|
+
*
|
|
12
|
+
* `gm.get_variables` was added alongside the picker, so a machine running an
|
|
13
|
+
* older `autocore_server` answers with an error. That is not a failure worth
|
|
14
|
+
* showing an operator: the picker still works, it just lists names without
|
|
15
|
+
* descriptions. So the hook falls back to the generated tag spec the HMI
|
|
16
|
+
* already has (`acTagSpec`), which carries FQDN, type and quantity but no prose,
|
|
17
|
+
* and reports the degradation through `partial` rather than through `error`.
|
|
18
|
+
*
|
|
19
|
+
* The fallback covers strictly less: `acctl codegen-tags` only emits variables
|
|
20
|
+
* marked `ux: true`, and a `${gm.…}` bound may legitimately name one that is
|
|
21
|
+
* not. Hence `partial` — a caller can say so on screen instead of quietly
|
|
22
|
+
* offering an incomplete list.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
26
|
+
import type { GmVariable } from '@adcops/autocore-varpick';
|
|
27
|
+
import { EventEmitterContext } from '../core/EventEmitterContext';
|
|
28
|
+
import { MessageType } from '../hub/CommandMessage';
|
|
29
|
+
import type { TagConfig } from '../core/AutoCoreTagTypes';
|
|
30
|
+
|
|
31
|
+
export interface GmIpcInvoker {
|
|
32
|
+
(topic: string, payload: object): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
data?: any;
|
|
35
|
+
error_message?: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UseGmVariablesOptions {
|
|
40
|
+
invoker?: GmIpcInvoker;
|
|
41
|
+
/**
|
|
42
|
+
* The HMI's generated tag spec, used when the server cannot describe its
|
|
43
|
+
* own variables. Without it, an old server yields an empty picker.
|
|
44
|
+
*/
|
|
45
|
+
fallbackTags?: readonly TagConfig[];
|
|
46
|
+
/** Skip the read entirely (e.g. while a dialog is closed). */
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UseGmVariablesResult {
|
|
51
|
+
variables: GmVariable[];
|
|
52
|
+
loading: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* True when the list came from the generated tag spec rather than the
|
|
55
|
+
* server — names and types, no descriptions, and `ux: true` variables only.
|
|
56
|
+
*/
|
|
57
|
+
partial: boolean;
|
|
58
|
+
/** Set only when there is nothing to show at all. */
|
|
59
|
+
error: string | null;
|
|
60
|
+
refresh: () => Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Derive what a tag spec can honestly say about a variable, and nothing more. */
|
|
64
|
+
function fromTagSpec(tags: readonly TagConfig[]): GmVariable[] {
|
|
65
|
+
return tags
|
|
66
|
+
.filter(t => typeof t.fqdn === 'string' && t.fqdn.toLowerCase().startsWith('gm.'))
|
|
67
|
+
.map(t => ({
|
|
68
|
+
fqdn: t.fqdn,
|
|
69
|
+
name: t.fqdn.slice(3),
|
|
70
|
+
type: String(t.valueType ?? ''),
|
|
71
|
+
description: '',
|
|
72
|
+
quantity: t.quantity ?? t.scale,
|
|
73
|
+
// Everything in the tag spec is there BECAUSE it is ux-exposed.
|
|
74
|
+
ux: true,
|
|
75
|
+
nonvolatile: false,
|
|
76
|
+
// The tag spec does not record a link, and guessing "writable"
|
|
77
|
+
// wrong in this direction is the safe one: the picker shows the
|
|
78
|
+
// variable and the machine refuses a pointless write, rather than
|
|
79
|
+
// the variable vanishing from a list with no explanation.
|
|
80
|
+
writable: true,
|
|
81
|
+
is_signal: false,
|
|
82
|
+
}))
|
|
83
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useGmVariables(options?: UseGmVariablesOptions): UseGmVariablesResult {
|
|
87
|
+
const ctx = useContext(EventEmitterContext);
|
|
88
|
+
const enabled = options?.enabled !== false;
|
|
89
|
+
|
|
90
|
+
const invoker: GmIpcInvoker = options?.invoker
|
|
91
|
+
?? (async (topic, payload) => await ctx.invoke(topic as any, MessageType.Request, payload as any));
|
|
92
|
+
|
|
93
|
+
// Same stability trick as useTisConfig: a caller that builds its invoker
|
|
94
|
+
// inline would otherwise change it every render and re-trigger the effect
|
|
95
|
+
// in a loop.
|
|
96
|
+
const invokerRef = useRef(invoker);
|
|
97
|
+
invokerRef.current = invoker;
|
|
98
|
+
const fallbackRef = useRef(options?.fallbackTags);
|
|
99
|
+
fallbackRef.current = options?.fallbackTags;
|
|
100
|
+
|
|
101
|
+
const [variables, setVariables] = useState<GmVariable[]>([]);
|
|
102
|
+
const [loading, setLoading] = useState<boolean>(enabled);
|
|
103
|
+
const [partial, setPartial] = useState<boolean>(false);
|
|
104
|
+
const [error, setError] = useState<string | null>(null);
|
|
105
|
+
|
|
106
|
+
const refresh = useCallback(async () => {
|
|
107
|
+
setLoading(true);
|
|
108
|
+
try {
|
|
109
|
+
const res = await invokerRef.current('gm.get_variables', {});
|
|
110
|
+
const list = res?.data?.variables;
|
|
111
|
+
if (res?.success && Array.isArray(list)) {
|
|
112
|
+
setVariables(list as GmVariable[]);
|
|
113
|
+
setPartial(false);
|
|
114
|
+
setError(null);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(res?.error_message || 'gm.get_variables returned no variables');
|
|
118
|
+
} catch (e: any) {
|
|
119
|
+
const tags = fallbackRef.current;
|
|
120
|
+
if (tags && tags.length > 0) {
|
|
121
|
+
setVariables(fromTagSpec(tags));
|
|
122
|
+
setPartial(true);
|
|
123
|
+
setError(null);
|
|
124
|
+
} else {
|
|
125
|
+
setVariables([]);
|
|
126
|
+
setPartial(false);
|
|
127
|
+
setError(String(e?.message ?? e));
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
setLoading(false);
|
|
131
|
+
}
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!enabled) { setLoading(false); return; }
|
|
136
|
+
void refresh();
|
|
137
|
+
}, [enabled, refresh]);
|
|
138
|
+
|
|
139
|
+
return { variables, loading, partial, error, refresh };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default useGmVariables;
|