@object-ui/app-shell 5.1.1 → 5.3.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 +322 -0
- package/dist/chrome/CommandPalette.d.ts +6 -1
- package/dist/chrome/CommandPalette.js +40 -4
- package/dist/chrome/ConsoleToaster.js +8 -1
- package/dist/chrome/ErrorBoundary.js +3 -0
- package/dist/chrome/RouteFader.d.ts +32 -0
- package/dist/chrome/RouteFader.js +52 -0
- package/dist/chrome/index.d.ts +2 -0
- package/dist/chrome/index.js +2 -0
- package/dist/chrome/toast-helpers.d.ts +48 -0
- package/dist/chrome/toast-helpers.js +59 -0
- package/dist/console/AppContent.js +108 -2
- package/dist/console/home/HomePage.js +5 -4
- package/dist/console/marketplace/MarkdownText.d.ts +19 -0
- package/dist/console/marketplace/MarkdownText.js +141 -0
- package/dist/console/marketplace/MarketplaceInstalledPage.d.ts +17 -0
- package/dist/console/marketplace/MarketplaceInstalledPage.js +64 -0
- package/dist/console/marketplace/MarketplacePackagePage.d.ts +7 -0
- package/dist/console/marketplace/MarketplacePackagePage.js +200 -0
- package/dist/console/marketplace/MarketplacePage.d.ts +8 -0
- package/dist/console/marketplace/MarketplacePage.js +94 -0
- package/dist/console/marketplace/PackageIcon.d.ts +19 -0
- package/dist/console/marketplace/PackageIcon.js +17 -0
- package/dist/console/marketplace/marketplaceApi.d.ts +122 -0
- package/dist/console/marketplace/marketplaceApi.js +207 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +6 -5
- package/dist/layout/AppHeader.js +16 -2
- package/dist/layout/AppSidebar.js +15 -11
- package/dist/layout/InboxPopover.js +43 -3
- package/dist/observability/index.d.ts +9 -0
- package/dist/observability/index.js +9 -0
- package/dist/observability/sentry.d.ts +46 -0
- package/dist/observability/sentry.js +120 -0
- package/dist/types.d.ts +0 -46
- package/dist/views/RecordDetailView.js +79 -15
- package/package.json +26 -25
- package/src/styles.css +49 -0
- package/dist/components/DashboardRenderer.d.ts +0 -8
- package/dist/components/DashboardRenderer.js +0 -16
- package/dist/components/FormRenderer.d.ts +0 -8
- package/dist/components/FormRenderer.js +0 -31
- package/dist/components/ObjectRenderer.d.ts +0 -8
- package/dist/components/ObjectRenderer.js +0 -74
- package/dist/components/PageRenderer.d.ts +0 -7
- package/dist/components/PageRenderer.js +0 -14
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Marketplace Package Detail Page.
|
|
4
|
+
*
|
|
5
|
+
* Shows full package metadata + readme + approved version list. Provides
|
|
6
|
+
* an "Install" button that opens a dialog with environment picker.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
10
|
+
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
11
|
+
import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2 } from 'lucide-react';
|
|
12
|
+
import { PackageIcon } from './PackageIcon';
|
|
13
|
+
import { MarkdownText } from './MarkdownText';
|
|
14
|
+
import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
|
|
15
|
+
export function MarketplacePackagePage() {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const { packageId, appName } = useParams();
|
|
18
|
+
const basePath = appName ? `/apps/${appName}` : '';
|
|
19
|
+
const [data, setData] = useState(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const [installOpen, setInstallOpen] = useState(false);
|
|
23
|
+
const [envs, setEnvs] = useState([]);
|
|
24
|
+
const [envsLoading, setEnvsLoading] = useState(false);
|
|
25
|
+
const [envsError, setEnvsError] = useState(null);
|
|
26
|
+
const [selectedEnv, setSelectedEnv] = useState('');
|
|
27
|
+
const [seedSampleData, setSeedSampleData] = useState(false);
|
|
28
|
+
const [installing, setInstalling] = useState(false);
|
|
29
|
+
const [installResult, setInstallResult] = useState(null);
|
|
30
|
+
// Local-install state (this runtime's own kernel — separate flow from cloud).
|
|
31
|
+
const [localInstalls, setLocalInstalls] = useState([]);
|
|
32
|
+
const [installingLocal, setInstallingLocal] = useState(false);
|
|
33
|
+
const [localResult, setLocalResult] = useState(null);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
(async () => {
|
|
37
|
+
const items = await listLocalInstalls();
|
|
38
|
+
if (!cancelled)
|
|
39
|
+
setLocalInstalls(items);
|
|
40
|
+
})();
|
|
41
|
+
return () => { cancelled = true; };
|
|
42
|
+
}, [packageId, localResult]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
(async () => {
|
|
46
|
+
if (!packageId)
|
|
47
|
+
return;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
const resp = await getMarketplacePackage(packageId);
|
|
52
|
+
if (!cancelled)
|
|
53
|
+
setData(resp);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
if (!cancelled)
|
|
57
|
+
setError(e?.message ?? String(e));
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (!cancelled)
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
return () => { cancelled = true; };
|
|
65
|
+
}, [packageId]);
|
|
66
|
+
const openInstall = async () => {
|
|
67
|
+
setInstallOpen(true);
|
|
68
|
+
setInstallResult(null);
|
|
69
|
+
setEnvsError(null);
|
|
70
|
+
setEnvsLoading(true);
|
|
71
|
+
try {
|
|
72
|
+
const [list, adminOrgIds] = await Promise.all([
|
|
73
|
+
listCloudEnvironments(),
|
|
74
|
+
listInstallableOrgIds(),
|
|
75
|
+
]);
|
|
76
|
+
// Only envs in orgs where the caller is owner/admin are installable.
|
|
77
|
+
// Backend enforces the same gate; mirroring it here avoids confusing
|
|
78
|
+
// 403s and lets us show a helpful empty-state message.
|
|
79
|
+
const installable = list.filter((env) => {
|
|
80
|
+
const orgId = env.organization_id;
|
|
81
|
+
return orgId ? adminOrgIds.has(String(orgId)) : false;
|
|
82
|
+
});
|
|
83
|
+
setEnvs(installable);
|
|
84
|
+
if (list.length > 0 && installable.length === 0) {
|
|
85
|
+
setEnvsError('You do not have permission to install apps in any environment. ' +
|
|
86
|
+
'Only organization owners and admins can install — ask your workspace admin.');
|
|
87
|
+
}
|
|
88
|
+
else if (installable.length === 1) {
|
|
89
|
+
setSelectedEnv(installable[0].id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
const status = e?.status;
|
|
94
|
+
if (status === 401 || status === 403) {
|
|
95
|
+
setEnvsError('You need to sign into ObjectStack Cloud first. Click "Open on cloud" below.');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
setEnvsError(e?.message ?? String(e));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
setEnvsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const doInstall = async () => {
|
|
106
|
+
if (!packageId || !selectedEnv)
|
|
107
|
+
return;
|
|
108
|
+
setInstalling(true);
|
|
109
|
+
setInstallResult(null);
|
|
110
|
+
try {
|
|
111
|
+
await installPackage({ packageId, environmentId: selectedEnv, seedSampleData });
|
|
112
|
+
setInstallResult({ ok: true, message: 'Installed successfully. Open the environment to see the new app.' });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
setInstallResult({ ok: false, message: e?.message ?? String(e) });
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
setInstalling(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Install this package into the LOCAL runtime kernel (not a cloud env).
|
|
123
|
+
* Single-target operation — no picker, no dialog. Same-origin POST so
|
|
124
|
+
* cloud session is not required. The local AuthPlugin session is what
|
|
125
|
+
* the backend validates.
|
|
126
|
+
*/
|
|
127
|
+
const doInstallLocal = async () => {
|
|
128
|
+
if (!packageId)
|
|
129
|
+
return;
|
|
130
|
+
setInstallingLocal(true);
|
|
131
|
+
setLocalResult(null);
|
|
132
|
+
try {
|
|
133
|
+
const result = await installLocal({ packageId });
|
|
134
|
+
setLocalResult({
|
|
135
|
+
ok: true,
|
|
136
|
+
message: `Installed v${result.version} to this runtime. Refresh the console to see "${data?.package?.display_name ?? result.manifestId}" in the app switcher.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
const code = e?.code;
|
|
141
|
+
let msg = e?.message ?? String(e);
|
|
142
|
+
if (code === 'manifest_conflict') {
|
|
143
|
+
msg = `${msg}\nTip: a local app already owns this manifest_id. Remove it from objectstack.config.ts first.`;
|
|
144
|
+
}
|
|
145
|
+
else if (code === 'unauthorized') {
|
|
146
|
+
msg = 'Sign in to this runtime first, then try again.';
|
|
147
|
+
}
|
|
148
|
+
else if (code === 'marketplace_unavailable') {
|
|
149
|
+
msg = 'This runtime has no OS_CLOUD_URL configured, so the marketplace catalog is unreachable.';
|
|
150
|
+
}
|
|
151
|
+
setLocalResult({ ok: false, message: msg });
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
setInstallingLocal(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Uninstall this package's cached manifest from the local runtime.
|
|
159
|
+
* NB: kernel API is additive only — the app remains live in the
|
|
160
|
+
* running kernel until the runtime restarts. We surface that
|
|
161
|
+
* caveat in the success message.
|
|
162
|
+
*/
|
|
163
|
+
const doUninstallLocal = async () => {
|
|
164
|
+
if (!localInstall)
|
|
165
|
+
return;
|
|
166
|
+
if (!confirm(`Uninstall ${localInstall.manifestId} v${localInstall.version} from this runtime?\n\nThe cached manifest will be removed. The app will remain loaded in the running kernel until the next restart.`)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
setInstallingLocal(true);
|
|
170
|
+
setLocalResult(null);
|
|
171
|
+
try {
|
|
172
|
+
await uninstallLocal(localInstall.manifestId);
|
|
173
|
+
setLocalResult({
|
|
174
|
+
ok: true,
|
|
175
|
+
message: `Removed cached manifest for ${localInstall.manifestId}. Restart the runtime to fully unload the app from the running kernel.`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
setLocalResult({ ok: false, message: e?.message ?? String(e) });
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
setInstallingLocal(false);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (loading) {
|
|
186
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
|
|
187
|
+
}
|
|
188
|
+
if (error || !data) {
|
|
189
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
|
|
190
|
+
}
|
|
191
|
+
const pkg = data.package;
|
|
192
|
+
const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
|
|
193
|
+
const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
|
|
194
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-2", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && (_jsxs(Badge, { variant: "outline", children: ["v", latestVersion] })), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600", children: ["Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-base text-foreground/80 mt-4 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex flex-col gap-2 shrink-0 min-w-[14rem]", children: [_jsxs(Button, { onClick: doInstallLocal, disabled: !latestVersion || installingLocal, size: "lg", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installingLocal
|
|
195
|
+
? 'Working…'
|
|
196
|
+
: localInstall
|
|
197
|
+
? `Reinstall to this runtime`
|
|
198
|
+
: 'Install to this runtime'] }), localInstall && (_jsxs(Button, { variant: "outline", onClick: doUninstallLocal, disabled: installingLocal, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Uninstall from this runtime"] })), _jsxs(Button, { variant: "ghost", onClick: openInstall, disabled: !latestVersion, size: "sm", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Install to cloud environment\u2026"] }), pkg.homepage_url && (_jsx("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Homepage"] }) })), localResult && (_jsx("div", { className: `rounded-md border p-2 text-xs whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: localResult.message }))] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
|
|
199
|
+
setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: "Choose an environment to install this app into. You need to be signed into ObjectStack Cloud." })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
|
|
200
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace Page — browse approved, marketplace-listed packages.
|
|
3
|
+
*
|
|
4
|
+
* Card-grid catalog of public packages, fetched from the tenant runtime's
|
|
5
|
+
* `/api/v1/marketplace/packages` proxy (which forwards to the configured
|
|
6
|
+
* cloud control plane). Click-through opens the package detail page.
|
|
7
|
+
*/
|
|
8
|
+
export declare function MarketplacePage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Marketplace Page — browse approved, marketplace-listed packages.
|
|
4
|
+
*
|
|
5
|
+
* Card-grid catalog of public packages, fetched from the tenant runtime's
|
|
6
|
+
* `/api/v1/marketplace/packages` proxy (which forwards to the configured
|
|
7
|
+
* cloud control plane). Click-through opens the package detail page.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
10
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Badge, Input, Button, Skeleton, } from '@object-ui/components';
|
|
12
|
+
import { Package, Search, RefreshCcw, Store, AlertCircle, CheckCircle2, Settings } from 'lucide-react';
|
|
13
|
+
import { PackageIcon } from './PackageIcon';
|
|
14
|
+
import { listMarketplacePackages, listLocalInstalls, } from './marketplaceApi';
|
|
15
|
+
function formatRelative(iso) {
|
|
16
|
+
if (!iso)
|
|
17
|
+
return '';
|
|
18
|
+
const d = new Date(iso);
|
|
19
|
+
if (Number.isNaN(d.getTime()))
|
|
20
|
+
return '';
|
|
21
|
+
const days = Math.floor((Date.now() - d.getTime()) / 86400000);
|
|
22
|
+
if (days < 1)
|
|
23
|
+
return 'today';
|
|
24
|
+
if (days < 30)
|
|
25
|
+
return `${days}d ago`;
|
|
26
|
+
if (days < 365)
|
|
27
|
+
return `${Math.floor(days / 30)}mo ago`;
|
|
28
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
29
|
+
}
|
|
30
|
+
export function MarketplacePage() {
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
const { appName } = useParams();
|
|
33
|
+
const basePath = appName ? `/apps/${appName}` : '';
|
|
34
|
+
const [items, setItems] = useState([]);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const [query, setQuery] = useState('');
|
|
38
|
+
const [category, setCategory] = useState('');
|
|
39
|
+
const [installed, setInstalled] = useState([]);
|
|
40
|
+
const installedByManifestId = useMemo(() => {
|
|
41
|
+
const m = new Map();
|
|
42
|
+
for (const e of installed)
|
|
43
|
+
m.set(e.manifestId, e);
|
|
44
|
+
return m;
|
|
45
|
+
}, [installed]);
|
|
46
|
+
const load = async () => {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
setError(null);
|
|
49
|
+
try {
|
|
50
|
+
const [resp, installs] = await Promise.all([
|
|
51
|
+
listMarketplacePackages({ limit: 100 }),
|
|
52
|
+
listLocalInstalls(),
|
|
53
|
+
]);
|
|
54
|
+
setItems(resp.items ?? []);
|
|
55
|
+
setInstalled(installs);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
setError(e?.message ?? String(e));
|
|
59
|
+
setItems([]);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
useEffect(() => { void load(); }, []);
|
|
66
|
+
const categories = useMemo(() => {
|
|
67
|
+
const s = new Set();
|
|
68
|
+
for (const it of items) {
|
|
69
|
+
if (it.category)
|
|
70
|
+
s.add(it.category);
|
|
71
|
+
}
|
|
72
|
+
return Array.from(s).sort();
|
|
73
|
+
}, [items]);
|
|
74
|
+
const filtered = useMemo(() => {
|
|
75
|
+
const q = query.trim().toLowerCase();
|
|
76
|
+
return items.filter((it) => {
|
|
77
|
+
if (category && it.category !== category)
|
|
78
|
+
return false;
|
|
79
|
+
if (!q)
|
|
80
|
+
return true;
|
|
81
|
+
const hay = `${it.display_name} ${it.manifest_id} ${it.description ?? ''}`.toLowerCase();
|
|
82
|
+
return hay.includes(q);
|
|
83
|
+
});
|
|
84
|
+
}, [items, query, category]);
|
|
85
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Store, { className: "h-6 w-6 text-primary", "aria-hidden": "true" }), _jsx("h1", { className: "text-xl sm:text-2xl font-bold tracking-tight", children: "App Marketplace" })] }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Browse approved apps published to the ObjectStack catalog. Click an app to view details and install it into one of your environments." })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace/installed`), children: [_jsx(Settings, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Installed", installed.length > 0 ? ` (${installed.length})` : ''] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading, children: [_jsx(RefreshCcw, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Refresh"] })] })] }), _jsxs("div", { className: "flex flex-col sm:flex-row gap-3", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground", "aria-hidden": "true" }), _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search apps by name or manifest ID\u2026", className: "pl-9", "aria-label": "Search marketplace apps" })] }), categories.length > 0 && (_jsxs("div", { className: "flex flex-wrap gap-1.5", children: [_jsx(Button, { size: "sm", variant: category === '' ? 'default' : 'outline', onClick: () => setCategory(''), children: "All" }), categories.map((cat) => (_jsx(Button, { size: "sm", variant: category === cat ? 'default' : 'outline', onClick: () => setCategory(cat), children: cat }, cat)))] }))] }), error && (_jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load marketplace" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2", children: ["By default this runtime points at the public ObjectStack cloud. Check the runtime is online, or override ", _jsx("code", { className: "font-mono", children: "OS_CLOUD_URL" }), " to point at a self-hosted control plane."] })] })] })), loading && items.length === 0 ? (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: Array.from({ length: 6 }).map((_, i) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-2", children: [_jsx(Skeleton, { className: "h-10 w-10 rounded-lg mb-2" }), _jsx(Skeleton, { className: "h-5 w-3/4" }), _jsx(Skeleton, { className: "h-4 w-1/2 mt-1" })] }), _jsx(CardContent, { children: _jsx(Skeleton, { className: "h-12 w-full" }) })] }, i))) })) : filtered.length === 0 ? (_jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: items.length === 0 ? 'No apps have been approved for the marketplace yet.' : 'No apps match your filters.' })) : (_jsx("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((pkg) => {
|
|
86
|
+
const localEntry = installedByManifestId.get(pkg.manifest_id);
|
|
87
|
+
return (_jsxs(Card, { className: "cursor-pointer transition-colors hover:bg-accent/50 flex flex-col", onClick: () => navigate(`${basePath}/system/marketplace/${pkg.id}`), role: "link", tabIndex: 0, onKeyDown: (e) => {
|
|
88
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
navigate(`${basePath}/system/marketplace/${pkg.id}`);
|
|
91
|
+
}
|
|
92
|
+
}, "data-testid": `marketplace-card-${pkg.manifest_id}`, children: [_jsxs(CardHeader, { className: "flex flex-row items-start gap-3 pb-2", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(CardTitle, { className: "text-base truncate", children: pkg.display_name || pkg.manifest_id }), _jsx(CardDescription, { className: "text-xs truncate", children: _jsx("code", { className: "font-mono", children: pkg.manifest_id }) })] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', className: "shrink-0", children: pkg.publisher }))] }), _jsxs(CardContent, { className: "flex-1 flex flex-col gap-2", children: [_jsx("p", { className: "text-sm text-muted-foreground line-clamp-2", children: pkg.description || 'No description provided.' }), _jsxs("div", { className: "flex items-center gap-2 mt-auto pt-2 flex-wrap", children: [localEntry && (_jsxs(Badge, { variant: "default", className: "text-xs bg-green-600 hover:bg-green-600", children: [_jsx(CheckCircle2, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "Installed v", localEntry.version] })), pkg.latest_version?.version && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [_jsx(Package, { className: "h-3 w-3 mr-1", "aria-hidden": "true" }), "v", pkg.latest_version.version] })), pkg.category && (_jsx(Badge, { variant: "outline", className: "text-xs", children: pkg.category })), pkg.latest_version?.published_at && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: formatRelative(pkg.latest_version.published_at) }))] })] })] }, pkg.id));
|
|
93
|
+
}) }))] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PackageIcon — square icon tile for marketplace packages.
|
|
3
|
+
*
|
|
4
|
+
* Renders the package's `icon_url` when present, falling back to the
|
|
5
|
+
* first letter of the display name (or manifest id) on missing-URL,
|
|
6
|
+
* load-error, or empty URL. The fallback also covers the common
|
|
7
|
+
* case where cloud rows point at icon assets that don't yet exist
|
|
8
|
+
* on the configured CDN.
|
|
9
|
+
*/
|
|
10
|
+
export interface PackageIconProps {
|
|
11
|
+
iconUrl?: string | null;
|
|
12
|
+
displayName?: string | null;
|
|
13
|
+
manifestId?: string | null;
|
|
14
|
+
/** Tailwind size classes — caller controls outer dimensions. */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Text size for the initial-letter fallback. */
|
|
17
|
+
initialClassName?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function PackageIcon({ iconUrl, displayName, manifestId, className, initialClassName, }: PackageIconProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* PackageIcon — square icon tile for marketplace packages.
|
|
4
|
+
*
|
|
5
|
+
* Renders the package's `icon_url` when present, falling back to the
|
|
6
|
+
* first letter of the display name (or manifest id) on missing-URL,
|
|
7
|
+
* load-error, or empty URL. The fallback also covers the common
|
|
8
|
+
* case where cloud rows point at icon assets that don't yet exist
|
|
9
|
+
* on the configured CDN.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
export function PackageIcon({ iconUrl, displayName, manifestId, className = 'h-10 w-10', initialClassName = 'text-base font-semibold', }) {
|
|
13
|
+
const [broken, setBroken] = useState(false);
|
|
14
|
+
const initial = ((displayName ?? manifestId ?? '?').trim()[0] ?? '?').toUpperCase();
|
|
15
|
+
const showImg = !!iconUrl && !broken;
|
|
16
|
+
return (_jsx("div", { className: `flex shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary overflow-hidden ${className}`, children: showImg ? (_jsx("img", { src: iconUrl, alt: "", className: `${className} object-cover`, onError: () => setBroken(true), loading: "lazy" })) : (_jsx("span", { className: initialClassName, children: initial })) }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace REST helper.
|
|
3
|
+
*
|
|
4
|
+
* Thin fetch wrapper around the tenant runtime's marketplace proxy
|
|
5
|
+
* (`/api/v1/marketplace/*`). The proxy forwards to the configured
|
|
6
|
+
* cloud control plane (`OS_CLOUD_URL`) — only browse endpoints are
|
|
7
|
+
* exposed. Install is performed against the cloud action endpoint
|
|
8
|
+
* directly (via `installPackage()` below), which requires the user
|
|
9
|
+
* to have a cloud session.
|
|
10
|
+
*
|
|
11
|
+
* See:
|
|
12
|
+
* cloud/packages/service-cloud/src/routes/marketplace.ts
|
|
13
|
+
* framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
|
|
14
|
+
*/
|
|
15
|
+
export interface MarketplacePackageSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
manifest_id: string;
|
|
18
|
+
display_name: string;
|
|
19
|
+
description?: string | null;
|
|
20
|
+
category?: string | null;
|
|
21
|
+
tags?: string | null;
|
|
22
|
+
icon_url?: string | null;
|
|
23
|
+
homepage_url?: string | null;
|
|
24
|
+
license?: string | null;
|
|
25
|
+
publisher?: string | null;
|
|
26
|
+
is_starter?: boolean;
|
|
27
|
+
created_at?: string;
|
|
28
|
+
updated_at?: string;
|
|
29
|
+
latest_version: MarketplacePackageVersion | null;
|
|
30
|
+
}
|
|
31
|
+
export interface MarketplacePackageDetail extends MarketplacePackageSummary {
|
|
32
|
+
readme?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface MarketplacePackageVersion {
|
|
35
|
+
id: string;
|
|
36
|
+
version: string;
|
|
37
|
+
status?: string;
|
|
38
|
+
is_prerelease?: boolean;
|
|
39
|
+
published_at?: string | null;
|
|
40
|
+
listing_status?: string;
|
|
41
|
+
reviewed_at?: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface MarketplaceListResponse {
|
|
44
|
+
items: MarketplacePackageSummary[];
|
|
45
|
+
total: number;
|
|
46
|
+
limit: number;
|
|
47
|
+
offset: number;
|
|
48
|
+
}
|
|
49
|
+
export interface MarketplaceDetailResponse {
|
|
50
|
+
package: MarketplacePackageDetail;
|
|
51
|
+
versions: MarketplacePackageVersion[];
|
|
52
|
+
}
|
|
53
|
+
export declare function listMarketplacePackages(params?: {
|
|
54
|
+
q?: string;
|
|
55
|
+
category?: string;
|
|
56
|
+
limit?: number;
|
|
57
|
+
offset?: number;
|
|
58
|
+
}): Promise<MarketplaceListResponse>;
|
|
59
|
+
export declare function getMarketplacePackage(id: string): Promise<MarketplaceDetailResponse>;
|
|
60
|
+
export interface InstallResponse {
|
|
61
|
+
installation?: {
|
|
62
|
+
id: string;
|
|
63
|
+
environment_id: string;
|
|
64
|
+
package_id: string;
|
|
65
|
+
version: string;
|
|
66
|
+
};
|
|
67
|
+
message?: string;
|
|
68
|
+
[k: string]: any;
|
|
69
|
+
}
|
|
70
|
+
export declare function installPackage(input: {
|
|
71
|
+
packageId: string;
|
|
72
|
+
environmentId: string;
|
|
73
|
+
seedSampleData?: boolean;
|
|
74
|
+
}): Promise<InstallResponse>;
|
|
75
|
+
/**
|
|
76
|
+
* Helper: list environments visible to the current cloud user (active
|
|
77
|
+
* organisation). Used to populate the install dialog's environment picker.
|
|
78
|
+
* Calls cloud directly — same auth model as `installPackage`.
|
|
79
|
+
*/
|
|
80
|
+
export interface CloudEnvironment {
|
|
81
|
+
id: string;
|
|
82
|
+
display_name?: string;
|
|
83
|
+
hostname?: string;
|
|
84
|
+
organization_id?: string;
|
|
85
|
+
plan?: string;
|
|
86
|
+
status?: string;
|
|
87
|
+
}
|
|
88
|
+
export declare function listCloudEnvironments(): Promise<CloudEnvironment[]>;
|
|
89
|
+
/**
|
|
90
|
+
* List orgs in which the current cloud user has an `owner` or `admin`
|
|
91
|
+
* role on `sys_member`. Used to filter the install dialog's env picker
|
|
92
|
+
* — only envs whose `organization_id` is in this set are installable
|
|
93
|
+
* (the backend enforces the same gate; this is the UX mirror).
|
|
94
|
+
*
|
|
95
|
+
* Returns an empty set on 401 / network failure so the install dialog
|
|
96
|
+
* can render a clean "no installable environments" state.
|
|
97
|
+
*/
|
|
98
|
+
export declare function listInstallableOrgIds(): Promise<Set<string>>;
|
|
99
|
+
export declare function cloudInstallDeepLink(packageId: string): string;
|
|
100
|
+
export interface LocalInstallEntry {
|
|
101
|
+
packageId: string;
|
|
102
|
+
versionId: string;
|
|
103
|
+
manifestId: string;
|
|
104
|
+
version: string;
|
|
105
|
+
installedAt: string;
|
|
106
|
+
installedBy: string | null;
|
|
107
|
+
}
|
|
108
|
+
export interface LocalInstallResult {
|
|
109
|
+
manifestId: string;
|
|
110
|
+
version: string;
|
|
111
|
+
versionId: string;
|
|
112
|
+
installedAt: string;
|
|
113
|
+
hotLoaded: boolean;
|
|
114
|
+
upgradedFrom: string | null;
|
|
115
|
+
note?: string;
|
|
116
|
+
}
|
|
117
|
+
export declare function installLocal(input: {
|
|
118
|
+
packageId: string;
|
|
119
|
+
versionId?: string;
|
|
120
|
+
}): Promise<LocalInstallResult>;
|
|
121
|
+
export declare function listLocalInstalls(): Promise<LocalInstallEntry[]>;
|
|
122
|
+
export declare function uninstallLocal(manifestId: string): Promise<void>;
|