Skip to content

Commit bc000f9

Browse files
committed
feat: update tray menu to watch for new/deleted/renamed runs
1 parent 2289f99 commit bc000f9

File tree

6 files changed

+77
-14
lines changed

6 files changed

+77
-14
lines changed

plugins/plugin-codeflare/src/tray/menus/profiles/dashboards/codeflare.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { CreateWindowFunction } from "@kui-shell/core"
2222
import runs from "../runs"
2323
import section from "../../section"
2424
import windowOptions from "../../../window"
25+
import UpdateFunction from "../../../update"
2526

2627
/** @return a new Window with a dashboard of the selected job run */
2728
function openRunInCodeflareDashboard(createWindow: CreateWindowFunction, profile: string, runId: string) {
@@ -36,7 +37,8 @@ function openRunInCodeflareDashboard(createWindow: CreateWindowFunction, profile
3637

3738
export default async function codeflareDashboards(
3839
profile: string,
39-
createWindow: CreateWindowFunction
40+
createWindow: CreateWindowFunction,
41+
updateFn: UpdateFunction
4042
): Promise<MenuItemConstructorOptions[]> {
4143
return [
4244
{
@@ -46,6 +48,6 @@ export default async function codeflareDashboards(
4648
title: "Codeflare Run Summary - " + profile,
4749
}),
4850
},
49-
...section("Recent Runs", await runs(profile, openRunInCodeflareDashboard.bind(undefined, createWindow))),
51+
...section("Recent Runs", await runs(profile, openRunInCodeflareDashboard.bind(undefined, createWindow), updateFn)),
5052
]
5153
}

plugins/plugin-codeflare/src/tray/menus/profiles/dashboards/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ import { MenuItemConstructorOptions } from "electron"
1818
import { CreateWindowFunction } from "@kui-shell/core"
1919

2020
import codeflare from "./codeflare"
21+
import UpdateFunction from "../../../update"
2122

2223
/** @return menu items that open dashboards for the given `profile` */
2324
export default async function dashboards(
2425
profile: string,
25-
createWindow: CreateWindowFunction
26+
createWindow: CreateWindowFunction,
27+
updateFn: UpdateFunction
2628
): Promise<MenuItemConstructorOptions[]> {
2729
const mlflow = { name: "MLFlow", portEnv: "MLFLOW_PORT" }
2830
const tensorboard = { name: "Tensorboard", portEnv: "TENSORBOARD_PORT" }
2931

3032
return [
31-
{ label: "CodeFlare", submenu: await codeflare(profile, createWindow) },
33+
{ label: "CodeFlare", submenu: await codeflare(profile, createWindow, updateFn) },
3234
{ label: "MLFlow", click: () => import("./open").then((_) => _.default(mlflow, profile, createWindow)) },
3335
{ label: "Tensorboard", click: () => import("./open").then((_) => _.default(tensorboard, profile, createWindow)) },
3436
]

plugins/plugin-codeflare/src/tray/menus/profiles/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import ProfileWatcher from "../../watchers/profile/list"
3030
/** @return a menu for the given `profile` */
3131
async function profileMenu(
3232
createWindow: CreateWindowFunction,
33-
updateFunction: UpdateFunction,
33+
updateFn: UpdateFunction,
3434
profileObj: Profiles.Profile
3535
): Promise<MenuItemConstructorOptions> {
3636
const profile = profileObj.name
@@ -39,8 +39,8 @@ async function profileMenu(
3939
label: profile,
4040
icon: profileIcon,
4141
submenu: [
42-
...section("Status", status(profile, updateFunction)),
43-
...section("Dashboards", await dashboards(profile, createWindow)),
42+
...section("Status", status(profile, updateFn)),
43+
...section("Dashboards", await dashboards(profile, createWindow, updateFn)),
4444
...section("Tasks", tasks(profile, createWindow)),
4545
],
4646
}
@@ -58,6 +58,9 @@ export default async function profilesMenu(
5858
watcher = new ProfileWatcher(updateFn, await Profiles.profilesPath({}, true))
5959
}
6060

61+
// one-time initialization of the watcher, if needed; we need to do
62+
// this after having assigned to our `watcher` variable, to avoid an
63+
// infinite loop
6164
await watcher.init()
6265

6366
// this will be a list of menu items, one per profile, and sorted by

plugins/plugin-codeflare/src/tray/menus/profiles/runs.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { MenuItemConstructorOptions } from "electron"
1818

19+
import UpdateFunction from "../../update"
1920
import ProfileRunWatcher, { RUNS_ERROR } from "../../watchers/profile/run"
2021

2122
/** Handler for "opening" the selected `runId` in the given `profile` */
@@ -34,11 +35,22 @@ export function runMenuItems(profile: string, open: RunOpener, runs: string[]):
3435
const watchers: Record<string, ProfileRunWatcher> = {}
3536

3637
/** @return menu items for the runs of the given profile */
37-
export default async function submenuForRuns(profile: string, open: RunOpener): Promise<MenuItemConstructorOptions[]> {
38+
export default async function submenuForRuns(
39+
profile: string,
40+
open: RunOpener,
41+
updateFn: UpdateFunction
42+
): Promise<MenuItemConstructorOptions[]> {
3843
if (!watchers[profile]) {
39-
watchers[profile] = await new ProfileRunWatcher(profile).init()
44+
watchers[profile] = await new ProfileRunWatcher(updateFn, profile)
4045
}
4146

47+
// one-time initialization of the watcher, if needed; we need to do
48+
// this after having assigned to our `watcher` variable, to avoid an
49+
// infinite loop
50+
await watchers[profile].init()
51+
4252
const { runs } = watchers[profile]
43-
return runs.length && runs[0] !== RUNS_ERROR ? runMenuItems(profile, open, runs) : [{ label: RUNS_ERROR }]
53+
return runs.length && runs[0] !== RUNS_ERROR
54+
? runMenuItems(profile, open, runs)
55+
: [{ label: RUNS_ERROR, enabled: false }]
4456
}

plugins/plugin-codeflare/src/tray/watchers/profile/list.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import UpdateFunction from "../../update"
2222

2323
/** Watch for new, removed, and renamed profiles */
2424
export default class ProfileWatcher {
25+
/** Our model */
2526
private _profiles: Profiles.Profile[] = []
2627

28+
/** Have we already performed the on-time init? */
2729
private _initDone = false
2830

2931
public constructor(

plugins/plugin-codeflare/src/tray/watchers/profile/run.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17+
import chokidar from "chokidar"
18+
import { basename } from "path"
19+
1720
import { readdir } from "fs/promises"
1821
import { Profiles } from "madwizard"
1922

23+
import UpdateFunction from "../../update"
24+
2025
export const RUNS_ERROR = "No runs found"
2126

2227
/**
@@ -25,16 +30,53 @@ export const RUNS_ERROR = "No runs found"
2530
* TODO make this actually watch
2631
*/
2732
export default class ProfileRunWatcher {
33+
/** Our model */
2834
private _runs: string[] = []
2935

30-
public constructor(private readonly profile: string) {}
36+
/** Have we already performed the on-time init? */
37+
private _initDone = false
38+
39+
public constructor(
40+
private readonly updateFn: UpdateFunction,
41+
private readonly profile: string,
42+
private readonly watcher = chokidar.watch(ProfileRunWatcher.path(profile))
43+
) {}
44+
45+
private static path(profile: string) {
46+
return Profiles.guidebookJobDataPath({ profile })
47+
}
3148

3249
/** Initialize `this._runs` model */
3350
public async init(): Promise<ProfileRunWatcher> {
34-
await this.readRunsDir()
51+
if (!this._initDone) {
52+
await this.readOnce()
53+
this.initWatcher()
54+
this._initDone = true
55+
}
3556
return this
3657
}
3758

59+
/** Initialize the filesystem watcher to notify us of new or removed profiles */
60+
private initWatcher() {
61+
this.watcher.on("add", async (path) => {
62+
const runId = basename(path)
63+
const idx = this.runs.findIndex((_) => _ === runId)
64+
if (idx < 0) {
65+
this._runs.push(runId)
66+
this.updateFn()
67+
}
68+
})
69+
70+
this.watcher.on("unlink", (path) => {
71+
const runId = basename(path)
72+
const idx = this.runs.findIndex((_) => _ === runId)
73+
if (idx >= 0) {
74+
this._runs.push(runId)
75+
this.updateFn()
76+
}
77+
})
78+
}
79+
3880
/** @return the current runs model */
3981
public get runs() {
4082
return this._runs
@@ -46,12 +88,12 @@ export default class ProfileRunWatcher {
4688
}
4789

4890
/** @return files of the directory of job runs for a given profile */
49-
private async readRunsDir() {
91+
private async readOnce() {
5092
try {
5193
// TODO do a "full" read with Dirents, so that we have filesystem
5294
// timestamps, and sort, so that the `.slice(0, 10)` below pulls
5395
// out the most recent runs
54-
this.runs = await readdir(Profiles.guidebookJobDataPath({ profile: this.profile }))
96+
this.runs = await readdir(ProfileRunWatcher.path(this.profile))
5597
} catch (err) {
5698
this.runs = [RUNS_ERROR]
5799
}

0 commit comments

Comments
 (0)