Merge pull request #5030 from minrk/eslint

jsx: update and address eslint
This commit is contained in:
Min RK
2025-04-07 12:37:25 +02:00
committed by GitHub
10 changed files with 279 additions and 124 deletions

View File

@@ -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

View File

@@ -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
View 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
View File

@@ -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": {

View File

@@ -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",

View File

@@ -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";

View File

@@ -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,
}; };

View File

@@ -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,12 +387,17 @@ 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 (
<Fragment key={`${userServerName}-row`}>
<tr <tr
key={`${userServerName}-row`} key={`${userServerName}-row`}
data-testid={`user-row-${userServerName}`} data-testid={`user-row-${userServerName}`}
@@ -395,7 +441,7 @@ const ServerDashboard = (props) => {
<SpawnPageButton server={server} user={user} /> <SpawnPageButton server={server} user={user} />
<EditUserButton user={user} /> <EditUserButton user={user} />
</td> </td>
</tr>, </tr>
<tr key={`${userServerName}-detail`}> <tr key={`${userServerName}-detail`}>
<td <td
colSpan={6} colSpan={6}
@@ -409,7 +455,7 @@ const ServerDashboard = (props) => {
> >
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}> <Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>User</Card.Title> <Card.Title>User</Card.Title>
<ServerRowTable data={userNoServers} /> <ServerRowTable data={user} exclude={["server", "servers"]} />
</Card> </Card>
<Card style={{ width: "100%", padding: 3, margin: "0 auto" }}> <Card style={{ width: "100%", padding: 3, margin: "0 auto" }}>
<Card.Title>Server</Card.Title> <Card.Title>Server</Card.Title>
@@ -418,16 +464,24 @@ const ServerDashboard = (props) => {
</CardGroup> </CardGroup>
</Collapse> </Collapse>
</td> </td>
</tr>, </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,

View File

@@ -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),
});

View File

@@ -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"