Skip to content

Commit 2e6df6b

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

File tree

7 files changed

+755
-424
lines changed

7 files changed

+755
-424
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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Button } from '@jupyterlab/ui-components';
3+
import { ReactWidget } from '@jupyterlab/apputils';
4+
5+
import '../../style/base.css';
6+
import { ISecret, ISecretsManager } from '../token';
7+
8+
interface ISecretsPanelProps {
9+
manager: ISecretsManager;
10+
}
11+
12+
interface IAddSecretFormProps {
13+
onSubmit: (name: string, value: string) => Promise<void>;
14+
onCancel: () => void;
15+
}
16+
17+
const AddSecretForm: React.FC<IAddSecretFormProps> = ({
18+
onSubmit,
19+
onCancel
20+
}) => {
21+
const [newSecretName, setNewSecretName] = useState('');
22+
const [newSecretValue, setNewSecretValue] = useState('');
23+
24+
const handleSubmit = async () => {
25+
if (!newSecretName || !newSecretValue) {
26+
return;
27+
}
28+
await onSubmit(newSecretName, newSecretValue);
29+
setNewSecretName('');
30+
setNewSecretValue('');
31+
};
32+
33+
return (
34+
<div className="jp-SecretsPanel-add-form">
35+
<div className="jp-SecretsPanel-add-form-inputs">
36+
<input
37+
type="text"
38+
placeholder="Secret name"
39+
value={newSecretName}
40+
onChange={e => setNewSecretName(e.target.value)}
41+
/>
42+
<input
43+
type="password"
44+
placeholder="Secret value"
45+
value={newSecretValue}
46+
onChange={e => setNewSecretValue(e.target.value)}
47+
/>
48+
</div>
49+
<div className="jp-SecretsPanel-add-form-buttons">
50+
<Button onClick={handleSubmit}>Add</Button>
51+
<Button onClick={onCancel}>Cancel</Button>
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
const SecretsPanel: React.FC<ISecretsPanelProps> = ({ manager }) => {
58+
const [secrets, setSecrets] = useState<ISecret[]>([]);
59+
const [isLoading, setIsLoading] = useState(true);
60+
const [isAddingSecret, setIsAddingSecret] = useState(false);
61+
62+
const fetchSecrets = async () => {
63+
try {
64+
const secretsList = await manager.list('default');
65+
if (secretsList) {
66+
setSecrets(secretsList.values);
67+
}
68+
} catch (error) {
69+
console.error('Error fetching secrets:', error);
70+
} finally {
71+
setIsLoading(false);
72+
}
73+
};
74+
75+
useEffect(() => {
76+
fetchSecrets();
77+
}, []);
78+
79+
const handleRemoveSecret = async (secretId: string) => {
80+
try {
81+
await manager.remove('default', secretId);
82+
setSecrets(secrets.filter(secret => secret.id !== secretId));
83+
} catch (error) {
84+
console.error('Error removing secret:', error);
85+
}
86+
};
87+
88+
const handleAddSecret = async (name: string, value: string) => {
89+
try {
90+
const secret: ISecret = {
91+
namespace: 'default',
92+
id: name,
93+
value: value
94+
};
95+
await manager.set('default', name, secret);
96+
await fetchSecrets();
97+
setIsAddingSecret(false);
98+
} catch (error) {
99+
console.error('Error adding secret:', error);
100+
}
101+
};
102+
103+
const handleCancelAdd = () => {
104+
setIsAddingSecret(false);
105+
};
106+
107+
if (isLoading) {
108+
return <div>Loading secrets...</div>;
109+
}
110+
111+
return (
112+
<div className="jp-SecretsPanel">
113+
<div className="jp-SecretsPanel-header">
114+
<h2>Secrets Manager</h2>
115+
</div>
116+
117+
<div className="jp-SecretsPanel-content">
118+
{secrets.length === 0 ? (
119+
<div className="jp-SecretsPanel-empty">
120+
No secrets found. Click "Add Secret" to create one.
121+
</div>
122+
) : (
123+
<ul className="jp-SecretsPanel-list">
124+
{secrets.map(secret => (
125+
<li key={secret.id} className="jp-SecretsPanel-list-item">
126+
<span className="jp-SecretsPanel-secret-name">{secret.id}</span>
127+
<Button
128+
className="jp-SecretsPanel-remove-button"
129+
onClick={() => handleRemoveSecret(secret.id)}
130+
>
131+
Remove
132+
</Button>
133+
</li>
134+
))}
135+
</ul>
136+
)}
137+
<Button
138+
className="jp-SecretsPanel-add-button"
139+
onClick={() => setIsAddingSecret(true)}
140+
>
141+
Add Secret
142+
</Button>
143+
</div>
144+
145+
{isAddingSecret && (
146+
<AddSecretForm onSubmit={handleAddSecret} onCancel={handleCancelAdd} />
147+
)}
148+
</div>
149+
);
150+
};
151+
152+
export interface ISecretsManagerWidgetOptions {
153+
manager: ISecretsManager;
154+
}
155+
156+
export class SecretsManagerWidget extends ReactWidget {
157+
constructor(options: ISecretsManagerWidgetOptions) {
158+
super();
159+
this.addClass('jp-SecretsManagerWidget');
160+
this._manager = options.manager;
161+
}
162+
163+
render(): JSX.Element {
164+
return <SecretsPanel manager={this._manager} />;
165+
}
166+
167+
private _manager: ISecretsManager;
168+
}

src/index.ts

Lines changed: 19 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 { LabIcon } from '@jupyterlab/ui-components';
811

912
/**
1013
* A basic secret connector extension, that should be disabled to provide a new
@@ -20,6 +23,12 @@ const inMemoryConnector: JupyterFrontEndPlugin<ISecretsConnector> = {
2023
}
2124
};
2225

26+
const logoIcon = new LabIcon({
27+
name: 'jupyter-secrets-manager:icon',
28+
// TODO: replace with a proper icon
29+
svgstr: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M13 6V4a5 5 0 0 0-10 0v2a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2zM5 4a3 3 0 1 1 6 0v2H5V4zm3 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></svg>'
30+
});
31+
2332
/**
2433
* The secret manager extension.
2534
*/
@@ -34,7 +43,16 @@ const manager: JupyterFrontEndPlugin<ISecretsManager> = {
3443
connector: ISecretsConnector
3544
): ISecretsManager => {
3645
console.log('JupyterLab extension jupyter-secrets-manager is activated!');
37-
return new SecretsManager({ connector });
46+
const secretsManager = new SecretsManager({ connector });
47+
const panel = new Panel();
48+
panel.id = 'jupyter-secrets-manager:panel';
49+
panel.title.icon = logoIcon;
50+
const secretsManagerWidget = new SecretsManagerWidget({
51+
manager: secretsManager
52+
});
53+
panel.addWidget(secretsManagerWidget);
54+
app.shell.add(panel, 'left');
55+
return secretsManager;
3856
}
3957
};
4058

style/base.css

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,109 @@
33
44
https://jupyterlab.readthedocs.io/en/stable/developer/css.html
55
*/
6+
7+
.jp-SecretsPanel {
8+
display: flex;
9+
flex-direction: column;
10+
height: 100%;
11+
background: var(--jp-layout-color1);
12+
color: var(--jp-ui-font-color1);
13+
}
14+
15+
.jp-SecretsPanel-header {
16+
padding: 8px 12px;
17+
border-bottom: 1px solid var(--jp-border-color1);
18+
}
19+
20+
.jp-SecretsPanel-header h2 {
21+
margin: 0;
22+
font-size: 14px;
23+
font-weight: 600;
24+
}
25+
26+
.jp-SecretsPanel-content {
27+
flex: 1;
28+
overflow-y: auto;
29+
padding: 8px 12px;
30+
}
31+
32+
.jp-SecretsPanel-empty {
33+
text-align: center;
34+
color: var(--jp-ui-font-color2);
35+
padding: 20px 0;
36+
}
37+
38+
.jp-SecretsPanel-list {
39+
list-style: none;
40+
padding: 0;
41+
margin: 0;
42+
}
43+
44+
.jp-SecretsPanel-list-item {
45+
display: flex;
46+
justify-content: space-between;
47+
align-items: center;
48+
padding: 8px 0;
49+
border-bottom: 1px solid var(--jp-border-color1);
50+
}
51+
52+
.jp-SecretsPanel-secret-name {
53+
font-size: 13px;
54+
color: var(--jp-ui-font-color1);
55+
}
56+
57+
.jp-SecretsPanel-remove-button {
58+
min-width: 80px;
59+
padding: 4px 8px;
60+
font-size: 12px;
61+
}
62+
63+
.jp-SecretsPanel-add-button {
64+
width: 100%;
65+
margin-top: 12px;
66+
padding: 8px;
67+
font-size: 13px;
68+
background: var(--jp-brand-color1);
69+
color: white;
70+
border: none;
71+
border-radius: 4px;
72+
cursor: pointer;
73+
}
74+
75+
.jp-SecretsPanel-add-button:hover {
76+
background: var(--jp-brand-color0);
77+
}
78+
79+
.jp-SecretsPanel-add-form {
80+
display: flex;
81+
flex-direction: column;
82+
gap: 8px;
83+
padding: 12px;
84+
background: var(--jp-layout-color2);
85+
border-radius: 4px;
86+
margin-bottom: 12px;
87+
}
88+
89+
.jp-SecretsPanel-add-form-inputs {
90+
display: flex;
91+
gap: 8px;
92+
width: 100%;
93+
}
94+
95+
.jp-SecretsPanel-add-form-inputs input {
96+
flex: 1;
97+
padding: 8px;
98+
border: 1px solid var(--jp-border-color1);
99+
border-radius: 4px;
100+
font-size: 13px;
101+
min-width: 0; /* Prevents flex items from overflowing */
102+
}
103+
104+
.jp-SecretsPanel-add-form-buttons {
105+
display: flex;
106+
gap: 8px;
107+
}
108+
109+
.jp-SecretsPanel-add-form-buttons button {
110+
flex: 1;
111+
}

0 commit comments

Comments
 (0)