diff --git a/README.md b/README.md
index 97a826b..2df4ef6 100644
--- a/README.md
+++ b/README.md
@@ -47,12 +47,13 @@ The available settings for this service are:
- `default_cpu_limit`: Default CPU limit of a user server; defaults to `None`
- `machine_profiles`: Instead of entering directly the CPU and Memory value, `tljh-repo2docker` can be configured with pre-defined machine profiles and users can only choose from the available option; defaults to `[]`
-Here is an example of registering `tljh_repo2docker`'s service with JupyterHub
+This service requires the following scopes : `read:users`, `admin:servers` and `read:roles:users`. Here is an example of registering `tljh_repo2docker`'s service with JupyterHub
```python
# jupyterhub_config.py
from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE
+import sys
c.JupyterHub.services.extend(
[
@@ -77,7 +78,7 @@ c.JupyterHub.load_roles = [
{
"description": "Role for tljh_repo2docker service",
"name": "tljh-repo2docker-service",
- "scopes": ["read:users", "read:servers", "read:roles:users"],
+ "scopes": ["read:users", "admin:servers", "read:roles:users"],
"services": ["tljh_repo2docker"],
},
{
@@ -99,6 +100,7 @@ Here is an example of the configuration
# jupyterhub_config.py
from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE
+import sys
c.JupyterHub.services.extend(
[
diff --git a/jupyterhub_config.py b/jupyterhub_config.py
index ec27fd9..05be037 100644
--- a/jupyterhub_config.py
+++ b/jupyterhub_config.py
@@ -14,6 +14,7 @@
tljh_custom_jupyterhub_config(c)
+
c.JupyterHub.authenticator_class = DummyAuthenticator
c.JupyterHub.allow_named_servers = True
@@ -37,8 +38,7 @@
"--machine_profiles",
'{"label": "Medium", "cpu": 4, "memory": 4}',
"--machine_profiles",
- '{"label": "Large", "cpu": 8, "memory": 8}'
-
+ '{"label": "Large", "cpu": 8, "memory": 8}',
],
"oauth_no_confirm": True,
"oauth_client_allowed_scopes": [
@@ -58,11 +58,11 @@
{
"description": "Role for tljh_repo2docker service",
"name": "tljh-repo2docker-service",
- "scopes": ["read:users", "read:servers", "read:roles:users"],
+ "scopes": ["read:users", "read:roles:users", "admin:servers"],
"services": ["tljh_repo2docker"],
},
{
- "name": 'tljh-repo2docker-service-admin',
+ "name": "tljh-repo2docker-service-admin",
"users": ["alice"],
"scopes": [TLJH_R2D_ADMIN_SCOPE],
},
diff --git a/src/common/AxiosContext.tsx b/src/common/AxiosContext.tsx
index 7fffa60..15fdd81 100644
--- a/src/common/AxiosContext.tsx
+++ b/src/common/AxiosContext.tsx
@@ -2,9 +2,8 @@ import { createContext, useContext } from 'react';
import { AxiosClient } from './axiosclient';
export const AxiosContext = createContext<{
- hubClient: AxiosClient;
serviceClient: AxiosClient;
-}>({ hubClient: new AxiosClient({}), serviceClient: new AxiosClient({}) });
+}>({ serviceClient: new AxiosClient({}) });
export const useAxios = () => {
return useContext(AxiosContext);
diff --git a/src/common/ButtonWithConfirm.tsx b/src/common/ButtonWithConfirm.tsx
index 97557c8..df7946a 100644
--- a/src/common/ButtonWithConfirm.tsx
+++ b/src/common/ButtonWithConfirm.tsx
@@ -3,9 +3,8 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
-import CircularProgress from '@mui/material/CircularProgress';
-import Box from '@mui/material/Box';
import { Fragment, memo, useCallback, useState } from 'react';
+import { Loading } from './LoadingAnimation';
interface IButtonWithConfirm {
buttonLabel: string;
@@ -15,11 +14,7 @@ interface IButtonWithConfirm {
okLabel?: string;
cancelLabel?: string;
}
-const Loading = () => (
-
-
-
-);
+
function _ButtonWithConfirm(props: IButtonWithConfirm) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
diff --git a/src/common/LoadingAnimation.tsx b/src/common/LoadingAnimation.tsx
new file mode 100644
index 0000000..9d35aae
--- /dev/null
+++ b/src/common/LoadingAnimation.tsx
@@ -0,0 +1,7 @@
+import CircularProgress from '@mui/material/CircularProgress';
+import Box from '@mui/material/Box';
+export const Loading = () => (
+
+
+
+);
diff --git a/src/common/axiosclient.ts b/src/common/axiosclient.ts
index 3be8759..2d3c60e 100644
--- a/src/common/axiosclient.ts
+++ b/src/common/axiosclient.ts
@@ -2,8 +2,6 @@ import urlJoin from 'url-join';
import { encodeUriComponents } from './utils';
import axios, { AxiosInstance } from 'axios';
-export const API_PREFIX = 'api';
-export const SPAWN_PREFIX = 'spawn';
export class AxiosClient {
constructor(options: AxiosClient.IOptions) {
this._baseUrl = options.baseUrl ?? '';
@@ -15,15 +13,15 @@ export class AxiosClient {
async request(args: {
method: 'get' | 'post' | 'put' | 'option' | 'delete';
- prefix: 'api' | 'spawn';
path: string;
query?: string;
data?: { [key: string]: any } | FormData;
+ params?: { [key: string]: string };
}): Promise {
- const { method, path } = args;
-
+ const { method, path, params } = args;
+ const prefix = 'api';
const data = args.data ?? {};
- let url = urlJoin(args.prefix, encodeUriComponents(path));
+ let url = urlJoin(prefix, encodeUriComponents(path));
if (args.query) {
const sep = url.indexOf('?') === -1 ? '?' : '&';
url = `${url}${sep}${args.query}`;
@@ -35,7 +33,8 @@ export class AxiosClient {
const response = await this._axios.request({
method,
url,
- data
+ data,
+ params
});
return response.data;
}
diff --git a/src/environments/App.tsx b/src/environments/App.tsx
index 587cfd3..6a3779c 100644
--- a/src/environments/App.tsx
+++ b/src/environments/App.tsx
@@ -20,12 +20,6 @@ export interface IAppProps {
export default function App(props: IAppProps) {
const jhData = useJupyterhub();
- const hubClient = useMemo(() => {
- const baseUrl = jhData.hubPrefix;
- const xsrfToken = jhData.xsrfToken;
- return new AxiosClient({ baseUrl, xsrfToken });
- }, [jhData]);
-
const serviceClient = useMemo(() => {
const baseUrl = jhData.servicePrefix;
const xsrfToken = jhData.xsrfToken;
@@ -34,7 +28,7 @@ export default function App(props: IAppProps) {
return (
-
+
void;
+ loading?: boolean;
}
function _EnvironmentList(props: IEnvironmentListProps) {
@@ -111,6 +112,7 @@ function _EnvironmentList(props: IEnvironmentListProps) {
return (
{
const response = await axios.serviceClient.request({
method: 'delete',
- prefix: API_PREFIX,
path: ENV_PREFIX,
data: { name: props.image }
});
diff --git a/src/servers/App.tsx b/src/servers/App.tsx
index 4eff3c2..d5b4f51 100644
--- a/src/servers/App.tsx
+++ b/src/servers/App.tsx
@@ -22,12 +22,6 @@ export interface IAppProps {
export default function App(props: IAppProps) {
const jhData = useJupyterhub();
- const hubClient = useMemo(() => {
- const baseUrl = jhData.hubPrefix;
- const xsrfToken = jhData.xsrfToken;
- return new AxiosClient({ baseUrl, xsrfToken });
- }, [jhData]);
-
const serviceClient = useMemo(() => {
const baseUrl = jhData.servicePrefix;
const xsrfToken = jhData.xsrfToken;
@@ -36,7 +30,7 @@ export default function App(props: IAppProps) {
return (
-
+
('');
const handleOpen = () => {
setOpen(true);
@@ -65,24 +67,22 @@ function _NewServerDialog(props: INewServerDialogProps) {
const createServer = useCallback(async () => {
const imageName = props.images[rowSelectionModel[0] as number].image_name;
- const data = new FormData();
- data.append('image', imageName);
- let path = '';
- if (serverName.length > 0) {
- path = `${jhData.user}/${serverName}`;
- } else {
- path = jhData.user;
- }
+ const data: { [key: string]: string } = {
+ imageName,
+ userName: jhData.user,
+ serverName
+ };
try {
- await axios.hubClient.request({
+ setLoading(true);
+ await axios.serviceClient.request({
method: 'post',
- prefix: SPAWN_PREFIX,
- path,
+ path: SERVER_PREFIX,
data
});
window.location.reload();
} catch (e: any) {
- console.error(e);
+ setLoading(false);
+ alert(e);
}
}, [serverName, rowSelectionModel, props.images, axios, jhData]);
const disabled = useMemo(() => {
@@ -106,6 +106,7 @@ function _NewServerDialog(props: INewServerDialogProps) {