@codenotch/codenotch.react 1.0.82 → 2.0.1

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.
@@ -1,4 +1,5 @@
1
- import { ProjectManifest, IAppManifest } from "@codenotch/codenotch.core";
1
+ import type { ReactNode } from "react";
2
+ import { IAppManifest, ICodenotchProjectManifest } from "@codenotch/codenotch.core";
2
3
  import { ICodenotchSignal, IDisposable } from "./Misc";
3
4
 
4
5
  /**
@@ -20,9 +21,7 @@ export interface ICodenotchEnv {
20
21
  accessToken?: string;
21
22
  /** Base URL of the current application. */
22
23
  baseUrl?: string;
23
- /** Translation dictionaries, keyed by language then by translation key: `i18n[language][key] = text`. */
24
- i18n?: { [language: string]: { [key: string]: string } };
25
- /** Current language code (e.g. `"en"`). Defaults to the first language found in {@link i18n}. */
24
+ /** Current language code (e.g. `"en"`). */
26
25
  language?: string;
27
26
  /** Current UI theme. Resolved from the `_theme` URL parameter, or `prefers-color-scheme` as a fallback. */
28
27
  theme?: 'light' | 'dark';
@@ -30,94 +29,12 @@ export interface ICodenotchEnv {
30
29
  /** Manifest of the current application (the `<AppName>.manifest.json` file next to the app). */
31
30
  appManifest?: IAppManifest;
32
31
  /** Manifest of the Codenotch project (the project's `manifest.json` file). */
33
- projectManifest?: ProjectManifest;
32
+ projectManifest?: ICodenotchProjectManifest;
34
33
 
35
34
  /** Any additional environment variable passed to `init()`. */
36
35
  [key: string]: any; // Allow additional properties
37
36
  }
38
37
 
39
-
40
- /**
41
- * Registry of the project's BPMN processes, used to type {@link ICodenotchApi.startProcess}.
42
- *
43
- * Empty by default: the Codenotch IDE fills it through declaration merging in the
44
- * auto-generated `typings/process.d.ts` file of each project, with one entry per
45
- * BPMN process. Each entry maps the process name to its start events (`nodes`,
46
- * keyed by start node id, valued with the start event's input parameters) and to
47
- * the `output` of its end event.
48
- *
49
- * When the registry has not been augmented (project built without the IDE
50
- * typings), `startProcess` falls back to plain string names and untyped inputs,
51
- * so the code still compiles.
52
- *
53
- * @example
54
- * // typings/process.d.ts (auto-generated by the Codenotch IDE)
55
- * import 'codenotch-react';
56
- * declare module 'codenotch-react' {
57
- * interface ProcessRegistry {
58
- * 'getTodos': {
59
- * nodes: { 'start': { UserId: string } };
60
- * output: { todos: any[] };
61
- * };
62
- * }
63
- * }
64
- */
65
- export interface ProcessRegistry {
66
- // Extended by codenotch IDE
67
- }
68
-
69
- /**
70
- * Registry of the project's translation keys, used to type {@link ICodenotchApi.i18n}.
71
- *
72
- * Empty by default: the Codenotch IDE fills it through declaration merging in the
73
- * auto-generated `typings/i18n.d.ts` file of each project, from the project's
74
- * `.i18n.csv` files. Each entry maps a translation key to the tuple of arguments
75
- * expected by its `{0}`, `{1}`, … placeholders.
76
- *
77
- * When the registry has not been augmented (project built without the IDE
78
- * typings), `i18n` falls back to plain string keys and untyped arguments,
79
- * so the code still compiles.
80
- *
81
- * @example
82
- * // typings/i18n.d.ts (auto-generated by the Codenotch IDE)
83
- * import 'codenotch-react';
84
- * declare module 'codenotch-react' {
85
- * interface TranslationRegistry {
86
- * 'welcome': [];
87
- * 'greeting': [arg1: any, arg2: any];
88
- * }
89
- * }
90
- */
91
- export interface TranslationRegistry {
92
- // Extended by codenotch IDE
93
- }
94
-
95
- // When the codenotch IDE has not augmented the registries, `keyof Registry` is `never`
96
- // and every call would fail to compile. These helpers fall back to plain string keys
97
- // and untyped arguments so the API stays usable without code generation.
98
-
99
- /** A BPMN process name: a key of {@link ProcessRegistry}, or any string when the registry is empty. */
100
- export type ProcessName = keyof ProcessRegistry extends never ? string : keyof ProcessRegistry;
101
-
102
- /** The start nodes of process `P`: its `nodes` map from {@link ProcessRegistry}, or an open map when unknown. */
103
- export type ProcessNodes<P> = P extends keyof ProcessRegistry
104
- ? ProcessRegistry[P] extends { nodes: infer N } ? N : { [nodeId: string]: any }
105
- : { [nodeId: string]: any };
106
-
107
- /** The end-event output of process `P` from {@link ProcessRegistry}, or `any` when unknown. */
108
- export type ProcessOutput<P> = P extends keyof ProcessRegistry
109
- ? ProcessRegistry[P] extends { output: infer O } ? O : any
110
- : any;
111
-
112
- /** A translation key: a key of {@link TranslationRegistry}, or any string when the registry is empty. */
113
- export type TranslationKey = keyof TranslationRegistry extends never ? string : keyof TranslationRegistry;
114
-
115
- /** The placeholder arguments of translation key `K` from {@link TranslationRegistry}, or `any[]` when unknown. */
116
- export type TranslationArgs<K> = K extends keyof TranslationRegistry
117
- ? TranslationRegistry[K] extends any[] ? TranslationRegistry[K] : any[]
118
- : any[];
119
-
120
-
121
38
  /**
122
39
  * Result of a BPMN process execution started with {@link ICodenotchApi.startProcess}.
123
40
  */
