UNPKG

@jupyterlab/services

Version:

Client APIs for the Jupyter services REST APIs

336 lines (289 loc) 8 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { URLExt } from '@jupyterlab/coreutils'; import { JSONExt, PartialJSONObject, ReadonlyJSONObject } from '@lumino/coreutils'; import { Poll } from '@lumino/polling'; import { ISignal, Signal } from '@lumino/signaling'; import { ServerConnection } from '../serverconnection'; import { BaseManager, IManager as IBaseManager } from '../basemanager'; /** * The url for the lab workspaces service. */ const SERVICE_USER_URL = 'api/me'; /** * The service's ID. * Used to uniquely identify the poll, and * the item in local storage. */ const SERVICE_ID = '@jupyterlab/services:UserManager#user'; /** * The user API service manager. */ export class UserManager extends BaseManager implements User.IManager { private _isReady = false; private _ready: Promise<void>; private _pollSpecs: Poll; private _identity: User.IIdentity; private _permissions: ReadonlyJSONObject; private _userChanged = new Signal<this, User.IUser>(this); private _connectionFailure = new Signal<this, Error>(this); /** * Create a new user manager. */ constructor(options: UserManager.IOptions = {}) { super(options); // Initialize internal data. this._ready = this.requestUser() .then(() => { if (this.isDisposed) { return; } this._isReady = true; }) .catch( _ => // Return a promise that will never resolve, so user service is never ready // This typically occurs when the backend has no user service new Promise(() => { // no-op }) ); this._pollSpecs = new Poll({ auto: false, factory: () => this.requestUser(), frequency: { interval: 61 * 1000, backoff: true, max: 300 * 1000 }, name: SERVICE_ID, standby: options.standby ?? 'when-hidden' }); void this.ready.then(() => { void this._pollSpecs.start(); }); } /** * The server settings for the manager. */ readonly serverSettings: ServerConnection.ISettings; /** * Test whether the manager is ready. */ get isReady(): boolean { return this._isReady; } /** * A promise that fulfills when the manager is ready. */ get ready(): Promise<void> { return this._ready; } /** * Get the most recently fetched identity. */ get identity(): User.IIdentity | null { return this._identity; } /** * Get the most recently fetched permissions. */ get permissions(): ReadonlyJSONObject | null { return this._permissions; } /** * A signal emitted when the user changes. */ get userChanged(): ISignal<this, User.IUser> { return this._userChanged; } /** * A signal emitted when there is a connection failure. */ get connectionFailure(): ISignal<this, Error> { return this._connectionFailure; } /** * Dispose of the resources used by the manager. */ dispose(): void { this._pollSpecs.dispose(); super.dispose(); } /** * Force a refresh of the specs from the server. * * @returns A promise that resolves when the specs are fetched. * * #### Notes * This is intended to be called only in response to a user action, * since the manager maintains its internal state. */ async refreshUser(): Promise<void> { await this._pollSpecs.refresh(); await this._pollSpecs.tick; } /** * Execute a request to the server to poll the user and update state. */ protected async requestUser(): Promise<void> { if (this.isDisposed) { return; } const { baseUrl } = this.serverSettings; const { makeRequest, ResponseError } = ServerConnection; const url = URLExt.join(baseUrl, SERVICE_USER_URL); const response: Response = await makeRequest(url, {}, this.serverSettings); if (response.status !== 200) { const err = await ResponseError.create(response); throw err; } const oldUser = { identity: this._identity, permissions: this._permissions }; const newUser = await response.json(); const identity = newUser.identity; // store the color and initials for the user // this info is not provided by the server const { localStorage } = window; const data = localStorage.getItem(SERVICE_ID); if (data && (!identity.initials || !identity.color)) { const localUser = JSON.parse(data); identity.initials = identity.initials || localUser.initials || identity.name.substring(0, 1); identity.color = identity.color || localUser.color || Private.getRandomColor(); } if (!JSONExt.deepEqual(newUser, oldUser)) { this._identity = identity; this._permissions = newUser.permissions; localStorage.setItem(SERVICE_ID, JSON.stringify(identity)); this._userChanged.emit(newUser); } } } /** * A namespace for `UserManager` statics. */ export namespace UserManager { /** * The instantiation options for a user manager. */ export interface IOptions extends BaseManager.IOptions { /** * When the manager stops polling the API. Defaults to `when-hidden`. */ standby?: Poll.Standby | (() => boolean | Poll.Standby); } } /** * A namespace for user API interfaces. */ export namespace User { /** * The interface describing a user identity. */ export interface IUser { readonly identity: IIdentity; readonly permissions: PartialJSONObject; } /** * The interface describing a user identity. */ export interface IIdentity extends PartialJSONObject { /** * User's unique identifier. */ readonly username: string; /** * User's full name. */ readonly name: string; /** * Shorter version of the name for displaying it on the UI. */ readonly display_name: string; /** * User's name initials. */ readonly initials: string; /** * User's cursor color and icon color if avatar_url is undefined * (there is no image). */ readonly color: string; /** * User's avatar url. * The url to the user's image for the icon. */ readonly avatar_url?: string; } /** * Object which manages user's identity. * * #### Notes * The manager is responsible for maintaining the state of the user. */ export interface IManager extends IBaseManager { /** * A signal emitted when the user changes. */ userChanged: ISignal<this, User.IUser>; /** * User's identity. * * #### Notes * The value will be null until the manager is ready. */ readonly identity: User.IIdentity | null; /** * User's permissions. * * #### Notes * The value will be null until the manager is ready. */ readonly permissions: ReadonlyJSONObject | null; /** * Force a refresh of user's identity from the server. * * @returns A promise that resolves when the identity is fetched. * * #### Notes * This is intended to be called only in response to a user action, * since the manager maintains its internal state. */ refreshUser(): Promise<void>; } } /** * A namespace for module-private functionality. * * Note: We do not want to export this function * to move it to css variables in the Theme. */ namespace Private { /** * Predefined colors for users */ const userColors = [ 'var(--jp-collaborator-color1)', 'var(--jp-collaborator-color2)', 'var(--jp-collaborator-color3)', 'var(--jp-collaborator-color4)', 'var(--jp-collaborator-color5)', 'var(--jp-collaborator-color6)', 'var(--jp-collaborator-color7)' ]; /** * Get a random color from the list of colors. */ export const getRandomColor = (): string => userColors[Math.floor(Math.random() * userColors.length)]; }