@adcops/autocore-react 3.3.65 → 3.3.68

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.
@@ -3,6 +3,25 @@ export type AmsTypeSchema = any;
3
3
  export type AmsSchemaRegistry = {
4
4
  [assetType: string]: AmsTypeSchema;
5
5
  };
6
+ /**
7
+ * One role an asset_type can play, derived from a `select=by_location`
8
+ * `asset_ref` in `project.json::test_methods`. Used by the AMS UI to
9
+ * present a dropdown when registering a new asset, instead of forcing
10
+ * the operator to type a free-form `location` string.
11
+ */
12
+ export interface AmsRole {
13
+ /** The literal `location` string the resolver matches against. */
14
+ location: string;
15
+ /** Human-readable label, e.g. "Triaxial Transducer (TSDR)". */
16
+ label: string | null;
17
+ /** Optional one-line explanation of what this role does. */
18
+ description: string | null;
19
+ /** Test method IDs whose start_test will pick up an asset in this role. */
20
+ used_by: string[];
21
+ }
22
+ export type AmsRoleRegistry = {
23
+ [assetType: string]: AmsRole[];
24
+ };
6
25
  export interface AmsAlerts {
7
26
  assetCount: number;
8
27
  calibrationOverdue: number;
@@ -23,6 +42,13 @@ export interface AmsSelection {
23
42
  export interface AmsContextValue {
24
43
  schemas: AmsSchemaRegistry;
25
44
  schemasLoaded: boolean;
45
+ /**
46
+ * Roles per asset_type. Empty array means the type isn't picked up
47
+ * via `by_location` anywhere in this project — registration UIs
48
+ * should hide the Role field for that type.
49
+ */
50
+ roles: AmsRoleRegistry;
51
+ rolesLoaded: boolean;
26
52
  alerts: AmsAlerts;
27
53
  assets: AmsAssetEntry[];
28
54
  refreshAssets: () => Promise<void>;
@@ -39,6 +65,7 @@ export interface AmsProviderProps {
39
65
  export declare const AmsProvider: React.FC<AmsProviderProps>;
40
66
  export declare const useAms: () => AmsContextValue;
41
67
  export declare const useAmsSchemas: () => AmsSchemaRegistry;
68
+ export declare const useAmsRoles: () => AmsRoleRegistry;
42
69
  export declare const useAmsAlerts: () => AmsAlerts;
43
70
  export declare const useAmsAssets: () => AmsAssetEntry[];
44
71
  export declare const useAmsSelection: () => readonly [AmsSelection, (patch: Partial<AmsSelection>) => void];
@@ -1 +1 @@
1
- {"version":3,"file":"AmsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AmsProvider.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,EAOV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAChC,MAAM,MAAM,iBAAiB,GAAG;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvE,MAAM,WAAW,SAAS;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,iBAAiB,CAAC;IACjD,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACpD,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACzE,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACpD,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;CACxD;AAwBD,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;CACvB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA8GlD,CAAC;AAMF,eAAO,MAAM,MAAM,uBAAwC,CAAC;AAC5D,eAAO,MAAM,aAAa,yBAAyC,CAAC;AACpE,eAAO,MAAM,YAAY,iBAAyC,CAAC;AACnE,eAAO,MAAM,YAAY,uBAAyC,CAAC;AACnE,eAAO,MAAM,eAAe,wCArJF,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAwJvD,CAAC"}
1
+ {"version":3,"file":"AmsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AmsProvider.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,EAOV,KAAK,SAAS,EACjB,MAAM,OAAO,CAAC;AAQf,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC;AAChC,MAAM,MAAM,iBAAiB,GAAG;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACpB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,EAAE,CAAC;CACrB;AACD,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,EAAE,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,SAAS;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,SAAS,GAAG,iBAAiB,CAAC;IACjD,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,KAAK,EAAE,eAAe,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACpD,gBAAgB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACzE,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IACpD,SAAS,EAAE,YAAY,CAAC;IACxB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;CACxD;AA0BD,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,SAAS,CAAC;CACvB;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAmIlD,CAAC;AAMF,eAAO,MAAM,MAAM,uBAAwC,CAAC;AAC5D,eAAO,MAAM,aAAa,yBAAyC,CAAC;AACpE,eAAO,MAAM,WAAW,uBAAyC,CAAC;AAClE,eAAO,MAAM,YAAY,iBAAyC,CAAC;AACnE,eAAO,MAAM,YAAY,uBAAyC,CAAC;AACnE,eAAO,MAAM,eAAe,wCA7KF,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAgLvD,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_ALERTS={assetCount:0,calibrationOverdue:0,laneUnavailable:0},AmsContext=createContext({schemas:{},schemasLoaded:!1,alerts:EMPTY_ALERTS,assets:[],refreshAssets:async()=>{},readAsset:async()=>null,listCalibrations:async()=>[],readCalibration:async()=>null,readUsage:async()=>null,selection:{assetType:null,assetId:null},setSelection:()=>{}});export const AmsProvider=({children:e})=>{const{invoke:s,subscribe:t,unsubscribe:a}=useContext(EventEmitterContext),[n,r]=useState({}),[c,u]=useState(!1),[l,o]=useState(EMPTY_ALERTS),[i,m]=useState([]),[d,C]=useState({assetType:null,assetId:null}),x=useCallback(e=>{C(s=>({...s,...e}))},[]);useEffect(()=>{let e=!1;return(async()=>{try{const t=await s("ams.list_schemas",MessageType.Request,{});if(e)return;t?.success&&t.data&&(r(t.data.asset_types??{}),u(!0))}catch(e){}})(),()=>{e=!0}},[]);const b=useCallback(async()=>{try{const e=await s("ams.list_assets",MessageType.Request,{include_retired:!0});e?.success&&m(e.data.assets??[])}catch(e){}},[s]);useEffect(()=>{b()},[b]),useEffect(()=>{const e=[t("ams.asset_changed",()=>{b()}),t("ams.calibration_added",()=>{b()}),t("ams.asset_count",e=>o(s=>({...s,assetCount:Number(e)||0}))),t("ams.alert_calibration_overdue",e=>o(s=>({...s,calibrationOverdue:Number(e)||0}))),t("ams.alert_lane_unavailable",e=>o(s=>({...s,laneUnavailable:Number(e)||0})))];return()=>{e.forEach(a)}},[t,a,b]);const y=useCallback(async e=>{try{const t=await s("ams.read_asset",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),_=useCallback(async e=>{try{const t=await s("ams.list_calibrations",MessageType.Request,{asset_id:e});return t?.success?t.data.cal_ids??[]:[]}catch{return[]}},[s]),A=useCallback(async(e,t)=>{try{const a=await s("ams.read_calibration",MessageType.Request,{asset_id:e,cal_id:t});return a?.success?a.data:null}catch{return null}},[s]),h=useCallback(async e=>{try{const t=await s("ams.read_usage",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),p=useMemo(()=>({schemas:n,schemasLoaded:c,alerts:l,assets:i,refreshAssets:b,readAsset:y,listCalibrations:_,readCalibration:A,readUsage:h,selection:d,setSelection:x}),[n,c,l,i,b,y,_,A,h,d,x]);return _jsx(AmsContext.Provider,{value:p,children:e})};export const useAms=()=>useContext(AmsContext);export const useAmsSchemas=()=>useContext(AmsContext).schemas;export const useAmsAlerts=()=>useContext(AmsContext).alerts;export const useAmsAssets=()=>useContext(AmsContext).assets;export const useAmsSelection=()=>{const e=useContext(AmsContext);return[e.selection,e.setSelection]};
1
+ import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_ALERTS={assetCount:0,calibrationOverdue:0,laneUnavailable:0},AmsContext=createContext({schemas:{},schemasLoaded:!1,roles:{},rolesLoaded:!1,alerts:EMPTY_ALERTS,assets:[],refreshAssets:async()=>{},readAsset:async()=>null,listCalibrations:async()=>[],readCalibration:async()=>null,readUsage:async()=>null,selection:{assetType:null,assetId:null},setSelection:()=>{}});export const AmsProvider=({children:e})=>{const{invoke:s,subscribe:t,unsubscribe:a}=useContext(EventEmitterContext),[n,r]=useState({}),[c,o]=useState(!1),[u,l]=useState({}),[i,m]=useState(!1),[d,C]=useState(EMPTY_ALERTS),[x,y]=useState([]),[b,_]=useState({assetType:null,assetId:null}),A=useCallback(e=>{_(s=>({...s,...e}))},[]);useEffect(()=>{let e=!1;return(async()=>{try{const t=await s("ams.list_schemas",MessageType.Request,{});if(e)return;t?.success&&t.data&&(r(t.data.asset_types??{}),o(!0))}catch(e){}})(),(async()=>{try{const t=await s("ams.list_roles",MessageType.Request,{});if(e)return;t?.success&&t.data?(l(t.data.roles??{}),m(!0)):m(!0)}catch(e){m(!0)}})(),()=>{e=!0}},[]);const p=useCallback(async()=>{try{const e=await s("ams.list_assets",MessageType.Request,{include_retired:!0});e?.success&&y(e.data.assets??[])}catch(e){}},[s]);useEffect(()=>{p()},[p]),useEffect(()=>{const e=[t("ams.asset_changed",()=>{p()}),t("ams.calibration_added",()=>{p()}),t("ams.asset_count",e=>C(s=>({...s,assetCount:Number(e)||0}))),t("ams.alert_calibration_overdue",e=>C(s=>({...s,calibrationOverdue:Number(e)||0}))),t("ams.alert_lane_unavailable",e=>C(s=>({...s,laneUnavailable:Number(e)||0})))];return()=>{e.forEach(a)}},[t,a,p]);const h=useCallback(async e=>{try{const t=await s("ams.read_asset",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),f=useCallback(async e=>{try{const t=await s("ams.list_calibrations",MessageType.Request,{asset_id:e});return t?.success?t.data.cal_ids??[]:[]}catch{return[]}},[s]),E=useCallback(async(e,t)=>{try{const a=await s("ams.read_calibration",MessageType.Request,{asset_id:e,cal_id:t});return a?.success?a.data:null}catch{return null}},[s]),S=useCallback(async e=>{try{const t=await s("ams.read_usage",MessageType.Request,{asset_id:e});return t?.success?t.data:null}catch{return null}},[s]),T=useMemo(()=>({schemas:n,schemasLoaded:c,roles:u,rolesLoaded:i,alerts:d,assets:x,refreshAssets:p,readAsset:h,listCalibrations:f,readCalibration:E,readUsage:S,selection:b,setSelection:A}),[n,c,u,i,d,x,p,h,f,E,S,b,A]);return _jsx(AmsContext.Provider,{value:T,children:e})};export const useAms=()=>useContext(AmsContext);export const useAmsSchemas=()=>useContext(AmsContext).schemas;export const useAmsRoles=()=>useContext(AmsContext).roles;export const useAmsAlerts=()=>useContext(AmsContext).alerts;export const useAmsAssets=()=>useContext(AmsContext).assets;export const useAmsSelection=()=>{const e=useContext(AmsContext);return[e.selection,e.setSelection]};
@@ -1 +1 @@
1
- {"version":3,"file":"AssetDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetDetailView.tsx"],"names":[],"mappings":"AAQA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAOnD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAqFnC,CAAC"}
1
+ {"version":3,"file":"AssetDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetDetailView.tsx"],"names":[],"mappings":"AAQA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAOnD,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EA4FnC,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";export const AssetDetailView=()=>{const{selection:e,schemas:s,readAsset:t,listCalibrations:a,readCalibration:r,readUsage:i}=useAms(),[l,n]=useState(null),[o,d]=useState([]),[c,m]=useState(null),[_,x]=useState(!1),p=async()=>{if(!e.assetId)return n(null),d([]),void m(null);const s=await t(e.assetId);n(s);const l=await a(e.assetId),o=await Promise.all(l.map(s=>r(e.assetId,s)));d(o.filter(Boolean));const c=await i(e.assetId);m(c)};if(useEffect(()=>{p()},[e.assetId]),!e.assetId)return _jsx("div",{style:{padding:"1rem",color:"#9ca3af"},children:"Select an asset from the registry to see its details."});if(!l)return _jsx("div",{style:{padding:"1rem"},children:"Loading…"});const u=s[l.asset_type]?.label??l.asset_type;return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr auto 1fr",gap:"0.5rem 1.5rem",alignItems:"baseline"},children:[_jsx("strong",{children:"Asset ID"})," ",_jsx("span",{children:l.asset_id}),_jsx("strong",{children:"Type"})," ",_jsx("span",{children:u}),_jsx("strong",{children:"Serial"})," ",_jsx("span",{children:l.serial||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Location"})," ",_jsx("span",{children:l.location||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Status"})," ",_jsx("span",{children:l.status}),_jsx("strong",{children:"Installed"}),_jsx("span",{children:l.install_date?new Date(l.install_date).toLocaleString():"—"}),_jsx("strong",{children:"Current Cal"}),_jsx("span",{children:l.current_calibration_id??_jsx("em",{style:{color:"#f59e0b"},children:"none"})}),_jsx("strong",{children:"Cycles"})," ",_jsx("span",{children:c?.cycles??0})]}),_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[_jsx("h4",{style:{margin:0},children:"Calibration History"}),_jsx(Button,{label:"+ Calibration",icon:"pi pi-plus",onClick:()=>x(!0)})]}),_jsxs(DataTable,{value:o,size:"small",stripedRows:!0,emptyMessage:"No calibrations recorded for this asset.",children:[_jsx(Column,{field:"cal_id",header:"Cal ID"}),_jsx(Column,{field:"performed_at",header:"Performed",body:e=>e.performed_at?new Date(e.performed_at).toLocaleString():"—"}),_jsx(Column,{field:"performed_by",header:"By"}),_jsx(Column,{field:"expires_at",header:"Expires",body:e=>e.expires_at?new Date(e.expires_at).toLocaleDateString():"—"}),_jsx(Column,{field:"cert_ref",header:"Cert"}),_jsx(Column,{header:"Values",body:e=>_jsx("code",{style:{fontSize:"0.75rem"},children:JSON.stringify(e.values)})})]}),_jsx(CalibrationEntryDialog,{visible:_,assetId:l.asset_id,assetType:l.asset_type,onHide:()=>x(!1),onAdded:()=>{p()}})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";export const AssetDetailView=()=>{const{selection:e,schemas:s,roles:t,readAsset:a,listCalibrations:r,readCalibration:i,readUsage:l}=useAms(),[n,o]=useState(null),[d,c]=useState([]),[_,m]=useState(null),[p,x]=useState(!1),u=async()=>{if(!e.assetId)return o(null),c([]),void m(null);const s=await a(e.assetId);o(s);const t=await r(e.assetId),n=await Promise.all(t.map(s=>i(e.assetId,s)));c(n.filter(Boolean));const d=await l(e.assetId);m(d)};if(useEffect(()=>{u()},[e.assetId]),!e.assetId)return _jsx("div",{style:{padding:"1rem",color:"#9ca3af"},children:"Select an asset from the registry to see its details."});if(!n)return _jsx("div",{style:{padding:"1rem"},children:"Loading…"});const f=s[n.asset_type]?.label??n.asset_type,j=t[n.asset_type]?.find(e=>e.location===n.location),h=j?j.label??j.location:n.location;return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"1rem"},children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr auto 1fr",gap:"0.5rem 1.5rem",alignItems:"baseline"},children:[_jsx("strong",{children:"Asset ID"})," ",_jsx("span",{children:n.asset_id}),_jsx("strong",{children:"Type"})," ",_jsx("span",{children:f}),_jsx("strong",{children:"Serial"})," ",_jsx("span",{children:n.serial||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Role"})," ",_jsx("span",{children:h||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Status"})," ",_jsx("span",{children:n.status}),_jsx("strong",{children:"Installed"}),_jsx("span",{children:n.install_date?new Date(n.install_date).toLocaleString():"—"}),_jsx("strong",{children:"Current Cal"}),_jsx("span",{children:n.current_calibration_id??_jsx("em",{style:{color:"#f59e0b"},children:"none"})}),_jsx("strong",{children:"Cycles"})," ",_jsx("span",{children:_?.cycles??0})]}),_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[_jsx("h4",{style:{margin:0},children:"Calibration History"}),_jsx(Button,{label:"+ Calibration",icon:"pi pi-plus",onClick:()=>x(!0)})]}),_jsxs(DataTable,{value:d,size:"small",stripedRows:!0,emptyMessage:"No calibrations recorded for this asset.",children:[_jsx(Column,{field:"cal_id",header:"Cal ID"}),_jsx(Column,{field:"performed_at",header:"Performed",body:e=>e.performed_at?new Date(e.performed_at).toLocaleString():"—"}),_jsx(Column,{field:"performed_by",header:"By"}),_jsx(Column,{field:"expires_at",header:"Expires",body:e=>e.expires_at?new Date(e.expires_at).toLocaleDateString():"—"}),_jsx(Column,{field:"cert_ref",header:"Cert"}),_jsx(Column,{header:"Values",body:e=>_jsx("code",{style:{fontSize:"0.75rem"},children:JSON.stringify(e.values)})})]}),_jsx(CalibrationEntryDialog,{visible:p,assetId:n.asset_id,assetType:n.asset_type,onHide:()=>x(!1),onAdded:()=>{u()}})]})};
@@ -1 +1 @@
1
- {"version":3,"file":"AssetRegistryTable.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetRegistryTable.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAoB7D,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EA+ItC,CAAC"}
1
+ {"version":3,"file":"AssetRegistryTable.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetRegistryTable.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAwC,MAAM,OAAO,CAAC;AA+C7D,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAmRtC,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useMemo,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";const EMPTY_ADD={open:!1,assetType:"",serial:"",location:""};export const AssetRegistryTable=()=>{const{schemas:e,assets:t,refreshAssets:s,setSelection:a,selection:l}=useAms(),{invoke:r}=useContext(EventEmitterContext),[o,i]=useState(null),[n,d]=useState(null),[c,p]=useState(EMPTY_ADD),u=useMemo(()=>Object.keys(e).map(t=>({label:e[t]?.label??t,value:t})),[e]),m=useMemo(()=>t.filter(e=>(!o||e.asset_type===o)&&(!n||e.status===n)),[t,o,n]);return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{style:{display:"flex",gap:"0.75rem",alignItems:"center"},children:[_jsx(Dropdown,{value:o,options:[{label:"All Types",value:null},...u],onChange:e=>i(e.value),placeholder:"Filter by type"}),_jsx(Dropdown,{value:n,options:[{label:"All Statuses",value:null},{label:"Active",value:"active"},{label:"Out for Service",value:"out_for_service"},{label:"Retired",value:"retired"}],onChange:e=>d(e.value),placeholder:"Filter by status"}),_jsx("span",{style:{marginLeft:"auto"},children:_jsx(Button,{icon:"pi pi-plus",label:"Add Asset",onClick:()=>p(e=>({...e,open:!0})),disabled:0===u.length})})]}),_jsxs(DataTable,{value:m,selectionMode:"single",selection:m.find(e=>e.asset_id===l.assetId)??null,onSelectionChange:e=>{const t=e.value;t&&a({assetType:t.asset_type,assetId:t.asset_id})},dataKey:"asset_id",emptyMessage:0===u.length?"AMS not enabled in this project (no asset_types declared).":"No assets registered yet.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"asset_id",header:"Asset ID"}),_jsx(Column,{field:"asset_type",header:"Type",body:t=>e[t.asset_type]?.label??t.asset_type}),_jsx(Column,{field:"serial",header:"Serial"}),_jsx(Column,{field:"location",header:"Location"}),_jsx(Column,{field:"status",header:"Status"}),_jsx(Column,{header:"Calibration",body:e=>e.current_calibration_id?_jsx("span",{title:e.current_calibration_id,children:"✓"}):_jsx("span",{style:{color:"#f59e0b"},children:"none"})})]}),_jsxs(Dialog,{header:"Add New Asset",visible:c.open,style:{width:"32rem"},onHide:()=>p(EMPTY_ADD),footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:()=>p(EMPTY_ADD)}),_jsx(Button,{label:"Create",icon:"pi pi-check",onClick:async()=>{if(c.assetType)try{const e=await r("ams.create_asset",MessageType.Request,{asset_type:c.assetType,serial:c.serial,location:c.location});e?.success&&(p(EMPTY_ADD),await s(),e.data?.asset_id&&a({assetType:c.assetType,assetId:e.data.asset_id}))}catch(e){}},disabled:!c.assetType})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Type *"}),_jsx(Dropdown,{value:c.assetType,options:u,onChange:e=>p(t=>({...t,assetType:e.value})),placeholder:"Choose asset type"}),_jsx("label",{children:"Serial"}),_jsx(InputText,{value:c.serial,onChange:e=>p(t=>({...t,serial:e.target.value}))}),_jsx("label",{children:"Location"}),_jsx(InputText,{value:c.location,onChange:e=>p(t=>({...t,location:e.target.value}))})]}),_jsxs("p",{style:{fontSize:"0.875rem",color:"#9ca3af",marginTop:"1rem"},children:["The asset_id is generated by the server (format: ",_jsxs("code",{children:[c.assetType?e[c.assetType]?.id_prefix??"A-":"A-","YYYYMMDDTHHMMSS"]}),"). Manufacturer serial is recorded for traceability but is not used as the unique key."]})]})]})};
1
+ import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useContext,useMemo,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Dropdown}from"primereact/dropdown";import{InputText}from"primereact/inputtext";import{Dialog}from"primereact/dialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";const ROLE_OTHER="__other__",EMPTY_ADD={open:!1,assetType:"",serial:"",roleSelection:"",location:""};function roleLabel(e){return e.label??e.location}function roleUsageSummary(e){return 0===e.used_by.length?"No test methods reference this role.":1===e.used_by.length?`Used by: ${e.used_by[0]}`:`Used by: ${e.used_by.join(", ")}`}export const AssetRegistryTable=()=>{const{schemas:e,roles:t,assets:s,refreshAssets:l,setSelection:a,selection:o}=useAms(),{invoke:r}=useContext(EventEmitterContext),[n,i]=useState(null),[c,d]=useState(null),[p,u]=useState(EMPTY_ADD),m=useMemo(()=>Object.keys(e).map(t=>({label:e[t]?.label??t,value:t})),[e]),_=useMemo(()=>p.assetType?t[p.assetType]??[]:[],[p.assetType,t]),h=_.length>0,y=useMemo(()=>{const e=_.map(e=>({label:roleLabel(e),value:e.location}));return e.push({label:"Other (advanced — type a custom role)",value:ROLE_OTHER}),e},[_]),x=useMemo(()=>p.roleSelection===ROLE_OTHER?null:_.find(e=>e.location===p.roleSelection)??null,[p.roleSelection,_]),b=useMemo(()=>s.filter(e=>(!n||e.asset_type===n)&&(!c||e.status===c)),[s,n,c]),g=!p.assetType||h&&(""===p.roleSelection||p.roleSelection===ROLE_OTHER&&!p.location.trim());return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{style:{display:"flex",gap:"0.75rem",alignItems:"center"},children:[_jsx(Dropdown,{value:n,options:[{label:"All Types",value:null},...m],onChange:e=>i(e.value),placeholder:"Filter by type"}),_jsx(Dropdown,{value:c,options:[{label:"All Statuses",value:null},{label:"Active",value:"active"},{label:"Out for Service",value:"out_for_service"},{label:"Retired",value:"retired"}],onChange:e=>d(e.value),placeholder:"Filter by status"}),_jsx("span",{style:{marginLeft:"auto"},children:_jsx(Button,{icon:"pi pi-plus",label:"Add Asset",onClick:()=>u(e=>({...e,open:!0})),disabled:0===m.length})})]}),_jsxs(DataTable,{value:b,selectionMode:"single",selection:b.find(e=>e.asset_id===o.assetId)??null,onSelectionChange:e=>{const t=e.value;t&&a({assetType:t.asset_type,assetId:t.asset_id})},dataKey:"asset_id",emptyMessage:0===m.length?"AMS not enabled in this project (no asset_types declared).":"No assets registered yet.",size:"small",stripedRows:!0,children:[_jsx(Column,{field:"asset_id",header:"Asset ID"}),_jsx(Column,{field:"asset_type",header:"Type",body:t=>e[t.asset_type]?.label??t.asset_type}),_jsx(Column,{field:"serial",header:"Serial"}),_jsx(Column,{field:"location",header:"Role",body:e=>e.location?((e,s)=>{const l=(t[e]??[]).find(e=>e.location===s);return l?roleLabel(l):s})(e.asset_type,e.location):_jsx("span",{style:{color:"#9ca3af"},children:"—"})}),_jsx(Column,{field:"status",header:"Status"}),_jsx(Column,{header:"Calibration",body:e=>e.current_calibration_id?_jsx("span",{title:e.current_calibration_id,children:"✓"}):_jsx("span",{style:{color:"#f59e0b"},children:"none"})})]}),_jsxs(Dialog,{header:"Add New Asset",visible:p.open,style:{width:"32rem"},onHide:()=>u(EMPTY_ADD),footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",severity:"secondary",onClick:()=>u(EMPTY_ADD)}),_jsx(Button,{label:"Create",icon:"pi pi-check",onClick:async()=>{if(p.assetType)try{const e=await r("ams.create_asset",MessageType.Request,{asset_type:p.assetType,serial:p.serial,location:p.location});e?.success&&(u(EMPTY_ADD),await l(),e.data?.asset_id&&a({assetType:p.assetType,assetId:e.data.asset_id}))}catch(e){}},disabled:g})]}),children:[_jsxs("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.5rem 1rem",alignItems:"center"},children:[_jsx("label",{children:"Type *"}),_jsx(Dropdown,{value:p.assetType,options:m,onChange:e=>(e=>{const s=t[e]??[];1===s.length?u(t=>({...t,assetType:e,roleSelection:s[0].location,location:s[0].location})):u(t=>({...t,assetType:e,roleSelection:"",location:""}))})(e.value),placeholder:"Choose asset type"}),_jsx("label",{children:"Serial"}),_jsx(InputText,{value:p.serial,onChange:e=>u(t=>({...t,serial:e.target.value}))}),h&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Role *"}),_jsx(Dropdown,{value:p.roleSelection,options:y,onChange:e=>{return t=e.value,void u(t===ROLE_OTHER?e=>({...e,roleSelection:ROLE_OTHER,location:""}):e=>({...e,roleSelection:t,location:t}));var t},placeholder:"Choose where this asset is mounted"}),p.roleSelection===ROLE_OTHER&&_jsxs(_Fragment,{children:[_jsx("label",{children:"Custom role"}),_jsx(InputText,{value:p.location,placeholder:"e.g. tsdr_secondary",onChange:e=>u(t=>({...t,location:e.target.value}))})]})]})]}),_jsxs("div",{style:{fontSize:"0.875rem",color:"#9ca3af",marginTop:"1rem"},children:[h&&x&&_jsxs(_Fragment,{children:[x.description&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:x.description}),_jsxs("p",{style:{margin:"0 0 0.5rem 0",color:"#34d399"},children:["✓ ",roleUsageSummary(x)]})]}),h&&p.roleSelection===ROLE_OTHER&&_jsx("p",{style:{margin:"0 0 0.5rem 0",color:"#f59e0b"},children:"⚠ No test method on this machine references this custom role yet. The asset will be registered but won't be picked up at start_test unless you add an asset_ref to a test method's project.json."}),!h&&p.assetType&&_jsx("p",{style:{margin:"0 0 0.5rem 0"},children:"This asset type isn't selected by physical role on this machine — test methods reference it by ID instead. Just register the asset and pick it by ID on the Test Setup form when running a test."}),_jsxs("p",{style:{margin:0},children:["The asset_id is generated by the server (format:"," ",_jsxs("code",{children:[p.assetType?e[p.assetType]?.id_prefix??"A-":"A-","YYYYMMDDTHHMMSS"]}),"). Manufacturer serial is recorded for traceability but is not used as the unique key."]})]})]})]})};
@@ -1,4 +1,5 @@
1
- export { AmsProvider, useAms, useAmsSchemas, useAmsAlerts, useAmsAssets, useAmsSelection } from './AmsProvider';
1
+ export { AmsProvider, useAms, useAmsSchemas, useAmsRoles, useAmsAlerts, useAmsAssets, useAmsSelection } from './AmsProvider';
2
+ export type { AmsRole, AmsRoleRegistry } from './AmsProvider';
2
3
  export { AssetRegistryTable } from './AssetRegistryTable';
