open-swe/ui/src/routes/integrations.tsx

124 lines
3.8 KiB
TypeScript
Raw Normal View History

feat: refactor UI into sidebar layout (#1318) * feat(dashboard): refactor UI into Cursor-style sidebar layout Replace the top-bar AppHeader with a left sidebar that holds the four primary sections (My Settings, Cloud Agents, Open SWE Review, Integrations) and the user account menu at the bottom. Admin remains a hidden route reachable from the sidebar only when is_admin. UI: - AppShell + AppSidebar with bottom-anchored avatar/sign-out menu. - My Settings: profile (first/last name) + PR destination preference. - Cloud Agents: model/effort, default repo, base branch, branch prefix, PR toggles (auto-fix CI, create PRs, allow artifacts), Slack notifications. - Open SWE Review: trigger mode, draft reviews, PR summaries, autofix mode + severity threshold, plus the existing per-repo review-style prompts (was /review-styles). - Integrations: GitHub install status + Slack/Linear status rows. - New Switch and Menu primitives. Backend: - Extend ProfileUpdate with first_name, last_name, base_branch, branch_prefix, auto_fix_ci, create_prs, allow_artifacts, slack_notifications, preferred_pr_destination. - Add team_settings module + /team-settings GET (any session) / PUT (admin only) for the reviewer configuration. * fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects - base-ui's Menu hit an "Invalid hook call / Cannot read properties of null (reading 'useRef')" crash inside fastComponent under Vite's dep optimisation. The sidebar account menu is the only consumer, so swap it for a useState-driven dropdown with click-outside + Esc handling and drop the unused Menu wrapper. - Initialise the Open SWE Review settings form with the same defaults the server returns instead of `null`, so the Selects don't switch from uncontrolled (`undefined`) to controlled on first data load. * fix(dashboard): address review feedback on admin overwrite + form reset - admin PUT /admin/profiles/{login} only sent model/effort/repo from the admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci / create_prs / slack_notifications etc back onto the target user's profile, clobbering whatever they had configured. Switch to model_dump(exclude_unset=True) and overlay the incoming fields on the user's existing stored profile so only sent fields change. - my-settings.tsx and cloud-agents.tsx initialised local state from profile.data on every change. Because useSaveProfile updates the cached profile on success, toggling any control would overwrite the user's typed-but-unsaved input in another field. Guard the initialisation effect with a ref so it runs once on first load. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
import { Navigate, createFileRoute } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import { GithubLogoIcon, KanbanIcon, SlackLogoIcon } from "@phosphor-icons/react";
import type { ComponentType } from "react";
import type { ReposPayload } from "@/lib/api";
import { AppShell, SettingsSection } from "@/components/AppShell";
import { Skeleton } from "@/components/ui/skeleton";
import { ApiError, api } from "@/lib/api";
import { useSession } from "@/lib/session";
import { cn } from "@/lib/utils";
export const Route = createFileRoute("/integrations")({ component: IntegrationsPage });
type IconType = ComponentType<{ className?: string; weight?: "regular" | "fill" | "duotone" }>;
interface IntegrationRowProps {
icon: IconType;
name: string;
description: string;
connected: boolean;
badge?: string;
}
function IntegrationRow({ icon: Icon, name, description, connected, badge }: IntegrationRowProps) {
return (
<div className="flex items-center justify-between gap-4 border-b border-border px-4 py-3 last:border-b-0">
<div className="flex items-center gap-3">
<div className="flex size-8 items-center justify-center rounded-md bg-muted">
<Icon className="size-4" />
</div>
<div>
<div className="flex items-center gap-2 text-xs font-medium">
{name}
{badge && (
<span className="rounded-sm bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground">
{badge}
</span>
)}
</div>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
<span
className={cn(
"rounded-full px-2 py-0.5 text-[10px] font-medium",
connected
? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground",
)}
>
{connected ? "Connected" : "Not connected"}
</span>
</div>
);
}
function IntegrationsPage() {
const session = useSession();
const repos = useQuery<ReposPayload>({
queryKey: ["repos"],
queryFn: async () => {
try {
return await api.repos();
} catch (e) {
if (e instanceof ApiError && e.status === 401)
return { installations: [], repositories: [] };
throw e;
}
},
enabled: !!session.data,
});
if (session.isLoading) {
return (
<main className="p-6">
<Skeleton className="h-64 w-full" />
</main>
);
}
if (!session.data) return <Navigate to="/login" />;
const installs = repos.data?.installations ?? [];
const githubDescription = installs.length
? `Connected to ${installs.length} ${installs.length === 1 ? "installation" : "installations"}: ${installs
.map((i) => i.account ?? "?")
.join(", ")}`
: "Install the open-swe GitHub App to enable PR and issue triggers.";
return (
<AppShell
user={session.data}
title="Integrations"
description="Connect external tools that can trigger or receive updates from open-swe."
>
<SettingsSection title="Source Control">
<IntegrationRow
icon={GithubLogoIcon}
name="GitHub"
description={githubDescription}
connected={installs.length > 0}
/>
</SettingsSection>
<SettingsSection title="Communication">
<IntegrationRow
icon={SlackLogoIcon}
name="Slack"
description="Trigger Open SWE from Slack and receive run notifications."
badge="Team-managed"
connected
/>
<IntegrationRow
icon={KanbanIcon}
name="Linear"
description="Delegate Linear issues to Open SWE via mentions or commands."
badge="Team-managed"
connected
/>
</SettingsSection>
</AppShell>
);
}