Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/webview-shared/createWebviewConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export function createWebviewConfig(
resolve: {
alias: {
"@repo/webview-shared": resolve(dirname, "../webview-shared/src"),
// @repo/ui ships TypeScript source and its package-internal
// subpath imports; bundling it needs the same direct resolution
"@repo/ui": resolve(dirname, "../ui/src"),
"#cx": resolve(dirname, "../ui/src/cx.ts"),
"#codicons": resolve(dirname, "../ui/src/codicons.ts"),
},
},
});
Expand Down
1 change: 1 addition & 0 deletions packages/workspaces/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@repo/shared": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/webview-shared": "workspace:*",
"@tanstack/react-query": "catalog:",
"@vscode-elements/react-elements": "catalog:",
Expand Down
6 changes: 5 additions & 1 deletion packages/workspaces/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { MOCK_WORKSPACES } from "./mockData";
import { WorkspacesPanel } from "./WorkspacesPanel";

export default function App() {
return <div>TODO</div>;
// Prototype: mock data only; IPC arrives with the real provider wiring.
return <WorkspacesPanel workspaces={MOCK_WORKSPACES} isOwner />;
}
60 changes: 60 additions & 0 deletions packages/workspaces/src/WorkspaceFilterSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from "@repo/ui";

/** Which workspace set the panel lists. */
export type WorkspaceFilter = "mine" | "all" | "shared";

const FILTER_LABELS: Record<WorkspaceFilter, string> = {
mine: "Mine",
all: "All",
shared: "Shared",
};

export interface WorkspaceFilterSelectProps {
/** "Shared" is hidden unless the signed-in user is an owner. */
isOwner?: boolean;
value: WorkspaceFilter;
onChange: (filter: WorkspaceFilter) => void;
}

export function WorkspaceFilterSelect({
isOwner = false,
value,
onChange,
}: WorkspaceFilterSelectProps): React.JSX.Element {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" className="workspaces-panel__filter">
{FILTER_LABELS[value]}
<Icon name="chevron-down" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={value}
onValueChange={(filter) => onChange(filter as WorkspaceFilter)}
>
<DropdownMenuRadioItem value="mine">
{FILTER_LABELS.mine}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="all">
{FILTER_LABELS.all}
</DropdownMenuRadioItem>
{isOwner ? (
<DropdownMenuRadioItem value="shared">
{FILTER_LABELS.shared}
</DropdownMenuRadioItem>
) : null}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}
50 changes: 50 additions & 0 deletions packages/workspaces/src/WorkspacesPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.workspaces-panel {
display: flex;
flex-direction: column;
height: 100vh;
}

.workspaces-panel__toolbar {
display: flex;
gap: var(--ui-spacing-40);
align-items: center;
padding: var(--ui-spacing-40);
}

.workspaces-panel__toolbar .ui-search-input {
flex: 1;
min-width: 0;
}

.workspaces-panel__filter {
gap: var(--ui-spacing-40);
flex: none;
}

.workspaces-panel__tree {
flex: 1;
min-height: 0;
overflow-y: auto;
}

.workspaces-panel__row-label {
display: inline-flex;
align-items: center;
gap: var(--ui-spacing-60);
min-width: 0;
}

.workspaces-panel__name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}

.workspaces-panel__owner {
color: var(--ui-description-foreground);
flex: none;
}

.workspaces-panel__highlight {
color: var(--vscode-list-highlightForeground, var(--ui-link-foreground));
}
146 changes: 146 additions & 0 deletions packages/workspaces/src/WorkspacesPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {
EmptyState,
ErrorState,
LoadingState,
SearchInput,
Tree,
type TreeNode,
} from "@repo/ui";
import { useMemo, useState } from "react";

import { workspaceNode } from "./rows";
import {
WorkspaceFilterSelect,
type WorkspaceFilter,
} from "./WorkspaceFilterSelect";
import "./WorkspacesPanel.css";

import type { MockWorkspaceEntry } from "./mockData";

export interface WorkspacesPanelProps {
readonly workspaces: readonly MockWorkspaceEntry[];
/** Gates the "Shared" filter option, like `coder.isOwner`. */
readonly isOwner?: boolean;
readonly state?: "ready" | "loading" | "error";
readonly onRetry?: () => void;
}

