From 0cbd6b321f2175298421ff4beea2f96ede62a6a0 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:33:58 -0800 Subject: [PATCH] update ui to look like vs marketplace --- webview-ui/src/components/mcp/McpView.tsx | 100 ++++--- .../mcp/marketplace/McpMarketplaceCard.tsx | 261 ++++++++++++------ .../mcp/marketplace/McpMarketplaceView.tsx | 183 +++++++----- 3 files changed, 360 insertions(+), 184 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 9450140f2f..c3f615efaa 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -13,6 +13,7 @@ import { McpServer } from "../../../../src/shared/mcp" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpMarketplaceView from "./marketplace/McpMarketplaceView" +import styled from "styled-components" type McpViewProps = { onDone: () => void @@ -20,7 +21,7 @@ type McpViewProps = { const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() - const [activeTab, setActiveTab] = useState(servers.length === 0 ? "marketplace" : "installed") + const [activeTab, setActiveTab] = useState("marketplace") // const [servers, setServers] = useState([ // // Add some mock servers for testing @@ -99,20 +100,34 @@ const McpView = ({ onDone }: McpViewProps) => { display: "flex", justifyContent: "space-between", alignItems: "center", - padding: "10px 17px 10px 20px", + padding: "10px 17px 5px 20px", }}>

MCP Servers

Done -
- setActiveTab(e.target.activeid)}> - Installed - Marketplace - Settings +
+ {/* Tabs container */} +
+ setActiveTab("marketplace")}> + Marketplace + + setActiveTab("installed")}> + Installed + +
- -
+ {/* Content container */} +
+ {activeTab === "marketplace" && } + {activeTab === "installed" && ( +
{
)} -
- - -
- -
-
- - -
- {/* Server Configuration Button */} -
+ {/* Settings Section */} +
{ vscode.postMessage({ type: "openMcpSettings" }) }}> Configure MCP Servers -
- {/* Advanced Settings Link */} -
- { - vscode.postMessage({ - type: "openExtensionSettings", - text: "cline.mcp", - }) - }} - style={{ fontSize: "12px" }}> - Advanced MCP Settings - +
+ { + vscode.postMessage({ + type: "openExtensionSettings", + text: "cline.mcp", + }) + }} + style={{ fontSize: "12px" }}> + Advanced MCP Settings + +
- - + )} +
) } +const StyledTabButton = styled.button<{ isActive: boolean }>` + background: none; + border: none; + border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")}; + color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")}; + padding: 8px 16px; + cursor: pointer; + font-size: 13px; + margin-bottom: -1px; + font-family: inherit; + + &:hover { + color: var(--vscode-foreground); + } +` + +const TabButton = ({ children, isActive, onClick }: { children: React.ReactNode; isActive: boolean; onClick: () => void }) => ( + + {children} + +) + // Server Row Component const ServerRow = ({ server }: { server: McpServer }) => { const [isExpanded, setIsExpanded] = useState(false) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index da11b6f78b..2e6ec6a708 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react" -import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { CSSProperties, useEffect, useState } from "react" +import styled from "styled-components" import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" import { vscode } from "../../../utils/vscode" @@ -27,40 +28,62 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) }, []) return ( -
-
-
+ <> + +
{ + console.log("Card clicked:", item.mcpId) + }} + style={{ + borderBottom: "1px solid var(--vscode-textCodeBlock-background)", + padding: "12px 16px", + }}> + {/* Main container with logo and content */} +
+ {/* Logo */} {item.logoUrl && ( {`${item.name} )} -
-
-
-

{item.name}

-
- by {item.author} -
-
- { + + {/* Content section */} +
+ {/* First row: name and install button */} +
+

+ {item.name} +

+
{ + e.stopPropagation() // Prevent card click when clicking install if (!isInstalled && !isDownloading) { setIsDownloading(true) vscode.postMessage({ @@ -68,22 +91,25 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) mcpId: item.mcpId, }) } - }}> - - {isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"} - + }} + style={{}}> + + {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} + +
-

{item.description}

+ + {/* Second row: metadata */}
- + -
- - {item.githubStars?.toLocaleString() ?? 0} -
-
- - {item.downloadCount?.toLocaleString() ?? 0} -
- {item.requiresApiKey && ( -
- -
- )} - {item.isRecommended && ( -
- -
- )} -
-
- {item.category} + {item.author} - {item.tags.map((tag) => ( - - {tag} - - ))} + | +
+ + {item.githubStars?.toLocaleString() ?? 0} +
+ | +
+ + {item.downloadCount?.toLocaleString() ?? 0} +
+ {item.requiresApiKey && ( + + )} + {item.isRecommended && ( + + )}
+ + {/* Description and tags */} +
+

{item.description}

+
+ + {item.category} + + {item.tags.map((tag, index) => ( + + {tag} + {index === item.tags.length - 1 ? "" : ""} + + ))} +
+
+
-
+ ) } +const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>` + font-size: 12px; + font-weight: 500; + padding: 2px 6px; + border-radius: 2px; + border: none; + cursor: pointer; + background: ${(props) => + props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"}; + color: var(--vscode-button-foreground); + + &:hover:not(:disabled) { + background: ${(props) => + props.$isInstalled ? "var(--vscode-button-secondaryHoverBackground)" : "var(--vscode-button-hoverBackground)"}; + } + + &:active:not(:disabled) { + background: ${(props) => + props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"}; + opacity: 0.7; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } +` + export default McpMarketplaceCard diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 966dd5bd47..2b34c90194 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,5 +1,13 @@ import { useEffect, useMemo, useState } from "react" -import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { + VSCodeButton, + VSCodeProgressRing, + VSCodeRadioGroup, + VSCodeRadio, + VSCodeDropdown, + VSCodeOption, + VSCodeTextField, +} from "@vscode/webview-ui-toolkit/react" import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" @@ -33,11 +41,6 @@ const selectStyles = { cursor: "pointer", // Show pointer cursor on hover } as const -const refreshStyles = { - height: controlHeight, - alignSelf: "flex-end", -} as const - const McpMarketplaceView = () => { const { mcpServers } = useExtensionState() const [items, setItems] = useState([]) @@ -46,7 +49,7 @@ const McpMarketplaceView = () => { const [isRefreshing, setIsRefreshing] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [selectedCategory, setSelectedCategory] = useState(null) - const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount") + const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name" | "newest">("downloadCount") const categories = useMemo(() => { const uniqueCategories = new Set(items.map((item) => item.category)) @@ -72,6 +75,8 @@ const McpMarketplaceView = () => { return b.githubStars - a.githubStars case "name": return a.name.localeCompare(b.name) + case "newest": + return b.githubStars - a.githubStars // FIXME: b.createdAt - a.createdAt // Assuming there's a createdAt field default: return 0 } @@ -154,73 +159,115 @@ const McpMarketplaceView = () => { } return ( -
-
-
- {" "} - {/* Added minWidth: 0 to prevent flex item from overflowing */} -
- setSearchQuery(e.target.value)} - className="mcp-search-input" - style={searchInputStyles} - /> - +
+ {/* Search row */} + setSearchQuery((e.target as HTMLInputElement).value)}> +
+ {searchQuery && ( +
setSearchQuery("")} + slot="end" style={{ - position: "absolute", - left: "10px", - top: "50%", - transform: "translateY(-50%)", - color: "var(--vscode-input-placeholderForeground)", - pointerEvents: "none", - fontSize: "14px", // Match input text size - lineHeight: 1, // Ensure icon is centered properly + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + cursor: "pointer", }} /> + )} + + + {/* Filter row */} +
+ + Filter: + +
+ setSelectedCategory((e.target as HTMLSelectElement).value || null)}> + All Categories + {categories.map((category) => ( + + {category} + + ))} +
- -
- fetchMarketplace(true)} - disabled={isRefreshing} - style={refreshStyles}> - - Refresh - + + {/* Sort row */} +
+ + Sort: + + setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}> + Most Installs + Most Stars + Newest + Name + +
+