diff --git a/.gitignore b/.gitignore index e3ca05fa..7683d902 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ jupyterhub_cookie_secret jupyterhub.sqlite jupyterhub.sqlite* 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.map share/jupyterhub/static/js/admin-react.js* diff --git a/docs/source/contributing/setup.md b/docs/source/contributing/setup.md index 92da6428..161f0ac3 100644 --- a/docs/source/contributing/setup.md +++ b/docs/source/contributing/setup.md @@ -67,10 +67,10 @@ a more detailed discussion. 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 - 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`. @@ -78,7 +78,7 @@ a more detailed discussion. If you do not have access to sudo, you may instead run the following commands: ```bash - npm install configurable-http-proxy yarn + npm install configurable-http-proxy export PATH=$PATH:$(pwd)/node_modules/.bin ``` @@ -87,7 +87,7 @@ a more detailed discussion. If you are using conda you can instead run: ```bash - conda install configurable-http-proxy yarn + conda install configurable-http-proxy ``` 4. Install an editable version of JupyterHub and its requirements for @@ -123,6 +123,14 @@ configuration: 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) & [spawner](LocalProcessSpawner) 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 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 This section lists common ways setting up your development environment may diff --git a/jsx/.gitignore b/jsx/.gitignore index 6ae0fdb6..a3485a53 100644 --- a/jsx/.gitignore +++ b/jsx/.gitignore @@ -1,2 +1,3 @@ node_modules build/admin-react.js +.yarn diff --git a/jsx/src/components/AddUser/AddUser.jsx b/jsx/src/components/AddUser/AddUser.jsx index 9dcc66ae..2310a284 100644 --- a/jsx/src/components/AddUser/AddUser.jsx +++ b/jsx/src/components/AddUser/AddUser.jsx @@ -1,7 +1,9 @@ import React, { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; +import { Button, Col } from "react-bootstrap"; import PropTypes from "prop-types"; +import ErrorAlert from "../../util/error"; const AddUser = (props) => { const [users, setUsers] = useState([]), @@ -27,31 +29,14 @@ const AddUser = (props) => { return ( <>
- {errorAlert != null ? ( -
-
-
- {errorAlert} - -
-
-
- ) : ( - <> - )} +
-
-
-
+ +
+

Add Users

-
+
+

+ setAdmin(!admin)} + /> + +
-
- ) : ( - <> - )} -
-
-
-
-

Editing user {username}

-
-
- -
- -

- setAdmin(!admin)} - /> - - -

- -
- -
-
- - - -
-
-
-
-
- + + + + + + + + + + + + ); }; diff --git a/jsx/src/components/GroupEdit/GroupEdit.jsx b/jsx/src/components/GroupEdit/GroupEdit.jsx index 0f95c62f..b856cb89 100644 --- a/jsx/src/components/GroupEdit/GroupEdit.jsx +++ b/jsx/src/components/GroupEdit/GroupEdit.jsx @@ -2,8 +2,10 @@ import React, { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { Link, useNavigate, useLocation } from "react-router-dom"; import PropTypes from "prop-types"; +import { Button, Card } from "react-bootstrap"; import GroupSelect from "../GroupSelect/GroupSelect"; import DynamicTable from "../DynamicTable/DynamicTable"; +import { MainContainer } from "../../util/layout"; const GroupEdit = (props) => { const [selected, setSelected] = useState([]), @@ -34,8 +36,6 @@ const GroupEdit = (props) => { validateUser, } = props; - console.log("group edit", location, location.state); - useEffect(() => { if (!location.state) { navigate("/groups"); @@ -49,47 +49,26 @@ const GroupEdit = (props) => { const [propvalues, setPropValues] = useState([]); return ( -
- {errorAlert != null ? ( -
-
-
- {errorAlert} - -
-
-
- ) : ( - <> - )} -
-
-

Editing Group {group_data.name}

-

-
Manage group members
-
-
- { - setSelected(selection); - setChanged(true); - }} - /> -
-
-
Manage group properties
-
-
-
-
+ +

Editing Group {group_data.name}

+ + +

Manage group members

+
+ + { + setSelected(selection); + setChanged(true); + }} + /> + + +

Manage group properties

+
+ { //Add keys /> -
-
- -
-
- +
+ +
+ + + + + - -
- -
- -

-

-
-
-
+ +
+ +
+ + + ); }; diff --git a/jsx/src/components/GroupSelect/GroupSelect.jsx b/jsx/src/components/GroupSelect/GroupSelect.jsx index 2f48bf3e..d2619ea4 100644 --- a/jsx/src/components/GroupSelect/GroupSelect.jsx +++ b/jsx/src/components/GroupSelect/GroupSelect.jsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; +import { Button } from "react-bootstrap"; import "./group-select.css"; const GroupSelect = (props) => { @@ -12,92 +13,80 @@ const GroupSelect = (props) => { if (!users) return null; return ( -
+ <> {error != null ? ( -
-
{error}
-
+
{error}
) : ( <> )} -
-
- { - setUsername(e.target.value); - }} - /> - - +
+
+
+
+ {selected.map((e, i) => ( +
{ - validateUser(username).then((exists) => { - if (exists && !selected.includes(username)) { - 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.`); - } - }); + let updated_selection = selected + .slice(0, i) + .concat(selected.slice(i + 1)); + onChange(updated_selection, users); + setSelected(updated_selection); }} > - Add user - - -
-
-
-
-
-
- {selected.map((e, i) => ( + {e} +
+ ))} + {users.map((e, i) => + selected.includes(e) ? undefined : (
{ - let updated_selection = selected - .slice(0, i) - .concat(selected.slice(i + 1)); + let updated_selection = selected.concat([e]); onChange(updated_selection, users); setSelected(updated_selection); }} > {e}
- ))} - {users.map((e, i) => - selected.includes(e) ? undefined : ( -
{ - let updated_selection = selected.concat([e]); - onChange(updated_selection, users); - setSelected(updated_selection); - }} - > - {e} -
- ), - )} -
+ ), + )}
-

