Skip to content

Modules view #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
{
Expand Down Expand Up @@ -301,6 +301,11 @@
"name": "Project",
"type": "webview"
},
{
"id": "modules",
"name": "Modules",
"type": "webview"
},
{
"id": "userInfo",
"name": "Seqera Cloud",
Expand Down
19 changes: 18 additions & 1 deletion src/webview/WebviewProvider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -72,6 +75,9 @@ class WebviewProvider implements vscode.WebviewViewProvider {
if (!workspaceId) return;
this.fetchComputeEnvs(workspaceId);
break;
case "runCommand":
this.runCommand(message.text);
break;
}
});

Expand Down Expand Up @@ -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;

Expand Down
9 changes: 6 additions & 3 deletions src/webview/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand All @@ -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)
];
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/Context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
138 changes: 138 additions & 0 deletions webview-ui/src/Layout/Modules/index.tsx
Original file line number Diff line number Diff line change
@@ -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<Record<string, any>>;
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<ModuleInfo[]>([]);
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) => (
<div key={module.name} className={styles.moduleCard}>
<div className={styles.moduleHeader}>
<h3>{module.name}</h3>
<div className={styles.actions}>
<button
className={styles.installButton}
onClick={() => installModule(module.name)}
title="Install this module"
>
Install
</button>
<button
className={styles.docsButton}
onClick={() => {
const url = `https://nf-co.re/modules/${module.name}`;
vscode.postMessage({
command: "openExternal",
url
});
}}
title="View documentation"
>
Docs
</button>
</div>
</div>

<p className={styles.description}>{module.meta.description}</p>
</div>
);

return (
<>
<div className={styles.filters}>
<Input
value={search}
onChange={(value) => setSearch(value)}
placeholder="Search modules"
/>
<Select
options={[
{ label: "Sort by popularity", value: "popularity" },
{ label: "Sort by name", value: "name" }
]}
value={sortBy}
onChange={(value) => setSortBy(value as string)}
/>
</div>
{filteredModules.length == 0 ? (
<section className="cozy">No modules found</section>
) : (
<div className={styles.modulesList}>
{filteredModules.map(moduleView)}
</div>
)}
</>
);
};

export default Modules;
94 changes: 94 additions & 0 deletions webview-ui/src/Layout/Modules/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.filters {
display: flex;
gap: 10px;
padding: 10px;
border-bottom: 1px solid var(--vscode-dropdown-border);
}

.modulesList {
padding: 10px;
display: flex;
flex-direction: column;
gap: 12px;
}

.moduleCard {
padding: 8px 10px;
border: 1px solid var(--vscode-dropdown-border);
border-radius: 4px;
background-color: var(--vscode-editor-background);
}

.moduleHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
gap: 8px;
}

.moduleHeader h3 {
margin: 0;
font-size: 16px;
color: var(--vscode-editor-foreground);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
}

.description {
margin: 4px 0 8px;
font-size: 14px;
color: var(--vscode-descriptionForeground);
}

.moduleDetails {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}

.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}

.tag {
background-color: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
}

.popularity {
color: var(--vscode-descriptionForeground);
font-size: 12px;
}

.actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}

.installButton, .docsButton {
background-color: transparent;
border: 1px solid var(--vscode-button-border);
color: var(--vscode-button-foreground);
font-size: 12px;
padding: 4px 8px;
cursor: pointer;
border-radius: 3px;
}

.installButton {
background-color: var(--vscode-button-background);
}

.installButton:hover, .docsButton:hover {
opacity: 0.8;
}
2 changes: 2 additions & 0 deletions webview-ui/src/Layout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { useWorkspaceContext } from "../Context";
import Cloud from "./Cloud";
import Modules from "./Modules";
import Project from "./Project";

const Layout = () => {
Expand All @@ -25,6 +26,7 @@ const Layout = () => {
}, [nodes, retryCount]);

if (viewID === "userInfo") return <Cloud />;
if (viewID === "modules") return <Modules />;
if (viewID === "project") return <Project />;
return null;
};
Expand Down