@object-ui/app-shell 7.1.0 → 7.2.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 +279 -0
- package/dist/console/AppContent.js +9 -15
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +36 -9
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.js +16 -2
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +49 -4
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +36 -4
- package/dist/views/metadata-admin/ResourceListPage.js +21 -4
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +20 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +8 -0
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +17 -3
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +16 -2
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +15 -3
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +6 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +21 -10
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/package-scope.d.ts +15 -0
- package/dist/views/metadata-admin/package-scope.js +16 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +22 -3
- package/dist/views/metadata-admin/previews/FlowCanvas.js +45 -6
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +42 -30
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +5 -3
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +9 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +4 -2
- package/package.json +38 -38
|
@@ -12,7 +12,7 @@ import { Avatar, AvatarImage, AvatarFallback, Button, Input, Empty, EmptyTitle,
|
|
|
12
12
|
import { Plus, Search, Loader2 } from 'lucide-react';
|
|
13
13
|
import { useAuth } from '@object-ui/auth';
|
|
14
14
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
15
|
-
import { useNavigate } from 'react-router-dom';
|
|
15
|
+
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
16
16
|
import { CreateWorkspaceDialog } from './CreateWorkspaceDialog';
|
|
17
17
|
import { resolveHomeUrl } from './resolveHomeUrl';
|
|
18
18
|
function getOrgInitials(name) {
|
|
@@ -26,6 +26,14 @@ function getOrgInitials(name) {
|
|
|
26
26
|
export function OrganizationsPage() {
|
|
27
27
|
const { t } = useObjectTranslation();
|
|
28
28
|
const navigate = useNavigate();
|
|
29
|
+
// Two deliberate ways to reach this page (vs the auto-skipping post-login
|
|
30
|
+
// redirect): `?manage=1` (avatar menu "My Organizations") shows the picker;
|
|
31
|
+
// `?create=1` (avatar menu "Create workspace") additionally opens the create
|
|
32
|
+
// dialog directly. Both suppress the single-org auto-skip below so a
|
|
33
|
+
// single-org user can actually reach "New organization" / the dialog.
|
|
34
|
+
const [searchParams] = useSearchParams();
|
|
35
|
+
const manageMode = searchParams.get('manage') === '1';
|
|
36
|
+
const wantsCreate = searchParams.get('create') === '1';
|
|
29
37
|
const { organizations, activeOrganization, isOrganizationsLoading, switchOrganization, getAuthConfig, } = useAuth();
|
|
30
38
|
const [query, setQuery] = useState('');
|
|
31
39
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
@@ -91,16 +99,27 @@ export function OrganizationsPage() {
|
|
|
91
99
|
return;
|
|
92
100
|
if (isOrganizationsLoading)
|
|
93
101
|
return;
|
|
102
|
+
if (manageMode || wantsCreate)
|
|
103
|
+
return; // came to manage/create — don't bounce
|
|
94
104
|
if (orgList.length !== 1)
|
|
95
105
|
return;
|
|
96
106
|
autoSelectedRef.current = true;
|
|
97
107
|
void handleSelect(orgList[0]);
|
|
98
108
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
|
-
}, [isOrganizationsLoading, orgList.length]);
|
|
109
|
+
}, [isOrganizationsLoading, orgList.length, manageMode, wantsCreate]);
|
|
110
|
+
// Open the create dialog when arriving via the header "Create workspace"
|
|
111
|
+
// entry (`?create=1`). Guarded so closing the dialog doesn't re-open it.
|
|
112
|
+
const createOpenedRef = useRef(false);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (wantsCreate && !createOpenedRef.current) {
|
|
115
|
+
createOpenedRef.current = true;
|
|
116
|
+
setIsCreateOpen(true);
|
|
117
|
+
}
|
|
118
|
+
}, [wantsCreate]);
|
|
100
119
|
// Show a spinner while we're either still loading, or about to auto-redirect
|
|
101
120
|
// because there's only one org. This prevents the picker from briefly
|
|
102
121
|
// flashing on screen for single-org users.
|
|
103
|
-
const willAutoSelect = !isOrganizationsLoading && orgList.length === 1;
|
|
122
|
+
const willAutoSelect = !manageMode && !wantsCreate && !isOrganizationsLoading && orgList.length === 1;
|
|
104
123
|
if (isOrganizationsLoading || willAutoSelect) {
|
|
105
124
|
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-20", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-muted-foreground" }) }));
|
|
106
125
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provisionEnvironment
|
|
3
|
+
*
|
|
4
|
+
* Eagerly ensure a freshly created organization has its **production**
|
|
5
|
+
* environment so a self-service "create another workspace" lands the user in a
|
|
6
|
+
* ready console — no onboarding-wizard detour.
|
|
7
|
+
*
|
|
8
|
+
* ObjectStack runs a 1-production-environment-per-organization model: an org's
|
|
9
|
+
* FIRST environment is born as its production env (allowed on every plan,
|
|
10
|
+
* including free). The cloud control plane exposes this as
|
|
11
|
+
* `POST /api/v1/cloud/environments`, which only needs a `displayName`; the org
|
|
12
|
+
* is resolved from `organizationId` (preferred) → the better-auth active org →
|
|
13
|
+
* the actor's first membership.
|
|
14
|
+
*
|
|
15
|
+
* Idempotent + best-effort by contract:
|
|
16
|
+
* - Some control planes auto-provision the production env on org create (the
|
|
17
|
+
* `auto-default-environment` plugin). This call then races that plugin and
|
|
18
|
+
* the loser gets a 403 `PRODUCTION_ENV_LIMIT` / 409 — which is SUCCESS for
|
|
19
|
+
* us (the org is already born-with-env), not a failure.
|
|
20
|
+
* - On a genuine failure (5xx / network) the caller swallows the error and
|
|
21
|
+
* the onboarding gate provisions the env lazily on first navigation.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
/** Result of ensuring the org's production environment exists. */
|
|
26
|
+
export interface ProvisionedEnvironment {
|
|
27
|
+
/** Environment id (control-plane `sys_environment` row), when this call minted it. */
|
|
28
|
+
id?: string;
|
|
29
|
+
/** Opaque system hostname, e.g. `os-<shortId>.<rootDomain>` for production. */
|
|
30
|
+
hostname?: string;
|
|
31
|
+
/**
|
|
32
|
+
* True when the org already had its production env (the control plane
|
|
33
|
+
* provisioned it on create). The born-with-env contract is still satisfied.
|
|
34
|
+
*/
|
|
35
|
+
alreadyProvisioned?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ensure the production environment exists for a just-created organization.
|
|
39
|
+
*
|
|
40
|
+
* Uses {@link createAuthenticatedFetch} so the request carries the Bearer token
|
|
41
|
+
* and the active-org `X-Tenant-ID` header; `organizationId` is also sent in the
|
|
42
|
+
* body so the target org is unambiguous even before the session active-org
|
|
43
|
+
* switch has propagated. The env is named `Production` to match the
|
|
44
|
+
* born-with-env convention used by the signup org.
|
|
45
|
+
*
|
|
46
|
+
* @throws on a genuine control-plane failure (5xx / network). A 403/409
|
|
47
|
+
* "already has its production env" is NOT an error — it resolves to
|
|
48
|
+
* `{ alreadyProvisioned: true }`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function provisionProductionEnvironment(opts: {
|
|
51
|
+
organizationId: string;
|
|
52
|
+
displayName?: string;
|
|
53
|
+
}): Promise<ProvisionedEnvironment>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provisionEnvironment
|
|
3
|
+
*
|
|
4
|
+
* Eagerly ensure a freshly created organization has its **production**
|
|
5
|
+
* environment so a self-service "create another workspace" lands the user in a
|
|
6
|
+
* ready console — no onboarding-wizard detour.
|
|
7
|
+
*
|
|
8
|
+
* ObjectStack runs a 1-production-environment-per-organization model: an org's
|
|
9
|
+
* FIRST environment is born as its production env (allowed on every plan,
|
|
10
|
+
* including free). The cloud control plane exposes this as
|
|
11
|
+
* `POST /api/v1/cloud/environments`, which only needs a `displayName`; the org
|
|
12
|
+
* is resolved from `organizationId` (preferred) → the better-auth active org →
|
|
13
|
+
* the actor's first membership.
|
|
14
|
+
*
|
|
15
|
+
* Idempotent + best-effort by contract:
|
|
16
|
+
* - Some control planes auto-provision the production env on org create (the
|
|
17
|
+
* `auto-default-environment` plugin). This call then races that plugin and
|
|
18
|
+
* the loser gets a 403 `PRODUCTION_ENV_LIMIT` / 409 — which is SUCCESS for
|
|
19
|
+
* us (the org is already born-with-env), not a failure.
|
|
20
|
+
* - On a genuine failure (5xx / network) the caller swallows the error and
|
|
21
|
+
* the onboarding gate provisions the env lazily on first navigation.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { createAuthenticatedFetch } from '@object-ui/auth';
|
|
26
|
+
import { getCloudBase } from '../../runtime-config';
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the production environment exists for a just-created organization.
|
|
29
|
+
*
|
|
30
|
+
* Uses {@link createAuthenticatedFetch} so the request carries the Bearer token
|
|
31
|
+
* and the active-org `X-Tenant-ID` header; `organizationId` is also sent in the
|
|
32
|
+
* body so the target org is unambiguous even before the session active-org
|
|
33
|
+
* switch has propagated. The env is named `Production` to match the
|
|
34
|
+
* born-with-env convention used by the signup org.
|
|
35
|
+
*
|
|
36
|
+
* @throws on a genuine control-plane failure (5xx / network). A 403/409
|
|
37
|
+
* "already has its production env" is NOT an error — it resolves to
|
|
38
|
+
* `{ alreadyProvisioned: true }`.
|
|
39
|
+
*/
|
|
40
|
+
export async function provisionProductionEnvironment(opts) {
|
|
41
|
+
const authFetch = createAuthenticatedFetch();
|
|
42
|
+
const res = await authFetch(`${getCloudBase()}/api/v1/cloud/environments`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
displayName: opts.displayName ?? 'Production',
|
|
47
|
+
organizationId: opts.organizationId,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
// 403 PRODUCTION_ENV_LIMIT / 409 ⇒ the org already owns its (one) production
|
|
52
|
+
// env — born-with-env is satisfied, so this is success, not a failure.
|
|
53
|
+
if (res.status === 403 || res.status === 409) {
|
|
54
|
+
return { alreadyProvisioned: true };
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Failed to provision production environment (status ${res.status})`);
|
|
57
|
+
}
|
|
58
|
+
// The control plane wraps payloads as `{ success, data }`; tolerate both.
|
|
59
|
+
const body = (await res.json().catch(() => ({})));
|
|
60
|
+
if (body && typeof body === 'object' && 'data' in body && body.data) {
|
|
61
|
+
return body.data;
|
|
62
|
+
}
|
|
63
|
+
return body ?? {};
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnvironmentEntitlementDialog — a friendly upgrade / limit dialog shown instead
|
|
3
|
+
* of a raw red error toast when an environment-create is gated by plan or
|
|
4
|
+
* capacity (DEV_ENV_PLAN_LOCKED / DEV_ENV_LIMIT / PRODUCTION_ENV_LIMIT).
|
|
5
|
+
*
|
|
6
|
+
* Driven by a single {@link EntitlementDialogSpec}, opened from two places:
|
|
7
|
+
* • proactively, from the env-list toolbar (a free-plan org clicking
|
|
8
|
+
* "Add environment" — see EnvironmentListToolbar), and
|
|
9
|
+
* • reactively, from the action runtime's apiHandler when the create POST
|
|
10
|
+
* comes back with an entitlement 403 (the safety net).
|
|
11
|
+
*
|
|
12
|
+
* The CTA renders as an anchor (not an SPA navigation) so a control-plane URL
|
|
13
|
+
* like `/settings/billing` always lands on the real page regardless of the
|
|
14
|
+
* console's own router. Relative URLs resolve against the control-plane origin
|
|
15
|
+
* (`apiBase`); absolute / mailto URLs are used as-is.
|
|
16
|
+
*/
|
|
17
|
+
import type { EntitlementDialogSpec } from './entitlements';
|
|
18
|
+
export interface EntitlementDialogState {
|
|
19
|
+
open: boolean;
|
|
20
|
+
spec?: EntitlementDialogSpec;
|
|
21
|
+
}
|
|
22
|
+
/** Resolve a CTA URL to a concrete href + whether it should open in a new tab. */
|
|
23
|
+
export declare function resolveCtaHref(url: string, apiBase: string): {
|
|
24
|
+
href: string;
|
|
25
|
+
external: boolean;
|
|
26
|
+
};
|
|
27
|
+
interface Props {
|
|
28
|
+
state: EntitlementDialogState;
|
|
29
|
+
/** Control-plane origin used to resolve relative CTA URLs. */
|
|
30
|
+
apiBase: string;
|
|
31
|
+
onOpenChange: (open: boolean) => void;
|
|
32
|
+
}
|
|
33
|
+
export declare function EnvironmentEntitlementDialog({ state, apiBase, onOpenChange }: Props): import("react").JSX.Element;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* EnvironmentEntitlementDialog — a friendly upgrade / limit dialog shown instead
|
|
4
|
+
* of a raw red error toast when an environment-create is gated by plan or
|
|
5
|
+
* capacity (DEV_ENV_PLAN_LOCKED / DEV_ENV_LIMIT / PRODUCTION_ENV_LIMIT).
|
|
6
|
+
*
|
|
7
|
+
* Driven by a single {@link EntitlementDialogSpec}, opened from two places:
|
|
8
|
+
* • proactively, from the env-list toolbar (a free-plan org clicking
|
|
9
|
+
* "Add environment" — see EnvironmentListToolbar), and
|
|
10
|
+
* • reactively, from the action runtime's apiHandler when the create POST
|
|
11
|
+
* comes back with an entitlement 403 (the safety net).
|
|
12
|
+
*
|
|
13
|
+
* The CTA renders as an anchor (not an SPA navigation) so a control-plane URL
|
|
14
|
+
* like `/settings/billing` always lands on the real page regardless of the
|
|
15
|
+
* console's own router. Relative URLs resolve against the control-plane origin
|
|
16
|
+
* (`apiBase`); absolute / mailto URLs are used as-is.
|
|
17
|
+
*/
|
|
18
|
+
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogCancel, Button, } from '@object-ui/components';
|
|
19
|
+
/** Resolve a CTA URL to a concrete href + whether it should open in a new tab. */
|
|
20
|
+
export function resolveCtaHref(url, apiBase) {
|
|
21
|
+
if (/^https?:\/\//i.test(url) || url.startsWith('mailto:')) {
|
|
22
|
+
return { href: url, external: !url.startsWith('mailto:') };
|
|
23
|
+
}
|
|
24
|
+
const base = (apiBase || '').replace(/\/+$/, '');
|
|
25
|
+
// A control-plane-relative path: prefix the API origin when we have one (dev:
|
|
26
|
+
// split SPA + backend). Empty base → same-origin relative (prod).
|
|
27
|
+
return { href: `${base}${url}`, external: Boolean(base) };
|
|
28
|
+
}
|
|
29
|
+
function CtaButton({ cta, apiBase, primary, onNavigate, }) {
|
|
30
|
+
const { href, external } = resolveCtaHref(cta.url, apiBase);
|
|
31
|
+
return (_jsx(Button, { asChild: true, variant: primary ? 'default' : 'outline', size: "sm", children: _jsx("a", { href: href, onClick: onNavigate, ...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {}), "data-testid": `entitlement-cta-${primary ? 'primary' : 'secondary'}`, children: cta.label }) }));
|
|
32
|
+
}
|
|
33
|
+
export function EnvironmentEntitlementDialog({ state, apiBase, onOpenChange }) {
|
|
34
|
+
const spec = state.spec;
|
|
35
|
+
return (_jsx(AlertDialog, { open: state.open, onOpenChange: (open) => { if (!open)
|
|
36
|
+
onOpenChange(false); }, children: _jsxs(AlertDialogContent, { "data-testid": "environment-entitlement-dialog", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: spec?.title }), _jsx(AlertDialogDescription, { children: spec?.message })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Close" }), spec?.secondaryCta && (_jsx(CtaButton, { cta: spec.secondaryCta, apiBase: apiBase, primary: false, onNavigate: () => onOpenChange(false) })), spec?.cta && (_jsx(CtaButton, { cta: spec.cta, apiBase: apiBase, primary: true, onNavigate: () => onOpenChange(false) }))] })] }) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EnvironmentListToolbar — the state-aware replacement for the generic
|
|
3
|
+
* `list_toolbar` action bar on the `sys_environment` list.
|
|
4
|
+
*
|
|
5
|
+
* The cloud serves ONE `create_environment` action; born-with-env makes its
|
|
6
|
+
* meaning depend on org state (which the action metadata can't express). This
|
|
7
|
+
* component reads the resolved entitlement state and renders the right
|
|
8
|
+
* affordance:
|
|
9
|
+
* • no production env → "Set up your production environment" (primary);
|
|
10
|
+
* the create POST provisions the org's one
|
|
11
|
+
* production env — the historical-data path that
|
|
12
|
+
* must never error.
|
|
13
|
+
* • has prod + dev allowed → "Add development environment" (the create POST
|
|
14
|
+
* makes a dev env).
|
|
15
|
+
* • has prod + dev NOT allowed → "Add environment" that opens an UPGRADE prompt
|
|
16
|
+
* instead of POST-ing into a 403.
|
|
17
|
+
* • still resolving / unknown → the action's default label (neutral), with the
|
|
18
|
+
* apiHandler entitlement dialog as the safety net.
|
|
19
|
+
*
|
|
20
|
+
* Create flows reuse the standard `action:bar` runner (name modal → apiHandler),
|
|
21
|
+
* so only the label/variant changes — no duplicate POST logic here.
|
|
22
|
+
*/
|
|
23
|
+
import { type EntitlementDialogSpec, type EnvironmentEntitlementsState } from './entitlements';
|
|
24
|
+
interface Props {
|
|
25
|
+
/** Toolbar actions already localized by the caller (ObjectView). */
|
|
26
|
+
actions: any[];
|
|
27
|
+
/** Resolved entitlement state, or null while still loading. */
|
|
28
|
+
entitlements: EnvironmentEntitlementsState | null;
|
|
29
|
+
/** Open the shared entitlement dialog (proactive upgrade prompt). */
|
|
30
|
+
onUpgrade: (spec: EntitlementDialogSpec) => void;
|
|
31
|
+
}
|
|
32
|
+
export declare function EnvironmentListToolbar({ actions, entitlements, onUpgrade }: Props): import("react").JSX.Element | null;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* EnvironmentListToolbar — the state-aware replacement for the generic
|
|
4
|
+
* `list_toolbar` action bar on the `sys_environment` list.
|
|
5
|
+
*
|
|
6
|
+
* The cloud serves ONE `create_environment` action; born-with-env makes its
|
|
7
|
+
* meaning depend on org state (which the action metadata can't express). This
|
|
8
|
+
* component reads the resolved entitlement state and renders the right
|
|
9
|
+
* affordance:
|
|
10
|
+
* • no production env → "Set up your production environment" (primary);
|
|
11
|
+
* the create POST provisions the org's one
|
|
12
|
+
* production env — the historical-data path that
|
|
13
|
+
* must never error.
|
|
14
|
+
* • has prod + dev allowed → "Add development environment" (the create POST
|
|
15
|
+
* makes a dev env).
|
|
16
|
+
* • has prod + dev NOT allowed → "Add environment" that opens an UPGRADE prompt
|
|
17
|
+
* instead of POST-ing into a 403.
|
|
18
|
+
* • still resolving / unknown → the action's default label (neutral), with the
|
|
19
|
+
* apiHandler entitlement dialog as the safety net.
|
|
20
|
+
*
|
|
21
|
+
* Create flows reuse the standard `action:bar` runner (name modal → apiHandler),
|
|
22
|
+
* so only the label/variant changes — no duplicate POST logic here.
|
|
23
|
+
*/
|
|
24
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
25
|
+
import { Button } from '@object-ui/components';
|
|
26
|
+
import { Plus } from 'lucide-react';
|
|
27
|
+
import { decideEnvironmentCta, upgradeDialogSpec, } from './entitlements';
|
|
28
|
+
const CREATE_ACTION = 'create_environment';
|
|
29
|
+
export function EnvironmentListToolbar({ actions, entitlements, onUpgrade }) {
|
|
30
|
+
const toolbarActions = (actions || []).filter((a) => a?.locations?.includes('list_toolbar'));
|
|
31
|
+
if (toolbarActions.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
const ctaKind = entitlements?.ready ? decideEnvironmentCta(entitlements) : null;
|
|
34
|
+
// Upgrade state: a free-plan org clicking "create" must NOT POST-then-403.
|
|
35
|
+
// Render a primary button that opens the upgrade prompt, plus any other
|
|
36
|
+
// (non-create) toolbar actions through the normal bar.
|
|
37
|
+
if (ctaKind === 'upgrade_for_development') {
|
|
38
|
+
const others = toolbarActions.filter((a) => a?.name !== CREATE_ACTION);
|
|
39
|
+
return (_jsxs(_Fragment, { children: [others.length > 0 && (_jsx(SchemaRenderer, { schema: { type: 'action:bar', location: 'list_toolbar', actions: others, size: 'sm', variant: 'outline' } })), _jsxs(Button, { size: "sm", onClick: () => onUpgrade(upgradeDialogSpec(entitlements)), className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", "data-testid": "environment-add-upgrade", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add environment" })] })] }));
|
|
40
|
+
}
|
|
41
|
+
// setup_production / add_development / loading: render the bar, overriding only
|
|
42
|
+
// the create action's label (and promoting production setup to a primary CTA).
|
|
43
|
+
const renderedActions = toolbarActions.map((a) => {
|
|
44
|
+
if (a?.name !== CREATE_ACTION || ctaKind == null)
|
|
45
|
+
return a;
|
|
46
|
+
if (ctaKind === 'setup_production') {
|
|
47
|
+
return { ...a, label: 'Set up your production environment', variant: 'primary' };
|
|
48
|
+
}
|
|
49
|
+
// add_development
|
|
50
|
+
return { ...a, label: 'Add development environment' };
|
|
51
|
+
});
|
|
52
|
+
return (_jsx(SchemaRenderer, { schema: {
|
|
53
|
+
type: 'action:bar',
|
|
54
|
+
location: 'list_toolbar',
|
|
55
|
+
actions: renderedActions,
|
|
56
|
+
size: 'sm',
|
|
57
|
+
variant: 'outline',
|
|
58
|
+
} }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment entitlement logic — the React-free decision layer behind the
|
|
3
|
+
* state-aware "create environment" affordance and the entitlement-error dialog.
|
|
4
|
+
*
|
|
5
|
+
* The Console environment list (`sys_environment`) renders a single
|
|
6
|
+
* `create_environment` toolbar action. Whether that should read "Set up your
|
|
7
|
+
* production environment", "Add development environment", or open an upgrade
|
|
8
|
+
* prompt depends on org state the action metadata can't express (does the org
|
|
9
|
+
* already own its one production env? is its plan allowed development envs?).
|
|
10
|
+
* This module turns the org-scoped capacity summary
|
|
11
|
+
* (GET /cloud/environment-entitlements) — with a row-derived fallback — into
|
|
12
|
+
* that decision, and maps the cloud env-create 403 bodies to a friendly dialog
|
|
13
|
+
* so a confused user never sees a raw red error toast.
|
|
14
|
+
*
|
|
15
|
+
* Kept dependency-free so it is trivially unit-testable.
|
|
16
|
+
*/
|
|
17
|
+
export type EntitlementErrorCode = 'DEV_ENV_PLAN_LOCKED' | 'DEV_ENV_LIMIT' | 'PRODUCTION_ENV_LIMIT';
|
|
18
|
+
export declare function isEntitlementErrorCode(code: unknown): code is EntitlementErrorCode;
|
|
19
|
+
/** A CTA link rendered in the entitlement dialog. */
|
|
20
|
+
export interface EntitlementCta {
|
|
21
|
+
label: string;
|
|
22
|
+
/** Absolute (http/mailto) or a control-plane-relative path (e.g. `/settings/billing`). */
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
/** Declarative spec the {@link EnvironmentEntitlementDialog} renders. */
|
|
26
|
+
export interface EntitlementDialogSpec {
|
|
27
|
+
code: string;
|
|
28
|
+
title: string;
|
|
29
|
+
message: string;
|
|
30
|
+
/** Primary CTA (e.g. Upgrade plan). */
|
|
31
|
+
cta?: EntitlementCta;
|
|
32
|
+
/** Secondary CTA (e.g. Contact sales). */
|
|
33
|
+
secondaryCta?: EntitlementCta;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_UPGRADE_URL = "/settings/billing";
|
|
36
|
+
/**
|
|
37
|
+
* Map a cloud env-create 403 body
|
|
38
|
+
* (`{ error, code, upgrade_url, contact_url, plan, current, limit }`) to a
|
|
39
|
+
* dialog spec. Returns `null` for any non-entitlement error so the caller falls
|
|
40
|
+
* back to its normal error handling (a red toast). This is the safety net: it
|
|
41
|
+
* fires regardless of whether the up-front state-aware presentation was right.
|
|
42
|
+
*/
|
|
43
|
+
export declare function entitlementDialogFromError(body: any): EntitlementDialogSpec | null;
|
|
44
|
+
/** Server summary shape (GET /cloud/environment-entitlements → `data`). */
|
|
45
|
+
export interface EnvironmentEntitlementsSummary {
|
|
46
|
+
plan?: string;
|
|
47
|
+
hasProductionEnv?: boolean;
|
|
48
|
+
production?: {
|
|
49
|
+
used: number;
|
|
50
|
+
limit: number;
|
|
51
|
+
canCreate: boolean;
|
|
52
|
+
};
|
|
53
|
+
development?: {
|
|
54
|
+
used: number;
|
|
55
|
+
limit: number;
|
|
56
|
+
canCreate: boolean;
|
|
57
|
+
};
|
|
58
|
+
seatCount?: number;
|
|
59
|
+
upgradeUrl?: string;
|
|
60
|
+
contactSalesUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Combined client state (authoritative summary, or a row-derived fallback). */
|
|
63
|
+
export interface EnvironmentEntitlementsState {
|
|
64
|
+
/** True once a usable signal exists (summary OR derived rows). */
|
|
65
|
+
ready: boolean;
|
|
66
|
+
hasProductionEnv: boolean;
|
|
67
|
+
/** Authoritative dev-create capability; `undefined` when unknown (no summary). */
|
|
68
|
+
canCreateDevelopmentEnv?: boolean;
|
|
69
|
+
plan?: string;
|
|
70
|
+
upgradeUrl: string;
|
|
71
|
+
contactSalesUrl?: string;
|
|
72
|
+
/** Where the signal came from — telemetry + degradation note + tests. */
|
|
73
|
+
source: 'summary' | 'derived' | 'unknown';
|
|
74
|
+
}
|
|
75
|
+
export type EnvironmentCtaKind = 'setup_production' | 'add_development' | 'upgrade_for_development';
|
|
76
|
+
/**
|
|
77
|
+
* Decide which toolbar affordance to present:
|
|
78
|
+
* • no production env → set up production (the create POST makes one;
|
|
79
|
+
* the critical historical-data path — never errors)
|
|
80
|
+
* • has prod + dev allowed → add development (POST makes a dev env)
|
|
81
|
+
* • has prod + dev NOT allowed → upgrade prompt (no POST)
|
|
82
|
+
* • has prod + dev unknown → add development (let the POST + dialog decide)
|
|
83
|
+
*/
|
|
84
|
+
export declare function decideEnvironmentCta(state: EnvironmentEntitlementsState): EnvironmentCtaKind;
|
|
85
|
+
/**
|
|
86
|
+
* The proactive (pre-POST) upgrade dialog shown when a free-plan org clicks
|
|
87
|
+
* "Add environment" but development envs aren't in its plan. Mirrors the copy
|
|
88
|
+
* of the reactive DEV_ENV_PLAN_LOCKED error so both paths read identically.
|
|
89
|
+
*/
|
|
90
|
+
export declare function upgradeDialogSpec(state: EnvironmentEntitlementsState): EntitlementDialogSpec;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment entitlement logic — the React-free decision layer behind the
|
|
3
|
+
* state-aware "create environment" affordance and the entitlement-error dialog.
|
|
4
|
+
*
|
|
5
|
+
* The Console environment list (`sys_environment`) renders a single
|
|
6
|
+
* `create_environment` toolbar action. Whether that should read "Set up your
|
|
7
|
+
* production environment", "Add development environment", or open an upgrade
|
|
8
|
+
* prompt depends on org state the action metadata can't express (does the org
|
|
9
|
+
* already own its one production env? is its plan allowed development envs?).
|
|
10
|
+
* This module turns the org-scoped capacity summary
|
|
11
|
+
* (GET /cloud/environment-entitlements) — with a row-derived fallback — into
|
|
12
|
+
* that decision, and maps the cloud env-create 403 bodies to a friendly dialog
|
|
13
|
+
* so a confused user never sees a raw red error toast.
|
|
14
|
+
*
|
|
15
|
+
* Kept dependency-free so it is trivially unit-testable.
|
|
16
|
+
*/
|
|
17
|
+
const ENTITLEMENT_ERROR_CODES = new Set([
|
|
18
|
+
'DEV_ENV_PLAN_LOCKED',
|
|
19
|
+
'DEV_ENV_LIMIT',
|
|
20
|
+
'PRODUCTION_ENV_LIMIT',
|
|
21
|
+
]);
|
|
22
|
+
export function isEntitlementErrorCode(code) {
|
|
23
|
+
return typeof code === 'string' && ENTITLEMENT_ERROR_CODES.has(code);
|
|
24
|
+
}
|
|
25
|
+
export const DEFAULT_UPGRADE_URL = '/settings/billing';
|
|
26
|
+
/**
|
|
27
|
+
* Map a cloud env-create 403 body
|
|
28
|
+
* (`{ error, code, upgrade_url, contact_url, plan, current, limit }`) to a
|
|
29
|
+
* dialog spec. Returns `null` for any non-entitlement error so the caller falls
|
|
30
|
+
* back to its normal error handling (a red toast). This is the safety net: it
|
|
31
|
+
* fires regardless of whether the up-front state-aware presentation was right.
|
|
32
|
+
*/
|
|
33
|
+
export function entitlementDialogFromError(body) {
|
|
34
|
+
const code = body?.code;
|
|
35
|
+
if (!isEntitlementErrorCode(code))
|
|
36
|
+
return null;
|
|
37
|
+
const serverMessage = typeof body?.error === 'string' && body.error ? body.error
|
|
38
|
+
: typeof body?.message === 'string' ? body.message : '';
|
|
39
|
+
const upgradeUrl = typeof body?.upgrade_url === 'string' && body.upgrade_url ? body.upgrade_url : DEFAULT_UPGRADE_URL;
|
|
40
|
+
const contactUrl = typeof body?.contact_url === 'string' && body.contact_url ? body.contact_url : '';
|
|
41
|
+
if (code === 'PRODUCTION_ENV_LIMIT') {
|
|
42
|
+
return {
|
|
43
|
+
code,
|
|
44
|
+
title: 'You already have your production environment',
|
|
45
|
+
message: serverMessage ||
|
|
46
|
+
'Each organization includes exactly one production environment. Create a separate organization for another, or contact us about an Enterprise arrangement.',
|
|
47
|
+
cta: contactUrl ? { label: 'Contact sales', url: contactUrl } : undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// DEV_ENV_PLAN_LOCKED / DEV_ENV_LIMIT — both resolve via an upgrade CTA.
|
|
51
|
+
return {
|
|
52
|
+
code,
|
|
53
|
+
title: code === 'DEV_ENV_PLAN_LOCKED'
|
|
54
|
+
? 'Development environments are a paid feature'
|
|
55
|
+
: 'Development environment limit reached',
|
|
56
|
+
message: serverMessage ||
|
|
57
|
+
(code === 'DEV_ENV_PLAN_LOCKED'
|
|
58
|
+
? 'Your free plan includes one production environment. Upgrade to add development environments — build in dev, then publish to production.'
|
|
59
|
+
: 'Capacity scales with AI seats. Add an AI seat, or archive an unused development environment to free one up.'),
|
|
60
|
+
cta: { label: 'Upgrade plan', url: upgradeUrl },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Decide which toolbar affordance to present:
|
|
65
|
+
* • no production env → set up production (the create POST makes one;
|
|
66
|
+
* the critical historical-data path — never errors)
|
|
67
|
+
* • has prod + dev allowed → add development (POST makes a dev env)
|
|
68
|
+
* • has prod + dev NOT allowed → upgrade prompt (no POST)
|
|
69
|
+
* • has prod + dev unknown → add development (let the POST + dialog decide)
|
|
70
|
+
*/
|
|
71
|
+
export function decideEnvironmentCta(state) {
|
|
72
|
+
if (!state.hasProductionEnv)
|
|
73
|
+
return 'setup_production';
|
|
74
|
+
if (state.canCreateDevelopmentEnv === false)
|
|
75
|
+
return 'upgrade_for_development';
|
|
76
|
+
return 'add_development';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The proactive (pre-POST) upgrade dialog shown when a free-plan org clicks
|
|
80
|
+
* "Add environment" but development envs aren't in its plan. Mirrors the copy
|
|
81
|
+
* of the reactive DEV_ENV_PLAN_LOCKED error so both paths read identically.
|
|
82
|
+
*/
|
|
83
|
+
export function upgradeDialogSpec(state) {
|
|
84
|
+
const planLabel = state.plan && state.plan !== 'free' ? `your ${state.plan} plan` : 'your free plan';
|
|
85
|
+
return {
|
|
86
|
+
code: 'DEV_ENV_PLAN_LOCKED',
|
|
87
|
+
title: 'Development environments are a paid feature',
|
|
88
|
+
message: `${planLabel} includes one production environment. Upgrade to add development environments — build in dev, then publish to production.`,
|
|
89
|
+
cta: { label: 'Upgrade plan', url: state.upgradeUrl || DEFAULT_UPGRADE_URL },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEnvironmentEntitlements — resolve the org's environment-capacity state so
|
|
3
|
+
* the `sys_environment` list can present the right "create" affordance up front.
|
|
4
|
+
*
|
|
5
|
+
* Two signals, in priority order:
|
|
6
|
+
* 1. AUTHORITATIVE — GET /cloud/environment-entitlements (org-scoped, computed
|
|
7
|
+
* by the same helper the create guard uses). Gives plan + dev-create
|
|
8
|
+
* capability precisely, including the seat-scaled / subscription cases the
|
|
9
|
+
* client can't derive from rows.
|
|
10
|
+
* 2. FALLBACK — when that endpoint is unavailable (older control plane / error),
|
|
11
|
+
* derive `hasProductionEnv` from the org's env rows via the data API (which
|
|
12
|
+
* is org-scoped on the control plane). This keeps the critical
|
|
13
|
+
* "set up your production environment" path working without a backend deploy;
|
|
14
|
+
* free-vs-paid is left unknown, and a stray create POST is caught by the
|
|
15
|
+
* entitlement dialog safety net.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when disabled (not the environment list) so callers can cheaply
|
|
18
|
+
* branch. Re-resolves when `refreshKey` changes (e.g. after a create).
|
|
19
|
+
*/
|
|
20
|
+
import { type EnvironmentEntitlementsState } from './entitlements';
|
|
21
|
+
export interface UseEnvironmentEntitlementsOptions {
|
|
22
|
+
/** Only fetch when this is the environment list (objectName === 'sys_environment'). */
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
dataSource: any;
|
|
25
|
+
/** Authenticated fetch (Bearer + tenant + cookies) — from the action runtime. */
|
|
26
|
+
authFetch: (url: string, init?: any) => Promise<Response>;
|
|
27
|
+
/** Control-plane origin (VITE_SERVER_URL); '' in same-origin production. */
|
|
28
|
+
apiBase: string;
|
|
29
|
+
/** Bump to re-resolve (e.g. the list's refreshKey after a successful create). */
|
|
30
|
+
refreshKey?: unknown;
|
|
31
|
+
}
|
|
32
|
+
export declare function useEnvironmentEntitlements(opts: UseEnvironmentEntitlementsOptions): EnvironmentEntitlementsState | null;
|