Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/extensions/agentic-workflows-dashboard/app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion .github/extensions/agentic-workflows-dashboard/extension.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { execFile } from "node:child_process/promises";
import { execFile as execFileCb } from "node:child_process";
import { promisify } from "node:util";
import { dirname, join, resolve } from "node:path";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";

const execFile = promisify(execFileCb);

import { createCanvas, joinSession } from "@github/copilot-sdk/extension";

import { createGhAwRunnerWithStatus, DEFAULT_LOG_TIMEOUT_MINUTES, DEFAULT_RUN_COUNT, createDashboardDataAccess } from "./app.js";
Expand Down Expand Up @@ -109,6 +112,7 @@ async function startServer() {
count: parseInt(reqUrl.searchParams.get("count") ?? String(DEFAULT_RUN_COUNT), 10),
window: reqUrl.searchParams.get("window") ?? "7d",
timeout: parseInt(reqUrl.searchParams.get("timeout") ?? String(DEFAULT_LOG_TIMEOUT_MINUTES), 10),
workflowName: reqUrl.searchParams.get("workflow_name") ?? "",
})
);
} else if (pathname === "/api/usage") {
Expand Down
23 changes: 21 additions & 2 deletions .github/extensions/agentic-workflows-dashboard/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface DashboardState {
reportWindows: ReportWindow[];
activeTab: DashboardTabId;
selectedWindow: ReportWindow["id"];
selectedWorkflowFilter: string;
logsTimeout: number;
pageSize: number;
cliStatus: CLIStatus | null;
Expand Down Expand Up @@ -76,6 +77,7 @@ interface DashboardState {
loadExperimentPage(page: number): void;
selectRun(runId: number): void;
viewRunDetails(runId: number): void;
selectWorkflowFilter(workflowName: string): Promise<void>;
loadAudit(): Promise<void>;
clearAudit(): void;
buildLogsCommand(count?: number): string;
Expand Down Expand Up @@ -212,6 +214,7 @@ Alpine.data("dashboardApp", (): DashboardState => ({
reportWindows,
activeTab: "definitions",
selectedWindow: "7d",
selectedWorkflowFilter: "",
logsTimeout: 1,
pageSize: 20,
cliStatus: null,
Expand Down Expand Up @@ -252,7 +255,7 @@ Alpine.data("dashboardApp", (): DashboardState => ({
this.commandInput = this.buildLogsCommand();
await this.fetchCliStatus();
if (this.cliStatus?.available) {
await Promise.all([this.fetchDefinitions(), this.fetchRuns(), this.fetchUsage(), this.fetchExperiments()]);
await Promise.all([this.fetchDefinitions(), this.fetchUsage(), this.fetchExperiments()]);
this.commandOutput = `$ ${this.cliStatus.command}\ngh aw version ${this.cliStatus.version}`;
return;
}
Expand All @@ -278,6 +281,13 @@ Alpine.data("dashboardApp", (): DashboardState => ({
await Promise.all([this.fetchRuns(), this.fetchUsage()]);
},

async selectWorkflowFilter(workflowName) {
this.selectedWorkflowFilter = workflowName;
this.commandInput = this.buildLogsCommand();
if (!this.cliStatus?.available) return;
await this.fetchRuns();
},

async fetchCliStatus() {
this.loadingCliStatus = true;
this.errorCliStatus = "";
Expand Down Expand Up @@ -305,6 +315,13 @@ Alpine.data("dashboardApp", (): DashboardState => ({
},

async fetchRuns() {
if (!this.selectedWorkflowFilter) {
this.runs = [];
this.runsMeta = null;
this.selectedRun = null;
this.loadRunPage(1);
return;
}
Comment on lines +318 to +324
this.loadingRuns = true;
this.errorRuns = "";
try {
Expand All @@ -313,6 +330,7 @@ Alpine.data("dashboardApp", (): DashboardState => ({
count: "100",
window: this.selectedWindow,
timeout: String(this.logsTimeout),
workflow_name: this.selectedWorkflowFilter,
});
const data = await fetchJson<RunsResponse>(`/api/runs?${params.toString()}`);
this.runsMeta = data;
Expand Down Expand Up @@ -459,7 +477,8 @@ Alpine.data("dashboardApp", (): DashboardState => ({

buildLogsCommand(count = DEFAULT_LOGS_COMMAND_COUNT) {
const window = this.currentWindow();
return `gh aw logs --json -c ${count} --start-date ${window.startDate} --timeout ${this.logsTimeout}`;
const workflowPart = this.selectedWorkflowFilter ? ` ${this.selectedWorkflowFilter}` : "";
return `gh aw logs --json -c ${count}${workflowPart} --start-date ${window.startDate} --timeout ${this.logsTimeout}`;
},

buildMaintenanceCommand(action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function combineOutput(stdout: string, stderr: string): string {

function spawnExecFile(file: string, args: string[], options: ExecOptions, callback: ExecCallback): void {
const { env, cwd, maxBuffer = 10 * 1024 * 1024 } = options ?? {};
const spawnOptions: SpawnOptions = { env, cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true };
const spawnOptions: SpawnOptions = { env, cwd, stdio: ["ignore", "pipe", "pipe"], windowsHide: true, detached: true };
console.error(`${LOG} spawn file=${file} args=${JSON.stringify(args)} cwd=${cwd}`);
const proc = spawn(file, args, spawnOptions);
const stdoutChunks: Buffer[] = [];
Expand Down
28 changes: 22 additions & 6 deletions .github/extensions/agentic-workflows-dashboard/web/app.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 22 additions & 5 deletions .github/extensions/agentic-workflows-dashboard/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,27 @@ <h2 class="Box-title">Workflow definitions <span class="Counter ml-1" x-text="de
<!-- Runs tab -->
<section class="mt-3" x-cloak x-show="isActiveTab('runs')">
<div class="Box">
<div class="Box-header d-flex flex-items-center flex-justify-between">
<div class="Box-header d-flex flex-items-center flex-justify-between flex-wrap gap-2">
<h2 class="Box-title">Workflow runs <span class="Counter ml-1" x-text="runsPaged.totalItems"></span></h2>
<span class="color-fg-muted text-small" x-text="buildReportSummaryMessage(runsMeta)"></span>
<div class="d-flex flex-items-center gap-2">
<select
class="form-select form-select-sm"
@change="selectWorkflowFilter($event.target.value)"
:disabled="loadingDefinitions || loadingRuns"
aria-label="Select workflow"
>
<option value="">— select a workflow —</option>
<template x-for="def in definitions" :key="def.workflow">
<option :value="def.workflow" :selected="selectedWorkflowFilter === def.workflow" x-text="def.workflow"></option>
</template>
</select>
<span class="color-fg-muted text-small" x-show="selectedWorkflowFilter" x-text="buildReportSummaryMessage(runsMeta)"></span>
</div>
</div>
<!-- no workflow selected -->
<div class="Box-body color-fg-muted text-center py-4" x-show="!selectedWorkflowFilter && !loadingDefinitions">
<div>Select a workflow above to load its runs.</div>
<div class="text-small mt-1">Filtering by workflow avoids downloading logs for all workflows at once.</div>
</div>
<!-- loading -->
<div class="Box-body p-0 awd-scroll" x-cloak x-show="loadingRuns">
Expand Down Expand Up @@ -142,21 +160,20 @@ <h2 class="Box-title">Workflow runs <span class="Counter ml-1" x-text="runsPaged
<!-- error -->
<div class="Box-body flash flash-error" x-cloak x-show="!loadingRuns && errorRuns" x-text="errorRuns"></div>
<!-- rows -->
<div class="Box-body p-0 awd-scroll" x-show="!loadingRuns && !errorRuns">
<div class="Box-body p-0 awd-scroll" x-show="selectedWorkflowFilter && !loadingRuns && !errorRuns">
<template x-for="run in runsPaged.items" :key="run.run_id">
<div class="Box-row awd-run-row" :class="selectedRun && selectedRun.run_id === run.run_id ? 'is-selected' : ''" @click="viewRunDetails(run.run_id)">
<div class="d-flex flex-items-center flex-justify-between gap-2">
<div class="d-flex flex-items-center gap-2 text-small">
<span class="text-bold">#<span x-text="run.run_id"></span></span>
<span class="color-fg-muted">· <span x-text="run.workflow_name"></span></span>
<span class="color-fg-muted" x-show="run.duration">· <span x-text="formatDuration(run.duration)"></span></span>
<span class="color-fg-muted">· <span x-text="formatAIC(run.aic ?? 0)"></span></span>
</div>
<span :class="runStatusClass(run)" x-text="runStatusLabel(run)"></span>
</div>
</div>
</template>
<div class="Box-row color-fg-muted text-center py-3" x-show="!loadingRuns && runsPaged.totalItems === 0">No runs found. Run <code>gh aw logs</code> to check availability.</div>
<div class="Box-row color-fg-muted text-center py-3" x-show="!loadingRuns && runsPaged.totalItems === 0">No runs found for this workflow in the selected window.</div>
</div>
<div class="Box-footer d-flex flex-items-center flex-justify-between" x-cloak x-show="!loadingRuns && runsPaged.totalPages > 1">
<button class="btn btn-sm" :disabled="!runsPaged.hasPreviousPage" @click="loadRunPage(runsPaged.page - 1)">Previous</button>
Expand Down
Loading