mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 01:54:09 +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
|
||||
files: jupyterhub/scopes.py
|
||||
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/preset-env": "^7.26.9",
|
||||
"@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/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -38,6 +40,7 @@
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -851,6 +854,16 @@
|
||||
"@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": {
|
||||
"version": "7.25.9",
|
||||
"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_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": {
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
||||
@@ -6040,11 +6063,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"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,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/globalthis": {
|
||||
|
@@ -52,6 +52,8 @@
|
||||
"@babel/core": "^7.26.10",
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@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/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -64,6 +66,7 @@
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"file-loader": "^6.2.0",
|
||||
"globals": "^16.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import React, { act } from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Provider, useDispatch, useSelector } from "react-redux";
|
||||
import { createStore } from "redux";
|
||||
import { HashRouter } from "react-router";
|
||||
|
@@ -179,6 +179,7 @@ GroupEdit.propTypes = {
|
||||
removeFromGroup: PropTypes.func,
|
||||
deleteGroup: PropTypes.func,
|
||||
updateGroups: PropTypes.func,
|
||||
updateProp: PropTypes.func,
|
||||
validateUser: PropTypes.func,
|
||||
};
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
|
||||
import { debounce } from "lodash";
|
||||
import PropTypes from "prop-types";
|
||||
import ErrorAlert from "../../util/error";
|
||||
import { User, Server } from "../../util/jhapiUtil";
|
||||
|
||||
import {
|
||||
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 }) => {
|
||||
if (!server.ready) {
|
||||
return null;
|
||||
@@ -222,6 +232,12 @@ const ServerDashboard = (props) => {
|
||||
extraClass: "stop-button",
|
||||
});
|
||||
};
|
||||
|
||||
StopServerButton.propTypes = {
|
||||
server: Server,
|
||||
user: User,
|
||||
};
|
||||
|
||||
const DeleteServerButton = ({ server, user }) => {
|
||||
if (!server.name) {
|
||||
// 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 }) => {
|
||||
if (server.ready) {
|
||||
return null;
|
||||
@@ -254,6 +275,11 @@ const ServerDashboard = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
StartServerButton.propTypes = {
|
||||
server: Server,
|
||||
user: User,
|
||||
};
|
||||
|
||||
const SpawnPageButton = ({ server, user }) => {
|
||||
if (server.ready) {
|
||||
return null;
|
||||
@@ -271,6 +297,11 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
SpawnPageButton.propTypes = {
|
||||
server: Server,
|
||||
user: User,
|
||||
};
|
||||
|
||||
const AccessServerButton = ({ server }) => {
|
||||
if (!server.ready) {
|
||||
return null;
|
||||
@@ -283,6 +314,9 @@ const ServerDashboard = (props) => {
|
||||
</a>
|
||||
);
|
||||
};
|
||||
AccessServerButton.propTypes = {
|
||||
server: Server,
|
||||
};
|
||||
|
||||
const EditUserButton = ({ user }) => {
|
||||
return (
|
||||
@@ -303,10 +337,17 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ServerRowTable = ({ data }) => {
|
||||
EditUserButton.propTypes = {
|
||||
user: User,
|
||||
};
|
||||
|
||||
const ServerRowTable = ({ data, exclude }) => {
|
||||
const sortedData = Object.keys(data)
|
||||
.sort()
|
||||
.reduce(function (result, key) {
|
||||
if (exclude && exclude.includes(key)) {
|
||||
return result;
|
||||
}
|
||||
let value = data[key];
|
||||
switch (key) {
|
||||
case "last_activity":
|
||||
@@ -346,88 +387,101 @@ const ServerDashboard = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const serverRow = (user, server) => {
|
||||
const { servers, ...userNoServers } = user;
|
||||
ServerRowTable.propTypes = {
|
||||
data: Server,
|
||||
exclude: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
const ServerRow = ({ user, server }) => {
|
||||
const serverNameDash = server.name ? `-${server.name}` : "";
|
||||
const userServerName = user.name + serverNameDash;
|
||||
const open = collapseStates[userServerName] || false;
|
||||
return [
|
||||
<tr
|
||||
key={`${userServerName}-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`}
|
||||
return (
|
||||
<Fragment key={`${userServerName}-row`}>
|
||||
<tr
|
||||
key={`${userServerName}-row`}
|
||||
data-testid={`user-row-${userServerName}`}
|
||||
className="user-row"
|
||||
>
|
||||
<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={userNoServers} />
|
||||
</Card>
|
||||
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
|
||||
<Card.Title>Server</Card.Title>
|
||||
<ServerRowTable data={server} />
|
||||
</Card>
|
||||
</CardGroup>
|
||||
</Collapse>
|
||||
</td>
|
||||
</tr>,
|
||||
];
|
||||
<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`}>
|
||||
<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) => {
|
||||
let userServers = Object.values({
|
||||
ServerRow.propTypes = {
|
||||
user: User,
|
||||
server: Server,
|
||||
};
|
||||
|
||||
const serverRows = user_data.flatMap((user) => {
|
||||
const userServers = Object.values({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
"": user.server || {},
|
||||
// eslint-disable-next-line react/prop-types
|
||||
...(user.servers || {}),
|
||||
});
|
||||
return userServers.map((server) => [user, server]);
|
||||
return userServers.map((server) => ServerRow({ user, server }));
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -583,7 +637,7 @@ const ServerDashboard = (props) => {
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{servers.flatMap(([user, server]) => serverRow(user, server))}
|
||||
{serverRows}
|
||||
</tbody>
|
||||
</table>
|
||||
<PaginationFooter
|
||||
@@ -607,7 +661,7 @@ const ServerDashboard = (props) => {
|
||||
};
|
||||
|
||||
ServerDashboard.propTypes = {
|
||||
user_data: PropTypes.array,
|
||||
user_data: PropTypes.arrayOf(User),
|
||||
updateUsers: PropTypes.func,
|
||||
shutdownHub: PropTypes.func,
|
||||
startServer: PropTypes.func,
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const jhdata = window.jhdata || {};
|
||||
const base_url = jhdata.base_url || "/";
|
||||
const xsrfToken = jhdata.xsrf_token;
|
||||
@@ -17,3 +19,21 @@ export const jhapiRequest = (endpoint, method, data) => {
|
||||
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": {
|
||||
"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",
|
||||
"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": {
|
||||
"sass": "^1.74.1"
|
||||
|
Reference in New Issue
Block a user