infaira-canvas 0.3.0 → 0.5.0
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/commands/init.js +89 -17
- package/package.json +2 -2
- package/templates/designer.d.ts +2 -2
- package/templates/ican.d.ts +27 -6
- package/templates/index.html +367 -57
- package/templates/resources/ican-components.js +0 -1
package/dist/commands/init.js
CHANGED
|
@@ -165,29 +165,85 @@ import { registerWidget } from './ican';
|
|
|
165
165
|
import type { IContextProvider } from './ican';
|
|
166
166
|
import './styles.scss';
|
|
167
167
|
|
|
168
|
+
// ─── useService hook ──────────────────────────────────────────────────────────
|
|
169
|
+
// Call any InfAIra microservice directly:
|
|
170
|
+
//
|
|
171
|
+
// useService(icanContext, 'Assets', 'Asset:List', { limit: 20 })
|
|
172
|
+
//
|
|
173
|
+
// The result is the raw microservice response — no extra wrapper to unwrap.
|
|
174
|
+
// For orchestrated flows that call multiple services, use executeAction with
|
|
175
|
+
// a Mythos model instead (see the commented-out examples below).
|
|
176
|
+
|
|
177
|
+
interface IServiceState<T> {
|
|
178
|
+
data: T | null;
|
|
179
|
+
loading: boolean;
|
|
180
|
+
error: string | null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function useService<T = unknown>(
|
|
184
|
+
ctx: IContextProvider | undefined,
|
|
185
|
+
app: string,
|
|
186
|
+
service: string,
|
|
187
|
+
params: Record<string, unknown> = {},
|
|
188
|
+
deps: unknown[] = [],
|
|
189
|
+
): IServiceState<T> & { refetch: () => void } {
|
|
190
|
+
const [state, setState] = React.useState<IServiceState<T>>({ data: null, loading: true, error: null });
|
|
191
|
+
const [tick, setTick] = React.useState(0);
|
|
192
|
+
const mounted = React.useRef(true);
|
|
193
|
+
React.useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []);
|
|
194
|
+
|
|
195
|
+
React.useEffect(() => {
|
|
196
|
+
if (!ctx) { setState({ data: null, loading: false, error: null }); return; }
|
|
197
|
+
setState(prev => ({ ...prev, loading: true, error: null }));
|
|
198
|
+
ctx.executeService(app, service, params)
|
|
199
|
+
.then(result => { if (mounted.current) setState({ data: result as T, loading: false, error: null }); })
|
|
200
|
+
.catch((err: unknown) => { if (mounted.current) setState({ data: null, loading: false, error: String(err) }); });
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [ctx, app, service, tick, ...deps]);
|
|
203
|
+
|
|
204
|
+
return { ...state, refetch: () => setTick(t => t + 1) };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
208
|
+
// Replace with real types from your microservice.
|
|
209
|
+
interface IItem {
|
|
210
|
+
id: number;
|
|
211
|
+
name: string;
|
|
212
|
+
[key: string]: unknown;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface IListResponse {
|
|
216
|
+
total: number;
|
|
217
|
+
data: IItem[];
|
|
218
|
+
}
|
|
219
|
+
|
|
168
220
|
export interface IWidgetProps {
|
|
169
221
|
icanContext?: IContextProvider;
|
|
170
222
|
instanceId?: string;
|
|
171
223
|
[key: string]: unknown;
|
|
172
224
|
}
|
|
173
225
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
226
|
+
// ─── Widget ───────────────────────────────────────────────────────────────────
|
|
227
|
+
const ${componentName}: React.FC<IWidgetProps> = ({ icanContext }) => {
|
|
228
|
+
// ── Fetch data via executeService ────────────────────────────────────────
|
|
229
|
+
// Replace 'Assets' / 'Asset:List' with your app + service path.
|
|
230
|
+
// All InfAIra apps: Assets, Locations, UserManagement, FMS, PPM, IBMS
|
|
231
|
+
const { data, loading, error, refetch } = useService<IListResponse>(
|
|
232
|
+
icanContext,
|
|
233
|
+
'Assets', // app key (matches manifest app_key)
|
|
234
|
+
'Asset:List', // Model:Action
|
|
235
|
+
{ limit: 20, skip: 0 },
|
|
236
|
+
);
|
|
178
237
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
setData('Hello from ${widgetName}!');
|
|
189
|
-
setLoading(false);
|
|
190
|
-
}, [icanContext]);
|
|
238
|
+
// ── For Mythos orchestrated flows, use executeAction instead: ─────────────
|
|
239
|
+
// React.useEffect(() => {
|
|
240
|
+
// icanContext?.executeAction('mythos:AssetManager', 'ListAssets', { limit: 20 })
|
|
241
|
+
// .then(result => {
|
|
242
|
+
// // result = { run_id, status, output: { assets: { total, data } } }
|
|
243
|
+
// const list = (result as { output?: { assets?: IListResponse } })?.output?.assets;
|
|
244
|
+
// console.log(list);
|
|
245
|
+
// });
|
|
246
|
+
// }, [icanContext]);
|
|
191
247
|
|
|
192
248
|
if (loading) {
|
|
193
249
|
return (
|
|
@@ -201,13 +257,29 @@ const ${componentName}: React.FC<IWidgetProps> = ({ icanContext, instanceId: _in
|
|
|
201
257
|
return (
|
|
202
258
|
<div className="widget-error">
|
|
203
259
|
<p className="widget-error-message">{error}</p>
|
|
260
|
+
<button className="widget-retry" onClick={refetch}>Retry</button>
|
|
204
261
|
</div>
|
|
205
262
|
);
|
|
206
263
|
}
|
|
207
264
|
|
|
265
|
+
const items = data?.data ?? [];
|
|
266
|
+
const total = data?.total ?? 0;
|
|
267
|
+
|
|
208
268
|
return (
|
|
209
269
|
<div className="widget-root">
|
|
210
|
-
<
|
|
270
|
+
<div className="widget-header">
|
|
271
|
+
<span className="widget-title">${widgetName}</span>
|
|
272
|
+
<span className="widget-count">{total} items</span>
|
|
273
|
+
</div>
|
|
274
|
+
{items.length === 0 ? (
|
|
275
|
+
<p className="widget-empty">No data found.</p>
|
|
276
|
+
) : (
|
|
277
|
+
<ul className="widget-list">
|
|
278
|
+
{items.map(item => (
|
|
279
|
+
<li key={item.id} className="widget-list-item">{item.name}</li>
|
|
280
|
+
))}
|
|
281
|
+
</ul>
|
|
282
|
+
)}
|
|
211
283
|
</div>
|
|
212
284
|
);
|
|
213
285
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infaira-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "InfAIra Canvas CLI — scaffold widgets that talk to ICan and Mythos (actions, events, public portals, paginated collections).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"infaira",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": {
|
|
18
|
-
"infaira-canvas": "
|
|
18
|
+
"infaira-canvas": "dist/index.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
package/templates/designer.d.ts
CHANGED
|
@@ -240,7 +240,7 @@ declare module "widget-designer/components" {
|
|
|
240
240
|
params?: { [key: string]: any }
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
export interface
|
|
243
|
+
export interface ICanActionConfig {
|
|
244
244
|
modelName: string;
|
|
245
245
|
action: string;
|
|
246
246
|
disableParamters?: boolean,
|
|
@@ -271,7 +271,7 @@ declare module "widget-designer/components" {
|
|
|
271
271
|
type: IActionType,
|
|
272
272
|
redirectUrl?: IRedirectUrl
|
|
273
273
|
redirectWidget?: IRedirectWidget;
|
|
274
|
-
|
|
274
|
+
canAction?: ICanActionConfig
|
|
275
275
|
}
|
|
276
276
|
export interface IActionButtonProps {
|
|
277
277
|
designer?: any
|
package/templates/ican.d.ts
CHANGED
|
@@ -658,7 +658,7 @@ declare module "ican/hooks" {
|
|
|
658
658
|
// ─── useFields ────────────────────────────────────────────────────────────
|
|
659
659
|
/**
|
|
660
660
|
* Lightweight controlled-form helper. Returns a getter/setter for each named
|
|
661
|
-
* field.
|
|
661
|
+
* field.
|
|
662
662
|
*/
|
|
663
663
|
export function useFields<T extends Record<string, unknown>>(
|
|
664
664
|
initial: T
|
|
@@ -725,6 +725,17 @@ declare module "ican/context" {
|
|
|
725
725
|
params?: Record<string, unknown>,
|
|
726
726
|
options?: { cancelPrevious?: boolean; key?: string }
|
|
727
727
|
): Promise<unknown>;
|
|
728
|
+
/**
|
|
729
|
+
* Call an application service directly. The ICan/Mythos service registry
|
|
730
|
+
* resolves `<app>.<service>` (e.g. 'Assets' + 'Asset:List') to the
|
|
731
|
+
* microservice and returns its raw response — no model/DAG needed.
|
|
732
|
+
*/
|
|
733
|
+
executeService(
|
|
734
|
+
app: string,
|
|
735
|
+
service: string,
|
|
736
|
+
params?: Record<string, unknown>,
|
|
737
|
+
options?: { cancelPrevious?: boolean; key?: string }
|
|
738
|
+
): Promise<unknown>;
|
|
728
739
|
/**
|
|
729
740
|
* List every Mythos action published and callable from this widget.
|
|
730
741
|
* Optional — present only when the ICan backend has MYTHOS_BASE_URL set.
|
|
@@ -732,11 +743,23 @@ declare module "ican/context" {
|
|
|
732
743
|
* once at mount and cache the result themselves.
|
|
733
744
|
*/
|
|
734
745
|
listMythosActions?: () => Promise<IMythosAction[]>;
|
|
746
|
+
/**
|
|
747
|
+
* List models for a config-panel model picker (`{ id, name }[]`). Optional —
|
|
748
|
+
* present only when MYTHOS_BASE_URL is set.
|
|
749
|
+
*/
|
|
750
|
+
listMythosModels?: () => Promise<Array<{ id: string; name: string }>>;
|
|
751
|
+
/**
|
|
752
|
+
* Returns true when the current user holds any of the given app roles.
|
|
753
|
+
* In the dev harness this is always true (full access); in production it
|
|
754
|
+
* checks the JWT principal's roles.
|
|
755
|
+
*/
|
|
756
|
+
hasAppRole?: (roles: string | string[]) => boolean | Promise<boolean>;
|
|
757
|
+
/** Localization helper — `$L(code, params?)` resolves a locale key to a translated string. */
|
|
758
|
+
$L?: (code: string, params?: Record<string, unknown>) => string;
|
|
735
759
|
/**
|
|
736
760
|
* Fire an external event into the Mythos engine. The event_type matches
|
|
737
761
|
* what Mythos event-handlers subscribe to (configured in the model's
|
|
738
|
-
* Events tab).
|
|
739
|
-
* Optional — present only when MYTHOS_BASE_URL is configured.
|
|
762
|
+
* Events tab). Optional — present only when MYTHOS_BASE_URL is configured.
|
|
740
763
|
*/
|
|
741
764
|
fireMythosEvent?: (
|
|
742
765
|
eventType: string,
|
|
@@ -744,7 +767,7 @@ declare module "ican/context" {
|
|
|
744
767
|
) => Promise<void>;
|
|
745
768
|
/**
|
|
746
769
|
* Build an IDataFunction over a Mythos collection — drop-in data source
|
|
747
|
-
* for DataList / DataTable.
|
|
770
|
+
* for DataList / DataTable.
|
|
748
771
|
* const fn = icanContext.fromMythosCollection('Inventory', 'items')
|
|
749
772
|
* const { items, pageToken } = await fn(50, '', { search: 'foo' })
|
|
750
773
|
* Optional — present only when MYTHOS_BASE_URL is configured.
|
|
@@ -790,7 +813,6 @@ interface Window {
|
|
|
790
813
|
React: unknown;
|
|
791
814
|
ReactDOM: unknown;
|
|
792
815
|
ICanComponents: Record<string, unknown>;
|
|
793
|
-
UXPComponents: Record<string, unknown>;
|
|
794
816
|
WidgetDesignerComponents: Record<string, unknown>;
|
|
795
817
|
/** Widget registry — qualifiedId → registration entry. */
|
|
796
818
|
__ican_registry: Map<string, unknown>;
|
|
@@ -798,7 +820,6 @@ interface Window {
|
|
|
798
820
|
registerWidget(registration: {
|
|
799
821
|
id: string;
|
|
800
822
|
component?: unknown;
|
|
801
|
-
/** UXP-legacy alias for `component`. */
|
|
802
823
|
widget?: unknown;
|
|
803
824
|
configs?: unknown;
|
|
804
825
|
defaultProps?: Record<string, unknown>;
|
package/templates/index.html
CHANGED
|
@@ -775,6 +775,92 @@
|
|
|
775
775
|
box-shadow: 0 0 6px rgba(74, 222, 128, 0.45);
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
+
.ican-conn-dot.error {
|
|
779
|
+
background: var(--ican-error, #f87171);
|
|
780
|
+
box-shadow: 0 0 6px rgba(248, 113, 113, 0.45);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.ican-conn-dot.testing {
|
|
784
|
+
background: #facc15;
|
|
785
|
+
animation: conn-pulse 0.9s ease-in-out infinite;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
@keyframes conn-pulse {
|
|
789
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
790
|
+
50% { opacity: 0.35; transform: scale(0.7); }
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.ican-conn-error {
|
|
794
|
+
font-size: 10px;
|
|
795
|
+
color: var(--ican-error, #f87171);
|
|
796
|
+
font-family: var(--ican-font-mono);
|
|
797
|
+
margin-top: 2px;
|
|
798
|
+
white-space: nowrap;
|
|
799
|
+
overflow: hidden;
|
|
800
|
+
text-overflow: ellipsis;
|
|
801
|
+
max-width: 100%;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.ican-conn-input-wrap {
|
|
805
|
+
position: relative;
|
|
806
|
+
display: flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
.ican-conn-input-wrap .ican-conn-input { padding-right: 34px; }
|
|
811
|
+
|
|
812
|
+
.ican-conn-show-btn {
|
|
813
|
+
position: absolute;
|
|
814
|
+
right: 8px;
|
|
815
|
+
background: none;
|
|
816
|
+
border: none;
|
|
817
|
+
cursor: pointer;
|
|
818
|
+
padding: 0;
|
|
819
|
+
color: var(--ican-secondary-text);
|
|
820
|
+
line-height: 1;
|
|
821
|
+
display: flex;
|
|
822
|
+
align-items: center;
|
|
823
|
+
flex-shrink: 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.ican-conn-show-btn:hover { color: var(--ican-primary-text); }
|
|
827
|
+
|
|
828
|
+
.ican-live-badge {
|
|
829
|
+
display: inline-flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
gap: 4px;
|
|
832
|
+
font-size: 9px;
|
|
833
|
+
font-weight: 700;
|
|
834
|
+
text-transform: uppercase;
|
|
835
|
+
letter-spacing: 0.8px;
|
|
836
|
+
padding: 2px 7px;
|
|
837
|
+
border-radius: 3px;
|
|
838
|
+
font-family: var(--ican-font-mono);
|
|
839
|
+
background: rgba(255,255,255,0.05);
|
|
840
|
+
color: var(--ican-tertiary-text);
|
|
841
|
+
border: 1px solid var(--ican-border);
|
|
842
|
+
user-select: none;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
.ican-live-badge.live {
|
|
846
|
+
background: rgba(74, 222, 128, 0.10);
|
|
847
|
+
color: #4ade80;
|
|
848
|
+
border-color: rgba(74, 222, 128, 0.22);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.ican-live-badge.live-error {
|
|
852
|
+
background: rgba(248, 113, 113, 0.10);
|
|
853
|
+
color: #f87171;
|
|
854
|
+
border-color: rgba(248, 113, 113, 0.22);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.ican-live-badge.live-testing {
|
|
858
|
+
background: rgba(250, 204, 21, 0.10);
|
|
859
|
+
color: #facc15;
|
|
860
|
+
border-color: rgba(250, 204, 21, 0.22);
|
|
861
|
+
animation: conn-pulse 0.9s ease-in-out infinite;
|
|
862
|
+
}
|
|
863
|
+
|
|
778
864
|
/* ═══════════════════════════════════════════════════════
|
|
779
865
|
Main Content
|
|
780
866
|
═══════════════════════════════════════════════════════ */
|
|
@@ -1408,62 +1494,163 @@
|
|
|
1408
1494
|
return THEMES[0];
|
|
1409
1495
|
}
|
|
1410
1496
|
|
|
1497
|
+
/* ─── Dev-harness security helpers ────────────────────────
|
|
1498
|
+
isValidBackendUrl: permits https:// (any host) and
|
|
1499
|
+
http://localhost / http://127.0.0.1 only. Plain http:// to
|
|
1500
|
+
external hosts is blocked (SSRF / mixed-content risk).
|
|
1501
|
+
─────────────────────────────────────────────────────────── */
|
|
1502
|
+
function isValidBackendUrl(raw) {
|
|
1503
|
+
if (!raw || typeof raw !== 'string') return false;
|
|
1504
|
+
var url;
|
|
1505
|
+
try { url = new URL(raw); } catch (_) { return false; }
|
|
1506
|
+
if (url.protocol === 'https:') return true;
|
|
1507
|
+
if (url.protocol === 'http:') {
|
|
1508
|
+
var h = url.hostname;
|
|
1509
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '[::1]';
|
|
1510
|
+
}
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function normalizeUrl(raw) {
|
|
1515
|
+
return (raw || '').trim().replace(/\/+$/, '');
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// fetchWithTimeout wraps fetch with an AbortController so hung requests
|
|
1519
|
+
// don't freeze the harness indefinitely. Default timeout: 15 s.
|
|
1520
|
+
function fetchWithTimeout(url, opts, ms) {
|
|
1521
|
+
ms = ms || 15000;
|
|
1522
|
+
var ctrl = new AbortController();
|
|
1523
|
+
var timer = setTimeout(function () { ctrl.abort(); }, ms);
|
|
1524
|
+
return fetch(url, Object.assign({}, opts, { signal: ctrl.signal })).then(
|
|
1525
|
+
function (r) { clearTimeout(timer); return r; },
|
|
1526
|
+
function (e) {
|
|
1527
|
+
clearTimeout(timer);
|
|
1528
|
+
if (e && e.name === 'AbortError') {
|
|
1529
|
+
throw new Error('Request timed out — is the Mythos engine reachable?');
|
|
1530
|
+
}
|
|
1531
|
+
throw new Error('Network error — check the Mythos URL and that CORS is enabled on the server');
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1411
1536
|
/* ─── Mock icanContext factory ─────────────────────────── */
|
|
1412
1537
|
function makeICanContext(themeKey) {
|
|
1413
1538
|
var info = getThemeInfo(themeKey);
|
|
1539
|
+
var orchUrl = (localStorage.getItem(LS_ORCH_URL) || '').replace(/\/+$/, '');
|
|
1540
|
+
var apiKey = localStorage.getItem(LS_API_KEY) || '';
|
|
1541
|
+
// "configured" = the developer entered a Mythos/Orch URL + API Key in the
|
|
1542
|
+
// Developer Tools (Connection) panel. When set, the harness hits a REAL
|
|
1543
|
+
// backend (the same data the widget will see in production). When unset,
|
|
1544
|
+
// it falls back to a clear "not configured" message so widgets don't crash.
|
|
1545
|
+
var configured = !!(orchUrl && apiKey);
|
|
1546
|
+
|
|
1547
|
+
// batch() POSTs the widget-batch endpoint Mythos exposes. One API-key gate
|
|
1548
|
+
// behind which executeAction, executeService and fromMythosCollection all
|
|
1549
|
+
// resolve. Security: URL is validated before every call so a stale
|
|
1550
|
+
// localStorage value can never route requests to an arbitrary host.
|
|
1551
|
+
function batch(items) {
|
|
1552
|
+
if (!configured) {
|
|
1553
|
+
return Promise.resolve(items.map(function (it) {
|
|
1554
|
+
console.warn('[ICan Dev] Not configured — set Mythos URL + API Key in Developer Tools.');
|
|
1555
|
+
return { id: it.id, ok: false, error: 'not configured: open Developer Tools and set the Mythos URL + API Key' };
|
|
1556
|
+
}));
|
|
1557
|
+
}
|
|
1558
|
+
if (!isValidBackendUrl(orchUrl)) {
|
|
1559
|
+
return Promise.resolve(items.map(function (it) {
|
|
1560
|
+
console.error('[ICan Dev] Blocked request to invalid URL:', orchUrl);
|
|
1561
|
+
return { id: it.id, ok: false, error: 'invalid URL — only https:// or http://localhost is allowed' };
|
|
1562
|
+
}));
|
|
1563
|
+
}
|
|
1564
|
+
return fetchWithTimeout(orchUrl + '/api/widget-batch', {
|
|
1565
|
+
method: 'POST',
|
|
1566
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
|
|
1567
|
+
body: JSON.stringify({ items: items }),
|
|
1568
|
+
}).then(function (r) {
|
|
1569
|
+
if (r.status === 401) throw new Error('Unauthorized — check the API Key in Developer Tools');
|
|
1570
|
+
if (r.status === 403) throw new Error('Forbidden — API Key may lack scope for this action');
|
|
1571
|
+
if (!r.ok) throw new Error('Mythos returned ' + r.status + ' — check server logs');
|
|
1572
|
+
return r.json();
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1414
1576
|
return {
|
|
1415
1577
|
environment: 'dev',
|
|
1416
1578
|
userKey: 'dev-user-001',
|
|
1417
1579
|
language: 'en',
|
|
1418
1580
|
root: 'root',
|
|
1419
|
-
orchUrl:
|
|
1420
|
-
apiKey:
|
|
1581
|
+
orchUrl: orchUrl,
|
|
1582
|
+
apiKey: apiKey,
|
|
1583
|
+
|
|
1584
|
+
// executeAction → real Mythos action. Returns the {run_id,status,output}
|
|
1585
|
+
// envelope per ican.d.ts (the batch returns the bare output as `data`).
|
|
1421
1586
|
executeAction: function (model, action, params) {
|
|
1422
|
-
// Mythos-routed calls use the `mythos:<ModelName>` prefix in production.
|
|
1423
|
-
// In the dev harness we just log them distinctively so authors can see
|
|
1424
|
-
// whether their widget is targeting ICan native or Mythos.
|
|
1425
1587
|
var isMythos = typeof model === 'string' && model.toLowerCase().indexOf('mythos:') === 0;
|
|
1426
1588
|
console.log('[ICan Dev]' + (isMythos ? ' [Mythos]' : '') + ' executeAction:', model, action, params);
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1589
|
+
return batch([{ id: 'a', model: model, action: action, parameters: params || {} }]).then(function (res) {
|
|
1590
|
+
var it = (res && res[0]) || {};
|
|
1591
|
+
if (!it.ok) { console.warn('[ICan Dev] executeAction error:', it.error); return { run_id: null, status: 'error', output: null, error: it.error }; }
|
|
1592
|
+
return { run_id: 'dev', status: 'ok', output: it.data };
|
|
1593
|
+
});
|
|
1432
1594
|
},
|
|
1595
|
+
|
|
1596
|
+
// executeService → real Mythos /api/services/dispatch (App.Service path).
|
|
1433
1597
|
executeService: function (app, service, params) {
|
|
1434
1598
|
console.log('[ICan Dev] executeService:', app, service, params);
|
|
1435
|
-
return
|
|
1599
|
+
return batch([{ id: 's', app: app, service: service, parameters: params || {} }]).then(function (res) {
|
|
1600
|
+
var it = (res && res[0]) || {};
|
|
1601
|
+
if (!it.ok) { console.warn('[ICan Dev] executeService error:', it.error); return null; }
|
|
1602
|
+
return it.data;
|
|
1603
|
+
});
|
|
1436
1604
|
},
|
|
1605
|
+
|
|
1606
|
+
// listMythosActions → real published-action index via the batch "discover"
|
|
1607
|
+
// item (API-key only — no JWT needed). Powers config-panel action pickers.
|
|
1437
1608
|
listMythosActions: function () {
|
|
1438
|
-
// Dev stub — returns a couple of synthetic actions so widgets that
|
|
1439
|
-
// populate dropdowns from discovery don't render empty.
|
|
1440
1609
|
console.log('[ICan Dev] listMythosActions');
|
|
1441
|
-
return Promise.resolve([
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1610
|
+
if (!configured) return Promise.resolve([]);
|
|
1611
|
+
return batch([{ id: 'd', discover: 'actions' }]).then(function (res) {
|
|
1612
|
+
var it = (res && res[0]) || {};
|
|
1613
|
+
return (it.ok && Array.isArray(it.data)) ? it.data : [];
|
|
1614
|
+
}).catch(function () { return []; });
|
|
1445
1615
|
},
|
|
1616
|
+
|
|
1617
|
+
// listMythosModels → model picker source (config panels).
|
|
1618
|
+
listMythosModels: function () {
|
|
1619
|
+
if (!configured) return Promise.resolve([]);
|
|
1620
|
+
return batch([{ id: 'd', discover: 'models' }]).then(function (res) {
|
|
1621
|
+
var it = (res && res[0]) || {};
|
|
1622
|
+
return (it.ok && Array.isArray(it.data)) ? it.data : [];
|
|
1623
|
+
}).catch(function () { return []; });
|
|
1624
|
+
},
|
|
1625
|
+
|
|
1626
|
+
// fireMythosEvent → real event publish (best-effort, fire-and-forget).
|
|
1446
1627
|
fireMythosEvent: function (eventType, payload) {
|
|
1447
1628
|
console.log('[ICan Dev] fireMythosEvent:', eventType, payload);
|
|
1448
|
-
return Promise.resolve();
|
|
1629
|
+
if (!configured || !isValidBackendUrl(orchUrl)) return Promise.resolve();
|
|
1630
|
+
return fetchWithTimeout(orchUrl + '/api/events/publish', {
|
|
1631
|
+
method: 'POST',
|
|
1632
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey },
|
|
1633
|
+
body: JSON.stringify({ event_type: eventType, payload: payload || {} }),
|
|
1634
|
+
}, 8000).then(function () {}).catch(function (e) {
|
|
1635
|
+
console.warn('[ICan Dev] fireMythosEvent failed:', e && e.message);
|
|
1636
|
+
});
|
|
1449
1637
|
},
|
|
1638
|
+
|
|
1639
|
+
// fromMythosCollection → real collection read through the batch.
|
|
1450
1640
|
fromMythosCollection: function (model, collection) {
|
|
1451
1641
|
return function (max, lastPageToken, args) {
|
|
1452
1642
|
console.log('[ICan Dev] fromMythosCollection:', model, collection, { max: max, lastPageToken: lastPageToken, args: args });
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
items
|
|
1456
|
-
|
|
1457
|
-
}
|
|
1458
|
-
pageToken: '',
|
|
1643
|
+
return batch([{ id: 'c', model: model, collection: collection, parameters: { max: max, last: lastPageToken || '0', filter: args || {} } }]).then(function (res) {
|
|
1644
|
+
var it = (res && res[0]) || {};
|
|
1645
|
+
var items = (it.ok && Array.isArray(it.data)) ? it.data : [];
|
|
1646
|
+
var last = parseInt(lastPageToken || '0', 10) || 0;
|
|
1647
|
+
return { items: items, pageToken: String(last + items.length) };
|
|
1459
1648
|
});
|
|
1460
1649
|
};
|
|
1461
1650
|
},
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
},
|
|
1466
|
-
hasAppRole: function () { return true; },
|
|
1651
|
+
|
|
1652
|
+
fireEvent: function (id) { console.log('[ICan Dev] fireEvent:', id); return Promise.resolve(); },
|
|
1653
|
+
hasAppRole: function () { return true; }, // dev harness: full access
|
|
1467
1654
|
$L: function (code) { return code; },
|
|
1468
1655
|
themeName: info.label,
|
|
1469
1656
|
themeType: info.type,
|
|
@@ -1834,46 +2021,108 @@
|
|
|
1834
2021
|
|
|
1835
2022
|
/* ─── ConnectionPanel ─────────────────────────────────── */
|
|
1836
2023
|
function ConnectionPanel(props) {
|
|
1837
|
-
var
|
|
1838
|
-
var
|
|
1839
|
-
var
|
|
1840
|
-
var
|
|
2024
|
+
var onSave = props.onSave;
|
|
2025
|
+
var onTest = props.onTest;
|
|
2026
|
+
var connStatus = props.connStatus || 'idle';
|
|
2027
|
+
var connError = props.connError || '';
|
|
2028
|
+
|
|
2029
|
+
// Local controlled state for the two fields + key visibility
|
|
2030
|
+
var urlState = React.useState(props.orchUrl || '');
|
|
2031
|
+
var localUrl = urlState[0]; var setLocalUrl = urlState[1];
|
|
2032
|
+
var keyState = React.useState(props.apiKey || '');
|
|
2033
|
+
var localKey = keyState[0]; var setLocalKey = keyState[1];
|
|
2034
|
+
var showKeyState = React.useState(false);
|
|
2035
|
+
var showKey = showKeyState[0]; var setShowKey = showKeyState[1];
|
|
2036
|
+
|
|
2037
|
+
var urlValidationErr = localUrl && !isValidBackendUrl(localUrl)
|
|
2038
|
+
? 'Only https:// or http://localhost allowed'
|
|
2039
|
+
: '';
|
|
2040
|
+
|
|
2041
|
+
var dotClass = 'ican-conn-dot';
|
|
2042
|
+
var statusText = 'Not configured';
|
|
2043
|
+
if (connStatus === 'testing') {
|
|
2044
|
+
dotClass += ' testing'; statusText = 'Testing…';
|
|
2045
|
+
} else if (connStatus === 'connected') {
|
|
2046
|
+
dotClass += ' connected'; statusText = 'Connected';
|
|
2047
|
+
} else if (connStatus === 'error') {
|
|
2048
|
+
dotClass += ' error'; statusText = 'Error';
|
|
2049
|
+
} else if (localUrl && localKey) {
|
|
2050
|
+
dotClass += ' connected'; statusText = 'Configured';
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
var isTesting = connStatus === 'testing';
|
|
1841
2054
|
|
|
1842
2055
|
return h('div', { className: 'ican-conn-panel' },
|
|
1843
2056
|
h('div', { className: 'ican-conn-field' },
|
|
1844
|
-
h('label', { className: 'ican-conn-label', htmlFor: 'conn-orch' }, '
|
|
2057
|
+
h('label', { className: 'ican-conn-label', htmlFor: 'conn-orch' }, 'Mythos URL'),
|
|
1845
2058
|
h('input', {
|
|
1846
2059
|
id: 'conn-orch',
|
|
1847
2060
|
className: 'ican-conn-input',
|
|
1848
2061
|
type: 'text',
|
|
1849
|
-
|
|
1850
|
-
placeholder: 'https://your-
|
|
1851
|
-
'aria-label': '
|
|
1852
|
-
|
|
1853
|
-
|
|
2062
|
+
value: localUrl,
|
|
2063
|
+
placeholder: 'https://your-mythos-engine:8090',
|
|
2064
|
+
'aria-label': 'Mythos engine URL',
|
|
2065
|
+
'aria-describedby': urlValidationErr ? 'conn-url-err' : undefined,
|
|
2066
|
+
onChange: function (e) { setLocalUrl(e.target.value); },
|
|
2067
|
+
}),
|
|
2068
|
+
urlValidationErr
|
|
2069
|
+
? h('div', { id: 'conn-url-err', className: 'ican-conn-error', role: 'alert' }, urlValidationErr)
|
|
2070
|
+
: null
|
|
1854
2071
|
),
|
|
1855
2072
|
h('div', { className: 'ican-conn-field' },
|
|
1856
2073
|
h('label', { className: 'ican-conn-label', htmlFor: 'conn-key' }, 'API Key'),
|
|
1857
|
-
h('
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2074
|
+
h('div', { className: 'ican-conn-input-wrap' },
|
|
2075
|
+
h('input', {
|
|
2076
|
+
id: 'conn-key',
|
|
2077
|
+
className: 'ican-conn-input',
|
|
2078
|
+
type: showKey ? 'text' : 'password',
|
|
2079
|
+
value: localKey,
|
|
2080
|
+
placeholder: showKey ? 'mk_live_…' : '••••••••••••••••',
|
|
2081
|
+
'aria-label': 'API Key',
|
|
2082
|
+
autoComplete: 'off',
|
|
2083
|
+
onChange: function (e) { setLocalKey(e.target.value); },
|
|
2084
|
+
}),
|
|
2085
|
+
h('button', {
|
|
2086
|
+
className: 'ican-conn-show-btn',
|
|
2087
|
+
type: 'button',
|
|
2088
|
+
title: showKey ? 'Hide key' : 'Show key',
|
|
2089
|
+
'aria-label': showKey ? 'Hide API Key' : 'Show API Key',
|
|
2090
|
+
onClick: function () { setShowKey(!showKey); },
|
|
2091
|
+
}, showKey
|
|
2092
|
+
? h('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },
|
|
2093
|
+
h('path', { d: 'M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94' }),
|
|
2094
|
+
h('path', { d: 'M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19' }),
|
|
2095
|
+
h('line', { x1: 1, y1: 1, x2: 23, y2: 23 })
|
|
2096
|
+
)
|
|
2097
|
+
: h('svg', { width: 13, height: 13, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2 },
|
|
2098
|
+
h('path', { d: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z' }),
|
|
2099
|
+
h('circle', { cx: 12, cy: 12, r: 3 })
|
|
2100
|
+
)
|
|
2101
|
+
)
|
|
2102
|
+
),
|
|
2103
|
+
connStatus === 'error' && connError
|
|
2104
|
+
? h('div', { className: 'ican-conn-error', role: 'alert' }, connError)
|
|
2105
|
+
: null
|
|
1866
2106
|
),
|
|
1867
2107
|
h('div', { className: 'ican-conn-actions' },
|
|
2108
|
+
h('button', {
|
|
2109
|
+
className: 'ican-btn ican-btn-secondary',
|
|
2110
|
+
type: 'button',
|
|
2111
|
+
disabled: isTesting || !!urlValidationErr,
|
|
2112
|
+
style: { height: 30, fontSize: 12 },
|
|
2113
|
+
title: 'Test the connection without saving',
|
|
2114
|
+
onClick: function () { onTest(localUrl, localKey); },
|
|
2115
|
+
}, isTesting ? 'Testing…' : 'Test'),
|
|
1868
2116
|
h('button', {
|
|
1869
2117
|
className: 'ican-btn ican-btn-primary',
|
|
1870
2118
|
type: 'button',
|
|
2119
|
+
disabled: !!urlValidationErr,
|
|
1871
2120
|
style: { height: 30, fontSize: 12 },
|
|
1872
|
-
onClick: function () { onSave(
|
|
2121
|
+
onClick: function () { onSave(localUrl, localKey); },
|
|
1873
2122
|
}, 'Save'),
|
|
1874
2123
|
h('div', { className: 'ican-conn-status' },
|
|
1875
|
-
h('div', { className:
|
|
1876
|
-
|
|
2124
|
+
h('div', { className: dotClass }),
|
|
2125
|
+
statusText
|
|
1877
2126
|
)
|
|
1878
2127
|
)
|
|
1879
2128
|
);
|
|
@@ -1899,6 +2148,8 @@
|
|
|
1899
2148
|
editMode: false,
|
|
1900
2149
|
layouts: JSON.parse(localStorage.getItem('ican_layouts') || '{}'),
|
|
1901
2150
|
gridWidth: window.innerWidth - 40,
|
|
2151
|
+
connStatus: 'idle', // idle | testing | connected | error
|
|
2152
|
+
connError: '',
|
|
1902
2153
|
};
|
|
1903
2154
|
|
|
1904
2155
|
this._onRegistryUpdate = this._onRegistryUpdate.bind(this);
|
|
@@ -1910,6 +2161,7 @@
|
|
|
1910
2161
|
this._addWidget = this._addWidget.bind(this);
|
|
1911
2162
|
this._removeWidget = this._removeWidget.bind(this);
|
|
1912
2163
|
this._saveConnection = this._saveConnection.bind(this);
|
|
2164
|
+
this._testConnection = this._testConnection.bind(this);
|
|
1913
2165
|
this._toggleEditMode = this._toggleEditMode.bind(this);
|
|
1914
2166
|
this._onLayoutChange = this._onLayoutChange.bind(this);
|
|
1915
2167
|
}
|
|
@@ -1965,12 +2217,50 @@
|
|
|
1965
2217
|
};
|
|
1966
2218
|
|
|
1967
2219
|
App.prototype._saveConnection = function (orchUrl, apiKey) {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2220
|
+
var url = normalizeUrl(orchUrl);
|
|
2221
|
+
var key = (apiKey || '').trim();
|
|
2222
|
+
if (url && !isValidBackendUrl(url)) {
|
|
2223
|
+
showToast('Invalid URL — only https:// or http://localhost allowed', 'error');
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
localStorage.setItem(LS_ORCH_URL, url);
|
|
2227
|
+
localStorage.setItem(LS_API_KEY, key);
|
|
2228
|
+
this.setState({ orchUrl: url, apiKey: key, connStatus: 'idle', connError: '' });
|
|
1971
2229
|
showToast('Connection settings saved', 'success');
|
|
1972
2230
|
};
|
|
1973
2231
|
|
|
2232
|
+
App.prototype._testConnection = function (orchUrl, apiKey) {
|
|
2233
|
+
var self = this;
|
|
2234
|
+
var url = normalizeUrl(orchUrl);
|
|
2235
|
+
var key = (apiKey || '').trim();
|
|
2236
|
+
if (!url || !key) {
|
|
2237
|
+
showToast('Enter the Mythos URL and API Key first', 'info');
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (!isValidBackendUrl(url)) {
|
|
2241
|
+
self.setState({ connStatus: 'error', connError: 'Only https:// or http://localhost allowed' });
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
self.setState({ connStatus: 'testing', connError: '' });
|
|
2245
|
+
fetchWithTimeout(url + '/api/widget-batch', {
|
|
2246
|
+
method: 'POST',
|
|
2247
|
+
headers: { 'Content-Type': 'application/json', 'X-API-Key': key },
|
|
2248
|
+
body: JSON.stringify({ items: [{ id: 'ping', discover: 'actions' }] }),
|
|
2249
|
+
}, 10000).then(function (r) {
|
|
2250
|
+
if (r.status === 401) throw new Error('Unauthorized — check your API Key');
|
|
2251
|
+
if (r.status === 403) throw new Error('Forbidden — API Key lacks required scope');
|
|
2252
|
+
if (!r.ok) throw new Error('Server returned ' + r.status);
|
|
2253
|
+
return r.json();
|
|
2254
|
+
}).then(function () {
|
|
2255
|
+
self.setState({ connStatus: 'connected', connError: '' });
|
|
2256
|
+
showToast('Connection successful', 'success');
|
|
2257
|
+
}).catch(function (e) {
|
|
2258
|
+
var msg = (e && e.message) || 'Connection failed';
|
|
2259
|
+
self.setState({ connStatus: 'error', connError: msg });
|
|
2260
|
+
showToast('Connection failed: ' + msg, 'error');
|
|
2261
|
+
});
|
|
2262
|
+
};
|
|
2263
|
+
|
|
1974
2264
|
App.prototype._toggleEditMode = function () {
|
|
1975
2265
|
this.setState(function (s) { return { editMode: !s.editMode }; });
|
|
1976
2266
|
};
|
|
@@ -2026,6 +2316,23 @@
|
|
|
2026
2316
|
title: 'Development environment',
|
|
2027
2317
|
'aria-label': 'Development environment',
|
|
2028
2318
|
}, 'DEV'),
|
|
2319
|
+
(function () {
|
|
2320
|
+
var cs = s.connStatus;
|
|
2321
|
+
var cfg = !!(s.orchUrl && s.apiKey);
|
|
2322
|
+
var cls = 'ican-live-badge';
|
|
2323
|
+
var label = 'MOCK';
|
|
2324
|
+
if (cs === 'testing') { cls += ' live-testing'; label = '…'; }
|
|
2325
|
+
else if (cs === 'connected') { cls += ' live'; label = 'LIVE'; }
|
|
2326
|
+
else if (cs === 'error') { cls += ' live-error'; label = 'ERROR'; }
|
|
2327
|
+
else if (cfg) { cls += ' live'; label = 'LIVE'; }
|
|
2328
|
+
return h('span', {
|
|
2329
|
+
className: cls,
|
|
2330
|
+
title: cfg
|
|
2331
|
+
? 'Connected to Mythos: ' + s.orchUrl
|
|
2332
|
+
: 'Mock mode — set Mythos URL in Developer Tools',
|
|
2333
|
+
'aria-label': cfg ? 'Live Mythos connection' : 'Mock mode',
|
|
2334
|
+
}, label);
|
|
2335
|
+
})(),
|
|
2029
2336
|
h('div', { className: 'ican-header-spacer' }),
|
|
2030
2337
|
|
|
2031
2338
|
/* Theme switcher */
|
|
@@ -2054,9 +2361,12 @@
|
|
|
2054
2361
|
s.showConn
|
|
2055
2362
|
? h('div', { style: { position: 'fixed', top: 56, left: 0, right: 0, zIndex: 98 } },
|
|
2056
2363
|
h(ConnectionPanel, {
|
|
2057
|
-
orchUrl:
|
|
2058
|
-
apiKey:
|
|
2059
|
-
onSave:
|
|
2364
|
+
orchUrl: s.orchUrl,
|
|
2365
|
+
apiKey: s.apiKey,
|
|
2366
|
+
onSave: self._saveConnection,
|
|
2367
|
+
onTest: self._testConnection,
|
|
2368
|
+
connStatus: s.connStatus,
|
|
2369
|
+
connError: s.connError,
|
|
2060
2370
|
})
|
|
2061
2371
|
)
|
|
2062
2372
|
: null,
|
|
@@ -1727,7 +1727,6 @@
|
|
|
1727
1727
|
if (!window.registerUI) window.registerUI = function () {};
|
|
1728
1728
|
if (!window.registerMenuItem) window.registerMenuItem = function () {};
|
|
1729
1729
|
if (!window.registerLocalization) window.registerLocalization = function () {};
|
|
1730
|
-
if (!window.UXPComponents) window.UXPComponents = {};
|
|
1731
1730
|
if (!window.WidgetDesignerComponents) window.WidgetDesignerComponents = {};
|
|
1732
1731
|
|
|
1733
1732
|
console.log('[ICan] Components loaded — InfAIra Canvas v2.0 (canonical, matches widget-bridge.ts)');
|