@adcops/autocore-react 3.5.3 → 3.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ams/AmsProvider.d.ts +14 -0
- package/dist/components/ams/AmsProvider.d.ts.map +1 -1
- package/dist/components/ams/AmsProvider.js +1 -1
- package/dist/components/ams/AssetDetailView.d.ts.map +1 -1
- package/dist/components/ams/AssetDetailView.js +1 -1
- package/dist/components/ams/AssetNameplateGrid.d.ts +13 -0
- package/dist/components/ams/AssetNameplateGrid.d.ts.map +1 -0
- package/dist/components/ams/AssetNameplateGrid.js +1 -0
- package/dist/components/ams/InstalledAssetPicker.d.ts +35 -0
- package/dist/components/ams/InstalledAssetPicker.d.ts.map +1 -0
- package/dist/components/ams/InstalledAssetPicker.js +1 -0
- package/dist/components/ams/index.d.ts +5 -0
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/dist/components/ams/useInstalledAsset.d.ts +26 -0
- package/dist/components/ams/useInstalledAsset.d.ts.map +1 -0
- package/dist/components/ams/useInstalledAsset.js +1 -0
- package/dist/components/tis/TestFieldRow.d.ts +19 -0
- package/dist/components/tis/TestFieldRow.d.ts.map +1 -1
- package/dist/components/tis/TestFieldRow.js +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.d.ts.map +1 -1
- package/dist/components/tis-editor/editor/TestFieldDialog.js +1 -1
- package/dist/components/tis-editor/types.d.ts +6 -2
- package/dist/components/tis-editor/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ams/AmsProvider.tsx +36 -1
- package/src/components/ams/AssetDetailView.tsx +2 -20
- package/src/components/ams/AssetNameplateGrid.tsx +62 -0
- package/src/components/ams/InstalledAssetPicker.tsx +209 -0
- package/src/components/ams/index.ts +7 -0
- package/src/components/ams/useInstalledAsset.ts +108 -0
- package/src/components/tis/TestFieldRow.tsx +31 -0
- package/src/components/tis-editor/editor/TestFieldDialog.tsx +29 -18
- package/src/components/tis-editor/types.ts +6 -2
|
@@ -73,6 +73,20 @@ export interface AmsContextValue {
|
|
|
73
73
|
listCalibrations: (assetId: string) => Promise<string[]>;
|
|
74
74
|
readCalibration: (assetId: string, calId: string) => Promise<any | null>;
|
|
75
75
|
readUsage: (assetId: string) => Promise<any | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Declare which asset is INSTALLED at a role (`ams.assign_role`).
|
|
78
|
+
*
|
|
79
|
+
* Pass `null` for `assetId` to record that nothing is fitted. One RPC,
|
|
80
|
+
* not two `updateAsset` calls: the server vacates the role and installs
|
|
81
|
+
* the new asset atomically, so the role is never momentarily empty or
|
|
82
|
+
* double-claimed — `resolve_method_refs` takes the FIRST active asset at
|
|
83
|
+
* a location, so a double-claimed role resolves to an arbitrary one.
|
|
84
|
+
*
|
|
85
|
+
* Resolves to `null` on success, or the server's message on refusal
|
|
86
|
+
* (wrong asset type for the role, asset not active, unknown id) so the
|
|
87
|
+
* caller can show the operator WHY the swap was refused.
|
|
88
|
+
*/
|
|
89
|
+
assignRole: (location: string, assetId: string | null) => Promise<string | null>;
|
|
76
90
|
selection: AmsSelection;
|
|
77
91
|
setSelection: (patch: Partial<AmsSelection>) => void;
|
|
78
92
|
}
|
|
@@ -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;;;;;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;IAClB;;;;;oCAKgC;IAChC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;;uCAQmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;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;
|
|
1
|
+
{"version":3,"file":"AmsProvider.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AmsProvider.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,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,
|
|
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,assignRole:async()=>"AmsProvider is not mounted",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),[l,u]=useState({}),[i,m]=useState(!1),[d,C]=useState(EMPTY_ALERTS),[y,_]=useState([]),[x,b]=useState({assetType:null,assetId:null}),A=useCallback(e=>{b(s=>({...s,...e}))},[]),h=useCallback(async()=>{try{const e=await s("ams.list_schemas",MessageType.Request,{});e?.success&&e.data&&(r(e.data.asset_types??{}),o(!0))}catch(e){}},[s]);useEffect(()=>{let e=!1;return h(),(async()=>{try{const t=await s("ams.list_roles",MessageType.Request,{});if(e)return;t?.success&&t.data?(u(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&&_(e.data.assets??[])}catch(e){}},[s]);useEffect(()=>{p()},[p]),useEffect(()=>{const e=[t("ams.asset_changed",()=>{p()}),t("ams.calibration_added",()=>{p()}),t("ams.calibration_updated",()=>{p()}),t("ams.schema_changed",()=>{h()}),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,h]);const g=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=useCallback(async(e,t)=>{try{const a=await s("ams.assign_role",MessageType.Request,{location:e,asset_id:t});return a?.success?(await p(),null):a?.error_message||"ams.assign_role failed"}catch(e){return String(e?.message??e)}},[s,p]),M=useMemo(()=>({schemas:n,schemasLoaded:c,roles:l,rolesLoaded:i,alerts:d,assets:y,refreshAssets:p,readAsset:g,listCalibrations:f,readCalibration:E,readUsage:S,assignRole:T,selection:x,setSelection:A}),[n,c,l,i,d,y,p,g,f,E,S,T,x,A]);return _jsx(AmsContext.Provider,{value:M,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,KAA0C,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AssetDetailView.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetDetailView.tsx"],"names":[],"mappings":"AAQA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAY/D,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAyRnC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{ConfirmDialog,confirmDialog}from"primereact/confirmdialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";import{AssetEditDialog}from"./AssetEditDialog";export const AssetDetailView=()=>{const{selection:e,schemas:t,roles:s,readAsset:i,listCalibrations:a,readCalibration:r,readUsage:l,refreshAssets:n,setSelection:o}=useAms(),{invoke:d}=useContext(EventEmitterContext),[c,m]=useState(null),[p,
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext,useEffect,useState}from"react";import{Button}from"primereact/button";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{ConfirmDialog,confirmDialog}from"primereact/confirmdialog";import{EventEmitterContext}from"../../core/EventEmitterContext";import{MessageType}from"../../hub/CommandMessage";import{useAms}from"./AmsProvider";import{CalibrationEntryDialog}from"./CalibrationEntryDialog";import{AssetEditDialog}from"./AssetEditDialog";import{AssetNameplateGrid}from"./AssetNameplateGrid";export const AssetDetailView=()=>{const{selection:e,schemas:t,roles:s,readAsset:i,listCalibrations:a,readCalibration:r,readUsage:l,refreshAssets:n,setSelection:o}=useAms(),{invoke:d}=useContext(EventEmitterContext),[c,m]=useState(null),[p,_]=useState([]),[u,h]=useState(null),[x,f]=useState(!1),[y,j]=useState(null),[b,g]=useState(!1),[v,C]=useState(!1),S=async()=>{if(!e.assetId)return m(null),_([]),void h(null);const t=await i(e.assetId);m(t);const s=await a(e.assetId),n=await Promise.all(s.map(t=>r(e.assetId,t)));_(n.filter(Boolean));const o=await l(e.assetId);h(o)};if(useEffect(()=>{S()},[e.assetId]),!e.assetId)return _jsx("div",{style:{padding:"1rem",color:"#9ca3af"},children:"Select an asset from the registry to see its details."});if(!c)return _jsx("div",{style:{padding:"1rem"},children:"Loading…"});const A=t[c.asset_type]?.label??c.asset_type,w=s[c.asset_type]?.find(e=>e.location===c.location),D=w?w.label??w.location:c.location,E=Array.isArray(t[c.asset_type]?.fields)?t[c.asset_type].fields:[],k=c.custom??{},I=w?.used_by_modules??[];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:c.asset_id}),_jsx("strong",{children:"Type"})," ",_jsx("span",{children:A}),_jsx("strong",{children:"Serial"})," ",_jsx("span",{children:c.serial||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Role"})," ",_jsx("span",{children:D||_jsx("em",{style:{color:"#9ca3af"},children:"(none)"})}),_jsx("strong",{children:"Status"})," ",_jsx("span",{children:c.status}),_jsx("strong",{children:"Installed"}),_jsx("span",{children:c.install_date?new Date(c.install_date).toLocaleString():"—"}),_jsx("strong",{children:"Current Cal"}),_jsx("span",{children:c.current_calibration_id??_jsx("em",{style:{color:"#f59e0b"},children:"none"})}),_jsx("strong",{children:"Cycles"})," ",_jsx("span",{children:u?.cycles??0})]}),I.length>0&&_jsxs("div",{style:{fontSize:"0.875rem",color:"#34d399"},children:["✓ Feeds module ",1===I.length?"config":"configs",":"," ",_jsx("strong",{children:I.join(", ")})," — ","their ",_jsxs("code",{children:["$","${ams.by_location."+c.location+".*}"]})," ","placeholders will resolve to the values below."]}),E.length>0&&_jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Nameplate"}),_jsx(AssetNameplateGrid,{schemaFields:E,custom:k})]}),_jsx(SubLocationsPanel,{schema:t[c.asset_type]?.sub_locations,values:c.sub_locations}),_jsx(SubLocationsCalibrationPanel,{schema:t[c.asset_type]?.sub_locations,currentCalibration:p.find(e=>e.cal_id===c.current_calibration_id)}),_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center"},children:[_jsx("h4",{style:{margin:0},children:"Calibration History"}),_jsxs("div",{style:{display:"flex",gap:"0.5rem"},children:[_jsx(Button,{label:"Edit",icon:"pi pi-pencil",outlined:!0,onClick:()=>C(!0),tooltip:"Edit serial, role, nameplate, and per-axis values. Type and install date are immutable.",tooltipOptions:{position:"left"}}),"active"===c.status&&_jsx(Button,{label:"Retire",icon:"pi pi-power-off",severity:"warning",outlined:!0,onClick:async()=>{if(c)try{const e=await d("ams.update_asset",MessageType.Request,{asset_id:c.asset_id,status:"retired"});e?.success&&(await n(),await S())}catch(e){}},tooltip:"Mark inactive. The asset stays on disk; deletion is a separate action available after retirement.",tooltipOptions:{position:"left"}}),"retired"===c.status&&_jsx(Button,{label:"Delete",icon:"pi pi-trash",severity:"danger",outlined:!0,loading:b,onClick:()=>{c&&confirmDialog({message:_jsxs("div",{children:[_jsxs("p",{style:{marginTop:0},children:["Permanently delete asset ",_jsx("code",{children:c.asset_id}),"?"]}),_jsx("p",{style:{marginBottom:0,fontSize:"0.875rem",color:"#9ca3af"},children:"This removes the AMS record (nameplate, calibration history, usage counters). Historical test records that referenced this asset are unaffected — they carry the full asset and calibration data inline."})]}),header:"Delete asset",icon:"pi pi-exclamation-triangle",acceptLabel:"Delete",rejectLabel:"Cancel",acceptClassName:"p-button-danger",accept:()=>{(async()=>{if(c){g(!0);try{const e=await d("ams.delete_asset",MessageType.Request,{asset_id:c.asset_id});e?.success?(o({assetType:null,assetId:null}),await n()):alert(`Delete failed: ${e?.error_message??"unknown error"}`)}catch(e){alert(`Delete threw: ${e?.message??e}`)}finally{g(!1)}}})()}})},tooltip:"Permanent. Removes the AMS record; historical test snapshots are preserved inline.",tooltipOptions:{position:"left"}}),_jsx(Button,{label:"+ Calibration",icon:"pi pi-plus",onClick:()=>{j(null),f(!0)}})]})]}),_jsx(ConfirmDialog,{}),_jsxs(DataTable,{value:p,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(Column,{header:"",style:{width:"5rem"},body:e=>e.cal_id===c.current_calibration_id?_jsx(Button,{icon:"pi pi-pencil",text:!0,rounded:!0,size:"small",tooltip:"Edit this calibration (fixes a typo in place).",tooltipOptions:{position:"left"},onClick:()=>{j(e),f(!0)}}):null})]}),_jsx(CalibrationEntryDialog,{visible:x,assetId:c.asset_id,assetType:c.asset_type,editing:y,onHide:()=>{f(!1),j(null)},onAdded:()=>{S()}}),_jsx(AssetEditDialog,{visible:v,asset:c,onHide:()=>C(!1),onSaved:async()=>{await n(),await S()}})]})};const SubLocationsPanel=({schema:e,values:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.fields))return null;const s=t&&"object"==typeof t?t:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:e.label??"Sub-locations"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.fields.map(e=>_jsxs("th",{title:e.description??void 0,style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})},SubLocationsCalibrationPanel=({schema:e,currentCalibration:t})=>{if(!e||!Array.isArray(e.keys)||!Array.isArray(e.calibration_fields)||0===e.calibration_fields.length)return null;if(!t)return null;const s=t.values&&"object"==typeof t.values?t.values:{};return _jsxs("div",{children:[_jsx("h4",{style:{margin:"0 0 0.5rem 0"},children:"Current Calibration — Per-axis Values"}),_jsx("div",{style:{overflowX:"auto"},children:_jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:"0.875rem"},children:[_jsx("thead",{children:_jsxs("tr",{children:[_jsx("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:e.key_label??"Key"}),e.calibration_fields.map(e=>_jsxs("th",{style:{textAlign:"left",padding:"0.25rem 0.5rem",borderBottom:"1px solid var(--surface-border)"},children:[e.label??e.name,e.units?` [${e.units}]`:""]},e.name))]})}),_jsx("tbody",{children:e.keys.map(t=>{const i=s[t]??{};return _jsxs("tr",{children:[_jsx("td",{style:{padding:"0.25rem 0.5rem",fontWeight:600},children:t}),e.calibration_fields.map(e=>{const t=i[e.name],s=null!=t&&""!==t;return _jsx("td",{style:{padding:"0.25rem 0.5rem",color:s?void 0:"#f59e0b"},children:s?String(t):"(not set)"},e.name)})]},t)})})]})})]})};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface AssetNameplateGridProps {
|
|
3
|
+
/** The asset_type schema's `fields` array. */
|
|
4
|
+
schemaFields: any[];
|
|
5
|
+
/** The asset record's `custom` object (nameplate values). */
|
|
6
|
+
custom: Record<string, any>;
|
|
7
|
+
/** Only render these field names, in this order. Omit for all of them. */
|
|
8
|
+
only?: string[];
|
|
9
|
+
/** Font size for the grid; the detail view runs slightly larger. */
|
|
10
|
+
fontSize?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const AssetNameplateGrid: React.FC<AssetNameplateGridProps>;
|
|
13
|
+
//# sourceMappingURL=AssetNameplateGrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssetNameplateGrid.d.ts","sourceRoot":"","sources":["../../../src/components/ams/AssetNameplateGrid.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,uBAAuB;IACpC,8CAA8C;IAC9C,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAmChE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React from"react";export const AssetNameplateGrid=({schemaFields:e,custom:t,only:s,fontSize:n="0.9rem"})=>{const r=s?s.map(t=>e.find(e=>e.name===t)).filter(Boolean):e;return 0===r.length?null:_jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"0.25rem 1rem",alignItems:"baseline",fontSize:n},children:r.map(e=>{const s=t[e.name],n=null!=s&&""!==s,r=e.label??e.name,i=n?e.units?`${s} ${e.units}`:String(s):"(not set)";return _jsxs(React.Fragment,{children:[_jsx("strong",{title:e.description??void 0,children:r}),_jsx("span",{style:{color:n?void 0:"#f59e0b"},children:i})]},e.name)})})};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface InstalledAssetPickerProps {
|
|
3
|
+
/** Asset type the role takes, e.g. `"load_cell"`. */
|
|
4
|
+
assetType: string;
|
|
5
|
+
/** The role's `location` string, e.g. `"press_load_cell"`. */
|
|
6
|
+
location: string;
|
|
7
|
+
/**
|
|
8
|
+
* Heading for the picker. Defaults to the role's label from
|
|
9
|
+
* `ams.list_roles`, then to the raw location.
|
|
10
|
+
*/
|
|
11
|
+
label?: string;
|
|
12
|
+
/** One line under the dropdown explaining what this role drives. */
|
|
13
|
+
description?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Nameplate field names folded into each option's text, so the operator can
|
|
16
|
+
* tell two assets apart without opening them (e.g.
|
|
17
|
+
* `["capacity", "capacity_units"]` → "2000 N"). Serial is always shown.
|
|
18
|
+
*/
|
|
19
|
+
summaryFields?: string[];
|
|
20
|
+
/** Render the fitted asset's full nameplate below the dropdown. */
|
|
21
|
+
showNameplate?: boolean;
|
|
22
|
+
/** Render the fitted asset's calibration state below the nameplate. */
|
|
23
|
+
showCalibration?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Block the swap. Use for "a test is running" — changing the fitted asset
|
|
26
|
+
* mid-run would re-scale a channel under the data being recorded.
|
|
27
|
+
*/
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Why it is blocked. Shown in place of the help text. */
|
|
30
|
+
disabledReason?: string;
|
|
31
|
+
/** Fired after a successful assignment with the new asset id (or null). */
|
|
32
|
+
onAssigned?: (assetId: string | null) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare const InstalledAssetPicker: React.FC<InstalledAssetPickerProps>;
|
|
35
|
+
//# sourceMappingURL=InstalledAssetPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InstalledAssetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ams/InstalledAssetPicker.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAQ9D,MAAM,WAAW,yBAAyB;IACtC,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,mEAAmE;IACnE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACjD;AAKD,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA8IpE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";import React,{useCallback,useMemo,useState}from"react";import{Dropdown}from"primereact/dropdown";import{Button}from"primereact/button";import{Dialog}from"primereact/dialog";import{Message}from"primereact/message";import{AssetNameplateGrid}from"./AssetNameplateGrid";import{useInstalledAsset}from"./useInstalledAsset";const NONE="__none__";export const InstalledAssetPicker=({assetType:e,location:s,label:t,description:a,summaryFields:l=[],showNameplate:i=!0,showCalibration:r=!0,disabled:n=!1,disabledReason:o,onAssigned:d})=>{const{candidates:c,records:m,installed:u,installedRecord:h,calibration:_,calibrationOverdue:p,role:x,schemaFields:g,assign:f}=useInstalledAsset(e,s),[j,b]=useState(null),[y,N]=useState(!1),[v,w]=useState(null),C=t??x?.label??s,A=a??x?.description??null,D=useCallback(e=>{const s=c.find(s=>s.asset_id===e),t=m[e]?.custom??{},a=[s?.serial?`SN ${s.serial}`:e],i=l.map(e=>t[e]).filter(e=>null!=e&&""!==e).join(" ");return i&&a.push(i),a.join(" — ")},[c,m,l]),I=useMemo(()=>[...c.map(e=>({label:D(e.asset_id),value:e.asset_id})),{label:"— nothing fitted —",value:NONE}],[c,D]),S=_?.expires_at?new Date(_.expires_at):null;return _jsxs("div",{style:{display:"flex",flexDirection:"column",gap:"0.5rem"},children:[_jsxs("div",{className:"p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",style:{flexGrow:1},children:C}),_jsx(Dropdown,{value:u?.asset_id??NONE,options:I,disabled:n||y,onChange:e=>{const s=e.value===NONE?null:e.value;s!==(u?.asset_id??null)&&w({assetId:s})},placeholder:"Select the fitted asset",style:{minWidth:"18rem"}})]}),n&&o?_jsx("small",{style:{color:"#f59e0b"},children:o}):A&&_jsx("small",{style:{color:"#9ca3af"},children:A}),0===c.length&&_jsx(Message,{severity:"warn",text:`No active ${e.replace(/_/g," ")} is registered. Add one on the Assets screen first.`}),!u&&c.length>0&&_jsx(Message,{severity:"warn",text:"Nothing is recorded as fitted in this role. Tests that need it will refuse to start."}),j&&_jsx(Message,{severity:"error",text:j}),u&&i&&_jsx(AssetNameplateGrid,{schemaFields:g,custom:h?.custom??{}}),u&&r&&_jsx("small",{style:{color:p?"#f59e0b":"#9ca3af"},children:u.current_calibration_id?p?`Calibration ${u.current_calibration_id} expired ${S.toLocaleDateString()}.`:`Calibration ${u.current_calibration_id}${S?`, in date until ${S.toLocaleDateString()}`:""}.`:"No calibration on record for this asset."}),_jsxs(Dialog,{visible:null!==v,header:`Change the ${C}?`,modal:!0,onHide:()=>w(null),footer:_jsxs(_Fragment,{children:[_jsx(Button,{label:"Cancel",outlined:!0,onClick:()=>w(null)}),_jsx(Button,{label:"Confirm",loading:y,onClick:()=>{(async e=>{N(!0),b(null);const s=await f(e);N(!1),w(null),s?b(s):d?.(e)})(v?.assetId??null)}})]}),children:[_jsx("p",{style:{margin:0},children:_jsxs(_Fragment,v?.assetId?{children:["Record ",_jsx("strong",{children:D(v.assetId)})," as the asset fitted in ",_jsx("strong",{children:C}),"."]}:{children:["Record that ",_jsx("strong",{children:"nothing"})," is fitted in ",_jsx("strong",{children:C}),"."]})}),_jsx("p",{style:{marginBottom:0,color:"#9ca3af"},children:"Confirm only if this matches the hardware actually installed. The machine reconfigures the channel from this asset's nameplate, and every test started from now on records it."})]})]})};
|
|
@@ -7,4 +7,9 @@ export { AssetEditDialog } from './AssetEditDialog';
|
|
|
7
7
|
export { SubLocationPicker } from './SubLocationPicker';
|
|
8
8
|
export { PlaceholderHealthPanel } from './PlaceholderHealthPanel';
|
|
9
9
|
export { MissingAssetsBanner } from './MissingAssetsBanner';
|
|
10
|
+
export { InstalledAssetPicker } from './InstalledAssetPicker';
|
|
11
|
+
export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
|
|
12
|
+
export { AssetNameplateGrid } from './AssetNameplateGrid';
|
|
13
|
+
export { useInstalledAsset } from './useInstalledAsset';
|
|
14
|
+
export type { InstalledAssetState } from './useInstalledAsset';
|
|
10
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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,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,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,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,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAG5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";
|
|
1
|
+
export{AmsProvider,useAms,useAmsSchemas,useAmsRoles,useAmsAlerts,useAmsAssets,useAmsSelection}from"./AmsProvider";export{AssetRegistryTable}from"./AssetRegistryTable";export{AssetDetailView}from"./AssetDetailView";export{CalibrationEntryDialog}from"./CalibrationEntryDialog";export{AssetEditDialog}from"./AssetEditDialog";export{SubLocationPicker}from"./SubLocationPicker";export{PlaceholderHealthPanel}from"./PlaceholderHealthPanel";export{MissingAssetsBanner}from"./MissingAssetsBanner";export{InstalledAssetPicker}from"./InstalledAssetPicker";export{AssetNameplateGrid}from"./AssetNameplateGrid";export{useInstalledAsset}from"./useInstalledAsset";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AmsAssetEntry, type AmsRole } from './AmsProvider';
|
|
2
|
+
export interface InstalledAssetState {
|
|
3
|
+
/** Every ACTIVE asset of this type — the assets that could be fitted. */
|
|
4
|
+
candidates: AmsAssetEntry[];
|
|
5
|
+
/** Full records for `candidates`, keyed by asset_id. */
|
|
6
|
+
records: Record<string, any>;
|
|
7
|
+
/** The registry entry fitted in this role, or null when nothing is. */
|
|
8
|
+
installed: AmsAssetEntry | null;
|
|
9
|
+
/** The fitted asset's full record; nameplate values live under `.custom`. */
|
|
10
|
+
installedRecord: any | null;
|
|
11
|
+
/** The fitted asset's current calibration record, or null. */
|
|
12
|
+
calibration: any | null;
|
|
13
|
+
/** True once the calibration on record has passed its `expires_at`. */
|
|
14
|
+
calibrationOverdue: boolean;
|
|
15
|
+
/** This role's metadata from `ams.list_roles`, when the project declares it. */
|
|
16
|
+
role: AmsRole | null;
|
|
17
|
+
/** The asset_type schema's nameplate `fields` array (empty if unknown). */
|
|
18
|
+
schemaFields: any[];
|
|
19
|
+
/**
|
|
20
|
+
* Declare what is fitted (`null` = nothing). Resolves to `null` on success
|
|
21
|
+
* or the server's refusal message.
|
|
22
|
+
*/
|
|
23
|
+
assign: (assetId: string | null) => Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
export declare function useInstalledAsset(assetType: string, location: string): InstalledAssetState;
|
|
26
|
+
//# sourceMappingURL=useInstalledAsset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInstalledAsset.d.ts","sourceRoot":"","sources":["../../../src/components/ams/useInstalledAsset.ts"],"names":[],"mappings":"AAeA,OAAO,EAAU,KAAK,aAAa,EAAE,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAEzE,MAAM,WAAW,mBAAmB;IAChC,yEAAyE;IACzE,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,uEAAuE;IACvE,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,eAAe,EAAE,GAAG,GAAG,IAAI,CAAC;IAC5B,8DAA8D;IAC9D,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,uEAAuE;IACvE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,gFAAgF;IAChF,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,2EAA2E;IAC3E,YAAY,EAAE,GAAG,EAAE,CAAC;IACpB;;;OAGG;IACH,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC9D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAkE1F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useCallback,useEffect,useMemo,useState}from"react";import{useAms}from"./AmsProvider";export function useInstalledAsset(e,s){const{assets:t,schemas:a,roles:i,readAsset:r,readCalibration:n,assignRole:l}=useAms(),[o,u]=useState({}),[c,d]=useState(null),f=useMemo(()=>t.filter(s=>s.asset_type===e&&"active"===s.status),[t,e]),m=useMemo(()=>f.find(e=>e.location===s)??null,[f,s]),_=useMemo(()=>(i[e]??[]).find(e=>e.location===s)??null,[i,e,s]),b=f.map(e=>e.asset_id).join(",");useEffect(()=>{let e=!1;return(async()=>{const s={};for(const t of f){const a=await r(t.asset_id);if(e)return;a&&(s[t.asset_id]=a)}e||u(s)})(),()=>{e=!0}},[b,r]),useEffect(()=>{let e=!1;return(async()=>{if(!m?.current_calibration_id)return void d(null);const s=await n(m.asset_id,m.current_calibration_id);e||d(s)})(),()=>{e=!0}},[m?.asset_id,m?.current_calibration_id,n]);const p=useCallback(e=>l(s,e),[l,s]),A=c?.expires_at?new Date(c.expires_at):null;return{candidates:f,records:o,installed:m,installedRecord:m?o[m.asset_id]??null:null,calibration:c,calibrationOverdue:!!A&&A.getTime()<Date.now(),role:_,schemaFields:Array.isArray(a[e]?.fields)?a[e].fields:[],assign:p}}
|
|
@@ -128,6 +128,25 @@ export interface TagLookup {
|
|
|
128
128
|
} | undefined;
|
|
129
129
|
rawValues: Record<string, unknown>;
|
|
130
130
|
}
|
|
131
|
+
/** Is this string a well-formed FQDN bound token? */
|
|
132
|
+
export declare const isBoundToken: (s: string) => boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Render a bound for an authoring text field — the method editor's Min/Max.
|
|
135
|
+
*
|
|
136
|
+
* A text field, not a numeric one, because a bound is a number OR a token. The
|
|
137
|
+
* editor used `InputNumber`, which rendered a token as **NaN** and — worse —
|
|
138
|
+
* coerced anything non-numeric to `undefined` on change, so opening the dialog
|
|
139
|
+
* and saving silently deleted a token bound.
|
|
140
|
+
*/
|
|
141
|
+
export declare const boundText: (b: number | string | undefined) => string;
|
|
142
|
+
/**
|
|
143
|
+
* Parse what an author typed into a bound field. Blank clears it; a finite
|
|
144
|
+
* number stores a number; anything else is kept VERBATIM so a half-typed token
|
|
145
|
+
* survives the keystroke. Use `boundInvalid` to flag what will not resolve.
|
|
146
|
+
*/
|
|
147
|
+
export declare const parseBound: (text: string) => number | string | undefined;
|
|
148
|
+
/** True when a bound is a string that is not a well-formed token. */
|
|
149
|
+
export declare const boundInvalid: (b: number | string | undefined) => boolean;
|
|
131
150
|
/**
|
|
132
151
|
* A declared bound resolved to a plain DISPLAY-unit number, or `undefined` for
|
|
133
152
|
* "no bound".
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestFieldRow.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestFieldRow.tsx"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TestFieldRow.d.ts","sourceRoot":"","sources":["../../../src/components/tis/TestFieldRow.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext}from"react";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";export const rawToDisplay=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?e*(t??1)+n:e;export const displayToRaw=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?(e-n)/(t??1):e;export const effectiveField=(e,t,n)=>{const l=e.quantity?t?.[e.quantity]:void 0,o=l?l.scale:e.scale,
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useContext}from"react";import{AutoCoreTagContext}from"../../core/AutoCoreTagContext";import{Button}from"primereact/button";import{Dropdown}from"primereact/dropdown";import{ValueInput}from"../ValueInput";import{TextInput}from"../TextInput";export const rawToDisplay=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?e*(t??1)+n:e;export const displayToRaw=(e,t,n=0)=>(t&&1!==t||n)&&"number"==typeof e&&Number.isFinite(e)?(e-n)/(t??1):e;export const effectiveField=(e,t,n)=>{const l=e.quantity?t?.[e.quantity]:void 0,o=l?l.scale:e.scale,r=l?l.offset??0:0;return{...l?{...e,scale:o,units:l.label,unitOffset:r}:{...e},min:resolveBound(e.min,o,r,n),max:resolveBound(e.max,o,r,n)}};const BOUND_TOKEN_RE=/^\$\{\s*([^}]+?)\s*\}$/;export const isBoundToken=e=>BOUND_TOKEN_RE.test(e.trim());export const boundText=e=>null==e?"":String(e);export const parseBound=e=>{const t=e.trim();if(""===t)return;const n=Number(t);return Number.isFinite(n)?n:t};export const boundInvalid=e=>"string"==typeof e&&!isBoundToken(e);export const resolveBound=(e,t,n,l)=>{if("number"==typeof e)return e;if("string"!=typeof e||!l)return;const o=e.match(BOUND_TOKEN_RE);if(!o)return;const r=l.findTagByFqdn(o[1]),i=r?l.rawValues[r.tagName]:void 0;if("number"!=typeof i||!Number.isFinite(i))return;const s=Number(rawToDisplay(i,t,n));return Number.isFinite(s)?s:void 0};export const labelOf=e=>{const t=e.label&&e.label.length>0?e.label:e.name;return e.units?`${t} [${e.units}]`:t};export const hasDescription=e=>"string"==typeof e.description&&e.description.length>0;export const fieldValueIsEmpty=e=>null==e||"string"==typeof e&&""===e.trim();export const rangeIssue=(e,t)=>{if(fieldValueIsEmpty(t)||""===t)return null;const n="number"==typeof e.min?e.min:void 0,l="number"==typeof e.max?e.max:void 0;if(null==n&&null==l)return null;const o=Number(rawToDisplay(Number(t),e.scale,e.unitOffset??0));if(!Number.isFinite(o))return null;const r=null!=n&&null!=l?Math.min(n,l):n,i=null!=n&&null!=l?Math.max(n,l):l;return null!=r&&o<r?`must be ≥ ${r}`:null!=i&&o>i?`must be ≤ ${i}`:null};export const fieldIssue=(e,t)=>e.required&&fieldValueIsEmpty(t)?"is required":rangeIssue(e,t);export const fieldIsValid=(e,t)=>null===fieldIssue(e,t);export const normalizeOptions=e=>(e??[]).map(e=>null!==e&&"object"==typeof e?{label:String(e.label??e.value),value:e.value}:{label:String(e),value:e});export const TestFieldRow=({field:e,value:t,onChange:n,valid:l,disabled:o,control:r,onInfo:i})=>{const{scales:s,rawValues:a,findTagByFqdn:u}=useContext(AutoCoreTagContext),p=effectiveField(e,s,{findTagByFqdn:u,rawValues:a}),m=p.unitOffset??0,c=l??fieldIsValid(p,t),d=!r&&Array.isArray(p.options)&&p.options.length>0?normalizeOptions(p.options):null,f=!r&&!d&&"string"!==p.type&&"bool"!==p.type,x=r??(d?_jsx(Dropdown,{value:t??null,options:d,onChange:e=>n(e.value),placeholder:`Select ${p.label??p.name}…`,className:"ac-dropdown-clearable"+(c?"":" p-invalid"),showClear:!p.required,disabled:o}):f?_jsx(ValueInput,{label:void 0,value:null!=t?Number(rawToDisplay(Number(t),p.scale,m)):null,min:p.min,max:p.max,onValueChanged:e=>n(displayToRaw(e,p.scale,m)),className:c?"":"p-invalid",disabled:o}):_jsx(TextInput,{label:void 0,value:null!=t?String(t):"",onValueChanged:e=>n(e),className:c?"":"p-invalid",disabled:o}));return _jsxs(React.Fragment,{children:[_jsx("span",{className:"ac-form-label",children:labelOf(p)}),x,i&&hasDescription(p)?_jsx(Button,{icon:"pi pi-info-circle",text:!0,rounded:!0,"aria-label":`About ${labelOf(p)}`,onClick:()=>i(p)}):_jsx("span",{"aria-hidden":"true"}),_jsx("span",{style:{color:c?"var(--green-500)":"var(--red-500)",display:"flex",alignItems:"center"},children:_jsx("i",{className:c?"pi pi-check":"pi pi-times"})})]})};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAA2C,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"TestFieldDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/tis-editor/editor/TestFieldDialog.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAA2C,MAAM,UAAU,CAAC;AAiDnF,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,SAAS,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACnC,8EAA8E;IAC9E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB;8CAC0C;IAC1C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;0EACsE;IACtE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAID,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CA4R1D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],AGG_FNS=[{label:"— none (analysis-fed) —",value:""},{label:"avg",value:"avg"},{label:"min",value:"min"},{label:"max",value:"max"},{label:"stddev",value:"stddev"},{label:"count (how many went into the rollup)",value:"count"}],AGG_FROMS=[{label:"cycle field (every cycle, every test)",value:"cycle_fields"},{label:"result (one value per test)",value:"results"}],COUNT_CHOICES=[{label:"cycles in this test",value:"test_cycles",scope:"test",from:"cycle_fields"},{label:"cycles for this sample, all tests",value:"project_cycles",scope:"project",from:"cycle_fields"},{label:"tests for this sample",value:"project_tests",scope:"project",from:"results"}],AGG_SCOPES=[{label:"this test only",value:"test"},{label:"this sample, every test in the project",value:"project"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:t,onCancel:a,onSave:l,siblingNames:
|
|
1
|
+
import{jsx as _jsx,Fragment as _Fragment,jsxs as _jsxs}from"react/jsx-runtime";import{useEffect,useState}from"react";import{Dialog}from"primereact/dialog";import{Button}from"primereact/button";import{InputText}from"primereact/inputtext";import{InputTextarea}from"primereact/inputtextarea";import{Dropdown}from"primereact/dropdown";import{Checkbox}from"primereact/checkbox";import{InputNumber}from"primereact/inputnumber";import{FormRow}from"../../forms/FormRow";import{boundText,parseBound,boundInvalid}from"../../tis/TestFieldRow";const FIELD_TYPES=[{label:"string",value:"string"},{label:"i32",value:"i32"},{label:"i64",value:"i64"},{label:"u32",value:"u32"},{label:"u64",value:"u64"},{label:"f32",value:"f32"},{label:"f64",value:"f64"},{label:"bool",value:"bool"}],AGG_FNS=[{label:"— none (analysis-fed) —",value:""},{label:"avg",value:"avg"},{label:"min",value:"min"},{label:"max",value:"max"},{label:"stddev",value:"stddev"},{label:"count (how many went into the rollup)",value:"count"}],AGG_FROMS=[{label:"cycle field (every cycle, every test)",value:"cycle_fields"},{label:"result (one value per test)",value:"results"}],COUNT_CHOICES=[{label:"cycles in this test",value:"test_cycles",scope:"test",from:"cycle_fields"},{label:"cycles for this sample, all tests",value:"project_cycles",scope:"project",from:"cycle_fields"},{label:"tests for this sample",value:"project_tests",scope:"project",from:"results"}],AGG_SCOPES=[{label:"this test only",value:"test"},{label:"this sample, every test in the project",value:"project"}],blank={name:"",type:"f32"};export const TestFieldDialog=({visible:e,initial:t,onCancel:a,onSave:l,siblingNames:r,isResultsField:o=!1,isCycleConfigField:n=!1,cycleFieldNames:i=[],resultsFieldNames:s=[]})=>{const[u,c]=useState(blank),[d,m]=useState(""),[h,p]=useState(null);useEffect(()=>{e&&(c(t?{...t}:{...blank}),m(null==t?.default?"":String(t.default)),p(null))},[e,t]);const g=u.aggregate,f=g?.from??"cycle_fields",v=g?.scope??"test",b=COUNT_CHOICES.find(e=>e.scope===v&&e.from===f)?.value??"test_cycles",x=("results"===f?s:i).filter(e=>e&&e!==u.name).map(e=>({label:e,value:e}));return _jsxs(Dialog,{header:t?`Edit field: ${t.name}`:"New field",visible:e,onHide:a,style:{width:"36rem"},children:[h&&_jsx("div",{style:{color:"#dc2626",marginBottom:"0.75rem"},children:h}),_jsx(FormRow,{label:"Name",required:!0,hint:"Wire-format key. Also the column name in CSV exports.",children:_jsx(InputText,{value:u.name,onChange:e=>c({...u,name:e.target.value})})}),_jsx(FormRow,{label:"Type",required:!0,children:_jsx(Dropdown,{value:u.type,options:FIELD_TYPES,onChange:e=>c({...u,type:e.value}),editable:!0})}),_jsx(FormRow,{label:"Units",hint:"Display label, appended to form labels (e.g. m/s).",children:_jsx(InputText,{value:u.units??"",onChange:e=>c({...u,units:e.target.value||void 0})})}),_jsx(FormRow,{label:"Label",hint:"Pretty form label. Falls back to name when empty.",children:_jsx(InputText,{value:u.label??"",onChange:e=>c({...u,label:e.target.value||void 0})})}),_jsx(FormRow,{label:"Required",hint:n?"Blocks the machine until the operator fills this in — tis.cycle_ready stays false and start_test is refused. This is what stops an unannotated row from being recorded.":void 0,children:_jsx(Checkbox,{checked:!!u.required,onChange:e=>c({...u,required:!!e.checked})})}),n&&_jsx(FormRow,{label:"Autoclear",hint:"Clear the operator's entry once a row has recorded it, so the next row can't inherit a stale value. Fires after each cycle under 'cycle' scope, at the end of the run under 'run' scope. Pair with Required — on its own it turns stale data into silently missing data.",children:_jsx(Checkbox,{checked:!!u.autoclear,onChange:e=>c({...u,autoclear:!!e.checked})})}),_jsx(FormRow,{label:"Source",hint:"Optional gm.* variable to bind this field to.",children:_jsx(InputText,{value:u.source??"",onChange:e=>c({...u,source:e.target.value||void 0}),placeholder:"gm.<variable_name>"})}),_jsx(FormRow,{label:"Scale",hint:"display = raw × scale. Leave blank for 1.0 (no conversion).",children:_jsx(InputNumber,{value:u.scale??null,onValueChange:e=>c({...u,scale:"number"==typeof e.value?e.value:void 0}),mode:"decimal",minFractionDigits:0,maxFractionDigits:9})}),_jsx(FormRow,{label:"Min",hint:"Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} — for a bound that moves, e.g. a load-cell force ceiling.",children:_jsx(InputText,{value:boundText(u.min),onChange:e=>c({...u,min:parseBound(e.target.value)}),className:boundInvalid(u.min)?"p-invalid":"",placeholder:"number or ${gm.…}"})}),_jsx(FormRow,{label:"Max",hint:"Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}.",children:_jsx(InputText,{value:boundText(u.max),onChange:e=>c({...u,max:parseBound(e.target.value)}),className:boundInvalid(u.max)?"p-invalid":"",placeholder:"number or ${gm.…}"})}),_jsx(FormRow,{label:"Default",hint:"Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.",children:_jsx(InputText,{value:d,onChange:e=>m(e.target.value),placeholder:"e.g. 5 or ${gm.safe_speed}"})}),o&&_jsxs(_Fragment,{children:[_jsx("div",{style:{borderTop:"1px solid #e5e7eb",margin:"0.75rem 0 0.25rem"}}),_jsx(FormRow,{label:"Aggregate",hint:"Cross-test rollup. When set, this result is derived server-side over the non-excluded tests sharing the same sample_id — the analysis script does not supply it.",children:_jsx(Dropdown,{value:g?.fn??"",options:AGG_FNS,onChange:e=>(e=>{if(""===e)return void c({...u,aggregate:void 0});if("count"===e)return void c({...u,aggregate:{fn:"count",from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"}});const t={fn:e,of:u.aggregate?.of,from:u.aggregate?.from??"cycle_fields",scope:u.aggregate?.scope??"test"};c({...u,aggregate:t})})(e.value)})}),g&&"count"===g.fn&&_jsx(FormRow,{label:"Count",hint:"What is being counted. A test's own cycles is the usual choice — it's the number the operator can see in the Cycle Data table.",children:_jsx(Dropdown,{value:b,options:COUNT_CHOICES,onChange:e=>{const t=COUNT_CHOICES.find(t=>t.value===e.value);t&&c({...u,aggregate:{...g,scope:t.scope,from:t.from}})}})}),g&&"count"!==g.fn&&_jsxs(_Fragment,{children:[_jsx(FormRow,{label:"Scope",hint:"How wide the reduction reaches. 'This test only' is the default: a value on a test's own results panel or report describes that test.",children:_jsx(Dropdown,{value:v,options:AGG_SCOPES,onChange:e=>c({...u,aggregate:{...g,scope:e.value}})})}),_jsx(FormRow,{label:"Aggregate from",hint:"Which per-test source the target field is read from.",children:_jsx(Dropdown,{value:f,options:AGG_FROMS,onChange:e=>c({...u,aggregate:{...g,from:e.value}})})}),_jsx(FormRow,{label:"Aggregate of",required:!0,hint:"The field to reduce across tests.",children:_jsx(Dropdown,{value:g.of??null,options:x,onChange:e=>c({...u,aggregate:{...g,of:e.value||void 0}}),editable:!0,placeholder:x.length?"Select a field":`No ${f} defined yet`})})]})]}),_jsx(FormRow,{label:"Description",hint:"Hover tooltip in the form.",children:_jsx(InputTextarea,{rows:2,value:u.description??"",onChange:e=>c({...u,description:e.target.value||void 0})})}),_jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:"0.5rem",marginTop:"1rem"},children:[_jsx(Button,{label:"Cancel",className:"p-button-text",onClick:a}),_jsx(Button,{label:"Save",onClick:()=>{const e={...u},a=(e=>{const t=e.trim();if(""!==t){if(t.startsWith("${"))return t;if("true"===t)return!0;if("false"===t)return!1;if(/^-?\d*\.?\d+$/.test(t)){const e=Number(t);if(Number.isFinite(e))return e}return t}})(d);void 0===a?delete e.default:e.default=a;const o=(e=>{if(!e.name.trim())return"Field name is required.";if(!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(e.name))return"Name must be a valid identifier (letters, digits, underscore; cannot start with a digit).";if(r.some(a=>a===e.name&&a!==t?.name))return`A field named "${e.name}" already exists in this array.`;if("number"==typeof e.min&&"number"==typeof e.max&&e.min>e.max)return`Min (${e.min}) cannot be greater than Max (${e.max}).`;for(const t of["min","max"])if(boundInvalid(e[t]))return`${"min"===t?"Min":"Max"} must be a number or an FQDN token like \${gm.some_var} — "${e[t]}" is neither.`;return e.aggregate&&"count"!==e.aggregate.fn&&!e.aggregate.of?.trim()?`Aggregate "${e.aggregate.fn}" needs a target field (the field to ${e.aggregate.fn} across tests).`:null})(e);o?p(o):l(e)}})]})]})};
|
|
@@ -19,8 +19,12 @@ export interface TestField {
|
|
|
19
19
|
/** Optional inclusive bounds for the operator's numeric value, authored
|
|
20
20
|
* in display units (same convention as `default`/`scale`). The form's
|
|
21
21
|
* numeric input rejects values outside `[min, max]`. */
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/** Inclusive bounds. A number is in display units; a string is an FQDN
|
|
23
|
+
* token `"${gm.<var>}"` resolved live from the machine — see
|
|
24
|
+
* `TestFieldRow.resolveBound`. The editor must preserve a token it does
|
|
25
|
+
* not understand rather than coercing it to a number. */
|
|
26
|
+
min?: number | string;
|
|
27
|
+
max?: number | string;
|
|
24
28
|
/** results_fields only: a cross-test rollup derived server-side over the
|
|
25
29
|
* non-excluded runs sharing this test's sample_id. When set, the field
|
|
26
30
|
* is NOT supplied by the analysis script — the server computes it. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,SAAS,GACf,QAAQ,GACR,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAC7B,KAAK,GAAG,KAAK,GACb,MAAM,CAAC;AAEb,MAAM,WAAW,SAAS;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;6DAEyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/tis-editor/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -105,6 +105,20 @@ export interface AmsContextValue {
|
|
|
105
105
|
listCalibrations: (assetId: string) => Promise<string[]>;
|
|
106
106
|
readCalibration: (assetId: string, calId: string) => Promise<any | null>;
|
|
107
107
|
readUsage: (assetId: string) => Promise<any | null>;
|
|
108
|
+
/**
|
|
109
|
+
* Declare which asset is INSTALLED at a role (`ams.assign_role`).
|
|
110
|
+
*
|
|
111
|
+
* Pass `null` for `assetId` to record that nothing is fitted. One RPC,
|
|
112
|
+
* not two `updateAsset` calls: the server vacates the role and installs
|
|
113
|
+
* the new asset atomically, so the role is never momentarily empty or
|
|
114
|
+
* double-claimed — `resolve_method_refs` takes the FIRST active asset at
|
|
115
|
+
* a location, so a double-claimed role resolves to an arbitrary one.
|
|
116
|
+
*
|
|
117
|
+
* Resolves to `null` on success, or the server's message on refusal
|
|
118
|
+
* (wrong asset type for the role, asset not active, unknown id) so the
|
|
119
|
+
* caller can show the operator WHY the swap was refused.
|
|
120
|
+
*/
|
|
121
|
+
assignRole: (location: string, assetId: string | null) => Promise<string | null>;
|
|
108
122
|
selection: AmsSelection;
|
|
109
123
|
setSelection: (patch: Partial<AmsSelection>) => void;
|
|
110
124
|
}
|
|
@@ -125,6 +139,7 @@ const AmsContext = createContext<AmsContextValue>({
|
|
|
125
139
|
listCalibrations: async () => [],
|
|
126
140
|
readCalibration: async () => null,
|
|
127
141
|
readUsage: async () => null,
|
|
142
|
+
assignRole: async () => 'AmsProvider is not mounted',
|
|
128
143
|
selection: { assetType: null, assetId: null },
|
|
129
144
|
setSelection: () => {},
|
|
130
145
|
});
|
|
@@ -257,6 +272,25 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
|
|
|
257
272
|
} catch { return null; }
|
|
258
273
|
}, [invoke]);
|
|
259
274
|
|
|
275
|
+
const assignRole = useCallback(async (location: string, assetId: string | null): Promise<string | null> => {
|
|
276
|
+
try {
|
|
277
|
+
const resp: any = await invoke('ams.assign_role' as any, MessageType.Request, {
|
|
278
|
+
location, asset_id: assetId,
|
|
279
|
+
} as any);
|
|
280
|
+
if (resp?.success) {
|
|
281
|
+
// The server broadcasts ams.asset_changed for every asset it
|
|
282
|
+
// touched, which already triggers a refresh — but the refresh
|
|
283
|
+
// is awaited here too so a caller that re-renders off `assets`
|
|
284
|
+
// sees the new occupant by the time this resolves.
|
|
285
|
+
await refreshAssets();
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return resp?.error_message || 'ams.assign_role failed';
|
|
289
|
+
} catch (e: any) {
|
|
290
|
+
return String(e?.message ?? e);
|
|
291
|
+
}
|
|
292
|
+
}, [invoke, refreshAssets]);
|
|
293
|
+
|
|
260
294
|
const value = useMemo<AmsContextValue>(() => ({
|
|
261
295
|
schemas,
|
|
262
296
|
schemasLoaded,
|
|
@@ -269,9 +303,10 @@ export const AmsProvider: React.FC<AmsProviderProps> = ({ children }) => {
|
|
|
269
303
|
listCalibrations,
|
|
270
304
|
readCalibration,
|
|
271
305
|
readUsage,
|
|
306
|
+
assignRole,
|
|
272
307
|
selection,
|
|
273
308
|
setSelection,
|
|
274
|
-
}), [schemas, schemasLoaded, roles, rolesLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, selection, setSelection]);
|
|
309
|
+
}), [schemas, schemasLoaded, roles, rolesLoaded, alerts, assets, refreshAssets, readAsset, listCalibrations, readCalibration, readUsage, assignRole, selection, setSelection]);
|
|
275
310
|
|
|
276
311
|
return <AmsContext.Provider value={value}>{children}</AmsContext.Provider>;
|
|
277
312
|
};
|
|
@@ -16,6 +16,7 @@ import { MessageType } from '../../hub/CommandMessage';
|
|
|
16
16
|
import { useAms } from './AmsProvider';
|
|
17
17
|
import { CalibrationEntryDialog } from './CalibrationEntryDialog';
|
|
18
18
|
import { AssetEditDialog } from './AssetEditDialog';
|
|
19
|
+
import { AssetNameplateGrid } from './AssetNameplateGrid';
|
|
19
20
|
|
|
20
21
|
export const AssetDetailView: React.FC = () => {
|
|
21
22
|
const { selection, schemas, roles, readAsset, listCalibrations, readCalibration, readUsage,
|
|
@@ -189,26 +190,7 @@ export const AssetDetailView: React.FC = () => {
|
|
|
189
190
|
{schemaFields.length > 0 && (
|
|
190
191
|
<div>
|
|
191
192
|
<h4 style={{ margin: '0 0 0.5rem 0' }}>Nameplate</h4>
|
|
192
|
-
<
|
|
193
|
-
gap: '0.25rem 1rem', alignItems: 'baseline',
|
|
194
|
-
fontSize: '0.9rem' }}>
|
|
195
|
-
{schemaFields.map((f: any) => {
|
|
196
|
-
const v = custom[f.name];
|
|
197
|
-
const present = v !== undefined && v !== null && v !== '';
|
|
198
|
-
const label = f.label ?? f.name;
|
|
199
|
-
const display = present
|
|
200
|
-
? (f.units ? `${v} ${f.units}` : String(v))
|
|
201
|
-
: '(not set)';
|
|
202
|
-
return (
|
|
203
|
-
<React.Fragment key={f.name}>
|
|
204
|
-
<strong title={f.description ?? undefined}>{label}</strong>
|
|
205
|
-
<span style={{ color: present ? undefined : '#f59e0b' }}>
|
|
206
|
-
{display}
|
|
207
|
-
</span>
|
|
208
|
-
</React.Fragment>
|
|
209
|
-
);
|
|
210
|
-
})}
|
|
211
|
-
</div>
|
|
193
|
+
<AssetNameplateGrid schemaFields={schemaFields} custom={custom} />
|
|
212
194
|
</div>
|
|
213
195
|
)}
|
|
214
196
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <AssetNameplateGrid> — the nameplate field/value grid for one asset.
|
|
5
|
+
*
|
|
6
|
+
* Extracted so <AssetDetailView> (the full record) and
|
|
7
|
+
* <InstalledAssetPicker> (what is fitted right now) render a nameplate the
|
|
8
|
+
* same way. A field the schema declares but the record has not filled in is
|
|
9
|
+
* drawn as an amber "(not set)" rather than blank space: that is exactly the
|
|
10
|
+
* state the placeholder resolver refuses to start a module on, so it is worth
|
|
11
|
+
* drawing the eye to.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
|
|
16
|
+
export interface AssetNameplateGridProps {
|
|
17
|
+
/** The asset_type schema's `fields` array. */
|
|
18
|
+
schemaFields: any[];
|
|
19
|
+
/** The asset record's `custom` object (nameplate values). */
|
|
20
|
+
custom: Record<string, any>;
|
|
21
|
+
/** Only render these field names, in this order. Omit for all of them. */
|
|
22
|
+
only?: string[];
|
|
23
|
+
/** Font size for the grid; the detail view runs slightly larger. */
|
|
24
|
+
fontSize?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AssetNameplateGrid: React.FC<AssetNameplateGridProps> = ({
|
|
28
|
+
schemaFields,
|
|
29
|
+
custom,
|
|
30
|
+
only,
|
|
31
|
+
fontSize = '0.9rem',
|
|
32
|
+
}) => {
|
|
33
|
+
const fields = only
|
|
34
|
+
? only
|
|
35
|
+
.map(name => schemaFields.find((f: any) => f.name === name))
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
: schemaFields;
|
|
38
|
+
|
|
39
|
+
if (fields.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
|
|
43
|
+
gap: '0.25rem 1rem', alignItems: 'baseline', fontSize }}>
|
|
44
|
+
{fields.map((f: any) => {
|
|
45
|
+
const v = custom[f.name];
|
|
46
|
+
const present = v !== undefined && v !== null && v !== '';
|
|
47
|
+
const label = f.label ?? f.name;
|
|
48
|
+
const display = present
|
|
49
|
+
? (f.units ? `${v} ${f.units}` : String(v))
|
|
50
|
+
: '(not set)';
|
|
51
|
+
return (
|
|
52
|
+
<React.Fragment key={f.name}>
|
|
53
|
+
<strong title={f.description ?? undefined}>{label}</strong>
|
|
54
|
+
<span style={{ color: present ? undefined : '#f59e0b' }}>
|
|
55
|
+
{display}
|
|
56
|
+
</span>
|
|
57
|
+
</React.Fragment>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* <InstalledAssetPicker> — "which asset is fitted in this role right now?"
|
|
5
|
+
*
|
|
6
|
+
* The operator-facing answer to a swap. A machine with a 2 kN and a 20 kN load
|
|
7
|
+
* cell in the drawer has ONE of them bolted to the press; this picker is where
|
|
8
|
+
* they say which, and everything downstream — the test record's asset
|
|
9
|
+
* snapshot, module placeholders, the control program's AssetWatch — follows
|
|
10
|
+
* from that one statement.
|
|
11
|
+
*
|
|
12
|
+
* It writes through `ams.assign_role`, never two `ams.update_asset` calls: the
|
|
13
|
+
* server vacates the role and installs the replacement atomically, because
|
|
14
|
+
* `resolve_method_refs` takes the FIRST active asset it finds at a location and
|
|
15
|
+
* a momentarily double-claimed role would therefore resolve to an arbitrary one.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately NOT a nameplate editor. The nameplate belongs to the asset
|
|
18
|
+
* record (Assets tab / <AssetDetailView>) — one source of truth. This picker
|
|
19
|
+
* only chooses which record is in force, and shows what that record says so the
|
|
20
|
+
* operator can confirm they grabbed the right hardware before pressing on.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
24
|
+
import { Dropdown } from 'primereact/dropdown';
|
|
25
|
+
import { Button } from 'primereact/button';
|
|
26
|
+
import { Dialog } from 'primereact/dialog';
|
|
27
|
+
import { Message } from 'primereact/message';
|
|
28
|
+
import { AssetNameplateGrid } from './AssetNameplateGrid';
|
|
29
|
+
import { useInstalledAsset } from './useInstalledAsset';
|
|
30
|
+
|
|
31
|
+
export interface InstalledAssetPickerProps {
|
|
32
|
+
/** Asset type the role takes, e.g. `"load_cell"`. */
|
|
33
|
+
assetType: string;
|
|
34
|
+
/** The role's `location` string, e.g. `"press_load_cell"`. */
|
|
35
|
+
location: string;
|
|
36
|
+
/**
|
|
37
|
+
* Heading for the picker. Defaults to the role's label from
|
|
38
|
+
* `ams.list_roles`, then to the raw location.
|
|
39
|
+
*/
|
|
40
|
+
label?: string;
|
|
41
|
+
/** One line under the dropdown explaining what this role drives. */
|
|
42
|
+
description?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Nameplate field names folded into each option's text, so the operator can
|
|
45
|
+
* tell two assets apart without opening them (e.g.
|
|
46
|
+
* `["capacity", "capacity_units"]` → "2000 N"). Serial is always shown.
|
|
47
|
+
*/
|
|
48
|
+
summaryFields?: string[];
|
|
49
|
+
/** Render the fitted asset's full nameplate below the dropdown. */
|
|
50
|
+
showNameplate?: boolean;
|
|
51
|
+
/** Render the fitted asset's calibration state below the nameplate. */
|
|
52
|
+
showCalibration?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Block the swap. Use for "a test is running" — changing the fitted asset
|
|
55
|
+
* mid-run would re-scale a channel under the data being recorded.
|
|
56
|
+
*/
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
/** Why it is blocked. Shown in place of the help text. */
|
|
59
|
+
disabledReason?: string;
|
|
60
|
+
/** Fired after a successful assignment with the new asset id (or null). */
|
|
61
|
+
onAssigned?: (assetId: string | null) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Option value standing for "nothing is fitted". */
|
|
65
|
+
const NONE = '__none__';
|
|
66
|
+
|
|
67
|
+
export const InstalledAssetPicker: React.FC<InstalledAssetPickerProps> = ({
|
|
68
|
+
assetType,
|
|
69
|
+
location,
|
|
70
|
+
label,
|
|
71
|
+
description,
|
|
72
|
+
summaryFields = [],
|
|
73
|
+
showNameplate = true,
|
|
74
|
+
showCalibration = true,
|
|
75
|
+
disabled = false,
|
|
76
|
+
disabledReason,
|
|
77
|
+
onAssigned,
|
|
78
|
+
}) => {
|
|
79
|
+
const {
|
|
80
|
+
candidates, records, installed, installedRecord,
|
|
81
|
+
calibration, calibrationOverdue, role, schemaFields, assign,
|
|
82
|
+
} = useInstalledAsset(assetType, location);
|
|
83
|
+
|
|
84
|
+
const [error, setError] = useState<string | null>(null);
|
|
85
|
+
const [busy, setBusy] = useState(false);
|
|
86
|
+
/** Non-null while the confirm dialog is open; carries the pending choice. */
|
|
87
|
+
const [pending, setPending] = useState<{ assetId: string | null } | null>(null);
|
|
88
|
+
|
|
89
|
+
const roleLabel = label ?? role?.label ?? location;
|
|
90
|
+
const roleHelp = description ?? role?.description ?? null;
|
|
91
|
+
|
|
92
|
+
/** "SN 4471-A — 2000 N" — enough to tell two assets apart in a list. */
|
|
93
|
+
const optionText = useCallback((assetId: string): string => {
|
|
94
|
+
const entry = candidates.find(c => c.asset_id === assetId);
|
|
95
|
+
const custom = (records[assetId]?.custom ?? {}) as Record<string, any>;
|
|
96
|
+
const bits: string[] = [entry?.serial ? `SN ${entry.serial}` : assetId];
|
|
97
|
+
const summary = summaryFields
|
|
98
|
+
.map(name => custom[name])
|
|
99
|
+
.filter(v => v !== undefined && v !== null && v !== '')
|
|
100
|
+
.join(' ');
|
|
101
|
+
if (summary) bits.push(summary);
|
|
102
|
+
return bits.join(' — ');
|
|
103
|
+
}, [candidates, records, summaryFields]);
|
|
104
|
+
|
|
105
|
+
const options = useMemo(() => [
|
|
106
|
+
...candidates.map(c => ({ label: optionText(c.asset_id), value: c.asset_id })),
|
|
107
|
+
{ label: '— nothing fitted —', value: NONE },
|
|
108
|
+
], [candidates, optionText]);
|
|
109
|
+
|
|
110
|
+
const commit = async (assetId: string | null) => {
|
|
111
|
+
setBusy(true);
|
|
112
|
+
setError(null);
|
|
113
|
+
const err = await assign(assetId);
|
|
114
|
+
setBusy(false);
|
|
115
|
+
setPending(null);
|
|
116
|
+
if (err) { setError(err); return; }
|
|
117
|
+
onAssigned?.(assetId);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const calExpiry = calibration?.expires_at ? new Date(calibration.expires_at) : null;
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
124
|
+
<div className="p-inputgroup">
|
|
125
|
+
<span className="p-inputgroup-addon" style={{ flexGrow: 1 }}>
|
|
126
|
+
{roleLabel}
|
|
127
|
+
</span>
|
|
128
|
+
<Dropdown
|
|
129
|
+
value={installed?.asset_id ?? NONE}
|
|
130
|
+
options={options}
|
|
131
|
+
disabled={disabled || busy}
|
|
132
|
+
onChange={(e) => {
|
|
133
|
+
const next = e.value === NONE ? null : (e.value as string);
|
|
134
|
+
if (next === (installed?.asset_id ?? null)) return;
|
|
135
|
+
setPending({ assetId: next });
|
|
136
|
+
}}
|
|
137
|
+
placeholder="Select the fitted asset"
|
|
138
|
+
style={{ minWidth: '18rem' }}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Help text, or the reason the operator can't change it. On screen
|
|
143
|
+
rather than in a hover tooltip — this runs on a touchscreen,
|
|
144
|
+
where a tooltip is unreachable. */}
|
|
145
|
+
{disabled && disabledReason
|
|
146
|
+
? <small style={{ color: '#f59e0b' }}>{disabledReason}</small>
|
|
147
|
+
: roleHelp && <small style={{ color: '#9ca3af' }}>{roleHelp}</small>}
|
|
148
|
+
|
|
149
|
+
{candidates.length === 0 && (
|
|
150
|
+
<Message severity="warn"
|
|
151
|
+
text={`No active ${assetType.replace(/_/g, ' ')} is registered. Add one on the Assets screen first.`} />
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{!installed && candidates.length > 0 && (
|
|
155
|
+
<Message severity="warn"
|
|
156
|
+
text="Nothing is recorded as fitted in this role. Tests that need it will refuse to start." />
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{error && <Message severity="error" text={error} />}
|
|
160
|
+
|
|
161
|
+
{installed && showNameplate && (
|
|
162
|
+
<AssetNameplateGrid
|
|
163
|
+
schemaFields={schemaFields}
|
|
164
|
+
custom={(installedRecord?.custom ?? {}) as Record<string, any>}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{installed && showCalibration && (
|
|
169
|
+
<small style={{ color: calibrationOverdue ? '#f59e0b' : '#9ca3af' }}>
|
|
170
|
+
{!installed.current_calibration_id
|
|
171
|
+
? 'No calibration on record for this asset.'
|
|
172
|
+
: calibrationOverdue
|
|
173
|
+
? `Calibration ${installed.current_calibration_id} expired ${calExpiry!.toLocaleDateString()}.`
|
|
174
|
+
: `Calibration ${installed.current_calibration_id}${
|
|
175
|
+
calExpiry ? `, in date until ${calExpiry.toLocaleDateString()}` : ''}.`}
|
|
176
|
+
</small>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{/* Confirm, because this restates what hardware is bolted to the
|
|
180
|
+
machine: downstream it re-scales a measurement channel and is
|
|
181
|
+
frozen into every test record started afterwards. */}
|
|
182
|
+
<Dialog
|
|
183
|
+
visible={pending !== null}
|
|
184
|
+
header={`Change the ${roleLabel}?`}
|
|
185
|
+
modal
|
|
186
|
+
onHide={() => setPending(null)}
|
|
187
|
+
footer={
|
|
188
|
+
<>
|
|
189
|
+
<Button label="Cancel" outlined onClick={() => setPending(null)} />
|
|
190
|
+
<Button label="Confirm" loading={busy}
|
|
191
|
+
onClick={() => { void commit(pending?.assetId ?? null); }} />
|
|
192
|
+
</>
|
|
193
|
+
}
|
|
194
|
+
>
|
|
195
|
+
<p style={{ margin: 0 }}>
|
|
196
|
+
{pending?.assetId
|
|
197
|
+
? <>Record <strong>{optionText(pending.assetId)}</strong> as the asset fitted
|
|
198
|
+
in <strong>{roleLabel}</strong>.</>
|
|
199
|
+
: <>Record that <strong>nothing</strong> is fitted in <strong>{roleLabel}</strong>.</>}
|
|
200
|
+
</p>
|
|
201
|
+
<p style={{ marginBottom: 0, color: '#9ca3af' }}>
|
|
202
|
+
Confirm only if this matches the hardware actually installed. The
|
|
203
|
+
machine reconfigures the channel from this asset's nameplate, and
|
|
204
|
+
every test started from now on records it.
|
|
205
|
+
</p>
|
|
206
|
+
</Dialog>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
@@ -14,3 +14,10 @@ export { AssetEditDialog } from './AssetEditDialog';
|
|
|
14
14
|
export { SubLocationPicker } from './SubLocationPicker';
|
|
15
15
|
export { PlaceholderHealthPanel } from './PlaceholderHealthPanel';
|
|
16
16
|
export { MissingAssetsBanner } from './MissingAssetsBanner';
|
|
17
|
+
// "What is fitted right now" — the operator's swap surface, and the
|
|
18
|
+
// nameplate grid it shares with <AssetDetailView>.
|
|
19
|
+
export { InstalledAssetPicker } from './InstalledAssetPicker';
|
|
20
|
+
export type { InstalledAssetPickerProps } from './InstalledAssetPicker';
|
|
21
|
+
export { AssetNameplateGrid } from './AssetNameplateGrid';
|
|
22
|
+
export { useInstalledAsset } from './useInstalledAsset';
|
|
23
|
+
export type { InstalledAssetState } from './useInstalledAsset';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Automated Design Corp. All Rights Reserved.
|
|
3
|
+
*
|
|
4
|
+
* useInstalledAsset — "what is fitted in this role, and what does its record
|
|
5
|
+
* say?" for one (assetType, location) pair.
|
|
6
|
+
*
|
|
7
|
+
* The registry cache in <AmsProvider> carries only identity (id, serial,
|
|
8
|
+
* status, location); anything that has to show or act on a NAMEPLATE needs the
|
|
9
|
+
* full record. This hook is the one place that fetch happens, so
|
|
10
|
+
* <InstalledAssetPicker> and a product's own panel (e.g. a load-channel
|
|
11
|
+
* verification table comparing the nameplate against what the hardware
|
|
12
|
+
* actually holds) read the same data instead of each querying for it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
16
|
+
import { useAms, type AmsAssetEntry, type AmsRole } from './AmsProvider';
|
|
17
|
+
|
|
18
|
+
export interface InstalledAssetState {
|
|
19
|
+
/** Every ACTIVE asset of this type — the assets that could be fitted. */
|
|
20
|
+
candidates: AmsAssetEntry[];
|
|
21
|
+
/** Full records for `candidates`, keyed by asset_id. */
|
|
22
|
+
records: Record<string, any>;
|
|
23
|
+
/** The registry entry fitted in this role, or null when nothing is. */
|
|
24
|
+
installed: AmsAssetEntry | null;
|
|
25
|
+
/** The fitted asset's full record; nameplate values live under `.custom`. */
|
|
26
|
+
installedRecord: any | null;
|
|
27
|
+
/** The fitted asset's current calibration record, or null. */
|
|
28
|
+
calibration: any | null;
|
|
29
|
+
/** True once the calibration on record has passed its `expires_at`. */
|
|
30
|
+
calibrationOverdue: boolean;
|
|
31
|
+
/** This role's metadata from `ams.list_roles`, when the project declares it. */
|
|
32
|
+
role: AmsRole | null;
|
|
33
|
+
/** The asset_type schema's nameplate `fields` array (empty if unknown). */
|
|
34
|
+
schemaFields: any[];
|
|
35
|
+
/**
|
|
36
|
+
* Declare what is fitted (`null` = nothing). Resolves to `null` on success
|
|
37
|
+
* or the server's refusal message.
|
|
38
|
+
*/
|
|
39
|
+
assign: (assetId: string | null) => Promise<string | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useInstalledAsset(assetType: string, location: string): InstalledAssetState {
|
|
43
|
+
const { assets, schemas, roles, readAsset, readCalibration, assignRole } = useAms();
|
|
44
|
+
|
|
45
|
+
const [records, setRecords] = useState<Record<string, any>>({});
|
|
46
|
+
const [calibration, setCalibration] = useState<any | null>(null);
|
|
47
|
+
|
|
48
|
+
const candidates = useMemo(
|
|
49
|
+
() => assets.filter(a => a.asset_type === assetType && a.status === 'active'),
|
|
50
|
+
[assets, assetType],
|
|
51
|
+
);
|
|
52
|
+
const installed = useMemo(
|
|
53
|
+
() => candidates.find(a => a.location === location) ?? null,
|
|
54
|
+
[candidates, location],
|
|
55
|
+
);
|
|
56
|
+
const role = useMemo(
|
|
57
|
+
() => (roles[assetType] ?? []).find(r => r.location === location) ?? null,
|
|
58
|
+
[roles, assetType, location],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Fetch the full record for every candidate. Keyed on the id list, so a
|
|
62
|
+
// registry refresh that left the candidate set alone does no I/O.
|
|
63
|
+
const candidateKey = candidates.map(c => c.asset_id).join(',');
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
let cancelled = false;
|
|
66
|
+
(async () => {
|
|
67
|
+
const next: Record<string, any> = {};
|
|
68
|
+
for (const c of candidates) {
|
|
69
|
+
const rec = await readAsset(c.asset_id);
|
|
70
|
+
if (cancelled) return;
|
|
71
|
+
if (rec) next[c.asset_id] = rec;
|
|
72
|
+
}
|
|
73
|
+
if (!cancelled) setRecords(next);
|
|
74
|
+
})();
|
|
75
|
+
return () => { cancelled = true; };
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [candidateKey, readAsset]);
|
|
78
|
+
|
|
79
|
+
// The fitted asset's current calibration, for the "is it in date?" line.
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
let cancelled = false;
|
|
82
|
+
(async () => {
|
|
83
|
+
if (!installed?.current_calibration_id) { setCalibration(null); return; }
|
|
84
|
+
const cal = await readCalibration(installed.asset_id, installed.current_calibration_id);
|
|
85
|
+
if (!cancelled) setCalibration(cal);
|
|
86
|
+
})();
|
|
87
|
+
return () => { cancelled = true; };
|
|
88
|
+
}, [installed?.asset_id, installed?.current_calibration_id, readCalibration]);
|
|
89
|
+
|
|
90
|
+
const assign = useCallback(
|
|
91
|
+
(assetId: string | null) => assignRole(location, assetId),
|
|
92
|
+
[assignRole, location],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const expiry = calibration?.expires_at ? new Date(calibration.expires_at) : null;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
candidates,
|
|
99
|
+
records,
|
|
100
|
+
installed,
|
|
101
|
+
installedRecord: installed ? (records[installed.asset_id] ?? null) : null,
|
|
102
|
+
calibration,
|
|
103
|
+
calibrationOverdue: !!expiry && expiry.getTime() < Date.now(),
|
|
104
|
+
role,
|
|
105
|
+
schemaFields: Array.isArray(schemas[assetType]?.fields) ? schemas[assetType].fields : [],
|
|
106
|
+
assign,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -181,6 +181,37 @@ export interface TagLookup {
|
|
|
181
181
|
/** Matches a bound token like `${gm.press_force_min_n}` (whole-string). */
|
|
182
182
|
const BOUND_TOKEN_RE = /^\$\{\s*([^}]+?)\s*\}$/;
|
|
183
183
|
|
|
184
|
+
/** Is this string a well-formed FQDN bound token? */
|
|
185
|
+
export const isBoundToken = (s: string): boolean => BOUND_TOKEN_RE.test(s.trim());
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Render a bound for an authoring text field — the method editor's Min/Max.
|
|
189
|
+
*
|
|
190
|
+
* A text field, not a numeric one, because a bound is a number OR a token. The
|
|
191
|
+
* editor used `InputNumber`, which rendered a token as **NaN** and — worse —
|
|
192
|
+
* coerced anything non-numeric to `undefined` on change, so opening the dialog
|
|
193
|
+
* and saving silently deleted a token bound.
|
|
194
|
+
*/
|
|
195
|
+
export const boundText = (b: number | string | undefined): string =>
|
|
196
|
+
b === undefined || b === null ? '' : String(b);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Parse what an author typed into a bound field. Blank clears it; a finite
|
|
200
|
+
* number stores a number; anything else is kept VERBATIM so a half-typed token
|
|
201
|
+
* survives the keystroke. Use `boundInvalid` to flag what will not resolve.
|
|
202
|
+
*/
|
|
203
|
+
export const parseBound = (text: string): number | string | undefined => {
|
|
204
|
+
const t = text.trim();
|
|
205
|
+
if (t === '') return undefined;
|
|
206
|
+
const n = Number(t);
|
|
207
|
+
if (Number.isFinite(n)) return n;
|
|
208
|
+
return t;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** True when a bound is a string that is not a well-formed token. */
|
|
212
|
+
export const boundInvalid = (b: number | string | undefined): boolean =>
|
|
213
|
+
typeof b === 'string' && !isBoundToken(b);
|
|
214
|
+
|
|
184
215
|
/**
|
|
185
216
|
* A declared bound resolved to a plain DISPLAY-unit number, or `undefined` for
|
|
186
217
|
* "no bound".
|
|
@@ -8,6 +8,10 @@ import { Checkbox } from 'primereact/checkbox';
|
|
|
8
8
|
import { InputNumber } from 'primereact/inputnumber';
|
|
9
9
|
import { FormRow } from '../../forms/FormRow';
|
|
10
10
|
import type { TestField, AggFn, AggFrom, AggScope, AggregateSpec } from '../types';
|
|
11
|
+
// One home for bound parsing/formatting — shared with the operator-facing
|
|
12
|
+
// form so the editor and the runtime can never disagree on what a bound is.
|
|
13
|
+
import { boundText, parseBound, boundInvalid } from '../../tis/TestFieldRow';
|
|
14
|
+
|
|
11
15
|
|
|
12
16
|
const FIELD_TYPES: { label: string; value: string }[] = [
|
|
13
17
|
{ label: 'string', value: 'string' },
|
|
@@ -116,6 +120,13 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
116
120
|
if (typeof f.min === 'number' && typeof f.max === 'number' && f.min > f.max) {
|
|
117
121
|
return `Min (${f.min}) cannot be greater than Max (${f.max}).`;
|
|
118
122
|
}
|
|
123
|
+
// A bound that is neither a number nor a well-formed token resolves to
|
|
124
|
+
// NO bound at run time — silently unlimited. Refuse the save instead.
|
|
125
|
+
for (const which of ['min', 'max'] as const) {
|
|
126
|
+
if (boundInvalid(f[which])) {
|
|
127
|
+
return `${which === 'min' ? 'Min' : 'Max'} must be a number or an FQDN token like \${gm.some_var} — "${f[which]}" is neither.`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
119
130
|
if (f.aggregate && f.aggregate.fn !== 'count' && !f.aggregate.of?.trim()) {
|
|
120
131
|
return `Aggregate "${f.aggregate.fn}" needs a target field (the field to ${f.aggregate.fn} across tests).`;
|
|
121
132
|
}
|
|
@@ -241,26 +252,26 @@ export const TestFieldDialog: React.FC<TestFieldDialogProps> = ({
|
|
|
241
252
|
maxFractionDigits={9}
|
|
242
253
|
/>
|
|
243
254
|
</FormRow>
|
|
244
|
-
<FormRow
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
255
|
+
<FormRow
|
|
256
|
+
label="Min"
|
|
257
|
+
hint="Optional lower bound the operator can enter (display units). Blank = no minimum. Or bind it to the machine with an FQDN token like ${gm.press_force_min_n} — for a bound that moves, e.g. a load-cell force ceiling."
|
|
258
|
+
>
|
|
259
|
+
<InputText
|
|
260
|
+
value={boundText(draft.min)}
|
|
261
|
+
onChange={(e) => setDraft({ ...draft, min: parseBound(e.target.value) })}
|
|
262
|
+
className={boundInvalid(draft.min) ? 'p-invalid' : ''}
|
|
263
|
+
placeholder="number or ${gm.…}"
|
|
253
264
|
/>
|
|
254
265
|
</FormRow>
|
|
255
|
-
<FormRow
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
266
|
+
<FormRow
|
|
267
|
+
label="Max"
|
|
268
|
+
hint="Optional upper bound the operator can enter (display units). Blank = no maximum. Or bind it to the machine with an FQDN token like ${gm.press_force_max_n}."
|
|
269
|
+
>
|
|
270
|
+
<InputText
|
|
271
|
+
value={boundText(draft.max)}
|
|
272
|
+
onChange={(e) => setDraft({ ...draft, max: parseBound(e.target.value) })}
|
|
273
|
+
className={boundInvalid(draft.max) ? 'p-invalid' : ''}
|
|
274
|
+
placeholder="number or ${gm.…}"
|
|
264
275
|
/>
|
|
265
276
|
</FormRow>
|
|
266
277
|
<FormRow label="Default" hint="Applied every time the method loads (operator can override). A literal (display units) like 5, or an FQDN token like ${gm.safe_speed} that snapshots that value on load.">
|
|
@@ -25,8 +25,12 @@ export interface TestField {
|
|
|
25
25
|
/** Optional inclusive bounds for the operator's numeric value, authored
|
|
26
26
|
* in display units (same convention as `default`/`scale`). The form's
|
|
27
27
|
* numeric input rejects values outside `[min, max]`. */
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/** Inclusive bounds. A number is in display units; a string is an FQDN
|
|
29
|
+
* token `"${gm.<var>}"` resolved live from the machine — see
|
|
30
|
+
* `TestFieldRow.resolveBound`. The editor must preserve a token it does
|
|
31
|
+
* not understand rather than coercing it to a number. */
|
|
32
|
+
min?: number | string;
|
|
33
|
+
max?: number | string;
|
|
30
34
|
/** results_fields only: a cross-test rollup derived server-side over the
|
|
31
35
|
* non-excluded runs sharing this test's sample_id. When set, the field
|
|
32
36
|
* is NOT supplied by the analysis script — the server computes it. */
|