@adcops/autocore-react 3.3.19 → 3.3.25
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/ResultHistoryTable.d.ts +7 -0
- package/dist/components/ResultHistoryTable.d.ts.map +1 -0
- package/dist/components/ResultHistoryTable.js +1 -0
- package/dist/components/TestSetupForm.d.ts +24 -0
- package/dist/components/TestSetupForm.d.ts.map +1 -0
- package/dist/components/TestSetupForm.js +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useServeletData.d.ts +36 -0
- package/dist/hooks/useServeletData.d.ts.map +1 -0
- package/dist/hooks/useServeletData.js +1 -0
- package/package.json +2 -2
- package/src/components/ResultHistoryTable.tsx +61 -0
- package/src/components/TestSetupForm.tsx +192 -0
- package/src/components/index.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useServeletData.ts +95 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResultHistoryTable.d.ts","sourceRoot":"","sources":["../../src/components/ResultHistoryTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAM/D,MAAM,WAAW,uBAAuB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAiDhE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsxs as _jsxs,jsx as _jsx}from"react/jsx-runtime";import React,{useState,useEffect,useContext}from"react";import{DataTable}from"primereact/datatable";import{Column}from"primereact/column";import{Button}from"primereact/button";import{EventEmitterContext}from"../core/EventEmitterContext";export const ResultHistoryTable=({projectId:t,definitionId:e})=>{const[s,r]=useState([]),[i,o]=useState(!1),{invoke:n}=useContext(EventEmitterContext),a=async()=>{o(!0);try{const s=await n("results.list_tests",{project_id:t,definition_id:e});s.success&&s.data&&s.data.tests&&r(s.data.tests)}catch(t){}o(!1)};useEffect(()=>{a()},[t,e]);return _jsxs("div",{children:[_jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:"1rem"},children:[_jsxs("h3",{children:["Test History: ",e]}),_jsx(Button,{icon:"pi pi-refresh",label:"Refresh",onClick:a,disabled:i})]}),_jsxs(DataTable,{value:s,loading:i,paginator:!0,rows:10,emptyMessage:"No tests found.",children:[_jsx(Column,{field:"run_id",header:"Run ID",sortable:!0}),_jsx(Column,{field:"start_time",header:"Date/Time",body:t=>{return(e=t.start_time)?new Date(e).toLocaleString():"";var e},sortable:!0}),_jsx(Column,{header:"Config / Results",body:t=>_jsxs("div",{style:{fontSize:"0.85em",color:"var(--text-secondary-color)"},children:[_jsxs("div",{children:[_jsx("strong",{children:"Config:"})," ",JSON.stringify(t.config)]}),_jsxs("div",{children:[_jsx("strong",{children:"Results:"})," ",JSON.stringify(t.results)]})]})})]})]})};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface TestFieldDef {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
units?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
source?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TestDefinition {
|
|
10
|
+
project_fields: TestFieldDef[];
|
|
11
|
+
config_fields: TestFieldDef[];
|
|
12
|
+
cycle_fields: TestFieldDef[];
|
|
13
|
+
results_fields: TestFieldDef[];
|
|
14
|
+
}
|
|
15
|
+
export interface TestSetupFormProps {
|
|
16
|
+
schema: TestDefinition;
|
|
17
|
+
defaultProjectId?: string;
|
|
18
|
+
defaultDefinitionId?: string;
|
|
19
|
+
onProjectChange?: (projectId: string) => void;
|
|
20
|
+
onDefinitionChange?: (definitionId: string) => void;
|
|
21
|
+
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare const TestSetupForm: React.FC<TestSetupFormProps>;
|
|
24
|
+
//# sourceMappingURL=TestSetupForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TestSetupForm.d.ts","sourceRoot":"","sources":["../../src/components/TestSetupForm.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAO/D,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC3B,cAAc,EAAE,YAAY,EAAE,CAAC;IAC/B,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,cAAc,EAAE,YAAY,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,kBAAkB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAChE;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAgKtD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";import React,{useState,useEffect,useContext}from"react";import{InputText}from"primereact/inputtext";import{AutoComplete}from"primereact/autocomplete";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export const TestSetupForm=({schema:e,defaultProjectId:s="",defaultDefinitionId:a="default",onProjectChange:t,onDefinitionChange:n,onValidationChange:r})=>{const[i,o]=useState({}),[c,p]=useState(s),[l,m]=useState(a);useEffect(()=>{t&&t(c)},[c,t]),useEffect(()=>{n&&n(l)},[l,n]);const[u,d]=useState([]),[f,x]=useState([]),[j,g]=useState(!1),{invoke:h}=useContext(EventEmitterContext);useEffect(()=>{(async()=>{try{const e=await h("results.list_projects",MessageType.Request,{});e.success&&e.data&&e.data.projects&&d(e.data.projects)}catch(e){}})()},[h]);useEffect(()=>{let s=!0;c&&""!==c.trim()||(s=!1),l&&""!==l.trim()||(s=!1);const a=[...e.project_fields,...e.config_fields];for(const e of a)if(e.required&&!e.source&&(void 0===i[e.name]||""===i[e.name])){s=!1;break}g(s),r&&r(s,i)},[i,e,c,l,r]);const _=e=>{if(e.source)return _jsxs("div",{className:"ac-form-span p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",children:e.name}),_jsx(InputText,{value:`Auto-fetched from ${e.source}`,disabled:!0}),e.units&&_jsx("span",{className:"p-inputgroup-addon",children:e.units}),_jsx("span",{className:"p-inputgroup-addon",style:{color:"var(--green-500)"},children:_jsx("i",{className:"pi pi-check"})})]},e.name);const s=(e=>!(e.required&&!e.source)||void 0!==i[e.name]&&""!==i[e.name])(e);return _jsxs("div",{className:"ac-form-span p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",children:e.name}),_jsx(InputText,{value:i[e.name]||"",onChange:s=>o({...i,[e.name]:s.target.value}),placeholder:`Enter ${e.name}`,className:s?"":"p-invalid"}),e.units&&_jsx("span",{className:"p-inputgroup-addon",children:e.units}),_jsx("span",{className:"p-inputgroup-addon",style:{color:s?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:s?"pi pi-check":"pi pi-times"})})]},e.name)};return _jsxs("div",{className:"ac-form-grid",style:{padding:"1.25rem"},children:[_jsxs("h3",{className:"ac-form-section",style:{display:"flex",alignItems:"center",gap:"10px"},children:["Project & Definition",_jsx("span",{style:{color:j?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:j?"pi pi-check-circle":"pi pi-exclamation-circle"})})]}),_jsxs("div",{className:"ac-form-span p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",children:"Project ID"}),_jsx(AutoComplete,{value:c,suggestions:f,completeMethod:e=>{const s=e.query.toLowerCase();x(u.filter(e=>e.toLowerCase().includes(s)))},onChange:e=>(e=>{const s=e.replace(/[^a-zA-Z0-9_]/g,"");p(s)})(e.value),dropdown:!0,placeholder:"Enter or select Project ID",className:c&&""!==c.trim()?"":"p-invalid"}),_jsx("span",{className:"p-inputgroup-addon",style:{color:c&&""!==c.trim()?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:c&&""!==c.trim()?"pi pi-check":"pi pi-times"})})]}),_jsxs("div",{className:"ac-form-span p-inputgroup",children:[_jsx("span",{className:"p-inputgroup-addon",children:"Definition ID"}),_jsx(InputText,{value:l,onChange:e=>(e=>{const s=e.replace(/[^a-zA-Z0-9_]/g,"");m(s)})(e.target.value),placeholder:"Enter Definition ID",className:l&&""!==l.trim()?"":"p-invalid"}),_jsx("span",{className:"p-inputgroup-addon",style:{color:l&&""!==l.trim()?"var(--green-500)":"var(--red-500)"},children:_jsx("i",{className:l&&""!==l.trim()?"pi pi-check":"pi pi-times"})})]}),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Project Information"}),e.project_fields.map(_),_jsx("h3",{className:"ac-form-section",style:{marginTop:"1rem"},children:"Test Configuration"}),e.config_fields.map(_)]})};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"./TestSetupForm";export*from"./ResultHistoryTable";
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAE,gBAAgB,EAAE,cAAc,EAAC,MAAM,YAAY,CAAC;AACnF,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,cAAc,mBAAmB,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";
|
|
1
|
+
export{useAdsRegisterSymbols,useAdsWriteValue,useAdsTapValue}from"./adsHooks";export{useScaledValue,kMillimeters2Inches}from"./useScaledValue";export*from"./useServeletData";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to read, write, and subscribe to a value in a generic servelet like MemoryStore or GNV.
|
|
3
|
+
*/
|
|
4
|
+
export declare function useServeletData<T = any>(domain: string, key: string, defaultValue?: T): {
|
|
5
|
+
value: T | undefined;
|
|
6
|
+
write: (newValue: T) => Promise<void>;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
error: string | null;
|
|
9
|
+
refresh: () => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Read, write, and subscribe to a non-volatile key in the Global Non-Volatile (GNV) store.
|
|
13
|
+
* @param group The config group (e.g. "app")
|
|
14
|
+
* @param key The config key (e.g. "description")
|
|
15
|
+
* @param defaultValue Optional fallback before load
|
|
16
|
+
*/
|
|
17
|
+
export declare function useGnv<T = any>(group: string, key: string, defaultValue?: T): {
|
|
18
|
+
value: T | undefined;
|
|
19
|
+
write: (newValue: T) => Promise<void>;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
error: string | null;
|
|
22
|
+
refresh: () => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Read, write, and subscribe to a volatile key in the MemoryStore.
|
|
26
|
+
* @param key The cache key (e.g. "temp_state")
|
|
27
|
+
* @param defaultValue Optional fallback before load
|
|
28
|
+
*/
|
|
29
|
+
export declare function useMemoryStore<T = any>(key: string, defaultValue?: T): {
|
|
30
|
+
value: T | undefined;
|
|
31
|
+
write: (newValue: T) => Promise<void>;
|
|
32
|
+
isLoading: boolean;
|
|
33
|
+
error: string | null;
|
|
34
|
+
refresh: () => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=useServeletData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useServeletData.d.ts","sourceRoot":"","sources":["../../src/hooks/useServeletData.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC;;sBAuDlC,CAAC;;;;EAapD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC;;;;;;EAE3E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,CAAC;;;;;;EAEpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useState,useEffect,useContext,useCallback}from"react";import{EventEmitterContext}from"../core/EventEmitterContext";import{MessageType}from"../hub/CommandMessage";export function useServeletData(e,t,r){const{invoke:s,subscribe:n,unsubscribe:a,isConnected:o}=useContext(EventEmitterContext),[u,c]=useState(r),[l,i]=useState(!0),[m,f]=useState(null),v=`${e}.${t}`,y=useCallback(async()=>{if(o())try{i(!0);const e=await s(v,MessageType.Read,{});e.success?(c(e.data),f(null)):f(e.error_message)}catch(e){f(e.message)}finally{i(!1)}},[v,s,o]);useEffect(()=>{let e=null,t=null,r=!0;const s=()=>{y(),e=n(v,e=>{r&&c(e.data)})};return o()?s():t=n("HUB/connected",()=>{r&&s()}),()=>{r=!1,null!==e&&a(e),null!==t&&a(t)}},[v,y,n,a,o]);return{value:u,write:useCallback(async e=>{try{const t=await s(v,MessageType.Write,e);if(!t.success)throw new Error(t.error_message)}catch(e){throw e}},[v,s]),isLoading:l,error:m,refresh:y}}export function useGnv(e,t,r){return useServeletData("gnv",`${e}.${t}`,r)}export function useMemoryStore(e,t){return useServeletData("memorystore",e,t)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adcops/autocore-react",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.25",
|
|
4
4
|
"description": "A React component library for industrial user interfaces.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"homepage": "https://automateddesign.com",
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@types/react": "^18",
|
|
43
|
-
"primereact": ">=10.
|
|
43
|
+
"primereact": ">=10.9.7",
|
|
44
44
|
"react": "^18",
|
|
45
45
|
"react-dom": "^18"
|
|
46
46
|
},
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { DataTable } from 'primereact/datatable';
|
|
3
|
+
import { Column } from 'primereact/column';
|
|
4
|
+
import { Button } from 'primereact/button';
|
|
5
|
+
import { EventEmitterContext } from '../core/EventEmitterContext';
|
|
6
|
+
|
|
7
|
+
export interface ResultHistoryTableProps {
|
|
8
|
+
projectId: string;
|
|
9
|
+
definitionId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ResultHistoryTable: React.FC<ResultHistoryTableProps> = ({ projectId, definitionId }) => {
|
|
13
|
+
const [tests, setTests] = useState<any[]>([]);
|
|
14
|
+
const [loading, setLoading] = useState(false);
|
|
15
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
16
|
+
|
|
17
|
+
const loadTests = async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
const resp: any = await invoke('results.list_tests' as any, {
|
|
21
|
+
project_id: projectId,
|
|
22
|
+
definition_id: definitionId
|
|
23
|
+
} as any);
|
|
24
|
+
if (resp.success && resp.data && resp.data.tests) {
|
|
25
|
+
setTests(resp.data.tests);
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error("Failed to load tests", err);
|
|
29
|
+
}
|
|
30
|
+
setLoading(false);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
loadTests();
|
|
35
|
+
}, [projectId, definitionId]);
|
|
36
|
+
|
|
37
|
+
const formatDate = (dateStr: string) => {
|
|
38
|
+
if (!dateStr) return '';
|
|
39
|
+
return new Date(dateStr).toLocaleString();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
|
45
|
+
<h3>Test History: {definitionId}</h3>
|
|
46
|
+
<Button icon="pi pi-refresh" label="Refresh" onClick={loadTests} disabled={loading} />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<DataTable value={tests} loading={loading} paginator rows={10} emptyMessage="No tests found.">
|
|
50
|
+
<Column field="run_id" header="Run ID" sortable />
|
|
51
|
+
<Column field="start_time" header="Date/Time" body={(rowData) => formatDate(rowData.start_time)} sortable />
|
|
52
|
+
<Column header="Config / Results" body={(rowData) => (
|
|
53
|
+
<div style={{ fontSize: '0.85em', color: 'var(--text-secondary-color)' }}>
|
|
54
|
+
<div><strong>Config:</strong> {JSON.stringify(rowData.config)}</div>
|
|
55
|
+
<div><strong>Results:</strong> {JSON.stringify(rowData.results)}</div>
|
|
56
|
+
</div>
|
|
57
|
+
)} />
|
|
58
|
+
</DataTable>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useState, useEffect, useContext } from 'react';
|
|
2
|
+
import { InputText } from 'primereact/inputtext';
|
|
3
|
+
import { AutoComplete } from 'primereact/autocomplete';
|
|
4
|
+
import type { AutoCompleteCompleteEvent } from 'primereact/autocomplete';
|
|
5
|
+
import { EventEmitterContext } from '../core/EventEmitterContext';
|
|
6
|
+
import { MessageType } from '../hub/CommandMessage';
|
|
7
|
+
|
|
8
|
+
export interface TestFieldDef {
|
|
9
|
+
name: string;
|
|
10
|
+
type: string;
|
|
11
|
+
units?: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
source?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TestDefinition {
|
|
17
|
+
project_fields: TestFieldDef[];
|
|
18
|
+
config_fields: TestFieldDef[];
|
|
19
|
+
cycle_fields: TestFieldDef[];
|
|
20
|
+
results_fields: TestFieldDef[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TestSetupFormProps {
|
|
24
|
+
schema: TestDefinition;
|
|
25
|
+
defaultProjectId?: string;
|
|
26
|
+
defaultDefinitionId?: string;
|
|
27
|
+
onProjectChange?: (projectId: string) => void;
|
|
28
|
+
onDefinitionChange?: (definitionId: string) => void;
|
|
29
|
+
onValidationChange?: (isValid: boolean, config: any) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const TestSetupForm: React.FC<TestSetupFormProps> = ({
|
|
33
|
+
schema,
|
|
34
|
+
defaultProjectId = "",
|
|
35
|
+
defaultDefinitionId = "default",
|
|
36
|
+
onProjectChange,
|
|
37
|
+
onDefinitionChange,
|
|
38
|
+
onValidationChange
|
|
39
|
+
}) => {
|
|
40
|
+
const [config, setConfig] = useState<any>({});
|
|
41
|
+
const [projectId, setProjectId] = useState<string>(defaultProjectId);
|
|
42
|
+
const [definitionId, setDefinitionId] = useState<string>(defaultDefinitionId);
|
|
43
|
+
|
|
44
|
+
// Notify parent when projectId or definitionId changes
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (onProjectChange) onProjectChange(projectId);
|
|
47
|
+
}, [projectId, onProjectChange]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (onDefinitionChange) onDefinitionChange(definitionId);
|
|
51
|
+
}, [definitionId, onDefinitionChange]);
|
|
52
|
+
|
|
53
|
+
const [existingProjects, setExistingProjects] = useState<string[]>([]);
|
|
54
|
+
const [filteredProjects, setFilteredProjects] = useState<string[]>([]);
|
|
55
|
+
|
|
56
|
+
const [isValid, setIsValid] = useState(false);
|
|
57
|
+
const { invoke } = useContext(EventEmitterContext);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const fetchProjects = async () => {
|
|
61
|
+
try {
|
|
62
|
+
const resp: any = await invoke('results.list_projects' as any, MessageType.Request as any, {} as any);
|
|
63
|
+
if (resp.success && resp.data && resp.data.projects) {
|
|
64
|
+
setExistingProjects(resp.data.projects);
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error("Failed to list projects", err);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
fetchProjects();
|
|
71
|
+
}, [invoke]);
|
|
72
|
+
|
|
73
|
+
const searchProjects = (event: AutoCompleteCompleteEvent) => {
|
|
74
|
+
const query = event.query.toLowerCase();
|
|
75
|
+
setFilteredProjects(existingProjects.filter(p => p.toLowerCase().includes(query)));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const handleProjectIdChange = (value: string) => {
|
|
79
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_]/g, '');
|
|
80
|
+
setProjectId(sanitized);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleDefinitionIdChange = (value: string) => {
|
|
84
|
+
const sanitized = value.replace(/[^a-zA-Z0-9_]/g, '');
|
|
85
|
+
setDefinitionId(sanitized);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
let valid = true;
|
|
90
|
+
if (!projectId || projectId.trim() === '') valid = false;
|
|
91
|
+
if (!definitionId || definitionId.trim() === '') valid = false;
|
|
92
|
+
|
|
93
|
+
const allFields = [...schema.project_fields, ...schema.config_fields];
|
|
94
|
+
|
|
95
|
+
for (const field of allFields) {
|
|
96
|
+
if (field.required && !field.source) {
|
|
97
|
+
if (config[field.name] === undefined || config[field.name] === '') {
|
|
98
|
+
valid = false;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
setIsValid(valid);
|
|
104
|
+
if (onValidationChange) {
|
|
105
|
+
onValidationChange(valid, config);
|
|
106
|
+
}
|
|
107
|
+
}, [config, schema, projectId, definitionId, onValidationChange]);
|
|
108
|
+
|
|
109
|
+
const isFieldValid = (field: TestFieldDef) => {
|
|
110
|
+
if (!field.required || field.source) return true;
|
|
111
|
+
return config[field.name] !== undefined && config[field.name] !== '';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const renderField = (field: TestFieldDef) => {
|
|
115
|
+
if (field.source) {
|
|
116
|
+
return (
|
|
117
|
+
<div key={field.name} className="ac-form-span p-inputgroup">
|
|
118
|
+
<span className="p-inputgroup-addon">{field.name}</span>
|
|
119
|
+
<InputText value={`Auto-fetched from ${field.source}`} disabled />
|
|
120
|
+
{field.units && <span className="p-inputgroup-addon">{field.units}</span>}
|
|
121
|
+
<span className="p-inputgroup-addon" style={{ color: 'var(--green-500)' }}>
|
|
122
|
+
<i className="pi pi-check"></i>
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const valid = isFieldValid(field);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div key={field.name} className="ac-form-span p-inputgroup">
|
|
132
|
+
<span className="p-inputgroup-addon">{field.name}</span>
|
|
133
|
+
<InputText
|
|
134
|
+
value={config[field.name] || ''}
|
|
135
|
+
onChange={(e) => setConfig({...config, [field.name]: e.target.value})}
|
|
136
|
+
placeholder={`Enter ${field.name}`}
|
|
137
|
+
className={!valid ? 'p-invalid' : ''}
|
|
138
|
+
/>
|
|
139
|
+
{field.units && <span className="p-inputgroup-addon">{field.units}</span>}
|
|
140
|
+
<span className="p-inputgroup-addon" style={{ color: valid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
141
|
+
<i className={valid ? "pi pi-check" : "pi pi-times"}></i>
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="ac-form-grid" style={{ padding: '1.25rem' }}>
|
|
149
|
+
<h3 className="ac-form-section" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
|
150
|
+
Project & Definition
|
|
151
|
+
<span style={{ color: isValid ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
152
|
+
<i className={isValid ? "pi pi-check-circle" : "pi pi-exclamation-circle"}></i>
|
|
153
|
+
</span>
|
|
154
|
+
</h3>
|
|
155
|
+
|
|
156
|
+
<div className="ac-form-span p-inputgroup">
|
|
157
|
+
<span className="p-inputgroup-addon">Project ID</span>
|
|
158
|
+
<AutoComplete
|
|
159
|
+
value={projectId}
|
|
160
|
+
suggestions={filteredProjects}
|
|
161
|
+
completeMethod={searchProjects}
|
|
162
|
+
onChange={(e) => handleProjectIdChange(e.value)}
|
|
163
|
+
dropdown
|
|
164
|
+
placeholder="Enter or select Project ID"
|
|
165
|
+
className={!projectId || projectId.trim() === '' ? 'p-invalid' : ''}
|
|
166
|
+
/>
|
|
167
|
+
<span className="p-inputgroup-addon" style={{ color: projectId && projectId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
168
|
+
<i className={projectId && projectId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
|
|
169
|
+
</span>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="ac-form-span p-inputgroup">
|
|
173
|
+
<span className="p-inputgroup-addon">Definition ID</span>
|
|
174
|
+
<InputText
|
|
175
|
+
value={definitionId}
|
|
176
|
+
onChange={(e) => handleDefinitionIdChange(e.target.value)}
|
|
177
|
+
placeholder="Enter Definition ID"
|
|
178
|
+
className={!definitionId || definitionId.trim() === '' ? 'p-invalid' : ''}
|
|
179
|
+
/>
|
|
180
|
+
<span className="p-inputgroup-addon" style={{ color: definitionId && definitionId.trim() !== '' ? 'var(--green-500)' : 'var(--red-500)' }}>
|
|
181
|
+
<i className={definitionId && definitionId.trim() !== '' ? "pi pi-check" : "pi pi-times"}></i>
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Project Information</h3>
|
|
186
|
+
{schema.project_fields.map(renderField)}
|
|
187
|
+
|
|
188
|
+
<h3 className="ac-form-section" style={{ marginTop: '1rem' }}>Test Configuration</h3>
|
|
189
|
+
{schema.config_fields.map(renderField)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useState, useEffect, useContext, useCallback } from 'react';
|
|
2
|
+
import { EventEmitterContext } from '../core/EventEmitterContext';
|
|
3
|
+
import { MessageType } from '../hub/CommandMessage';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to read, write, and subscribe to a value in a generic servelet like MemoryStore or GNV.
|
|
7
|
+
*/
|
|
8
|
+
export function useServeletData<T = any>(domain: string, key: string, defaultValue?: T) {
|
|
9
|
+
const { invoke, subscribe, unsubscribe, isConnected } = useContext(EventEmitterContext);
|
|
10
|
+
const [value, setValue] = useState<T | undefined>(defaultValue);
|
|
11
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
|
|
14
|
+
const fqdn = `${domain}.${key}`;
|
|
15
|
+
|
|
16
|
+
const fetchValue = useCallback(async () => {
|
|
17
|
+
if (!isConnected()) return;
|
|
18
|
+
try {
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
const resp = await invoke(fqdn, MessageType.Read, {});
|
|
21
|
+
if (resp.success) {
|
|
22
|
+
setValue(resp.data as T);
|
|
23
|
+
setError(null);
|
|
24
|
+
} else {
|
|
25
|
+
setError(resp.error_message);
|
|
26
|
+
}
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
setError(err.message);
|
|
29
|
+
} finally {
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
}
|
|
32
|
+
}, [fqdn, invoke, isConnected]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let subId: number | null = null;
|
|
36
|
+
let connSubId: number | null = null;
|
|
37
|
+
let isMounted = true;
|
|
38
|
+
|
|
39
|
+
const setup = () => {
|
|
40
|
+
fetchValue();
|
|
41
|
+
subId = subscribe(fqdn, (msg) => {
|
|
42
|
+
if (isMounted) {
|
|
43
|
+
setValue(msg.data as T);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (isConnected()) {
|
|
49
|
+
setup();
|
|
50
|
+
} else {
|
|
51
|
+
connSubId = subscribe("HUB/connected", () => {
|
|
52
|
+
if (isMounted) setup();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
isMounted = false;
|
|
58
|
+
if (subId !== null) unsubscribe(subId);
|
|
59
|
+
if (connSubId !== null) unsubscribe(connSubId);
|
|
60
|
+
};
|
|
61
|
+
}, [fqdn, fetchValue, subscribe, unsubscribe, isConnected]);
|
|
62
|
+
|
|
63
|
+
const writeValue = useCallback(async (newValue: T) => {
|
|
64
|
+
try {
|
|
65
|
+
// Use invoke directly to avoid Hub's default behavior of wrapping the payload in { value: ... }
|
|
66
|
+
const resp = await invoke(fqdn, MessageType.Write, newValue as any);
|
|
67
|
+
if (!resp.success) {
|
|
68
|
+
throw new Error(resp.error_message);
|
|
69
|
+
}
|
|
70
|
+
} catch (err: any) {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}, [fqdn, invoke]);
|
|
74
|
+
|
|
75
|
+
return { value, write: writeValue, isLoading, error, refresh: fetchValue };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read, write, and subscribe to a non-volatile key in the Global Non-Volatile (GNV) store.
|
|
80
|
+
* @param group The config group (e.g. "app")
|
|
81
|
+
* @param key The config key (e.g. "description")
|
|
82
|
+
* @param defaultValue Optional fallback before load
|
|
83
|
+
*/
|
|
84
|
+
export function useGnv<T = any>(group: string, key: string, defaultValue?: T) {
|
|
85
|
+
return useServeletData<T>('gnv', `${group}.${key}`, defaultValue);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read, write, and subscribe to a volatile key in the MemoryStore.
|
|
90
|
+
* @param key The cache key (e.g. "temp_state")
|
|
91
|
+
* @param defaultValue Optional fallback before load
|
|
92
|
+
*/
|
|
93
|
+
export function useMemoryStore<T = any>(key: string, defaultValue?: T) {
|
|
94
|
+
return useServeletData<T>('memorystore', key, defaultValue);
|
|
95
|
+
}
|