@adcops/autocore-react 3.3.65 → 3.3.67
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 +27 -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/AssetRegistryTable.d.ts.map +1 -1
- package/dist/components/ams/AssetRegistryTable.js +1 -1
- package/dist/components/ams/index.d.ts +2 -1
- package/dist/components/ams/index.d.ts.map +1 -1
- package/dist/components/ams/index.js +1 -1
- package/package.json +1 -1
- package/src/components/ams/AmsProvider.tsx +51 -2
- package/src/components/ams/AssetDetailView.tsx +9 -2
- package/src/components/ams/AssetRegistryTable.tsx +172 -13
- package/src/components/ams/index.ts +2 -1
|
@@ -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;
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|
|
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,
|
|
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;
|
|
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";
|
package/package.json
CHANGED
|
@@ -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>
|
|
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 = {
|
|
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="
|
|
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={
|
|
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) =>
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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';
|