Skip to content

Commit ff9562e

Browse files
committed
Add status widget
1 parent b141389 commit ff9562e

File tree

4 files changed

+274
-2
lines changed

4 files changed

+274
-2
lines changed

src/contents.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ export class Drive implements Contents.IDrive {
174174
Signal.clearData(this);
175175
}
176176

177+
get loadingContents(): ISignal<this, LoadingContentsArgs> {
178+
return this._loadingContents;
179+
}
180+
177181
/**
178182
* Get an encoded download url given a file path.
179183
*
@@ -217,7 +221,15 @@ export class Drive implements Contents.IDrive {
217221
): Promise<Contents.IModel> {
218222
let data: Contents.IModel;
219223

224+
this._loadingContents.emit({
225+
type: 'loading',
226+
path: localPath,
227+
driveName: this._name,
228+
itemType: 'directory'
229+
});
230+
220231
if (localPath !== '') {
232+
console.log('debug: get() called with localPath:', localPath);
221233
const currentDrive = extractCurrentDrive(localPath, this._drivesList);
222234

223235
// when accessed the first time, mount drive
@@ -305,6 +317,12 @@ export class Drive implements Contents.IDrive {
305317
}
306318

307319
Contents.validateContentsModel(data);
320+
this._loadingContents.emit({
321+
type: 'loaded',
322+
path: localPath,
323+
driveName: this._name,
324+
itemType: 'directory'
325+
});
308326
return data;
309327
}
310328

@@ -1052,8 +1070,16 @@ export class Drive implements Contents.IDrive {
10521070
private _isDisposed: boolean = false;
10531071
private _disposed = new Signal<this, void>(this);
10541072
private _registeredFileTypes: IRegisteredFileTypes = {};
1073+
private _loadingContents = new Signal<this, LoadingContentsArgs>(this);
10551074
}
10561075

1076+
type LoadingContentsArgs = {
1077+
type: 'loading' | 'loaded';
1078+
path: string;
1079+
driveName: string;
1080+
itemType: 'directory' | 'file';
1081+
};
1082+
10571083
export namespace Drive {
10581084
/**
10591085
* The options used to initialize a `Drive`.

src/plugins/driveBrowserPlugin.ts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
JupyterFrontEnd,
66
JupyterFrontEndPlugin
77
} from '@jupyterlab/application';
8+
import { IStatusBar } from '@jupyterlab/statusbar';
89
import {
910
IFileBrowserFactory,
1011
FileBrowser,
@@ -38,6 +39,127 @@ import { Drive } from '../contents';
3839
import { setListingLimit } from '../requests';
3940
import { CommandIDs } from '../token';
4041

42+
/**
43+
* Status bar widget for displaying drive information
44+
*/
45+
class DriveStatusWidget extends Widget {
46+
constructor() {
47+
super();
48+
this.addClass('jp-drive-status-widget');
49+
this.node.textContent = 'Drives: Ready';
50+
this._currentPath = '';
51+
this._isLoading = false;
52+
53+
// Listen for custom events from getContents
54+
window.addEventListener('drive-status-update', (event: Event) => {
55+
const customEvent = event as CustomEvent;
56+
this.handleStatusUpdate(customEvent.detail);
57+
});
58+
}
59+
60+
updateStatus(text: string) {
61+
this.node.textContent = `Drives: ${text}`;
62+
}
63+
64+
setDriveCount(count: number) {
65+
this.node.textContent = `Drives: ${count} connected`;
66+
}
67+
68+
setError(message: string) {
69+
this.node.textContent = `Drives: ${message}`;
70+
this.addClass('jp-drive-status-error');
71+
}
72+
73+
clearError() {
74+
this.removeClass('jp-drive-status-error');
75+
}
76+
77+
/**
78+
* Update status when navigating to a directory
79+
*/
80+
setDirectoryLoading(path: string) {
81+
console.log('[DEBUG] Setting directory loading:', path);
82+
this._isLoading = true;
83+
this._currentPath = path;
84+
const displayPath =
85+
path === '' ? 'Root' : path.split('/').pop() || 'Directory';
86+
this.node.textContent = `Drives: Opening ${displayPath}...`;
87+
this.addClass('jp-drive-status-loading');
88+
}
89+
90+
/**
91+
* Update status when a file is being opened
92+
*/
93+
setFileLoading(path: string) {
94+
console.log('[DEBUG] Setting file loading:', path);
95+
this._isLoading = true;
96+
this._currentPath = path;
97+
const fileName = path.split('/').pop() || 'File';
98+
this.node.textContent = `Drives: Opening ${fileName}...`;
99+
this.addClass('jp-drive-status-loading');
100+
}
101+
102+
/**
103+
* Clear loading state and show current status
104+
*/
105+
setLoaded(path: string, type: 'directory' | 'file' = 'directory') {
106+
this._isLoading = false;
107+
this._currentPath = path;
108+
this.removeClass('jp-drive-status-loading');
109+
110+
if (type === 'directory') {
111+
const displayPath =
112+
path === '' ? 'Root' : path.split('/').pop() || 'Directory';
113+
this.node.textContent = `Drives: ${displayPath}`;
114+
} else {
115+
const fileName = path.split('/').pop() || 'File';
116+
this.node.textContent = `Drives: ${fileName}`;
117+
}
118+
}
119+
120+
/**
121+
* Get current path
122+
*/
123+
get currentPath(): string {
124+
return this._currentPath;
125+
}
126+
127+
/**
128+
* Check if currently loading
129+
*/
130+
get isLoading(): boolean {
131+
return this._isLoading;
132+
}
133+
134+
/**
135+
* Handle status updates from getContents function
136+
*/
137+
private handleStatusUpdate(detail: any) {
138+
console.log('[DEBUG] Status update received:', detail);
139+
140+
if (detail.type === 'loading') {
141+
const fullPath = detail.driveName + '/' + detail.path;
142+
if (detail.path === '') {
143+
this.setDirectoryLoading('');
144+
} else {
145+
// Determine if it's a directory or file based on path
146+
const isDirectory = detail.path.endsWith('/') || detail.path === '';
147+
if (isDirectory) {
148+
this.setDirectoryLoading(fullPath);
149+
} else {
150+
this.setFileLoading(fullPath);
151+
}
152+
}
153+
} else if (detail.type === 'loaded') {
154+
const fullPath = detail.driveName + '/' + detail.path;
155+
this.setLoaded(fullPath, detail.itemType);
156+
}
157+
}
158+
159+
private _currentPath: string;
160+
private _isLoading: boolean;
161+
}
162+
41163
/**
42164
* The file browser factory ID.
43165
*/
@@ -75,7 +197,8 @@ export const driveFileBrowser: JupyterFrontEndPlugin<void> = {
75197
IRouter,
76198
JupyterFrontEnd.ITreeResolver,
77199
ILabShell,
78-
ILayoutRestorer
200+
ILayoutRestorer,
201+
IStatusBar
79202
],
80203
activate: async (
81204
app: JupyterFrontEnd,
@@ -86,7 +209,8 @@ export const driveFileBrowser: JupyterFrontEndPlugin<void> = {
86209
router: IRouter | null,
87210
tree: JupyterFrontEnd.ITreeResolver | null,
88211
labShell: ILabShell | null,
89-
restorer: ILayoutRestorer | null
212+
restorer: ILayoutRestorer | null,
213+
statusBar: IStatusBar | null
90214
): Promise<void> => {
91215
console.log(
92216
'JupyterLab extension jupyter-drives:drives-file-browser is activated!'
@@ -125,6 +249,42 @@ export const driveFileBrowser: JupyterFrontEndPlugin<void> = {
125249
restorer.add(driveBrowser, 'drive-file-browser');
126250
}
127251

252+
// Register status bar widget
253+
if (statusBar) {
254+
const driveStatusWidget = new DriveStatusWidget();
255+
256+
statusBar.registerStatusItem('driveBrowserStatus', {
257+
item: driveStatusWidget,
258+
align: 'right',
259+
rank: 500,
260+
isActive: () => true
261+
});
262+
263+
// Update status when drive browser is ready
264+
driveStatusWidget.updateStatus('Connected');
265+
266+
// Listen for drive changes and update status
267+
drive.loadingContents.connect((sender, args) => {
268+
const path = driveBrowser.model.path;
269+
console.log('[DEBUG] Path changed:', path, 'args:', args);
270+
if (args.type === 'loading') {
271+
driveStatusWidget.setDirectoryLoading(path);
272+
} else if (args.type === 'loaded') {
273+
console.log('loaded');
274+
// driveStatusWidget.setLoaded(path, args.itemType);
275+
}
276+
// Status updates are now handled by custom events from getContents
277+
});
278+
279+
// Listen for model changes to update drive count
280+
drive.loadingContents.connect(() => {
281+
console.log('[DEBUG] Model refreshed');
282+
// const items = driveBrowser.model.items();
283+
// const driveCount = Array.from(items).length;
284+
// driveStatusWidget.setDriveCount(driveCount);
285+
});
286+
}
287+
128288
const uploader = new Uploader({ model: driveBrowser.model, translator });
129289
toolbarRegistry.addFactory(FILE_BROWSER_FACTORY, 'uploader', () => {
130290
return uploader;

src/requests.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ export async function getContents(
8989
driveName: string,
9090
options: { path: string; registeredFileTypes: IRegisteredFileTypes }
9191
) {
92+
console.log(
93+
'debug: getContents() called with driveName:',
94+
driveName,
95+
'options:',
96+
options
97+
);
98+
99+
// Emit custom event for status widget to listen to
100+
const event = new CustomEvent('drive-status-update', {
101+
detail: {
102+
type: 'loading',
103+
path: options.path,
104+
driveName: driveName
105+
}
106+
});
107+
window.dispatchEvent(event);
108+
92109
const response = await requestAPI<any>(
93110
'drives/' + driveName + '/' + options.path,
94111
'GET'
@@ -99,6 +116,19 @@ export async function getContents(
99116
if (response.data) {
100117
// listing the contents of a directory
101118
if (isDir) {
119+
console.log('debug: isDir:', isDir);
120+
121+
// Emit event for directory loaded
122+
const loadedEvent = new CustomEvent('drive-status-update', {
123+
detail: {
124+
type: 'loaded',
125+
path: options.path,
126+
driveName: driveName,
127+
itemType: 'directory'
128+
}
129+
});
130+
window.dispatchEvent(loadedEvent);
131+
102132
const fileList: IContentsList = {};
103133

104134
response.data.forEach((row: any) => {
@@ -139,6 +169,19 @@ export async function getContents(
139169
}
140170
// getting the contents of a file
141171
else {
172+
console.log('debug: isFile:');
173+
174+
// Emit event for file loaded
175+
const loadedEvent = new CustomEvent('drive-status-update', {
176+
detail: {
177+
type: 'loaded',
178+
path: options.path,
179+
driveName: driveName,
180+
itemType: 'file'
181+
}
182+
});
183+
window.dispatchEvent(loadedEvent);
184+
142185
const [fileType, fileMimeType, fileFormat] = getFileType(
143186
PathExt.extname(PathExt.basename(options.path)),
144187
options.registeredFileTypes

style/base.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,46 @@ li {
157157
fill: var(--jp-ui-inverse-font-color0) !important;
158158
color: var(--jp-ui-inverse-font-color0) !important;
159159
}
160+
161+
/* Status bar widget styling */
162+
.jp-drive-status-widget {
163+
font-size: 12px;
164+
font-weight: 500;
165+
color: var(--jp-ui-font-color1);
166+
padding: 2px 8px;
167+
border-radius: 3px;
168+
background-color: var(--jp-layout-color2);
169+
border: 1px solid var(--jp-border-color1);
170+
transition: all 0.2s ease;
171+
}
172+
173+
.jp-drive-status-widget:hover {
174+
background-color: var(--jp-layout-color3);
175+
}
176+
177+
.jp-drive-status-error {
178+
color: var(--jp-error-color1);
179+
background-color: var(--jp-error-color0);
180+
border-color: var(--jp-error-color1);
181+
}
182+
183+
.jp-drive-status-loading {
184+
color: var(--jp-ui-font-color1);
185+
background-color: var(--jp-layout-color3);
186+
border-color: var(--jp-brand-color1);
187+
animation: pulse 1.5s ease-in-out infinite;
188+
}
189+
190+
@keyframes pulse {
191+
0% {
192+
opacity: 1;
193+
}
194+
195+
50% {
196+
opacity: 0.7;
197+
}
198+
199+
100% {
200+
opacity: 1;
201+
}
202+
}

0 commit comments

Comments
 (0)