@stigmer/react 0.0.99 → 0.0.100
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/index.d.ts +17 -3
- package/index.d.ts.map +1 -1
- package/index.js +16 -3
- package/index.js.map +1 -1
- package/internal/menu.d.ts +17 -0
- package/internal/menu.d.ts.map +1 -0
- package/internal/menu.js +43 -0
- package/internal/menu.js.map +1 -0
- package/library/LibraryBreadcrumbContext.d.ts +30 -0
- package/library/LibraryBreadcrumbContext.d.ts.map +1 -0
- package/library/LibraryBreadcrumbContext.js +39 -0
- package/library/LibraryBreadcrumbContext.js.map +1 -0
- package/library/index.d.ts +1 -0
- package/library/index.d.ts.map +1 -1
- package/library/index.js +1 -0
- package/library/index.js.map +1 -1
- package/organization/OrgProvider.d.ts +59 -0
- package/organization/OrgProvider.d.ts.map +1 -0
- package/organization/OrgProvider.js +130 -0
- package/organization/OrgProvider.js.map +1 -0
- package/organization/OrgSwitcher.d.ts +36 -0
- package/organization/OrgSwitcher.d.ts.map +1 -0
- package/organization/OrgSwitcher.js +73 -0
- package/organization/OrgSwitcher.js.map +1 -0
- package/organization/index.d.ts +6 -0
- package/organization/index.d.ts.map +1 -1
- package/organization/index.js +3 -0
- package/organization/index.js.map +1 -1
- package/organization/useOrgGate.d.ts +101 -0
- package/organization/useOrgGate.d.ts.map +1 -0
- package/organization/useOrgGate.js +99 -0
- package/organization/useOrgGate.js.map +1 -0
- package/package.json +5 -4
- package/runner/RunnerListPanel.d.ts +13 -8
- package/runner/RunnerListPanel.d.ts.map +1 -1
- package/runner/RunnerListPanel.js +10 -6
- package/runner/RunnerListPanel.js.map +1 -1
- package/settings/ApiKeysSection.d.ts +3 -0
- package/settings/ApiKeysSection.d.ts.map +1 -0
- package/settings/ApiKeysSection.js +30 -0
- package/settings/ApiKeysSection.js.map +1 -0
- package/settings/EnvironmentsSection.d.ts +3 -0
- package/settings/EnvironmentsSection.d.ts.map +1 -0
- package/settings/EnvironmentsSection.js +49 -0
- package/settings/EnvironmentsSection.js.map +1 -0
- package/settings/IdentityProvidersSection.d.ts +12 -0
- package/settings/IdentityProvidersSection.d.ts.map +1 -0
- package/settings/IdentityProvidersSection.js +34 -0
- package/settings/IdentityProvidersSection.js.map +1 -0
- package/settings/InvitationsSection.d.ts +3 -0
- package/settings/InvitationsSection.d.ts.map +1 -0
- package/settings/InvitationsSection.js +13 -0
- package/settings/InvitationsSection.js.map +1 -0
- package/settings/MembersSection.d.ts +3 -0
- package/settings/MembersSection.d.ts.map +1 -0
- package/settings/MembersSection.js +14 -0
- package/settings/MembersSection.js.map +1 -0
- package/settings/OAuthAppsSection.d.ts +3 -0
- package/settings/OAuthAppsSection.d.ts.map +1 -0
- package/settings/OAuthAppsSection.js +33 -0
- package/settings/OAuthAppsSection.js.map +1 -0
- package/settings/OrgProfileSection.d.ts +3 -0
- package/settings/OrgProfileSection.d.ts.map +1 -0
- package/settings/OrgProfileSection.js +15 -0
- package/settings/OrgProfileSection.js.map +1 -0
- package/settings/PlatformClientsSection.d.ts +3 -0
- package/settings/PlatformClientsSection.d.ts.map +1 -0
- package/settings/PlatformClientsSection.js +48 -0
- package/settings/PlatformClientsSection.js.map +1 -0
- package/settings/UsageSection.d.ts +3 -0
- package/settings/UsageSection.d.ts.map +1 -0
- package/settings/UsageSection.js +14 -0
- package/settings/UsageSection.js.map +1 -0
- package/settings/index.d.ts +13 -0
- package/settings/index.d.ts.map +1 -0
- package/settings/index.js +11 -0
- package/settings/index.js.map +1 -0
- package/settings/settings-nav.d.ts +25 -0
- package/settings/settings-nav.d.ts.map +1 -0
- package/settings/settings-nav.js +41 -0
- package/settings/settings-nav.js.map +1 -0
- package/src/index.ts +32 -1
- package/src/internal/menu.tsx +160 -0
- package/src/library/LibraryBreadcrumbContext.tsx +70 -0
- package/src/library/index.ts +6 -0
- package/src/organization/OrgProvider.tsx +184 -0
- package/src/organization/OrgSwitcher.tsx +275 -0
- package/src/organization/index.ts +10 -0
- package/src/organization/useOrgGate.ts +183 -0
- package/src/runner/RunnerListPanel.tsx +14 -9
- package/src/settings/ApiKeysSection.tsx +96 -0
- package/src/settings/EnvironmentsSection.tsx +162 -0
- package/src/settings/IdentityProvidersSection.tsx +123 -0
- package/src/settings/InvitationsSection.tsx +42 -0
- package/src/settings/MembersSection.tsx +41 -0
- package/src/settings/OAuthAppsSection.tsx +100 -0
- package/src/settings/OrgProfileSection.tsx +41 -0
- package/src/settings/PlatformClientsSection.tsx +149 -0
- package/src/settings/UsageSection.tsx +41 -0
- package/src/settings/index.ts +13 -0
- package/src/settings/settings-nav.ts +78 -0
- package/src/user/UserMenu.tsx +241 -0
- package/src/user/index.ts +2 -0
- package/styles.css +1 -1
- package/user/UserMenu.d.ts +82 -0
- package/user/UserMenu.d.ts.map +1 -0
- package/user/UserMenu.js +51 -0
- package/user/UserMenu.js.map +1 -0
- package/user/index.d.ts +3 -0
- package/user/index.d.ts.map +1 -0
- package/user/index.js +2 -0
- package/user/index.js.map +1 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export { OrgProvider, useOrg, useActiveOrgSlug } from "./OrgProvider";
|
|
2
|
+
export type { OrgContextValue } from "./OrgProvider";
|
|
3
|
+
export { useOrgGate } from "./useOrgGate";
|
|
4
|
+
export type {
|
|
5
|
+
UseOrgGateOptions,
|
|
6
|
+
OrgGateState,
|
|
7
|
+
UseOrgGateReturn,
|
|
8
|
+
} from "./useOrgGate";
|
|
1
9
|
export { useOrganization } from "./useOrganization";
|
|
2
10
|
export type { UseOrganizationReturn } from "./useOrganization";
|
|
3
11
|
export { useCreateOrganization } from "./useCreateOrganization";
|
|
@@ -8,3 +16,5 @@ export { CreateOrganizationForm } from "./CreateOrganizationForm";
|
|
|
8
16
|
export type { CreateOrganizationFormProps } from "./CreateOrganizationForm";
|
|
9
17
|
export { OrgProfilePanel } from "./OrgProfilePanel";
|
|
10
18
|
export type { OrgProfilePanelProps } from "./OrgProfilePanel";
|
|
19
|
+
export { OrgSwitcher } from "./OrgSwitcher";
|
|
20
|
+
export type { OrgSwitcherProps } from "./OrgSwitcher";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useOrg } from "./OrgProvider";
|
|
5
|
+
|
|
6
|
+
const PROVISIONING_POLL_MS = 2_000;
|
|
7
|
+
const PROVISIONING_TIMEOUT_MS = 10_000;
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options passed to {@link useOrgGate} by the host application.
|
|
15
|
+
*
|
|
16
|
+
* Both values are computed by the consumer using framework-specific APIs
|
|
17
|
+
* (e.g. `usePathname()` in Next.js, `useLocation()` in react-router) so
|
|
18
|
+
* that the hook itself has zero framework dependencies.
|
|
19
|
+
*/
|
|
20
|
+
export interface UseOrgGateOptions {
|
|
21
|
+
/** True when the current route should bypass the gate (e.g. `/invite/` links). */
|
|
22
|
+
readonly isBypassed: boolean;
|
|
23
|
+
/** True when the auth mode supports server-side personal org provisioning. */
|
|
24
|
+
readonly isOidcMode: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Discriminated union representing the current state of the org gate.
|
|
29
|
+
*
|
|
30
|
+
* Narrow on `status` to access variant-specific data:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* if (state.status === "error") {
|
|
34
|
+
* console.log(state.message); // string — only available on "error"
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type OrgGateState =
|
|
39
|
+
| {
|
|
40
|
+
/** Gate is bypassed for the current route. */
|
|
41
|
+
readonly status: "bypassed";
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
/** Initial organization list fetch is in progress. */
|
|
45
|
+
readonly status: "loading";
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
/** Personal organization provisioning is in progress. */
|
|
49
|
+
readonly status: "provisioning";
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
/** Organization fetch failed and user action is required. */
|
|
53
|
+
readonly status: "error";
|
|
54
|
+
/** Human-readable failure message suitable for UI display. */
|
|
55
|
+
readonly message: string;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
/** No organizations are available for the current user. */
|
|
59
|
+
readonly status: "no-orgs";
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
/** At least one organization is available and the app can render. */
|
|
63
|
+
readonly status: "ready";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return value of {@link useOrgGate}.
|
|
68
|
+
*
|
|
69
|
+
* `retry` and `refresh` are always available; the consumer invokes them
|
|
70
|
+
* in the appropriate state (`retry` when `status === "error"`, `refresh`
|
|
71
|
+
* when `status === "no-orgs"` after creating an organization).
|
|
72
|
+
*/
|
|
73
|
+
export interface UseOrgGateReturn {
|
|
74
|
+
/** Current gate state. Discriminated union on `status`. */
|
|
75
|
+
readonly state: OrgGateState;
|
|
76
|
+
/** Re-attempt the organization fetch after a failure. */
|
|
77
|
+
readonly retry: () => void;
|
|
78
|
+
/** Refetch orgs (e.g. after creating one). Optionally auto-select by slug. */
|
|
79
|
+
readonly refresh: (targetSlug?: string) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Hook
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Headless behavior hook that encapsulates the org-gate provisioning
|
|
88
|
+
* state machine.
|
|
89
|
+
*
|
|
90
|
+
* The hook observes the organization context from {@link useOrg} and
|
|
91
|
+
* drives the following lifecycle:
|
|
92
|
+
*
|
|
93
|
+
* 1. **`bypassed`** — `isBypassed` is true; the gate is inactive.
|
|
94
|
+
* 2. **`loading`** — the initial org list fetch is in flight.
|
|
95
|
+
* 3. **`provisioning`** — OIDC mode, zero orgs: the server is creating
|
|
96
|
+
* the personal org. The hook polls every 2 s and times out after 10 s.
|
|
97
|
+
* 4. **`error`** — the org fetch failed; `message` carries the reason.
|
|
98
|
+
* 5. **`no-orgs`** — no organizations exist (or provisioning timed out);
|
|
99
|
+
* the consumer should show an onboarding form.
|
|
100
|
+
* 6. **`ready`** — at least one org exists; render the app.
|
|
101
|
+
*
|
|
102
|
+
* The consumer computes `isBypassed` and `isOidcMode` using
|
|
103
|
+
* framework-specific APIs and passes them in, keeping this hook free of
|
|
104
|
+
* routing or auth-framework dependencies (DD-004).
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* const { state, retry, refresh } = useOrgGate({ isBypassed, isOidcMode });
|
|
109
|
+
*
|
|
110
|
+
* switch (state.status) {
|
|
111
|
+
* case "bypassed":
|
|
112
|
+
* case "ready":
|
|
113
|
+
* return <>{children}</>;
|
|
114
|
+
* case "loading":
|
|
115
|
+
* return <Spinner />;
|
|
116
|
+
* case "provisioning":
|
|
117
|
+
* return <WelcomeScreen />;
|
|
118
|
+
* case "error":
|
|
119
|
+
* return <ErrorScreen message={state.message} onRetry={retry} />;
|
|
120
|
+
* case "no-orgs":
|
|
121
|
+
* return <OnboardingForm onCreated={(org) => refresh(org.slug)} />;
|
|
122
|
+
* }
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export function useOrgGate(options: UseOrgGateOptions): UseOrgGateReturn {
|
|
126
|
+
const { isBypassed, isOidcMode } = options;
|
|
127
|
+
const { orgs, isLoading, error, retry, refresh } = useOrg();
|
|
128
|
+
|
|
129
|
+
const [provisioningStarted, setProvisioningStarted] = useState(false);
|
|
130
|
+
const [provisioningTimedOut, setProvisioningTimedOut] = useState(false);
|
|
131
|
+
|
|
132
|
+
// React-sanctioned "adjust state during render" pattern: the guard on
|
|
133
|
+
// `!provisioningStarted` prevents infinite re-render loops.
|
|
134
|
+
if (
|
|
135
|
+
!isBypassed &&
|
|
136
|
+
!provisioningStarted &&
|
|
137
|
+
!isLoading &&
|
|
138
|
+
orgs.length === 0 &&
|
|
139
|
+
!error &&
|
|
140
|
+
isOidcMode
|
|
141
|
+
) {
|
|
142
|
+
setProvisioningStarted(true);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const isProvisioning =
|
|
146
|
+
provisioningStarted && orgs.length === 0 && !provisioningTimedOut;
|
|
147
|
+
|
|
148
|
+
// Poll for the personal org while provisioning is in progress.
|
|
149
|
+
// Errors from refresh() are absorbed — transient failures (identity not
|
|
150
|
+
// yet created) are expected during the provisioning window.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!isProvisioning) return;
|
|
153
|
+
|
|
154
|
+
const interval = setInterval(() => refresh(), PROVISIONING_POLL_MS);
|
|
155
|
+
const timeout = setTimeout(
|
|
156
|
+
() => setProvisioningTimedOut(true),
|
|
157
|
+
PROVISIONING_TIMEOUT_MS,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
clearInterval(interval);
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
};
|
|
164
|
+
}, [isProvisioning, refresh]);
|
|
165
|
+
|
|
166
|
+
// Resolve state — order matters: bypass and provisioning take priority.
|
|
167
|
+
let state: OrgGateState;
|
|
168
|
+
if (isBypassed) {
|
|
169
|
+
state = { status: "bypassed" };
|
|
170
|
+
} else if (isProvisioning) {
|
|
171
|
+
state = { status: "provisioning" };
|
|
172
|
+
} else if (isLoading) {
|
|
173
|
+
state = { status: "loading" };
|
|
174
|
+
} else if (error) {
|
|
175
|
+
state = { status: "error", message: error };
|
|
176
|
+
} else if (orgs.length === 0) {
|
|
177
|
+
state = { status: "no-orgs" };
|
|
178
|
+
} else {
|
|
179
|
+
state = { status: "ready" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { state, retry, refresh };
|
|
183
|
+
}
|
|
@@ -41,10 +41,11 @@ export interface RunnerListPanelProps {
|
|
|
41
41
|
* Include system-managed (ephemeral cloud) runners in the list.
|
|
42
42
|
*
|
|
43
43
|
* System-managed runners are auto-provisioned for cloud executions
|
|
44
|
-
* and labeled `stigmer.ai/system-managed: "true"`.
|
|
45
|
-
*
|
|
44
|
+
* and labeled `stigmer.ai/system-managed: "true"`. They are excluded
|
|
45
|
+
* by default so user-facing views only show user-created runners.
|
|
46
|
+
* Pass `true` in admin views that need full fleet visibility.
|
|
46
47
|
*
|
|
47
|
-
* @default
|
|
48
|
+
* @default false
|
|
48
49
|
*/
|
|
49
50
|
readonly includeSystemManaged?: boolean;
|
|
50
51
|
/** Expose refetch so parent components can trigger a list refresh. */
|
|
@@ -64,8 +65,8 @@ export interface RunnerListPanelProps {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
68
|
+
* Panel that displays runners in an organization with lifecycle
|
|
69
|
+
* management actions.
|
|
69
70
|
*
|
|
70
71
|
* Each runner is rendered as a card row showing name, phase indicator,
|
|
71
72
|
* machine information, and operational metadata. Non-system-managed
|
|
@@ -73,8 +74,12 @@ export interface RunnerListPanelProps {
|
|
|
73
74
|
* inline confirmation — no modals or portals.
|
|
74
75
|
*
|
|
75
76
|
* Rows are sorted by phase (active runners first) then alphabetically
|
|
76
|
-
* by name. System-managed runners display a "System"
|
|
77
|
-
* no action affordances.
|
|
77
|
+
* by name. System-managed runners (when included) display a "System"
|
|
78
|
+
* badge and have no action affordances.
|
|
79
|
+
*
|
|
80
|
+
* By default only user-created runners are shown. Pass
|
|
81
|
+
* `includeSystemManaged={true}` for admin views that need full fleet
|
|
82
|
+
* visibility.
|
|
78
83
|
*
|
|
79
84
|
* Designed for the Settings > Runners page but embeddable in any
|
|
80
85
|
* context that needs runner fleet management. Fetches data via
|
|
@@ -89,7 +94,7 @@ export interface RunnerListPanelProps {
|
|
|
89
94
|
*
|
|
90
95
|
* <RunnerListPanel
|
|
91
96
|
* org="acme"
|
|
92
|
-
* includeSystemManaged
|
|
97
|
+
* includeSystemManaged
|
|
93
98
|
* onStopped={(runner) => toast(`${runner.metadata?.name} stopped`)}
|
|
94
99
|
* onDeleted={(runner) => toast(`${runner.metadata?.name} deleted`)}
|
|
95
100
|
* onRefetchRef={(refetch) => { refetchRef.current = refetch; }}
|
|
@@ -98,7 +103,7 @@ export interface RunnerListPanelProps {
|
|
|
98
103
|
*/
|
|
99
104
|
export function RunnerListPanel({
|
|
100
105
|
org,
|
|
101
|
-
includeSystemManaged =
|
|
106
|
+
includeSystemManaged = false,
|
|
102
107
|
onRefetchRef,
|
|
103
108
|
onStopped,
|
|
104
109
|
onDeleted,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import type { ApiKey } from "@stigmer/protos/ai/stigmer/iam/apikey/v1/api_pb";
|
|
5
|
+
import { ApiKeyListPanel } from "../api-key/ApiKeyListPanel";
|
|
6
|
+
import { CreateApiKeyForm } from "../api-key/CreateApiKeyForm";
|
|
7
|
+
import { ApiKeyCreatedAlert } from "../api-key/ApiKeyCreatedAlert";
|
|
8
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
|
|
9
|
+
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice";
|
|
10
|
+
import { useActiveOrgSlug } from "../organization/OrgProvider";
|
|
11
|
+
|
|
12
|
+
type FlowState =
|
|
13
|
+
| { phase: "idle" }
|
|
14
|
+
| { phase: "creating" }
|
|
15
|
+
| { phase: "reveal"; rawKey: string; keyName: string };
|
|
16
|
+
|
|
17
|
+
/** Settings section for listing and creating organization API keys. */
|
|
18
|
+
export function ApiKeysSection() {
|
|
19
|
+
const org = useActiveOrgSlug();
|
|
20
|
+
const apiKeysAvailable = useResourceAvailable(ApiResourceKind.api_key);
|
|
21
|
+
const [flow, setFlow] = useState<FlowState>({ phase: "idle" });
|
|
22
|
+
const listRefetchRef = useRef<(() => void) | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleRefetchRef = useCallback((refetch: () => void) => {
|
|
25
|
+
listRefetchRef.current = refetch;
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const handleCreated = useCallback((apiKey: ApiKey) => {
|
|
29
|
+
const rawKey = apiKey.spec?.keyHash ?? "";
|
|
30
|
+
const keyName = apiKey.metadata?.name ?? "API key";
|
|
31
|
+
setFlow({ phase: "reveal", rawKey, keyName });
|
|
32
|
+
listRefetchRef.current?.();
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const handleDismissReveal = useCallback(() => {
|
|
36
|
+
setFlow({ phase: "idle" });
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<section aria-labelledby="api-keys-heading">
|
|
41
|
+
<div className="mb-3 flex items-center justify-between">
|
|
42
|
+
<h2
|
|
43
|
+
id="api-keys-heading"
|
|
44
|
+
className="text-foreground text-sm font-semibold"
|
|
45
|
+
>
|
|
46
|
+
API Keys
|
|
47
|
+
</h2>
|
|
48
|
+
|
|
49
|
+
{apiKeysAvailable && flow.phase === "idle" && (
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
onClick={() => setFlow({ phase: "creating" })}
|
|
53
|
+
className="text-primary hover:text-foreground text-xs font-medium transition-colors"
|
|
54
|
+
>
|
|
55
|
+
+ New API key
|
|
56
|
+
</button>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
<p className="text-muted-foreground mb-4 text-xs">
|
|
60
|
+
API keys authenticate CLI sessions and programmatic access to the
|
|
61
|
+
Stigmer API. Keys are scoped to your identity and work across all
|
|
62
|
+
your organizations.
|
|
63
|
+
</p>
|
|
64
|
+
|
|
65
|
+
{!apiKeysAvailable ? (
|
|
66
|
+
<CloudFeatureNotice>
|
|
67
|
+
API keys are not available in local mode. When running locally, the
|
|
68
|
+
CLI authenticates directly without API keys.
|
|
69
|
+
</CloudFeatureNotice>
|
|
70
|
+
) : (
|
|
71
|
+
<>
|
|
72
|
+
{flow.phase === "reveal" && (
|
|
73
|
+
<ApiKeyCreatedAlert
|
|
74
|
+
rawKey={flow.rawKey}
|
|
75
|
+
keyName={flow.keyName}
|
|
76
|
+
onDismiss={handleDismissReveal}
|
|
77
|
+
className="mb-4"
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{flow.phase === "creating" && (
|
|
82
|
+
<div className="border-border bg-card mb-4 rounded-lg border p-4">
|
|
83
|
+
<CreateApiKeyForm
|
|
84
|
+
org={org}
|
|
85
|
+
onCreated={handleCreated}
|
|
86
|
+
onCancel={() => setFlow({ phase: "idle" })}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
<ApiKeyListPanel onRefetchRef={handleRefetchRef} />
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { getUserMessage } from "@stigmer/sdk";
|
|
5
|
+
import { usePersonalEnvironment } from "../environment/usePersonalEnvironment";
|
|
6
|
+
import { EnvironmentVariableEditor } from "../environment/EnvironmentVariableEditor";
|
|
7
|
+
import { EnvironmentListPanel } from "../environment/EnvironmentListPanel";
|
|
8
|
+
import { CreateEnvironmentForm } from "../environment/CreateEnvironmentForm";
|
|
9
|
+
import { useActiveOrgSlug } from "../organization/OrgProvider";
|
|
10
|
+
|
|
11
|
+
const ENV_EXCLUDE_LABELS: Record<string, string>[] = [
|
|
12
|
+
{ "stigmer.ai/personal": "true" },
|
|
13
|
+
{ "stigmer.ai/managed": "true" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/** Settings section for personal and organization environment variables. */
|
|
17
|
+
export function EnvironmentsSection() {
|
|
18
|
+
const org = useActiveOrgSlug();
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-10">
|
|
22
|
+
<PersonalEnvironmentCard org={org} />
|
|
23
|
+
<EnvironmentsCard org={org} />
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function PersonalEnvironmentCard({ org }: { org: string }) {
|
|
29
|
+
const {
|
|
30
|
+
environment,
|
|
31
|
+
isLoading,
|
|
32
|
+
error,
|
|
33
|
+
getOrCreate,
|
|
34
|
+
isMutating,
|
|
35
|
+
} = usePersonalEnvironment(org || null);
|
|
36
|
+
|
|
37
|
+
const bootstrapAttempted = useRef(false);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
bootstrapAttempted.current = false;
|
|
41
|
+
}, [org]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!org || isLoading || environment || bootstrapAttempted.current) return;
|
|
45
|
+
bootstrapAttempted.current = true;
|
|
46
|
+
getOrCreate().catch(() => {});
|
|
47
|
+
}, [org, isLoading, environment, getOrCreate]);
|
|
48
|
+
|
|
49
|
+
const environmentId = environment?.metadata?.id;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<section aria-labelledby="personal-env-heading">
|
|
53
|
+
<div className="mb-3 flex items-baseline gap-2">
|
|
54
|
+
<h2
|
|
55
|
+
id="personal-env-heading"
|
|
56
|
+
className="text-foreground text-sm font-semibold"
|
|
57
|
+
>
|
|
58
|
+
Personal Environment
|
|
59
|
+
</h2>
|
|
60
|
+
<span className="bg-primary-subtle text-primary rounded-full px-2 py-0.5 text-[0.6rem] font-medium uppercase tracking-wider">
|
|
61
|
+
You
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
<p className="text-muted-foreground mb-4 text-xs">
|
|
65
|
+
Your private secrets and configuration, automatically managed for you.
|
|
66
|
+
Only visible to you — used when running agents that require your
|
|
67
|
+
personal credentials.
|
|
68
|
+
</p>
|
|
69
|
+
|
|
70
|
+
{isLoading || isMutating ? (
|
|
71
|
+
<SkeletonRows count={3} />
|
|
72
|
+
) : error ? (
|
|
73
|
+
<p className="text-destructive text-xs" role="alert">
|
|
74
|
+
{getUserMessage(error)}
|
|
75
|
+
</p>
|
|
76
|
+
) : environmentId ? (
|
|
77
|
+
<EnvironmentVariableEditor environmentId={environmentId} />
|
|
78
|
+
) : (
|
|
79
|
+
<p className="text-muted-foreground text-xs">
|
|
80
|
+
Your personal environment will be created automatically when needed.
|
|
81
|
+
</p>
|
|
82
|
+
)}
|
|
83
|
+
</section>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function EnvironmentsCard({ org }: { org: string }) {
|
|
88
|
+
const [showCreate, setShowCreate] = useState(false);
|
|
89
|
+
const listRefetchRef = useRef<(() => void) | null>(null);
|
|
90
|
+
|
|
91
|
+
const handleRefetchRef = useCallback((refetch: () => void) => {
|
|
92
|
+
listRefetchRef.current = refetch;
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
const handleCreated = useCallback(() => {
|
|
96
|
+
setShowCreate(false);
|
|
97
|
+
listRefetchRef.current?.();
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<section aria-labelledby="org-env-heading">
|
|
102
|
+
<div className="mb-3 flex items-center justify-between">
|
|
103
|
+
<h2
|
|
104
|
+
id="org-env-heading"
|
|
105
|
+
className="text-foreground text-sm font-semibold"
|
|
106
|
+
>
|
|
107
|
+
Environments
|
|
108
|
+
</h2>
|
|
109
|
+
|
|
110
|
+
{!showCreate && (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => setShowCreate(true)}
|
|
114
|
+
className="text-primary hover:text-foreground text-xs font-medium transition-colors"
|
|
115
|
+
>
|
|
116
|
+
+ New environment
|
|
117
|
+
</button>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
<p className="text-muted-foreground mb-4 text-xs">
|
|
121
|
+
Named environments for your organization. Store credentials, API tokens,
|
|
122
|
+
and configuration that agents need at runtime.
|
|
123
|
+
</p>
|
|
124
|
+
|
|
125
|
+
{showCreate && (
|
|
126
|
+
<div className="border-border bg-card mb-4 rounded-lg border p-4">
|
|
127
|
+
<CreateEnvironmentForm
|
|
128
|
+
org={org}
|
|
129
|
+
onCreated={handleCreated}
|
|
130
|
+
onCancel={() => setShowCreate(false)}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{org ? (
|
|
136
|
+
<EnvironmentListPanel
|
|
137
|
+
org={org}
|
|
138
|
+
excludeLabels={ENV_EXCLUDE_LABELS}
|
|
139
|
+
onRefetchRef={handleRefetchRef}
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
<p className="text-muted-foreground py-4 text-center text-xs">
|
|
143
|
+
Select an organization to view environments.
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
</section>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function SkeletonRows({ count }: { count: number }) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="space-y-2" aria-busy="true" aria-label="Loading">
|
|
153
|
+
{Array.from({ length: count }, (_, i) => (
|
|
154
|
+
<div
|
|
155
|
+
key={i}
|
|
156
|
+
className="bg-muted-subtle h-8 animate-pulse rounded"
|
|
157
|
+
style={{ width: `${85 - i * 10}%` }}
|
|
158
|
+
/>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from "react";
|
|
4
|
+
import type { IdentityProvider } from "@stigmer/protos/ai/stigmer/iam/identityprovider/v1/api_pb";
|
|
5
|
+
import { IdentityProviderListPanel } from "../identity-provider/IdentityProviderListPanel";
|
|
6
|
+
import { IdentityProviderWizard } from "../identity-provider/IdentityProviderWizard";
|
|
7
|
+
import { IdentityProviderDetailPanel } from "../identity-provider/IdentityProviderDetailPanel";
|
|
8
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
|
|
9
|
+
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice";
|
|
10
|
+
import { useOrg } from "../organization/OrgProvider";
|
|
11
|
+
|
|
12
|
+
/** Props for {@link IdentityProvidersSection}. */
|
|
13
|
+
export interface IdentityProvidersSectionProps {
|
|
14
|
+
/**
|
|
15
|
+
* Base URL used to construct the SSO login link shown in the detail panel.
|
|
16
|
+
* Defaults to `window.location.origin` when omitted (correct for web apps).
|
|
17
|
+
* Desktop apps should pass the cloud console origin instead.
|
|
18
|
+
*/
|
|
19
|
+
readonly ssoLoginBaseUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type FlowState =
|
|
23
|
+
| { phase: "idle" }
|
|
24
|
+
| { phase: "creating" }
|
|
25
|
+
| { phase: "editing"; identityProvider: IdentityProvider };
|
|
26
|
+
|
|
27
|
+
/** Settings section for configuring OIDC identity providers. */
|
|
28
|
+
export function IdentityProvidersSection({
|
|
29
|
+
ssoLoginBaseUrl,
|
|
30
|
+
}: IdentityProvidersSectionProps = {}) {
|
|
31
|
+
const { activeOrg } = useOrg();
|
|
32
|
+
const idpAvailable = useResourceAvailable(ApiResourceKind.identity_provider);
|
|
33
|
+
const orgSlug = activeOrg?.metadata?.slug ?? "";
|
|
34
|
+
|
|
35
|
+
const [flow, setFlow] = useState<FlowState>({ phase: "idle" });
|
|
36
|
+
const listRefetchRef = useRef<(() => void) | null>(null);
|
|
37
|
+
|
|
38
|
+
const handleRefetchRef = useCallback((refetch: () => void) => {
|
|
39
|
+
listRefetchRef.current = refetch;
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleCreated = useCallback(() => {
|
|
43
|
+
listRefetchRef.current?.();
|
|
44
|
+
setFlow({ phase: "idle" });
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleUpdated = useCallback(() => {
|
|
48
|
+
listRefetchRef.current?.();
|
|
49
|
+
setFlow({ phase: "idle" });
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
const baseUrl =
|
|
53
|
+
ssoLoginBaseUrl ??
|
|
54
|
+
(typeof window !== "undefined" ? window.location.origin : "");
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<section aria-labelledby="identity-providers-heading">
|
|
58
|
+
<div className="mb-3 flex items-center justify-between">
|
|
59
|
+
<h2
|
|
60
|
+
id="identity-providers-heading"
|
|
61
|
+
className="text-foreground text-sm font-semibold"
|
|
62
|
+
>
|
|
63
|
+
Identity Providers
|
|
64
|
+
</h2>
|
|
65
|
+
|
|
66
|
+
{idpAvailable && orgSlug && flow.phase === "idle" && (
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={() => setFlow({ phase: "creating" })}
|
|
70
|
+
className="text-primary hover:text-foreground text-xs font-medium transition-colors"
|
|
71
|
+
>
|
|
72
|
+
+ New identity provider
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
<p className="text-muted-foreground mb-4 text-xs">
|
|
77
|
+
Identity providers define external OIDC trust relationships for
|
|
78
|
+
federated authentication. Configure providers for platform-managed
|
|
79
|
+
organizations or self-managed SSO.
|
|
80
|
+
</p>
|
|
81
|
+
|
|
82
|
+
{!idpAvailable ? (
|
|
83
|
+
<CloudFeatureNotice>
|
|
84
|
+
Identity providers are not available in local mode. Federated
|
|
85
|
+
authentication requires Stigmer Cloud.
|
|
86
|
+
</CloudFeatureNotice>
|
|
87
|
+
) : !orgSlug ? (
|
|
88
|
+
<p className="text-muted-foreground py-4 text-center text-xs">
|
|
89
|
+
Select an organization to manage identity providers.
|
|
90
|
+
</p>
|
|
91
|
+
) : flow.phase === "creating" ? (
|
|
92
|
+
<div className="border-border bg-card rounded-lg border p-4">
|
|
93
|
+
<IdentityProviderWizard
|
|
94
|
+
org={orgSlug}
|
|
95
|
+
onCreated={handleCreated}
|
|
96
|
+
onCancel={() => setFlow({ phase: "idle" })}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
) : flow.phase === "editing" ? (
|
|
100
|
+
<div className="border-border bg-card rounded-lg border p-4">
|
|
101
|
+
<IdentityProviderDetailPanel
|
|
102
|
+
identityProvider={flow.identityProvider}
|
|
103
|
+
ssoLoginUrl={
|
|
104
|
+
flow.identityProvider.spec?.isSsoProvider
|
|
105
|
+
? `${baseUrl}/login?org=${orgSlug}`
|
|
106
|
+
: undefined
|
|
107
|
+
}
|
|
108
|
+
onUpdated={handleUpdated}
|
|
109
|
+
onBack={() => setFlow({ phase: "idle" })}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
<IdentityProviderListPanel
|
|
114
|
+
org={orgSlug}
|
|
115
|
+
onEdit={(idp) =>
|
|
116
|
+
setFlow({ phase: "editing", identityProvider: idp })
|
|
117
|
+
}
|
|
118
|
+
onRefetchRef={handleRefetchRef}
|
|
119
|
+
/>
|
|
120
|
+
)}
|
|
121
|
+
</section>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { InvitationManager } from "../invitation/InvitationManager";
|
|
4
|
+
import { useResourceAvailable, ApiResourceKind } from "../deployment-mode";
|
|
5
|
+
import { CloudFeatureNotice } from "../internal/CloudFeatureNotice";
|
|
6
|
+
import { useActiveOrgSlug } from "../organization/OrgProvider";
|
|
7
|
+
|
|
8
|
+
/** Settings section for creating and managing organization invitations. */
|
|
9
|
+
export function InvitationsSection() {
|
|
10
|
+
const org = useActiveOrgSlug();
|
|
11
|
+
const invitationsAvailable = useResourceAvailable(ApiResourceKind.invitation);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<section aria-labelledby="invitations-heading">
|
|
15
|
+
<div className="mb-3">
|
|
16
|
+
<h2
|
|
17
|
+
id="invitations-heading"
|
|
18
|
+
className="text-foreground text-sm font-semibold"
|
|
19
|
+
>
|
|
20
|
+
Invitations
|
|
21
|
+
</h2>
|
|
22
|
+
</div>
|
|
23
|
+
<p className="text-muted-foreground mb-4 text-xs">
|
|
24
|
+
Shareable invite links that grant organization membership with a
|
|
25
|
+
configurable role. Create single-use links for specific people or
|
|
26
|
+
multi-use links for public sharing.
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
{!invitationsAvailable ? (
|
|
30
|
+
<CloudFeatureNotice>
|
|
31
|
+
Invitations are not available in local mode.
|
|
32
|
+
</CloudFeatureNotice>
|
|
33
|
+
) : !org ? (
|
|
34
|
+
<p className="text-muted-foreground py-4 text-center text-xs">
|
|
35
|
+
Select an organization to manage invitations.
|
|
36
|
+
</p>
|
|
37
|
+
) : (
|
|
38
|
+
<InvitationManager org={org} />
|
|
39
|
+
)}
|
|
40
|
+
</section>
|
|
41
|
+
);
|
|
42
|
+
}
|