@@ -134,59 +51,24 @@ export interface IProcessResult<T = any> {
134
51
  }
135
52
 
136
53
  /**
137
- * The Codenotch client API, obtained by calling `useCodenotch()`.
54
+ * The Codenotch client API.
55
+ *
56
+ * Obtained with the `useCodenotch()` hook in function components, through the
57
+ * `cn` prop injected by `withCodenotch()` in class components, or with the
58
+ * plain `getCodenotch()` function anywhere else (handlers, modules, services).
138
59
  *
139
60
  * It is the bridge between a React app and the Codenotch runtime: it starts
140
- * server-side BPMN processes, runs CNQL queries, translates i18n keys,
61
+ * server-side BPMN processes, runs CNQL queries,
141
62
  * listens to real-time signals and manages theme/language.
142
- *
143
- * @example
144
- * import { useCodenotch } from 'codenotch-react';
145
- *
146
- * const cn = useCodenotch();
147
- * const title = cn.i18n('welcome');
148
- * const result = await cn.startProcess('getTodos', 'start', { UserId: '42' });
149
63
  */
150
64
  export interface ICodenotchApi {
151
65
 
152
66
  /**
153
67
  * Start a server-side BPMN process and wait for its completion.
154
- *
155
- * @param processName Name of the process — the `.bpmn` file name without extension (e.g. `processes/getTodos.bpmn` → `'getTodos'`).
156
- * @param startNodeId Id of the start event to trigger (conventionally `'start'`).
157
- * @param inputs Input parameters declared by that start event.
158
- * @returns The process result; check `isError` before using `output` (the parameters of the end event reached).
159
- * @example
160
- * const result = await cn.startProcess('getTodos', 'start', { UserId: userId });
161
- * if (!result.isError) {
162
- * setTodos(result.output.todos);
163
- * }
164
- */
165
- startProcess<P extends ProcessName, N extends keyof ProcessNodes<P>>(
166
- processName: P,
167
- startNodeId: N,
168
- inputs: ProcessNodes<P>[N]
169
- ): Promise<IProcessResult<ProcessOutput<P>>>;
170
-
171
- /**
172
- * Translate a key for the current language ({@link ICodenotchEnv.language}).
173
- *
174
- * Placeholders `{0}`, `{1}`, … in the translated text are replaced by `args`.
175
- * Never throws: if the key or the language is missing, a warning is logged
176
- * and the key itself is returned.
177
- *
178
- * @param key Translation key, as defined in the project's `.i18n.csv` files.
179
- * @param args Values for the `{0}`, `{1}`, … placeholders of the translation.
180
- * @example
181
- * cn.i18n('welcome'); // "Bienvenue"
182
- * cn.i18n('greeting', 'Fabian', 3); // "Bonjour Fabian, 3 messages" (from "Bonjour {0}, {1} messages")
183
68
  */
184
- i18n<K extends TranslationKey>(
185
- key: K,
186
- ...args: TranslationArgs<K>
187
- ): string;
69
+ startProcess<TProcess, TInput, TOutput>(process: TProcess, inputs: TInput): Promise<IProcessResult<TOutput>>;
188
70
 
189
- /** Runtime environment (cluster, service, language, theme, i18n dictionaries, manifests…). Populated by `init()`. */
71
+ /** Runtime environment (cluster, service, language, theme dictionaries, manifests…). Populated by `init()`. */
190
72
  readonly env: ICodenotchEnv;
191
73
 
192
74
  /**
@@ -223,7 +105,7 @@ export interface ICodenotchApi {
223
105
  * </div>
224
106
  * );
225
107
  */
226
- readonly showDialog: (node: JSX.Element) => ICodenotchDialog;
108
+ readonly showDialog: (node: ReactNode) => ICodenotchDialog;
227
109
 
228
110
  /**
229
111
  * Fetch the text content of a file of the deployed Codenotch project.
@@ -238,26 +120,11 @@ export interface ICodenotchApi {
238
120
  */
239
121
  readonly getProjectFileUrl: (relativePath: string) => string;
240
122
 
241
- /**
242
- * Subscribe to a real-time signal emitted by the project's BPMN processes (via SignalR).
243
- *
244
- * @param signalId Id of the signal, as declared in the BPMN process.
245
- * @param callback Called each time the signal is received, with `{ signalId, data }`.
246
- * @returns A disposable — call `dispose()` to unsubscribe (e.g. in a `useEffect` cleanup).
247
- * @example
248
- * useEffect(() => {
249
- * let sub: IDisposable | undefined;
250
- * cn.listenSignal('todosChanged', () => refresh()).then(s => sub = s);
251
- * return () => sub?.dispose();
252
- * }, []);
253
- */
254
- readonly listenSignal: (signalId: string, callback: (data: ICodenotchSignal) => void) => Promise<IDisposable>;
255
-
256
123
  /** Set the UI theme. Also toggles the `light`/`dark` class on `<html>` (Tailwind `dark:` variants). */
257
124
  readonly setTheme: (theme: 'light' | 'dark') => void;
258
125
  /** Current UI theme, if resolved. */
259
126
  readonly getTheme: () => 'light' | 'dark' | undefined;
260
- /** Set the current language used by {@link i18n}. Must be one of the project's languages. */
127
+ /** Set the current language. Must be one of the project's languages. */
261
128
  readonly setLanguage: (lang: string) => void;
262
129
  /** Current language code, if any. */
263
130
  readonly getLanguage: () => string | undefined;
@@ -268,11 +135,25 @@ export interface ICodenotchApi {
268
135
  /** Generate a random UUID v4. */
269
136
  readonly uuid: () => string;
270
137
  /** Manifest of the Codenotch project. Throws if not available in the environment. */
271
- readonly getProjectManifest: () => ProjectManifest;
138
+ readonly getProjectManifest: () => ICodenotchProjectManifest;
272
139
  /** Manifest of the current application, if any. */
273
140
  readonly getAppManifest: () => IAppManifest | undefined;
274
141
  }
275
142
 
143
+ /**
144
+ * Props injected by the `withCodenotch()` higher-order component.
145
+ * Extend it in the props of a class component wrapped by `withCodenotch`.
146
+ *
147
+ * @example
148
+ * interface Props extends WithCodenotchProps { userId: string }
149
+ * class TodoList extends React.Component<Props> { ... }
150
+ * export default withCodenotch(TodoList);
151
+ */
152
+ export interface WithCodenotchProps {
153
+ /** The Codenotch client API; refreshed (new reference) whenever the environment changes. */
154
+ cn: ICodenotchApi;
155
+ }
156
+
276
157
  /**
277
158
  * Handle on a dialog opened with {@link ICodenotchApi.showDialog}.
278
159
  */
package/README.md DELETED
@@ -1,145 +0,0 @@
1
- # codenotch-react
2
-
3
- React bindings for [Codenotch](https://codenotch.com) applications.
4
-
5
- Codenotch is a full-stack development platform: a project combines server-side **BPMN processes**, **SQL tables** / **NoSQL documents**, **i18n** translations and **React** apps. This package is the client-side bridge between those React apps and the Codenotch runtime: it lets a component start BPMN processes, run CNQL queries, translate i18n keys, listen to real-time signals, and manage theme/language.
6
-
7
- > **Requirements** — Codenotch apps run on **React 16** (`react@^16.14.0`). Do not use React 17/18 APIs (`createRoot`, `useId`, automatic JSX runtime…); hooks work fine.
8
-
9
- ## Quick start
10
-
11
- ```tsx
12
- import React from 'react';
13
- import { useCodenotch } from 'codenotch-react';
14
-
15
- const MyApp: React.FC = () => {
16
- const cn = useCodenotch();
17
-
18
- return <div className="p-4">
19
- <h1 className="text-xl font-bold">{cn.i18n('welcome')}</h1>
20
- </div>;
21
- };
22
-
23
- export default MyApp;
24
- ```
25
-
26
- `useCodenotch()` is **not a React hook** despite its name: it is a plain function that returns the API object bound to the current environment. It can be called anywhere — components, event handlers, plain modules.
27
-
28
- The environment (cluster URL, service name, language, translations…) is set up by `init(envVariables)`, which the Codenotch runtime calls automatically from the page hosting the application. You only call `init` yourself in custom hosting scenarios.
29
-
30
- ## API overview
31
-
32
- | Member | Description |
33
- |--------|-------------|
34
- | `cn.env` | Readonly environment: `clusterUrl`, `serviceName`, `tenantName`, `accessToken`, `language`, `theme`, `i18n`, `appManifest`, `projectManifest`. |
35
- | `cn.i18n(key, ...args)` | Translate a key for the current language, filling `{0}`, `{1}`, … placeholders. |
36
- | `cn.startProcess(name, startNodeId, inputs)` | Start a server-side BPMN process and await its result. |
37
- | `cn.requestCnql(cnql, verbose?)` | Run a CNQL query (XML, SELECT-only) against the project's tables. |
38
- | `cn.listenSignal(signalId, callback)` | Subscribe to real-time signals emitted by BPMN processes. |
39
- | `cn.showDialog(jsx)` | Render a JSX element in a fullscreen modal dialog. |
40
- | `cn.setTheme(t)` / `cn.getTheme()` | `'light' \| 'dark'`; `setTheme` also toggles the class on `<html>` (Tailwind `dark:`). |
41
- | `cn.setLanguage(l)` / `cn.getLanguage()` / `cn.getLanguages()` | Current language and the languages declared in `manifest.json`. |
42
- | `cn.getProjectFile(path)` / `cn.getProjectFileUrl(path)` | Read/link a file of the deployed project. |
43
- | `cn.getUrlParams()` | Query-string parameters of the current URL as a plain object. |
44
- | `cn.uuid()` | Random UUID v4. |
45
- | `cn.getProjectManifest()` / `cn.getAppManifest()` | Project / application manifests. |
46
-
47
- Full signatures and JSDoc live in `dist/index.d.ts` (sources in `src/`).
48
-
49
- ## Calling a BPMN process
50
-
51
- `processName` is the `.bpmn` file name without extension; `startNodeId` is the id of the start event to trigger (conventionally `'start'`); `inputs` are the input parameters declared by that start event. The result's `output` contains the parameters of the end event reached.
52
-
53
- ```tsx
54
- const result = await cn.startProcess('getTodos', 'start', { UserId: userId });
55
-
56
- if (!result.isError) {
57
- setTodos(result.output.todos);
58
- } else {
59
- console.error(result.errorMessage);
60
- }
61
- ```
62
-
63
- ## Querying tables with CNQL
64
-
65
- CNQL is Codenotch's XML query language over the project's SQL tables (SELECT only — writes go through BPMN processes). The root `xmlns` must be the project's `serviceName`; each queried table's `Ref` attribute names its result set:
66
-
67
- ```tsx
68
- const data = await cn.requestCnql(`
69
- <CNQL xmlns="myproject" PageSize="10" PageIndex="0">
70
- <Users Ref="results">
71
- <Id />
72
- <Email />
73
- <IsAdmin Equal="true" />
74
- </Users>
75
- </CNQL>`);
76
-
77
- console.log(data.results); // [{ Id: '...', Email: '...' }, ...]
78
- ```
79
-
80
- ## Translations (i18n)
81
-
82
- Translations come from the project's `.i18n.csv` files and are injected into `cn.env.i18n` at startup. `cn.i18n(key, ...args)` translates for the current language and replaces `{0}`, `{1}`, … placeholders; it never throws — an unknown key is returned as-is (with a console warning).
83
-
84
- ```tsx
85
- cn.i18n('welcome'); // "Bienvenue"
86
- cn.i18n('greeting', 'Ada', 3); // "Bonjour Ada, 3 messages" (from "Bonjour {0}, {1} messages")
87
- cn.setLanguage('en'); // switch language at runtime
88
- ```
89
-
90
- ## Real-time signals
91
-
92
- BPMN processes can broadcast signals; subscribe from the UI with `listenSignal`:
93
-
94
- ```tsx
95
- useEffect(() => {
96
- let sub: IDisposable | undefined;
97
- cn.listenSignal('todosChanged', (signal) => refresh(signal.data))
98
- .then(s => { sub = s; });
99
- return () => sub?.dispose();
100
- }, []);
101
- ```
102
-
103
- ## Dialogs
104
-
105
- ```tsx
106
- const dialog = cn.showDialog(
107
- <div className="bg-white dark:bg-gray-800 p-6 rounded shadow">
108
- <p>{cn.i18n('confirm.message')}</p>
109
- <button onClick={() => dialog.close()}>{cn.i18n('close')}</button>
110
- </div>
111
- );
112
- ```
113
-
114
- The dialog stays open until you call `dialog.close()`.
115
-
116
- ## Type-safe processes and translation keys
117
-
118
- `startProcess` and `i18n` are typed through two registries, `ProcessRegistry` and `TranslationRegistry`, that the **Codenotch IDE fills by declaration merging**: it generates `typings/process.d.ts` and `typings/i18n.d.ts` in each project from the `.bpmn` and `.i18n.csv` files (never edit those files — refresh the project instead). The generated files look like:
119
-
120
- ```ts
121
- import 'codenotch-react';
122
-
123
- declare module 'codenotch-react' {
124
- interface TranslationRegistry {
125
- 'welcome': [];
126
- 'greeting': [arg1: any, arg2: any];
127
- }
128
-
129
- interface ProcessRegistry {
130
- 'getTodos': {
131
- nodes: { 'start': { UserId: string } };
132
- output: { todos: any[] };
133
- };
134
- }
135
- }
136
- ```
137
-
138
- With these in the compilation, keys, inputs and outputs are strictly checked and autocompleted. **Without them** (project compiled outside the IDE), the registries are empty and both methods gracefully fall back to plain `string` keys and untyped arguments — the code still compiles.
139
-
140
- ## Notes for AI assistants
141
-
142
- - The complete typed API surface is in `dist/index.d.ts`; the readable implementation ships in `src/` (entry point `src/index.ts`).
143
- - `useCodenotch()` is a plain function, not a hook — no hook rules apply.
144
- - Never hand-edit a project's `typings/i18n.d.ts` / `typings/process.d.ts`: they are regenerated by the Codenotch IDE.
145
- - Codenotch apps are React 16 + Tailwind CSS; CNQL is SELECT-only (writes go through BPMN processes via `startProcess`).
@@ -1,46 +0,0 @@
1
- import React from "react";
2
- import { SpaRenderStatus } from '@echino/echino.ui.framework/components/SpaBuilder/common/ISpaRenderProps';
3
- import type { ICodenotchEnv } from "../models/Codenotch";
4
- interface IAumlProps {
5
- /** The AUML (XML) application description to render. */
6
- value: string;
7
- /** When `true`, logs compilation and token-refresh details to the console. */
8
- verbose?: boolean;
9
- /** Codenotch environment; provides the i18n dictionaries injected into the AUML and the theme. */
10
- env?: ICodenotchEnv;
11
- }
12
- interface IAumlState {
13
- loaded: boolean;
14
- inspectorEnabled: boolean;
15
- }
16
- /**
17
- * Renders a legacy AUML (XML) application description inside a React app.
18
- *
19
- * Compiles the AUML with the environment's i18n variables, renders it through
20
- * the Echino SPA renderer, shows a loading overlay until completed, and keeps
21
- * the user's access token refreshed in the background.
22
- *
23
- * Relies on globals injected by the Codenotch runtime hosting page
24
- * (`serviceName`, `tenant`, `user`, `languages`, `appManifest`…) — it is not
25
- * usable outside a Codenotch-served application.
26
- */
27
- export declare class Auml extends React.Component<IAumlProps, IAumlState> {
28
- _refreshTokenTimer: NodeJS.Timeout | undefined;
29
- constructor(props: IAumlProps);
30
- componentDidMount(): void;
31
- registerRefreshToken(): void;
32
- componentWillUnmount(): void;
33
- progress(s: SpaRenderStatus): void;
34
- retrieveInputs(): {
35
- [k: string]: string;
36
- };
37
- getTimeToTokenExpiration(): number | null;
38
- getTimeToNewTokenExpiration(expirationDateTime: string): number;
39
- msToTime(ms: number): string;
40
- parseJwt(token: string): any;
41
- render(): React.JSX.Element;
42
- renderLoadingContent(): React.JSX.Element;
43
- refreshExpiredToken(): Promise<void>;
44
- }
45
- export {};
46
- //# sourceMappingURL=Auml.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Auml.d.ts","sourceRoot":"","sources":["../../src/components/Auml.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,eAAe,EAAE,MAAM,0EAA0E,CAAC;AAE3G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,UAAU,UAAU;IAChB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kGAAkG;IAClG,GAAG,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,UAAU,UAAU;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,IAAK,SAAQ,KAAK,CAAC,SAAS,CAAC,UAAU,EAAE,UAAU,CAAC;IAE7D,kBAAkB,EAAE,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;gBAEnC,KAAK,EAAE,UAAU;IAa7B,iBAAiB;IAIjB,oBAAoB;IAoCpB,oBAAoB;IAOpB,QAAQ,CAAC,CAAC,EAAE,eAAe;IAS3B,cAAc;;;IAMd,wBAAwB,IAAI,MAAM,GAAG,IAAI;IAqCzC,2BAA2B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM;IAO/D,QAAQ,CAAC,EAAE,EAAE,MAAM;IAmBnB,QAAQ,CAAC,KAAK,EAAE,MAAM;IAUtB,MAAM;IAoDN,oBAAoB;IAkBd,mBAAmB;CAoC5B"}
@@ -1,208 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Auml = void 0;
7
- const react_1 = __importDefault(require("react"));
8
- const SpaRenderWithBrowserRouter_1 = require("@echino/echino.ui.framework/components/SpaBuilder/SpaRender/SpaRenderWithBrowserRouter");
9
- const I18nUtils_1 = __importDefault(require("../utils/I18nUtils"));
10
- /**
11
- * Renders a legacy AUML (XML) application description inside a React app.
12
- *
13
- * Compiles the AUML with the environment's i18n variables, renders it through
14
- * the Echino SPA renderer, shows a loading overlay until completed, and keeps
15
- * the user's access token refreshed in the background.
16
- *
17
- * Relies on globals injected by the Codenotch runtime hosting page
18
- * (`serviceName`, `tenant`, `user`, `languages`, `appManifest`…) — it is not
19
- * usable outside a Codenotch-served application.
20
- */
21
- class Auml extends react_1.default.Component {
22
- constructor(props) {
23
- super(props);
24
- this.state = {
25
- loaded: false,
26
- inspectorEnabled: false
27
- };
28
- if (typeof user === 'undefined') {
29
- let globalObject = typeof window !== 'undefined' ? window : globalThis;
30
- globalObject['user'] = null; // Make sure user is defined
31
- }
32
- }
33
- componentDidMount() {
34
- this.registerRefreshToken();
35
- }
36
- registerRefreshToken() {
37
- try {
38
- // Setup a method to refresh our access token when it expires
39
- if (this._refreshTokenTimer) {
40
- clearTimeout(this._refreshTokenTimer);
41
- }
42
- if (user === null) {
43
- if (this.props.verbose === true) {
44
- console.log("No user found, will not refresh the token");
45
- }
46
- return;
47
- }
48
- let timeToExpireMs = this.getTimeToTokenExpiration();
49
- if (timeToExpireMs === null) {
50
- if (this.props.verbose === true) {
51
- console.log("No identity token found, attempting refreshing the token in 5 minutes");
52
- }
53
- this._refreshTokenTimer = setTimeout(() => this.refreshExpiredToken(), 5 * 60 * 1000);
54
- return;
55
- }
56
- if (timeToExpireMs <= 0) {
57
- // Refresh it immediatelty
58
- this.refreshExpiredToken();
59
- }
60
- else {
61
- this._refreshTokenTimer = setTimeout(() => this.refreshExpiredToken(), timeToExpireMs);
62
- }
63
- }
64
- catch (err) {
65
- console.warn("Could not setup token refresh timer: " + err);
66
- }
67
- }
68
- componentWillUnmount() {
69
- // Cleanup the refresh of the token
70
- if (this._refreshTokenTimer) {
71
- clearTimeout(this._refreshTokenTimer);
72
- }
73
- }
74
- progress(s) {
75
- //console.log('progessStatus ', s);
76
- if (s === 'completed') {
77
- this.setState({ loaded: true });
78
- }
79
- }
80
- // Get input from the query parameters in the url
81
- retrieveInputs() {
82
- return Object.fromEntries(new URLSearchParams(window.location.search).entries());
83
- }
84
- getTimeToTokenExpiration() {
85
- // The identity token is the only one we have access here on the client
86
- // We use it to know when the access token (which we can't read) will expire
87
- let identityToken = null;
88
- let identityTokenKey = `${tenant.name}IdToken=`;
89
- let cookies = document.cookie.split(';');
90
- for (let c of cookies) {
91
- if (c.trim().startsWith(identityTokenKey)) {
92
- identityToken = c.trim().slice(identityTokenKey.length);
93
- break;
94
- }
95
- }
96
- if (identityToken === null) {
97
- return null; // token not found
98
- }
99
- let identityTokenParsed = this.parseJwt(identityToken);
100
- let expires = identityTokenParsed.exp; // Timestamp in second since Unix epoch
101
- let timeToExpiresMs = expires * 1000 - new Date().getTime();
102
- let timeToExpireStr = this.msToTime(timeToExpiresMs);
103
- if (this.props.verbose === true) {
104
- console.log(`Token will expire at ${new Date(expires * 1000).toISOString()} (in ${timeToExpireStr}), setting up a timer to refresh it`);
105
- }
106
- // Refresh it a bit before the expiration (5 min)
107
- timeToExpiresMs -= 5 * 60 * 1000;
108
- return timeToExpiresMs;
109
- }
110
- getTimeToNewTokenExpiration(expirationDateTime) {
111
- let timeToExpiresMs = new Date(expirationDateTime).getTime() - new Date().getTime();
112
- // Refresh it a bit before the expiration (5 min)
113
- timeToExpiresMs -= 5 * 60 * 1000;
114
- return timeToExpiresMs;
115
- }
116
- msToTime(ms) {
117
- let seconds = Math.floor((ms / 1000) % 60), minutes = Math.floor((ms / (1000 * 60)) % 60), hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
118
- let timeString = seconds + " seconds";
119
- if (minutes > 0) {
120
- timeString = minutes + " minutes " + timeString;
121
- }
122
- if (hours > 0) {
123
- timeString = hours + " hours " + timeString;
124
- }
125
- return timeString;
126
- }
127
- parseJwt(token) {
128
- var base64Url = token.split('.')[1];
129
- var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
130
- var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
131
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
132
- }).join(''));
133
- return JSON.parse(jsonPayload);
134
- }
135
- render() {
136
- let inputs = this.retrieveInputs();
137
- let theme = this.props.env?.theme === "dark";
138
- let aumlManifest = null;
139
- try {
140
- let appManifestObj = JSON.parse(appManifest);
141
- aumlManifest = appManifestObj.auml;
142
- }
143
- catch { }
144
- let auml = this.props.value;
145
- if (this.props.env) {
146
- try {
147
- auml = I18nUtils_1.default.compileAuml(auml, this.props.env);
148
- if (this.props.verbose === true) {
149
- console.log("Compiled AUML with i18n variables: ", auml);
150
- console.log("i18n variables: ", this.props.env.i18n);
151
- }
152
- }
153
- catch (err) {
154
- console.error("Error compiling AUML with i18n variables: " + err);
155
- }
156
- }
157
- return react_1.default.createElement("div", { className: "app" },
158
- react_1.default.createElement(SpaRenderWithBrowserRouter_1.SpaRenderWithBrowserRouter, { appDescription: auml, tenant: tenant, serviceName: serviceName, packageVersions: packageVersions, user: user, languages: languages, onProgress: (s) => this.progress(s), input: inputs, theme: theme, manifest: aumlManifest, inspectorEnabled: this.state.inspectorEnabled, children: [] }),
159
- !this.state.loaded &&
160
- react_1.default.createElement("div", { className: `app-loading ${this.props.env?.theme ?? 'light'}` },
161
- this.renderLoadingContent(),
162
- react_1.default.createElement("i", { className: "fas fa-circle-notch fa-spin" })));
163
- }
164
- renderLoadingContent() {
165
- try {
166
- //@ts-ignore
167
- let tenant = global.tenant;
168
- if (tenant.logoUrl) {
169
- return react_1.default.createElement("img", { src: tenant.logoUrl, alt: tenant.displayName });
170
- }
171
- else {
172
- return react_1.default.createElement("div", { className: 'app-loading-title' }, tenant.displayName);
173
- }
174
- }
175
- catch (e) {
176
- console.warn("Could not load tenant information: " + e);
177
- return react_1.default.createElement("div", { className: 'app-loading-title' }, "Loading...");
178
- }
179
- }
180
- async refreshExpiredToken() {
181
- console.log("Token will expire soon, requesting a new one...");
182
- // Using the refresh token we ask for a new access token using /portal/login/refresh
183
- // If the refresh token has also expired, we will be redirected to the login page
184
- let redirectUrl = window.location.href; // Where to send us back in case we need to be redirected to the login page
185
- let url = `${tenant.clusterUrl}/portal/login/refresh?redirectUrl=${encodeURIComponent(redirectUrl)}`;
186
- let response = await fetch(url); // For this request to work, we need to have a refresh token in the cookies
187
- if (response.ok) {
188
- // Setup next refresh
189
- let timeToExpireMs;
190
- try {
191
- let newTokenExpiration = await response.text();
192
- timeToExpireMs = this.getTimeToNewTokenExpiration(newTokenExpiration);
193
- }
194
- catch {
195
- timeToExpireMs = 2 * 60 * 60 * 1000; // refresh in 2 hours
196
- }
197
- console.log(`Refresh request ok, next refresh in ${this.msToTime(timeToExpireMs)}`);
198
- this._refreshTokenTimer = setTimeout(() => this.refreshExpiredToken(), timeToExpireMs);
199
- }
200
- else {
201
- let content = await response.text();
202
- console.error("Could not refresh the token", content);
203
- console.log("Retrying refreshing the token in 5 minutes...");
204
- this._refreshTokenTimer = setTimeout(() => this.refreshExpiredToken(), 5 * 60 * 1000);
205
- }
206
- }
207
- }
208
- exports.Auml = Auml;