feat(frontend): add FTUE welcome wizard for first-time users#1473
feat(frontend): add FTUE welcome wizard for first-time users#1473mergify[bot] merged 3 commits intomainfrom
Conversation
Adds a multi-step onboarding wizard that guides new users through workspace creation, integration setup, and starting their first session. - 4-step wizard: Welcome → Create Workspace → Connect Integrations → Completion - Dynamic integration registry (INTEGRATION_REGISTRY) with compile-time completeness guard -- new integrations only need one registry entry - Extracted WorkspaceForm with callback interface (shared between wizard and CreateWorkspaceDialog, no behavior change for existing users) - useShouldShowOnboarding hook: triggers on zero workspaces + localStorage flag - Responsive layout: 1-col (mobile) → 2-col (tablet) → 3-col (desktop) - Server config via meta tags (github-app-slug, github-callback-url) - 19 new tests (hook, registry, wizard transitions) - Documentation: COMPONENT_PATTERNS.md and DEVELOPMENT.md updated with extension guide for humans and agents Made-with: Cursor
✅ Deploy Preview for cheerful-kitten-f556a0 canceled.
|
📝 WalkthroughWalkthroughThis PR implements a comprehensive onboarding wizard system for new workspaces, introducing a guided 4-step setup flow (welcome → create workspace → integrate services → completion) with documentation patterns, a registry-driven integration card system enforcing compile-time completeness, and reusable WorkspaceForm component. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ProjectsPage
participant WelcomeWizard
participant CreateWorkspaceStep
participant IntegrationsStep
participant CompletionStep
participant API as Backend API
participant Storage as SessionStorage
User->>ProjectsPage: Navigate to /projects
ProjectsPage->>ProjectsPage: useShouldShowOnboarding()<br/>(check project count)
ProjectsPage->>WelcomeWizard: open=true (if no projects)
User->>WelcomeWizard: Click "Get Started"
WelcomeWizard->>WelcomeWizard: stepIndex: 0→1
User->>CreateWorkspaceStep: Wizard renders step
CreateWorkspaceStep->>WorkspaceForm: Display form
User->>WorkspaceForm: Enter name/displayName
User->>WorkspaceForm: Click "Create & Continue"
WorkspaceForm->>API: POST /projects (CreateProjectRequest)
API-->>WorkspaceForm: Created workspace
WorkspaceForm->>WelcomeWizard: onNext(workspaceName)
WelcomeWizard->>WelcomeWizard: stepIndex: 1→2
User->>IntegrationsStep: Wizard renders step
IntegrationsStep->>API: Fetch IntegrationsStatus
API-->>IntegrationsStep: Connection status
IntegrationsStep->>IntegrationsStep: Render IntegrationRegistry cards
User->>IntegrationsStep: Click GitHub App install
IntegrationsStep->>Storage: Persist wizard state (stepIndex, workspaceName)
Storage-->>IntegrationsStep: Saved to sessionStorage
IntegrationsStep->>User: Redirect to GitHub OAuth
User->>IntegrationsStep: Complete OAuth, redirect back
IntegrationsStep->>Storage: Restore wizard state from sessionStorage
IntegrationsStep->>WelcomeWizard: Resume at step 2
User->>IntegrationsStep: Click "Next" (or manual navigation)
WelcomeWizard->>WelcomeWizard: stepIndex: 2→3
User->>CompletionStep: Wizard renders completion
User->>CompletionStep: Click "Start a session"
CompletionStep->>WelcomeWizard: onNext()
WelcomeWizard->>WelcomeWizard: stepIndex: 3 (last) → onDismiss()
WelcomeWizard->>ProjectsPage: onDismiss()
ProjectsPage->>ProjectsPage: setWizardOpen(false)
ProjectsPage->>ProjectsPage: dismissOnboarding()
ProjectsPage->>User: Show projects list
🚥 Pre-merge checks | ✅ 7 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (7 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| <head> | ||
| <meta name="backend-ws-base" content={wsBase} /> | ||
| <meta name="github-app-slug" content={env.GITHUB_APP_SLUG ?? ''} /> | ||
| <meta name="github-callback-url" content={process.env.GITHUB_CALLBACK_URL ?? ''} /> |
There was a problem hiding this comment.
this being made more global may have throw a problem for me that I wanted to share:
Screen.Recording.2026-04-27.at.4.50.04.PM.mov
There was a problem hiding this comment.
sessionStorage resume after GitHub OAuth redirect (code path exists but untested end-to-end)
^ we may have infra callback URIS or secrets that need to rotate
- Add displayName to test wrapper components (react/display-name) - Remove unused ONBOARDING_FLAG import from wizard test - Remove unused wizardState destructure in CreateWorkspaceStep Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts (1)
43-89: Add a rejected-query test to lock the intended error behavior.Current tests only assert resolved responses. Please add one case where
listProjectsPaginatedrejects and assertshouldShow === falseafter loading settles, so this onboarding gate doesn’t regress.As per coding guidelines,
components/frontend/src/**/*.{ts,tsx}: - Verify loading/error states and error handling in React Query hooks.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts` around lines 43 - 89, Add a new test in use-should-show-onboarding.test.ts that simulates listProjectsPaginated rejecting: set mockListPaginated.mockRejectedValue(new Error('fail')), ensure ONBOARDING_FLAG is not set in localStorage, render the hook via createWrapper and wait for result.current.isLoading to become false, then assert result.current.shouldShow === false; place this alongside the other cases so the test verifies useShouldShowOnboarding handles rejected queries by hiding the wizard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/frontend/src/components/onboarding/steps/integrations-step.tsx`:
- Around line 17-18: The integrations query failure path isn't handled: when
useIntegrationsStatus() returns integrations === undefined and isLoading ===
false the component renders empty content; update the IntegrationsStep (or the
component using useIntegrationsStatus) to detect the error case (integrations
=== undefined && !isLoading), render an error state/UI with a clear message, and
wire a retry action that calls refetch from useIntegrationsStatus(); keep
existing loading handling (isLoading) and normal success rendering for
integrations present.
In `@components/frontend/src/components/onboarding/use-should-show-onboarding.ts`:
- Around line 24-33: The hook treats a failed projects query as no projects and
reads localStorage unsafely; update logic to require a successful query before
deciding there are no projects by using the query success flag from
useProjectsPaginated (e.g., check isSuccess alongside isLoading when computing
shouldShow) so errors don't open onboarding, and wrap the ONBOARDING_FLAG read
in the useState initializer in a try/catch with feature checks for window and
localStorage access (fall back to false on any exception) to avoid throws in
restricted browser modes.
In `@components/frontend/src/components/onboarding/welcome-wizard.tsx`:
- Around line 58-67: The wizard never clears the persisted SESSION_KEY on normal
dismiss/completion, causing stale state to resume; update the close/completion
paths (the else branch in handleNext and any explicit dismiss/close handlers
referenced around lines 76-103) to call the same persistence-clearing logic used
in integrations-step.tsx (removeItem(SESSION_KEY) or its helper) before calling
onDismiss() or setStepIndex, and ensure any other exit paths (cancel/close
buttons) also remove SESSION_KEY so the wizard state is always cleared on every
close.
- Around line 42-56: The resume-from-sessionStorage code should validate and
clamp the parsed payload before restoring: when reading SESSION_KEY and
JSON.parse result, ensure parsed.step is a finite integer within
0..(STEPS.length-1) before calling setStepIndex, and only assign
createdWorkspaceName to setWizardState after validating it's a string (or null);
always call sessionStorage.removeItem(SESSION_KEY) in a finally block so
corrupted/invalid JSON doesn't leave stale data, and wrap the parse/validate
logic to handle non-number, out-of-range, or NaN step values safely (use
Number.isInteger and bounds checks) rather than trusting parsed.step directly.
In `@components/frontend/src/components/workspace-form.tsx`:
- Around line 22-29: The generated workspace name can end with a hyphen after
truncation and the OpenShift branch never sets the visible validation state,
causing submit to be silently disabled; update generateWorkspaceName to apply
the truncation then remove leading/trailing hyphens (i.e., run the /^\-+|\-+$/g
trim step after slice) so no trailing hyphen remains, and ensure the OpenShift
path invokes the same name validation flow that sets nameError (or explicitly
sets nameError based on validateWorkspaceName/generateWorkspaceName) so the UI
shows the error and submit enabling logic (which checks nameError) behaves
consistently.
- Around line 50-51: The code derives platform mode from
useClusterInfo.isOpenShift and allows submitting while cluster detection is
unresolved, risking wrong payload; update the component to wait for
clusterLoading to be false (and handle a cluster error state) before deriving
mode from isOpenShift, disable the submit action (and the submit button) when
clusterLoading is true or when useClusterInfo reports an error, and ensure the
submit handler consults this gated state (e.g., check clusterLoading ||
clusterError and return/throw) so setFormData/formData cannot produce the wrong
payload shape until platform type is known.
---
Nitpick comments:
In
`@components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts`:
- Around line 43-89: Add a new test in use-should-show-onboarding.test.ts that
simulates listProjectsPaginated rejecting: set
mockListPaginated.mockRejectedValue(new Error('fail')), ensure ONBOARDING_FLAG
is not set in localStorage, render the hook via createWrapper and wait for
result.current.isLoading to become false, then assert result.current.shouldShow
=== false; place this alongside the other cases so the test verifies
useShouldShowOnboarding handles rejected queries by hiding the wizard.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: fd4597ed-d29a-492f-831c-9a3f06316545
📒 Files selected for processing (17)
components/frontend/COMPONENT_PATTERNS.mdcomponents/frontend/DEVELOPMENT.mdcomponents/frontend/src/app/layout.tsxcomponents/frontend/src/app/projects/page.tsxcomponents/frontend/src/components/create-workspace-dialog.tsxcomponents/frontend/src/components/onboarding/__tests__/integration-registry.test.tscomponents/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.tscomponents/frontend/src/components/onboarding/__tests__/welcome-wizard.test.tsxcomponents/frontend/src/components/onboarding/integration-registry.tscomponents/frontend/src/components/onboarding/steps/completion-step.tsxcomponents/frontend/src/components/onboarding/steps/create-workspace-step.tsxcomponents/frontend/src/components/onboarding/steps/integrations-step.tsxcomponents/frontend/src/components/onboarding/steps/welcome-step.tsxcomponents/frontend/src/components/onboarding/use-app-config.tscomponents/frontend/src/components/onboarding/use-should-show-onboarding.tscomponents/frontend/src/components/onboarding/welcome-wizard.tsxcomponents/frontend/src/components/workspace-form.tsx
| const { data: integrations, isLoading, refetch } = useIntegrationsStatus(); | ||
| const appConfig = useAppConfig(); |
There was a problem hiding this comment.
Handle the integrations query failure path.
If useIntegrationsStatus() fails, isLoading is false and integrations is undefined, so the main body falls through to null and the step renders as a blank panel with only the footer buttons. Surface an error state with a retry action instead of silently hiding the content.
Suggested fix
- const { data: integrations, isLoading, refetch } = useIntegrationsStatus();
+ const {
+ data: integrations,
+ isLoading,
+ isError,
+ refetch,
+ } = useIntegrationsStatus();
...
- {isLoading ? (
+ {isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
+ ) : isError ? (
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-4">
+ <p className="text-sm text-destructive">
+ Failed to load integrations. Please try again.
+ </p>
+ <Button
+ variant="outline"
+ className="mt-3"
+ onClick={() => {
+ persistStateBeforeRedirect();
+ refetch();
+ }}
+ >
+ Retry
+ </Button>
+ </div>
) : integrations ? (As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: Verify loading/error states and error handling in React Query hooks.
Also applies to: 63-87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/onboarding/steps/integrations-step.tsx`
around lines 17 - 18, The integrations query failure path isn't handled: when
useIntegrationsStatus() returns integrations === undefined and isLoading ===
false the component renders empty content; update the IntegrationsStep (or the
component using useIntegrationsStatus) to detect the error case (integrations
=== undefined && !isLoading), render an error state/UI with a clear message, and
wire a retry action that calls refetch from useIntegrationsStatus(); keep
existing loading handling (isLoading) and normal success rendering for
integrations present.
| const { data, isLoading } = useProjectsPaginated({ limit: 1 }); | ||
|
|
||
| const [dismissed, setDismissed] = useState(() => { | ||
| if (typeof window === "undefined") return false; | ||
| return localStorage.getItem(ONBOARDING_FLAG) === "true"; | ||
| }); | ||
|
|
||
| const hasProjects = (data?.totalCount ?? 0) > 0; | ||
| const shouldShow = !isLoading && !hasProjects && !dismissed; | ||
|
|
There was a problem hiding this comment.
Gate onboarding on successful query and harden localStorage reads.
On Line 32, a failed projects query is currently treated as “zero projects,” which can incorrectly open onboarding for existing users. Also, Line 28 can throw when storage access is blocked (privacy/restricted browser modes), causing hook initialization to fail.
Proposed fix
- const { data, isLoading } = useProjectsPaginated({ limit: 1 });
+ const { data, isLoading, isError } = useProjectsPaginated({ limit: 1 });
const [dismissed, setDismissed] = useState(() => {
if (typeof window === "undefined") return false;
- return localStorage.getItem(ONBOARDING_FLAG) === "true";
+ try {
+ return localStorage.getItem(ONBOARDING_FLAG) === "true";
+ } catch {
+ return false;
+ }
});
const hasProjects = (data?.totalCount ?? 0) > 0;
- const shouldShow = !isLoading && !hasProjects && !dismissed;
+ const shouldShow = !isLoading && !isError && !hasProjects && !dismissed;As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: - Verify loading/error states and error handling in React Query hooks.
Also applies to: 26-29
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/onboarding/use-should-show-onboarding.ts`
around lines 24 - 33, The hook treats a failed projects query as no projects and
reads localStorage unsafely; update logic to require a successful query before
deciding there are no projects by using the query success flag from
useProjectsPaginated (e.g., check isSuccess alongside isLoading when computing
shouldShow) so errors don't open onboarding, and wrap the ONBOARDING_FLAG read
in the useState initializer in a try/catch with feature checks for window and
localStorage access (fall back to false on any exception) to avoid throws in
restricted browser modes.
| // Resume from sessionStorage after OAuth redirect (GitHub App install) | ||
| useEffect(() => { | ||
| try { | ||
| const saved = sessionStorage.getItem(SESSION_KEY); | ||
| if (saved) { | ||
| const parsed = JSON.parse(saved) as { step?: number; createdWorkspaceName?: string }; | ||
| if (parsed.step !== undefined) setStepIndex(parsed.step); | ||
| if (parsed.createdWorkspaceName) | ||
| setWizardState((s) => ({ ...s, createdWorkspaceName: parsed.createdWorkspaceName ?? null })); | ||
| sessionStorage.removeItem(SESSION_KEY); | ||
| } | ||
| } catch { | ||
| // sessionStorage may be unavailable or contain invalid JSON | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Harden the resume payload before restoring it.
Line 48 trusts parsed.step directly. A stale/corrupted sessionStorage value outside 0..STEPS.length - 1 makes STEPS[stepIndex] undefined on the next render, and invalid JSON is never removed because cleanup only happens on the happy path. Clamp the step and clear the key in finally.
Suggested fix
useEffect(() => {
try {
const saved = sessionStorage.getItem(SESSION_KEY);
- if (saved) {
- const parsed = JSON.parse(saved) as { step?: number; createdWorkspaceName?: string };
- if (parsed.step !== undefined) setStepIndex(parsed.step);
- if (parsed.createdWorkspaceName)
- setWizardState((s) => ({ ...s, createdWorkspaceName: parsed.createdWorkspaceName ?? null }));
- sessionStorage.removeItem(SESSION_KEY);
- }
+ if (!saved) return;
+
+ const parsed = JSON.parse(saved) as {
+ step?: number;
+ createdWorkspaceName?: string;
+ };
+
+ if (
+ typeof parsed.step === "number" &&
+ parsed.step >= 0 &&
+ parsed.step < STEPS.length
+ ) {
+ setStepIndex(parsed.step);
+ }
+
+ if (typeof parsed.createdWorkspaceName === "string" && parsed.createdWorkspaceName) {
+ setWizardState((s) => ({
+ ...s,
+ createdWorkspaceName: parsed.createdWorkspaceName,
+ }));
+ }
} catch {
// sessionStorage may be unavailable or contain invalid JSON
+ } finally {
+ try {
+ sessionStorage.removeItem(SESSION_KEY);
+ } catch {
+ // ignore storage failures
+ }
}
}, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/onboarding/welcome-wizard.tsx` around
lines 42 - 56, The resume-from-sessionStorage code should validate and clamp the
parsed payload before restoring: when reading SESSION_KEY and JSON.parse result,
ensure parsed.step is a finite integer within 0..(STEPS.length-1) before calling
setStepIndex, and only assign createdWorkspaceName to setWizardState after
validating it's a string (or null); always call
sessionStorage.removeItem(SESSION_KEY) in a finally block so corrupted/invalid
JSON doesn't leave stale data, and wrap the parse/validate logic to handle
non-number, out-of-range, or NaN step values safely (use Number.isInteger and
bounds checks) rather than trusting parsed.step directly.
| const handleNext = useCallback( | ||
| (update?: Partial<WizardState>) => { | ||
| if (update) setWizardState((prev) => ({ ...prev, ...update })); | ||
| if (stepIndex < STEPS.length - 1) { | ||
| setStepIndex((i) => i + 1); | ||
| } else { | ||
| onDismiss(); | ||
| } | ||
| }, | ||
| [stepIndex, onDismiss] |
There was a problem hiding this comment.
Clear persisted wizard state on every close path.
components/frontend/src/components/onboarding/steps/integrations-step.tsx writes SESSION_KEY before refresh, but this component never clears it on normal dismiss/completion. If the user refreshes integration status and then finishes or skips the wizard without a remount, the next open/reload can resume with stale step/workspace data.
Suggested fix
export function WelcomeWizard({ open, onDismiss }: WelcomeWizardProps) {
@@
+ const dismissWizard = useCallback(() => {
+ try {
+ sessionStorage.removeItem(SESSION_KEY);
+ } catch {
+ // sessionStorage may be unavailable
+ }
+ onDismiss();
+ }, [onDismiss]);
+
const handleNext = useCallback(
(update?: Partial<WizardState>) => {
if (update) setWizardState((prev) => ({ ...prev, ...update }));
if (stepIndex < STEPS.length - 1) {
setStepIndex((i) => i + 1);
} else {
- onDismiss();
+ dismissWizard();
}
},
- [stepIndex, onDismiss]
+ [stepIndex, dismissWizard]
);
@@
- <Dialog open={open} onOpenChange={(o) => { if (!o) onDismiss(); }}>
+ <Dialog open={open} onOpenChange={(o) => { if (!o) dismissWizard(); }}>
@@
<StepComponent
onNext={handleNext}
- onSkip={onDismiss}
+ onSkip={dismissWizard}
wizardState={wizardState}
/>
@@
<button
type="button"
- onClick={onDismiss}
+ onClick={dismissWizard}
className="text-xs text-muted-foreground hover:text-foreground transition-colors self-center mt-2"
>Also applies to: 76-103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/onboarding/welcome-wizard.tsx` around
lines 58 - 67, The wizard never clears the persisted SESSION_KEY on normal
dismiss/completion, causing stale state to resume; update the close/completion
paths (the else branch in handleNext and any explicit dismiss/close handlers
referenced around lines 76-103) to call the same persistence-clearing logic used
in integrations-step.tsx (removeItem(SESSION_KEY) or its helper) before calling
onDismiss() or setStepIndex, and ensure any other exit paths (cancel/close
buttons) also remove SESSION_KEY so the wizard state is always cleared on every
close.
| function generateWorkspaceName(displayName: string): string { | ||
| return displayName | ||
| .toLowerCase() | ||
| .replace(/\s+/g, "-") | ||
| .replace(/[^a-z0-9-]/g, "") | ||
| .replace(/-+/g, "-") | ||
| .replace(/^-+|-+$/g, "") | ||
| .slice(0, 63); |
There was a problem hiding this comment.
OpenShift path can silently disable submit without an actionable error
Line 192 disables submit on nameError, but in the OpenShift branch (Line 120-132) that error is never shown. Also, Line 22-29 can produce a post-truncation trailing hyphen, triggering this hidden invalid state.
Proposed fix
function generateWorkspaceName(displayName: string): string {
- return displayName
+ return displayName
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "")
- .slice(0, 63);
+ .slice(0, 63)
+ .replace(/^-+|-+$/g, "");
}
...
{isOpenShift && (
<div className="space-y-2">
<Label htmlFor="displayName">Workspace Name *</Label>
<Input
id="displayName"
data-testid="workspace-display-name-input"
value={formData.displayName}
onChange={(e) => handleDisplayNameChange(e.target.value)}
placeholder="e.g. My Research Workspace"
maxLength={100}
/>
+ {nameError && (
+ <p className="text-sm text-red-600 dark:text-red-400">{nameError}</p>
+ )}
</div>
)}As per coding guidelines, **/*: "Flag bugs, security vulnerabilities, logic errors, data loss risks, and meaningful refactoring opportunities."
Also applies to: 57-71, 120-132, 192-192
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/workspace-form.tsx` around lines 22 - 29,
The generated workspace name can end with a hyphen after truncation and the
OpenShift branch never sets the visible validation state, causing submit to be
silently disabled; update generateWorkspaceName to apply the truncation then
remove leading/trailing hyphens (i.e., run the /^\-+|\-+$/g trim step after
slice) so no trailing hyphen remains, and ensure the OpenShift path invokes the
same name validation flow that sets nameError (or explicitly sets nameError
based on validateWorkspaceName/generateWorkspaceName) so the UI shows the error
and submit enabling logic (which checks nameError) behaves consistently.
| const { isOpenShift, isLoading: clusterLoading } = useClusterInfo(); | ||
| const [formData, setFormData] = useState<CreateProjectRequest>({ name: "", displayName: "", description: "" }); |
There was a problem hiding this comment.
Gate form mode and submit on cluster-info resolution
Line 50/Line 120-159 currently derive mode from isOpenShift even while cluster detection is unresolved (or errored), and Line 192 still allows submit. This can send the wrong payload shape before the platform type is known.
Proposed fix
- const { isOpenShift, isLoading: clusterLoading } = useClusterInfo();
+ const { isOpenShift, isLoading: clusterLoading, isError: clusterError } = useClusterInfo();
+ if (clusterLoading) {
+ return (
+ <Alert>
+ <Loader2 className="h-4 w-4 animate-spin" />
+ <AlertDescription>Detecting cluster capabilities…</AlertDescription>
+ </Alert>
+ );
+ }
+
+ if (clusterError) {
+ return (
+ <Alert variant="destructive">
+ <AlertDescription>
+ Could not determine cluster type. Please retry before creating a workspace.
+ </AlertDescription>
+ </Alert>
+ );
+ }
...
- <Button data-testid="create-workspace-submit" type="submit" disabled={isSubmitting || !!nameError}>
+ <Button
+ data-testid="create-workspace-submit"
+ type="submit"
+ disabled={isSubmitting || clusterLoading || clusterError || !!nameError}
+ >As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: "Verify loading/error states and error handling in React Query hooks."
Also applies to: 120-159, 192-192
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/frontend/src/components/workspace-form.tsx` around lines 50 - 51,
The code derives platform mode from useClusterInfo.isOpenShift and allows
submitting while cluster detection is unresolved, risking wrong payload; update
the component to wait for clusterLoading to be false (and handle a cluster error
state) before deriving mode from isOpenShift, disable the submit action (and the
submit button) when clusterLoading is true or when useClusterInfo reports an
error, and ensure the submit handler consults this gated state (e.g., check
clusterLoading || clusterError and return/throw) so setFormData/formData cannot
produce the wrong payload shape until platform type is known.
Merge Queue Status
This pull request spent 18 seconds in the queue, including 4 seconds running CI. Required conditions to merge |
Summary
INTEGRATION_REGISTRYwith compile-time completeness guard so new integrations are automatically surfaced in the wizardWorkspaceFormwith callback interface, shared betweenCreateWorkspaceDialogand the wizard (no behavior change for existing users)Step 1 - new users with no workspaces get a intro
Step 2 - create workspace
Step 3 - Connect integrations
What's new
components/onboarding/welcome-wizard.tsxcomponents/onboarding/integration-registry.tscomponents/onboarding/use-should-show-onboarding.tscomponents/onboarding/use-app-config.tscomponents/onboarding/steps/*.tsxcomponents/workspace-form.tsxWhat changed
app/layout.tsx: Addedgithub-app-slugandgithub-callback-urlmeta tags (2 lines)create-workspace-dialog.tsx: Refactored to use sharedWorkspaceForm(285 → 79 lines)app/projects/page.tsx: Wires wizard + onboarding hookCOMPONENT_PATTERNS.md/DEVELOPMENT.md: Extension guide for humans and agentsTesting performed
isConnectedderivations, wizard step transitions)tsc --noEmitcleanTesting NOT performed (reviewer note)
sessionStorageresume after GitHub OAuth redirect (code path exists but untested end-to-end)How to add a new integration
IntegrationsStatusinservices/api/integrations.tsIntegrationEntrytoINTEGRATION_REGISTRYinintegration-registry.tsThe compile-time guard errors if a status key is missing from the registry.
Made with Cursor / Spec that drove these code change: https://gist.github.com/bobbravo2/1f5c26092780d8a0dbdb63f557604921
Summary by CodeRabbit
Release Notes
New Features
Documentation