@knime/hub-features 1.14.28 → 1.15.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/CHANGELOG.md +19 -0
- package/package.json +3 -3
- package/src/embeddingSDK/guest.ts +124 -0
- package/src/embeddingSDK/host.ts +128 -0
- package/src/embeddingSDK/index.ts +5 -0
- package/src/embeddingSDK/messages.ts +36 -0
- package/src/embeddingSDK/types.ts +61 -0
- package/src/embeddingSDK/util.ts +19 -0
- package/src/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @knime/hub-features
|
|
2
2
|
|
|
3
|
+
## 1.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 34a4517: Add embedding SDK
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [34a4517]
|
|
12
|
+
- @knime/utils@1.7.0
|
|
13
|
+
- @knime/components@1.41.4
|
|
14
|
+
|
|
15
|
+
## 1.14.29
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [dae9c04]
|
|
20
|
+
- @knime/components@1.41.3
|
|
21
|
+
|
|
3
22
|
## 1.14.28
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/hub-features",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Vue components & composables for shared hub features",
|
|
5
5
|
"homepage": "https://knime.github.io/webapps-common/",
|
|
6
6
|
"license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"lodash-es": "4.17.21",
|
|
32
32
|
"ofetch": "^1.4.1",
|
|
33
33
|
"typescript": "^5.8.3",
|
|
34
|
-
"@knime/components": "1.41.
|
|
34
|
+
"@knime/components": "1.41.4",
|
|
35
35
|
"@knime/styles": "1.14.2",
|
|
36
|
-
"@knime/utils": "1.
|
|
36
|
+
"@knime/utils": "1.7.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"consola": "3.x",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import { promise as PromiseUtils } from "@knime/utils";
|
|
3
|
+
|
|
4
|
+
import { MESSAGES } from "./messages";
|
|
5
|
+
import {
|
|
6
|
+
type EmbeddingContext,
|
|
7
|
+
type GenericEvent,
|
|
8
|
+
type UserActivityInfo,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
let __embeddingContext: Readonly<EmbeddingContext>;
|
|
12
|
+
const logger = () => consola.withTag("Embedding SDK::GUEST application");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manually set the Embedding context. Use with caution as this should be done
|
|
16
|
+
* automatically out-of-the-box. It's mostly exposed to be helpful for dev setups
|
|
17
|
+
*/
|
|
18
|
+
export const setContext = (value: EmbeddingContext) => {
|
|
19
|
+
if (__embeddingContext) {
|
|
20
|
+
logger().warn("Cannot set context, it is already set");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
__embeddingContext = Object.freeze(value);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getContext = () => {
|
|
28
|
+
if (!__embeddingContext) {
|
|
29
|
+
logger().warn("Embedding:: Context was accessed before it was set.");
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return __embeddingContext;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Only relevant for tests
|
|
38
|
+
*/
|
|
39
|
+
export const clearContext = () => {
|
|
40
|
+
if (import.meta.env.PROD) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// @ts-expect-error should not be called in prod
|
|
45
|
+
__embeddingContext = undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type EmbeddingContextValidator = (
|
|
49
|
+
context: EmbeddingContext,
|
|
50
|
+
) => { valid: true; error: null } | { valid: false; error: string };
|
|
51
|
+
const defaultvalidator: EmbeddingContextValidator = () => ({
|
|
52
|
+
valid: true,
|
|
53
|
+
error: null,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Wait for host application to send a message containing the embedding context
|
|
58
|
+
*/
|
|
59
|
+
export const waitForContext = (
|
|
60
|
+
validator: EmbeddingContextValidator = defaultvalidator,
|
|
61
|
+
): Promise<EmbeddingContext> => {
|
|
62
|
+
const { promise, reject, resolve } =
|
|
63
|
+
PromiseUtils.createUnwrappedPromise<EmbeddingContext>();
|
|
64
|
+
|
|
65
|
+
function teardown() {
|
|
66
|
+
window.removeEventListener("message", onMessage);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onMessage(event: MessageEvent) {
|
|
70
|
+
if (event.data.type !== MESSAGES.EMBEDDING_CONTEXT) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { data } = event as MessageEvent<{
|
|
75
|
+
type: string;
|
|
76
|
+
payload: EmbeddingContext;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
const validation = validator(data.payload);
|
|
80
|
+
if (!validation.valid) {
|
|
81
|
+
logger().fatal("Incorrect embedding context payload", {
|
|
82
|
+
data,
|
|
83
|
+
error: validation.error,
|
|
84
|
+
});
|
|
85
|
+
reject(new Error(validation.error));
|
|
86
|
+
teardown();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
logger().info("Received embedding context message", data);
|
|
91
|
+
|
|
92
|
+
setContext(data.payload);
|
|
93
|
+
resolve(data.payload);
|
|
94
|
+
teardown();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
window.addEventListener("message", onMessage, false);
|
|
98
|
+
|
|
99
|
+
// send message to parent after postmessage listener has been set-up
|
|
100
|
+
logger().info("Awaiting to receive embedding context");
|
|
101
|
+
window.parent.postMessage({ type: MESSAGES.AWAITING_EMBEDDING_CONTEXT }, "*");
|
|
102
|
+
|
|
103
|
+
return promise;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const sendEmbeddingFailureMessage = (error: unknown) => {
|
|
107
|
+
window.parent.postMessage({ type: MESSAGES.EMBEDDING_FAILED, error }, "*");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const dispatchGenericEventToHost = (event: GenericEvent) => {
|
|
111
|
+
window.parent.postMessage(
|
|
112
|
+
{ type: MESSAGES.GENERIC_EVENT, payload: event },
|
|
113
|
+
"*",
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const notifyActivityChange = (userActivityInfo: UserActivityInfo) => {
|
|
118
|
+
logger().info("Sending user activity info", { payload: userActivityInfo });
|
|
119
|
+
|
|
120
|
+
window.parent.postMessage(
|
|
121
|
+
{ type: MESSAGES.USER_ACTIVITY, payload: userActivityInfo },
|
|
122
|
+
"*",
|
|
123
|
+
);
|
|
124
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
2
|
+
import { MESSAGES } from "./messages";
|
|
3
|
+
import {
|
|
4
|
+
type EmbeddingContext,
|
|
5
|
+
type GenericEvent,
|
|
6
|
+
type GenericEventHandlers,
|
|
7
|
+
type UserActivityInfo,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import { messageValidators } from "./util";
|
|
10
|
+
|
|
11
|
+
const logger = () => consola.withTag("Embedding SDK::HOST embedder");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This function acts as a handshake to make sure the event with the
|
|
15
|
+
* embedding context is only sent once the embedded application is ready to
|
|
16
|
+
* receive it and not earlier. It also listens to possible embedding failure
|
|
17
|
+
* from the child application
|
|
18
|
+
*/
|
|
19
|
+
export const init = (callbacks: {
|
|
20
|
+
onReady: () => void;
|
|
21
|
+
onError: (error: Error) => void;
|
|
22
|
+
}) => {
|
|
23
|
+
function teardown() {
|
|
24
|
+
window.removeEventListener("message", onMessage);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onMessage(event: MessageEvent) {
|
|
28
|
+
if (!messageValidators.isInitializationMessage(event)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (event.data?.type === MESSAGES.AWAITING_EMBEDDING_CONTEXT) {
|
|
33
|
+
logger().info("Got confirmation to send embedding context");
|
|
34
|
+
callbacks.onReady();
|
|
35
|
+
teardown();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (event.data?.type === MESSAGES.EMBEDDING_FAILED) {
|
|
39
|
+
logger().error("Received embedding failure message", {
|
|
40
|
+
event: event.data,
|
|
41
|
+
});
|
|
42
|
+
callbacks.onError(event.data.error);
|
|
43
|
+
teardown();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
window.addEventListener("message", onMessage);
|
|
48
|
+
logger().info("Initialization done");
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const sendEmbeddingContext = (
|
|
52
|
+
iframe: HTMLIFrameElement,
|
|
53
|
+
targetOrigin: string,
|
|
54
|
+
context: EmbeddingContext,
|
|
55
|
+
) => {
|
|
56
|
+
logger().info("Sending embedding context", context);
|
|
57
|
+
|
|
58
|
+
iframe.contentWindow?.postMessage(
|
|
59
|
+
{
|
|
60
|
+
type: MESSAGES.EMBEDDING_CONTEXT,
|
|
61
|
+
payload: {
|
|
62
|
+
// AP uses `url` instead of `wsConnectionUri` in older versions, so it needs
|
|
63
|
+
// to be present to preserve backwards compatibility
|
|
64
|
+
url: context.wsConnectionUri,
|
|
65
|
+
wsConnectionUri: context.wsConnectionUri,
|
|
66
|
+
|
|
67
|
+
restApiBaseUrl: context.restApiBaseUrl,
|
|
68
|
+
userIdleTimeout: context.userIdleTimeout,
|
|
69
|
+
sessionId: context.sessionId,
|
|
70
|
+
jobId: context.jobId,
|
|
71
|
+
} satisfies EmbeddingContext,
|
|
72
|
+
},
|
|
73
|
+
targetOrigin,
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sets up a listener to receive user activity events to detect idleness
|
|
79
|
+
*/
|
|
80
|
+
export const listenToActivityEvents = (callbacks: {
|
|
81
|
+
onActivity: (event: UserActivityInfo) => void;
|
|
82
|
+
}) => {
|
|
83
|
+
const onMessage = (event: MessageEvent) => {
|
|
84
|
+
if (!messageValidators.isUserActivityMessage(event)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger().info("Received user activity change event", {
|
|
89
|
+
event: event.data,
|
|
90
|
+
});
|
|
91
|
+
callbacks.onActivity(event.data.payload);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
window.addEventListener("message", onMessage);
|
|
95
|
+
logger().info("Attaching user activity listener");
|
|
96
|
+
|
|
97
|
+
return () => window.removeEventListener("message", onMessage);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sets up handlers for commands dispatched from the embedded guest application
|
|
102
|
+
*/
|
|
103
|
+
export const setupGenericEventHandlers = <K extends GenericEvent["kind"]>(
|
|
104
|
+
handlers: GenericEventHandlers,
|
|
105
|
+
) => {
|
|
106
|
+
const onMessage = (event: MessageEvent) => {
|
|
107
|
+
if (!messageValidators.isGenericEvent<K>(event)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const genericEvent = event.data.payload;
|
|
112
|
+
logger().info("Received GenericEvent", { event: event.data });
|
|
113
|
+
|
|
114
|
+
const isCallable =
|
|
115
|
+
Object.prototype.hasOwnProperty.call(handlers, genericEvent.kind) &&
|
|
116
|
+
typeof handlers[genericEvent.kind] === "function";
|
|
117
|
+
|
|
118
|
+
if (isCallable) {
|
|
119
|
+
logger().trace("Calling provided handler", { event: event.data });
|
|
120
|
+
handlers[genericEvent.kind]?.(genericEvent);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
window.addEventListener("message", onMessage);
|
|
125
|
+
logger().info("Attaching listener for GenericEvents");
|
|
126
|
+
|
|
127
|
+
return () => window.removeEventListener("message", onMessage);
|
|
128
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ----------- IMPORTANT NOTES ---------------
|
|
3
|
+
* 1. These messages are considered API. So they **must** remain
|
|
4
|
+
* backwards compatible
|
|
5
|
+
* 2. Instead of removal of properties from the embedding context payload,
|
|
6
|
+
* we must use deprecation notes that list which AP version last used the
|
|
7
|
+
* deprecated property
|
|
8
|
+
* 3. The message variable name can change, but not it's value. This is because
|
|
9
|
+
* these values are used since AP browser embedding was first implemented, so
|
|
10
|
+
* they must be retained in order to preserve backwars compatibility
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MESSAGES = {
|
|
14
|
+
/**
|
|
15
|
+
* Message used to establish a handshake and setup
|
|
16
|
+
* before waiting for embedding context transfer
|
|
17
|
+
*/
|
|
18
|
+
AWAITING_EMBEDDING_CONTEXT: "KNIME_UI__AWAITING_CONNECTION_INFO",
|
|
19
|
+
/**
|
|
20
|
+
* Message that contains the embedding context information
|
|
21
|
+
*/
|
|
22
|
+
EMBEDDING_CONTEXT: "KNIME_UI__CONNECTION_INFO",
|
|
23
|
+
/**
|
|
24
|
+
* Message used for when embedding failed
|
|
25
|
+
*/
|
|
26
|
+
EMBEDDING_FAILED: "KNIME_UI__CONNECTION_FAIL",
|
|
27
|
+
/**
|
|
28
|
+
* Message to report user activity events
|
|
29
|
+
*/
|
|
30
|
+
USER_ACTIVITY: "KNIME_UI__USER_ACTIVITY",
|
|
31
|
+
/**
|
|
32
|
+
* General purpose event to transfer data between host application and embedded
|
|
33
|
+
* application.
|
|
34
|
+
*/
|
|
35
|
+
GENERIC_EVENT: "KNIME_UI__GENERIC_EVENT",
|
|
36
|
+
} as const;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Toast } from "@knime/components";
|
|
2
|
+
|
|
3
|
+
export type EmbeddingContext = {
|
|
4
|
+
/**
|
|
5
|
+
* URI of the WS used by the embedded application.
|
|
6
|
+
* (e.g in case of browser AP, this is the WS url provided by the ws-proxy)
|
|
7
|
+
*/
|
|
8
|
+
wsConnectionUri: string;
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated URI of the WS used by the embedded application.
|
|
11
|
+
* Last used in AP 5.8.0
|
|
12
|
+
*/
|
|
13
|
+
url?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Base URL of the Hub's REST API. This will be used by the embedded application
|
|
16
|
+
* to make requests to the hub API. It can also be used to fetch resources
|
|
17
|
+
* dynamically at runtime and get assets needed to render UI extensions
|
|
18
|
+
* (e.g port views, node views, etc)
|
|
19
|
+
*/
|
|
20
|
+
restApiBaseUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* Id of the job loaded into the executor that is providing the embedded application
|
|
23
|
+
*/
|
|
24
|
+
jobId: string;
|
|
25
|
+
/**
|
|
26
|
+
* @deprecated only needed due to backwards compatibility because it's used
|
|
27
|
+
* by older AP versions. Last used in AP 5.4.0
|
|
28
|
+
*/
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Time in MS after which the user is considered idle if no interactions
|
|
32
|
+
* are being made on the page
|
|
33
|
+
*/
|
|
34
|
+
userIdleTimeout?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ShowNotificationEvent = {
|
|
38
|
+
kind: "showNotification";
|
|
39
|
+
payload: Toast;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ClearNotificationEvent = {
|
|
43
|
+
kind: "clearNotification";
|
|
44
|
+
payload: { id: string } | { deduplicationKey: string };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type GenericEvent = ShowNotificationEvent | ClearNotificationEvent;
|
|
48
|
+
|
|
49
|
+
export type GenericEventByKind<K extends GenericEvent["kind"]> = Extract<
|
|
50
|
+
GenericEvent,
|
|
51
|
+
{ kind: K }
|
|
52
|
+
>;
|
|
53
|
+
|
|
54
|
+
export type GenericEventHandlers = {
|
|
55
|
+
[K in GenericEvent["kind"]]?: (event: GenericEventByKind<K>) => void;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type UserActivityInfo = {
|
|
59
|
+
idle: boolean;
|
|
60
|
+
lastActive: string;
|
|
61
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MESSAGES } from "./messages";
|
|
2
|
+
import { type GenericEvent, type GenericEventByKind } from "./types";
|
|
3
|
+
|
|
4
|
+
export const messageValidators = {
|
|
5
|
+
isInitializationMessage: (event: MessageEvent) =>
|
|
6
|
+
[MESSAGES.AWAITING_EMBEDDING_CONTEXT, MESSAGES.EMBEDDING_FAILED].includes(
|
|
7
|
+
event.data?.type,
|
|
8
|
+
),
|
|
9
|
+
|
|
10
|
+
isUserActivityMessage: (event: MessageEvent) =>
|
|
11
|
+
MESSAGES.USER_ACTIVITY === event.data?.type,
|
|
12
|
+
|
|
13
|
+
isGenericEvent: <K extends GenericEvent["kind"]>(
|
|
14
|
+
event: MessageEvent,
|
|
15
|
+
): event is MessageEvent<{
|
|
16
|
+
type: typeof MESSAGES.GENERIC_EVENT;
|
|
17
|
+
payload: GenericEventByKind<K>;
|
|
18
|
+
}> => event.data?.type === MESSAGES.GENERIC_EVENT,
|
|
19
|
+
};
|
package/src/index.ts
CHANGED