-

-
+ ); }; diff --git a/jsx/src/components/Groups/Groups.jsx b/jsx/src/components/Groups/Groups.jsx index 0bc4392f..052769fa 100644 --- a/jsx/src/components/Groups/Groups.jsx +++ b/jsx/src/components/Groups/Groups.jsx @@ -2,9 +2,11 @@ import React, { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; import PropTypes from "prop-types"; +import { Button, Card } from "react-bootstrap"; import { Link, useNavigate } from "react-router-dom"; import { usePaginationParams } from "../../util/paginationParams"; import PaginationFooter from "../PaginationFooter/PaginationFooter"; +import { MainContainer } from "../../util/layout"; const Groups = (props) => { const groups_data = useSelector((state) => state.groups_data); @@ -41,59 +43,53 @@ const Groups = (props) => { } return ( -
-
-
-
-
-

Groups

-
-
-
    - {groups_data.length > 0 ? ( - groups_data.map((e, i) => ( -
  • - - {e.users.length + " users"} - - - {e.name} - -
  • - )) - ) : ( -
    -

    no groups created...

    -
    - )} -
- setOffset(offset + limit)} - prev={() => setOffset(offset - limit)} - handleLimit={handleLimit} - /> -
-
- - -
-
-
-
-
+ + + +

Groups

+
+ +
    + {groups_data.length > 0 ? ( + groups_data.map((e, i) => ( +
  • + + {e.users.length + " users"} + + + {e.name} + +
  • + )) + ) : ( +
    +

    no groups created...

    +
    + )} +
+ setOffset(offset + limit)} + prev={() => setOffset(offset - limit)} + handleLimit={handleLimit} + /> +
+ + + + + + + + + +
+
); }; diff --git a/jsx/src/components/PaginationFooter/PaginationFooter.jsx b/jsx/src/components/PaginationFooter/PaginationFooter.jsx index 803c85c8..444dea2c 100644 --- a/jsx/src/components/PaginationFooter/PaginationFooter.jsx +++ b/jsx/src/components/PaginationFooter/PaginationFooter.jsx @@ -1,6 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import { FormControl } from "react-bootstrap"; +import { Button, FormControl } from "react-bootstrap"; import "./pagination-footer.css"; @@ -13,7 +13,7 @@ const PaginationFooter = (props) => { {total ? `of ${total}` : ""}
{offset >= 1 ? ( - + ) : ( - + )} {offset + visible < total ? ( - + ) : ( - + )}