@vertesia/ui 0.54.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/LICENSE +13 -0
- package/README.md +1 -0
- package/package.json +70 -0
- package/src/context/HostContext.ts +73 -0
- package/src/context/index.ts +20 -0
- package/src/host/PluginHost.tsx +37 -0
- package/src/host/PluginManager.tsx +197 -0
- package/src/host/index.ts +2 -0
- package/src/index.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2024 Composable Prompts
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Composable Prompts hooks for React
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vertesia/ui",
|
|
3
|
+
"version": "0.54.0",
|
|
4
|
+
"description": "Vertesia UI components and and hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"homepage": "https://docs.vertesiahq.com",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vertesia",
|
|
15
|
+
"UI",
|
|
16
|
+
"react",
|
|
17
|
+
"components",
|
|
18
|
+
"hooks"
|
|
19
|
+
],
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/react": "^19.1.0",
|
|
22
|
+
"@types/react-dom": "^19.1.1",
|
|
23
|
+
"typescript": "^5.0.2"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=19.0.0",
|
|
27
|
+
"react-dom": ">=19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"optionalDependencies": {
|
|
30
|
+
"react": ">=19.0.0",
|
|
31
|
+
"react-dom": ">=19.0.0"
|
|
32
|
+
},
|
|
33
|
+
"types": "./lib/index.d.ts",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@vertesia/client": "0.54.0",
|
|
36
|
+
"@vertesia/common": "0.54.0"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": {
|
|
40
|
+
"types": "./lib/index.d.ts",
|
|
41
|
+
"default": "./lib/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./host": {
|
|
44
|
+
"types": "./lib/host/index.d.ts",
|
|
45
|
+
"default": "./lib/host/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./context": {
|
|
48
|
+
"types": "./lib/context/index.d.ts",
|
|
49
|
+
"default": "./lib/context/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"typesVersions": {
|
|
53
|
+
"*": {
|
|
54
|
+
".": [
|
|
55
|
+
"./lib/index.d.ts"
|
|
56
|
+
],
|
|
57
|
+
"host": [
|
|
58
|
+
"./lib/host.d.ts"
|
|
59
|
+
],
|
|
60
|
+
"context": [
|
|
61
|
+
"./lib/context/index.d.ts"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"eslint": "eslint './src/**/*.{jsx,js,tsx,ts}'",
|
|
67
|
+
"build": "rm -rf ./lib ./tsconfig.tsbuildinfo && tsc --build",
|
|
68
|
+
"clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { VertesiaClient, ZenoClient } from "@vertesia/client";
|
|
2
|
+
import type { AccountRef, AuthTokenPayload, ProjectRef } from "@vertesia/common";
|
|
3
|
+
|
|
4
|
+
export const HOST_CONTEXT_VAR = '__vetesia_host_context__';
|
|
5
|
+
|
|
6
|
+
// TODO This file redeclare some types from the composable UI / router
|
|
7
|
+
// we need to make these pblics and move shared composable-ui parts here.
|
|
8
|
+
|
|
9
|
+
export interface UserSession {
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
client: VertesiaClient;
|
|
12
|
+
authError?: Error;
|
|
13
|
+
authToken?: AuthTokenPayload;
|
|
14
|
+
lastSelectedAccount?: string | null;
|
|
15
|
+
lastSelectedProject?: string | null;
|
|
16
|
+
onboardingComplete?: boolean;
|
|
17
|
+
store: ZenoClient;
|
|
18
|
+
user?: AuthTokenPayload;
|
|
19
|
+
account?: AccountRef;
|
|
20
|
+
accounts?: AccountRef[];
|
|
21
|
+
project?: ProjectRef;
|
|
22
|
+
rawAuthToken: Promise<string>
|
|
23
|
+
}
|
|
24
|
+
export interface NavigateOptions {
|
|
25
|
+
replace?: boolean;
|
|
26
|
+
state?: any;
|
|
27
|
+
/**
|
|
28
|
+
* if defined prepend the basePath to the `to` argument
|
|
29
|
+
*/
|
|
30
|
+
basePath?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RouterContext {
|
|
34
|
+
location: Location,
|
|
35
|
+
// route: Route,
|
|
36
|
+
// router: BaseRouter,
|
|
37
|
+
params: Record<string, string>,
|
|
38
|
+
state: any,
|
|
39
|
+
/**
|
|
40
|
+
* The path that matched the route. For widlcard `/*` paths this doens;t include the wildcard part.
|
|
41
|
+
* You can get the wildcard path from `remainingPath`.
|
|
42
|
+
*/
|
|
43
|
+
matchedRoutePath: string,
|
|
44
|
+
remainingPath?: string,
|
|
45
|
+
navigate: (path: string, options?: NavigateOptions) => void;
|
|
46
|
+
}
|
|
47
|
+
type LazyImportFn = () => Promise<any>;
|
|
48
|
+
interface ComponentRoute {
|
|
49
|
+
path: string;
|
|
50
|
+
Component: React.ComponentType<any>;
|
|
51
|
+
}
|
|
52
|
+
interface LazyComponentRoute {
|
|
53
|
+
path: string;
|
|
54
|
+
LazyComponent: LazyImportFn;
|
|
55
|
+
}
|
|
56
|
+
type Route = ComponentRoute | LazyComponentRoute;
|
|
57
|
+
export interface MultiPagePluginProps {
|
|
58
|
+
title: string;
|
|
59
|
+
routes: Route[],
|
|
60
|
+
/**
|
|
61
|
+
* The path to use for the root resource. Defaults to '/'. Cannot contains path vairables or wildcards
|
|
62
|
+
*/
|
|
63
|
+
index?: string;
|
|
64
|
+
children?: React.ReactNode;
|
|
65
|
+
fixLinks?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface HostContext {
|
|
68
|
+
useUserSession: () => UserSession;
|
|
69
|
+
useRouterContext: () => RouterContext;
|
|
70
|
+
useNavigate: () => (path: string, options?: NavigateOptions) => void;
|
|
71
|
+
useLocation: () => Location;
|
|
72
|
+
MultiPagePlugin: React.ComponentType<MultiPagePluginProps>;
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file contains the host context which is a set of live components and hooks shared
|
|
3
|
+
* with the plugins.
|
|
4
|
+
* The plugin must import a component or hook from the host context by using:
|
|
5
|
+
* import {SomeComponnet, useSomeHook } from '@vetesia/ui-extension-sdk/context';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HOST_CONTEXT_VAR, HostContext } from "./HostContext.js";
|
|
9
|
+
|
|
10
|
+
const context = (globalThis as any)[HOST_CONTEXT_VAR] as HostContext;
|
|
11
|
+
|
|
12
|
+
export const {
|
|
13
|
+
useUserSession,
|
|
14
|
+
useRouterContext,
|
|
15
|
+
useNavigate,
|
|
16
|
+
useLocation,
|
|
17
|
+
MultiPagePlugin,
|
|
18
|
+
} = context;
|
|
19
|
+
|
|
20
|
+
export * from "./HostContext.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useInsertionEffect, useState } from "react";
|
|
2
|
+
import { usePluginModule } from "./PluginManager.js";
|
|
3
|
+
import { HOST_CONTEXT_VAR, HostContext } from "../context/HostContext.js";
|
|
4
|
+
|
|
5
|
+
function createSharedContext(context: HostContext): HostContext {
|
|
6
|
+
(globalThis as any)[HOST_CONTEXT_VAR] = context;
|
|
7
|
+
return context;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PluginHost({ pluginId, slot, context }: { pluginId: string, slot: string, context: HostContext }) {
|
|
11
|
+
const [contextCreated, setContextCreated] = useState(false);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
createSharedContext(context);
|
|
14
|
+
setContextCreated(true);
|
|
15
|
+
}, []);
|
|
16
|
+
return contextCreated && <_PluginHost pluginId={pluginId} slot={slot} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _PluginHost({ pluginId, slot }: { pluginId: string, slot: string }) {
|
|
20
|
+
const { plugin, module, error } = usePluginModule(pluginId);
|
|
21
|
+
useInsertionEffect(() => {
|
|
22
|
+
if (module) {
|
|
23
|
+
plugin!.install();
|
|
24
|
+
}
|
|
25
|
+
}, [module]);
|
|
26
|
+
|
|
27
|
+
if (!plugin) {
|
|
28
|
+
return <div>Plugin {pluginId} not found</div>
|
|
29
|
+
} else if (error) {
|
|
30
|
+
return <div>Failed to load plugin {plugin.manifest.name} from {plugin.manifest.src}: {error.message}</div>
|
|
31
|
+
} else if (module) {
|
|
32
|
+
return module.mount(slot)
|
|
33
|
+
} else {
|
|
34
|
+
return <div>Loading ...</div>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { PluginManifest } from "@vertesia/common";
|
|
3
|
+
|
|
4
|
+
export enum PluginInstanceStatus {
|
|
5
|
+
registered,
|
|
6
|
+
loading,
|
|
7
|
+
loaded,
|
|
8
|
+
error, //loading error
|
|
9
|
+
installed,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PluginModule {
|
|
13
|
+
mount(slot: string): React.ReactNode;
|
|
14
|
+
css?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class PluginInstance {
|
|
18
|
+
status: PluginInstanceStatus = PluginInstanceStatus.registered;
|
|
19
|
+
_module?: PluginModule;
|
|
20
|
+
error?: Error;
|
|
21
|
+
|
|
22
|
+
constructor(public manifest: PluginManifest) {
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get isInstalled() {
|
|
26
|
+
return this.status === PluginInstanceStatus.installed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get isLoading() {
|
|
30
|
+
return this.status === PluginInstanceStatus.loading;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get styleId() {
|
|
34
|
+
return `plugin-style-${this.manifest.id}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async _load() {
|
|
38
|
+
if (this.status === PluginInstanceStatus.registered) {
|
|
39
|
+
try {
|
|
40
|
+
const module = await import(/* @vite-ignore */ this.manifest.src);
|
|
41
|
+
this.status = PluginInstanceStatus.loading;
|
|
42
|
+
if (typeof module.mount !== "function") {
|
|
43
|
+
throw new Error(`Plugin ${this.manifest.id} does not provide a mount function`);
|
|
44
|
+
}
|
|
45
|
+
this._module = module;
|
|
46
|
+
this.status = PluginInstanceStatus.loaded;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
this.status = PluginInstanceStatus.error;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`Plugin ${this.manifest.id} was already loaded`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getModule(): Promise<PluginModule> {
|
|
57
|
+
if (!this._module) {
|
|
58
|
+
await this._load();
|
|
59
|
+
}
|
|
60
|
+
return this._module!;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
install() {
|
|
64
|
+
if (this.status === PluginInstanceStatus.loaded && this._module) {
|
|
65
|
+
const module = this._module;
|
|
66
|
+
if (module.css) {
|
|
67
|
+
// inject css
|
|
68
|
+
let style = document.getElementById(this.styleId) as HTMLStyleElement;
|
|
69
|
+
if (!style) {
|
|
70
|
+
style = document.createElement('style');
|
|
71
|
+
style.id = this.styleId;
|
|
72
|
+
style.appendChild(document.createTextNode(module.css));
|
|
73
|
+
document.head.appendChild(style);
|
|
74
|
+
this.status = PluginInstanceStatus.installed;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else if (this.status !== PluginInstanceStatus.installed) {
|
|
78
|
+
throw new Error(`Plugin ${this.manifest.id} is not loaded: ` + this.status);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async uninstall() {
|
|
83
|
+
if (this.status === PluginInstanceStatus.installed) {
|
|
84
|
+
const style = document.getElementById(this.styleId);
|
|
85
|
+
if (style) {
|
|
86
|
+
style.remove();
|
|
87
|
+
this.status = PluginInstanceStatus.loaded;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class PluginManager {
|
|
94
|
+
plugins: Record<string, PluginInstance> = {};
|
|
95
|
+
constructor(manifests: PluginManifest[] = []) {
|
|
96
|
+
this.addAll(manifests);
|
|
97
|
+
}
|
|
98
|
+
addAll(manifests: PluginManifest[]) {
|
|
99
|
+
return manifests.map(manifest => this.add(manifest));
|
|
100
|
+
}
|
|
101
|
+
add(manifest: PluginManifest) {
|
|
102
|
+
const instance = new PluginInstance(manifest);
|
|
103
|
+
this.plugins[manifest.id] = instance;
|
|
104
|
+
return instance;
|
|
105
|
+
}
|
|
106
|
+
remove(id: string) {
|
|
107
|
+
const instance = this.plugins[id];
|
|
108
|
+
if (instance) {
|
|
109
|
+
instance.uninstall();
|
|
110
|
+
delete this.plugins[id];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
get(id: string) {
|
|
114
|
+
return this.plugins[id];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface PluginManagerState {
|
|
119
|
+
manager: PluginManager;
|
|
120
|
+
refresh(): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const PluginManagerContext = createContext<PluginManagerState | null>(null);
|
|
124
|
+
|
|
125
|
+
interface PluginsProviderProps {
|
|
126
|
+
plugins: PluginManifest[];
|
|
127
|
+
children?: React.ReactNode | React.ReactNode[];
|
|
128
|
+
}
|
|
129
|
+
export function PluginsProvider({ plugins, children }: PluginsProviderProps) {
|
|
130
|
+
const [key, setKey] = useState(0);
|
|
131
|
+
const manager = useMemo(() => new PluginManager(plugins), [plugins]);
|
|
132
|
+
const ctx = {
|
|
133
|
+
manager,
|
|
134
|
+
refresh: () => setKey(key + 1),
|
|
135
|
+
}
|
|
136
|
+
return <PluginManagerContext.Provider key={key} value={ctx}>
|
|
137
|
+
{children}
|
|
138
|
+
</PluginManagerContext.Provider>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function usePluginManager() {
|
|
142
|
+
const ctx = useContext(PluginManagerContext);
|
|
143
|
+
if (!ctx) {
|
|
144
|
+
throw new Error('No PluginManagerContext found');
|
|
145
|
+
}
|
|
146
|
+
return ctx;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function usePluginInstance(id: string) {
|
|
150
|
+
return usePluginManager().manager.get(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the plugin instance and load the module. Returns null if the plugin is not yet loaded.
|
|
155
|
+
* When the plugin loads then returns the plugin instance
|
|
156
|
+
* @param id
|
|
157
|
+
* @returns
|
|
158
|
+
*/
|
|
159
|
+
export function usePluginModule(id: string): {
|
|
160
|
+
plugin: PluginInstance | null,
|
|
161
|
+
module: PluginModule | null,
|
|
162
|
+
error: Error | null,
|
|
163
|
+
} {
|
|
164
|
+
const [module, setModule] = useState<PluginModule | null>(null);
|
|
165
|
+
const { manager } = usePluginManager();
|
|
166
|
+
const plugin = manager.get(id);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (plugin) {
|
|
169
|
+
plugin.getModule().then(setModule);
|
|
170
|
+
}
|
|
171
|
+
}, [plugin]);
|
|
172
|
+
if (!plugin) {
|
|
173
|
+
return {
|
|
174
|
+
plugin: null,
|
|
175
|
+
module: null,
|
|
176
|
+
error: new Error(`Plugin ${id} not found`),
|
|
177
|
+
}
|
|
178
|
+
} else if (plugin.error) {
|
|
179
|
+
return {
|
|
180
|
+
plugin: plugin,
|
|
181
|
+
module: plugin._module || null,
|
|
182
|
+
error: plugin.error,
|
|
183
|
+
};
|
|
184
|
+
} else if (module) {
|
|
185
|
+
return {
|
|
186
|
+
plugin: plugin,
|
|
187
|
+
module: module,
|
|
188
|
+
error: null,
|
|
189
|
+
};
|
|
190
|
+
} else {
|
|
191
|
+
return {
|
|
192
|
+
plugin: plugin,
|
|
193
|
+
module: module,
|
|
194
|
+
error: plugin.error || null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// TODO export here common components, hooks and utility functions
|