Skip to content

Commit

Permalink
Fix last used and favourite tracking issues: (#39)
Browse files Browse the repository at this point in the history
- fix last used not getting saved when kernel was created from dialog
- fix favourites not being updated when changed in another
  launcher/dialog
- fix last used not being updated for auto-started kernels
  • Loading branch information
krassowski authored May 20, 2024
1 parent 279ccb5 commit 31ee635
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 37 deletions.
29 changes: 25 additions & 4 deletions src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
// Distributed under the terms of the Modified BSD License.
import type { CommandRegistry } from '@lumino/commands';
import { ReadonlyJSONObject } from '@lumino/coreutils';
import type { ISignal } from '@lumino/signaling';
import { Time } from '@jupyterlab/coreutils';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { TranslationBundle } from '@jupyterlab/translation';
import { FilterBox, UseSignal, MenuSvg } from '@jupyterlab/ui-components';
import { Table } from './base-table';
import * as React from 'react';
import { ISettingsLayout, CommandIDs, IKernelItem } from '../types';
import {
ISettingsLayout,
IFavoritesDatabase,
ILastUsedDatabase,
CommandIDs,
IKernelItem
} from '../types';
import { starIcon } from '../icons';

const STAR_BUTTON_CLASS = 'jp-starIconButton';
Expand Down Expand Up @@ -78,6 +85,8 @@ export function KernelTable(props: {
onClick: (item: IKernelItem) => void;
hideColumns?: string[];
showWidgetType?: boolean;
favouritesChanged: ISignal<IFavoritesDatabase, void>;
lastUsedChanged: ISignal<ILastUsedDatabase, void>;
}) {
const { trans } = props;
let query: string;
Expand All @@ -94,6 +103,19 @@ export function KernelTable(props: {
// Hoisted to avoid "Rendered fewer hooks than expected" error on toggling the Star column
const [, forceUpdate] = React.useReducer(x => x + 1, 0);

React.useEffect(() => {
props.favouritesChanged.connect(forceUpdate);
return () => {
props.favouritesChanged.disconnect(forceUpdate);
};
});
React.useEffect(() => {
props.lastUsedChanged.connect(forceUpdate);
return () => {
props.lastUsedChanged.disconnect(forceUpdate);
};
});

const metadataAvailable = new Set<string>();
for (const item of props.items) {
const kernelMetadata = item.metadata?.kernel;
Expand Down Expand Up @@ -263,10 +285,9 @@ export function KernelTable(props: {
: STAR_BUTTON_CLASS
}
title={title}
onClick={event => {
row.toggleStar();
forceUpdate();
onClick={async event => {
event.stopPropagation();
await row.toggleStar();
}}
>
<starIcon.react className="jp-starIcon" />
Expand Down
15 changes: 13 additions & 2 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,19 @@ export class LastUsedDatabase
}

async recordAsUsedNow(item: ILauncher.IItemOptions) {
this._set(item, new Date().toUTCString());
await this.recordAsUsed(item, new Date());
}

async recordAsUsed(item: ILauncher.IItemOptions, date: Date) {
await this._set(item, date.toUTCString());
this._changed.emit();
}

get changed() {
return this._changed;
}

private _changed = new Signal<LastUsedDatabase, void>(this);
}

export class FavoritesDatabase
Expand All @@ -94,7 +105,7 @@ export class FavoritesDatabase
}

async set(item: ILauncher.IItemOptions, isFavourite: boolean) {
this._set(item, isFavourite);
await this._set(item, isFavourite);
this._changed.emit();
}

Expand Down
50 changes: 39 additions & 11 deletions src/dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class CustomSessionContextDialogs extends SessionContextDialogs {
trans,
acceptDialog: () => {
dialog.resolve(1);
}
},
type: sessionContext.type
}),
buttons,
checkbox: hasCheckbox
Expand Down Expand Up @@ -204,6 +205,11 @@ export class KernelSelector extends ReactWidget {
protected render(): React.ReactElement<any> | null {
const items: ILauncher.IItemOptions[] = [];
const specs = this.options.data.specs!.kernelspecs!;
// Note: this command is not executed, but it is only used to match favourite/last used metadata
const command =
this.options.type === 'console'
? 'console:create'
: 'notebook:create-new';

for (const spec of Object.values(specs)) {
if (!spec) {
Expand All @@ -212,11 +218,17 @@ export class KernelSelector extends ReactWidget {
const kernelIconUrl =
spec.resources['logo-svg'] || spec.resources['logo-64x64'];
items.push({
command: 'notebook:create-new',
args: {
isLauncher: true,
kernelName: spec.name
},
command,
args:
this.options.type === 'console'
? {
isLauncher: true,
kernelPreference: { name: spec.name }
}
: {
isLauncher: true,
kernelName: spec.name
},
kernelIconUrl,
metadata: {
kernel: JSONExt.deepCopy(spec.metadata || {}) as ReadonlyJSONValue
Expand All @@ -233,11 +245,17 @@ export class KernelSelector extends ReactWidget {
const kernelIconUrl =
spec.resources['logo-svg'] || spec.resources['logo-64x64'];
runningItems.push({
command: 'notebook:create-new',
args: {
isLauncher: true,
kernelName: spec.name
},
command,
args:
this.options.type === 'console'
? {
isLauncher: true,
kernelPreference: { name: spec.name }
}
: {
isLauncher: true,
kernelName: spec.name
},
kernelIconUrl,
metadata: {
kernel: {
Expand Down Expand Up @@ -268,6 +286,8 @@ export class KernelSelector extends ReactWidget {
this._selection = item;
this.options.acceptDialog();
}}
favouritesChanged={this._favoritesDatabase.changed}
lastUsedChanged={this._lastUsedDatabase.changed}
/>
{runningKernelsItems.length > 0 ? (
<>
Expand All @@ -286,6 +306,8 @@ export class KernelSelector extends ReactWidget {
this.options.acceptDialog();
}}
hideColumns={['last-used', 'star']}
favouritesChanged={this._favoritesDatabase.changed}
lastUsedChanged={this._lastUsedDatabase.changed}
/>
</>
) : null}
Expand All @@ -297,9 +319,13 @@ export class KernelSelector extends ReactWidget {
if (!this._selection) {
return null;
}
// Update last used date
this._selection.markAsUsedNow().catch(console.warn);
// User selected an existing kernel
if (this._selection.metadata?.model) {
return this._selection.metadata.model as unknown as Kernel.IModel;
}
// User starts a new kernel
return { name: this._selection.args!.kernelName as string };
}

Expand All @@ -321,5 +347,7 @@ export namespace KernelSelector {
data: SessionContext.IKernelSearch;
acceptDialog: () => void;
name: string;
// known values are "notebook" and "console"
type: string;
}
}
35 changes: 35 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,40 @@ function activate(
addCommands(app, trans, settings);
});

// Detect kernels started outside of the launcher, e.g. automatically due to having notebooks open from previous session
const lastSessionActivity: Record<string, string> = {};
const recordSessionActivity = async () => {
const models = [...app.serviceManager.sessions.running()];
for (const model of models) {
if (!model.kernel) {
continue;
}
let command: string;
if (model.type === 'notebook') {
command = 'notebook:create-new';
} else if (model.type === 'console') {
command = 'console:create';
} else {
command = 'unknown';
}
const key = command + '-' + model.kernel.name;
const activity = model.kernel.last_activity;
if (activity && lastSessionActivity[key] !== activity) {
lastSessionActivity[key] = activity;
const item = {
command,
args: {
isLauncher: true,
kernelName: model.kernel.name
}
};
await database.lastUsed.recordAsUsed(item, new Date(activity));
}
}
};
app.serviceManager.sessions.ready.then(recordSessionActivity);
app.serviceManager.sessions.runningChanged.connect(recordSessionActivity);

commands.addCommand(CommandIDs.create, {
label: trans.__('New Launcher'),
icon: args => (args.toolbar ? addIcon : undefined),
Expand All @@ -87,6 +121,7 @@ function activate(

const settings = await settingRegistry.load(MAIN_PLUGIN_ID);
await Promise.all([database.lastUsed.ready, database.favorites.ready]);

const launcher = new Launcher({
model,
cwd,
Expand Down
33 changes: 15 additions & 18 deletions src/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export class Item implements IItem {
caption: string;
icon: VirtualElement.IRenderer | undefined;
iconClass: string;
starred: boolean;

constructor(
private _options: {
Expand All @@ -32,8 +31,7 @@ export class Item implements IItem {
favoritesDatabase: IFavoritesDatabase;
}
) {
const { item, commands, lastUsedDatabase, favoritesDatabase, cwd } =
_options;
const { item, commands, cwd } = _options;
const args = { ...item.args, cwd };
// base
this.command = item.command;
Expand All @@ -47,8 +45,6 @@ export class Item implements IItem {
this.icon = commands.icon(item.command, args);
this.caption = commands.caption(item.command, args);
this.label = commands.label(item.command, args);
this.lastUsed = lastUsedDatabase.get(item);
this.starred = favoritesDatabase.get(item) ?? false;
// special handling for conda-store
// https://www.nebari.dev/docs/faq/#why-is-there-duplication-in-names-of-environments
const kernel = this.metadata['kernel'] as JSONObject | undefined;
Expand Down Expand Up @@ -79,12 +75,18 @@ export class Item implements IItem {
this.icon = codeServerIcon;
}
}
get starred() {
const { item, favoritesDatabase } = this._options;
return favoritesDatabase.get(item) ?? false;
}
get lastUsed(): Date | null {
return this._lastUsed;
const value = this._lastUsed;
this._setRefreshClock(value);
return value;
}
set lastUsed(value: Date | null) {
this._lastUsed = value;
this._setRefreshClock();
private get _lastUsed(): Date | null {
const { item, lastUsedDatabase } = this._options;
return lastUsedDatabase.get(item);
}
get refreshLastUsed(): ISignal<IItem, void> {
return this._refreshLastUsed;
Expand All @@ -93,24 +95,20 @@ export class Item implements IItem {
const { item, commands, lastUsedDatabase } = this._options;
await commands.execute(item.command, this.args);
await lastUsedDatabase.recordAsUsedNow(item);
this.lastUsed = lastUsedDatabase.get(item);
this._refreshLastUsed.emit();
}
async markAsUsed() {
async markAsUsedNow() {
const { item, lastUsedDatabase } = this._options;
await lastUsedDatabase.recordAsUsedNow(item);
this.lastUsed = lastUsedDatabase.get(item);
this._refreshLastUsed.emit();
}
toggleStar() {
async toggleStar() {
const { item, favoritesDatabase } = this._options;
const wasStarred = favoritesDatabase.get(item);
const newState = !wasStarred;
this.starred = newState;
return favoritesDatabase.set(item, newState);
}
private _setRefreshClock() {
const value = this._lastUsed;
private _setRefreshClock(value: Date | null) {
if (this._refreshClock !== null) {
window.clearTimeout(this._refreshClock);
this._refreshClock = null;
Expand All @@ -132,10 +130,9 @@ export class Item implements IItem {
: 60 * minute;
this._refreshClock = window.setTimeout(() => {
this._refreshLastUsed.emit();
this._setRefreshClock();
this._setRefreshClock(this._lastUsed);
}, interval);
}
private _refreshLastUsed = new Signal<Item, void>(this);
private _refreshClock: number | null = null;
private _lastUsed: Date | null = null;
}
10 changes: 9 additions & 1 deletion src/launcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ function LauncherBody(props: {
otherItems: IItem[];
commands: CommandRegistry;
settings: ISettingRegistry.ISettings;
favouritesChanged?: ISignal<IFavoritesDatabase, void>;
favouritesChanged: ISignal<IFavoritesDatabase, void>;
lastUsedChanged: ISignal<ILastUsedDatabase, void>;
}): React.ReactElement {
const { trans, cwd, typeItems, otherItems, favouritesChanged } = props;
const [query, updateQuery] = React.useState<string>('');
Expand Down Expand Up @@ -162,6 +163,8 @@ function LauncherBody(props: {
settings={props.settings}
trans={trans}
onClick={item => item.execute()}
favouritesChanged={props.favouritesChanged}
lastUsedChanged={props.lastUsedChanged}
/>
) : (
'No starred items'
Expand All @@ -182,6 +185,8 @@ function LauncherBody(props: {
settings={props.settings}
trans={trans}
onClick={item => item.execute()}
favouritesChanged={props.favouritesChanged}
lastUsedChanged={props.lastUsedChanged}
/>
</CollapsibleSection>
<CollapsibleSection
Expand All @@ -198,6 +203,8 @@ function LauncherBody(props: {
settings={props.settings}
trans={trans}
onClick={item => item.execute()}
favouritesChanged={props.favouritesChanged}
lastUsedChanged={props.lastUsedChanged}
/>
</CollapsibleSection>
</div>
Expand Down Expand Up @@ -326,6 +333,7 @@ export class NewLauncher extends Launcher {
otherItems={otherItems}
settings={this._settings}
favouritesChanged={this._favoritesDatabase.changed}
lastUsedChanged={this._lastUsedDatabase.changed}
/>
);
}
Expand Down
Loading

0 comments on commit 31ee635

Please sign in to comment.