function filterEntries(
entries: readonly MockWorkspaceEntry[],
filter: WorkspaceFilter,
query: string,
): readonly MockWorkspaceEntry[] {
const lowered = query.trim().toLocaleLowerCase();
return entries.filter((entry) => {
if (filter === "mine" && entry.owner !== "me") return false;
if (filter === "shared" && !entry.shared) return false;
if (lowered === "") return true;
const haystack = [
entry.workspace.name,
entry.workspace.owner_name,
entry.workspace.template_display_name,
...entry.agents.map((agent) => agent.name),
]
.join(" ")
.toLocaleLowerCase();
return haystack.includes(lowered);
});
}

/** Expands workspaces and agents, leaving leaf sections collapsed. */
function initialExpandedIds(nodes: readonly TreeNode[]): readonly string[] {
const ids: string[] = [];
const visit = (node: TreeNode, depth: number): void => {
if (node.children && depth < 2) {
ids.push(node.id);
}
node.children?.forEach((child) => visit(child, depth + 1));
};
nodes.forEach((node) => visit(node, 0));
return ids;
}

/** Prototype panel: toolbar, filtered tree, and the loading/error/empty states. */
export function WorkspacesPanel({
workspaces,
isOwner = false,
state = "ready",
onRetry,
}: WorkspacesPanelProps): React.JSX.Element {
const [filter, setFilter] = useState<WorkspaceFilter>("mine");
const [query, setQuery] = useState("");
const [selectedItemId, setSelectedItemId] = useState<string | undefined>();

const nodes = useMemo(
() =>
filterEntries(workspaces, filter, query).map((entry) =>
workspaceNode(entry, filter !== "mine", query),
),
[workspaces, filter, query],
);
const [expandedIds, setExpandedIds] = useState<readonly string[]>(() =>
initialExpandedIds(nodes),
);

let body: React.JSX.Element;
if (state === "loading") {
body = <LoadingState title="Loading workspaces" />;
} else if (state === "error") {
body = (
<ErrorState
title="Failed to load workspaces"
description="The Coder deployment could not be reached."
onRetry={onRetry}
/>
);
} else if (workspaces.length === 0) {
body = (
<EmptyState
icon="inbox"
title="No workspaces"
description="Create a workspace to get started."
/>
);
} else if (nodes.length === 0) {
body = (
<EmptyState
icon="search"
title="No matching workspaces"
description={`No results for "${query}".`}
/>
);
} else {
body = (
<div className="workspaces-panel__tree">
<Tree
aria-label="Workspaces"
nodes={nodes}
expandedIds={expandedIds}
onExpandedIdsChange={setExpandedIds}
selectedItemId={selectedItemId}
onSelectedItemChange={setSelectedItemId}
stickyScroll
/>
</div>
);
}

return (
<div className="workspaces-panel">
<div className="workspaces-panel__toolbar">
<SearchInput
value={query}
onChange={setQuery}
label="Search workspaces"
placeholder="Search workspaces"
/>
<WorkspaceFilterSelect
isOwner={isOwner}
value={filter}
onChange={setFilter}
/>
</div>
{body}
</div>
);
}
15 changes: 14 additions & 1 deletion packages/workspaces/src/index.css
Original file line number Diff line number Diff line change
@@ -1 +1,14 @@
/* TODO */
/* UI library theme tokens and codicon font */
@import "@repo/ui/codicon.css";
@import "@repo/ui/tokens.css";

body {
margin: 0;
padding: 0;
font-family: var(--ui-font-family);
font-size: var(--ui-font-size);
font-weight: var(--ui-font-weight-regular);
color: var(--ui-foreground);
background: var(--ui-background);
overflow: hidden;
}
5 changes: 4 additions & 1 deletion packages/workspaces/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TooltipProvider } from "@repo/ui";
import { ErrorBoundary } from "@repo/webview-shared/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
Expand All @@ -19,7 +20,9 @@ createRoot(root).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<App />
<TooltipProvider>
<App />
</TooltipProvider>
</ErrorBoundary>
</QueryClientProvider>
</StrictMode>,
Expand Down
Loading