diff --git a/.gitignore b/.gitignore
index 5ff18d83..338f2f00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ htmlcov
pip-wheel-metadata
docs/source/reference/metrics.rst
oldest-requirements.txt
+jupyterhub-proxy.pid
diff --git a/jsx/.eslintrc.json b/jsx/.eslintrc.json
index 673f8ecb..191fd680 100644
--- a/jsx/.eslintrc.json
+++ b/jsx/.eslintrc.json
@@ -1,7 +1,7 @@
{
"extends": ["plugin:react/recommended"],
"parserOptions": {
- "ecmaVersion": 6,
+ "ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
diff --git a/jsx/src/App.jsx b/jsx/src/App.jsx
index 9fa36e7c..7047e50a 100644
--- a/jsx/src/App.jsx
+++ b/jsx/src/App.jsx
@@ -2,11 +2,11 @@ import React, { Component, useEffect } from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore } from "redux";
-import { Button } from "react-bootstrap";
+import { compose } from "recompose";
import { initialState, reducers } from "./Store";
import { jhapiRequest } from "./util/jhapiUtil";
+import withAPI from "./util/withAPI";
import { HashRouter, Switch, Route, Link } from "react-router-dom";
-import { createBrowserHistory } from "history";
import ServerDashboard from "./components/ServerDashboard/ServerDashboard";
import Groups from "./components/Groups/Groups";
@@ -37,12 +37,32 @@ const App = (props) => {
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/jsx/src/components/AddUser/AddUser.jsx b/jsx/src/components/AddUser/AddUser.jsx
index af6ceb60..5407e566 100644
--- a/jsx/src/components/AddUser/AddUser.jsx
+++ b/jsx/src/components/AddUser/AddUser.jsx
@@ -107,17 +107,4 @@ AddUser.propTypes = {
}),
};
-const withUserAPI = withProps((props) => ({
- addUsers: (usernames, admin) =>
- jhapiRequest("/users", "POST", { usernames, admin }),
- failRegexEvent: () =>
- alert(
- "Removed " +
- JSON.stringify(removed_users) +
- " for either containing special characters or being too short."
- ),
- refreshUserData: () =>
- jhapiRequest("/users", "GET").then((data) => data.json()),
-}));
-
-export default compose(withUserAPI)(AddUser);
+export default AddUser;
diff --git a/jsx/src/components/AddUser/AddUser.test.js b/jsx/src/components/AddUser/AddUser.test.js
index 4d5a2909..ec5bb165 100644
--- a/jsx/src/components/AddUser/AddUser.test.js
+++ b/jsx/src/components/AddUser/AddUser.test.js
@@ -1,31 +1,53 @@
import React from "react";
-import Enzyme, { shallow } from "enzyme";
-import AddUser from "./AddUser.pre";
+import Enzyme, { mount } from "enzyme";
+import AddUser from "./AddUser";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
+import { Provider, useDispatch } from "react-redux";
+import { createStore } from "redux";
+import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() });
+jest.mock("react-redux", () => ({
+ ...jest.requireActual("react-redux"),
+ useDispatch: jest.fn(),
+}));
+
describe("AddUser Component: ", () => {
var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var addUserJsx = (callbackSpy) => (
- {} }}
- />
+ {}, {})}>
+
+ {} }}
+ />
+
+
);
+ beforeEach(() => {
+ useDispatch.mockImplementation((callback) => {
+ return () => {};
+ });
+ });
+
+ afterEach(() => {
+ useDispatch.mockClear();
+ });
+
it("Renders", () => {
- let component = shallow(addUserJsx(mockAsync()));
+ let component = mount(addUserJsx(mockAsync()));
expect(component.find(".container").length).toBe(1);
});
it("Removes users when they fail Regex", () => {
let callbackSpy = mockAsync(),
- component = shallow(addUserJsx(callbackSpy)),
+ component = mount(addUserJsx(callbackSpy)),
textarea = component.find("textarea").first();
textarea.simulate("blur", { target: { value: "foo\nbar\n!!*&*" } });
let submit = component.find("#submit");
@@ -35,7 +57,7 @@ describe("AddUser Component: ", () => {
it("Correctly submits admin", () => {
let callbackSpy = mockAsync(),
- component = shallow(addUserJsx(callbackSpy)),
+ component = mount(addUserJsx(callbackSpy)),
input = component.find("input").first();
input.simulate("change", { target: { checked: true } });
let submit = component.find("#submit");
diff --git a/jsx/src/components/CreateGroup/CreateGroup.jsx b/jsx/src/components/CreateGroup/CreateGroup.jsx
index 355091f1..431c2e18 100644
--- a/jsx/src/components/CreateGroup/CreateGroup.jsx
+++ b/jsx/src/components/CreateGroup/CreateGroup.jsx
@@ -81,16 +81,4 @@ CreateGroup.propTypes = {
}),
};
-const withGroupsAPI = withProps((props) => ({
- createGroup: (groupName) => jhapiRequest("/groups/" + groupName, "POST"),
- failRegexEvent: () =>
- alert(
- "Removed " +
- JSON.stringify(removed_users) +
- " for either containing special characters or being too short."
- ),
- refreshGroupsData: () =>
- jhapiRequest("/groups", "GET").then((data) => data.json()),
-}));
-
-export default compose(withGroupsAPI)(CreateGroup);
+export default CreateGroup;
diff --git a/jsx/src/components/CreateGroup/CreateGroup.test.js b/jsx/src/components/CreateGroup/CreateGroup.test.js
index 74d09a32..ed0a0790 100644
--- a/jsx/src/components/CreateGroup/CreateGroup.test.js
+++ b/jsx/src/components/CreateGroup/CreateGroup.test.js
@@ -1,30 +1,52 @@
import React from "react";
-import Enzyme, { mount, shallow } from "enzyme";
-import CreateGroup from "./CreateGroup.pre";
+import Enzyme, { mount } from "enzyme";
+import CreateGroup from "./CreateGroup";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
+import { Provider, useDispatch } from "react-redux";
+import { createStore } from "redux";
+import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() });
+jest.mock("react-redux", () => ({
+ ...jest.requireActual("react-redux"),
+ useDispatch: jest.fn(),
+}));
+
describe("CreateGroup Component: ", () => {
var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var createGroupJsx = (callbackSpy) => (
- {} }}
- />
+ {}, {})}>
+
+ {} }}
+ />
+
+
);
+ beforeEach(() => {
+ useDispatch.mockImplementation((callback) => {
+ return () => {};
+ });
+ });
+
+ afterEach(() => {
+ useDispatch.mockClear();
+ });
+
it("Renders", () => {
- let component = shallow(createGroupJsx());
+ let component = mount(createGroupJsx());
expect(component.find(".container").length).toBe(1);
});
it("Calls createGroup and refreshGroupsData on submit", () => {
let callbackSpy = mockAsync(),
- component = shallow(createGroupJsx(callbackSpy)),
+ component = mount(createGroupJsx(callbackSpy)),
input = component.find("input").first(),
submit = component.find("#submit").first();
input.simulate("change", { target: { value: "" } });
diff --git a/jsx/src/components/EditUser/EditUser.jsx b/jsx/src/components/EditUser/EditUser.jsx
index f899b205..26d52543 100644
--- a/jsx/src/components/EditUser/EditUser.jsx
+++ b/jsx/src/components/EditUser/EditUser.jsx
@@ -1,12 +1,8 @@
import React, { useState } from "react";
-import { useSelector, useDispatch } from "react-redux";
-import { compose, withProps } from "recompose";
+import { useDispatch } from "react-redux";
import PropTypes from "prop-types";
-
import { Link } from "react-router-dom";
-import { jhapiRequest } from "../../util/jhapiUtil";
-
const EditUser = (props) => {
var dispatch = useDispatch();
@@ -160,22 +156,4 @@ EditUser.propTypes = {
refreshUserData: PropTypes.func,
};
-const withUserAPI = withProps((props) => ({
- editUser: (username, updated_username, admin) =>
- jhapiRequest("/users/" + username, "PATCH", {
- name: updated_username,
- admin,
- }),
- deleteUser: (username) => jhapiRequest("/users/" + username, "DELETE"),
- failRegexEvent: () =>
- alert(
- "Cannot change username - either contains special characters or is too short."
- ),
- noChangeEvent: () => {
- returns;
- },
- refreshUserData: () =>
- jhapiRequest("/users", "GET").then((data) => data.json()),
-}));
-
-export default compose(withUserAPI)(EditUser);
+export default EditUser;
diff --git a/jsx/src/components/EditUser/EditUser.test.js b/jsx/src/components/EditUser/EditUser.test.js
index 462ffff4..5c879e6a 100644
--- a/jsx/src/components/EditUser/EditUser.test.js
+++ b/jsx/src/components/EditUser/EditUser.test.js
@@ -1,30 +1,54 @@
import React from "react";
-import Enzyme, { shallow } from "enzyme";
-import EditUser from "./EditUser.pre";
+import Enzyme, { mount } from "enzyme";
+import EditUser from "./EditUser";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
+import { Provider, useDispatch } from "react-redux";
+import { createStore } from "redux";
+import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() });
+jest.mock("react-redux", () => ({
+ ...jest.requireActual("react-redux"),
+ useDispatch: jest.fn(),
+}));
+
describe("EditUser Component: ", () => {
var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var mockSync = () => jest.fn();
- var editUserJsx = (callbackSpy) => (
- {} }}
- failRegexEvent={callbackSpy}
- noChangeEvent={callbackSpy}
- />
+ var editUserJsx = (callbackSpy, empty) => (
+ {}, {})}>
+
+ {} }}
+ failRegexEvent={callbackSpy}
+ noChangeEvent={callbackSpy}
+ />
+
+
);
+ beforeEach(() => {
+ useDispatch.mockImplementation((callback) => {
+ return () => {};
+ });
+ });
+
+ afterEach(() => {
+ useDispatch.mockClear();
+ });
+
it("Calls the delete user function when the button is pressed", () => {
let callbackSpy = mockAsync(),
- component = shallow(editUserJsx(callbackSpy)),
+ component = mount(editUserJsx(callbackSpy)),
deleteUser = component.find("#delete-user");
deleteUser.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
@@ -32,9 +56,15 @@ describe("EditUser Component: ", () => {
it("Submits the edits when the button is pressed", () => {
let callbackSpy = mockSync(),
- component = shallow(editUserJsx(callbackSpy)),
+ component = mount(editUserJsx(callbackSpy)),
submit = component.find("#submit");
submit.simulate("click");
expect(callbackSpy).toHaveBeenCalled();
});
+
+ it("Doesn't render when no data is provided", () => {
+ let callbackSpy = mockSync(),
+ component = mount(editUserJsx(callbackSpy, true));
+ expect(component.find(".container").length).toBe(0);
+ });
});
diff --git a/jsx/src/components/GroupEdit/GroupEdit.jsx b/jsx/src/components/GroupEdit/GroupEdit.jsx
index 2d1121ed..3e226706 100644
--- a/jsx/src/components/GroupEdit/GroupEdit.jsx
+++ b/jsx/src/components/GroupEdit/GroupEdit.jsx
@@ -137,14 +137,4 @@ GroupEdit.propTypes = {
refreshGroupsData: PropTypes.func,
};
-const withGroupsAPI = withProps((props) => ({
- addToGroup: (users, groupname) =>
- jhapiRequest("/groups/" + groupname + "/users", "POST", { users }),
- removeFromGroup: (users, groupname) =>
- jhapiRequest("/groups/" + groupname + "/users", "DELETE", { users }),
- deleteGroup: (name) => jhapiRequest("/groups/" + name, "DELETE"),
- refreshGroupsData: () =>
- jhapiRequest("/groups", "GET").then((data) => data.json()),
-}));
-
-export default compose(withGroupsAPI)(GroupEdit);
+export default GroupEdit;
diff --git a/jsx/src/components/GroupEdit/GroupEdit.test.jsx b/jsx/src/components/GroupEdit/GroupEdit.test.jsx
index 0b0bbf42..aef1c3bd 100644
--- a/jsx/src/components/GroupEdit/GroupEdit.test.jsx
+++ b/jsx/src/components/GroupEdit/GroupEdit.test.jsx
@@ -1,39 +1,62 @@
import React from "react";
import Enzyme, { mount, shallow } from "enzyme";
-import GroupEdit from "./GroupEdit.pre";
+import GroupEdit from "./GroupEdit";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
+import { Provider, useSelector } from "react-redux";
+import { createStore } from "redux";
import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() });
+jest.mock("react-redux", () => ({
+ ...jest.requireActual("react-redux"),
+ useSelector: jest.fn(),
+}));
+
describe("GroupEdit Component: ", () => {
var mockAsync = () =>
jest.fn().mockImplementation(() => Promise.resolve({ key: "value" }));
var groupEditJsx = (callbackSpy) => (
- {},
- },
- }}
- addToGroup={callbackSpy}
- removeFromGroup={callbackSpy}
- deleteGroup={callbackSpy}
- history={{ push: (a) => callbackSpy }}
- refreshGroupsData={() => {}}
- />
+ {}, {})}>
+
+ {},
+ },
+ }}
+ addToGroup={callbackSpy}
+ removeFromGroup={callbackSpy}
+ deleteGroup={callbackSpy}
+ history={{ push: (a) => callbackSpy }}
+ refreshGroupsData={callbackSpy}
+ />
+
+
);
- var deepGroupEditJsx = (callbackSpy) => (
- {groupEditJsx(callbackSpy)}
- );
+ var mockAppState = () => ({
+ user_data: JSON.parse(
+ '[{"kind":"user","name":"foo","admin":true,"groups":[],"server":"/user/foo/","pending":null,"created":"2020-12-07T18:46:27.112695Z","last_activity":"2020-12-07T21:00:33.336354Z","servers":{"":{"name":"","last_activity":"2020-12-07T20:58:02.437408Z","started":"2020-12-07T20:58:01.508266Z","pending":null,"ready":true,"state":{"pid":28085},"url":"/user/foo/","user_options":{},"progress_url":"/hub/api/users/foo/server/progress"}}},{"kind":"user","name":"bar","admin":false,"groups":[],"server":null,"pending":null,"created":"2020-12-07T18:46:27.115528Z","last_activity":"2020-12-07T20:43:51.013613Z","servers":{}}]'
+ ),
+ });
+
+ beforeEach(() => {
+ useSelector.mockImplementation((callback) => {
+ return callback(mockAppState());
+ });
+ });
+
+ afterEach(() => {
+ useSelector.mockClear();
+ });
it("Adds a newly selected user to group on submit", () => {
let callbackSpy = mockAsync(),
- component = mount(deepGroupEditJsx(callbackSpy)),
+ component = mount(groupEditJsx(callbackSpy)),
unselected = component.find(".unselected"),
submit = component.find("#submit");
unselected.simulate("click");
@@ -43,7 +66,7 @@ describe("GroupEdit Component: ", () => {
it("Removes a user from group on submit", () => {
let callbackSpy = mockAsync(),
- component = mount(deepGroupEditJsx(callbackSpy)),
+ component = mount(groupEditJsx(callbackSpy)),
selected = component.find(".selected"),
submit = component.find("#submit");
selected.simulate("click");
@@ -53,9 +76,10 @@ describe("GroupEdit Component: ", () => {
it("Calls deleteGroup on button click", () => {
let callbackSpy = mockAsync(),
- component = shallow(groupEditJsx(callbackSpy)),
+ component = mount(groupEditJsx(callbackSpy)),
deleteGroup = component.find("#delete-group").first();
deleteGroup.simulate("click");
- expect(callbackSpy).toHaveBeenCalled();
+ expect(callbackSpy).toHaveBeenNthCalledWith(1, "group");
+ expect(callbackSpy).toHaveBeenNthCalledWith(2);
});
});
diff --git a/jsx/src/components/Groups/Groups.jsx b/jsx/src/components/Groups/Groups.jsx
index 1f92ae28..7465141d 100644
--- a/jsx/src/components/Groups/Groups.jsx
+++ b/jsx/src/components/Groups/Groups.jsx
@@ -102,21 +102,4 @@ Groups.propTypes = {
}),
};
-const withGroupsAPI = withProps((props) => ({
- refreshGroupsData: () =>
- jhapiRequest("/groups", "GET").then((data) => data.json()),
- refreshUserData: () =>
- jhapiRequest("/users", "GET").then((data) => data.json()),
- addUsersToGroup: (name, new_users) =>
- jhapiRequest("/groups/" + name + "/users", "POST", {
- body: { users: new_users },
- json: true,
- }),
- removeUsersFromGroup: (name, removed_users) =>
- jhapiRequest("/groups/" + name + "/users", "DELETE", {
- body: { users: removed_users },
- json: true,
- }),
-}));
-
-export default compose(withGroupsAPI)(Groups);
+export default Groups;
diff --git a/jsx/src/components/Groups/Groups.test.js b/jsx/src/components/Groups/Groups.test.js
index f7396433..f1eed6b5 100644
--- a/jsx/src/components/Groups/Groups.test.js
+++ b/jsx/src/components/Groups/Groups.test.js
@@ -1,30 +1,58 @@
import React from "react";
-import Enzyme, { shallow } from "enzyme";
-import Groups from "./Groups.pre";
+import Enzyme, { mount } from "enzyme";
+import Groups from "./Groups";
import Adapter from "@wojtekmaj/enzyme-adapter-react-17";
+import { Provider, useSelector } from "react-redux";
+import { createStore } from "redux";
+import { HashRouter } from "react-router-dom";
Enzyme.configure({ adapter: new Adapter() });
+jest.mock("react-redux", () => ({
+ ...jest.requireActual("react-redux"),
+ useSelector: jest.fn(),
+ useDispatch: jest.fn(),
+}));
+
describe("Groups Component: ", () => {
var groupsJsx = () => (
-
+ {}, {})}>
+
+
+
+
);
+ var mockAppState = () => ({
+ user_data: JSON.parse(
+ '[{"kind":"user","name":"foo","admin":true,"groups":[],"server":"/user/foo/","pending":null,"created":"2020-12-07T18:46:27.112695Z","last_activity":"2020-12-07T21:00:33.336354Z","servers":{"":{"name":"","last_activity":"2020-12-07T20:58:02.437408Z","started":"2020-12-07T20:58:01.508266Z","pending":null,"ready":true,"state":{"pid":28085},"url":"/user/foo/","user_options":{},"progress_url":"/hub/api/users/foo/server/progress"}}},{"kind":"user","name":"bar","admin":false,"groups":[],"server":null,"pending":null,"created":"2020-12-07T18:46:27.115528Z","last_activity":"2020-12-07T20:43:51.013613Z","servers":{}}]'
+ ),
+ groups_data: JSON.parse(
+ '[{"kind":"group","name":"testgroup","users":[]}, {"kind":"group","name":"testgroup2","users":["foo", "bar"]}]'
+ ),
+ });
+
+ beforeEach(() => {
+ useSelector.mockImplementation((callback) => {
+ return callback(mockAppState());
+ });
+ });
+
+ afterEach(() => {
+ useSelector.mockClear();
+ });
+
it("Renders groups_data prop into links", () => {
- let component = shallow(groupsJsx()),
+ let component = mount(groupsJsx()),
links = component.find(".group-edit-link");
expect(links.length).toBe(2);
});
it("Renders nothing if required data is not available", () => {
- let component = shallow();
+ useSelector.mockImplementation((callback) => {
+ return callback({});
+ });
+ let component = mount(groupsJsx());
expect(component.html()).toBe("");
});
});
diff --git a/jsx/src/components/ServerDashboard/ServerDashboard.jsx b/jsx/src/components/ServerDashboard/ServerDashboard.jsx
index 1724f9b4..d110f939 100644
--- a/jsx/src/components/ServerDashboard/ServerDashboard.jsx
+++ b/jsx/src/components/ServerDashboard/ServerDashboard.jsx
@@ -155,7 +155,7 @@ const ServerDashboard = (props) => {
{/* Shutdown Jupyterhub */}