@stigmer/react 0.0.99 → 0.0.101

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.
Files changed (112) hide show
  1. package/index.d.ts +17 -3
  2. package/index.d.ts.map +1 -1
  3. package/index.js +16 -3
  4. package/index.js.map +1 -1
  5. package/internal/menu.d.ts +17 -0
  6. package/internal/menu.d.ts.map +1 -0
  7. package/internal/menu.js +43 -0
  8. package/internal/menu.js.map +1 -0
  9. package/library/LibraryBreadcrumbContext.d.ts +30 -0
  10. package/library/LibraryBreadcrumbContext.d.ts.map +1 -0
  11. package/library/LibraryBreadcrumbContext.js +39 -0
  12. package/library/LibraryBreadcrumbContext.js.map +1 -0
  13. package/library/index.d.ts +1 -0
  14. package/library/index.d.ts.map +1 -1
  15. package/library/index.js +1 -0
  16. package/library/index.js.map +1 -1
  17. package/organization/OrgProvider.d.ts +59 -0
  18. package/organization/OrgProvider.d.ts.map +1 -0
  19. package/organization/OrgProvider.js +130 -0
  20. package/organization/OrgProvider.js.map +1 -0
  21. package/organization/OrgSwitcher.d.ts +36 -0
  22. package/organization/OrgSwitcher.d.ts.map +1 -0
  23. package/organization/OrgSwitcher.js +73 -0
  24. package/organization/OrgSwitcher.js.map +1 -0
  25. package/organization/index.d.ts +6 -0
  26. package/organization/index.d.ts.map +1 -1
  27. package/organization/index.js +3 -0
  28. package/organization/index.js.map +1 -1
  29. package/organization/useOrgGate.d.ts +101 -0
  30. package/organization/useOrgGate.d.ts.map +1 -0
  31. package/organization/useOrgGate.js +99 -0
  32. package/organization/useOrgGate.js.map +1 -0
  33. package/package.json +5 -4
  34. package/runner/RunnerListPanel.d.ts +13 -8
  35. package/runner/RunnerListPanel.d.ts.map +1 -1
  36. package/runner/RunnerListPanel.js +10 -6
  37. package/runner/RunnerListPanel.js.map +1 -1
  38. package/settings/ApiKeysSection.d.ts +3 -0
  39. package/settings/ApiKeysSection.d.ts.map +1 -0
  40. package/settings/ApiKeysSection.js +30 -0
  41. package/settings/ApiKeysSection.js.map +1 -0
  42. package/settings/EnvironmentsSection.d.ts +3 -0
  43. package/settings/EnvironmentsSection.d.ts.map +1 -0
  44. package/settings/EnvironmentsSection.js +49 -0
  45. package/settings/EnvironmentsSection.js.map +1 -0
  46. package/settings/IdentityProvidersSection.d.ts +12 -0
  47. package/settings/IdentityProvidersSection.d.ts.map +1 -0
  48. package/settings/IdentityProvidersSection.js +34 -0
  49. package/settings/IdentityProvidersSection.js.map +1 -0
  50. package/settings/InvitationsSection.d.ts +3 -0
  51. package/settings/InvitationsSection.d.ts.map +1 -0
  52. package/settings/InvitationsSection.js +13 -0
  53. package/settings/InvitationsSection.js.map +1 -0
  54. package/settings/MembersSection.d.ts +3 -0
  55. package/settings/MembersSection.d.ts.map +1 -0
  56. package/settings/MembersSection.js +14 -0
  57. package/settings/MembersSection.js.map +1 -0
  58. package/settings/OAuthAppsSection.d.ts +3 -0
  59. package/settings/OAuthAppsSection.d.ts.map +1 -0
  60. package/settings/OAuthAppsSection.js +33 -0
  61. package/settings/OAuthAppsSection.js.map +1 -0
  62. package/settings/OrgProfileSection.d.ts +3 -0
  63. package/settings/OrgProfileSection.d.ts.map +1 -0
  64. package/settings/OrgProfileSection.js +15 -0
  65. package/settings/OrgProfileSection.js.map +1 -0
  66. package/settings/PlatformClientsSection.d.ts +3 -0
  67. package/settings/PlatformClientsSection.d.ts.map +1 -0
  68. package/settings/PlatformClientsSection.js +48 -0
  69. package/settings/PlatformClientsSection.js.map +1 -0
  70. package/settings/UsageSection.d.ts +3 -0
  71. package/settings/UsageSection.d.ts.map +1 -0
  72. package/settings/UsageSection.js +14 -0
  73. package/settings/UsageSection.js.map +1 -0
  74. package/settings/index.d.ts +13 -0
  75. package/settings/index.d.ts.map +1 -0
  76. package/settings/index.js +11 -0
  77. package/settings/index.js.map +1 -0
  78. package/settings/settings-nav.d.ts +25 -0
  79. package/settings/settings-nav.d.ts.map +1 -0
  80. package/settings/settings-nav.js +41 -0
  81. package/settings/settings-nav.js.map +1 -0
  82. package/src/index.ts +32 -1
  83. package/src/internal/menu.tsx +160 -0
  84. package/src/library/LibraryBreadcrumbContext.tsx +70 -0
  85. package/src/library/index.ts +6 -0
  86. package/src/organization/OrgProvider.tsx +184 -0
  87. package/src/organization/OrgSwitcher.tsx +275 -0
  88. package/src/organization/index.ts +10 -0
  89. package/src/organization/useOrgGate.ts +183 -0
  90. package/src/runner/RunnerListPanel.tsx +14 -9
  91. package/src/settings/ApiKeysSection.tsx +96 -0
  92. package/src/settings/EnvironmentsSection.tsx +162 -0
  93. package/src/settings/IdentityProvidersSection.tsx +123 -0
  94. package/src/settings/InvitationsSection.tsx +42 -0
  95. package/src/settings/MembersSection.tsx +41 -0
  96. package/src/settings/OAuthAppsSection.tsx +100 -0
  97. package/src/settings/OrgProfileSection.tsx +41 -0
  98. package/src/settings/PlatformClientsSection.tsx +149 -0
  99. package/src/settings/UsageSection.tsx +41 -0
  100. package/src/settings/index.ts +13 -0
  101. package/src/settings/settings-nav.ts +78 -0
  102. package/src/user/UserMenu.tsx +241 -0
  103. package/src/user/index.ts +2 -0
  104. package/styles.css +1 -1
  105. package/user/UserMenu.d.ts +82 -0
  106. package/user/UserMenu.d.ts.map +1 -0
  107. package/user/UserMenu.js +51 -0
  108. package/user/UserMenu.js.map +1 -0
  109. package/user/index.d.ts +3 -0
  110. package/user/index.d.ts.map +1 -0
  111. package/user/index.js +2 -0
  112. 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"`. Including them
45
- * gives admins full visibility into all compute resources.
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 true
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
- * Admin panel that displays all runners in an organization with
68
- * lifecycle management actions.
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" badge and have
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={false}
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 = true,
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
+ }