Skip to content

Commit

Permalink
Start single server from backend (#81)
Browse files Browse the repository at this point in the history
* Start server from backend

* Start and delete server via service

* Split server api handler

* Handle missing `ContainerConfig` key

* Remove unused hub client

* Update README
  • Loading branch information
trungleduc authored Apr 3, 2024
1 parent 466f6fb commit 094e463
Show file tree
Hide file tree
Showing 22 changed files with 140 additions and 149 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[
Expand All @@ -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"],
},
{
Expand All @@ -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(
[
Expand Down
8 changes: 4 additions & 4 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

tljh_custom_jupyterhub_config(c)


c.JupyterHub.authenticator_class = DummyAuthenticator

c.JupyterHub.allow_named_servers = True
Expand All @@ -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": [
Expand All @@ -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],
},
Expand Down
3 changes: 1 addition & 2 deletions src/common/AxiosContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 2 additions & 7 deletions src/common/ButtonWithConfirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,11 +14,7 @@ interface IButtonWithConfirm {
okLabel?: string;
cancelLabel?: string;
}
const Loading = () => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
);

function _ButtonWithConfirm(props: IButtonWithConfirm) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
Expand Down
7 changes: 7 additions & 0 deletions src/common/LoadingAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
export const Loading = () => (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
);
13 changes: 6 additions & 7 deletions src/common/axiosclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '';
Expand All @@ -15,15 +13,15 @@ export class AxiosClient {

async request<T = any>(args: {
method: 'get' | 'post' | 'put' | 'option' | 'delete';
prefix: 'api' | 'spawn';
path: string;
query?: string;
data?: { [key: string]: any } | FormData;
params?: { [key: string]: string };
}): Promise<T> {
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}`;
Expand All @@ -35,7 +33,8 @@ export class AxiosClient {
const response = await this._axios.request<T>({
method,
url,
data
data,
params
});
return response.data;
}
Expand Down
8 changes: 1 addition & 7 deletions src/environments/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,7 +28,7 @@ export default function App(props: IAppProps) {

return (
<ThemeProvider theme={customTheme}>
<AxiosContext.Provider value={{ hubClient, serviceClient }}>
<AxiosContext.Provider value={{ serviceClient }}>
<ScopedCssBaseline>
<Stack sx={{ padding: 1 }} spacing={1}>
<NewEnvironmentDialog
Expand Down
2 changes: 2 additions & 0 deletions src/environments/EnvironmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export interface IEnvironmentListProps {
selectable?: boolean;
rowSelectionModel?: GridRowSelectionModel;
setRowSelectionModel?: (selected: GridRowSelectionModel) => void;
loading?: boolean;
}

function _EnvironmentList(props: IEnvironmentListProps) {
Expand All @@ -111,6 +112,7 @@ function _EnvironmentList(props: IEnvironmentListProps) {
return (
<Box sx={{ padding: 1 }}>
<DataGrid
loading={Boolean(props.loading)}
rows={rows}
columns={columns}
initialState={{
Expand Down
2 changes: 0 additions & 2 deletions src/environments/NewEnvironmentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from '@mui/material';
import { Fragment, memo, useCallback, useMemo, useState } from 'react';

import { API_PREFIX } from '../common/axiosclient';
import { useAxios } from '../common/AxiosContext';
import { SmallTextField } from '../common/SmallTextField';
import { ENV_PREFIX } from './types';
Expand Down Expand Up @@ -174,7 +173,6 @@ function _NewEnvironmentDialog(props: INewEnvironmentDialogProps) {
data.password = data.password ?? '';
const response = await axios.serviceClient.request({
method: 'post',
prefix: API_PREFIX,
path: ENV_PREFIX,
data
});
Expand Down
2 changes: 0 additions & 2 deletions src/environments/RemoveEnvironmentButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { memo, useCallback } from 'react';
import { useAxios } from '../common/AxiosContext';
import { ButtonWithConfirm } from '../common/ButtonWithConfirm';
import { ENV_PREFIX } from './types';
import { API_PREFIX } from '../common/axiosclient';

interface IRemoveEnvironmentButton {
name: string;
Expand All @@ -18,7 +17,6 @@ function _RemoveEnvironmentButton(props: IRemoveEnvironmentButton) {
const removeEnv = useCallback(async () => {
const response = await axios.serviceClient.request({
method: 'delete',
prefix: API_PREFIX,
path: ENV_PREFIX,
data: { name: props.image }
});
Expand Down
8 changes: 1 addition & 7 deletions src/servers/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,7 +30,7 @@ export default function App(props: IAppProps) {

return (
<ThemeProvider theme={customTheme}>
<AxiosContext.Provider value={{ hubClient, serviceClient }}>
<AxiosContext.Provider value={{ serviceClient }}>
<ScopedCssBaseline>
<Stack sx={{ padding: 1 }} spacing={1}>
<NewServerDialog
Expand Down
29 changes: 16 additions & 13 deletions src/servers/NewServerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import { IEnvironmentData } from '../environments/types';
import { SmallTextField } from '../common/SmallTextField';

import { useAxios } from '../common/AxiosContext';
import { SPAWN_PREFIX } from '../common/axiosclient';
import { useJupyterhub } from '../common/JupyterhubContext';
import { SERVER_PREFIX } from './types';

export interface INewServerDialogProps {
images: IEnvironmentData[];
allowNamedServers: boolean;
Expand All @@ -36,6 +37,7 @@ function _NewServerDialog(props: INewServerDialogProps) {
const axios = useAxios();
const jhData = useJupyterhub();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [serverName, setServerName] = useState<string>('');
const handleOpen = () => {
setOpen(true);
Expand Down Expand Up @@ -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(() => {
Expand All @@ -106,6 +106,7 @@ function _NewServerDialog(props: INewServerDialogProps) {
</Box>
<Dialog open={open} onClose={handleClose} fullWidth maxWidth={'md'}>
<DialogTitle>Server Options</DialogTitle>

<DialogContent>
{props.allowNamedServers && (
<Box sx={{ padding: 1 }}>
Expand All @@ -131,8 +132,10 @@ function _NewServerDialog(props: INewServerDialogProps) {
selectable
rowSelectionModel={rowSelectionModel}
setRowSelectionModel={updateSelectedRow}
loading={loading}
/>
</DialogContent>

<DialogActions>
<Button variant="contained" color="error" onClick={handleClose}>
Cancel
Expand Down
Loading

0 comments on commit 094e463

Please sign in to comment.