Skip to content

Commit 4499cf8

Browse files
Hua CaoHua Cao
authored andcommitted
add frontend panel and add docker instructions
1 parent 2918010 commit 4499cf8

File tree

11 files changed

+789
-428
lines changed

11 files changed

+789
-428
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,6 @@ dmypy.json
123123

124124
# Yarn cache
125125
.yarn/
126+
127+
# Test results
128+
junit.xml

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,32 @@ In development mode, you will also need to remove the symlink created by `jupyte
7272
command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
7373
folder is located. Then you can remove the symlink named `jupyter-secrets-manager` within that folder.
7474

75+
### Docker Development
76+
77+
In project root folder, use the following command to create a docker container with a volumn of this project.
78+
79+
```bash
80+
docker run -d --name jupyter-secrets-manager -p 8888:8888 -p 8000:8000 -v $(pwd):/workspace --user root quay.io/jupyter/base-notebook:latest jupyter lab --ip=0.0.0.0 --allow-root --no-browser --NotebookApp.token='my-token'
81+
```
82+
83+
then you could open localhost:8888 to see the jupyterlab page.
84+
85+
open a terminal in JupyterLab, run the following command to develop install jupyter secrets manager package
86+
87+
```bash
88+
cd /workspace
89+
pip install -e "."
90+
jupyter labextension develop . --overwrite
91+
```
92+
93+
then run the following in laptop terminal to restart the docker container
94+
95+
```bash
96+
docker stop jupyter-secrets-manager && docker start jupyter-secrets-manager
97+
```
98+
99+
After restart, you will see jupyter-secrets-manger is already installed, and your changes for python file will automatically take effect,for typescript change, you will need to either rebuild it by `jlpm build` or watch it by `jlpm watch`
100+
75101
### Testing the extension
76102

77103
#### Frontend tests

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"@jupyterlab/application": "^4.0.0",
6060
"@jupyterlab/statedb": "^4.0.0",
6161
"@lumino/algorithm": "^2.0.0",
62-
"@lumino/coreutils": "^2.1.2"
62+
"@lumino/coreutils": "^2.1.2",
63+
"@lumino/widgets": "^2.0.0"
6364
},
6465
"devDependencies": {
6566
"@jupyterlab/builder": "^4.0.0",

src/components/SecretsPanel.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { ReactWidget } from '@jupyterlab/apputils';
3+
4+
import '../../style/base.css';
5+
import { ISecret, ISecretsManager } from '../token';
6+
7+
interface ISecretsPanelProps {
8+
manager: ISecretsManager;
9+
}
10+
11+
const EyeIcon = () => (
12+
<svg viewBox="0 0 24 24" fill="currentColor">
13+
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
14+
</svg>
15+
);
16+
17+
const EyeOffIcon = () => (
18+
<svg viewBox="0 0 24 24" fill="currentColor">
19+
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
20+
</svg>
21+
);
22+
23+
const SecretsPanel: React.FC<ISecretsPanelProps> = ({ manager }) => {
24+
const [secrets, setSecrets] = useState<ISecret[]>([]);
25+
const [isLoading, setIsLoading] = useState(true);
26+
const [namespaces, setNamespaces] = useState<string[]>([]);
27+
const [currentNamespace, setCurrentNamespace] = useState('default');
28+
const [visibleSecrets, setVisibleSecrets] = useState<Set<string>>(new Set());
29+
30+
const fetchNamespaces = async () => {
31+
try {
32+
const namespacesList = await manager.listNamespaces();
33+
if (namespacesList) {
34+
setNamespaces(namespacesList);
35+
}
36+
} catch (error) {
37+
console.error('Error fetching namespaces:', error);
38+
}
39+
};
40+
41+
const fetchSecrets = async () => {
42+
try {
43+
const secretsList = await manager.list(currentNamespace);
44+
if (secretsList) {
45+
setSecrets(secretsList.values);
46+
}
47+
} catch (error) {
48+
console.error('Error fetching secrets:', error);
49+
} finally {
50+
setIsLoading(false);
51+
}
52+
};
53+
54+
useEffect(() => {
55+
fetchNamespaces();
56+
}, []);
57+
58+
useEffect(() => {
59+
fetchSecrets();
60+
}, [currentNamespace]);
61+
62+
const toggleSecretVisibility = (secretId: string) => {
63+
setVisibleSecrets(prev => {
64+
const newSet = new Set(prev);
65+
if (newSet.has(secretId)) {
66+
newSet.delete(secretId);
67+
} else {
68+
newSet.add(secretId);
69+
}
70+
return newSet;
71+
});
72+
};
73+
74+
if (isLoading) {
75+
return <div>Loading secrets...</div>;
76+
}
77+
78+
return (
79+
<div className="jp-SecretsPanel">
80+
<div className="jp-SecretsPanel-header">
81+
<h2>Secrets Manager</h2>
82+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
83+
<select
84+
className="jp-SecretsPanel-namespace-select"
85+
value={currentNamespace}
86+
onChange={e => setCurrentNamespace(e.target.value)}
87+
>
88+
{namespaces.map(namespace => (
89+
<option key={namespace} value={namespace}>
90+
{namespace}
91+
</option>
92+
))}
93+
</select>
94+
</div>
95+
</div>
96+
97+
<div className="jp-SecretsPanel-content">
98+
{secrets.length === 0 ? (
99+
<div className="jp-SecretsPanel-empty">No secrets found.</div>
100+
) : (
101+
<ul className="jp-SecretsPanel-list">
102+
{secrets.map(secret => (
103+
<li key={secret.id} className="jp-SecretsPanel-list-item">
104+
<span className="jp-SecretsPanel-secret-name">{secret.id}</span>
105+
<span
106+
className={`jp-SecretsPanel-secret-value ${!visibleSecrets.has(secret.id) ? 'hidden' : ''}`}
107+
>
108+
{visibleSecrets.has(secret.id) ? secret.value : '••••••••'}
109+
</span>
110+
<button
111+
className="jp-SecretsPanel-eye-button"
112+
onClick={() => toggleSecretVisibility(secret.id)}
113+
title={
114+
visibleSecrets.has(secret.id)
115+
? 'Hide secret'
116+
: 'Show secret'
117+
}
118+
>
119+
{visibleSecrets.has(secret.id) ? <EyeIcon /> : <EyeOffIcon />}
120+
</button>
121+
</li>
122+
))}
123+
</ul>
124+
)}
125+
</div>
126+
</div>
127+
);
128+
};
129+
130+
export interface ISecretsManagerWidgetOptions {
131+
manager: ISecretsManager;
132+
}
133+
134+
export class SecretsManagerWidget extends ReactWidget {
135+
constructor(options: ISecretsManagerWidgetOptions) {
136+
super();
137+
this.addClass('jp-SecretsManagerWidget');
138+
this._manager = options.manager;
139+
}
140+
141+
render(): JSX.Element {
142+
return <SecretsPanel manager={this._manager} />;
143+
}
144+
145+
private _manager: ISecretsManager;
146+
}

src/connectors/in-memory.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ export class InMemoryConnector implements ISecretsConnector {
2323
const ids: string[] = [];
2424
const values: ISecret[] = [];
2525
this._secrets.forEach((value, key) => {
26-
if (value.namespace === query) {
27-
ids.push(key);
28-
values.push(value);
26+
if (query && value.namespace !== query) {
27+
return;
2928
}
29+
ids.push(key);
30+
values.push(value);
3031
});
3132
return { ids, values };
3233
}

src/connectors/local-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ passwords are stored as plain text in the local storage of the browser'
4040
const secrets = JSON.parse(localStorage.getItem(this.storage) ?? '{}');
4141
const initialValue: ISecretsList = { ids: [], values: [] };
4242
return Object.keys(secrets)
43-
.filter(key => secrets[key].namespace === query)
43+
.filter(key => !query || secrets[key].namespace === query)
4444
.reduce((acc, cur) => {
4545
acc.ids.push(cur);
4646
acc.values.push(secrets[cur]);

src/index.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
import { SecretsManager } from './manager';
66
import { ISecretsConnector, ISecretsManager } from './token';
77
import { InMemoryConnector } from './connectors';
8+
import { SecretsManagerWidget } from './components/SecretsPanel';
9+
import { Panel } from '@lumino/widgets';
10+
import { lockIcon } from '@jupyterlab/ui-components';
811

912
/**
1013
* A basic secret connector extension, that should be disabled to provide a new
@@ -34,7 +37,41 @@ const manager: JupyterFrontEndPlugin<ISecretsManager> = {
3437
connector: ISecretsConnector
3538
): ISecretsManager => {
3639
console.log('JupyterLab extension jupyter-secrets-manager is activated!');
37-
return new SecretsManager({ connector });
40+
const secretsManager = new SecretsManager({ connector });
41+
const panel = new Panel();
42+
panel.id = 'jupyter-secrets-manager:panel';
43+
panel.title.icon = lockIcon;
44+
const secretsManagerWidget = new SecretsManagerWidget({
45+
manager: secretsManager
46+
});
47+
panel.addWidget(secretsManagerWidget);
48+
app.shell.add(panel, 'left');
49+
50+
secretsManager.set('default', 'test', {
51+
id: 'test',
52+
value: 'test',
53+
namespace: 'default'
54+
});
55+
56+
secretsManager.set('default', 'test2', {
57+
id: 'test2',
58+
value: 'test2',
59+
namespace: 'default'
60+
});
61+
62+
secretsManager.set('default2', 'test', {
63+
id: 'test3',
64+
value: 'test3',
65+
namespace: 'default2'
66+
});
67+
68+
secretsManager.set('default3', 'test', {
69+
id: 'test4',
70+
value: 'test4',
71+
namespace: 'default3'
72+
});
73+
74+
return secretsManager;
3875
}
3976
};
4077

src/manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ export class SecretsManager implements ISecretsManager {
5050
return this._set(Private.buildSecretId(namespace, id), secret);
5151
}
5252

53+
async listNamespaces(): Promise<string[]> {
54+
if (!this._connector.list) {
55+
return [];
56+
}
57+
await this._ready.promise;
58+
const secrets = await this._connector.list();
59+
const namespaces = Array.from(
60+
new Set(secrets.ids.map(id => id.split(':')[0]))
61+
);
62+
return namespaces;
63+
}
64+
5365
/**
5466
* List the secrets for a namespace as a ISecretsList.
5567
*/

src/token.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export interface ISecretsManager {
7070
* Detach all attached input for a namespace.
7171
*/
7272
detachAll(namespace: string): Promise<void>;
73+
/**
74+
* List all namespaces.
75+
*/
76+
listNamespaces(): Promise<string[]>;
7377
}
7478

7579
/**

0 commit comments

Comments
 (0)