@vertesia/ui 0.72.0 → 0.73.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/lib/esm/features/facets/RunsFacetsNav.js +15 -5
- package/lib/esm/features/facets/RunsFacetsNav.js.map +1 -1
- package/lib/esm/shell/apps/AppInstallationProvider.js +13 -0
- package/lib/esm/shell/apps/AppInstallationProvider.js.map +1 -0
- package/lib/esm/shell/apps/AppProjectSelector.js +25 -0
- package/lib/esm/shell/apps/AppProjectSelector.js.map +1 -0
- package/lib/esm/shell/apps/StandaloneApp.js +60 -0
- package/lib/esm/shell/apps/StandaloneApp.js.map +1 -0
- package/lib/esm/shell/apps/index.js +3 -1
- package/lib/esm/shell/apps/index.js.map +1 -1
- package/lib/esm/shell/login/InviteAcceptModal.js +1 -1
- package/lib/esm/shell/login/InviteAcceptModal.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/features/facets/RunsFacetsNav.d.ts +1 -0
- package/lib/types/features/facets/RunsFacetsNav.d.ts.map +1 -1
- package/lib/types/features/facets/VFacetsNav.d.ts +1 -1
- package/lib/types/features/facets/VFacetsNav.d.ts.map +1 -1
- package/lib/types/shell/apps/AppInstallationProvider.d.ts +12 -0
- package/lib/types/shell/apps/AppInstallationProvider.d.ts.map +1 -0
- package/lib/types/shell/apps/AppProjectSelector.d.ts +12 -0
- package/lib/types/shell/apps/AppProjectSelector.d.ts.map +1 -0
- package/lib/types/shell/apps/StandaloneApp.d.ts +23 -0
- package/lib/types/shell/apps/StandaloneApp.d.ts.map +1 -0
- package/lib/types/shell/apps/index.d.ts +3 -1
- package/lib/types/shell/apps/index.d.ts.map +1 -1
- package/lib/vertesia-ui-features.js +1 -1
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-shell.js +1 -1
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/package.json +7 -7
- package/src/features/facets/RunsFacetsNav.tsx +17 -5
- package/src/features/facets/VFacetsNav.tsx +1 -1
- package/src/shell/apps/AppInstallationProvider.tsx +22 -0
- package/src/shell/apps/AppProjectSelector.tsx +47 -0
- package/src/shell/apps/StandaloneApp.tsx +108 -0
- package/src/shell/apps/index.ts +3 -1
- package/src/shell/login/InviteAcceptModal.tsx +1 -1
- package/lib/esm/shell/apps/CheckAppAccess.js +0 -23
- package/lib/esm/shell/apps/CheckAppAccess.js.map +0 -1
- package/lib/types/shell/apps/CheckAppAccess.d.ts +0 -6
- package/lib/types/shell/apps/CheckAppAccess.d.ts.map +0 -1
- package/src/shell/apps/CheckAppAccess.tsx +0 -25
|
@@ -13,6 +13,7 @@ interface RunsFacetsNavProps {
|
|
|
13
13
|
environments?: any[];
|
|
14
14
|
models?: any[];
|
|
15
15
|
statuses?: any[];
|
|
16
|
+
tags?: any[];
|
|
16
17
|
finish_reason?: any[];
|
|
17
18
|
created_by?: any[];
|
|
18
19
|
};
|
|
@@ -35,16 +36,25 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
|
|
|
35
36
|
if (facets.environments) {
|
|
36
37
|
const environmentFilterGroup = VEnvironmentFacet({
|
|
37
38
|
buckets: facets.environments || [],
|
|
38
|
-
name: '
|
|
39
|
+
name: 'environments',
|
|
39
40
|
});
|
|
40
41
|
customFilterGroups.push(environmentFilterGroup);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// Add tags filter as stringList type (allows custom input)
|
|
45
|
+
const tagsFilterGroup = {
|
|
46
|
+
name: 'tags',
|
|
47
|
+
placeholder: 'Tags',
|
|
48
|
+
type: 'stringList' as const,
|
|
49
|
+
multiple: true
|
|
50
|
+
};
|
|
51
|
+
customFilterGroups.push(tagsFilterGroup);
|
|
52
|
+
|
|
43
53
|
if (facets.models) {
|
|
44
54
|
const modelFilterGroup = VStringFacet({
|
|
45
55
|
search: null as any, // This will be provided by the search context
|
|
46
56
|
buckets: facets.models || [],
|
|
47
|
-
name: '
|
|
57
|
+
name: 'model'
|
|
48
58
|
});
|
|
49
59
|
customFilterGroups.push(modelFilterGroup);
|
|
50
60
|
}
|
|
@@ -53,7 +63,7 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
|
|
|
53
63
|
const statusFilterGroup = VStringFacet({
|
|
54
64
|
search: null as any, // This will be provided by the search context
|
|
55
65
|
buckets: facets.statuses || [],
|
|
56
|
-
name: '
|
|
66
|
+
name: 'status'
|
|
57
67
|
});
|
|
58
68
|
customFilterGroups.push(statusFilterGroup);
|
|
59
69
|
}
|
|
@@ -105,11 +115,13 @@ export function useRunsFilterGroups(facets: RunsFacetsNavProps['facets']): Filte
|
|
|
105
115
|
export function useRunsFilterHandler(search: SearchInterface) {
|
|
106
116
|
return (newFilters: BaseFilter[]) => {
|
|
107
117
|
if (newFilters.length === 0) {
|
|
108
|
-
|
|
118
|
+
// Clear filters without applying defaults - user wants to remove all filters
|
|
119
|
+
search.clearFilters(true, false);
|
|
109
120
|
return;
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
|
|
123
|
+
// Clear all filters first without defaults, then apply new ones
|
|
124
|
+
search.clearFilters(false, false);
|
|
113
125
|
|
|
114
126
|
newFilters.forEach(filter => {
|
|
115
127
|
if (filter.value && filter.value.length > 0) {
|
|
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
export interface SearchInterface {
|
|
5
5
|
getFilterValue(name: string): any;
|
|
6
6
|
setFilterValue(name: string, value: any): void;
|
|
7
|
-
clearFilters(autoSearch?: boolean): void;
|
|
7
|
+
clearFilters(autoSearch?: boolean, applyDefaults?: boolean): void;
|
|
8
8
|
search(): Promise<boolean | undefined>;
|
|
9
9
|
readonly isRunning: boolean;
|
|
10
10
|
query: Record<string, any>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AppInstallationWithManifest } from "@vertesia/common";
|
|
2
|
+
import { createContext, ReactNode, useContext } from "react";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const AppInstallationContext = createContext<AppInstallationWithManifest | null>(null);
|
|
6
|
+
|
|
7
|
+
export function AppInstallationProvider({ installation, children }: { installation: AppInstallationWithManifest, children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<AppInstallationContext.Provider value={installation}>
|
|
10
|
+
{children}
|
|
11
|
+
</AppInstallationContext.Provider>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the current app installation obejct when called in an app context otheriwse returns null
|
|
17
|
+
*/
|
|
18
|
+
export function useAppInstallation() {
|
|
19
|
+
return useContext(AppInstallationContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ProjectRef, RequireAtLeastOne } from "@vertesia/common";
|
|
2
|
+
import { SelectBox, useFetch } from "@vertesia/ui/core";
|
|
3
|
+
import { useUserSession } from "@vertesia/ui/session";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
interface AppProjectSelectorProps {
|
|
7
|
+
app: RequireAtLeastOne<{ id?: string, name?: string }, 'id' | 'name'>;
|
|
8
|
+
onChange: (value: ProjectRef) => void;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
}
|
|
11
|
+
export function AppProjectSelector({ app, onChange, placeholder }: AppProjectSelectorProps) {
|
|
12
|
+
const { client, project } = useUserSession();
|
|
13
|
+
const { data: projects, error } = useFetch(() => {
|
|
14
|
+
return client.apps.getAppInstallationProjects(app);
|
|
15
|
+
}, [app.id, app.name])
|
|
16
|
+
|
|
17
|
+
if (error) {
|
|
18
|
+
return <span className='text-red-600'>Error: failed to fetch projects: {error.message}</span>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <SelectProject placeholder={placeholder} initialValue={project?.id} projects={projects || []} onChange={onChange} />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SelectProjectProps {
|
|
25
|
+
initialValue?: string
|
|
26
|
+
projects: ProjectRef[]
|
|
27
|
+
onChange: (value: ProjectRef) => void
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
}
|
|
30
|
+
function SelectProject({ initialValue, projects, onChange, placeholder = "Select Project" }: Readonly<SelectProjectProps>) {
|
|
31
|
+
const [value, setValue] = useState<ProjectRef | undefined>(() => {
|
|
32
|
+
return initialValue ? projects.find(p => p.id === initialValue) : undefined
|
|
33
|
+
});
|
|
34
|
+
const _onChange = (value: ProjectRef) => {
|
|
35
|
+
setValue(value)
|
|
36
|
+
onChange(value)
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
<SelectBox
|
|
40
|
+
by="id"
|
|
41
|
+
value={value}
|
|
42
|
+
options={projects}
|
|
43
|
+
optionLabel={(option) => option.name}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
onChange={_onChange} />
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AppInstallationWithManifest, ProjectRef } from "@vertesia/common";
|
|
2
|
+
import { Center } from "@vertesia/ui/core";
|
|
3
|
+
import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY, useUserSession } from "@vertesia/ui/session";
|
|
4
|
+
import { LockIcon } from "lucide-react";
|
|
5
|
+
import { ComponentType, ReactNode, useEffect, useState } from "react";
|
|
6
|
+
import { AppInstallationProvider } from "./AppInstallationProvider";
|
|
7
|
+
import { AppProjectSelector } from "./AppProjectSelector";
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
interface StandaloneAppProps {
|
|
11
|
+
/**
|
|
12
|
+
* The app name.
|
|
13
|
+
* The name must be the name used to register the app in Vertesia. It will be used to check if thre user has access to the app.
|
|
14
|
+
*
|
|
15
|
+
* Also, this component is providing an AppInfo context that can be retrieved using the useAppInfo() hook.
|
|
16
|
+
*/
|
|
17
|
+
name: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A react element to display if the access is denied to the app.
|
|
21
|
+
* If not specified a simple message will be displayed
|
|
22
|
+
*/
|
|
23
|
+
AccessDenied?: ComponentType<AccessDeniedMessageProps>;
|
|
24
|
+
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
export function StandaloneApp({ name, AccessDenied = AccessDeniedMessage, children }: StandaloneAppProps) {
|
|
28
|
+
return name ? (
|
|
29
|
+
<StandaloneAppImpl name={name} AccessDenied={AccessDenied}>{children}</StandaloneAppImpl>
|
|
30
|
+
) : (
|
|
31
|
+
<UnknownAppName />
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
export function StandaloneAppImpl({ name, AccessDenied = AccessDeniedMessage, children }: StandaloneAppProps) {
|
|
35
|
+
const { authToken, client } = useUserSession();
|
|
36
|
+
const [installation, setInstallation] = useState<AppInstallationWithManifest | null>(null)
|
|
37
|
+
const [state, setState] = useState<"loading" | "error" | "loaded">("loading");
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!authToken) {
|
|
41
|
+
setState("loading");
|
|
42
|
+
} else {
|
|
43
|
+
const isAppVisible = authToken.apps.includes(name);
|
|
44
|
+
if (isAppVisible) {
|
|
45
|
+
client.apps.getAppInstallationByName(name).then(inst => {
|
|
46
|
+
if (!inst) {
|
|
47
|
+
console.log(`App ${name} not found!`);
|
|
48
|
+
setState("error");
|
|
49
|
+
} else {
|
|
50
|
+
setState("loaded");
|
|
51
|
+
setInstallation(inst);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
setState("error");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, [name, authToken]);
|
|
59
|
+
|
|
60
|
+
if (state === "loading") {
|
|
61
|
+
return null;
|
|
62
|
+
} else if (state === "error") {
|
|
63
|
+
return <AccessDenied name={name} />
|
|
64
|
+
} else if (installation) {
|
|
65
|
+
return <AppInstallationProvider installation={installation}>
|
|
66
|
+
{children}
|
|
67
|
+
</AppInstallationProvider>
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface AccessDeniedMessageProps {
|
|
72
|
+
name: string;
|
|
73
|
+
}
|
|
74
|
+
function AccessDeniedMessage({ name }: AccessDeniedMessageProps) {
|
|
75
|
+
const { project } = useUserSession();
|
|
76
|
+
const onChange = (project: ProjectRef) => {
|
|
77
|
+
localStorage.setItem(LastSelectedAccountId_KEY, project.account);
|
|
78
|
+
localStorage.setItem(LastSelectedProjectId_KEY + '-' + project.account, project.id);
|
|
79
|
+
window.location.reload();
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<Center className="pt-10 flex flex-col items-center text-center text-gray-700">
|
|
83
|
+
<LockIcon className="w-10 h-10 mb-4 text-gray-500" />
|
|
84
|
+
<div className="text-xl font-semibold">Access Denied</div>
|
|
85
|
+
<div className="mt-2 text-sm text-gray-500">
|
|
86
|
+
You don't have permission to view the <span className="font-semibold">{name}</span> app in project: <span className="font-semibold">«{project?.name}»</span>.
|
|
87
|
+
</div>
|
|
88
|
+
<div className="mt-4">
|
|
89
|
+
<AppProjectSelector app={{ name }} onChange={onChange} />
|
|
90
|
+
</div>
|
|
91
|
+
</Center>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function UnknownAppName() {
|
|
96
|
+
return (
|
|
97
|
+
<Center className="pt-10 flex flex-col items-center text-center text-gray-700">
|
|
98
|
+
<LockIcon className="w-10 h-10 mb-4 text-gray-500" />
|
|
99
|
+
<div className="text-xl font-semibold">Application not registered</div>
|
|
100
|
+
<div className="mt-2 text-sm text-gray-500">
|
|
101
|
+
Before starting to code a Vertesia application you must register an application manifest
|
|
102
|
+
in Vertesia Studio then install it in one or more projects.
|
|
103
|
+
<p />
|
|
104
|
+
Then use the created app name as a parameter to <code><StandaloneApp name="your-app-name"></code> in the <code>src/main.ts</code> file.
|
|
105
|
+
</div>
|
|
106
|
+
</Center>
|
|
107
|
+
)
|
|
108
|
+
}
|
package/src/shell/apps/index.ts
CHANGED
|
@@ -68,7 +68,7 @@ export function InviteAcceptModal() {
|
|
|
68
68
|
<div className="w-full font-semibold">{invite.data.account.name ?? invite.data.account}</div>
|
|
69
69
|
{invite.data.project && <div className="w-full text-base">- {invite.data.project.name}</div>}
|
|
70
70
|
<div className="text-xs">Role: {invite.data.role}</div>
|
|
71
|
-
<div className="text-xs">by {invite.data.
|
|
71
|
+
<div className="text-xs">by {invite.data.invited_by.name}</div>
|
|
72
72
|
</div>
|
|
73
73
|
<div className="flex flex-col gap-4">
|
|
74
74
|
<Button size={'xs'} onClick={() => accept(invite)}>Accept</Button> <Button size={'xs'} variant="secondary" onClick={() => reject(invite)}>Reject</Button>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { ErrorBox } from "@vertesia/ui/core";
|
|
3
|
-
import { useUserSession } from "@vertesia/ui/session";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
export function CheckAppAccess({ name, children }) {
|
|
6
|
-
const [hasAccess, setHasAccess] = useState(null);
|
|
7
|
-
const { authToken } = useUserSession();
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
if (authToken) {
|
|
10
|
-
setHasAccess(authToken.apps?.includes(name) || false);
|
|
11
|
-
}
|
|
12
|
-
}, [authToken]);
|
|
13
|
-
if (hasAccess == null) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
if (hasAccess) {
|
|
17
|
-
return children;
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
return _jsx(ErrorBox, { title: "Forbidden", children: "Access is not granted to this application" });
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=CheckAppAccess.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CheckAppAccess.js","sourceRoot":"","sources":["../../../../src/shell/apps/CheckAppAccess.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAa,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGvD,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAyC;IACpF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAiB,IAAI,CAAC,CAAC;IACjE,MAAM,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,CAAC;IAEvC,SAAS,CAAC,GAAG,EAAE;QACX,IAAI,SAAS,EAAE,CAAC;YACZ,YAAY,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC;QAC1D,CAAC;IACL,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACZ,OAAO,QAAQ,CAAC;IACpB,CAAC;SAAM,CAAC;QACJ,OAAO,KAAC,QAAQ,IAAC,KAAK,EAAC,WAAW,0DAAqD,CAAA;IAC3F,CAAC;AACL,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { ReactNode } from "react";
|
|
2
|
-
export declare function CheckAppAccess({ name, children }: {
|
|
3
|
-
name: string;
|
|
4
|
-
children: ReactNode;
|
|
5
|
-
}): string | number | bigint | boolean | import("react/jsx-runtime").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
6
|
-
//# sourceMappingURL=CheckAppAccess.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"CheckAppAccess.d.ts","sourceRoot":"","sources":["../../../../src/shell/apps/CheckAppAccess.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAuB,MAAM,OAAO,CAAC;AAGvD,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,2UAmBvF"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { ErrorBox } from "@vertesia/ui/core";
|
|
2
|
-
import { useUserSession } from "@vertesia/ui/session";
|
|
3
|
-
import { ReactNode, useEffect, useState } from "react";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export function CheckAppAccess({ name, children }: { name: string, children: ReactNode }) {
|
|
7
|
-
const [hasAccess, setHasAccess] = useState<boolean | null>(null);
|
|
8
|
-
const { authToken } = useUserSession();
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
if (authToken) {
|
|
12
|
-
setHasAccess(authToken.apps?.includes(name) || false);
|
|
13
|
-
}
|
|
14
|
-
}, [authToken])
|
|
15
|
-
|
|
16
|
-
if (hasAccess == null) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (hasAccess) {
|
|
21
|
-
return children;
|
|
22
|
-
} else {
|
|
23
|
-
return <ErrorBox title="Forbidden">Access is not granted to this application</ErrorBox>
|
|
24
|
-
}
|
|
25
|
-
}
|