diff --git a/package.json b/package.json index da5509d5..f9fd5c96 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "view/title": [ { "command": "nextflow.seqera.reloadWebview", - "when": "view == project || view == userInfo", + "when": "view == project || view == modules || view == userInfo", "group": "navigation@10" }, { @@ -301,6 +301,11 @@ "name": "Project", "type": "webview" }, + { + "id": "modules", + "name": "Modules", + "type": "webview" + }, { "id": "userInfo", "name": "Seqera Cloud", diff --git a/src/webview/WebviewProvider/index.ts b/src/webview/WebviewProvider/index.ts index 6e522d8e..799ae67e 100644 --- a/src/webview/WebviewProvider/index.ts +++ b/src/webview/WebviewProvider/index.ts @@ -22,7 +22,7 @@ class WebviewProvider implements vscode.WebviewViewProvider { constructor( private readonly _context: vscode.ExtensionContext, - private readonly viewID: "project" | "userInfo", + private readonly viewID: "project" | "modules" | "userInfo", private readonly _authProvider?: AuthProvider ) { this._extensionUri = _context.extensionUri; @@ -40,6 +40,9 @@ class WebviewProvider implements vscode.WebviewViewProvider { case "openFile": this.openFile(message.path, message.line); break; + case "openExternal": + this.openExternal(message.url); + break; case "openChat": this.openChat(); break; @@ -72,6 +75,9 @@ class WebviewProvider implements vscode.WebviewViewProvider { if (!workspaceId) return; this.fetchComputeEnvs(workspaceId); break; + case "runCommand": + this.runCommand(message.text); + break; } }); @@ -165,10 +171,21 @@ class WebviewProvider implements vscode.WebviewViewProvider { }); } + private async openExternal(url: string) { + await vscode.env.openExternal(vscode.Uri.parse(url)); + } + private async openChat() { await vscode.commands.executeCommand("nextflow.chatbot.openChat"); } + private async runCommand(command: string) { + await vscode.commands.executeCommand( + "workbench.action.terminal.sendSequence", + { text: `${command}\n` } + ); + } + private initHTML(view: vscode.WebviewView) { this._currentView = view; diff --git a/src/webview/index.ts b/src/webview/index.ts index 2302863d..7a750710 100644 --- a/src/webview/index.ts +++ b/src/webview/index.ts @@ -12,13 +12,14 @@ export function activateWebview( authProvider: AuthProvider ) { const projectProvider = new WebviewProvider(context, "project"); - const resourcesProvider = new ResourcesProvider(); + const modulesProvider = new WebviewProvider(context, "modules"); const userInfoProvider = new WebviewProvider( context, "userInfo", authProvider ); - + const resourcesProvider = new ResourcesProvider(); + const refresh = (uris?: readonly vscode.Uri[]) => { if ( uris === undefined || @@ -31,6 +32,7 @@ export function activateWebview( // Register views const providers = [ vscode.window.registerWebviewViewProvider("project", projectProvider), + vscode.window.registerWebviewViewProvider("modules", modulesProvider), vscode.window.registerWebviewViewProvider("userInfo", userInfoProvider), vscode.window.registerTreeDataProvider("resources", resourcesProvider) ]; @@ -41,8 +43,9 @@ export function activateWebview( // Register command vscode.commands.registerCommand("nextflow.seqera.reloadWebview", () => { - userInfoProvider.initViewData(true); refresh(); + modulesProvider.initViewData(); + userInfoProvider.initViewData(true); }); // Register events diff --git a/webview-ui/src/Context/index.tsx b/webview-ui/src/Context/index.tsx index fc42ff92..6678b7fa 100644 --- a/webview-ui/src/Context/index.tsx +++ b/webview-ui/src/Context/index.tsx @@ -29,7 +29,7 @@ export type AuthState = { error?: string; }; -type ViewID = "project" | "userInfo" | ""; +type ViewID = "project" | "modules" | "userInfo" | ""; const Context = ({ children }: Props) => { const viewID = window.initialData?.viewID as ViewID; diff --git a/webview-ui/src/Layout/Modules/index.tsx b/webview-ui/src/Layout/Modules/index.tsx new file mode 100644 index 00000000..88752830 --- /dev/null +++ b/webview-ui/src/Layout/Modules/index.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect } from "react"; +import { useWorkspaceContext } from "../../Context"; +import { getVscode } from "../../Context/utils"; +import Input from "../../components/Input"; +import Select from "../../components/Select"; +import styles from "./styles.module.css"; + +const COMPONENTS_JSON_URL = "https://raw.githubusercontent.com/nf-core/website/refs/heads/main/public/components.json"; + +type ModuleInfo = { + name: string; + path: string; + type: string; + meta: { + name: string; + description: string; + keywords: string[]; + tools: Array>; + input?: any[]; + output?: any[]; + authors?: string[]; + maintainers?: string[]; + }; + pipelines?: Array<{ name: string; version: string }>; +}; + +const Modules = () => { + const {} = useWorkspaceContext(); + const vscode = getVscode(); + const [modules, setModules] = useState([]); + const [search, setSearch] = useState(""); + const [sortBy, setSortBy] = useState("popularity"); + + useEffect(() => { + fetch(COMPONENTS_JSON_URL) + .then(response => response.json()) + .then(data => { + if (data.modules) { + setModules(data.modules); + } + }) + .catch(error => console.error("Error loading modules:", error)); + }, []); + + // Sort and filter modules + let filteredModules = modules; + + // Apply search filter + if (search) { + filteredModules = filteredModules.filter(module => + module.name.toLowerCase().includes(search.toLowerCase()) || + module.meta.description.toLowerCase().includes(search.toLowerCase()) || + module.meta.keywords.some(keyword => + keyword.toLowerCase().includes(search.toLowerCase()) + ) + ); + } + + // Apply sorting + filteredModules.sort((a, b) => { + if (sortBy === "popularity") { + const aCount = a.pipelines?.length || 0; + const bCount = b.pipelines?.length || 0; + return bCount - aCount; // Descending order + } else if (sortBy === "name") { + return a.name.localeCompare(b.name); + } + return 0; + }); + + const installModule = (name: string) => { + vscode.postMessage({ + command: "runCommand", + text: `nf-core modules install ${name}` + }); + }; + + const moduleView = (module: ModuleInfo) => ( +
+
+

{module.name}

+
+ + +
+
+ +

{module.meta.description}

+
+ ); + + return ( + <> +
+ setSearch(value)} + placeholder="Search modules" + /> +