mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 21:43:01 +00:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,6 +22,8 @@ jupyterhub_cookie_secret
|
|||||||
jupyterhub.sqlite
|
jupyterhub.sqlite
|
||||||
jupyterhub.sqlite*
|
jupyterhub.sqlite*
|
||||||
share/jupyterhub/static/components
|
share/jupyterhub/static/components
|
||||||
|
share/jupyterhub/static/css/style.css
|
||||||
|
share/jupyterhub/static/css/style.css.map
|
||||||
share/jupyterhub/static/css/style.min.css
|
share/jupyterhub/static/css/style.min.css
|
||||||
share/jupyterhub/static/css/style.min.css.map
|
share/jupyterhub/static/css/style.min.css.map
|
||||||
share/jupyterhub/static/js/admin-react.js*
|
share/jupyterhub/static/js/admin-react.js*
|
||||||
|
@@ -67,10 +67,10 @@ a more detailed discussion.
|
|||||||
|
|
||||||
This should return a version number greater than or equal to 5.0.
|
This should return a version number greater than or equal to 5.0.
|
||||||
|
|
||||||
3. Install `configurable-http-proxy` (required to run and test the default JupyterHub configuration) and `yarn` (required to build some components):
|
3. Install `configurable-http-proxy` (required to run and test the default JupyterHub configuration):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g configurable-http-proxy yarn
|
npm install -g configurable-http-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
If you get an error that says `Error: EACCES: permission denied`, you might need to prefix the command with `sudo`.
|
If you get an error that says `Error: EACCES: permission denied`, you might need to prefix the command with `sudo`.
|
||||||
@@ -78,7 +78,7 @@ a more detailed discussion.
|
|||||||
If you do not have access to sudo, you may instead run the following commands:
|
If you do not have access to sudo, you may instead run the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install configurable-http-proxy yarn
|
npm install configurable-http-proxy
|
||||||
export PATH=$PATH:$(pwd)/node_modules/.bin
|
export PATH=$PATH:$(pwd)/node_modules/.bin
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ a more detailed discussion.
|
|||||||
If you are using conda you can instead run:
|
If you are using conda you can instead run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda install configurable-http-proxy yarn
|
conda install configurable-http-proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Install an editable version of JupyterHub and its requirements for
|
4. Install an editable version of JupyterHub and its requirements for
|
||||||
@@ -123,6 +123,14 @@ configuration:
|
|||||||
jupyterhub -f testing/jupyterhub_config.py
|
jupyterhub -f testing/jupyterhub_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The test configuration enables a few things to make testing easier:
|
||||||
|
|
||||||
|
- use 'dummy' authentication and 'simple' spawner
|
||||||
|
- named servers are enabled
|
||||||
|
- listen only on localhost
|
||||||
|
- 'admin' is an admin user, if you want to test the admin page
|
||||||
|
- disable caching of static files
|
||||||
|
|
||||||
The default JupyterHub [authenticator](PAMAuthenticator)
|
The default JupyterHub [authenticator](PAMAuthenticator)
|
||||||
& [spawner](LocalProcessSpawner)
|
& [spawner](LocalProcessSpawner)
|
||||||
require your system to have user accounts for each user you want to log in to
|
require your system to have user accounts for each user you want to log in to
|
||||||
@@ -139,6 +147,29 @@ SimpleLocalProcessSpawner. If you are working on just authenticator-related
|
|||||||
parts, use only SimpleLocalProcessSpawner. Similarly, if you are working on
|
parts, use only SimpleLocalProcessSpawner. Similarly, if you are working on
|
||||||
just spawner-related parts, use only DummyAuthenticator.
|
just spawner-related parts, use only DummyAuthenticator.
|
||||||
|
|
||||||
|
## Building frontend components
|
||||||
|
|
||||||
|
The testing configuration file also disables caching of static files,
|
||||||
|
which allows you to edit and rebuild these files without restarting JupyterHub.
|
||||||
|
|
||||||
|
If you are working on the admin react page, which is in the `jsx` directory, you can run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd jsx
|
||||||
|
npm install
|
||||||
|
npm run build:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
to continuously rebuild the admin page, requiring only a refresh of the page.
|
||||||
|
|
||||||
|
If you are working on the frontend SCSS files, you can run the same `build:watch` command
|
||||||
|
in the _top level_ directory of the repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build:watch
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
This section lists common ways setting up your development environment may
|
This section lists common ways setting up your development environment may
|
||||||
|
1
jsx/.gitignore
vendored
1
jsx/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
node_modules
|
node_modules
|
||||||
build/admin-react.js
|
build/admin-react.js
|
||||||
|
.yarn
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { Button, Col } from "react-bootstrap";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import ErrorAlert from "../../util/error";
|
||||||
|
|
||||||
const AddUser = (props) => {
|
const AddUser = (props) => {
|
||||||
const [users, setUsers] = useState([]),
|
const [users, setUsers] = useState([]),
|
||||||
@@ -27,31 +29,14 @@ const AddUser = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container" data-testid="container">
|
<div className="container" data-testid="container">
|
||||||
{errorAlert != null ? (
|
<ErrorAlert errorAlert={errorAlert} setErrorAlert={setErrorAlert} />
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
|
||||||
<div className="alert alert-danger">
|
|
||||||
{errorAlert}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="close"
|
|
||||||
onClick={() => setErrorAlert(null)}
|
|
||||||
>
|
|
||||||
<span>×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
<Col md={{ span: 10, offset: 1 }} lg={{ span: 8, offset: 2 }}>
|
||||||
<div className="panel panel-default">
|
<div className="card">
|
||||||
<div className="panel-heading">
|
<div className="card-header">
|
||||||
<h4>Add Users</h4>
|
<h4>Add Users</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="card-body">
|
||||||
<form>
|
<form>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -82,15 +67,17 @@ const AddUser = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-footer">
|
<div className="card-footer">
|
||||||
<button id="return" className="btn btn-light">
|
<Link to="/">
|
||||||
<Link to="/">Back</Link>
|
<Button variant="light" id="return">
|
||||||
</button>
|
Back
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<button
|
<Button
|
||||||
id="submit"
|
id="submit"
|
||||||
data-testid="submit"
|
data-testid="submit"
|
||||||
className="btn btn-primary"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
addUsers(users, admin)
|
addUsers(users, admin)
|
||||||
.then((data) =>
|
.then((data) =>
|
||||||
@@ -111,10 +98,10 @@ const AddUser = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Users
|
Add Users
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { Button, Card } from "react-bootstrap";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { MainContainer } from "../../util/layout";
|
||||||
|
|
||||||
const CreateGroup = (props) => {
|
const CreateGroup = (props) => {
|
||||||
const [groupName, setGroupName] = useState(""),
|
const [groupName, setGroupName] = useState(""),
|
||||||
@@ -24,85 +26,61 @@ const CreateGroup = (props) => {
|
|||||||
const { createGroup, updateGroups } = props;
|
const { createGroup, updateGroups } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MainContainer errorAlert={errorAlert} setErrorAlert={setErrorAlert}>
|
||||||
<div className="container" data-testid="container">
|
<Card>
|
||||||
{errorAlert != null ? (
|
<Card.Header>
|
||||||
<div className="row">
|
<h4>Create Group</h4>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
</Card.Header>
|
||||||
<div className="alert alert-danger">
|
<Card.Body>
|
||||||
{errorAlert}
|
<div className="input-group">
|
||||||
<button
|
<input
|
||||||
type="button"
|
className="group-name-input"
|
||||||
className="close"
|
data-testid="group-input"
|
||||||
onClick={() => setErrorAlert(null)}
|
type="text"
|
||||||
>
|
id="group-name"
|
||||||
<span>×</span>
|
value={groupName}
|
||||||
</button>
|
placeholder="group name..."
|
||||||
</div>
|
onChange={(e) => {
|
||||||
</div>
|
setGroupName(e.target.value.trim());
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</Card.Body>
|
||||||
<></>
|
<Card.Footer>
|
||||||
)}
|
<Link to="/groups">
|
||||||
<div className="row">
|
<Button variant="light" id="return">
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
Back
|
||||||
<div className="panel panel-default">
|
</Button>
|
||||||
<div className="panel-heading">
|
</Link>
|
||||||
<h4>Create Group</h4>
|
<span> </span>
|
||||||
</div>
|
<Button
|
||||||
<div className="panel-body">
|
id="submit"
|
||||||
<div className="input-group">
|
data-testid="submit"
|
||||||
<input
|
variant="primary"
|
||||||
className="group-name-input"
|
onClick={() => {
|
||||||
data-testid="group-input"
|
createGroup(groupName)
|
||||||
type="text"
|
.then((data) => {
|
||||||
id="group-name"
|
return data.status < 300
|
||||||
value={groupName}
|
? updateGroups(0, limit)
|
||||||
placeholder="group name..."
|
.then((data) => dispatchPageUpdate(data, 0))
|
||||||
onChange={(e) => {
|
.then(() => navigate("/groups"))
|
||||||
setGroupName(e.target.value.trim());
|
.catch(() =>
|
||||||
}}
|
setErrorAlert(`Could not update groups list.`),
|
||||||
></input>
|
)
|
||||||
</div>
|
: setErrorAlert(
|
||||||
</div>
|
`Failed to create group. ${
|
||||||
<div className="panel-footer">
|
data.status == 409 ? "Group already exists." : ""
|
||||||
<button id="return" className="btn btn-light">
|
}`,
|
||||||
<Link to="/">Back</Link>
|
);
|
||||||
</button>
|
})
|
||||||
<span> </span>
|
.catch(() => setErrorAlert(`Failed to create group.`));
|
||||||
<button
|
}}
|
||||||
id="submit"
|
>
|
||||||
data-testid="submit"
|
Create
|
||||||
className="btn btn-primary"
|
</Button>
|
||||||
onClick={() => {
|
</Card.Footer>
|
||||||
createGroup(groupName)
|
</Card>
|
||||||
.then((data) => {
|
</MainContainer>
|
||||||
return data.status < 300
|
|
||||||
? updateGroups(0, limit)
|
|
||||||
.then((data) => dispatchPageUpdate(data, 0))
|
|
||||||
.then(() => navigate("/groups"))
|
|
||||||
.catch(() =>
|
|
||||||
setErrorAlert(`Could not update groups list.`),
|
|
||||||
)
|
|
||||||
: setErrorAlert(
|
|
||||||
`Failed to create group. ${
|
|
||||||
data.status == 409
|
|
||||||
? "Group already exists."
|
|
||||||
: ""
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => setErrorAlert(`Failed to create group.`));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import "./table-select.css";
|
import "./table-select.css";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
const DynamicTable = (props) => {
|
const DynamicTable = (props) => {
|
||||||
var [message, setMessage] = useState(""),
|
var [message, setMessage] = useState(""),
|
||||||
@@ -94,8 +95,8 @@ const DynamicTable = (props) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<Button
|
||||||
className="form-control btn btn-default"
|
variant="danger"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
propvalues.splice(i, 1);
|
propvalues.splice(i, 1);
|
||||||
propkeys.splice(i, 1);
|
propkeys.splice(i, 1);
|
||||||
@@ -110,7 +111,7 @@ const DynamicTable = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -150,15 +151,14 @@ const DynamicTable = (props) => {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<Button
|
||||||
id="add-item"
|
id="add-item"
|
||||||
data-testid="add-item"
|
data-testid="add-item"
|
||||||
className="form-control btn btn-default"
|
className="text-nowrap"
|
||||||
type="button"
|
|
||||||
onClick={() => handleAddItem()}
|
onClick={() => handleAddItem()}
|
||||||
>
|
>
|
||||||
Add Item
|
Add Item
|
||||||
</button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
@import url(../../style/root.css);
|
@import url(../../style/root.css);
|
||||||
|
|
||||||
.properties-table {
|
.properties-table {
|
||||||
width: 95%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-table-keyvalues {
|
.properties-table-keyvalues {
|
||||||
width: 95%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
@@ -2,6 +2,8 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { Button, Card } from "react-bootstrap";
|
||||||
|
import { MainContainer } from "../../util/layout";
|
||||||
|
|
||||||
const EditUser = (props) => {
|
const EditUser = (props) => {
|
||||||
const limit = useSelector((state) => state.limit),
|
const limit = useSelector((state) => state.limit),
|
||||||
@@ -39,129 +41,103 @@ const EditUser = (props) => {
|
|||||||
[admin, setAdmin] = useState(has_admin);
|
[admin, setAdmin] = useState(has_admin);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MainContainer errorAlert={errorAlert} setErrorAlert={setErrorAlert}>
|
||||||
<div className="container" data-testid="container">
|
<Card>
|
||||||
{errorAlert != null ? (
|
<Card.Header>
|
||||||
<div className="row">
|
<h1>Editing user {username}</h1>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
</Card.Header>
|
||||||
<div className="alert alert-danger">
|
<Card.Body>
|
||||||
{errorAlert}
|
<form>
|
||||||
<button
|
<div className="form-group">
|
||||||
type="button"
|
<textarea
|
||||||
className="close"
|
className="form-control"
|
||||||
onClick={() => setErrorAlert(null)}
|
data-testid="edit-username-input"
|
||||||
>
|
id="exampleFormControlTextarea1"
|
||||||
<span>×</span>
|
rows="3"
|
||||||
</button>
|
placeholder="updated username"
|
||||||
</div>
|
onBlur={(e) => {
|
||||||
|
setUpdatedUsername(e.target.value);
|
||||||
|
}}
|
||||||
|
></textarea>
|
||||||
|
<br></br>
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
checked={admin}
|
||||||
|
type="checkbox"
|
||||||
|
id="admin-check"
|
||||||
|
onChange={() => setAdmin(!admin)}
|
||||||
|
/>
|
||||||
|
<span> </span>
|
||||||
|
<label className="form-check-label">Admin</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
) : (
|
</Card.Body>
|
||||||
<></>
|
<Card.Footer>
|
||||||
)}
|
<Link to="/">
|
||||||
<div className="row">
|
<Button variant="light">Back</Button>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
</Link>
|
||||||
<div className="panel panel-default">
|
<span> </span>
|
||||||
<div className="panel-heading">
|
<Button
|
||||||
<h4>Editing user {username}</h4>
|
id="submit"
|
||||||
</div>
|
data-testid="submit"
|
||||||
<div className="panel-body">
|
variant="primary"
|
||||||
<form>
|
onClick={(e) => {
|
||||||
<div className="form-group">
|
e.preventDefault();
|
||||||
<textarea
|
if (updatedUsername == "" && admin == has_admin) {
|
||||||
className="form-control"
|
noChangeEvent();
|
||||||
data-testid="edit-username-input"
|
return;
|
||||||
id="exampleFormControlTextarea1"
|
} else {
|
||||||
rows="3"
|
editUser(
|
||||||
placeholder="updated username"
|
username,
|
||||||
onBlur={(e) => {
|
updatedUsername != "" ? updatedUsername : username,
|
||||||
setUpdatedUsername(e.target.value);
|
admin,
|
||||||
}}
|
)
|
||||||
></textarea>
|
.then((data) => {
|
||||||
<br></br>
|
data.status < 300
|
||||||
<input
|
? updateUsers(0, limit)
|
||||||
className="form-check-input"
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
checked={admin}
|
.then(() => navigate("/"))
|
||||||
type="checkbox"
|
.catch(() =>
|
||||||
id="admin-check"
|
setErrorAlert(`Could not update users list.`),
|
||||||
onChange={() => setAdmin(!admin)}
|
)
|
||||||
/>
|
: setErrorAlert(`Failed to edit user.`);
|
||||||
<span> </span>
|
})
|
||||||
<label className="form-check-label">Admin</label>
|
.catch(() => {
|
||||||
<br></br>
|
setErrorAlert(`Failed to edit user.`);
|
||||||
<button
|
});
|
||||||
id="delete-user"
|
}
|
||||||
data-testid="delete-user"
|
}}
|
||||||
className="btn btn-danger btn-sm"
|
>
|
||||||
onClick={(e) => {
|
Apply
|
||||||
e.preventDefault();
|
</Button>
|
||||||
deleteUser(username)
|
<Button
|
||||||
.then((data) => {
|
id="delete-user"
|
||||||
data.status < 300
|
data-testid="delete-user"
|
||||||
? updateUsers(0, limit)
|
variant="danger"
|
||||||
.then((data) => dispatchPageChange(data, 0))
|
className="float-end"
|
||||||
.then(() => navigate("/"))
|
onClick={(e) => {
|
||||||
.catch(() =>
|
e.preventDefault();
|
||||||
setErrorAlert(
|
deleteUser(username)
|
||||||
`Could not update users list.`,
|
.then((data) => {
|
||||||
),
|
data.status < 300
|
||||||
)
|
? updateUsers(0, limit)
|
||||||
: setErrorAlert(`Failed to edit user.`);
|
.then((data) => dispatchPageChange(data, 0))
|
||||||
})
|
.then(() => navigate("/"))
|
||||||
.catch(() => {
|
.catch(() =>
|
||||||
setErrorAlert(`Failed to edit user.`);
|
setErrorAlert(`Could not update users list.`),
|
||||||
});
|
)
|
||||||
}}
|
: setErrorAlert(`Failed to edit user.`);
|
||||||
>
|
})
|
||||||
Delete user
|
.catch(() => {
|
||||||
</button>
|
setErrorAlert(`Failed to edit user.`);
|
||||||
</div>
|
});
|
||||||
</form>
|
}}
|
||||||
</div>
|
>
|
||||||
<div className="panel-footer">
|
Delete user
|
||||||
<button className="btn btn-light">
|
</Button>
|
||||||
<Link to="/">Back</Link>
|
</Card.Footer>
|
||||||
</button>
|
</Card>
|
||||||
<span> </span>
|
</MainContainer>
|
||||||
<button
|
|
||||||
id="submit"
|
|
||||||
data-testid="submit"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (updatedUsername == "" && admin == has_admin) {
|
|
||||||
noChangeEvent();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
editUser(
|
|
||||||
username,
|
|
||||||
updatedUsername != "" ? updatedUsername : username,
|
|
||||||
admin,
|
|
||||||
)
|
|
||||||
.then((data) => {
|
|
||||||
data.status < 300
|
|
||||||
? updateUsers(0, limit)
|
|
||||||
.then((data) => dispatchPageChange(data, 0))
|
|
||||||
.then(() => navigate("/"))
|
|
||||||
.catch(() =>
|
|
||||||
setErrorAlert(`Could not update users list.`),
|
|
||||||
)
|
|
||||||
: setErrorAlert(`Failed to edit user.`);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setErrorAlert(`Failed to edit user.`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,8 +2,10 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { Link, useNavigate, useLocation } from "react-router-dom";
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { Button, Card } from "react-bootstrap";
|
||||||
import GroupSelect from "../GroupSelect/GroupSelect";
|
import GroupSelect from "../GroupSelect/GroupSelect";
|
||||||
import DynamicTable from "../DynamicTable/DynamicTable";
|
import DynamicTable from "../DynamicTable/DynamicTable";
|
||||||
|
import { MainContainer } from "../../util/layout";
|
||||||
|
|
||||||
const GroupEdit = (props) => {
|
const GroupEdit = (props) => {
|
||||||
const [selected, setSelected] = useState([]),
|
const [selected, setSelected] = useState([]),
|
||||||
@@ -34,8 +36,6 @@ const GroupEdit = (props) => {
|
|||||||
validateUser,
|
validateUser,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
console.log("group edit", location, location.state);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.state) {
|
if (!location.state) {
|
||||||
navigate("/groups");
|
navigate("/groups");
|
||||||
@@ -49,47 +49,26 @@ const GroupEdit = (props) => {
|
|||||||
const [propvalues, setPropValues] = useState([]);
|
const [propvalues, setPropValues] = useState([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" data-testid="container">
|
<MainContainer errorAlert={errorAlert} setErrorAlert={setErrorAlert}>
|
||||||
{errorAlert != null ? (
|
<h1>Editing Group {group_data.name}</h1>
|
||||||
<div className="row">
|
<Card>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
<Card.Header>
|
||||||
<div className="alert alert-danger">
|
<h2>Manage group members</h2>
|
||||||
{errorAlert}
|
</Card.Header>
|
||||||
<button
|
<Card.Body>
|
||||||
type="button"
|
<GroupSelect
|
||||||
className="close"
|
users={group_data.users}
|
||||||
onClick={() => setErrorAlert(null)}
|
validateUser={validateUser}
|
||||||
>
|
onChange={(selection) => {
|
||||||
<span>×</span>
|
setSelected(selection);
|
||||||
</button>
|
setChanged(true);
|
||||||
</div>
|
}}
|
||||||
</div>
|
/>
|
||||||
</div>
|
</Card.Body>
|
||||||
) : (
|
<Card.Header>
|
||||||
<></>
|
<h2>Manage group properties</h2>
|
||||||
)}
|
</Card.Header>
|
||||||
<div className="row">
|
<Card.Body>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
|
||||||
<h3>Editing Group {group_data.name}</h3>
|
|
||||||
<br></br>
|
|
||||||
<div className="alert alert-info">Manage group members</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<GroupSelect
|
|
||||||
users={group_data.users}
|
|
||||||
validateUser={validateUser}
|
|
||||||
onChange={(selection) => {
|
|
||||||
setSelected(selection);
|
|
||||||
setChanged(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
|
||||||
<div className="alert alert-info">Manage group properties</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
current_propobject={group_data.properties}
|
current_propobject={group_data.properties}
|
||||||
setProp={setProp}
|
setProp={setProp}
|
||||||
@@ -98,19 +77,21 @@ const GroupEdit = (props) => {
|
|||||||
|
|
||||||
//Add keys
|
//Add keys
|
||||||
/>
|
/>
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<span id="error"></span>
|
||||||
|
</div>
|
||||||
<div className="row">
|
</Card.Body>
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
<Card.Footer>
|
||||||
<button id="return" className="btn btn-light">
|
<Link to="/groups">
|
||||||
<Link to="/groups">Back</Link>
|
<Button variant="light" id="return">
|
||||||
</button>
|
Back
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
<span> </span>
|
<span> </span>
|
||||||
<button
|
<Button
|
||||||
id="submit"
|
id="submit"
|
||||||
data-testid="submit"
|
data-testid="submit"
|
||||||
className="btn btn-primary"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// check for changes
|
// check for changes
|
||||||
let new_users = selected.filter(
|
let new_users = selected.filter(
|
||||||
@@ -158,15 +139,12 @@ const GroupEdit = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</Button>
|
||||||
<div>
|
<Button
|
||||||
<span id="error"></span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
id="delete-group"
|
id="delete-group"
|
||||||
data-testid="delete-group"
|
data-testid="delete-group"
|
||||||
className="btn btn-danger"
|
variant="danger"
|
||||||
style={{ float: "right" }}
|
className="float-end"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
var groupName = group_data.name;
|
var groupName = group_data.name;
|
||||||
deleteGroup(groupName)
|
deleteGroup(groupName)
|
||||||
@@ -182,12 +160,13 @@ const GroupEdit = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete Group
|
Delete Group
|
||||||
</button>
|
</Button>
|
||||||
<br></br>
|
<div>
|
||||||
<br></br>
|
<span id="error"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card.Footer>
|
||||||
</div>
|
</Card>
|
||||||
|
</MainContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
import "./group-select.css";
|
import "./group-select.css";
|
||||||
|
|
||||||
const GroupSelect = (props) => {
|
const GroupSelect = (props) => {
|
||||||
@@ -12,92 +13,80 @@ const GroupSelect = (props) => {
|
|||||||
if (!users) return null;
|
if (!users) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<>
|
||||||
{error != null ? (
|
{error != null ? (
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 text-left">
|
<div className="alert alert-danger">{error}</div>
|
||||||
<div className="alert alert-danger">{error}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 text-left">
|
<div className="input-group">
|
||||||
<div className="input-group">
|
<input
|
||||||
<input
|
id="username-input"
|
||||||
id="username-input"
|
data-testid="username-input"
|
||||||
data-testid="username-input"
|
type="text"
|
||||||
type="text"
|
className="form-control"
|
||||||
className="form-control"
|
placeholder="Add by username"
|
||||||
placeholder="Add by username"
|
value={username}
|
||||||
value={username}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
setUsername(e.target.value);
|
||||||
setUsername(e.target.value);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Button
|
||||||
<span className="input-group-btn">
|
id="validate-user"
|
||||||
<button
|
data-testid="validate-user"
|
||||||
id="validate-user"
|
onClick={() => {
|
||||||
data-testid="validate-user"
|
validateUser(username).then((exists) => {
|
||||||
className="btn btn-default"
|
if (exists && !selected.includes(username)) {
|
||||||
type="button"
|
let updated_selection = selected.concat([username]);
|
||||||
|
onChange(updated_selection, users);
|
||||||
|
setUsername("");
|
||||||
|
setSelected(updated_selection);
|
||||||
|
if (error != null) setError(null);
|
||||||
|
} else if (!exists) {
|
||||||
|
setError(`"${username}" is not a valid JupyterHub user.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add user
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="users-container">
|
||||||
|
<hr></hr>
|
||||||
|
<div>
|
||||||
|
{selected.map((e, i) => (
|
||||||
|
<div
|
||||||
|
key={"selected" + i}
|
||||||
|
className="item selected"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
validateUser(username).then((exists) => {
|
let updated_selection = selected
|
||||||
if (exists && !selected.includes(username)) {
|
.slice(0, i)
|
||||||
let updated_selection = selected.concat([username]);
|
.concat(selected.slice(i + 1));
|
||||||
onChange(updated_selection, users);
|
onChange(updated_selection, users);
|
||||||
setUsername("");
|
setSelected(updated_selection);
|
||||||
setSelected(updated_selection);
|
|
||||||
if (error != null) setError(null);
|
|
||||||
} else if (!exists) {
|
|
||||||
setError(`"${username}" is not a valid JupyterHub user.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add user
|
{e}
|
||||||
</button>
|
</div>
|
||||||
</span>
|
))}
|
||||||
</div>
|
{users.map((e, i) =>
|
||||||
</div>
|
selected.includes(e) ? undefined : (
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2 text-left">
|
|
||||||
<div className="users-container">
|
|
||||||
<hr></hr>
|
|
||||||
<div>
|
|
||||||
{selected.map((e, i) => (
|
|
||||||
<div
|
<div
|
||||||
key={"selected" + i}
|
key={"unselected" + i}
|
||||||
className="item selected"
|
className="item unselected"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let updated_selection = selected
|
let updated_selection = selected.concat([e]);
|
||||||
.slice(0, i)
|
|
||||||
.concat(selected.slice(i + 1));
|
|
||||||
onChange(updated_selection, users);
|
onChange(updated_selection, users);
|
||||||
setSelected(updated_selection);
|
setSelected(updated_selection);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{e}
|
{e}
|
||||||
</div>
|
</div>
|
||||||
))}
|
),
|
||||||
{users.map((e, i) =>
|
)}
|
||||||
selected.includes(e) ? undefined : (
|
|
||||||
<div
|
|
||||||
key={"unselected" + i}
|
|
||||||
className="item unselected"
|
|
||||||
onClick={() => {
|
|
||||||
let updated_selection = selected.concat([e]);
|
|
||||||
onChange(updated_selection, users);
|
|
||||||
setSelected(updated_selection);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{e}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<br></br>
|
|
||||||
<br></br>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,9 +2,11 @@ import React, { useEffect } from "react";
|
|||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import { Button, Card } from "react-bootstrap";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { usePaginationParams } from "../../util/paginationParams";
|
import { usePaginationParams } from "../../util/paginationParams";
|
||||||
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
import PaginationFooter from "../PaginationFooter/PaginationFooter";
|
||||||
|
import { MainContainer } from "../../util/layout";
|
||||||
|
|
||||||
const Groups = (props) => {
|
const Groups = (props) => {
|
||||||
const groups_data = useSelector((state) => state.groups_data);
|
const groups_data = useSelector((state) => state.groups_data);
|
||||||
@@ -41,59 +43,53 @@ const Groups = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" data-testid="container">
|
<MainContainer>
|
||||||
<div className="row">
|
<Card>
|
||||||
<div className="col-md-12 col-lg-10 col-lg-offset-1">
|
<Card.Header>
|
||||||
<div className="panel panel-default">
|
<h4>Groups</h4>
|
||||||
<div className="panel-heading">
|
</Card.Header>
|
||||||
<h4>Groups</h4>
|
<Card.Body>
|
||||||
</div>
|
<ul className="list-group">
|
||||||
<div className="panel-body">
|
{groups_data.length > 0 ? (
|
||||||
<ul className="list-group">
|
groups_data.map((e, i) => (
|
||||||
{groups_data.length > 0 ? (
|
<li className="list-group-item" key={"group-item" + i}>
|
||||||
groups_data.map((e, i) => (
|
<span className="badge rounded-pill bg-success mx-2">
|
||||||
<li className="list-group-item" key={"group-item" + i}>
|
{e.users.length + " users"}
|
||||||
<span className="badge badge-pill badge-success">
|
</span>
|
||||||
{e.users.length + " users"}
|
<Link to="/group-edit" state={{ group_data: e }}>
|
||||||
</span>
|
{e.name}
|
||||||
<Link to="/group-edit" state={{ group_data: e }}>
|
</Link>
|
||||||
{e.name}
|
</li>
|
||||||
</Link>
|
))
|
||||||
</li>
|
) : (
|
||||||
))
|
<div>
|
||||||
) : (
|
<h4>no groups created...</h4>
|
||||||
<div>
|
</div>
|
||||||
<h4>no groups created...</h4>
|
)}
|
||||||
</div>
|
</ul>
|
||||||
)}
|
<PaginationFooter
|
||||||
</ul>
|
offset={offset}
|
||||||
<PaginationFooter
|
limit={limit}
|
||||||
offset={offset}
|
visible={groups_data.length}
|
||||||
limit={limit}
|
total={total}
|
||||||
visible={groups_data.length}
|
next={() => setOffset(offset + limit)}
|
||||||
total={total}
|
prev={() => setOffset(offset - limit)}
|
||||||
next={() => setOffset(offset + limit)}
|
handleLimit={handleLimit}
|
||||||
prev={() => setOffset(offset - limit)}
|
/>
|
||||||
handleLimit={handleLimit}
|
</Card.Body>
|
||||||
/>
|
<Card.Footer>
|
||||||
</div>
|
<Link to="/">
|
||||||
<div className="panel-footer">
|
<Button variant="light" id="return">
|
||||||
<button className="btn btn-light adjacent-span-spacing">
|
Back
|
||||||
<Link to="/">Back</Link>
|
</Button>
|
||||||
</button>
|
</Link>
|
||||||
<button
|
<span> </span>
|
||||||
className="btn btn-primary adjacent-span-spacing"
|
<Link to="/create-group">
|
||||||
onClick={() => {
|
<Button variant="primary">New Group</Button>
|
||||||
navigate("/create-group");
|
</Link>
|
||||||
}}
|
</Card.Footer>
|
||||||
>
|
</Card>
|
||||||
New Group
|
</MainContainer>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { FormControl } from "react-bootstrap";
|
import { Button, FormControl } from "react-bootstrap";
|
||||||
|
|
||||||
import "./pagination-footer.css";
|
import "./pagination-footer.css";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const PaginationFooter = (props) => {
|
|||||||
{total ? `of ${total}` : ""}
|
{total ? `of ${total}` : ""}
|
||||||
<br />
|
<br />
|
||||||
{offset >= 1 ? (
|
{offset >= 1 ? (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<Button variant="light" size="sm">
|
||||||
<span
|
<span
|
||||||
className="active-pagination"
|
className="active-pagination"
|
||||||
data-testid="paginate-prev"
|
data-testid="paginate-prev"
|
||||||
@@ -21,14 +21,14 @@ const PaginationFooter = (props) => {
|
|||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<Button variant="light" size="sm">
|
||||||
<span className="inactive-pagination">Previous</span>
|
<span className="inactive-pagination">Previous</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{offset + visible < total ? (
|
{offset + visible < total ? (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<Button variant="light" size="sm">
|
||||||
<span
|
<span
|
||||||
className="active-pagination"
|
className="active-pagination"
|
||||||
data-testid="paginate-next"
|
data-testid="paginate-next"
|
||||||
@@ -36,11 +36,11 @@ const PaginationFooter = (props) => {
|
|||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<button className="btn btn-sm btn-light spaced">
|
<Button variant="light" size="sm">
|
||||||
<span className="inactive-pagination">Next</span>
|
<span className="inactive-pagination">Next</span>
|
||||||
</button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<label>
|
<label>
|
||||||
Items per page:
|
Items per page:
|
||||||
|
@@ -2,6 +2,7 @@ import React, { useEffect, useState, Fragment } from "react";
|
|||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import ErrorAlert from "../../util/error";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -151,11 +152,20 @@ const ServerDashboard = (props) => {
|
|||||||
setNameFilter(event.target.value);
|
setNameFilter(event.target.value);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
const ServerButton = ({ server, user, action, name, extraClass }) => {
|
const ServerButton = ({
|
||||||
|
server,
|
||||||
|
user,
|
||||||
|
action,
|
||||||
|
name,
|
||||||
|
variant,
|
||||||
|
extraClass,
|
||||||
|
}) => {
|
||||||
var [isDisabled, setIsDisabled] = useState(false);
|
var [isDisabled, setIsDisabled] = useState(false);
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className={`btn btn-xs ${extraClass}`}
|
size="xs"
|
||||||
|
variant={variant}
|
||||||
|
className={extraClass}
|
||||||
disabled={isDisabled || server.pending}
|
disabled={isDisabled || server.pending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsDisabled(true);
|
setIsDisabled(true);
|
||||||
@@ -183,7 +193,7 @@ const ServerDashboard = (props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -196,7 +206,8 @@ const ServerDashboard = (props) => {
|
|||||||
user,
|
user,
|
||||||
action: stopServer,
|
action: stopServer,
|
||||||
name: "Stop Server",
|
name: "Stop Server",
|
||||||
extraClass: "btn-danger stop-button",
|
variant: "danger",
|
||||||
|
extraClass: "stop-button",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const DeleteServerButton = ({ server, user }) => {
|
const DeleteServerButton = ({ server, user }) => {
|
||||||
@@ -212,7 +223,8 @@ const ServerDashboard = (props) => {
|
|||||||
user,
|
user,
|
||||||
action: deleteServer,
|
action: deleteServer,
|
||||||
name: "Delete Server",
|
name: "Delete Server",
|
||||||
extraClass: "btn-danger stop-button",
|
variant: "danger",
|
||||||
|
extraClass: "stop-button",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,7 +237,8 @@ const ServerDashboard = (props) => {
|
|||||||
user,
|
user,
|
||||||
action: startServer,
|
action: startServer,
|
||||||
name: server.pending ? "Server is pending" : "Start Server",
|
name: server.pending ? "Server is pending" : "Start Server",
|
||||||
extraClass: "btn-success start-button",
|
variant: "success",
|
||||||
|
extraClass: "start-button",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,7 +252,9 @@ const ServerDashboard = (props) => {
|
|||||||
server.name ? "/" + server.name : ""
|
server.name ? "/" + server.name : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<button className="btn btn-light btn-xs">Spawn Page</button>
|
<Button variant="light" size="xs">
|
||||||
|
Spawn Page
|
||||||
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -250,15 +265,18 @@ const ServerDashboard = (props) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<a href={server.url || ""}>
|
<a href={server.url || ""}>
|
||||||
<button className="btn btn-primary btn-xs">Access Server</button>
|
<Button variant="primary" size="xs">
|
||||||
|
Access Server
|
||||||
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditUserButton = ({ user }) => {
|
const EditUserButton = ({ user }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
className="btn btn-light btn-xs"
|
size="xs"
|
||||||
|
variant="light"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate("/edit-user", {
|
navigate("/edit-user", {
|
||||||
state: {
|
state: {
|
||||||
@@ -269,7 +287,7 @@ const ServerDashboard = (props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
Edit User
|
Edit User
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +318,7 @@ const ServerDashboard = (props) => {
|
|||||||
}, {});
|
}, {});
|
||||||
return (
|
return (
|
||||||
<ReactObjectTableViewer
|
<ReactObjectTableViewer
|
||||||
className="table-striped table-bordered"
|
className="table table-striped table-bordered"
|
||||||
style={{
|
style={{
|
||||||
padding: "3px 6px",
|
padding: "3px 6px",
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
@@ -342,7 +360,7 @@ const ServerDashboard = (props) => {
|
|||||||
variant={open ? "secondary" : "primary"}
|
variant={open ? "secondary" : "primary"}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
<span className="caret"></span>
|
<span className="fa fa-caret-down"></span>
|
||||||
</Button>{" "}
|
</Button>{" "}
|
||||||
</span>
|
</span>
|
||||||
<span data-testid={`user-name-div-${userServerName}`}>
|
<span data-testid={`user-name-div-${userServerName}`}>
|
||||||
@@ -402,26 +420,9 @@ const ServerDashboard = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container" data-testid="container">
|
<div className="container" data-testid="container">
|
||||||
{errorAlert != null ? (
|
<ErrorAlert errorAlert={errorAlert} setErrorAlert={setErrorAlert} />
|
||||||
<div className="row">
|
|
||||||
<div className="col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2">
|
|
||||||
<div className="alert alert-danger">
|
|
||||||
{errorAlert}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="close"
|
|
||||||
onClick={() => setErrorAlert(null)}
|
|
||||||
>
|
|
||||||
<span>×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<div className="server-dashboard-container">
|
<div className="server-dashboard-container">
|
||||||
<Row>
|
<Row className="rows-cols-lg-auto g-3 mb-3 align-items-center">
|
||||||
<Col md={4}>
|
<Col md={4}>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
@@ -432,27 +433,32 @@ const ServerDashboard = (props) => {
|
|||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={3}>
|
<Col md={4}>
|
||||||
{/* div.checkbox required for BS3 CSS */}
|
<Form.Check
|
||||||
<div className="checkbox">
|
inline
|
||||||
<label title="check to only show running servers, otherwise show all">
|
title="check to only show running servers, otherwise show all"
|
||||||
<Form.Check
|
>
|
||||||
inline
|
<Form.Check.Input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="active_servers"
|
name="active_servers"
|
||||||
id="active-servers-filter"
|
id="active-servers-filter"
|
||||||
checked={state_filter == "active"}
|
checked={state_filter == "active"}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setStateFilter(event.target.checked ? "active" : null);
|
setStateFilter(event.target.checked ? "active" : null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Form.Check.Label for="active-servers-filter">
|
||||||
{"only active servers"}
|
{"only active servers"}
|
||||||
</label>
|
</Form.Check.Label>
|
||||||
</div>
|
</Form.Check>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col md="auto" style={{ float: "right", margin: 15 }}>
|
<Col md={{ span: 3, offset: 1 }}>
|
||||||
<Link to="/groups">{"> Manage Groups"}</Link>
|
<Link to="/groups">
|
||||||
|
<Button variant="light" className="form-control">
|
||||||
|
{"Manage Groups"}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<table className="table table-bordered table-hover">
|
<table className="table table-bordered table-hover">
|
||||||
@@ -565,7 +571,7 @@ const ServerDashboard = (props) => {
|
|||||||
Stop All
|
Stop All
|
||||||
</Button>
|
</Button>
|
||||||
{/* spacing between start/stop and Shutdown */}
|
{/* spacing between start/stop and Shutdown */}
|
||||||
<span style={{ marginLeft: "56px" }}> </span>
|
<span style={{ marginLeft: "30px" }}> </span>
|
||||||
{/* Shutdown Jupyterhub */}
|
{/* Shutdown Jupyterhub */}
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
|
@@ -7,13 +7,6 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-light {
|
|
||||||
/* backport bs5 btn-light colors */
|
|
||||||
background-color: #f9fafb;
|
|
||||||
border-color: #f9fafb;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-dashboard-container .btn-light {
|
.server-dashboard-container .btn-light {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
@@ -80,7 +73,7 @@ goals:
|
|||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
}
|
}
|
||||||
.admin-table-head #actions-header {
|
.admin-table-head #actions-header {
|
||||||
width: 350px;
|
width: 410px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* vertical stack server buttons on small windows */
|
/* vertical stack server buttons on small windows */
|
||||||
|
32
jsx/src/util/error.jsx
Normal file
32
jsx/src/util/error.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Alert, Col, Row } from "react-bootstrap";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const ErrorAlert = (props) => {
|
||||||
|
const { errorAlert, setErrorAlert } = props;
|
||||||
|
if (!errorAlert) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col md={{ span: 10, offset: 1 }} lg={{ span: 8, offset: 2 }}>
|
||||||
|
<Alert variant="danger">
|
||||||
|
{errorAlert}
|
||||||
|
<Button
|
||||||
|
variant="close"
|
||||||
|
className="float-end"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={() => setErrorAlert(null)}
|
||||||
|
></Button>
|
||||||
|
</Alert>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorAlert.propTypes = {
|
||||||
|
errorAlert: PropTypes.string,
|
||||||
|
setErrorAlert: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorAlert;
|
38
jsx/src/util/layout.jsx
Normal file
38
jsx/src/util/layout.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { withProps } from "recompose";
|
||||||
|
import { Col, Row, Container } from "react-bootstrap";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import ErrorAlert from "./error";
|
||||||
|
|
||||||
|
export const MainCol = (props) => {
|
||||||
|
// main column layout
|
||||||
|
// sets default width, span
|
||||||
|
return withProps({
|
||||||
|
md: { span: 10, offset: 1 },
|
||||||
|
lg: { span: 8, offset: 2 },
|
||||||
|
...props,
|
||||||
|
})(Col)();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MainContainer = (props) => {
|
||||||
|
// default container for an admin page
|
||||||
|
// adds errorAlert and sets main column width
|
||||||
|
props = props || {};
|
||||||
|
return (
|
||||||
|
<Container data-testid="container">
|
||||||
|
<ErrorAlert
|
||||||
|
errorAlert={props.errorAlert}
|
||||||
|
setErrorAlert={props.setErrorAlert}
|
||||||
|
/>
|
||||||
|
<Row>
|
||||||
|
<MainCol>{props.children}</MainCol>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MainContainer.propTypes = {
|
||||||
|
errorAlert: PropTypes.string,
|
||||||
|
setErrorAlert: PropTypes.func,
|
||||||
|
children: PropTypes.array,
|
||||||
|
};
|
@@ -15,7 +15,9 @@ class CacheControlStaticFilesHandler(StaticFileHandler):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def set_extra_headers(self, path):
|
def set_extra_headers(self, path):
|
||||||
if "v" not in self.request.arguments:
|
if "v" not in self.request.arguments or self.settings.get(
|
||||||
|
"no_cache_static", False
|
||||||
|
):
|
||||||
self.add_header("Cache-Control", "no-cache")
|
self.add_header("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
|
||||||
|
@@ -329,6 +329,64 @@ async def open_home_page(app, browser, user):
|
|||||||
await expect(browser).to_have_url(re.compile(".*/hub/home"))
|
await expect(browser).to_have_url(re.compile(".*/hub/home"))
|
||||||
|
|
||||||
|
|
||||||
|
async def test_home_nav_collapse(app, browser, user_special_chars):
|
||||||
|
user = user_special_chars.user
|
||||||
|
await open_home_page(app, browser, user)
|
||||||
|
nav = browser.locator(".navbar")
|
||||||
|
navbar_collapse = nav.locator(".navbar-collapse")
|
||||||
|
logo = nav.locator("#jupyterhub-logo")
|
||||||
|
home = nav.get_by_text("Home")
|
||||||
|
logout_name = nav.get_by_text(user.name)
|
||||||
|
logout_btn = nav.get_by_text("Logout")
|
||||||
|
toggler = nav.locator(".navbar-toggler")
|
||||||
|
|
||||||
|
await expect(nav).to_be_visible()
|
||||||
|
|
||||||
|
await browser.set_viewport_size({"width": 640, "height": 480})
|
||||||
|
# links visible, nav items visible, collapse not visible
|
||||||
|
await expect(logo).to_be_visible()
|
||||||
|
await expect(home).to_be_visible()
|
||||||
|
await expect(logout_name).to_be_visible()
|
||||||
|
await expect(logout_btn).to_be_visible()
|
||||||
|
await expect(toggler).not_to_be_visible()
|
||||||
|
|
||||||
|
# below small breakpoint (576px)
|
||||||
|
await browser.set_viewport_size({"width": 500, "height": 480})
|
||||||
|
# logo visible, links and logout not visible, toggler visible
|
||||||
|
await expect(logo).to_be_visible()
|
||||||
|
await expect(home).not_to_be_visible()
|
||||||
|
await expect(logout_name).not_to_be_visible()
|
||||||
|
await expect(logout_btn).not_to_be_visible()
|
||||||
|
await expect(toggler).to_be_visible()
|
||||||
|
|
||||||
|
# click toggler, links should be visible
|
||||||
|
await toggler.click()
|
||||||
|
# wait for expand to finish
|
||||||
|
# expand animates through `collapse -> collapsing -> collapse show`
|
||||||
|
await expect(navbar_collapse).to_have_class(re.compile(r"\bshow\b"))
|
||||||
|
await expect(home).to_be_visible()
|
||||||
|
await expect(logout_name).to_be_visible()
|
||||||
|
await expect(logout_btn).to_be_visible()
|
||||||
|
await expect(toggler).to_be_visible()
|
||||||
|
# wait for expand animation
|
||||||
|
# click toggler again, links should hide
|
||||||
|
# need to wait for expand to complete
|
||||||
|
await toggler.click()
|
||||||
|
await expect(navbar_collapse).not_to_have_class(re.compile(r"\bshow\b"))
|
||||||
|
await expect(home).not_to_be_visible()
|
||||||
|
await expect(logout_name).not_to_be_visible()
|
||||||
|
await expect(logout_btn).not_to_be_visible()
|
||||||
|
await expect(toggler).to_be_visible()
|
||||||
|
|
||||||
|
# resize, should re-show
|
||||||
|
await browser.set_viewport_size({"width": 640, "height": 480})
|
||||||
|
await expect(logo).to_be_visible()
|
||||||
|
await expect(home).to_be_visible()
|
||||||
|
await expect(logout_name).to_be_visible()
|
||||||
|
await expect(logout_btn).to_be_visible()
|
||||||
|
await expect(toggler).not_to_be_visible()
|
||||||
|
|
||||||
|
|
||||||
async def test_start_button_server_not_started(app, browser, user_special_chars):
|
async def test_start_button_server_not_started(app, browser, user_special_chars):
|
||||||
"""verify that when server is not started one button is available,
|
"""verify that when server is not started one button is available,
|
||||||
after starting 2 buttons are available"""
|
after starting 2 buttons are available"""
|
||||||
@@ -413,7 +471,7 @@ async def test_token_request_form_and_panel(app, browser, user_special_chars):
|
|||||||
"""verify elements of the request token form"""
|
"""verify elements of the request token form"""
|
||||||
|
|
||||||
await open_token_page(app, browser, user_special_chars.user)
|
await open_token_page(app, browser, user_special_chars.user)
|
||||||
request_btn = browser.locator('//div[@class="text-center"]').get_by_role("button")
|
request_btn = browser.locator('//button[@type="submit"]')
|
||||||
expected_btn_name = 'Request new API token'
|
expected_btn_name = 'Request new API token'
|
||||||
# check if the request token button is enabled
|
# check if the request token button is enabled
|
||||||
# check the buttons name
|
# check the buttons name
|
||||||
@@ -455,7 +513,7 @@ async def test_token_request_form_and_panel(app, browser, user_special_chars):
|
|||||||
expected_panel_token_heading = "Your new API Token"
|
expected_panel_token_heading = "Your new API Token"
|
||||||
token_area = browser.locator('#token-area')
|
token_area = browser.locator('#token-area')
|
||||||
await expect(token_area).to_be_visible()
|
await expect(token_area).to_be_visible()
|
||||||
token_area_heading = token_area.locator('//div[@class="panel-heading"]')
|
token_area_heading = token_area.locator('div.card-header')
|
||||||
await expect(token_area_heading).to_have_text(expected_panel_token_heading)
|
await expect(token_area_heading).to_have_text(expected_panel_token_heading)
|
||||||
token_result = browser.locator('#token-result')
|
token_result = browser.locator('#token-result')
|
||||||
await expect(token_result).not_to_be_empty()
|
await expect(token_result).not_to_be_empty()
|
||||||
@@ -463,7 +521,7 @@ async def test_token_request_form_and_panel(app, browser, user_special_chars):
|
|||||||
# verify that "Your new API Token" panel is hidden after refresh the page
|
# verify that "Your new API Token" panel is hidden after refresh the page
|
||||||
await browser.reload(wait_until="load")
|
await browser.reload(wait_until="load")
|
||||||
await expect(token_area).to_be_hidden()
|
await expect(token_area).to_be_hidden()
|
||||||
api_token_table_area = browser.locator('//div[@class="row"]').nth(2)
|
api_token_table_area = browser.locator("div#api-tokens-section")
|
||||||
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
||||||
expected_table_name = "API Tokens"
|
expected_table_name = "API Tokens"
|
||||||
await expect(api_token_table_area.get_by_role("heading")).to_have_text(
|
await expect(api_token_table_area.get_by_role("heading")).to_have_text(
|
||||||
@@ -516,7 +574,7 @@ async def test_request_token_expiration(
|
|||||||
# reload the page
|
# reload the page
|
||||||
await browser.reload(wait_until="load")
|
await browser.reload(wait_until="load")
|
||||||
# API Tokens table: verify that elements are displayed
|
# API Tokens table: verify that elements are displayed
|
||||||
api_token_table_area = browser.locator("div#api-tokens-section").nth(0)
|
api_token_table_area = browser.locator("div#api-tokens-section")
|
||||||
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
||||||
await expect(api_token_table_area.locator("tr.token-row")).to_have_count(1)
|
await expect(api_token_table_area.locator("tr.token-row")).to_have_count(1)
|
||||||
|
|
||||||
@@ -619,12 +677,14 @@ async def test_request_token_permissions(
|
|||||||
error_message = await error_dialog.locator(".modal-body").inner_text()
|
error_message = await error_dialog.locator(".modal-body").inner_text()
|
||||||
assert "API request failed (400)" in error_message
|
assert "API request failed (400)" in error_message
|
||||||
assert expected_error in error_message
|
assert expected_error in error_message
|
||||||
|
await error_dialog.locator("button[aria-label='Close']").click()
|
||||||
|
await expect(error_dialog).not_to_be_visible()
|
||||||
return
|
return
|
||||||
|
|
||||||
await browser.reload(wait_until="load")
|
await browser.reload(wait_until="load")
|
||||||
|
|
||||||
# API Tokens table: verify that elements are displayed
|
# API Tokens table: verify that elements are displayed
|
||||||
api_token_table_area = browser.locator("div#api-tokens-section").nth(0)
|
api_token_table_area = browser.locator("div#api-tokens-section")
|
||||||
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
await expect(api_token_table_area.get_by_role("table")).to_be_visible()
|
||||||
await expect(api_token_table_area.locator("tr.token-row")).to_have_count(1)
|
await expect(api_token_table_area.locator("tr.token-row")).to_have_count(1)
|
||||||
|
|
||||||
@@ -670,9 +730,7 @@ async def test_revoke_token(app, browser, token_type, user_special_chars):
|
|||||||
await browser.wait_for_load_state("load")
|
await browser.wait_for_load_state("load")
|
||||||
await expect(browser).to_have_url(re.compile(".*/hub/token"))
|
await expect(browser).to_have_url(re.compile(".*/hub/token"))
|
||||||
if token_type == "both" or token_type == "request_by_user":
|
if token_type == "both" or token_type == "request_by_user":
|
||||||
request_btn = browser.locator('//div[@class="text-center"]').get_by_role(
|
request_btn = browser.locator('//button[@type="submit"]')
|
||||||
"button"
|
|
||||||
)
|
|
||||||
await request_btn.click()
|
await request_btn.click()
|
||||||
# wait for token response to show up on the page
|
# wait for token response to show up on the page
|
||||||
await browser.wait_for_load_state("load")
|
await browser.wait_for_load_state("load")
|
||||||
@@ -879,9 +937,9 @@ async def test_oauth_page(
|
|||||||
|
|
||||||
# login user
|
# login user
|
||||||
await login(browser, user.name, password=str(user.name))
|
await login(browser, user.name, password=str(user.name))
|
||||||
auth_btn = browser.locator('//input[@type="submit"]')
|
auth_btn = browser.locator('//button[@type="submit"]')
|
||||||
await expect(auth_btn).to_be_enabled()
|
await expect(auth_btn).to_be_enabled()
|
||||||
text_permission = browser.get_by_role("paragraph")
|
text_permission = browser.get_by_role("paragraph").nth(1)
|
||||||
await expect(text_permission).to_contain_text(f"JupyterHub service {service.name}")
|
await expect(text_permission).to_contain_text(f"JupyterHub service {service.name}")
|
||||||
await expect(text_permission).to_contain_text(f"oauth URL: {expected_redirect_url}")
|
await expect(text_permission).to_contain_text(f"oauth URL: {expected_redirect_url}")
|
||||||
|
|
||||||
@@ -1348,7 +1406,7 @@ async def test_singleuser_xsrf(
|
|||||||
# visit target user, sets credentials for second server
|
# visit target user, sets credentials for second server
|
||||||
await browser.goto(public_url(app, target_user))
|
await browser.goto(public_url(app, target_user))
|
||||||
await expect(browser).to_have_url(re.compile(r".*/oauth2/authorize"))
|
await expect(browser).to_have_url(re.compile(r".*/oauth2/authorize"))
|
||||||
auth_button = browser.locator('//input[@type="submit"]')
|
auth_button = browser.locator('//button[@type="submit"]')
|
||||||
await expect(auth_button).to_be_enabled()
|
await expect(auth_button).to_be_enabled()
|
||||||
await auth_button.click()
|
await auth_button.click()
|
||||||
await expect(browser).to_have_url(re.compile(rf".*/user/{target_user.name}/.*"))
|
await expect(browser).to_have_url(re.compile(rf".*/user/{target_user.name}/.*"))
|
||||||
|
@@ -48,16 +48,16 @@ async def test_share_code_flow_full(app, browser, full_spawn, create_user_with_s
|
|||||||
# back to accept-share page
|
# back to accept-share page
|
||||||
await expect(browser).to_have_url(re.compile(r".*/accept-share"))
|
await expect(browser).to_have_url(re.compile(r".*/accept-share"))
|
||||||
|
|
||||||
header_text = await browser.locator("//h2").first.text_content()
|
header_text = await browser.locator("p.lead").first.text_content()
|
||||||
assert f"access {user.name}'s server" in header_text
|
assert f"access {user.name}'s server" in header_text
|
||||||
assert f"You ({share_user.name})" in header_text
|
assert f"You ({share_user.name})" in header_text
|
||||||
# TODO verify form
|
# TODO verify form
|
||||||
submit = browser.locator('//input[@type="submit"]')
|
submit = browser.locator('//button[@type="submit"]')
|
||||||
await submit.click()
|
await submit.click()
|
||||||
|
|
||||||
# redirects to server, which triggers oauth approval
|
# redirects to server, which triggers oauth approval
|
||||||
await expect(browser).to_have_url(re.compile(r".*/oauth2/authorize"))
|
await expect(browser).to_have_url(re.compile(r".*/oauth2/authorize"))
|
||||||
submit = browser.locator('//input[@type="submit"]')
|
submit = browser.locator('//button[@type="submit"]')
|
||||||
await submit.click()
|
await submit.click()
|
||||||
|
|
||||||
# finally, we are at the server!
|
# finally, we are at the server!
|
||||||
|
@@ -1328,7 +1328,7 @@ async def test_services_nav_links(
|
|||||||
r = await get_page("home", app, cookies=cookies)
|
r = await get_page("home", app, cookies=cookies)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
page = BeautifulSoup(r.text)
|
page = BeautifulSoup(r.text)
|
||||||
nav = page.find("ul", class_="nav")
|
nav = page.find("ul", class_="navbar-nav")
|
||||||
# find service links
|
# find service links
|
||||||
nav_urls = [a["href"] for a in nav.find_all("a")]
|
nav_urls = [a["href"] for a in nav.find_all("a")]
|
||||||
if present:
|
if present:
|
||||||
|
403
package-lock.json
generated
403
package-lock.json
generated
@@ -10,209 +10,205 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^3.4.1",
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||||
"font-awesome": "^4.7.0",
|
"bootstrap": "^5.3.0",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"requirejs": "^2.3.6"
|
"requirejs": "^2.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"less": "^3.9.0",
|
"sass": "^1.74.1"
|
||||||
"less-plugin-clean-css": "^1.5.1",
|
|
||||||
"prettier": "^1.16.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/amdefine": {
|
"node_modules/@fortawesome/fontawesome-free": {
|
||||||
"version": "1.0.1",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz",
|
||||||
"integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==",
|
"integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==",
|
||||||
"dev": true,
|
"hasInstallScript": true,
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bootstrap": {
|
|
||||||
"version": "3.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz",
|
|
||||||
"integrity": "sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clean-css": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "3.4.28",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
"integrity": "sha512-aTWyttSdI2mYi07kWqHi24NUU9YlELFKGOAgFzZjDN1064DMAOy2FBuoyGmkKRlXkbpXd0EVHmiVkbKhKoirTw==",
|
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||||
"dev": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"funding": {
|
||||||
"commander": "2.8.x",
|
"type": "opencollective",
|
||||||
"source-map": "0.4.x"
|
"url": "https://opencollective.com/popperjs"
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"cleancss": "bin/cleancss"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clean-css/node_modules/source-map": {
|
"node_modules/anymatch": {
|
||||||
"version": "0.4.4",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"amdefine": ">=0.0.4"
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.8.1",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
"integrity": "sha512-+pJLBFVk+9ZZdlAOB5WuIElVPPth47hILFkmGym57aq8kwxsowvByvB0DHs1vQAhyMZzdcpTtF0VDKGkSDR4ZQ==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
|
||||||
"graceful-readlink": ">= 1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.x"
|
"node": ">=8"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/copy-anything": {
|
|
||||||
"version": "2.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
|
|
||||||
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-what": "^3.14.1"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/errno": {
|
"node_modules/bootstrap": {
|
||||||
"version": "0.1.8",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
|
||||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/twbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/bootstrap"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/braces": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prr": "~1.0.1"
|
"fill-range": "^7.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
|
||||||
"errno": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/font-awesome": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.3"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.2.11",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/graceful-readlink": {
|
"node_modules/fill-range": {
|
||||||
"version": "1.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
"integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==",
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/image-size": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "0.5.5",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"dependencies": {
|
||||||
"bin": {
|
"binary-extensions": "^2.0.0"
|
||||||
"image-size": "bin/image-size.js"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-what": {
|
"node_modules/is-number": {
|
||||||
"version": "3.14.1",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jquery": {
|
"node_modules/jquery": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
|
||||||
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
|
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
|
||||||
},
|
},
|
||||||
"node_modules/less": {
|
|
||||||
"version": "3.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
|
|
||||||
"integrity": "sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"copy-anything": "^2.0.1",
|
|
||||||
"tslib": "^1.10.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"lessc": "bin/lessc"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"errno": "^0.1.1",
|
|
||||||
"graceful-fs": "^4.1.2",
|
|
||||||
"image-size": "~0.5.0",
|
|
||||||
"make-dir": "^2.1.0",
|
|
||||||
"mime": "^1.4.1",
|
|
||||||
"native-request": "^1.0.5",
|
|
||||||
"source-map": "~0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/less-plugin-clean-css": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/less-plugin-clean-css/-/less-plugin-clean-css-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-Pc68AFHAEJO3aAoRvnUTW5iAiAv6y+TQsWLTTwVNqjiDno6xCvxz1AtfQl7Y0MZSpHPalFajM1EU4RB5UVINpw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"clean-css": "^3.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/make-dir": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"pify": "^4.0.1",
|
|
||||||
"semver": "^5.6.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
@@ -221,42 +217,39 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/native-request": {
|
"node_modules/normalize-path": {
|
||||||
"version": "1.1.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/native-request/-/native-request-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-uZ5rQaeRn15XmpgE0xoPL8YWqcX90VtCFglYwAgkvKM5e8fog+vePLAhHxuuv/gRkrQxIeh5U3q9sMNUrENqWw==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/pify": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/picomatch": {
|
||||||
"version": "1.19.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"engines": {
|
||||||
"prettier": "bin-prettier.js"
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prr": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/requirejs": {
|
"node_modules/requirejs": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz",
|
||||||
@@ -269,31 +262,43 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/sass": {
|
||||||
"version": "5.7.2",
|
"version": "1.74.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz",
|
||||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
"integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
"dependencies": {
|
||||||
|
"chokidar": ">=3.0.0 <4.0.0",
|
||||||
|
"immutable": "^4.0.0",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver"
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map-js": {
|
||||||
"version": "0.6.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "1.14.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
package.json
12
package.json
@@ -10,17 +10,15 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "python3 ./bower-lite",
|
"postinstall": "python3 ./bower-lite",
|
||||||
"fmt": "prettier --write --trailing-comma es5 share/jupyterhub/static/js/*",
|
"css": "sass --style compressed -I share/jupyterhub/static/components --source-map share/jupyterhub/static/scss/style.scss:share/jupyterhub/static/css/style.min.css",
|
||||||
"lessc": "lessc"
|
"build:watch": "npm run css -- --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"less": "^3.9.0",
|
"sass": "^1.74.1"
|
||||||
"less-plugin-clean-css": "^1.5.1",
|
|
||||||
"prettier": "^1.16.4"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap": "^3.4.1",
|
"@fortawesome/fontawesome-free": "^6.1.1",
|
||||||
"font-awesome": "^4.7.0",
|
"bootstrap": "^5.3.0",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"requirejs": "^2.3.6"
|
"requirejs": "^2.3.6"
|
||||||
|
52
setup.py
52
setup.py
@@ -113,27 +113,34 @@ class NPM(BaseCommand):
|
|||||||
|
|
||||||
|
|
||||||
class CSS(BaseCommand):
|
class CSS(BaseCommand):
|
||||||
description = "compile CSS from LESS"
|
description = "compile CSS"
|
||||||
|
|
||||||
def should_run(self):
|
def should_run(self):
|
||||||
"""Does less need to run?"""
|
"""Does CSS need to run?"""
|
||||||
# from IPython.html.tasks.py
|
|
||||||
|
|
||||||
css_targets = [pjoin(static, 'css', 'style.min.css')]
|
css_targets = [pjoin(static, 'css', 'style.min.css')]
|
||||||
css_maps = [t + '.map' for t in css_targets]
|
css_maps = [t + '.map' for t in css_targets]
|
||||||
targets = css_targets + css_maps
|
targets = css_targets + css_maps
|
||||||
if not all(os.path.exists(t) for t in targets):
|
earliest_target_mtime = float('inf')
|
||||||
# some generated files don't exist
|
earliest_target_name = ''
|
||||||
return True
|
for t in targets:
|
||||||
earliest_target = sorted(mtime(t) for t in targets)[0]
|
if not os.path.exists(t):
|
||||||
|
print(f"Need to build css target: {t}")
|
||||||
|
return True
|
||||||
|
target_mtime = mtime(t)
|
||||||
|
if target_mtime < earliest_target_mtime:
|
||||||
|
earliest_target_name = t
|
||||||
|
earliest_target_mtime = target_mtime
|
||||||
|
|
||||||
# check if any .less files are newer than the generated targets
|
# check if any .scss files are newer than the generated targets
|
||||||
for dirpath, dirnames, filenames in os.walk(static):
|
for dirpath, dirnames, filenames in os.walk(static):
|
||||||
for f in filenames:
|
for f in filenames:
|
||||||
if f.endswith('.less'):
|
if f.endswith('.scss'):
|
||||||
path = pjoin(static, dirpath, f)
|
path = pjoin(static, dirpath, f)
|
||||||
timestamp = mtime(path)
|
timestamp = mtime(path)
|
||||||
if timestamp > earliest_target:
|
if timestamp > earliest_target_mtime:
|
||||||
|
print(
|
||||||
|
f"mtime for {path} > {earliest_target_name}, needs update"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -144,33 +151,18 @@ class CSS(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.run_command('js')
|
self.run_command('js')
|
||||||
print("Building css with less")
|
print("Building css")
|
||||||
|
|
||||||
style_less = pjoin(static, 'less', 'style.less')
|
args = ['npm', 'run', 'css']
|
||||||
style_css = pjoin(static, 'css', 'style.min.css')
|
|
||||||
sourcemap = style_css + '.map'
|
|
||||||
|
|
||||||
args = [
|
|
||||||
'npm',
|
|
||||||
'run',
|
|
||||||
'lessc',
|
|
||||||
'--',
|
|
||||||
'--clean-css',
|
|
||||||
f'--source-map-basepath={static}',
|
|
||||||
f'--source-map={sourcemap}',
|
|
||||||
'--source-map-rootpath=../',
|
|
||||||
style_less,
|
|
||||||
style_css,
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
check_call(args, cwd=here, shell=shell)
|
check_call(args, cwd=here, shell=shell)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print("Failed to run lessc: %s" % e, file=sys.stderr)
|
print("Failed to build css: %s" % e, file=sys.stderr)
|
||||||
print("You can install js dependencies with `npm install`", file=sys.stderr)
|
print("You can install js dependencies with `npm install`", file=sys.stderr)
|
||||||
raise
|
raise
|
||||||
# update data-files in case this created new files
|
# update data-files in case this created new files
|
||||||
self.distribution.data_files = get_data_files()
|
self.distribution.data_files = get_data_files()
|
||||||
assert not self.should_run(), 'CSS.run failed'
|
assert not self.should_run(), 'CSS.run did not produce up-to-date output'
|
||||||
|
|
||||||
|
|
||||||
class JSX(BaseCommand):
|
class JSX(BaseCommand):
|
||||||
|
@@ -118,7 +118,8 @@ define(["jquery"], function ($) {
|
|||||||
var msg = log_ajax_error(jqXHR, status, error);
|
var msg = log_ajax_error(jqXHR, status, error);
|
||||||
var dialog = $("#error-dialog");
|
var dialog = $("#error-dialog");
|
||||||
dialog.find(".ajax-error").text(msg);
|
dialog.find(".ajax-error").text(msg);
|
||||||
dialog.modal();
|
var modal = new bootstrap.Modal(dialog[0]);
|
||||||
|
modal.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
var utils = {
|
var utils = {
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
/*!
|
|
||||||
*
|
|
||||||
* Twitter Bootstrap
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@import "../components/bootstrap/less/bootstrap.less";
|
|
||||||
@import "../components/bootstrap/less/responsive-utilities.less";
|
|
||||||
|
|
||||||
/*!
|
|
||||||
*
|
|
||||||
* Font Awesome
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@import "../components/font-awesome/less/font-awesome.less";
|
|
||||||
@fa-font-path: "../components/font-awesome/fonts";
|
|
||||||
|
|
||||||
/*!
|
|
||||||
*
|
|
||||||
* Jupyter
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
@import "./variables.less";
|
|
||||||
@import "./page.less";
|
|
||||||
@import "./admin.less";
|
|
||||||
@import "./error.less";
|
|
||||||
@import "./login.less";
|
|
@@ -1,27 +0,0 @@
|
|||||||
@border-radius-small: 2px;
|
|
||||||
@border-radius-base: 2px;
|
|
||||||
@border-radius-large: 3px;
|
|
||||||
@navbar-height: 40px;
|
|
||||||
@grid-float-breakpoint: @screen-xs-min;
|
|
||||||
|
|
||||||
@navbar-default-color: #222;
|
|
||||||
@navbar-default-link-color: @navbar-default-color;
|
|
||||||
// darken background on hover, no change to text
|
|
||||||
@navbar-default-link-hover-color: @navbar-default-color;
|
|
||||||
@navbar-default-link-hover-bg: darken(@navbar-default-bg, 10%);
|
|
||||||
|
|
||||||
@jupyter-orange: #f37524;
|
|
||||||
|
|
||||||
@jupyter-red: #e34f21;
|
|
||||||
// color blind-friendly alternative to red/green
|
|
||||||
// from 5-class RdYlBu via colorbrewer.org
|
|
||||||
// eliminate distinction between 'primary' and 'success'
|
|
||||||
@brand-primary: #2c7bb6;
|
|
||||||
@brand-success: @brand-primary;
|
|
||||||
@brand-danger: #d7191c;
|
|
||||||
|
|
||||||
@text-muted: #222;
|
|
||||||
|
|
||||||
.btn-jupyter {
|
|
||||||
.button-variant(#fff; @jupyter-orange; @jupyter-red);
|
|
||||||
}
|
|
20
share/jupyterhub/static/scss/cssvariables.css
Normal file
20
share/jupyterhub/static/scss/cssvariables.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/* CSS variables
|
||||||
|
note: SCSS variable overrides must be loaded _before_ bootstrap (variables.scss)
|
||||||
|
while CSS variable overrides must be loaded _after_ bootstrap (cssvariables.scss)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
/* higher contrast nav links by default */
|
||||||
|
--bs-navbar-color: rgba(black, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
/* no color change on nav links
|
||||||
|
darken background on hover, no change to text
|
||||||
|
background part is in page.scss
|
||||||
|
*/
|
||||||
|
|
||||||
|
--bs-nav-link-color: var(--bs-navbar-color);
|
||||||
|
--bs-nav-link-hover-color: var(--bs-nav-link-color);
|
||||||
|
--bs-nav-link-active-color: var(--bs-nav-link-color);
|
||||||
|
}
|
@@ -6,7 +6,6 @@ div.error {
|
|||||||
div.ajax-error {
|
div.ajax-error {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
.alert-danger();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.error > h1 {
|
div.error > h1 {
|
@@ -3,21 +3,19 @@
|
|||||||
height: 80vh;
|
height: 80vh;
|
||||||
|
|
||||||
& #insecure-login-warning {
|
& #insecure-login-warning {
|
||||||
.bg-warning();
|
background-color: $warning-bg-subtle;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-login {
|
.service-login {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: table-cell;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: auto auto 20% auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
display: table-cell;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin: auto auto 20% auto;
|
margin: auto;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,9 +28,9 @@
|
|||||||
.auth-form-header {
|
.auth-form-header {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: @jupyter-orange;
|
background: $jupyter-orange;
|
||||||
border-radius: @border-radius-large @border-radius-large 0 0;
|
|
||||||
font-size: large;
|
font-size: large;
|
||||||
|
border-radius: $border-radius-large $border-radius-large 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-form-header > h1 {
|
.auth-form-header > h1 {
|
||||||
@@ -45,6 +43,6 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: thin silver solid;
|
border: thin silver solid;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0 0 @border-radius-large @border-radius-large;
|
border-radius: 0 0 $border-radius-large $border-radius-large;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,15 +1,25 @@
|
|||||||
@import "../components/bootstrap/less/variables.less";
|
$logo-height: 28px;
|
||||||
|
$grid-float-breakpoint: map-get($grid-breakpoints, "sm");
|
||||||
@logo-height: 28px;
|
|
||||||
|
|
||||||
#jupyterhub-logo {
|
#jupyterhub-logo {
|
||||||
@media (max-width: @grid-float-breakpoint) {
|
@media (max-width: $grid-float-breakpoint) {
|
||||||
// same length as the navbar-toggle element, displayed on responsive mode
|
// same length as the navbar-toggle element, displayed on responsive mode
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
.jpy-logo {
|
.jpy-logo {
|
||||||
height: @logo-height;
|
height: $logo-height;
|
||||||
margin-top: (@navbar-height - @logo-height) / 2;
|
margin-top: calc($navbar-brand-height - $logo-height) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav {
|
||||||
|
.nav-link {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
// no color change
|
||||||
|
color: var(--#{$prefix}navbar-color);
|
||||||
|
background-color: darken($body-tertiary-bg, 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +28,7 @@
|
|||||||
span {
|
span {
|
||||||
// same as .nav > li > a from bootstrap, but applied to the span[id="login_widget"]
|
// same as .nav > li > a from bootstrap, but applied to the span[id="login_widget"]
|
||||||
// or any other span that matches .nav > li > span, but only in responsive mode
|
// or any other span that matches .nav > li > span, but only in responsive mode
|
||||||
@media (max-width: @grid-float-breakpoint) {
|
@media (max-width: $grid-float-breakpoint) {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
@@ -68,7 +78,16 @@
|
|||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
||||||
0 0 8px @jupyter-orange;
|
0 0 8px $jupyter-orange;
|
||||||
border-color: @jupyter-orange;
|
border-color: $jupyter-orange;
|
||||||
outline-color: @jupyter-orange;
|
outline-color: $jupyter-orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-jupyter {
|
||||||
|
@include button-variant(
|
||||||
|
$background: $jupyter-orange,
|
||||||
|
$border: $jupyter-red,
|
||||||
|
$color: #fff,
|
||||||
|
$hover-color: #fff
|
||||||
|
);
|
||||||
}
|
}
|
71
share/jupyterhub/static/scss/style.scss
Normal file
71
share/jupyterhub/static/scss/style.scss
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*!
|
||||||
|
*
|
||||||
|
* Bootstrap
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
|
||||||
|
@import "../components/bootstrap/scss/functions"; // Required
|
||||||
|
|
||||||
|
// 2. Include any default variable overrides here
|
||||||
|
@import "./variables.scss";
|
||||||
|
|
||||||
|
@import "../components/bootstrap/scss/bootstrap"; // Full bootstrap (maybe wasteful?)
|
||||||
|
|
||||||
|
// // 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
|
||||||
|
// @import "../components/bootstrap/scss/variables"; // Required
|
||||||
|
// @import "../components/bootstrap/scss/variables-dark"; // Required
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// // 4. Include any default map overrides here
|
||||||
|
//
|
||||||
|
// // 5. Include remainder of required parts
|
||||||
|
// @import "../components/bootstrap/scss/maps"; // Required
|
||||||
|
// @import "../components/bootstrap/scss/mixins"; // Required
|
||||||
|
// @import "../components/bootstrap/scss/root"; // Required
|
||||||
|
//
|
||||||
|
// // 6. Optionally include any other parts as needed
|
||||||
|
// @import "../components/bootstrap/scss/utilities";
|
||||||
|
// @import "../components/bootstrap/scss/reboot";
|
||||||
|
// @import "../components/bootstrap/scss/type";
|
||||||
|
// @import "../components/bootstrap/scss/images";
|
||||||
|
// @import "../components/bootstrap/scss/navbar";
|
||||||
|
// @import "../components/bootstrap/scss/alert";
|
||||||
|
// @import "../components/bootstrap/scss/buttons";
|
||||||
|
// @import "../components/bootstrap/scss/containers";
|
||||||
|
// @import "../components/bootstrap/scss/grid";
|
||||||
|
// @import "../components/bootstrap/scss/modal";
|
||||||
|
|
||||||
|
// 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
|
||||||
|
// @import "../components/bootstrap/scss/utilities/api";
|
||||||
|
|
||||||
|
// CSS variables must be loaded _after_ bootstrap to override
|
||||||
|
@import "./cssvariables";
|
||||||
|
|
||||||
|
// redefine .btn-xs, removed in bootstrap 4
|
||||||
|
.btn-xs {
|
||||||
|
// $padding-y, $padding-x, $font-size, $border-radius
|
||||||
|
@include button-size(1px, 5px, 14px, 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
*
|
||||||
|
* Font Awesome
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
$fa-font-path: "../components/@fortawesome/fontawesome-free/webfonts";
|
||||||
|
@import "../components/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||||
|
// You can include all the other styles the same as before
|
||||||
|
@import "../components/@fortawesome/fontawesome-free/scss/regular.scss";
|
||||||
|
@import "../components/@fortawesome/fontawesome-free/scss/solid.scss";
|
||||||
|
|
||||||
|
/*!
|
||||||
|
*
|
||||||
|
* Jupyter
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import "./page.scss";
|
||||||
|
@import "./admin.scss";
|
||||||
|
@import "./error.scss";
|
||||||
|
@import "./login.scss";
|
11
share/jupyterhub/static/scss/variables.scss
Normal file
11
share/jupyterhub/static/scss/variables.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
$border-radius-large: 5px;
|
||||||
|
|
||||||
|
$jupyter-orange: #f37524;
|
||||||
|
$jupyter-red: #e34f21;
|
||||||
|
|
||||||
|
// accessible alternative to red/green
|
||||||
|
// from 5-class RdYlBu via colorbrewer.org
|
||||||
|
// eliminate distinction between 'primary' and 'success'
|
||||||
|
$primary: #2c7bb6;
|
||||||
|
$success: $primary;
|
||||||
|
$danger: #d7191c;
|
@@ -3,13 +3,14 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container col-md-6 col-md-offset-3">
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
<h1 class="text-center">Accept sharing invitation</h1>
|
<h1 class="text-center">Accept sharing invitation</h1>
|
||||||
|
<p class="lead">
|
||||||
<h2>
|
|
||||||
You ({{ user.name }}) have been invited to access {{ owner.name }}'s server
|
You ({{ user.name }}) have been invited to access {{ owner.name }}'s server
|
||||||
{%- if spawner.name %} ({{ spawner.name }}){%- endif %} at <a href="{{ spawner_url | safe }}">{{ spawner_url }}</a>.
|
{%- if spawner.name %} ({{ spawner.name }}){%- endif %} at <a href="{{ spawner_url | safe }}">{{ spawner_url }}</a>
|
||||||
</h2>
|
</p>
|
||||||
|
|
||||||
{% if not spawner_ready %}
|
{% if not spawner_ready %}
|
||||||
<p class="alert alert-danger">
|
<p class="alert alert-danger">
|
||||||
@@ -19,18 +20,18 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<form method="POST" action="">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
By accepting the invitation, you will be granted the following permissions,
|
By accepting the invitation, you will be granted the following permissions,
|
||||||
restricted to this particular server:
|
restricted to this particular server:
|
||||||
</p>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<div>
|
|
||||||
<form method="POST" action="">
|
|
||||||
{# these are the 'real' inputs to the form -#}
|
{# these are the 'real' inputs to the form -#}
|
||||||
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
|
<input type="hidden" name="_xsrf" value="{{ xsrf }}" />
|
||||||
<input type="hidden" name="code" value="{{ code }}" />
|
<input type="hidden" name="code" value="{{ code }}" />
|
||||||
{% for scope_info in scope_descriptions -%}
|
{% for scope_info in scope_descriptions -%}
|
||||||
<div class="checkbox input-group">
|
<div class="form-check input-group">
|
||||||
<label>
|
<label>
|
||||||
<span>
|
<span>
|
||||||
{{ scope_info['description'] }}
|
{{ scope_info['description'] }}
|
||||||
@@ -40,12 +41,15 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
<p>
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="submit" class="form-control btn btn-jupyter" >Accept invitation</button>
|
||||||
|
<p class="small">
|
||||||
After accepting the invitation, you will be redirected to <a href="{{ next_url | safe }}">{{ next_url }}</a>.
|
After accepting the invitation, you will be redirected to <a href="{{ next_url | safe }}">{{ next_url }}</a>.
|
||||||
</p>
|
</p>
|
||||||
<input type="submit" value="Accept invitation" class="form-control btn-jupyter" />
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
</div></div></div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -11,9 +11,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<div class="container-fluid navbar-default small version_footer">
|
<div class="py-2 px-4 bg-body-tertiary small version_footer">
|
||||||
<div class="navbar-text">
|
|
||||||
JupyterHub {{ server_version }}
|
JupyterHub {{ server_version }}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -44,10 +44,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="home-server-row add-server-row">
|
<tr class="home-server-row add-server-row">
|
||||||
<td colspan="4">
|
<td colspan="4">
|
||||||
<input class="new-server-name" placeholder="Name your server">
|
<input class="new-server-name" aria-label="server name" placeholder="name-your-server">
|
||||||
<a role="button" class="new-server-btn" class="add-server btn btn-xs btn-primary">
|
<button role="button" type="button" class="new-server-btn btn btn-xs btn-primary">
|
||||||
Add New Server
|
Add New Server
|
||||||
</a>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for spawner in named_spawners %}
|
{% for spawner in named_spawners %}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
>
|
>
|
||||||
start
|
start
|
||||||
</a>
|
</a>
|
||||||
<a role="button" class="delete-server btn btn-xs btn-danger{% if spawner.active %} hidden{% endif %}" id="delete-{{ spawner.name }}">delete</a>
|
<button role="button" class="delete-server btn btn-xs btn-danger{% if spawner.active %} hidden{% endif %}" id="delete-{{ spawner.name }}">delete</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
<div class="auth-form-header">
|
<div class="auth-form-header">
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class='auth-form-body'>
|
<div class='auth-form-body m-auto'>
|
||||||
|
|
||||||
<p id='insecure-login-warning' class='hidden'>
|
<p id='insecure-login-warning' class='hidden'>
|
||||||
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
|
Warning: JupyterHub seems to be served over an unsecured HTTP connection.
|
||||||
|
@@ -4,13 +4,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container col-md-6 col-md-offset-3">
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
<h1 class="text-center">Authorize access</h1>
|
<h1 class="text-center">Authorize access</h1>
|
||||||
|
<p class="lead">
|
||||||
<h2>
|
|
||||||
An application is requesting authorization to access data associated with your JupyterHub account
|
An application is requesting authorization to access data associated with your JupyterHub account
|
||||||
</h2>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{ oauth_client.description }} (oauth URL: {{ oauth_client.redirect_uri }})
|
{{ oauth_client.description }} (oauth URL: {{ oauth_client.redirect_uri }})
|
||||||
would like permission to identify you.
|
would like permission to identify you.
|
||||||
@@ -19,10 +19,14 @@
|
|||||||
your behalf.
|
your behalf.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>This will grant the application permission to:</h3>
|
<form method="POST" action="">
|
||||||
<div>
|
<div class="card">
|
||||||
<form method="POST" action="">
|
<div class="card-header">
|
||||||
|
<p class="h5">This will grant the application permission to:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<input type="hidden" name="_xsrf" value="{{ xsrf }}"/>
|
<input type="hidden" name="_xsrf" value="{{ xsrf }}"/>
|
||||||
{# these are the 'real' inputs to the form -#}
|
{# these are the 'real' inputs to the form -#}
|
||||||
{% for scope in allowed_scopes %}
|
{% for scope in allowed_scopes %}
|
||||||
@@ -40,13 +44,17 @@
|
|||||||
{% if scope_info['filter'] %}
|
{% if scope_info['filter'] %}
|
||||||
Applies to {{ scope_info['filter'] }}.
|
Applies to {{ scope_info['filter'] }}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<input type="submit" value="Authorize" class="form-control btn-jupyter"/>
|
</div>
|
||||||
</form>
|
<div class="card-footer">
|
||||||
</div>
|
<button type="submit" class="form-control btn btn-jupyter mt-2">Authorize</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -5,15 +5,14 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
<h2 class="modal-title" id="{{key}}-label">{{title}}</h1>
|
||||||
<h1 class="modal-title" id="{{key}}-label">{{title}}</h1>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
<button type="button" class="btn {{btn_class}}" data-bs-dismiss="modal" data-dismiss="modal">{{btn_label}}</button>
|
||||||
<button type="button" class="btn {{btn_class}}" data-dismiss="modal" data-dismiss="modal">{{btn_label}}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,9 +37,9 @@
|
|||||||
<link rel="icon" href="{{ static_url("favicon.ico") }}" type="image/x-icon">
|
<link rel="icon" href="{{ static_url("favicon.ico") }}" type="image/x-icon">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.bundle.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
|
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
<script src="{{static_url("components/jquery/dist/jquery.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||||
<script src="{{static_url("components/bootstrap/dist/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<script>
|
<script>
|
||||||
require.config({
|
require.config({
|
||||||
@@ -51,15 +50,8 @@
|
|||||||
paths: {
|
paths: {
|
||||||
components: '../components',
|
components: '../components',
|
||||||
jquery: '../components/jquery/dist/jquery.min',
|
jquery: '../components/jquery/dist/jquery.min',
|
||||||
bootstrap: '../components/bootstrap/dist/js/bootstrap.min',
|
|
||||||
moment: "../components/moment/moment",
|
moment: "../components/moment/moment",
|
||||||
},
|
},
|
||||||
shim: {
|
|
||||||
bootstrap: {
|
|
||||||
deps: ["jquery"],
|
|
||||||
exports: "bootstrap"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -104,36 +96,31 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
{% block nav_bar %}
|
{% block nav_bar %}
|
||||||
<nav class="navbar navbar-default">
|
<nav class="navbar navbar-expand-sm bg-body-tertiary mb-4">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="navbar-header">
|
|
||||||
{% block logo %}
|
{% block logo %}
|
||||||
<span id="jupyterhub-logo" class="pull-left">
|
<span id="jupyterhub-logo" class="navbar-brand">
|
||||||
<a href="{{logo_url or base_url}}"><img src='{{base_url}}logo' alt='JupyterHub logo' class='jpy-logo' title='Home'/></a>
|
<a href="{{logo_url or base_url}}"><img src='{{base_url}}logo' alt='JupyterHub logo' class='jpy-logo' title='Home'/></a>
|
||||||
</span>
|
</span>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#thenavbar" aria-expanded="false">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#thenavbar" aria-controls="thenavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="sr-only">Toggle navigation</span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="thenavbar">
|
<div class="collapse navbar-collapse" id="thenavbar">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<ul class="nav navbar-nav">
|
<ul class="navbar-nav me-auto mb-0">
|
||||||
{% block nav_bar_left_items %}
|
{% block nav_bar_left_items %}
|
||||||
<li><a href="{{base_url}}home">Home</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{base_url}}home">Home</a></li>
|
||||||
<li><a href="{{base_url}}token">Token</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{base_url}}token">Token</a></li>
|
||||||
{% if 'admin-ui' in parsed_scopes %}
|
{% if 'admin-ui' in parsed_scopes %}
|
||||||
<li><a href="{{base_url}}admin">Admin</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{base_url}}admin">Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if services %}
|
{% if services %}
|
||||||
<li class="dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Services<span class="caret"></span></a>
|
<a href="#" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-expanded="false">Services</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% for service in services %}
|
{% for service in services %}
|
||||||
{% block service scoped %}
|
{% block service scoped %}
|
||||||
@@ -146,16 +133,16 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav me-2">
|
||||||
{% block nav_bar_right_items %}
|
{% block nav_bar_right_items %}
|
||||||
<li>
|
<li class="nav-item">
|
||||||
{% block login_widget %}
|
{% block login_widget %}
|
||||||
<span id="login_widget">
|
<span id="login_widget">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<p class="navbar-text">{{user.name}}</p>
|
<span class="navbar-text">{{user.name}}</span>
|
||||||
<a id="logout" role="button" class="navbar-btn btn-sm btn btn-default" href="{{logout_url}}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
|
<a id="logout" role="button" class="btn btn-sm btn-outline-dark" href="{{logout_url}}"> <i aria-hidden="true" class="fa fa-sign-out"></i> Logout</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a id="login" role="button" class="btn-sm btn navbar-btn btn-default" href="{{login_url}}">Login</a>
|
<a id="login" role="button" class="btn btn-sm btn-outline-dark" href="{{login_url}}">Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -187,7 +174,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% call modal('Error', btn_label='OK') %}
|
{% call modal('Error', btn_label='OK') %}
|
||||||
<div class="ajax-error">
|
<div class="ajax-error alert-danger">
|
||||||
The error
|
The error
|
||||||
</div>
|
</div>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
@@ -11,12 +11,13 @@
|
|||||||
<h1>Server Options</h1>
|
<h1>Server Options</h1>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<div class="row col-sm-offset-2 col-sm-8">
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
{% if for_user and user.name != for_user.name -%}
|
{% if for_user and user.name != for_user.name -%}
|
||||||
<p>Spawning server for {{ for_user.name }}</p>
|
<p>Spawning server for {{ for_user.name }}</p>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% if error_message -%}
|
{% if error_message -%}
|
||||||
<p class="spawn-error-msg text-danger">
|
<p class="spawn-error-msg alert alert-danger">
|
||||||
Error: {{error_message}}
|
Error: {{error_message}}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
{{spawner_options_form | safe}}
|
{{spawner_options_form | safe}}
|
||||||
<br>
|
<br>
|
||||||
<div class="feedback-container">
|
<div class="feedback-container">
|
||||||
<input type="submit" value="Start" class="btn btn-jupyter form-control">
|
<button type="submit" class="btn btn-jupyter form-control">Start</button>
|
||||||
<div class="feedback-widget hidden">
|
<div class="feedback-widget hidden">
|
||||||
<i class="fa fa-spinner"></i>
|
<i class="fa fa-spinner"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -17,8 +17,8 @@
|
|||||||
<p id="progress-message"></p>
|
<p id="progress-message"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8 col-md-offset-2">
|
<div class="col-md-8">
|
||||||
<details id="progress-details">
|
<details id="progress-details">
|
||||||
<summary>Event log</summary>
|
<summary>Event log</summary>
|
||||||
<div id="progress-log"></div>
|
<div id="progress-log"></div>
|
||||||
|
@@ -4,22 +4,22 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="sr-only">Manage JupyterHub Tokens</h1>
|
<h1 class="sr-only">Manage JupyterHub Tokens</h1>
|
||||||
<div class="row">
|
<div class="row justify-content-center">
|
||||||
<form id="request-token-form" class="col-md-offset-3 col-md-6">
|
<form id="request-token-form" class="col-lg-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="token-note">Note</label>
|
<label for="token-note" class="form-label">Note</label>
|
||||||
<input
|
<input
|
||||||
id="token-note"
|
id="token-note"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
placeholder="note to identify your new token">
|
placeholder="note to identify your new token">
|
||||||
<small id="note-note" class="form-text text-muted">
|
<small id="note-note" class="form-text">
|
||||||
This note will help you keep track of what your tokens are for.
|
This note will help you keep track of what your tokens are for.
|
||||||
</small>
|
</small>
|
||||||
<br/>
|
<br/>
|
||||||
<label for="token-expiration-seconds">Token expires in</label>
|
<label for="token-expiration-seconds" class="form-label">Token expires in</label>
|
||||||
{% block expiration_options %}
|
{% block expiration_options %}
|
||||||
<select id="token-expiration-seconds"
|
<select id="token-expiration-seconds"
|
||||||
class="form-control">
|
class="form-select">
|
||||||
<!-- unit used for each value is `seconds` -->
|
<!-- unit used for each value is `seconds` -->
|
||||||
<option value="3600">1 Hour</option>
|
<option value="3600">1 Hour</option>
|
||||||
<option value="86400">1 Day</option>
|
<option value="86400">1 Day</option>
|
||||||
@@ -27,19 +27,19 @@
|
|||||||
<option value="" selected="selected">Never</option>
|
<option value="" selected="selected">Never</option>
|
||||||
</select>
|
</select>
|
||||||
{% endblock expiration_options %}
|
{% endblock expiration_options %}
|
||||||
<small id="note-expires-at" class="form-text text-muted">
|
<small id="note-expires-at" class="form-text">
|
||||||
You can configure when your token will expire.
|
You can configure when your token will expire.
|
||||||
</small>
|
</small>
|
||||||
<br/>
|
<br/>
|
||||||
<label for="token-scopes">Permissions</label>
|
<label for="token-scopes" class="form-label">Permissions</label>
|
||||||
<input id="token-scopes" class="form-control" placeholder="list of scopes for the token to have, separated by space">
|
<input id="token-scopes" class="form-control" placeholder="list of scopes for the token to have, separated by space">
|
||||||
<small id="note-token-scopes" class="form-text text-muted">
|
<small id="note-token-scopes" class="form-text">
|
||||||
You can limit the permissions of the token so it can only do what you want it to.
|
You can limit the permissions of the token so it can only do what you want it to.
|
||||||
If none are specified, the token will have permission to do everything you can do.
|
If none are specified, the token will have permission to do everything you can do.
|
||||||
See the <a href="https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes">JupyterHub documentation for a list of available scopes</a>.
|
See the <a href="https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes">JupyterHub documentation for a list of available scopes</a>.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center m-4">
|
||||||
<button type="submit" class="btn btn-lg btn-jupyter">
|
<button type="submit" class="btn btn-lg btn-jupyter">
|
||||||
Request new API token
|
Request new API token
|
||||||
</button>
|
</button>
|
||||||
@@ -47,17 +47,17 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row justify-content-center">
|
||||||
<div id="token-area" class="col-md-6 col-md-offset-3" style="display: none;">
|
<div id="token-area" class="col-lg-6" style="display: none;">
|
||||||
<div class="panel panel-default">
|
<div class="card">
|
||||||
<div class="panel-heading">
|
<div class="card-header">
|
||||||
Your new API Token
|
Your new API Token
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="card-body">
|
||||||
<p class="lead text-center">
|
<p class="card-title text-center">
|
||||||
<span id="token-result"></span>
|
<span id="token-result"></span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p class="card-text">
|
||||||
Copy this token. You won't be able to see it again,
|
Copy this token. You won't be able to see it again,
|
||||||
but you can always come back here to get a new one.
|
but you can always come back here to get a new one.
|
||||||
</p>
|
</p>
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
|
|
||||||
{% if api_tokens %}
|
{% if api_tokens %}
|
||||||
<div class="row" id="api-tokens-section">
|
<div class="row" id="api-tokens-section">
|
||||||
|
<div class="col">
|
||||||
<h2>API Tokens</h2>
|
<h2>API Tokens</h2>
|
||||||
<p>
|
<p>
|
||||||
These are tokens with access to the JupyterHub API.
|
These are tokens with access to the JupyterHub API.
|
||||||
@@ -86,10 +87,10 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for token in api_tokens %}
|
{% for token in api_tokens %}
|
||||||
<tr class="token-row" data-token-id="{{token.api_id}}">
|
<tr class="token-row container" data-token-id="{{token.api_id}}">
|
||||||
{% block token_row scoped %}
|
{% block token_row scoped %}
|
||||||
<td class="note-col col-sm-4">{{token.note}}</td>
|
<td class="note-col col">{{token.note}}</td>
|
||||||
<td class="scope-col col-sm-1">
|
<td class="scope-col col">
|
||||||
<details>
|
<details>
|
||||||
<summary>scopes</summary>
|
<summary>scopes</summary>
|
||||||
{% for scope in token.scopes %}
|
{% for scope in token.scopes %}
|
||||||
@@ -97,28 +98,28 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</details>
|
</details>
|
||||||
</td>
|
</td>
|
||||||
<td class="time-col col-sm-3">
|
<td class="time-col col">
|
||||||
{%- if token.last_activity -%}
|
{%- if token.last_activity -%}
|
||||||
{{ token.last_activity.isoformat() + 'Z' }}
|
{{ token.last_activity.isoformat() + 'Z' }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
Never
|
Never
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
<td class="time-col col-sm-3">
|
<td class="time-col col">
|
||||||
{%- if token.created -%}
|
{%- if token.created -%}
|
||||||
{{ token.created.isoformat() + 'Z' }}
|
{{ token.created.isoformat() + 'Z' }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
N/A
|
N/A
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
<td class="time-col col-sm-3">
|
<td class="time-col col">
|
||||||
{%- if token.expires_at -%}
|
{%- if token.expires_at -%}
|
||||||
{{ token.expires_at.isoformat() + 'Z' }}
|
{{ token.expires_at.isoformat() + 'Z' }}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
Never
|
Never
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
</td>
|
</td>
|
||||||
<td class="col-sm-1 text-center">
|
<td class="col text-center">
|
||||||
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
|
<button class="revoke-token-btn btn btn-xs btn-danger">revoke</button>
|
||||||
</td>
|
</td>
|
||||||
{% endblock token_row %}
|
{% endblock token_row %}
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if oauth_clients %}
|
{% if oauth_clients %}
|
||||||
|
@@ -6,16 +6,25 @@ to enable testing without administrative privileges.
|
|||||||
|
|
||||||
c = get_config() # noqa
|
c = get_config() # noqa
|
||||||
|
|
||||||
from jupyterhub.auth import DummyAuthenticator
|
c.JupyterHub.authenticator_class = "dummy"
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = DummyAuthenticator
|
|
||||||
|
|
||||||
# Optionally set a global password that all users must use
|
# Optionally set a global password that all users must use
|
||||||
# c.DummyAuthenticator.password = "your_password"
|
# c.DummyAuthenticator.password = "your_password"
|
||||||
|
|
||||||
from jupyterhub.spawner import SimpleLocalProcessSpawner
|
c.JupyterHub.spawner_class = "simple"
|
||||||
|
|
||||||
c.JupyterHub.spawner_class = SimpleLocalProcessSpawner
|
|
||||||
|
|
||||||
# only listen on localhost for testing
|
# only listen on localhost for testing
|
||||||
c.JupyterHub.bind_url = 'http://127.0.0.1:8000'
|
c.JupyterHub.bind_url = 'http://127.0.0.1:8000'
|
||||||
|
|
||||||
|
# don't cache static files
|
||||||
|
c.JupyterHub.tornado_settings = {
|
||||||
|
"no_cache_static": True,
|
||||||
|
"slow_spawn_timeout": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JupyterHub.allow_named_servers = True
|
||||||
|
c.JupyterHub.default_url = "/hub/home"
|
||||||
|
|
||||||
|
# make sure admin UI is available and any user can login
|
||||||
|
c.Authenticator.admin_users = {"admin"}
|
||||||
|
c.Authenticator.allow_all = True
|
||||||
|
Reference in New Issue
Block a user