mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
Merge pull request #5030 from minrk/eslint
jsx: update and address eslint
This commit is contained in:
@@ -69,3 +69,18 @@ repos:
|
|||||||
- --update
|
- --update
|
||||||
files: jupyterhub/scopes.py
|
files: jupyterhub/scopes.py
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
|
# run eslint in the jsx directory
|
||||||
|
# need to pass through 'jsx:install-run' hook in
|
||||||
|
# top-level package.json to ensure dependencies are installed
|
||||||
|
# eslint pre-commit hook doesn't really work with eslint 9,
|
||||||
|
# so use `npm run lint:fix`
|
||||||
|
- id: jsx-eslint
|
||||||
|
name: eslint in jsx/
|
||||||
|
entry: npm run jsx:install-run lint:fix
|
||||||
|
pass_filenames: false
|
||||||
|
language: node
|
||||||
|
files: "jsx/.*"
|
||||||
|
# can't run on pre-commit; hangs, for some reason
|
||||||
|
stages:
|
||||||
|
- manual
|
||||||
|
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:react/recommended"],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2018,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"plugins": ["eslint-plugin-react", "prettier", "unused-imports"],
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"semi": "off",
|
|
||||||
"quotes": "off",
|
|
||||||
"prettier/prettier": "warn",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"unused-imports/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"vars": "all",
|
|
||||||
"varsIgnorePattern": "^regeneratorRuntime|^_",
|
|
||||||
"args": "after-used",
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["**/*.test.js", "**/*.test.jsx"],
|
|
||||||
"env": {
|
|
||||||
"jest": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
77
jsx/eslint.config.mjs
Normal file
77
jsx/eslint.config.mjs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
import react from "eslint-plugin-react";
|
||||||
|
import prettier from "eslint-plugin-prettier";
|
||||||
|
import unusedImports from "eslint-plugin-unused-imports";
|
||||||
|
import globals from "globals";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
extends: compat.extends("plugin:react/recommended"),
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
react,
|
||||||
|
prettier,
|
||||||
|
"unused-imports": unusedImports,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
|
||||||
|
ecmaVersion: 2018,
|
||||||
|
sourceType: "module",
|
||||||
|
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
semi: "off",
|
||||||
|
quotes: "off",
|
||||||
|
"prettier/prettier": "warn",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"unused-imports/no-unused-imports": "error",
|
||||||
|
|
||||||
|
"unused-imports/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
vars: "all",
|
||||||
|
varsIgnorePattern: "^regeneratorRuntime|^_",
|
||||||
|
args: "after-used",
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.test.js", "**/*.test.jsx"],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
32
jsx/package-lock.json
generated
32
jsx/package-lock.json
generated
@@ -26,6 +26,8 @@
|
|||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.23.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
@@ -851,6 +854,16 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-classes/node_modules/globals": {
|
||||||
|
"version": "11.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/plugin-transform-computed-properties": {
|
"node_modules/@babel/plugin-transform-computed-properties": {
|
||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz",
|
||||||
@@ -1742,6 +1755,16 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/traverse/node_modules/globals": {
|
||||||
|
"version": "11.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.27.0",
|
"version": "7.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||||
@@ -6040,11 +6063,16 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "11.12.0",
|
"version": "16.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz",
|
||||||
|
"integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globalthis": {
|
"node_modules/globalthis": {
|
||||||
|
@@ -52,6 +52,8 @@
|
|||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@babel/preset-env": "^7.26.9",
|
"@babel/preset-env": "^7.26.9",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@eslint/js": "^9.23.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import React, { act } from "react";
|
import React, { act } from "react";
|
||||||
import "@testing-library/jest-dom";
|
import "@testing-library/jest-dom";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { Provider, useDispatch, useSelector } from "react-redux";
|
import { Provider, useDispatch, useSelector } from "react-redux";
|
||||||
import { createStore } from "redux";
|
import { createStore } from "redux";
|
||||||
import { HashRouter } from "react-router";
|
import { HashRouter } from "react-router";
|
||||||
|
@@ -179,6 +179,7 @@ GroupEdit.propTypes = {
|
|||||||
removeFromGroup: PropTypes.func,
|
removeFromGroup: PropTypes.func,
|
||||||
deleteGroup: PropTypes.func,
|
deleteGroup: PropTypes.func,
|
||||||
updateGroups: PropTypes.func,
|
updateGroups: PropTypes.func,
|
||||||
|
updateProp: PropTypes.func,
|
||||||
validateUser: PropTypes.func,
|
validateUser: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ 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 ErrorAlert from "../../util/error";
|
||||||
|
import { User, Server } from "../../util/jhapiUtil";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -209,6 +210,15 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ServerButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
user: User,
|
||||||
|
action: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
variant: PropTypes.string,
|
||||||
|
extraClass: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
const StopServerButton = ({ server, user }) => {
|
const StopServerButton = ({ server, user }) => {
|
||||||
if (!server.ready) {
|
if (!server.ready) {
|
||||||
return null;
|
return null;
|
||||||
@@ -222,6 +232,12 @@ const ServerDashboard = (props) => {
|
|||||||
extraClass: "stop-button",
|
extraClass: "stop-button",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StopServerButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
const DeleteServerButton = ({ server, user }) => {
|
const DeleteServerButton = ({ server, user }) => {
|
||||||
if (!server.name) {
|
if (!server.name) {
|
||||||
// It's not possible to delete unnamed servers
|
// It's not possible to delete unnamed servers
|
||||||
@@ -240,6 +256,11 @@ const ServerDashboard = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeleteServerButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
const StartServerButton = ({ server, user }) => {
|
const StartServerButton = ({ server, user }) => {
|
||||||
if (server.ready) {
|
if (server.ready) {
|
||||||
return null;
|
return null;
|
||||||
@@ -254,6 +275,11 @@ const ServerDashboard = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StartServerButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
const SpawnPageButton = ({ server, user }) => {
|
const SpawnPageButton = ({ server, user }) => {
|
||||||
if (server.ready) {
|
if (server.ready) {
|
||||||
return null;
|
return null;
|
||||||
@@ -271,6 +297,11 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SpawnPageButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
const AccessServerButton = ({ server }) => {
|
const AccessServerButton = ({ server }) => {
|
||||||
if (!server.ready) {
|
if (!server.ready) {
|
||||||
return null;
|
return null;
|
||||||
@@ -283,6 +314,9 @@ const ServerDashboard = (props) => {
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
AccessServerButton.propTypes = {
|
||||||
|
server: Server,
|
||||||
|
};
|
||||||
|
|
||||||
const EditUserButton = ({ user }) => {
|
const EditUserButton = ({ user }) => {
|
||||||
return (
|
return (
|
||||||
@@ -303,10 +337,17 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServerRowTable = ({ data }) => {
|
EditUserButton.propTypes = {
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerRowTable = ({ data, exclude }) => {
|
||||||
const sortedData = Object.keys(data)
|
const sortedData = Object.keys(data)
|
||||||
.sort()
|
.sort()
|
||||||
.reduce(function (result, key) {
|
.reduce(function (result, key) {
|
||||||
|
if (exclude && exclude.includes(key)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
let value = data[key];
|
let value = data[key];
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "last_activity":
|
case "last_activity":
|
||||||
@@ -346,88 +387,101 @@ const ServerDashboard = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverRow = (user, server) => {
|
ServerRowTable.propTypes = {
|
||||||
const { servers, ...userNoServers } = user;
|
data: Server,
|
||||||
|
exclude: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServerRow = ({ user, server }) => {
|
||||||
const serverNameDash = server.name ? `-${server.name}` : "";
|
const serverNameDash = server.name ? `-${server.name}` : "";
|
||||||
const userServerName = user.name + serverNameDash;
|
const userServerName = user.name + serverNameDash;
|
||||||
const open = collapseStates[userServerName] || false;
|
const open = collapseStates[userServerName] || false;
|
||||||
return [
|
return (
|
||||||
<tr
|
<Fragment key={`${userServerName}-row`}>
|
||||||
key={`${userServerName}-row`}
|
<tr
|
||||||
data-testid={`user-row-${userServerName}`}
|
key={`${userServerName}-row`}
|
||||||
className="user-row"
|
data-testid={`user-row-${userServerName}`}
|
||||||
>
|
className="user-row"
|
||||||
<td data-testid="user-row-name">
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
setCollapseStates({
|
|
||||||
...collapseStates,
|
|
||||||
[userServerName]: !open,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
aria-controls={`${userServerName}-collapse`}
|
|
||||||
aria-expanded={open}
|
|
||||||
data-testid={`${userServerName}-collapse-button`}
|
|
||||||
variant={open ? "secondary" : "primary"}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<span className="fa fa-caret-down"></span>
|
|
||||||
</Button>{" "}
|
|
||||||
</span>
|
|
||||||
<span data-testid={`user-name-div-${userServerName}`}>
|
|
||||||
{user.name}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td data-testid="user-row-admin">{user.admin ? "admin" : ""}</td>
|
|
||||||
|
|
||||||
<td data-testid="user-row-server">
|
|
||||||
<p className="text-secondary">{server.name}</p>
|
|
||||||
</td>
|
|
||||||
<td data-testid="user-row-last-activity">
|
|
||||||
{server.last_activity ? timeSince(server.last_activity) : "Never"}
|
|
||||||
</td>
|
|
||||||
<td data-testid="user-row-server-activity" className="actions">
|
|
||||||
<StartServerButton server={server} user={user} />
|
|
||||||
<StopServerButton server={server} user={user} />
|
|
||||||
<DeleteServerButton server={server} user={user} />
|
|
||||||
<AccessServerButton server={server} />
|
|
||||||
<SpawnPageButton server={server} user={user} />
|
|
||||||
<EditUserButton user={user} />
|
|
||||||
</td>
|
|
||||||
</tr>,
|
|
||||||
<tr key={`${userServerName}-detail`}>
|
|
||||||
<td
|
|
||||||
colSpan={6}
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
data-testid={`${userServerName}-td`}
|
|
||||||
>
|
>
|
||||||
<Collapse in={open} data-testid={`${userServerName}-collapse`}>
|
<td data-testid="user-row-name">
|
||||||
<CardGroup
|
<span>
|
||||||
id={`${userServerName}-card-group`}
|
<Button
|
||||||
style={{ width: "100%", margin: "0 auto", float: "none" }}
|
onClick={() =>
|
||||||
>
|
setCollapseStates({
|
||||||
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
...collapseStates,
|
||||||
<Card.Title>User</Card.Title>
|
[userServerName]: !open,
|
||||||
<ServerRowTable data={userNoServers} />
|
})
|
||||||
</Card>
|
}
|
||||||
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
aria-controls={`${userServerName}-collapse`}
|
||||||
<Card.Title>Server</Card.Title>
|
aria-expanded={open}
|
||||||
<ServerRowTable data={server} />
|
data-testid={`${userServerName}-collapse-button`}
|
||||||
</Card>
|
variant={open ? "secondary" : "primary"}
|
||||||
</CardGroup>
|
size="sm"
|
||||||
</Collapse>
|
>
|
||||||
</td>
|
<span className="fa fa-caret-down"></span>
|
||||||
</tr>,
|
</Button>{" "}
|
||||||
];
|
</span>
|
||||||
|
<span data-testid={`user-name-div-${userServerName}`}>
|
||||||
|
{user.name}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td data-testid="user-row-admin">{user.admin ? "admin" : ""}</td>
|
||||||
|
|
||||||
|
<td data-testid="user-row-server">
|
||||||
|
<p className="text-secondary">{server.name}</p>
|
||||||
|
</td>
|
||||||
|
<td data-testid="user-row-last-activity">
|
||||||
|
{server.last_activity ? timeSince(server.last_activity) : "Never"}
|
||||||
|
</td>
|
||||||
|
<td data-testid="user-row-server-activity" className="actions">
|
||||||
|
<StartServerButton server={server} user={user} />
|
||||||
|
<StopServerButton server={server} user={user} />
|
||||||
|
<DeleteServerButton server={server} user={user} />
|
||||||
|
<AccessServerButton server={server} />
|
||||||
|
<SpawnPageButton server={server} user={user} />
|
||||||
|
<EditUserButton user={user} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr key={`${userServerName}-detail`}>
|
||||||
|
<td
|
||||||
|
colSpan={6}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
data-testid={`${userServerName}-td`}
|
||||||
|
>
|
||||||
|
<Collapse in={open} data-testid={`${userServerName}-collapse`}>
|
||||||
|
<CardGroup
|
||||||
|
id={`${userServerName}-card-group`}
|
||||||
|
style={{ width: "100%", margin: "0 auto", float: "none" }}
|
||||||
|
>
|
||||||
|
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
||||||
|
<Card.Title>User</Card.Title>
|
||||||
|
<ServerRowTable data={user} exclude={["server", "servers"]} />
|
||||||
|
</Card>
|
||||||
|
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
||||||
|
<Card.Title>Server</Card.Title>
|
||||||
|
<ServerRowTable data={server} />
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
|
</Collapse>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let servers = user_data.flatMap((user) => {
|
ServerRow.propTypes = {
|
||||||
let userServers = Object.values({
|
user: User,
|
||||||
|
server: Server,
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverRows = user_data.flatMap((user) => {
|
||||||
|
const userServers = Object.values({
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
"": user.server || {},
|
"": user.server || {},
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
...(user.servers || {}),
|
...(user.servers || {}),
|
||||||
});
|
});
|
||||||
return userServers.map((server) => [user, server]);
|
return userServers.map((server) => ServerRow({ user, server }));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -583,7 +637,7 @@ const ServerDashboard = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{servers.flatMap(([user, server]) => serverRow(user, server))}
|
{serverRows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<PaginationFooter
|
<PaginationFooter
|
||||||
@@ -607,7 +661,7 @@ const ServerDashboard = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ServerDashboard.propTypes = {
|
ServerDashboard.propTypes = {
|
||||||
user_data: PropTypes.array,
|
user_data: PropTypes.arrayOf(User),
|
||||||
updateUsers: PropTypes.func,
|
updateUsers: PropTypes.func,
|
||||||
shutdownHub: PropTypes.func,
|
shutdownHub: PropTypes.func,
|
||||||
startServer: PropTypes.func,
|
startServer: PropTypes.func,
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const jhdata = window.jhdata || {};
|
const jhdata = window.jhdata || {};
|
||||||
const base_url = jhdata.base_url || "/";
|
const base_url = jhdata.base_url || "/";
|
||||||
const xsrfToken = jhdata.xsrf_token;
|
const xsrfToken = jhdata.xsrf_token;
|
||||||
@@ -17,3 +19,21 @@ export const jhapiRequest = (endpoint, method, data) => {
|
|||||||
body: data ? JSON.stringify(data) : null,
|
body: data ? JSON.stringify(data) : null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// need to declare the subset of fields we use, at least
|
||||||
|
export const Server = PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
url: PropTypes.string,
|
||||||
|
active: PropTypes.boolean,
|
||||||
|
pending: PropTypes.string,
|
||||||
|
last_activity: PropTypes.string,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const User = PropTypes.shape({
|
||||||
|
admin: PropTypes.boolean,
|
||||||
|
name: PropTypes.string,
|
||||||
|
last_activity: PropTypes.string,
|
||||||
|
url: PropTypes.string,
|
||||||
|
server: Server,
|
||||||
|
servers: PropTypes.objectOf(Server),
|
||||||
|
});
|
||||||
|
@@ -11,7 +11,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "python3 ./bower-lite",
|
"postinstall": "python3 ./bower-lite",
|
||||||
"css": "sass --style compressed -I share/jupyterhub/static/components --source-map share/jupyterhub/static/scss/style.scss:share/jupyterhub/static/css/style.min.css",
|
"css": "sass --style compressed -I share/jupyterhub/static/components --source-map share/jupyterhub/static/scss/style.scss:share/jupyterhub/static/css/style.min.css",
|
||||||
"build:watch": "npm run css -- --watch"
|
"build:watch": "npm run css -- --watch",
|
||||||
|
"jsx:install-run": "npm install --prefix jsx && npm run --prefix jsx",
|
||||||
|
"jsx:run": "npm run --prefix jsx"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"sass": "^1.74.1"
|
"sass": "^1.74.1"
|
||||||
|
Reference in New Issue
Block a user