@knime/hub-features 1.14.29 → 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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 1.14.29
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/hub-features",
3
- "version": "1.14.29",
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.3",
34
+ "@knime/components": "1.41.4",
35
35
  "@knime/styles": "1.14.2",
36
- "@knime/utils": "1.6.2"
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,5 @@
1
+ import * as guest from "./guest";
2
+ import * as host from "./host";
3
+
4
+ export const embeddingSDK = { host, guest };
5
+ export * from "./types";
@@ -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
@@ -2,3 +2,4 @@ export * from "./rfcErrors";
2
2
  export * from "./useFileUpload";
3
3
  export * from "./useDownloadArtifact";
4
4
  export * from "./components/avatars";
5
+ export * from "./embeddingSDK";