// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import * as vscode from "vscode"; import { queryWorkspace } from "./workspaceActions"; import { log } from "qsharp-lang"; import { targetSupportQir } from "./providerProperties"; import { QuantumUris } from "./networkRequests"; // See docs at https://code.visualstudio.com/api/extension-guides/tree-view const pendingStatuses = [ "Waiting", "Queued", "Executing", "Finishing", "CancellationRequested", "Cancelling", ]; const noQirMsq = `Note: As this target does not currently support QIR, this VS Code extension cannot submit jobs to it. See https://aka.ms/qdk.qir for more info`; // Convert a date such as "2023-07-24T17:25:09.1309979Z" into local time function localDate(date: string) { return new Date(date).toLocaleString(); } export class WorkspaceTreeProvider implements vscode.TreeDataProvider { static instance: WorkspaceTreeProvider; private treeState: Map = new Map(); private didChangeTreeDataEmitter = new vscode.EventEmitter< WorkspaceTreeItem | undefined >(); readonly onDidChangeTreeData: vscode.Event = this.didChangeTreeDataEmitter.event; updateWorkspace(workspace: WorkspaceConnection) { this.treeState.set(workspace.id, workspace); this.didChangeTreeDataEmitter.fire(undefined); } removeWorkspace(workspaceId: string) { this.treeState.delete(workspaceId); this.didChangeTreeDataEmitter.fire(undefined); } async refreshWorkspace(workspace: WorkspaceConnection) { log.debug("In refreshWorkspace for workspace: ", workspace.id); await queryWorkspace(workspace); this.updateWorkspace(workspace); } getWorkspaceIds() { return [...this.treeState.keys()]; } getWorkspace(workspaceId: string) { return this.treeState.get(workspaceId); } hasWorkspace(workspaceId: string) { return this.treeState.has(workspaceId); } workspaceHasJob(workspaceId: string, jobId: string): boolean { const workspace = this.getWorkspace(workspaceId); if (!workspace) return false; return workspace.jobs.some((job) => job.id === jobId); } hasJobsPending(workspaceId: string): boolean { const workspace = this.getWorkspace(workspaceId); if (!workspace) return false; return workspace.jobs.some((job) => pendingStatuses.includes(job.status)); } getTreeItem( element: WorkspaceTreeItem, ): vscode.TreeItem | Thenable { return element; } getChildren( element?: WorkspaceTreeItem | undefined, ): vscode.ProviderResult { if (!element) { return [...this.treeState.values()].map( (workspace) => new WorkspaceTreeItem( workspace.name, workspace, "workspace", workspace, ), ); } else { return element.getChildren(); } } } export type WorkspaceConnection = { id: string; name: string; endpointUri: string; tenantId: string; subscriptionId?: string; offeringId?: string; apiKey?: string; providers: Provider[]; jobs: Job[]; lastRequestError?: string; }; export type Provider = { providerId: string; currentAvailability: "Available" | "Degraded" | "Unavailable"; targets: Target[]; }; export type Target = { id: string; providerId: string; currentAvailability: "Available" | "Degraded" | "Unavailable"; averageQueueTime: number; // minutes }; export type Job = { id: string; name: string; target: string; status: | "Waiting" | "Queued" | "Executing" | "Completed" | "Succeeded" | "Failed" | "Finishing" | "CancellationRequested" | "Cancelling" | "Cancelled"; outputDataUri?: string; count?: number; shots?: number; creationTime: string; beginExecutionTime?: string; endExecutionTime?: string; cancellationTime?: string; costEstimate?: any; errorData?: { code: string; message: string }; }; function shouldShowQueueTime(target: Target) { return ( target.currentAvailability !== "Unavailable" && typeof target.averageQueueTime === "number" ); // Could be 0 seconds } function getQueueTimeText(seconds: number): string { // If the queue time is less than 2 minute, show it in seconds if (seconds < 120) { return `${seconds} seconds`; } else if (seconds < 60 * 60 * 4) { // Otherwise, show it in minutes if less than 4 hours return `${Math.round(seconds / 60)} minutes`; } else { // Otherwise, show it in hours return `${Math.round(seconds / (60 * 60))} hours`; } } // A workspace has an array in properties.providers, each of which has a 'providerId' property, // e.g. 'ionq', and a 'provisioningState' property, e.g. 'Succeeded'. Filter the list to only // include those that have succeeded. Then, when querying the providerStatus, only add the targets // for the providers that are present. (Also, filter out providers that have no targets). export class WorkspaceTreeItem extends vscode.TreeItem { constructor( label: string, public workspace: WorkspaceConnection, public type: | "workspace" | "providerHeader" | "provider" | "target" | "jobHeader" | "job", public itemData: | WorkspaceConnection | Provider[] | Provider | Target[] | Target | Job[] | Job, ) { super(label, vscode.TreeItemCollapsibleState.Collapsed); this.contextValue = type; switch (type) { case "workspace": if (!this.workspace.lastRequestError) { this.iconPath = new vscode.ThemeIcon("notebook"); } else { this.iconPath = new vscode.ThemeIcon("alert"); this.tooltip = this.workspace.lastRequestError; } break; case "providerHeader": { break; } case "provider": { this.iconPath = new vscode.ThemeIcon("layers"); break; } case "target": { const target = itemData as Target; const supportsQir = targetSupportQir(target.id); if (supportsQir) { this.contextValue = "qir-target"; } this.iconPath = new vscode.ThemeIcon("package"); this.collapsibleState = vscode.TreeItemCollapsibleState.None; if (target.currentAvailability || target.averageQueueTime) { const hover = new vscode.MarkdownString( `${ target.currentAvailability ? `__Status__: ${target.currentAvailability}
` : "" } ${ shouldShowQueueTime(target) ? `__Queue time__: ${getQueueTimeText( target.averageQueueTime, )}
` : "" } ${supportsQir ? "" : "\n" + noQirMsq}`, ); hover.supportHtml = true; this.tooltip = hover; } break; } case "job": { const job = itemData as Job; this.collapsibleState = vscode.TreeItemCollapsibleState.None; const { isV2Workspace } = QuantumUris.parseEndpointUri( workspace.endpointUri, ); const v2Suffix = isV2Workspace ? "-v2" : ""; switch (job.status) { case "Executing": this.contextValue = `job-cancelable${v2Suffix}`; // falls through case "Finishing": this.iconPath = new vscode.ThemeIcon("run-all"); break; case "Queued": case "Waiting": this.iconPath = new vscode.ThemeIcon("loading~spin"); this.contextValue = `job-cancelable${v2Suffix}`; break; case "CancellationRequested": case "Cancelling": this.iconPath = new vscode.ThemeIcon("circle-slash"); break; case "Cancelled": this.iconPath = new vscode.ThemeIcon("circle-slash"); this.contextValue = `result${v2Suffix}`; break; case "Failed": this.iconPath = new vscode.ThemeIcon("error"); this.contextValue = `result${v2Suffix}`; break; case "Completed": case "Succeeded": this.iconPath = new vscode.ThemeIcon("pass"); this.contextValue = `result-download${v2Suffix}`; break; } // Tooltip const md = `__Created__: ${localDate(job.creationTime)}
__Target__: ${job.target}
__Status__: ${job.status}
${ job.beginExecutionTime ? `__Started__: ${localDate(job.beginExecutionTime)}
` : "" } ${ job.endExecutionTime ? `__Completed__: ${localDate(job.endExecutionTime)}
` : "" } ${ job.errorData ? `__Error__:
\n\`\`\`\n${job.errorData.code}: ${job.errorData.message}\n\`\`\`\n
` : "" } `; const hover = new vscode.MarkdownString(md); hover.supportHtml = true; this.tooltip = hover; break; } default: break; } } getChildren(): WorkspaceTreeItem[] { switch (this.type) { case "workspace": return [ new WorkspaceTreeItem( "Providers", this.workspace, "providerHeader", this.workspace.providers, ), new WorkspaceTreeItem( "Jobs", this.workspace, "jobHeader", this.workspace.jobs, ), ]; case "providerHeader": return (this.itemData as Provider[]).map( (provider) => new WorkspaceTreeItem( provider.providerId, this.workspace, "provider", provider, ), ); case "provider": return (this.itemData as Provider).targets.map( (target) => new WorkspaceTreeItem(target.id, this.workspace, "target", target), ); case "jobHeader": return (this.itemData as Job[]).map( (job) => new WorkspaceTreeItem( job.name || job.id, this.workspace, "job", job, ), ); case "target": case "job": default: return []; } } }