3
4
  export { AssetDetailView } from './AssetDetailView';
4
5
  export { CalibrationEntryDialog } from './CalibrationEntryDialog';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ams/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC7H,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1 +1 @@
1
- export{AmsProvider,useAms,useAmsSchemas,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{SubLocationPicker}from"./SubLocationPicker";
1
+ export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{SubLocationPicker}from"./SubLocationPicker";
@@ -1,4 +1,17 @@
1
1
  import React from 'react';
2
+ /**
3
+ * One asset_ref declared on a test method. We only consume the subset
4
+ * the form cares about — `field`, `asset_type`, `select`, `from`,
5
+ * `location`. Other fields (calibration_required, label, description)
6
+ * exist on the wire but aren't form-relevant.
7
+ */
8
+ interface TisAssetRef {
9
+ field: string;
10
+ asset_type: string;
11
+ select: 'by_location' | 'by_id_field';
12
+ from?: string;
13
+ location?: string;
14
+ }
2
15
  export interface TestFieldDef {
3
16
  /** Canonical key — wire format, generated code, on-disk JSON. */
4
17
  name: string;
@@ -17,12 +30,30 @@ export interface TestMethod {
17
30
  config_fields: TestFieldDef[];
18
31
  cycle_fields: TestFieldDef[];
19
32
  results_fields: TestFieldDef[];
33
+ /**
34
+ * AMS asset references resolved at start_test. The form scans these
35
+ * for `select=by_id_field` entries pointing at a config field
36
+ * (`from: "config.<name>"`) and renders that field as a Dropdown of
37
+ * matching AMS assets instead of a free-form text input. No
38
+ * project.json change required — the form derives this from the
39
+ * existing schema.
40
+ */
41
+ asset_refs?: TisAssetRef[];
20
42
  /** Optional pretty label for the Test Method picker. Falls back
21
43
  * to the canonical method_id key. */
22
44
  label?: string;
23
45
  /** Optional long-form description shown in the picker dialog
24
46
  * when this method is highlighted. */
25
47
  description?: string;
48
+ /** Optional post-cycle analysis dispatch (`{ script, function }`).
49
+ * Consumed server-side by the codegen; the form doesn't render
50
+ * anything based on it but accepts it so generated schema
51
+ * literals from `acctl codegen-tags` typecheck cleanly. */
52
+ analysis?: any;
53
+ /** Free-form view declarations for chart components. Same rationale
54
+ * as `analysis` — the form ignores them; the type just has to
55
+ * accept them so generated schemas typecheck. */
56
+ views?: Record<string, any>;
26
57
  }
27
58
  /**
28
59
  * Test-setup form. Renders Sample ID, Test Method picker, and Test
@@ -41,4 +72,5 @@ export interface TestSetupFormProps {
41
72
  onValidationChange?: (isValid: boolean, config: any) => void;
42
73
  }
43
74
  export declare const TestSetupForm: React.FC<TestSetupFormProps>;
75
+ export {};
44
76
  //# sourceMappingURL=TestSetupForm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAYxE,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAmBD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmStD,CAAC"}
1
+ {"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAcxE;;;;;GAKG;AACH,UAAU,WAAW;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,GAAG,aAAa,CAAC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IACzB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B;0CACsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;gEAG4D;IAC5D,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf;;sDAEkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAyED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqUtD,CAAC"}
@@ -1 +1 @@
1
- import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e;export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:a})=>{const i=useTis(),{invoke:o,write:n}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(i.schemas),[i.schemas]),d=i.selection.projectId,m=""!==d.trim()&&i.projectKnown(d.trim()),[p,u]=useState(i.selection.methodId||t||i.defaultMethodId||""),[f,h]=useState(""),[x,g]=useState({}),j=e??(p?i.schemas[p]:void 0);useEffect(()=>{i.selection.methodId!==p&&p&&i.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{i.selection.sampleId!==f&&i.setSelection({sampleId:f})},[f]),useEffect(()=>{i.state.stagedSampleId&&i.state.stagedSampleId!==f&&h(i.state.stagedSampleId)},[i.state.stagedSampleId]),useEffect(()=>{i.selection.methodId&&i.selection.methodId!==p&&u(i.selection.methodId)},[i.selection.methodId]);const[_,v]=useState(!1),[y,I]=useState(!1);useEffect(()=>{j&&g(e=>{let t=e;for(const s of j.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const a=l(s.source);if(!a)continue;const i=r[a.tagName];null!=i&&(t[s.name]!==i&&(t===e&&(t={...e}),t[s.name]=i))}return t})},[j,r,l]),useEffect(()=>{if(!j)return void v(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),f.trim()||(e=!1),e&&!i.projectFieldsLoaded&&(e=!1);for(const t of j.config_fields)if("sample_id"!==t.name&&t.required){const s=x[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(v(e),a&&a(e,x),e){const{sample_id:e,...t}=x??{},s={...i.projectFields,...t};o("tis.stage_test",MessageType.Request,{project_id:d,method_id:p,sample_id:f,config:s}).catch(e=>{})}},[x,j,d,p,f,m,i.projectFields,i.projectFieldsLoaded,a,o]);const T=async(e,t)=>{if(g({...x,[e.name]:t}),e.source)try{await n(e.source,t)}catch(e){}};if(!j)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:i.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});if(!m)return _jsxs("div",{style:{padding:"1.25rem",maxWidth:"600px"},children:[_jsx("h3",{className:"ac-form-section",children:"No project selected"}),_jsxs("p",{style:{color:"var(--text-secondary-color)",marginTop:"0.5rem"},children:["Pick a project on the ",_jsx("strong",{children:"Project"})," tab first",""!==d.trim()&&` (or click + there to create "${d.trim()}")`,"."]})]});return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx("span",{style:{color:_?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:_?"pi pi-check-circle":"pi pi-exclamation-circle"})}),_jsxs("span",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)",fontWeight:"normal",marginLeft:"0.25rem"},children:["project: ",_jsx("strong",{children:d})]})]}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:f,onValueChanged:e=>{h(e)},className:f.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:f.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:f.trim()?"pi pi-check":"pi pi-times"})}),c.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(p,j),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>I(!0),tooltip:c.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:p?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:p?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),j.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=x[e.name];return void 0!==t&&""!==t&&null!==t})(e),s="string"!==e.type&&"bool"!==e.type,a=`acFormInfo_${e.name}`;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),s?_jsx(ValueInput,{label:void 0,value:null!=x[e.name]?Number(x[e.name]):null,onValueChanged:t=>T(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=x[e.name]?String(x[e.name]):"",onValueChanged:t=>T(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsxs(_Fragment,{children:[_jsx(Tooltip,{target:`#${a}`,position:"left"}),_jsx("span",{id:a,"data-pr-tooltip":e.description,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",cursor:"help"},children:_jsx("i",{className:"pi pi-info-circle",style:{color:"var(--text-secondary-color)"}})})]}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(TestMethodDialog,{visible:y,onHide:()=>I(!1),currentMethodId:p,onSelected:e=>u(e)})]})};
1
+ import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext,useMemo}from"react";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{Dropdown}from"primereact/dropdown";import{Tooltip}from"primereact/tooltip";import{EventEmitterContext}from"../../core/EventEmitterContext";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{MessageType}from"../../hub/CommandMessage";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";import{useTis}from"./TisProvider";import{useAmsAssets,useAmsRoles}from"../ams/AmsProvider";import{TestMethodDialog}from"./TestMethodDialog";const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t},hasDescription=e=>"string"==typeof e.description&&e.description.length>0,methodLabelOf=(e,t)=>t?.label&&t.label.length>0?t.label:e,AssetIdPicker=({assetType:e,value:t,onChange:s,invalid:a})=>{const i=useAmsAssets(),o=useAmsRoles(),n=useMemo(()=>i.filter(t=>t.asset_type===e&&"active"===t.status).map(e=>{const t=o[e.asset_type]?.find(t=>t.location===e.location)?.label,s=[e.asset_id];return t?s.push(`— ${t}`):e.location&&s.push(`— ${e.location}`),e.serial&&s.push(`(s/n ${e.serial})`),{label:s.join(" "),value:e.asset_id}}),[i,o,e]);return _jsx(Dropdown,{value:t,options:n,onChange:e=>s(e.value??""),placeholder:0===n.length?`No active ${e} assets registered — add one in Settings → Assets`:`Select ${e}…`,className:a?"p-invalid":"",filter:!0,showClear:!0,disabled:0===n.length})};export const TestSetupForm=({schema:e,defaultMethodId:t,onMethodChange:s,onValidationChange:a})=>{const i=useTis(),{invoke:o,write:n}=useContext(EventEmitterContext),{rawValues:r,findTagByFqdn:l}=useContext(AutoCoreTagContext),c=useMemo(()=>Object.keys(i.schemas),[i.schemas]),d=i.selection.projectId,m=""!==d.trim()&&i.projectKnown(d.trim()),[p,u]=useState(i.selection.methodId||t||i.defaultMethodId||""),[f,h]=useState(i.selection.sampleId||""),g=i.stagedConfig,x=e=>{const t="function"==typeof e?e(i.stagedConfig):e;t!==i.stagedConfig&&i.setStagedConfig(t)},_=e??(p?i.schemas[p]:void 0);useEffect(()=>{i.selection.methodId!==p&&p&&i.setSelection({methodId:p}),s&&s(p)},[p]),useEffect(()=>{i.selection.sampleId!==f&&i.setSelection({sampleId:f})},[f]),useEffect(()=>{i.state.stagedSampleId&&i.state.stagedSampleId!==f&&h(i.state.stagedSampleId)},[i.state.stagedSampleId]),useEffect(()=>{i.selection.methodId&&i.selection.methodId!==p&&u(i.selection.methodId)},[i.selection.methodId]);const[j,v]=useState(!1),[y,I]=useState(!1);useEffect(()=>{_&&x(e=>{let t=e;for(const s of _.config_fields){if("sample_id"===s.name)continue;if(!s.source)continue;const a=l(s.source);if(!a)continue;const i=r[a.tagName];null!=i&&(t[s.name]!==i&&(t===e&&(t={...e}),t[s.name]=i))}return t})},[_,r,l]),useEffect(()=>{if(!_)return void v(!1);let e=!0;m||(e=!1),p.trim()||(e=!1),f.trim()||(e=!1),e&&!i.projectFieldsLoaded&&(e=!1);for(const t of _.config_fields)if("sample_id"!==t.name&&t.required){const s=g[t.name];if(void 0===s||""===s||null===s){e=!1;break}}if(v(e),a&&a(e,g),e){const{sample_id:e,...t}=g??{},s={...i.projectFields,...t};o("tis.stage_test",MessageType.Request,{project_id:d,method_id:p,sample_id:f,config:s}).catch(e=>{})}},[g,_,d,p,f,m,i.projectFields,i.projectFieldsLoaded,a,o]);const C=async(e,t)=>{if(x({...g,[e.name]:t}),e.source)try{await n(e.source,t)}catch(e){}};if(!_)return _jsx("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:_jsx("h3",{className:"ac-form-section",children:i.schemasLoaded?"No Test Method Selected":"Loading test methods…"})});if(!m)return _jsxs("div",{style:{padding:"1.25rem",maxWidth:"600px"},children:[_jsx("h3",{className:"ac-form-section",children:"No project selected"}),_jsxs("p",{style:{color:"var(--text-secondary-color)",marginTop:"0.5rem"},children:["Pick a project on the ",_jsx("strong",{children:"Project"})," tab first",""!==d.trim()&&` (or click + there to create "${d.trim()}")`,"."]})]});return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem",gridTemplateColumns:"auto 1fr 1.75rem 1.75rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Test Setup",_jsx("span",{style:{color:j?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:j?"pi pi-check-circle":"pi pi-exclamation-circle"})}),_jsxs("span",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)",fontWeight:"normal",marginLeft:"0.25rem"},children:["project: ",_jsx("strong",{children:d})]})]}),_jsx("span",{className:"ac-form-label",children:"Sample ID"}),_jsx(TextInput,{label:void 0,value:f,onValueChanged:e=>{h(e)},className:f.trim()?"":"p-invalid"}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:f.trim()?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:f.trim()?"pi pi-check":"pi pi-times"})}),c.length>0&&_jsxs(_Fragment,{children:[_jsx("span",{className:"ac-form-label",children:"Test Method"}),_jsxs("div",{className:"p-inputgroup",style:{flex:1},children:[_jsx(InputText,{value:methodLabelOf(p,_),readOnly:!0,style:{flex:1},tabIndex:-1}),_jsx(Button,{icon:"pi pi-pencil",type:"button",onClick:()=>I(!0),tooltip:c.length>1?"Change test method":"View test method details",tooltipOptions:{position:"top"}})]}),_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:p?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:p?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),_.config_fields.map(e=>{if("sample_id"===e.name)return null;const t=(e=>{if(!e.required)return!0;const t=g[e.name];return void 0!==t&&""!==t&&null!==t})(e),s=`acFormInfo_${e.name}`,a=(e=>{const t=_?.asset_refs??[],s=`config.${e.name}`,a=t.find(e=>"by_id_field"===e.select&&e.from===s);return a?a.asset_type:null})(e),i=!a&&"string"!==e.type&&"bool"!==e.type;return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(e)}),a?_jsx(AssetIdPicker,{assetType:a,value:null!=g[e.name]?String(g[e.name]):"",onChange:t=>C(e,t),invalid:!t}):i?_jsx(ValueInput,{label:void 0,value:null!=g[e.name]?Number(g[e.name]):null,onValueChanged:t=>C(e,t),className:t?"":"p-invalid"}):_jsx(TextInput,{label:void 0,value:null!=g[e.name]?String(g[e.name]):"",onValueChanged:t=>C(e,t),className:t?"":"p-invalid"}),hasDescription(e)?_jsxs(_Fragment,{children:[_jsx(Tooltip,{target:`#${s}`,position:"left"}),_jsx("span",{id:s,"data-pr-tooltip":e.description,style:{display:"inline-flex",alignItems:"center",justifyContent:"center",cursor:"help"},children:_jsx("i",{className:"pi pi-info-circle",style:{color:"var(--text-secondary-color)"}})})]}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:t?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:t?"pi pi-check":"pi pi-times"})})]},e.name)}),_jsx(TestMethodDialog,{visible:y,onHide:()=>I(!1),currentMethodId:p,onSelected:e=>u(e)})]})};
@@ -77,6 +77,21 @@ export interface TisContextValue {
77
77
  /** Stash freshly-known project_fields without a round trip — used
78
78
  * by the create / edit dialogs after a successful submit. */
79
79
  setProjectFields: (id: string, fields: Record<string, any>) => void;
80
+ /**
81
+ * In-progress test draft — the operator's pending Sample ID and
82
+ * config_field values for the active method. Held on the provider
83
+ * (not in `<TestSetupForm>`'s local React state) so the values
84
+ * survive remount when the operator switches tabs. Wiped via
85
+ * `clearStagedConfig()` after a successful start_test or when the
86
+ * operator cancels the staged record.
87
+ */
88
+ stagedConfig: Record<string, any>;
89
+ /** Merge a partial patch into `stagedConfig`. Existing fields not
90
+ * mentioned in the patch are preserved. */
91
+ setStagedConfig: (patch: Record<string, any>) => void;
92
+ /** Reset `stagedConfig` to `{}`. Called by the form on a fresh
93
+ * method selection or after a run completes. */
94
+ clearStagedConfig: () => void;
80
95
  /** Fetch the run list for a (project, method?) pair. Method may be
81
96
  * omitted to aggregate runs across every method in the project —
82
97
  * the History tab uses this. */
@@ -1 +1 @@
1
- {"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TisProvider.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TisProvider.tsx"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useReducer,useRef,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_STATE={staged:!1,stagedProjectId:"",stagedMethodId:"",stagedSampleId:"",active:!1,activeProjectId:"",activeMethodId:"",activeSampleId:"",activeRunId:""},EMPTY_SELECTION={projectId:"",methodId:"",sampleId:"",runId:""},TisContext=createContext({schemas:{},defaultMethodId:"",schemasLoaded:!1,state:EMPTY_STATE,selection:EMPTY_SELECTION,setSelection:()=>{},existingProjects:[],projectKnown:()=>!1,refreshProjects:async()=>{},markProjectJustCreated:()=>{},projectFields:{},projectFieldsLoaded:!1,loadProjectFields:async()=>null,setProjectFields:()=>{},fetchRuns:async()=>[],fetchRun:async()=>null,runCache:{}});function liveReducer(e,t){switch(t.kind){case"staged":return{...e,staged:t.value};case"staged_project_id":return{...e,stagedProjectId:t.value};case"staged_method_id":return{...e,stagedMethodId:t.value};case"staged_sample_id":return{...e,stagedSampleId:t.value};case"active":return{...e,active:t.value};case"active_project_id":return{...e,activeProjectId:t.value};case"active_method_id":return{...e,activeMethodId:t.value};case"active_sample_id":return{...e,activeSampleId:t.value};case"active_run_id":return{...e,activeRunId:t.value}}}export const TisProvider=({children:e,defaultMethodId:t})=>{const{invoke:s,subscribe:a,unsubscribe:c}=useContext(EventEmitterContext),[r,d]=useState({}),[u,n]=useState(t??""),[i,o]=useState(!1),[l,p]=useReducer(liveReducer,EMPTY_STATE),[_,m]=useState({projectId:null,methodId:null,sampleId:null,runId:null});useEffect(()=>{let e=!1;return(async()=>{try{const a=await s("tis.list_schemas",MessageType.Request,{});if(e)return;if(a?.success&&a.data){const e=a.data.test_methods??{},s=a.data.default_method_id??"";d(e),!t&&s&&n(s),o(!0)}}catch(e){}})(),()=>{e=!0}},[]),useEffect(()=>{const e=[a("tis.staged",e=>p({kind:"staged",value:!!e})),a("tis.staged_project_id",e=>p({kind:"staged_project_id",value:String(e??"")})),a("tis.staged_method_id",e=>p({kind:"staged_method_id",value:String(e??"")})),a("tis.staged_sample_id",e=>p({kind:"staged_sample_id",value:String(e??"")})),a("tis.active",e=>p({kind:"active",value:!!e})),a("tis.active_project_id",e=>p({kind:"active_project_id",value:String(e??"")})),a("tis.active_method_id",e=>p({kind:"active_method_id",value:String(e??"")})),a("tis.active_sample_id",e=>p({kind:"active_sample_id",value:String(e??"")})),a("tis.active_run_id",e=>p({kind:"active_run_id",value:String(e??"")}))];return()=>{e.forEach(c)}},[a,c]);const I=useRef({}),[v,f]=useState(0),h=useCallback(()=>f(e=>e+1),[]),j=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,cycles:[...s.cycles,t]},h()},[h]),C=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,results:t},h()},[h]);useEffect(()=>{const e=a("tis.cycle_added",e=>{e?.run_id&&e.cycle&&j(e.run_id,e.cycle)}),t=a("tis.results_updated",e=>{e?.run_id&&C(e.run_id,e.results??{})});return()=>{c(e),c(t)}},[a,c,j,C]);const g=useMemo(()=>({projectId:_.projectId??l.activeProjectId,methodId:_.methodId??(l.activeMethodId||u),sampleId:_.sampleId??l.activeSampleId,runId:_.runId??l.activeRunId}),[_,l,u]),y=useCallback(e=>{m(t=>({projectId:void 0===e.projectId?t.projectId:e.projectId,methodId:void 0===e.methodId?t.methodId:e.methodId,sampleId:void 0===e.sampleId?t.sampleId:e.sampleId,runId:void 0===e.runId?t.runId:e.runId}))},[]),x=useRef("");useEffect(()=>{const e=l.activeRunId;e&&e!==x.current&&(x.current=e,m({projectId:null,methodId:null,sampleId:null,runId:null}))},[l.activeRunId]);const S=useCallback(async(e,t)=>{if(!e)return[];const a={project_id:e};t&&(a.method_id=t);try{const e=await s("tis.list_tests",MessageType.Request,a);if(e?.success&&e.data?.tests)return e.data.tests}catch(e){}return[]},[s]),T=useCallback(async(e,t,a)=>{if(!e||!t||!a)return null;try{const c=await s("tis.read_test",MessageType.Request,{project_id:e,method_id:t,run_id:a}),r=await s("tis.read_cycles",MessageType.Request,{project_id:e,method_id:t,run_id:a,offset:0,limit:1e3,order:"asc"});if(!c?.success)return null;const d={meta:c.data??null,cycles:r?.success?r.data?.cycles??[]:[],results:c.data?.results??{},rawData:I.current[a]?.rawData??{}};return I.current[a]=d,h(),d}catch(e){return null}},[s,h]),E=useMemo(()=>({...I.current}),[v]),[R,k]=useState([]),M=useRef(new Set),[P,b]=useState(0),[w,F]=useState({}),q=useCallback(async()=>{try{const e=await s("tis.list_projects",MessageType.Request,{});e?.success&&e.data?.projects&&k(e.data.projects)}catch(e){}},[s]),D=useCallback(e=>{e&&(M.current.has(e)||(M.current.add(e),b(e=>e+1)))},[]),L=useCallback(e=>!!e&&(!!M.current.has(e)||R.includes(e)),[R,P]),Y=useCallback((e,t)=>{e&&F(s=>({...s,[e]:t}))},[]),A=useCallback(async e=>{if(!e)return null;try{const t=await s("tis.read_project",MessageType.Request,{project_id:e});if(t?.success){const s=t.data?.project_fields??{};return F(t=>({...t,[e]:s})),s}}catch(e){}return null},[s]);useEffect(()=>{q()},[q]),useEffect(()=>{const e=a("tis.project_created",()=>{q()}),t=a("tis.project_updated",e=>{const t="string"==typeof e?.project_id?e.project_id:"";t&&A(t)});return()=>{c(e),c(t)}},[a,c,q,A]),useEffect(()=>{const e=g.projectId;e&&L(e)&&void 0===w[e]&&A(e)},[g.projectId,L,w,A]);const J=w[g.projectId]??{},K=void 0!==w[g.projectId],N=useMemo(()=>({schemas:r,defaultMethodId:u,schemasLoaded:i,state:l,selection:g,setSelection:y,existingProjects:R,projectKnown:L,refreshProjects:q,markProjectJustCreated:D,projectFields:J,projectFieldsLoaded:K,loadProjectFields:A,setProjectFields:Y,fetchRuns:S,fetchRun:T,runCache:E}),[r,u,i,l,g,y,R,L,q,D,J,K,A,Y,S,T,E]);return _jsx(TisContext.Provider,{value:N,children:e})};export const useTis=()=>useContext(TisContext);export const useTisSchemas=()=>useContext(TisContext).schemas;export const useTisState=()=>useContext(TisContext).state;export const useTisSelection=()=>{const{selection:e,setSelection:t}=useContext(TisContext);return[e,t]};export const useTisRuns=(e,t)=>{const{fetchRuns:s}=useContext(TisContext),[a,c]=useState([]),[r,d]=useState(!1),u=useCallback(async()=>{if(e){d(!0);try{c(await s(e,t))}finally{d(!1)}}else c([])},[e,t,s]);return useEffect(()=>{u()},[u]),{runs:a,loading:r,refresh:u}};export const useTisRun=e=>{const{selection:t,fetchRun:s,runCache:a}=useContext(TisContext),[c,r]=useState(!1),d=e??t.runId;useEffect(()=>{if(!d)return;if(a[d]?.meta)return;const e=t.projectId,c=t.methodId;e&&c&&(r(!0),s(e,c,d).finally(()=>r(!1)))},[d,t.projectId,t.methodId,s,a]);const u=d?a[d]:null;return{meta:u?.meta??null,cycles:u?.cycles??[],results:u?.results??{},rawData:u?.rawData??{},loading:c}};export{TisContext};
1
+ import{jsx as _jsx}from"react/jsx-runtime";import React,{createContext,useCallback,useContext,useEffect,useMemo,useReducer,useRef,useState}from"react";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";const EMPTY_STATE={staged:!1,stagedProjectId:"",stagedMethodId:"",stagedSampleId:"",active:!1,activeProjectId:"",activeMethodId:"",activeSampleId:"",activeRunId:""},EMPTY_SELECTION={projectId:"",methodId:"",sampleId:"",runId:""},TisContext=createContext({schemas:{},defaultMethodId:"",schemasLoaded:!1,state:EMPTY_STATE,selection:EMPTY_SELECTION,setSelection:()=>{},existingProjects:[],projectKnown:()=>!1,refreshProjects:async()=>{},markProjectJustCreated:()=>{},projectFields:{},projectFieldsLoaded:!1,loadProjectFields:async()=>null,setProjectFields:()=>{},stagedConfig:{},setStagedConfig:()=>{},clearStagedConfig:()=>{},fetchRuns:async()=>[],fetchRun:async()=>null,runCache:{}});function liveReducer(e,t){switch(t.kind){case"staged":return{...e,staged:t.value};case"staged_project_id":return{...e,stagedProjectId:t.value};case"staged_method_id":return{...e,stagedMethodId:t.value};case"staged_sample_id":return{...e,stagedSampleId:t.value};case"active":return{...e,active:t.value};case"active_project_id":return{...e,activeProjectId:t.value};case"active_method_id":return{...e,activeMethodId:t.value};case"active_sample_id":return{...e,activeSampleId:t.value};case"active_run_id":return{...e,activeRunId:t.value}}}export const TisProvider=({children:e,defaultMethodId:t})=>{const{invoke:s,subscribe:a,unsubscribe:c}=useContext(EventEmitterContext),[r,d]=useState({}),[n,u]=useState(t??""),[i,o]=useState(!1),[l,p]=useReducer(liveReducer,EMPTY_STATE),[_,m]=useState({projectId:null,methodId:null,sampleId:null,runId:null});useEffect(()=>{let e=!1;return(async()=>{try{const a=await s("tis.list_schemas",MessageType.Request,{});if(e)return;if(a?.success&&a.data){const e=a.data.test_methods??{},s=a.data.default_method_id??"";d(e),!t&&s&&u(s),o(!0)}}catch(e){}})(),()=>{e=!0}},[]),useEffect(()=>{const e=[a("tis.staged",e=>p({kind:"staged",value:!!e})),a("tis.staged_project_id",e=>p({kind:"staged_project_id",value:String(e??"")})),a("tis.staged_method_id",e=>p({kind:"staged_method_id",value:String(e??"")})),a("tis.staged_sample_id",e=>p({kind:"staged_sample_id",value:String(e??"")})),a("tis.active",e=>p({kind:"active",value:!!e})),a("tis.active_project_id",e=>p({kind:"active_project_id",value:String(e??"")})),a("tis.active_method_id",e=>p({kind:"active_method_id",value:String(e??"")})),a("tis.active_sample_id",e=>p({kind:"active_sample_id",value:String(e??"")})),a("tis.active_run_id",e=>p({kind:"active_run_id",value:String(e??"")}))];return()=>{e.forEach(c)}},[a,c]);const I=useRef({}),[f,v]=useState(0),h=useCallback(()=>v(e=>e+1),[]),j=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,cycles:[...s.cycles,t]},h()},[h]),g=useCallback((e,t)=>{if(!e)return;const s=I.current[e]??{meta:null,cycles:[],results:{},rawData:{}};I.current[e]={...s,results:t},h()},[h]);useEffect(()=>{const e=a("tis.cycle_added",e=>{e?.run_id&&e.cycle&&j(e.run_id,e.cycle)}),t=a("tis.results_updated",e=>{e?.run_id&&g(e.run_id,e.results??{})});return()=>{c(e),c(t)}},[a,c,j,g]);const C=useMemo(()=>({projectId:_.projectId??l.activeProjectId,methodId:_.methodId??(l.activeMethodId||n),sampleId:_.sampleId??l.activeSampleId,runId:_.runId??l.activeRunId}),[_,l,n]),S=useCallback(e=>{m(t=>({projectId:void 0===e.projectId?t.projectId:e.projectId,methodId:void 0===e.methodId?t.methodId:e.methodId,sampleId:void 0===e.sampleId?t.sampleId:e.sampleId,runId:void 0===e.runId?t.runId:e.runId}))},[]),y=useRef("");useEffect(()=>{const e=l.activeRunId;e&&e!==y.current&&(y.current=e,m({projectId:null,methodId:null,sampleId:null,runId:null}))},[l.activeRunId]);const x=useCallback(async(e,t)=>{if(!e)return[];const a={project_id:e};t&&(a.method_id=t);try{const e=await s("tis.list_tests",MessageType.Request,a);if(e?.success&&e.data?.tests)return e.data.tests}catch(e){}return[]},[s]),T=useCallback(async(e,t,a)=>{if(!e||!t||!a)return null;try{const c=await s("tis.read_test",MessageType.Request,{project_id:e,method_id:t,run_id:a}),r=await s("tis.read_cycles",MessageType.Request,{project_id:e,method_id:t,run_id:a,offset:0,limit:1e3,order:"asc"});if(!c?.success)return null;const d={meta:c.data??null,cycles:r?.success?r.data?.cycles??[]:[],results:c.data?.results??{},rawData:I.current[a]?.rawData??{}};return I.current[a]=d,h(),d}catch(e){return null}},[s,h]),E=useMemo(()=>({...I.current}),[f]),[k,R]=useState([]),M=useRef(new Set),[P,b]=useState(0),[w,F]=useState({}),q=useCallback(async()=>{try{const e=await s("tis.list_projects",MessageType.Request,{});e?.success&&e.data?.projects&&R(e.data.projects)}catch(e){}},[s]),D=useCallback(e=>{e&&(M.current.has(e)||(M.current.add(e),b(e=>e+1)))},[]),L=useCallback(e=>!!e&&(!!M.current.has(e)||k.includes(e)),[k,P]),Y=useCallback((e,t)=>{e&&F(s=>({...s,[e]:t}))},[]),A=useCallback(async e=>{if(!e)return null;try{const t=await s("tis.read_project",MessageType.Request,{project_id:e});if(t?.success){const s=t.data?.project_fields??{};return F(t=>({...t,[e]:s})),s}}catch(e){}return null},[s]);useEffect(()=>{q()},[q]),useEffect(()=>{const e=a("tis.project_created",()=>{q()}),t=a("tis.project_updated",e=>{const t="string"==typeof e?.project_id?e.project_id:"";t&&A(t)});return()=>{c(e),c(t)}},[a,c,q,A]),useEffect(()=>{const e=C.projectId;e&&L(e)&&void 0===w[e]&&A(e)},[C.projectId,L,w,A]);const J=w[C.projectId]??{},K=void 0!==w[C.projectId],[N,O]=useState({}),z=useCallback(e=>{O(t=>({...t,...e}))},[]),B=useCallback(()=>{O({})},[]),G=useMemo(()=>({schemas:r,defaultMethodId:n,schemasLoaded:i,state:l,selection:C,setSelection:S,existingProjects:k,projectKnown:L,refreshProjects:q,markProjectJustCreated:D,projectFields:J,projectFieldsLoaded:K,loadProjectFields:A,setProjectFields:Y,stagedConfig:N,setStagedConfig:z,clearStagedConfig:B,fetchRuns:x,fetchRun:T,runCache:E}),[r,n,i,l,C,S,k,L,q,D,J,K,A,Y,N,z,B,x,T,E]);return _jsx(TisContext.Provider,{value:G,children:e})};export const useTis=()=>useContext(TisContext);export const useTisSchemas=()=>useContext(TisContext).schemas;export const useTisState=()=>useContext(TisContext).state;export const useTisSelection=()=>{const{selection:e,setSelection:t}=useContext(TisContext);return[e,t]};export const useTisRuns=(e,t)=>{const{fetchRuns:s}=useContext(TisContext),[a,c]=useState([]),[r,d]=useState(!1),n=useCallback(async()=>{if(e){d(!0);try{c(await s(e,t))}finally{d(!1)}}else c([])},[e,t,s]);return useEffect(()=>{n()},[n]),{runs:a,loading:r,refresh:n}};export const useTisRun=e=>{const{selection:t,fetchRun:s,runCache:a}=useContext(TisContext),[c,r]=useState(!1),d=e??t.runId;useEffect(()=>{if(!d)return;if(a[d]?.meta)return;const e=t.projectId,c=t.methodId;e&&c&&(r(!0),s(e,c,d).finally(()=>r(!1)))},[d,t.projectId,t.methodId,s,a]);const n=d?a[d]:null;return{meta:n?.meta??null,cycles:n?.cycles??[],results:n?.results??{},rawData:n?.rawData??{},loading:c}};export{TisContext};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adcops/autocore-react",
3
- "version": "3.3.65",
3
+ "version": "3.3.68",
4
4
  "description": "A React component library for industrial user interfaces.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -33,6 +33,24 @@ import { MessageType } from '../../hub/CommandMessage';
