mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 15:03:16 +00:00
fix: Improve account selector (#626)
* fix: Improve account selector * cr * cr * cr
This commit is contained in:
parent
9906dd4e2a
commit
bedd2c9b68
4 changed files with 198 additions and 47 deletions
|
|
@ -16,6 +16,7 @@ import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
|
|||
import { useThreadsStatus } from "@/hooks/useThreadsStatus";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { threadsToMetadata } from "@/lib/thread-utils";
|
||||
import { UserPopover } from "@/components/user-popover";
|
||||
import { OpenSWELogoSVG } from "@/components/icons/openswe";
|
||||
|
||||
type FilterStatus =
|
||||
|
|
@ -122,8 +123,8 @@ function AllThreadsPageContent() {
|
|||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<InstallationSelector />
|
||||
<ThemeToggle />
|
||||
<UserPopover />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { TooltipIconButton } from "../ui/tooltip-icon-button";
|
||||
|
||||
const GITHUB_APP_INSTALLED_KEY = "github_app_installed";
|
||||
|
||||
export function GitHubLogoutButton() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
if (response.ok) {
|
||||
localStorage.removeItem(GITHUB_APP_INSTALLED_KEY);
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
console.error("Logout failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipIconButton
|
||||
tooltip="Logout"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
className="text-xs"
|
||||
size="sm"
|
||||
>
|
||||
<LogOut className="size-4" />
|
||||
</TooltipIconButton>
|
||||
);
|
||||
}
|
||||
194
apps/web/src/components/user-popover.tsx
Normal file
194
apps/web/src/components/user-popover.tsx
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { Building2, LogOut, User } from "lucide-react";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
interface UserPopoverProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function UserPopover({ className }: UserPopoverProps) {
|
||||
const {
|
||||
installations,
|
||||
currentInstallation,
|
||||
installationsLoading: isLoading,
|
||||
installationsError: error,
|
||||
switchInstallation,
|
||||
} = useGitHubAppProvider();
|
||||
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const GITHUB_APP_INSTALLED_KEY = "github_app_installed";
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
const response = await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
if (response.ok) {
|
||||
localStorage.removeItem(GITHUB_APP_INSTALLED_KEY);
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
console.error("Logout failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during logout:", error);
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValueChange = async (value: string) => {
|
||||
await switchInstallation(value);
|
||||
};
|
||||
|
||||
const getAccountIcon = (accountType: "User" | "Organization") => {
|
||||
return accountType === "Organization" ? (
|
||||
<Building2 className="h-4 w-4" />
|
||||
) : (
|
||||
<User className="h-4 w-4" />
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading || !currentInstallation) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled
|
||||
className={cn("h-8 w-8 rounded-full p-0", className)}
|
||||
>
|
||||
<div className="bg-muted h-6 w-6 animate-pulse rounded-full" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled
|
||||
className={cn("h-8 w-8 rounded-full p-0", className)}
|
||||
>
|
||||
<div className="bg-destructive/20 flex h-6 w-6 items-center justify-center rounded-full">
|
||||
<GitHubSVG className="text-destructive h-3 w-3" />
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={cn("hover:bg-accent h-8 w-8 rounded-full p-0", className)}
|
||||
>
|
||||
<img
|
||||
src={currentInstallation.avatarUrl}
|
||||
alt={`${currentInstallation.accountName} avatar`}
|
||||
className="h-6 w-6 rounded-full"
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-80 p-0"
|
||||
align="end"
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<img
|
||||
src={currentInstallation.avatarUrl}
|
||||
alt={`${currentInstallation.accountName} avatar`}
|
||||
className="h-10 w-10 rounded-full"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">
|
||||
{currentInstallation.accountName}
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-sm">
|
||||
{getAccountIcon(currentInstallation.accountType)}
|
||||
<span className="capitalize">
|
||||
{currentInstallation.accountType.toLowerCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{installations.length > 1 && (
|
||||
<>
|
||||
<div className="mb-4 space-y-2">
|
||||
<label className="text-sm font-medium">Switch Account</label>
|
||||
<Select
|
||||
value={currentInstallation.id.toString()}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<img
|
||||
src={currentInstallation.avatarUrl}
|
||||
alt={`${currentInstallation.accountName} avatar`}
|
||||
className="h-4 w-4 rounded-full"
|
||||
/>
|
||||
<span className="truncate">
|
||||
{currentInstallation.accountName}
|
||||
</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{installations.map((installation) => (
|
||||
<SelectItem
|
||||
key={installation.id}
|
||||
value={installation.id.toString()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={installation.avatarUrl}
|
||||
alt={`${installation.accountName} avatar`}
|
||||
className="h-4 w-4 rounded-full"
|
||||
/>
|
||||
<span>{installation.accountName}</span>
|
||||
{getAccountIcon(installation.accountType)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Separator className="mb-4" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-red-600 hover:bg-red-50 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-950/50 dark:hover:text-red-300"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
{isLoggingOut ? "Signing out..." : "Sign out"}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,10 +19,9 @@ import { GitHubInstallationBanner } from "../github/installation-banner";
|
|||
import { ApiKeyBanner } from "../api-key-banner";
|
||||
import { QuickActions } from "./quick-actions";
|
||||
import { DraftsSection } from "./drafts-section";
|
||||
import { GitHubLogoutButton } from "../github/github-oauth-button";
|
||||
import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
|
||||
import { TooltipIconButton } from "../ui/tooltip-icon-button";
|
||||
import { InstallationSelector } from "../github/installation-selector";
|
||||
import { UserPopover } from "../user-popover";
|
||||
|
||||
import { useThreadsStatus } from "@/hooks/useThreadsStatus";
|
||||
import { Thread } from "@langchain/langgraph-sdk";
|
||||
|
|
@ -126,7 +125,6 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
|||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<InstallationSelector />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">ready</span>
|
||||
<div className="h-1 w-1 rounded-full bg-green-500 dark:bg-green-600"></div>
|
||||
|
|
@ -134,7 +132,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
|||
<OpenDocumentationButton />
|
||||
<OpenSettingsButton />
|
||||
<ThemeToggle />
|
||||
<GitHubLogoutButton />
|
||||
<UserPopover />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue