Skip to content

Commit

Permalink
Using the secrets manager
Browse files Browse the repository at this point in the history
  • Loading branch information
brichet committed Mar 6, 2025
1 parent c7d428d commit 9c45c15
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 6 deletions.
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IFormRendererRegistry } from '@jupyterlab/ui-components';
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
import { ISecretsManager } from 'jupyter-secrets-manager';

Check failure on line 21 in src/index.ts

View workflow job for this annotation

GitHub Actions / check_release

Cannot find module 'jupyter-secrets-manager' or its corresponding type declarations.

import { ChatHandler } from './chat-handler';
import { CompletionProvider } from './completion-provider';
Expand Down Expand Up @@ -153,19 +154,20 @@ const aiProviderPlugin: JupyterFrontEndPlugin<IAIProvider> = {
id: '@jupyterlite/ai:ai-provider',
autoStart: true,
requires: [IFormRendererRegistry, ISettingRegistry],
optional: [IRenderMimeRegistry],
optional: [IRenderMimeRegistry, ISecretsManager],
provides: IAIProvider,
activate: (
app: JupyterFrontEnd,
editorRegistry: IFormRendererRegistry,
settingRegistry: ISettingRegistry,
rmRegistry?: IRenderMimeRegistry
rmRegistry?: IRenderMimeRegistry,
secretsManager?: ISecretsManager
): IAIProvider => {
const aiProvider = new AIProvider();

editorRegistry.addRenderer(
'@jupyterlite/ai:ai-provider.AIprovider',
aiSettingsRenderer({ rmRegistry })
aiSettingsRenderer({ rmRegistry, secretsManager })
);
settingRegistry
.load(aiProviderPlugin.id)
Expand Down
70 changes: 67 additions & 3 deletions src/settings/panel.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { FormComponent, IFormRenderer } from '@jupyterlab/ui-components';
import { ArrayExt } from '@lumino/algorithm';
import { JSONExt } from '@lumino/coreutils';
import { IChangeEvent } from '@rjsf/core';
import type { FieldProps } from '@rjsf/utils';
import validator from '@rjsf/validator-ajv8';
import { JSONSchema7 } from 'json-schema';
import { ISecretsManager } from 'jupyter-secrets-manager';

Check failure on line 10 in src/settings/panel.tsx

View workflow job for this annotation

GitHub Actions / check_release

Cannot find module 'jupyter-secrets-manager' or its corresponding type declarations.
import React from 'react';

import { IDict, instructions } from './instructions';
import baseSettings from './schemas/base.json';
import ProviderSettings from './schemas';

const SECRETS_NAMESPACE = '@jupyterlite/ai';
const MD_MIME_TYPE = 'text/markdown';
const STORAGE_NAME = '@jupyterlite/ai:settings';
const INSTRUCTION_CLASS = 'jp-AISettingsInstructions';

export const aiSettingsRenderer = (options: {
rmRegistry?: IRenderMimeRegistry;
secretsManager?: ISecretsManager;
}): IFormRenderer => {
return {
fieldRenderer: (props: FieldProps) => {
Expand All @@ -43,6 +47,7 @@ export class AiSettings extends React.Component<
constructor(props: FieldProps) {
super(props);
this._rmRegistry = props.formContext.rmRegistry ?? null;
this._secretsManager = props.formContext.secretsManager ?? null;
this._settings = props.formContext.settings;

// Initialize the providers schema.
Expand Down Expand Up @@ -91,6 +96,48 @@ export class AiSettings extends React.Component<
.catch(console.error);
}

async componentWillUpdate(): Promise<void> {
if (!this._secretsManager) {
return;
}
const inputs = this._formRef.current?.getElementsByTagName('input') || [];
for (let i = 0; i < inputs.length; i++) {
if (inputs[i].type.toLowerCase() === 'password') {
(await this._secretsManager.list(SECRETS_NAMESPACE)).forEach(id =>

Check failure on line 106 in src/settings/panel.tsx

View workflow job for this annotation

GitHub Actions / check_release

Parameter 'id' implicitly has an 'any' type.
this._secretsManager?.detach(SECRETS_NAMESPACE, id)
);
}
}
}

componentDidUpdate(): void {
if (!this._secretsManager) {
return;
}
// Attach the password inputs to the secrets manager only if they have changed.
const inputs = this._formRef.current?.getElementsByTagName('input') || [];
if (ArrayExt.shallowEqual(inputs, this._formInputs)) {
return;
}
this._formInputs = [...inputs];
this._unsavedFields = [];
for (let i = 0; i < inputs.length; i++) {
if (inputs[i].type.toLowerCase() === 'password') {
const label = inputs[i].getAttribute('label');
if (label) {
const id = `${this._provider}-${label}`;
this._secretsManager.attach(
SECRETS_NAMESPACE,
id,
inputs[i],
(value: string) => this._onPasswordUpdated(label, value)
);
this._unsavedFields.push(label);
}
}
}
}

/**
* Get the current provider from the local storage.
*/
Expand Down Expand Up @@ -120,8 +167,10 @@ export class AiSettings extends React.Component<
* Save settings in local storage for a given provider.
*/
saveSettings(value: IDict<any>) {
const currentSettings = { ...value };
const settings = JSON.parse(localStorage.getItem(STORAGE_NAME) ?? '{}');
settings[this._provider] = value;
this._unsavedFields.forEach(field => delete currentSettings[field]);
settings[this._provider] = currentSettings;
localStorage.setItem(STORAGE_NAME, JSON.stringify(settings));
}

Expand Down Expand Up @@ -198,6 +247,17 @@ export class AiSettings extends React.Component<
.catch(console.error);
};

/**
* Callback function called when the password input has been programmatically updated
* with the secret manager.
*/
private _onPasswordUpdated = (fieldName: string, value: string) => {
this._currentSettings[fieldName] = value;
this._settings
.set('AIprovider', { provider: this._provider, ...this._currentSettings })
.catch(console.error);
};

/**
* Triggered when the form value has changed, to update the current settings and save
* it in local storage.
Expand All @@ -213,7 +273,7 @@ export class AiSettings extends React.Component<

render(): JSX.Element {
return (
<>
<div ref={this._formRef}>
<WrappedFormComponent
formData={{ provider: this._provider }}
schema={this._providerSchema}
Expand All @@ -235,14 +295,18 @@ export class AiSettings extends React.Component<
onChange={this._onFormChange}
uiSchema={this._uiSchema}
/>
</>
</div>
);
}

private _provider: string;
private _providerSchema: JSONSchema7;
private _rmRegistry: IRenderMimeRegistry | null;
private _secretsManager: ISecretsManager | null;
private _currentSettings: IDict<any> = { provider: 'None' };
private _uiSchema: IDict<any> = {};
private _settings: ISettingRegistry.ISettings;
private _formRef = React.createRef<HTMLDivElement>();
private _unsavedFields: string[] = [];
private _formInputs: HTMLInputElement[] = [];
}

0 comments on commit 9c45c15

Please sign in to comment.