33
33
  export type AmsTypeSchema = any;
34
34
  export type AmsSchemaRegistry = { [assetType: string]: AmsTypeSchema };
35
35
 
36
+ /**
37
+ * One role an asset_type can play, derived from a `select=by_location`
38
+ * `asset_ref` in `project.json::test_methods`. Used by the AMS UI to
39
+ * present a dropdown when registering a new asset, instead of forcing
40
+ * the operator to type a free-form `location` string.
41
+ */
42
+ export interface AmsRole {
43
+ /** The literal `location` string the resolver matches against. */
44
+ location: string;
45
+ /** Human-readable label, e.g. "Triaxial Transducer (TSDR)". */
46
+ label: string | null;
47
+ /** Optional one-line explanation of what this role does. */
48
+ description: string | null;
49
+ /** Test method IDs whose start_test will pick up an asset in this role. */
50
+ used_by: string[];
51
+ }
52
+ export type AmsRoleRegistry = { [assetType: string]: AmsRole[] };
53
+
36
54
  export interface AmsAlerts {
37
55
  assetCount: number;
38
56
  calibrationOverdue: number;
@@ -56,6 +74,13 @@ export interface AmsSelection {
56
74
  export interface AmsContextValue {
57
75
  schemas: AmsSchemaRegistry;
58
76
  schemasLoaded: boolean;
77
+ /**
78
+ * Roles per asset_type. Empty array means the type isn't picked up
79
+ * via `by_location` anywhere in this project — registration UIs
80
+ * should hide the Role field for that type.
81
+ */
82
+ roles: AmsRoleRegistry;
83
+ rolesLoaded: boolean;
59
84
  alerts: AmsAlerts;
60
85
  assets: AmsAssetEntry[];
61
86
  refreshAssets: () => Promise<void>;
@@ -74,6 +99,8 @@ const EMPTY_ALERTS: AmsAlerts = {
74
99
  const AmsContext = createContext<AmsContextValue>({
75
100
  schemas: {},
76
101
  schemasLoaded: false,
102
+ roles: {},
103
+ rolesLoaded: false,
77
104
  alerts: EMPTY_ALERTS,
78
105
  assets: [],
79
106
  refreshAssets: async () => {},
@@ -98,6 +125,8 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
98
125
 
99
126
  const [schemas, setSchemas] = useState<AmsSchemaRegistry>({});
100
127
  const [schemasLoaded, setSchemasLoaded] = useState(false);
128
+ const [roles, setRoles] = useState<AmsRoleRegistry>({});
129
+ const [rolesLoaded, setRolesLoaded] = useState(false);
101
130
  const [alerts, setAlerts] = useState<AmsAlerts>(EMPTY_ALERTS);
102
131
  const [assets, setAssets] = useState<AmsAssetEntry[]>([]);
103
132
  const [selection, setSelectionState] = useState<AmsSelection>({ assetType: null, assetId: null });
@@ -107,7 +136,7 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
107
136
  }, []);
108
137
 
109
138
  // -----------------------------------------------------------------
110
- // Schema load — once on mount.
139
+ // Schema + role load — once on mount.
111
140
  // -----------------------------------------------------------------
112
141
  useEffect(() => {
113
142
  let cancelled = false;
@@ -125,6 +154,23 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
125
154
  console.error('[AmsProvider] ams.list_schemas threw:', e);
126
155
  }
127
156
  })();
157
+ (async () => {
158
+ try {
159
+ const resp: any = await invoke('ams.list_roles' as any, MessageType.Request, {} as any);
160
+ if (cancelled) return;
161
+ if (resp?.success && resp.data) {
162
+ setRoles((resp.data.roles ?? {}) as AmsRoleRegistry);
163
+ setRolesLoaded(true);
164
+ } else {
165
+ // Server may not yet support list_roles — degrade gracefully.
166
+ console.warn('[AmsProvider] ams.list_roles failed:', resp?.error_message);
167
+ setRolesLoaded(true);
168
+ }
169
+ } catch (e) {
170
+ console.error('[AmsProvider] ams.list_roles threw:', e);
171
+ setRolesLoaded(true);
172
+ }
173
+ })();
128
174
  return () => { cancelled = true; };
129
175
  // eslint-disable-next-line react-hooks/exhaustive-deps
130
176
  }, []);
@@ -191,6 +237,8 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
191
237
  const value = useMemo<AmsContextValue>(() => ({
192
238
  schemas,
193
239
  schemasLoaded,
240
+ roles,
241
+ rolesLoaded,
194
242
  alerts,
195
243
  assets,
196
244
  refreshAssets,
@@ -200,7 +248,7 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
200
248
  readUsage,
201
249
  selection,
202
250
  setSelection,
203
- }), [schemas, schemasLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, selection, setSelection]);
251
+ }), [schemas, schemasLoaded, roles, rolesLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, selection, setSelection]);
204
252
 
