diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts index 05b7a4685bd..01f74ab0350 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/status.ts @@ -1,4 +1,4 @@ -import { projects, workspaces } from "@superset/local-db"; +import { workspaces } from "@superset/local-db"; import { and, eq, isNull } from "drizzle-orm"; import { localDb } from "main/lib/local-db"; import { z } from "zod"; @@ -8,7 +8,6 @@ import { setLastActiveWorkspace, touchWorkspace, } from "../utils/db-helpers"; -import { getOriginRemoteUrl, parseGitRemoteUrl } from "../utils/git"; export const createStatusProcedures = () => { return router({ @@ -101,82 +100,6 @@ export const createStatusProcedures = () => { return { success: true, isUnread: input.isUnread }; }), - linkToCloud: publicProcedure - .input(z.object({ id: z.string(), cloudWorkspaceId: z.string().uuid() })) - .mutation(({ input }) => { - const workspace = getWorkspaceNotDeleting(input.id); - if (!workspace) { - throw new Error( - `Workspace ${input.id} not found or is being deleted`, - ); - } - - localDb - .update(workspaces) - .set({ cloudWorkspaceId: input.cloudWorkspaceId }) - .where(eq(workspaces.id, input.id)) - .run(); - - return { success: true, cloudWorkspaceId: input.cloudWorkspaceId }; - }), - - unlinkFromCloud: publicProcedure - .input(z.object({ id: z.string() })) - .mutation(({ input }) => { - const workspace = getWorkspaceNotDeleting(input.id); - if (!workspace) { - throw new Error( - `Workspace ${input.id} not found or is being deleted`, - ); - } - - localDb - .update(workspaces) - .set({ cloudWorkspaceId: null }) - .where(eq(workspaces.id, input.id)) - .run(); - - return { success: true }; - }), - - getRepoInfo: publicProcedure - .input(z.object({ id: z.string() })) - .query(async ({ input }) => { - const workspace = getWorkspaceNotDeleting(input.id); - if (!workspace) { - throw new Error( - `Workspace ${input.id} not found or is being deleted`, - ); - } - - const project = localDb - .select() - .from(projects) - .where(eq(projects.id, workspace.projectId)) - .get(); - - if (!project) { - throw new Error(`Project not found for workspace ${input.id}`); - } - - const remoteUrl = await getOriginRemoteUrl(project.mainRepoPath); - if (!remoteUrl) { - return { hasRemote: false as const }; - } - - const parsed = parseGitRemoteUrl(remoteUrl); - if (!parsed) { - return { hasRemote: false as const }; - } - - return { - hasRemote: true as const, - repoOwner: parsed.owner, - repoName: parsed.repo, - repoUrl: parsed.repoUrl, - }; - }), - setActive: publicProcedure .input(z.object({ workspaceId: z.string() })) .mutation(({ input }) => { diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts index cbac037b402..e871e4bdaa6 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -698,87 +698,6 @@ export async function hasOriginRemote(mainRepoPath: string): Promise { } } -/** - * Gets the origin remote URL for a repository. - * @param repoPath - Path to the repository - * @returns The origin remote URL, or null if not found - */ -export async function getOriginRemoteUrl( - repoPath: string, -): Promise { - try { - const git = simpleGit(repoPath); - const remotes = await git.getRemotes(true); - const origin = remotes.find((r) => r.name === "origin"); - return origin?.refs?.fetch ?? origin?.refs?.push ?? null; - } catch { - return null; - } -} - -/** - * Parses a git remote URL to extract owner and repo name. - * Supports formats: - * - https://github.com/owner/repo.git - * - https://github.com/owner/repo - * - git@github.com:owner/repo.git - * - git@github.com:owner/repo - * - ssh://git@github.com/owner/repo.git - */ -export function parseGitRemoteUrl(url: string): { - owner: string; - repo: string; - repoUrl: string; -} | null { - // Normalize the URL - let normalized = url.trim(); - - // Remove .git suffix if present - if (normalized.endsWith(".git")) { - normalized = normalized.slice(0, -4); - } - - // Handle SSH format: git@github.com:owner/repo - const sshMatch = normalized.match(/^git@([^:]+):(.+)\/(.+)$/); - if (sshMatch) { - const [, host, owner, repo] = sshMatch; - return { - owner, - repo, - repoUrl: `https://${host}/${owner}/${repo}`, - }; - } - - // Handle SSH URL format: ssh://git@github.com/owner/repo - const sshUrlMatch = normalized.match(/^ssh:\/\/git@([^/]+)\/(.+)\/(.+)$/); - if (sshUrlMatch) { - const [, host, owner, repo] = sshUrlMatch; - return { - owner, - repo, - repoUrl: `https://${host}/${owner}/${repo}`, - }; - } - - // Handle HTTPS format: https://github.com/owner/repo - try { - const urlObj = new URL(normalized); - const pathParts = urlObj.pathname.split("/").filter(Boolean); - if (pathParts.length >= 2) { - const [owner, repo] = pathParts; - return { - owner, - repo, - repoUrl: `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}`, - }; - } - } catch { - // Not a valid URL - } - - return null; -} - export async function getDefaultBranch(mainRepoPath: string): Promise { const git = simpleGit(mainRepoPath); diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx index 447d778feb7..5d9425166d8 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx @@ -40,7 +40,6 @@ import { useNewWorkspaceModalOpen, usePreSelectedProjectId, } from "renderer/stores/new-workspace-modal"; -import { ENABLE_CLOUD_WORKSPACES } from "shared/constants"; import { sanitizeBranchName, sanitizeSegment } from "shared/utils/branch"; import { ExistingWorktreesList } from "./components/ExistingWorktreesList"; @@ -300,19 +299,17 @@ export function NewWorkspaceModal() { > Existing - {ENABLE_CLOUD_WORKSPACES && ( - - )} + @@ -483,7 +480,7 @@ export function NewWorkspaceModal() { onOpenSuccess={handleClose} /> )} - {ENABLE_CLOUD_WORKSPACES && mode === "cloud" && ( + {mode === "cloud" && (
Cloud Workspaces diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/CloudWorkspaceButton.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/CloudWorkspaceButton.tsx deleted file mode 100644 index e044acc444a..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/CloudWorkspaceButton.tsx +++ /dev/null @@ -1,299 +0,0 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@superset/ui/dropdown-menu"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; -import { - HiOutlineCloud, - HiOutlineCloudArrowUp, - HiOutlineExclamationTriangle, - HiOutlineGlobeAlt, - HiOutlineLink, - HiOutlinePlus, -} from "react-icons/hi2"; -import { LuGitBranch, LuLoader, LuUnlink } from "react-icons/lu"; -import { env } from "renderer/env.renderer"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; -import { authClient } from "renderer/lib/auth-client"; -import { electronTrpc } from "renderer/lib/electron-trpc"; - -interface CloudWorkspaceButtonProps { - workspaceId: string; - workspaceName: string; - branch: string; - cloudWorkspaceId: string | null; -} - -export function CloudWorkspaceButton({ - workspaceId, - workspaceName, - branch, - cloudWorkspaceId, -}: CloudWorkspaceButtonProps) { - const [isLinking, setIsLinking] = useState(false); - const [error, setError] = useState(null); - - const { data: session } = authClient.useSession(); - const utils = electronTrpc.useUtils(); - const queryClient = useQueryClient(); - const organizationId = session?.session?.activeOrganizationId; - - const { data: repoInfo } = electronTrpc.workspaces.getRepoInfo.useQuery( - { id: workspaceId }, - { enabled: !cloudWorkspaceId }, - ); - - const { data: cloudWorkspace, isLoading: isLoadingCloudStatus } = useQuery({ - queryKey: ["cloudWorkspace", cloudWorkspaceId], - queryFn: () => - apiTrpcClient.cloudWorkspace.byId.query(cloudWorkspaceId as string), - enabled: !!cloudWorkspaceId, - staleTime: 30_000, - }); - - const isCloudDeleted = cloudWorkspace?.deletedAt != null; - - const { data: matchingWorkspaces } = useQuery({ - queryKey: [ - "cloudWorkspace", - "matching", - organizationId, - repoInfo?.repoOwner, - repoInfo?.repoName, - ], - queryFn: () => - apiTrpcClient.cloudWorkspace.findMatching.query({ - organizationId: organizationId ?? "", - repoOwner: repoInfo?.repoOwner ?? "", - repoName: repoInfo?.repoName ?? "", - }), - enabled: - !!organizationId && - !!repoInfo?.hasRemote && - !!repoInfo.repoOwner && - !!repoInfo.repoName && - !cloudWorkspaceId, - staleTime: 30_000, - }); - - const linkToCloudMutation = electronTrpc.workspaces.linkToCloud.useMutation({ - onSuccess: () => { - utils.workspaces.get.invalidate({ id: workspaceId }); - utils.workspaces.getAllGrouped.invalidate(); - }, - }); - - const unlinkFromCloudMutation = - electronTrpc.workspaces.unlinkFromCloud.useMutation({ - onSuccess: () => { - utils.workspaces.get.invalidate({ id: workspaceId }); - utils.workspaces.getAllGrouped.invalidate(); - }, - }); - - const handleCreateAndLink = async () => { - if (!organizationId || !repoInfo?.hasRemote) { - return; - } - - setIsLinking(true); - setError(null); - - try { - const result = await apiTrpcClient.cloudWorkspace.create.mutate({ - organizationId, - repoOwner: repoInfo.repoOwner, - repoName: repoInfo.repoName, - repoUrl: repoInfo.repoUrl, - name: workspaceName, - branch, - }); - - await linkToCloudMutation.mutateAsync({ - id: workspaceId, - cloudWorkspaceId: result.cloudWorkspace.id, - }); - - queryClient.invalidateQueries({ - queryKey: ["cloudWorkspace", "matching"], - }); - } catch (err) { - console.error("[cloud-workspace] Failed to create and link:", err); - setError(err instanceof Error ? err.message : "Failed to link to cloud"); - } finally { - setIsLinking(false); - } - }; - - const handleLinkToExisting = async (existingCloudWorkspaceId: string) => { - setIsLinking(true); - setError(null); - - try { - await linkToCloudMutation.mutateAsync({ - id: workspaceId, - cloudWorkspaceId: existingCloudWorkspaceId, - }); - } catch (err) { - console.error("[cloud-workspace] Failed to link:", err); - setError(err instanceof Error ? err.message : "Failed to link to cloud"); - } finally { - setIsLinking(false); - } - }; - - const handleOpenInWeb = () => { - if (cloudWorkspaceId) { - window.open( - `${env.NEXT_PUBLIC_WEB_URL}/cloud/workspace/${cloudWorkspaceId}`, - "_blank", - ); - } - }; - - const handleUnlink = () => { - unlinkFromCloudMutation.mutate({ id: workspaceId }); - }; - - if (cloudWorkspaceId) { - if (isLoadingCloudStatus) { - return ( - - ); - } - - if (isCloudDeleted) { - return ( - - - - - -
- Cloud workspace was deleted -
- - - - Unlink - -
-
- ); - } - - return ( - - - - - - - - Open in web - - - - - Unlink - - - - ); - } - - if (!repoInfo?.hasRemote) { - return null; - } - - const hasMatchingWorkspaces = - matchingWorkspaces && matchingWorkspaces.length > 0; - - return ( - - - - - - {hasMatchingWorkspaces && ( - <> - - Existing workspaces - - {matchingWorkspaces.map((ws) => ( - handleLinkToExisting(ws.id)} - disabled={isLinking || !organizationId} - > - -
- {ws.name} - - - {ws.branch} - -
-
- ))} - - - )} - - -
- Create new workspace - - {repoInfo.repoOwner}/{repoInfo.repoName} ยท {branch} - -
-
- {error && ( -
{error}
- )} - {!organizationId && ( -
- Please select an organization first -
- )} -
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx index e6bf6674af6..9e36080cd8c 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/index.tsx @@ -1,7 +1,5 @@ import { useParams } from "@tanstack/react-router"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { ENABLE_CLOUD_WORKSPACES } from "shared/constants"; -import { CloudWorkspaceButton } from "./CloudWorkspaceButton"; import { OpenInMenuButton } from "./OpenInMenuButton"; import { OrganizationDropdown } from "./OrganizationDropdown"; import { WindowControls } from "./WindowControls"; @@ -28,14 +26,6 @@ export function TopBar() {
- {ENABLE_CLOUD_WORKSPACES && workspace && ( - - )} {workspace?.worktreePath && ( ))}
@@ -211,7 +209,6 @@ export function ProjectSection({ isUnread={workspace.isUnread} index={wsIndex} shortcutIndex={shortcutBaseIndex + wsIndex} - cloudWorkspaceId={workspace.cloudWorkspaceId} /> ))}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx index 1be439430b9..0ffbff2c9f0 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx @@ -14,15 +14,10 @@ import { Input } from "@superset/ui/input"; import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; -import { useQuery } from "@tanstack/react-query"; import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useMemo, useState } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { - HiMiniXMark, - HiOutlineCloud, - HiOutlineExclamationTriangle, -} from "react-icons/hi2"; +import { HiMiniXMark } from "react-icons/hi2"; import { LuCopy, LuEye, @@ -33,7 +28,6 @@ import { LuPencil, LuX, } from "react-icons/lu"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useReorderWorkspaces, @@ -45,7 +39,6 @@ import { StatusIndicator } from "renderer/screens/main/components/StatusIndicato import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename"; import { useTabsStore } from "renderer/stores/tabs/store"; import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; -import { ENABLE_CLOUD_WORKSPACES } from "shared/constants"; import { getHighestPriorityStatus } from "shared/tabs-types"; import { STROKE_WIDTH } from "../constants"; import { @@ -76,8 +69,6 @@ interface WorkspaceListItemProps { shortcutIndex?: number; /** Whether the sidebar is in collapsed mode (icon-only view) */ isCollapsed?: boolean; - /** Cloud workspace ID if linked to cloud */ - cloudWorkspaceId?: string | null; } export function WorkspaceListItem({ @@ -91,20 +82,8 @@ export function WorkspaceListItem({ index, shortcutIndex, isCollapsed = false, - cloudWorkspaceId, }: WorkspaceListItemProps) { const isBranchWorkspace = type === "branch"; - - const { data: cloudWorkspace } = useQuery({ - queryKey: ["cloudWorkspace", cloudWorkspaceId], - queryFn: () => - apiTrpcClient.cloudWorkspace.byId.query(cloudWorkspaceId as string), - enabled: ENABLE_CLOUD_WORKSPACES && !!cloudWorkspaceId, - staleTime: 30_000, - }); - - const isCloudWorkspace = ENABLE_CLOUD_WORKSPACES && !!cloudWorkspaceId; - const isCloudDeleted = cloudWorkspace?.deletedAt != null; const navigate = useNavigate(); const matchRoute = useMatchRoute(); const reorderWorkspaces = useReorderWorkspaces(); @@ -117,6 +96,7 @@ export function WorkspaceListItem({ ); const utils = electronTrpc.useUtils(); + // Derive isActive from route const isActive = !!matchRoute({ to: "/workspace/$workspaceId", params: { workspaceId: id }, @@ -132,9 +112,11 @@ export function WorkspaceListItem({ toast.error(`Failed to update unread status: ${error.message}`), }); + // Shared delete logic const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); + // Lazy-load GitHub status on hover to avoid N+1 queries const { data: githubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, @@ -144,6 +126,7 @@ export function WorkspaceListItem({ }, ); + // Lazy-load local git changes on hover const { data: localChanges } = electronTrpc.changes.getStatus.useQuery( { worktreePath }, { @@ -152,6 +135,7 @@ export function WorkspaceListItem({ }, ); + // Calculate total local changes (staged + unstaged + untracked) const localDiffStats = useMemo(() => { if (!localChanges) return null; const allFiles = [ @@ -165,6 +149,7 @@ export function WorkspaceListItem({ return { additions, deletions }; }, [localChanges]); + // Memoize workspace pane IDs to avoid recalculating on every render const workspacePaneIds = useMemo(() => { const workspaceTabs = tabs.filter((t) => t.workspaceId === id); return new Set( @@ -172,7 +157,9 @@ export function WorkspaceListItem({ ); }, [tabs, id]); + // Compute aggregate status for workspace using shared priority logic const workspaceStatus = useMemo(() => { + // Generator avoids array allocation function* paneStatuses() { for (const paneId of workspacePaneIds) { yield panes[paneId]?.status; @@ -215,6 +202,7 @@ export function WorkspaceListItem({ } }; + // Drag and drop const [{ isDragging }, drag] = useDrag( () => ({ type: WORKSPACE_TYPE, @@ -247,14 +235,18 @@ export function WorkspaceListItem({ }); const pr = githubStatus?.pr; + // Show diff stats from PR if available, otherwise from local changes const diffStats = localDiffStats || (pr && (pr.additions > 0 || pr.deletions > 0) ? { additions: pr.additions, deletions: pr.deletions } : null); const showDiffStats = !!diffStats; + + // Determine if we should show the branch subtitle const showBranchSubtitle = !isBranchWorkspace; + // Collapsed sidebar: show just the icon with hover card (worktree) or tooltip (branch) if (isCollapsed) { const collapsedButton = ( ); - if (isBranchWorkspace || isCloudWorkspace) { + // Branch workspaces get a simple tooltip + if (isBranchWorkspace) { return ( {collapsedButton} {name || branch} - - {isCloudDeleted - ? "Cloud workspace deleted" - : isCloudWorkspace - ? "Cloud workspace" - : "Local workspace"} + + Local workspace ); } + // Worktree workspaces get the full hover card with context menu return ( <> + {/* Active indicator - left border */} {isActive && (
)} + {/* Icon with status indicator */}
{workspaceStatus === "working" ? ( - ) : isCloudWorkspace && isCloudDeleted ? ( - - ) : isCloudWorkspace ? ( - ) : isBranchWorkspace ? ( - {isCloudWorkspace && isCloudDeleted ? ( - <> -

- Cloud workspace deleted -

-

- The linked cloud workspace was deleted -

- - ) : isCloudWorkspace ? ( - <> -

Cloud workspace

-

- Linked to cloud for remote access -

- - ) : isBranchWorkspace ? ( + {isBranchWorkspace ? ( <>

Local workspace

@@ -481,6 +432,7 @@ export function WorkspaceListItem({ + {/* Content area */}

{rename.isRenaming ? ( ) : (
+ {/* Row 1: Title + actions */}
+ {/* Keyboard shortcut */} {shortcutIndex !== undefined && shortcutIndex < MAX_KEYBOARD_SHORTCUT_INDEX && ( @@ -518,10 +472,12 @@ export function WorkspaceListItem({ )} + {/* Branch switcher for branch workspaces */} {isBranchWorkspace && ( )} + {/* Diff stats (transforms to X on hover) or close button for worktree workspaces */} {!isBranchWorkspace && (showDiffStats && diffStats ? ( + {/* Row 2: Git info (branch + PR badge) */} {(showBranchSubtitle || pr) && (
{showBranchSubtitle && ( @@ -594,6 +551,7 @@ export function WorkspaceListItem({ ); + // Wrap with context menu and hover card if (isBranchWorkspace) { return ( <> diff --git a/apps/desktop/src/shared/constants.ts b/apps/desktop/src/shared/constants.ts index 76090a3ba6c..7d4d80c53b9 100644 --- a/apps/desktop/src/shared/constants.ts +++ b/apps/desktop/src/shared/constants.ts @@ -59,6 +59,3 @@ export const DEFAULT_AUTO_APPLY_DEFAULT_PRESET = true; export const EXTERNAL_LINKS = { SETUP_TEARDOWN_SCRIPTS: `${process.env.NEXT_PUBLIC_DOCS_URL}/setup-teardown-scripts`, } as const; - -// Feature toggles (flip to true when ready to ship) -export const ENABLE_CLOUD_WORKSPACES = false; diff --git a/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/WorkspaceList.tsx b/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/WorkspaceList.tsx deleted file mode 100644 index 0eaf29c085a..00000000000 --- a/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/WorkspaceList.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { Badge } from "@superset/ui/badge"; -import { useQuery } from "@tanstack/react-query"; -import { Cloud, GitBranch } from "lucide-react"; -import Link from "next/link"; -import { useTRPC } from "@/trpc/react"; - -export function WorkspaceList() { - const trpc = useTRPC(); - - const { - data: workspaces, - isLoading, - isError, - } = useQuery(trpc.cloudWorkspace.all.queryOptions()); - - if (isLoading) { - return ( -
- Loading workspaces... -
- ); - } - - if (isError) { - return ( -
- Failed to load workspaces. Please try again. -
- ); - } - - if (!workspaces || workspaces.length === 0) { - return ( -
- No cloud workspaces found. -
- ); - } - - return ( -
- {workspaces.map((workspace) => ( - -
- -
-

{workspace.name}

-
- - {workspace.branch} - {workspace.repository && ( - in {workspace.repository.name} - )} -
-
-
- Active - - ))} -
- ); -} diff --git a/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/index.ts b/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/index.ts deleted file mode 100644 index 271c7477c89..00000000000 --- a/apps/web/src/app/(dashboard)/cloud/components/WorkspaceList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { WorkspaceList } from "./WorkspaceList"; diff --git a/apps/web/src/app/(dashboard)/cloud/new/page.tsx b/apps/web/src/app/(dashboard)/cloud/new/page.tsx deleted file mode 100644 index e48ba55f2be..00000000000 --- a/apps/web/src/app/(dashboard)/cloud/new/page.tsx +++ /dev/null @@ -1,205 +0,0 @@ -"use client"; - -import { Button } from "@superset/ui/button"; -import { Input } from "@superset/ui/input"; -import { Label } from "@superset/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@superset/ui/select"; -import { Skeleton } from "@superset/ui/skeleton"; -import { toast } from "@superset/ui/sonner"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Settings } from "lucide-react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { useTRPC } from "@/trpc/react"; - -export default function NewCloudWorkspacePage() { - const [selectedRepoId, setSelectedRepoId] = useState(""); - const [name, setName] = useState(""); - const [branch, setBranch] = useState(""); - - const router = useRouter(); - const trpc = useTRPC(); - const queryClient = useQueryClient(); - - const { data: organization, isLoading: isLoadingOrg } = useQuery( - trpc.user.myOrganization.queryOptions(), - ); - - const { data: repositories, isLoading: isLoadingRepos } = useQuery({ - ...trpc.integration.github.listRepositories.queryOptions({ - organizationId: organization?.id ?? "", - }), - enabled: !!organization?.id, - }); - - const selectedRepo = repositories?.find((r) => r.id === selectedRepoId); - - const createMutation = useMutation( - trpc.cloudWorkspace.create.mutationOptions({ - onSuccess: () => { - toast.success("Workspace created", { - description: "Your cloud workspace has been created successfully.", - }); - queryClient.invalidateQueries({ - queryKey: trpc.cloudWorkspace.all.queryKey(), - }); - router.push("/cloud"); - }, - onError: (error) => { - toast.error("Failed to create workspace", { - description: error.message, - }); - }, - }), - ); - - const handleRepoChange = (repoId: string) => { - setSelectedRepoId(repoId); - const repo = repositories?.find((r) => r.id === repoId); - if (repo) { - setBranch(repo.defaultBranch); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!organization) { - toast.error("No organization found"); - return; - } - - if (!selectedRepo) { - toast.error("Please select a repository"); - return; - } - - createMutation.mutate({ - organizationId: organization.id, - repoOwner: selectedRepo.owner, - repoName: selectedRepo.name, - repoUrl: `https://github.com/${selectedRepo.fullName}`, - name: name || `${selectedRepo.name}-workspace`, - branch: branch || selectedRepo.defaultBranch, - }); - }; - - if (isLoadingOrg) { - return ( -
Loading...
- ); - } - - if (!organization) { - return ( -
-

- You need to be part of an organization to create workspaces. -

-
- ); - } - - return ( -
- - - Back to Cloud Workspaces - - -
-

Create Cloud Workspace

-

- Create a new cloud workspace from a GitHub repository. -

-
- -
-
- - {isLoadingRepos ? ( - - ) : repositories && repositories.length > 0 ? ( -
- - -
- ) : ( -

- No repositories found. Please connect your GitHub account in{" "} - - Integrations - - . -

- )} -
- -
- - setName(e.target.value)} - /> -

- Leave empty to auto-generate from repository name. -

-
- -
- - setBranch(e.target.value)} - /> -
- -
- - -
-
-
- ); -} diff --git a/apps/web/src/app/(dashboard)/cloud/page.tsx b/apps/web/src/app/(dashboard)/cloud/page.tsx deleted file mode 100644 index 34ca019fdd0..00000000000 --- a/apps/web/src/app/(dashboard)/cloud/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { Plus } from "lucide-react"; -import Link from "next/link"; -import { WorkspaceList } from "./components/WorkspaceList"; - -export default function CloudPage() { - return ( -
-
-
-
-

Cloud Workspaces

-

- Manage your cloud workspaces here. -

-
- -
- -
- -
-
-
- ); -} diff --git a/apps/web/src/app/(dashboard)/cloud/workspace/[id]/page.tsx b/apps/web/src/app/(dashboard)/cloud/workspace/[id]/page.tsx deleted file mode 100644 index 7bd2af33dd8..00000000000 --- a/apps/web/src/app/(dashboard)/cloud/workspace/[id]/page.tsx +++ /dev/null @@ -1,193 +0,0 @@ -"use client"; - -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@superset/ui/alert-dialog"; -import { Button } from "@superset/ui/button"; -import { toast } from "@superset/ui/sonner"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - ArrowLeft, - Cloud, - ExternalLink, - GitBranch, - Trash2, -} from "lucide-react"; -import Link from "next/link"; -import { useParams, useRouter } from "next/navigation"; -import { useTRPC } from "@/trpc/react"; - -export default function WorkspaceDetailPage() { - const params = useParams(); - const id = params.id as string; - const router = useRouter(); - - const trpc = useTRPC(); - const queryClient = useQueryClient(); - - const { - data: workspace, - isLoading, - isError, - } = useQuery(trpc.cloudWorkspace.byId.queryOptions(id)); - - const deleteMutation = useMutation( - trpc.cloudWorkspace.delete.mutationOptions({ - onSuccess: () => { - toast.success("Workspace deleted", { - description: "The workspace has been deleted successfully.", - }); - queryClient.invalidateQueries({ - queryKey: trpc.cloudWorkspace.all.queryKey(), - }); - router.push("/cloud"); - }, - onError: (error) => { - toast.error("Failed to delete workspace", { - description: error.message, - }); - }, - }), - ); - - const handleDelete = () => { - deleteMutation.mutate(id); - }; - - if (isLoading) { - return ( -
- Loading workspace... -
- ); - } - - if (isError || !workspace) { - return ( -
- - - Back to Cloud Workspaces - -
- Workspace not found. -
-
- ); - } - - return ( -
- - - Back to Cloud Workspaces - - -
-
-
- -
-
-

{workspace.name}

-
- - {workspace.branch} -
-
-
- - - - - - - - Delete Workspace - - Are you sure you want to delete "{workspace.name}"? This action - cannot be undone. - - - - Cancel - - {deleteMutation.isPending ? "Deleting..." : "Delete"} - - - - -
- -
-
-

Workspace Details

-
-
-
Name
-
{workspace.name}
-
-
-
Branch
-
{workspace.branch}
-
-
-
Created
-
{new Date(workspace.createdAt).toLocaleDateString()}
-
-
-
- - {workspace.repository && ( -
-

Repository

-
-
-
Name
-
{workspace.repository.name}
-
-
-
Owner
-
{workspace.repository.repoOwner}
-
-
-
Default Branch
-
{workspace.repository.defaultBranch}
-
-
- -
- )} -
-
- ); -} diff --git a/apps/web/src/app/(dashboard)/components/SidebarNav/SidebarNav.tsx b/apps/web/src/app/(dashboard)/components/SidebarNav/SidebarNav.tsx index 8d7fb7d90aa..bd7041e82c6 100644 --- a/apps/web/src/app/(dashboard)/components/SidebarNav/SidebarNav.tsx +++ b/apps/web/src/app/(dashboard)/components/SidebarNav/SidebarNav.tsx @@ -7,7 +7,6 @@ import { usePathname } from "next/navigation"; const navItems = [ { href: "/", label: "Home" }, { href: "/integrations", label: "Integrations" }, - { href: "/cloud", label: "Cloud" }, ]; export function SidebarNav() { @@ -24,7 +23,9 @@ export function SidebarNav() { href={item.href} className={cn( "font-mono transition-opacity", - isActive ? "opacity-100" : "opacity-60 hover:opacity-80", + isActive + ? "underline opacity-100" + : "opacity-60 hover:opacity-80", )} > {item.label} diff --git a/packages/db/drizzle/0013_add_cloud_workspaces.sql b/packages/db/drizzle/0013_add_cloud_workspaces.sql deleted file mode 100644 index d489eed87f6..00000000000 --- a/packages/db/drizzle/0013_add_cloud_workspaces.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE "cloud_workspaces" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "organization_id" uuid NOT NULL, - "repository_id" uuid NOT NULL, - "name" text NOT NULL, - "branch" text NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -ALTER TABLE "cloud_workspaces" ADD CONSTRAINT "cloud_workspaces_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "cloud_workspaces" ADD CONSTRAINT "cloud_workspaces_repository_id_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "cloud_workspaces_organization_id_idx" ON "cloud_workspaces" USING btree ("organization_id");--> statement-breakpoint -CREATE INDEX "cloud_workspaces_repository_id_idx" ON "cloud_workspaces" USING btree ("repository_id"); \ No newline at end of file diff --git a/packages/db/src/schema/relations.ts b/packages/db/src/schema/relations.ts index 64e349391ec..c66c8f9e24c 100644 --- a/packages/db/src/schema/relations.ts +++ b/packages/db/src/schema/relations.ts @@ -15,7 +15,6 @@ import { } from "./github"; import { agentCommands, - cloudWorkspaces, devicePresence, integrationConnections, repositories, @@ -60,7 +59,6 @@ export const organizationsRelations = relations(organizations, ({ many }) => ({ taskStatuses: many(taskStatuses), integrations: many(integrationConnections), githubInstallations: many(githubInstallations), - cloudWorkspaces: many(cloudWorkspaces), devicePresence: many(devicePresence), agentCommands: many(agentCommands), })); @@ -102,7 +100,6 @@ export const repositoriesRelations = relations( references: [organizations.id], }), tasks: many(tasks), - cloudWorkspaces: many(cloudWorkspaces), }), ); @@ -193,20 +190,6 @@ export const githubPullRequestsRelations = relations( }), ); -export const cloudWorkspacesRelations = relations( - cloudWorkspaces, - ({ one }) => ({ - organization: one(organizations, { - fields: [cloudWorkspaces.organizationId], - references: [organizations.id], - }), - repository: one(repositories, { - fields: [cloudWorkspaces.repositoryId], - references: [repositories.id], - }), - }), -); - // Agent relations export const devicePresenceRelations = relations(devicePresence, ({ one }) => ({ user: one(users, { diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index 7a263ed7373..c330c0cb477 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -71,7 +71,7 @@ export const taskStatuses = pgTable( name: text().notNull(), color: text().notNull(), - type: text().notNull(), + type: text().notNull(), // "backlog" | "unstarted" | "started" | "completed" | "canceled" position: real().notNull(), progressPercent: real("progress_percent"), @@ -139,7 +139,7 @@ export const tasks = pgTable( // External sync (null if local-only task) externalProvider: integrationProvider("external_provider"), externalId: text("external_id"), - externalKey: text("external_key"), + externalKey: text("external_key"), // "SUPER-172", "#123" externalUrl: text("external_url"), lastSyncedAt: timestamp("last_synced_at"), syncError: text("sync_error"), @@ -175,6 +175,7 @@ export const tasks = pgTable( export type InsertTask = typeof tasks.$inferInsert; export type SelectTask = typeof tasks.$inferSelect; +// Integration connections for external providers (Linear, GitHub, etc.) export const integrationConnections = pgTable( "integration_connections", { @@ -218,6 +219,7 @@ export type InsertIntegrationConnection = export type SelectIntegrationConnection = typeof integrationConnections.$inferSelect; +// Stripe subscriptions (org-based billing) export const subscriptions = pgTable( "subscriptions", { @@ -254,34 +256,6 @@ export const subscriptions = pgTable( export type InsertSubscription = typeof subscriptions.$inferInsert; export type SelectSubscription = typeof subscriptions.$inferSelect; -export const cloudWorkspaces = pgTable( - "cloud_workspaces", - { - id: uuid().primaryKey().defaultRandom(), - organizationId: uuid("organization_id") - .notNull() - .references(() => organizations.id, { onDelete: "cascade" }), - repositoryId: uuid("repository_id") - .notNull() - .references(() => repositories.id, { onDelete: "cascade" }), - name: text().notNull(), - branch: text().notNull(), - deletedAt: timestamp("deleted_at"), - createdAt: timestamp("created_at").notNull().defaultNow(), - updatedAt: timestamp("updated_at") - .notNull() - .defaultNow() - .$onUpdate(() => new Date()), - }, - (table) => [ - index("cloud_workspaces_organization_id_idx").on(table.organizationId), - index("cloud_workspaces_repository_id_idx").on(table.repositoryId), - ], -); - -export type InsertCloudWorkspace = typeof cloudWorkspaces.$inferInsert; -export type SelectCloudWorkspace = typeof cloudWorkspaces.$inferSelect; - // Device presence - tracks online devices for command routing export const devicePresence = pgTable( "device_presence", diff --git a/packages/local-db/drizzle/0013_add_cloud_workspace_id.sql b/packages/local-db/drizzle/0013_add_cloud_workspace_id.sql deleted file mode 100644 index 9ef9e7148aa..00000000000 --- a/packages/local-db/drizzle/0013_add_cloud_workspace_id.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `workspaces` ADD `cloud_workspace_id` text;--> statement-breakpoint -CREATE INDEX `workspaces_cloud_workspace_id_idx` ON `workspaces` (`cloud_workspace_id`); \ No newline at end of file diff --git a/packages/local-db/src/schema/schema.ts b/packages/local-db/src/schema/schema.ts index 585a4ef98bb..6a968a7aefe 100644 --- a/packages/local-db/src/schema/schema.ts +++ b/packages/local-db/src/schema/schema.ts @@ -106,14 +106,11 @@ export const workspaces = sqliteTable( // Timestamp when deletion was initiated. Non-null means deletion in progress. // Workspaces with deletingAt set should be filtered out from queries. deletingAt: integer("deleting_at"), - // Optional link to cloud workspace (UUID from cloud_workspaces table) - cloudWorkspaceId: text("cloud_workspace_id"), }, (table) => [ index("workspaces_project_id_idx").on(table.projectId), index("workspaces_worktree_id_idx").on(table.worktreeId), index("workspaces_last_opened_at_idx").on(table.lastOpenedAt), - index("workspaces_cloud_workspace_id_idx").on(table.cloudWorkspaceId), // NOTE: Migration 0006 creates an additional partial unique index: // CREATE UNIQUE INDEX workspaces_unique_branch_per_project // ON workspaces(project_id) WHERE type = 'branch' diff --git a/packages/trpc/src/root.ts b/packages/trpc/src/root.ts index 12f0a06d3d5..7e333dee987 100644 --- a/packages/trpc/src/root.ts +++ b/packages/trpc/src/root.ts @@ -3,7 +3,6 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { adminRouter } from "./router/admin"; import { agentRouter } from "./router/agent"; import { analyticsRouter } from "./router/analytics"; -import { cloudWorkspaceRouter } from "./router/cloudWorkspace"; import { deviceRouter } from "./router/device"; import { integrationRouter } from "./router/integration"; import { organizationRouter } from "./router/organization"; @@ -16,7 +15,6 @@ export const appRouter = createTRPCRouter({ admin: adminRouter, agent: agentRouter, analytics: analyticsRouter, - cloudWorkspace: cloudWorkspaceRouter, device: deviceRouter, integration: integrationRouter, organization: organizationRouter, diff --git a/packages/trpc/src/router/cloudWorkspace/cloudWorkspace.ts b/packages/trpc/src/router/cloudWorkspace/cloudWorkspace.ts deleted file mode 100644 index 98d8a61fb05..00000000000 --- a/packages/trpc/src/router/cloudWorkspace/cloudWorkspace.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { db, dbWs } from "@superset/db/client"; -import { cloudWorkspaces, repositories } from "@superset/db/schema"; -import type { TRPCRouterRecord } from "@trpc/server"; -import { and, desc, eq, isNull } from "drizzle-orm"; -import { z } from "zod"; -import { protectedProcedure } from "../../trpc"; - -export const cloudWorkspaceRouter = { - all: protectedProcedure.query(() => { - return db.query.cloudWorkspaces.findMany({ - where: isNull(cloudWorkspaces.deletedAt), - orderBy: desc(cloudWorkspaces.createdAt), - with: { - organization: true, - repository: true, - }, - }); - }), - - byOrganization: protectedProcedure - .input(z.string().uuid()) - .query(({ input }) => { - return db.query.cloudWorkspaces.findMany({ - where: and( - eq(cloudWorkspaces.organizationId, input), - isNull(cloudWorkspaces.deletedAt), - ), - orderBy: desc(cloudWorkspaces.createdAt), - with: { - repository: true, - }, - }); - }), - - byId: protectedProcedure.input(z.string().uuid()).query(({ input }) => { - return db.query.cloudWorkspaces.findFirst({ - where: eq(cloudWorkspaces.id, input), - with: { - organization: true, - repository: true, - }, - }); - }), - - findMatching: protectedProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - repoOwner: z.string().min(1), - repoName: z.string().min(1), - }), - ) - .query(async ({ input }) => { - const { organizationId, repoOwner, repoName } = input; - - const repository = await db.query.repositories.findFirst({ - where: and( - eq(repositories.organizationId, organizationId), - eq(repositories.repoOwner, repoOwner), - eq(repositories.repoName, repoName), - ), - }); - - if (!repository) { - return []; - } - - return db.query.cloudWorkspaces.findMany({ - where: and( - eq(cloudWorkspaces.repositoryId, repository.id), - isNull(cloudWorkspaces.deletedAt), - ), - orderBy: desc(cloudWorkspaces.createdAt), - }); - }), - - create: protectedProcedure - .input( - z.object({ - organizationId: z.string().uuid(), - repoOwner: z.string().min(1), - repoName: z.string().min(1), - repoUrl: z.string().url(), - name: z.string().min(1), - branch: z.string().min(1), - }), - ) - .mutation(async ({ input }) => { - const { organizationId, repoOwner, repoName, repoUrl, name, branch } = - input; - - return dbWs.transaction(async (tx) => { - let repository = await tx.query.repositories.findFirst({ - where: and( - eq(repositories.organizationId, organizationId), - eq(repositories.repoOwner, repoOwner), - eq(repositories.repoName, repoName), - ), - }); - - if (!repository) { - const slug = `${repoOwner}-${repoName}`.toLowerCase(); - const [newRepo] = await tx - .insert(repositories) - .values({ - organizationId, - name: repoName, - slug, - repoUrl, - repoOwner, - repoName, - defaultBranch: "main", - }) - .returning(); - - if (!newRepo) { - throw new Error("Failed to create repository"); - } - repository = newRepo; - } - - const [cloudWorkspace] = await tx - .insert(cloudWorkspaces) - .values({ - organizationId, - repositoryId: repository.id, - name, - branch, - }) - .returning(); - - return { - cloudWorkspace, - repository, - }; - }); - }), - - delete: protectedProcedure - .input(z.string().uuid()) - .mutation(async ({ input }) => { - await dbWs - .update(cloudWorkspaces) - .set({ deletedAt: new Date() }) - .where(eq(cloudWorkspaces.id, input)); - - return { success: true }; - }), -} satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/cloudWorkspace/index.ts b/packages/trpc/src/router/cloudWorkspace/index.ts deleted file mode 100644 index b83b0b61f78..00000000000 --- a/packages/trpc/src/router/cloudWorkspace/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { cloudWorkspaceRouter } from "./cloudWorkspace";