205
253
  return <AmsContext.Provider value={value}>{children}</AmsContext.Provider>;
206
254
  };
@@ -211,6 +259,7 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
211
259
 
212
260
  export const useAms = () => useContext(AmsContext);
213
261
  export const useAmsSchemas = () => useContext(AmsContext).schemas;
262
+ export const useAmsRoles = () => useContext(AmsContext).roles;
214
263
  export const useAmsAlerts = () => useContext(AmsContext).alerts;
215
264
  export const useAmsAssets = () => useContext(AmsContext).assets;
216
265
  export const useAmsSelection = () => {
@@ -14,7 +14,7 @@ import { useAms } from './AmsProvider';
14
14
  import { CalibrationEntryDialog } from './CalibrationEntryDialog';
15
15
 
16
16
  export const AssetDetailView: React.FC = () => {
17
- const { selection, schemas, readAsset, listCalibrations, readCalibration, readUsage } = useAms();
17
+ const { selection, schemas, roles, readAsset, listCalibrations, readCalibration, readUsage } = useAms();
18
18
  const [asset, setAsset] = useState<any | null>(null);
19
19
  const [cals, setCals] = useState<any[]>([]);
20
20
  const [usage, setUsage] = useState<any | null>(null);
@@ -48,6 +48,13 @@ export const AssetDetailView: React.FC = () => {
48
48
  }
49
49
 
50
50
  const typeLabel = schemas[asset.asset_type]?.label ?? asset.asset_type;
51
+ // Resolve the asset's location string back to a human-readable role
52
+ // label when one is declared in project.json — e.g. "Triaxial
53
+ // Transducer (TSDR)" instead of `tsdr`. Falls back to the raw
54
+ // location string for assets registered with custom roles or
55
+ // running against an older project.
56
+ const roleHit = roles[asset.asset_type]?.find(r => r.location === asset.location);
57
+ const roleLabel = roleHit ? (roleHit.label ?? roleHit.location) : asset.location;
51
58
 
52
59
  return (
53
60
  <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
@@ -56,7 +63,7 @@ export const AssetDetailView: React.FC = () => {
56
63
  <strong>Type</strong> <span>{typeLabel}</span>
57
64
 
58
65
  <strong>Serial</strong> <span>{asset.serial || <em style={{ color: '#9ca3af' }}>(none)</em>}</span>
59
- <strong>Location</strong> <span>{asset.location || <em style={{ color: '#9ca3af' }}>(none)</em>}</span>
66
+ <strong>Role</strong> <span>{roleLabel || <em style={{ color: '#9ca3af' }}>(none)</em>}</span>
60
67
 
61
68
  <strong>Status</strong> <span>{asset.status}</span>
62
69
  <strong>Installed</strong>
@@ -14,19 +14,46 @@ import { InputText } from 'primereact/inputtext';
14
14
  import { Dialog } from 'primereact/dialog';
15
15
  import { EventEmitterContext } from '../../core/EventEmitterContext';
16
16
  import { MessageType } from '../../hub/CommandMessage';
17
- import { useAms, type AmsAssetEntry } from './AmsProvider';
17
+ import { useAms, type AmsAssetEntry, type AmsRole } from './AmsProvider';
18
+
19
+ // Sentinel value for the "Other..." dropdown option, which lets the
20
+ // operator type a free-form role for the rare case (custom builds,
21
+ // future test methods not yet declared in project.json).
22
+ const ROLE_OTHER = '__other__';
18
23
 
19
24
  interface AddDialogState {
20
25
  open: boolean;
21
26
  assetType: string;
22
27
  serial: string;
28
+ /** Currently-selected role from the dropdown, or ROLE_OTHER when typing. */
29
+ roleSelection: string;
30
+ /** Free-form text — used directly when roleSelection===ROLE_OTHER, or
31
+ * matches `roleSelection` for known roles. This is what gets sent
32
+ * to the server as `location`. */
23
33
  location: string;
24
34
  }
25
35
 
26
- const EMPTY_ADD: AddDialogState = { open: false, assetType: '', serial: '', location: '' };
36
+ const EMPTY_ADD: AddDialogState = {
37
+ open: false, assetType: '', serial: '', roleSelection: '', location: '',
38
+ };
39
+
40
+ /** Pretty label for one role in the dropdown. */
41
+ function roleLabel(r: AmsRole): string {
42
+ return r.label ?? r.location;
43
+ }
44
+
45
+ /** Human-readable summary of which test methods will pick up an asset
46
+ * in this role at start_test. Drives the match-indicator below the
47
+ * Role dropdown so the operator can see they're configuring the right
48
+ * thing before saving. */
49
+ function roleUsageSummary(r: AmsRole): string {
50
+ if (r.used_by.length === 0) return 'No test methods reference this role.';
51
+ if (r.used_by.length === 1) return `Used by: ${r.used_by[0]}`;
52
+ return `Used by: ${r.used_by.join(', ')}`;
53
+ }
27
54
 
28
55
  export const AssetRegistryTable: React.FC = () => {
29
- const { schemas, assets, refreshAssets, setSelection, selection } = useAms();
56
+ const { schemas, roles, assets, refreshAssets, setSelection, selection } = useAms();
30
57
  const { invoke } = useContext(EventEmitterContext);
31
58
 
32
59
  const [filterType, setFilterType] = useState<string | null>(null);
@@ -38,6 +65,42 @@ export const AssetRegistryTable: React.FC = () => {
38
65
  [schemas],
39
66
  );
40
67
 
68
+ /** Roles available for the type currently picked in the Add dialog,
69
+ * plus the "Other..." escape hatch. Empty when the type isn't
70
+ * referenced by `by_location` anywhere — in that case we hide the
71
+ * Role field entirely. */
72
+ const rolesForType: AmsRole[] = useMemo(
73
+ () => (addState.assetType ? roles[addState.assetType] ?? [] : []),
74
+ [addState.assetType, roles],
75
+ );
76
+
77
+ const typeHasRoles = rolesForType.length > 0;
78
+
79
+ const roleDropdownOptions = useMemo(() => {
80
+ const opts = rolesForType.map(r => ({
81
+ label: roleLabel(r),
82
+ value: r.location,
83
+ }));
84
+ opts.push({ label: 'Other (advanced — type a custom role)', value: ROLE_OTHER });
85
+ return opts;
86
+ }, [rolesForType]);
87
+
88
+ /** The role descriptor (if any) backing the current selection. Used
89
+ * to render the description and match-indicator under the dropdown. */
90
+ const selectedRole: AmsRole | null = useMemo(() => {
91
+ if (addState.roleSelection === ROLE_OTHER) return null;
92
+ return rolesForType.find(r => r.location === addState.roleSelection) ?? null;
93
+ }, [addState.roleSelection, rolesForType]);
94
+
95
+ /** Resolve label-for-display against the role registry. Used in the
96
+ * table's Role column body to show "Triaxial Transducer (TSDR)"
97
+ * instead of `tsdr` when there's a matching role. */
98
+ const resolveRoleLabel = (assetType: string, location: string): string => {
99
+ const list = roles[assetType] ?? [];
100
+ const hit = list.find(r => r.location === location);
101
+ return hit ? roleLabel(hit) : location;
102
+ };
103
+
41
104
  const filtered = useMemo(() => {
42
105
  return assets.filter(a => {
43
106
  if (filterType && a.asset_type !== filterType) return false;
@@ -68,6 +131,38 @@ export const AssetRegistryTable: React.FC = () => {
68
131
  }
69
132
  };
70
133
 
134
+ /** When the operator changes asset type in the dialog, pre-select
135
+ * the only role if there's exactly one (the common case for fixed
136
+ * hardware), or leave it empty so they make an explicit choice
137
+ * when there are multiple. Hide the field entirely when none. */
138
+ const onAssetTypeChange = (newType: string) => {
139
+ const list = roles[newType] ?? [];
140
+ if (list.length === 1) {
141
+ setAddState(s => ({ ...s, assetType: newType, roleSelection: list[0].location, location: list[0].location }));
142
+ } else {
143
+ setAddState(s => ({ ...s, assetType: newType, roleSelection: '', location: '' }));
144
+ }
145
+ };
146
+
147
+ /** Role dropdown change handler. ROLE_OTHER puts us into free-form
148
+ * mode where the operator types into a text field. */
149
+ const onRoleChange = (value: string) => {
150
+ if (value === ROLE_OTHER) {
151
+ setAddState(s => ({ ...s, roleSelection: ROLE_OTHER, location: '' }));
152
+ } else {
153
+ setAddState(s => ({ ...s, roleSelection: value, location: value }));
154
+ }
155
+ };
156
+
157
+ /** Disable Create until the operator has picked a role (or supplied
158
+ * free-form text). Asset types with no roles bypass this check. */
159
+ const createDisabled =
160
+ !addState.assetType ||
161
+ (typeHasRoles && (
162
+ addState.roleSelection === '' ||
163
+ (addState.roleSelection === ROLE_OTHER && !addState.location.trim())
164
+ ));
165
+
71
166
  return (
72
167
  <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
73
168
  <div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
@@ -120,7 +215,13 @@ export const AssetRegistryTable: React.FC = () => {
120
215
  body={(r: AmsAssetEntry) => schemas[r.asset_type]?.label ?? r.asset_type}
121
216
  />
122
217
  <Column field="serial" header="Serial" />
123
- <Column field="location" header="Location" />
218
+ <Column field="location" header="Role"
219
+ body={(r: AmsAssetEntry) =>
220
+ r.location
221
+ ? resolveRoleLabel(r.asset_type, r.location)
222
+ : <span style={{ color: '#9ca3af' }}>—</span>
223
+ }
224
+ />
124
225
  <Column field="status" header="Status" />
125
226
  <Column header="Calibration"
126
227
  body={(r: AmsAssetEntry) =>
@@ -141,7 +242,7 @@ export const AssetRegistryTable: React.FC = () => {
141
242
  <Button label="Cancel" severity="secondary" onClick={() => setAddState(EMPTY_ADD)} />
142
243
  <Button label="Create" icon="pi pi-check"
143
244
  onClick={onCreate}
144
- disabled={!addState.assetType}
245
+ disabled={createDisabled}
145
246
  />
146
247
  </>
147
248
  }
@@ -151,20 +252,78 @@ export const AssetRegistryTable: React.FC = () => {
151
252
  <Dropdown
152
253
  value={addState.assetType}
153
254
  options={typeOptions}
154
- onChange={(e) => setAddState(s => ({ ...s, assetType: e.value }))}
255
+ onChange={(e) => onAssetTypeChange(e.value)}
155
256
  placeholder="Choose asset type"
156
257
  />
157
258
  <label>Serial</label>
158
259
  <InputText value={addState.serial}
159
260
  onChange={(e) => setAddState(s => ({ ...s, serial: e.target.value }))} />
160
- <label>Location</label>
161
- <InputText value={addState.location}
162
- onChange={(e) => setAddState(s => ({ ...s, location: e.target.value }))} />
261
+
262
+ {/* Role field: only shown when this asset_type has at
263
+ least one declared role in project.json. Asset
264
+ types referenced only by_id_field (e.g. surfaces
265
+ picked per test by ID) skip this entirely. */}
266
+ {typeHasRoles && (
267
+ <>
268
+ <label>Role *</label>
269
+ <Dropdown
270
+ value={addState.roleSelection}
271
+ options={roleDropdownOptions}
272
+ onChange={(e) => onRoleChange(e.value)}
273
+ placeholder="Choose where this asset is mounted"
274
+ />
275
+ {addState.roleSelection === ROLE_OTHER && (
276
+ <>
277
+ <label>Custom role</label>
278
+ <InputText
279
+ value={addState.location}
280
+ placeholder="e.g. tsdr_secondary"
281
+ onChange={(e) => setAddState(s => ({ ...s, location: e.target.value }))}
282
+ />
283
+ </>
284
+ )}
285
+ </>
286
+ )}
287
+ </div>
288
+
289
+ {/* Help text + match indicator. Updates as the operator
290
+ picks a role so they see exactly which test methods
291
+ will pick up the asset they're about to register. */}
292
+ <div style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '1rem' }}>
293
+ {typeHasRoles && selectedRole && (
294
+ <>
295
+ {selectedRole.description && (
296
+ <p style={{ margin: '0 0 0.5rem 0' }}>{selectedRole.description}</p>
297
+ )}
298
+ <p style={{ margin: '0 0 0.5rem 0', color: '#34d399' }}>
299
+ ✓ {roleUsageSummary(selectedRole)}
300
+ </p>
301
+ </>
302
+ )}
303
+ {typeHasRoles && addState.roleSelection === ROLE_OTHER && (
304
+ <p style={{ margin: '0 0 0.5rem 0', color: '#f59e0b' }}>
305
+ ⚠ No test method on this machine references this custom role yet.
306
+ The asset will be registered but won't be picked up at start_test
307
+ unless you add an asset_ref to a test method's project.json.
308
+ </p>
309
+ )}
310
+ {!typeHasRoles && addState.assetType && (
311
+ <p style={{ margin: '0 0 0.5rem 0' }}>
312
+ This asset type isn't selected by physical role on this machine —
313
+ test methods reference it by ID instead. Just register the asset
314
+ and pick it by ID on the Test Setup form when running a test.
315
+ </p>
316
+ )}
317
+ <p style={{ margin: 0 }}>
318
+ The asset_id is generated by the server (format:{' '}
319
+ <code>
320
+ {addState.assetType ? (schemas[addState.assetType]?.id_prefix ?? 'A-') : 'A-'}
321
+ YYYYMMDDTHHMMSS
322
+ </code>
323
+ ). Manufacturer serial is recorded for traceability but is not used
324
+ as the unique key.
325
+ </p>
163
326
  </div>
164
- <p style={{ fontSize: '0.875rem', color: '#9ca3af', marginTop: '1rem' }}>
165
- The asset_id is generated by the server (format: <code>{addState.assetType ? (schemas[addState.assetType]?.id_prefix ?? 'A-') : 'A-'}YYYYMMDDTHHMMSS</code>).
166
- Manufacturer serial is recorded for traceability but is not used as the unique key.
167
- </p>
168
327
  </Dialog>
169
328
  </div>
170
329
  );
@@ -5,7 +5,8 @@
5
5
  * See autocore-server/doc/ams_product_plan.md for the full design.
6
6
  */
7
7
 
8
- export { AmsProvider, useAms, useAmsSchemas, useAmsAlerts, useAmsAssets, useAmsSelection } from './AmsProvider';
8
+ export { AmsProvider, useAms, useAmsSchemas, useAmsRoles, useAmsAlerts, useAmsAssets, useAmsSelection } from './AmsProvider';
9
+ export type { AmsRole, AmsRoleRegistry } from './AmsProvider';
9
10
  export { AssetRegistryTable } from './AssetRegistryTable';
10
11
  export { AssetDetailView } from './AssetDetailView';
11
12
  export { CalibrationEntryDialog } from './CalibrationEntryDialog';
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect, useContext, useMemo } from 'react';
2
2
  import { Button } from 'primereact/button';
3
3
  import { InputText } from 'primereact/inputtext';
4
+ import { Dropdown } from 'primereact/dropdown';
4
5
  import { Tooltip } from 'primereact/tooltip';
5
6
  import { EventEmitterContext } from '../../core/EventEmitterContext';
6
7
  import { AutoCoreTagContext } from '../../core/AutoCoreTagContext';
@@ -8,8 +9,23 @@ import { MessageType } from '../../hub/CommandMessage';
8
9
  import { ValueInput } from '../ValueInput';
9
10
  import { TextInput } from '../TextInput';
10
11
  import { useTis } from './TisProvider';
12
+ import { useAmsAssets, useAmsRoles, type AmsAssetEntry } from '../ams/AmsProvider';
11
13
  import { TestMethodDialog } from './TestMethodDialog';
12
14
 
15
+ /**
16
+ * One asset_ref declared on a test method. We only consume the subset
17
+ * the form cares about — `field`, `asset_type`, `select`, `from`,
18
+ * `location`. Other fields (calibration_required, label, description)
19
+ * exist on the wire but aren't form-relevant.
20
+ */
21
+ interface TisAssetRef {
22
+ field: string;
23
+ asset_type: string;
24
+ select: 'by_location' | 'by_id_field';
25
+ from?: string;
26
+ location?: string;
27
+ }
28
+
13
29
  export interface TestFieldDef {
14
30
  /** Canonical key — wire format, generated code, on-disk JSON. */
15
31
  name: string;
@@ -29,12 +45,30 @@ export interface TestMethod {
29
45
  config_fields: TestFieldDef[];
30
46
  cycle_fields: TestFieldDef[];
31
47
  results_fields: TestFieldDef[];
48
+ /**
49
+ * AMS asset references resolved at start_test. The form scans these
50
+ * for `select=by_id_field` entries pointing at a config field
51
+ * (`from: "config.<name>"`) and renders that field as a Dropdown of
52
+ * matching AMS assets instead of a free-form text input. No
53
+ * project.json change required — the form derives this from the
54
+ * existing schema.
55
+ */
56
+ asset_refs?: TisAssetRef[];
32
57
  /** Optional pretty label for the Test Method picker. Falls back
33
58
  * to the canonical method_id key. */
34
59
  label?: string;
35
60
  /** Optional long-form description shown in the picker dialog
36
61
  * when this method is highlighted. */
37
62
  description?: string;
63
+ /** Optional post-cycle analysis dispatch (`{ script, function }`).
64
+ * Consumed server-side by the codegen; the form doesn't render
65
+ * anything based on it but accepts it so generated schema
66
+ * literals from `acctl codegen-tags` typecheck cleanly. */
67
+ analysis?: any;
68
+ /** Free-form view declarations for chart components. Same rationale
69
+ * as `analysis` — the form ignores them; the type just has to
70
+ * accept them so generated schemas typecheck. */
71
+ views?: Record<string, any>;
38
72
  }
39
73
 
40
74
  /**
@@ -69,6 +103,60 @@ const hasDescription = (f: TestFieldDef): boolean =>
69
103
  const methodLabelOf = (methodId: string, schema: TestMethod | undefined): string =>
70
104
  (schema?.label && schema.label.length > 0) ? schema.label : methodId;
71
105
 
106
+ /**
107
+ * Dropdown over the active subset of AMS assets of a given type. Used
108
+ * for config fields that an `asset_ref` resolves via
109
+ * `select=by_id_field` — the operator picks an asset by ID from the
110
+ * registry rather than typing it. The dropdown's option labels
111
+ * include the asset's role (when the asset has a known role) and
112
+ * serial so the operator can identify "the right one" without leaving
113
+ * the form.
114
+ *
115
+ * Falls back to a graceful empty state when the AMS provider isn't
116
+ * mounted (no assets) — reads as "No matching assets registered" so
117
+ * the operator knows the gap and can go register one in Settings.
118
+ */
119
+ const AssetIdPicker: React.FC<{
120
+ assetType: string;
121
+ value: string;
122
+ onChange: (val: string) => void;
123
+ invalid?: boolean;
124
+ }> = ({ assetType, value, onChange, invalid }) => {
125
+ const assets = useAmsAssets();
126
+ const roles = useAmsRoles();
127
+
128
+ const options = useMemo(() => {
129
+ const filtered = (assets as AmsAssetEntry[])
130
+ .filter(a => a.asset_type === assetType && a.status === 'active');
131
+ return filtered.map(a => {
132
+ const roleLabel = roles[a.asset_type]
133
+ ?.find(r => r.location === a.location)?.label;
134
+ const labelParts = [a.asset_id];
135
+ if (roleLabel) labelParts.push(`— ${roleLabel}`);
136
+ else if (a.location) labelParts.push(`— ${a.location}`);
137
+ if (a.serial) labelParts.push(`(s/n ${a.serial})`);
138
+ return { label: labelParts.join(' '), value: a.asset_id };
139
+ });
140
+ }, [assets, roles, assetType]);
141
+
142
+ return (
143
+ <Dropdown
144
+ value={value}
145
+ options={options}
146
+ onChange={(e) => onChange(e.value ?? '')}
147
+ placeholder={
148
+ options.length === 0
149
+ ? `No active ${assetType} assets registered — add one in Settings → Assets`
150
+ : `Select ${assetType}…`
151
+ }
152
+ className={invalid ? 'p-invalid' : ''}
153
+ filter
154
+ showClear
155
+ disabled={options.length === 0}
156
+ />
157
+ );
158
+ };
159
+
72
160
  // -------------------------------------------------------------------------
73
161
 
74
162
  export const TestSetupForm: React.FC<TestSetupFormProps> = ({
@@ -92,8 +180,18 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
92
180
  const [methodId, setMethodIdLocal] = useState<string>(
93
181
  tis.selection.methodId || defaultMethodId || tis.defaultMethodId || ''
94
182
  );
95
- const [sampleId, setSampleIdLocal] = useState<string>('');
96
- const [config, setConfig] = useState<any>({});
183
+ // Sample ID + config_field values come from the TisProvider rather
184
+ // than local React state so they survive form unmount when the
185
+ // operator switches tabs. Initial value reads from selection /
186
+ // stagedConfig; subsequent edits write back to the provider.
187
+ const [sampleId, setSampleIdLocal] = useState<string>(tis.selection.sampleId || '');
188
+ const config = tis.stagedConfig;
189
+ const setConfig = (updater: any) => {
190
+ const next = typeof updater === 'function' ? updater(tis.stagedConfig) : updater;
191
+ if (next !== tis.stagedConfig) {
192
+ tis.setStagedConfig(next);
193
+ }
194
+ };
97
195
 
98
196
  const schema = schemaOverride ?? (methodId ? tis.schemas[methodId] : undefined);
99
197
 
@@ -218,15 +316,39 @@ export const TestSetupForm: React.FC<TestSetupFormProps> = ({
218
316
  }
219
317
  };
220
318
 
319
+ /**
320
+ * If the test method has an `asset_ref` whose `select=by_id_field`
321
+ * pulls from this config field, return the asset_type the field
322
+ * should pick from. The form renders that field as a dropdown of
323
+ * AMS assets instead of a free-form text input.
324
+ *
325
+ * The schema already encodes the relationship via `from:
326
+ * "config.<field_name>"` — no project.json change required.
327
+ */
328
+ const assetTypeForField = (field: TestFieldDef): string | null => {
329
+ const refs = (schema?.asset_refs ?? []) as TisAssetRef[];
330
+ const expectedFrom = `config.${field.name}`;
331
+ const hit = refs.find(r => r.select === 'by_id_field' && r.from === expectedFrom);
332
+ return hit ? hit.asset_type : null;
333
+ };
334
+
221
335
  const renderConfigField = (field: TestFieldDef) => {
222
336
  if (field.name === 'sample_id') return null;
223
337
  const valid = isFieldValid(field);
224
- const isNum = field.type !== 'string' && field.type !== 'bool';
225
338
  const tooltipId = `acFormInfo_${field.name}`;
339
+ const assetType = assetTypeForField(field);
340
+ const isNum = !assetType && field.type !== 'string' && field.type !== 'bool';
226
341
  return (
227
342
  <React.Fragment key={field.name}>
228
343
  <span className="ac-form-label">{labelOf(field)}</span>
229
- {isNum ? (
344
+ {assetType ? (
345
+ <AssetIdPicker
346
+ assetType={assetType}
347
+ value={config[field.name] != null ? String(config[field.name]) : ''}
348
+ onChange={(val) => handleFieldChange(field, val)}
349
+ invalid={!valid}
350
+ />
351
+ ) : isNum ? (
230
352
  <ValueInput
231
353
  label={undefined}
232
354
  value={config[field.name] != null ? Number(config[field.name]) : null}
@@ -129,6 +129,22 @@ export interface TisContextValue {
129
129
  * by the create / edit dialogs after a successful submit. */
130
130
  setProjectFields: (id: string, fields: Record<string, any>) => void;
131
131
 
132
+ /**
133
+ * In-progress test draft — the operator's pending Sample ID and
134
+ * config_field values for the active method. Held on the provider
135
+ * (not in `<TestSetupForm>`'s local React state) so the values
136
+ * survive remount when the operator switches tabs. Wiped via
137
+ * `clearStagedConfig()` after a successful start_test or when the
138
+ * operator cancels the staged record.
139
+ */
140
+ stagedConfig: Record<string, any>;
141
+ /** Merge a partial patch into `stagedConfig`. Existing fields not
142
+ * mentioned in the patch are preserved. */
143
+ setStagedConfig: (patch: Record<string, any>) => void;
144
+ /** Reset `stagedConfig` to `{}`. Called by the form on a fresh
145
+ * method selection or after a run completes. */
146
+ clearStagedConfig: () => void;
147
+
132
148
  /** Fetch the run list for a (project, method?) pair. Method may be
133
149
  * omitted to aggregate runs across every method in the project —
134
150
  * the History tab uses this. */
@@ -167,6 +183,9 @@ const TisContext = createContext<TisContextValue>({
167
183
  projectFieldsLoaded: false,
168
184
  loadProjectFields: async () => null,
169
185
  setProjectFields: () => {},
186
+ stagedConfig: {},
187
+ setStagedConfig: () => {},
188
+ clearStagedConfig: () => {},
170
189
  fetchRuns: async () => [],
171
190
  fetchRun: async () => null,
172
191
  runCache: {},
@@ -506,17 +525,32 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
506
525
  const projectFields = projectFieldsCache[selection.projectId] ?? {};
507
526
  const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
508
527
 
528
+ // Operator's pending Sample ID and config_field values live here
529
+ // (rather than as local state in <TestSetupForm>) so they survive
530
+ // the form unmounting when the user switches to another tab. The
531
+ // form re-hydrates from this on remount, so leaving the Test tab
532
+ // and coming back is transparent.
533
+ const [stagedConfig, setStagedConfigState] = useState<Record<string, any>>({});
534
+ const setStagedConfig = useCallback((patch: Record<string, any>) => {
535
+ setStagedConfigState(prev => ({ ...prev, ...patch }));
536
+ }, []);
537
+ const clearStagedConfig = useCallback(() => {
538
+ setStagedConfigState({});
539
+ }, []);
540
+
509
541
  const value: TisContextValue = useMemo(() => ({
510
542
  schemas, defaultMethodId, schemasLoaded,
511
543
  state, selection, setSelection,
512
544
  existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
513
545
  projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
546
+ stagedConfig, setStagedConfig, clearStagedConfig,
514
547
  fetchRuns, fetchRun, runCache,
515
548
  }), [
516
549
  schemas, defaultMethodId, schemasLoaded,
517
550
  state, selection, setSelection,
518
551
  existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
519
552
  projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
553
+ stagedConfig, setStagedConfig, clearStagedConfig,
520
554
  fetchRuns, fetchRun, runCache,
521
555
  ]);
522
556