diff --git a/web/package-lock.json b/web/package-lock.json index 9bea717c..24ba4485 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,6 +30,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "eslint-config-prettier": "^10.1.8", "localforage": "^1.10.0", "lucide-react": "^0.292.0", "match-sorter": "^6.3.1", @@ -67,7 +68,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -808,7 +808,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -823,7 +822,6 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -832,7 +830,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -855,7 +852,6 @@ "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -870,7 +866,6 @@ "version": "8.54.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -929,7 +924,6 @@ "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", @@ -943,7 +937,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -955,8 +948,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", - "dev": true + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", @@ -2423,8 +2415,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitejs/plugin-react": { "version": "4.2.0", @@ -2470,7 +2461,6 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2482,7 +2472,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2491,7 +2480,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2507,7 +2495,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2549,8 +2536,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-hidden": { "version": "1.2.3", @@ -2693,7 +2679,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "engines": { "node": ">=6" } @@ -3101,7 +3086,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3132,7 +3116,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3148,8 +3131,7 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, "node_modules/delayed-stream": { "version": "1.0.0", @@ -3190,7 +3172,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -3263,7 +3244,6 @@ "version": "8.54.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3314,6 +3294,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", @@ -3339,7 +3334,6 @@ "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -3355,7 +3349,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3367,7 +3360,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3382,7 +3374,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3398,7 +3389,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3409,14 +3399,12 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -3428,7 +3416,6 @@ "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -3443,7 +3430,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3452,7 +3438,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3464,7 +3449,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -3481,7 +3465,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -3493,7 +3476,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -3505,7 +3487,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -3514,7 +3495,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3522,8 +3502,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -3554,14 +3533,12 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "node_modules/fastq": { "version": "1.15.0", @@ -3575,7 +3552,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -3598,7 +3574,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3614,7 +3589,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3627,8 +3601,7 @@ "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { "version": "1.15.3", @@ -3722,7 +3695,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3781,8 +3753,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/has-flag": { "version": "3.0.0", @@ -3808,7 +3779,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, "engines": { "node": ">= 4" } @@ -3822,7 +3792,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3838,7 +3807,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -3918,7 +3886,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3926,8 +3893,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jiti": { "version": "1.21.0", @@ -3946,7 +3912,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -3969,20 +3934,17 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", @@ -4000,7 +3962,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -4009,7 +3970,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4051,7 +4011,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -4077,8 +4036,7 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -4176,8 +4134,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4209,8 +4166,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/node-releases": { "version": "2.0.13", @@ -4271,7 +4227,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -4288,7 +4243,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4303,7 +4257,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -4318,7 +4271,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "dependencies": { "callsites": "^3.0.0" }, @@ -4330,7 +4282,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -4347,7 +4298,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4540,7 +4490,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -4641,7 +4590,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -4858,7 +4806,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "engines": { "node": ">=4" } @@ -4876,7 +4823,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -4982,7 +4928,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4994,7 +4939,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5033,7 +4977,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5045,7 +4988,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -5187,8 +5129,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/thenify": { "version": "3.3.1", @@ -5255,7 +5196,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -5267,7 +5207,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -5328,7 +5267,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -5446,7 +5384,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -5486,7 +5423,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/web/package.json b/web/package.json index bbd67669..f152ecba 100644 --- a/web/package.json +++ b/web/package.json @@ -32,6 +32,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "cmdk": "^0.2.0", + "eslint-config-prettier": "^10.1.8", "localforage": "^1.10.0", "lucide-react": "^0.292.0", "match-sorter": "^6.3.1", diff --git a/web/src/app/compose-library/compose-library-list.tsx b/web/src/app/compose-library/compose-library-list.tsx index 4c984994..c9d1419e 100644 --- a/web/src/app/compose-library/compose-library-list.tsx +++ b/web/src/app/compose-library/compose-library-list.tsx @@ -17,10 +17,57 @@ import { Button } from "@/components/ui/button" import useComposeLibraryItemList from "@/hooks/useComposeLibraryItemList" import { CLASSES_CLICKABLE_TABLE_ROW } from "@/lib/utils" import { TableNoData } from "@/components/widgets/table-no-data" +import { useMemo, useState } from "react" +import { SortableIcon } from "@/components/widgets/sortable-icon" + +type SortKey = "projectName" | "type" export default function ComposeLibraryList() { const navigate = useNavigate() const { isLoading, composeLibraryItems } = useComposeLibraryItemList() + const [sortKey, setSortKey] = useState("projectName") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") + + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortKey(key) + setSortOrder("asc") + } + } + + const sortedAndFilteredLibraryItems = useMemo(() => { + if (!composeLibraryItems?.items) return [] + + const sorted = [...composeLibraryItems.items].sort((a, b) => { + let aValue: string = "" + let bValue: string = "" + + switch (sortKey) { + case "projectName": + aValue = a.projectName + bValue = b.projectName + break + case "type": + aValue = a.type + bValue = b.type + break + default: + break + } + + if (aValue < bValue) { + return sortOrder === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortOrder === "asc" ? 1 : -1 + } + return 0 + }) + + return sorted + }, [composeLibraryItems, sortKey, sortOrder]) if (isLoading) return @@ -49,37 +96,58 @@ export default function ComposeLibraryList() { - Library Project Name - Type + handleSort("projectName")} + > + Library Project Name + + + handleSort("type")} + > + Type + + {composeLibraryItems?.totalRows === -1 && ( )} - {composeLibraryItems?.items && - composeLibraryItems?.items.map((item) => ( - { - if (item.type === "filesystem") { - navigate( - `/composelibrary/${item.type}/${item.projectName}/edit` - ) - } - if (item.type === "github") { - navigate(`/composelibrary/${item.type}/${item.id}/edit`) - } - }} - > - {item.projectName} - - {item.type === "filesystem" ? "File System" : ""} - {item.type === "github" ? "GitHub" : ""} - - - ))} + {sortedAndFilteredLibraryItems.map((item) => ( + { + if (item.type === "filesystem") { + navigate( + `/composelibrary/${item.type}/${item.projectName}/edit` + ) + } + if (item.type === "github") { + navigate(`/composelibrary/${item.type}/${item.id}/edit`) + } + }} + > + {item.projectName} + + {item.type === "filesystem" ? "File System" : ""} + {item.type === "github" ? "GitHub" : ""} + + + ))}
diff --git a/web/src/app/containers/container-list.tsx b/web/src/app/containers/container-list.tsx index 7004719d..1557bb6e 100644 --- a/web/src/app/containers/container-list.tsx +++ b/web/src/app/containers/container-list.tsx @@ -19,7 +19,7 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import apiBaseUrl from "@/lib/api-base-url" import { IContainer, IPort } from "@/lib/api-models" -import { useState } from "react" +import { useMemo, useState } from "react" import TopBar from "@/components/widgets/top-bar" import TopBarActions from "@/components/widgets/top-bar-actions" import MainArea from "@/components/widgets/main-area" @@ -38,6 +38,10 @@ import { import TableButtonDelete from "@/components/widgets/table-button-delete" import { TableNoData } from "@/components/widgets/table-no-data" import DeleteDialog from "@/components/delete-dialog" +import { Input } from "@/components/ui/input" +import { SortableIcon } from "@/components/widgets/sortable-icon" + +type SortKey = "name" | "image" | "state" export default function ContainerList() { const { nodeId } = useParams() @@ -49,9 +53,63 @@ export default function ContainerList() { const [deleteContainerConfirmationOpen, setDeleteContainerConfirmationOpen] = useState(false) const [deleteInProgress, setDeleteInProgress] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [sortKey, setSortKey] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") + + const sortedAndFilteredContainers = useMemo(() => { + if (!containers?.items) return [] + + const filtered = containers.items.filter( + (container) => + container.name.toLowerCase().includes(searchTerm.toLowerCase()) || + container.image.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const sorted = [...filtered].sort((a, b) => { + let aValue: string | number = "" + let bValue: string | number = "" + + switch (sortKey) { + case "name": + aValue = a.name + bValue = b.name + break + case "image": + aValue = a.image + bValue = b.image + break + case "state": + aValue = a.state + bValue = b.state + break + default: + break + } + + if (aValue < bValue) { + return sortOrder === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortOrder === "asc" ? 1 : -1 + } + return 0 + }) + + return sorted + }, [containers, searchTerm, sortKey, sortOrder]) if (isLoading) return + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortKey(key) + setSortOrder("asc") + } + } + const handleStartContainer = async (id: string) => { try { await axios(`${apiBaseUrl()}/nodes/${nodeId}/containers/start`, { @@ -187,7 +245,7 @@ export default function ContainerList() { deleteHandler={handleDeleteContainer} isProcessing={deleteInProgress} title="Delete Container" - message={`Are you sure you want to delete container '${container?.name}?'`} + message={`Are you sure you want to delete container '{container?.name}?'`} /> )} @@ -199,110 +257,151 @@ export default function ContainerList() { Containers - +
+ setSearchTerm(e.target.value)} + /> + +
- Name - + handleSort("name")} + > + Name + + + handleSort("image")} + > Image + Ports - State + handleSort("state")} + > + State + + Actions - {containers?.items?.length === 0 && } - {containers?.items && - containers?.items.map((item) => ( - { - navigate(`/nodes/${nodeId}/containers/${item.name}/logs`) - }} - > - - - - {item.name} -
- - {item.id.substring(0, 12)} - + {sortedAndFilteredContainers.length === 0 && ( + + )} + {sortedAndFilteredContainers.map((item) => ( + { + navigate(`/nodes/${nodeId}/containers/${item.name}/logs`) + }} + > + + + + {item.name} +
+ + {item.id.substring(0, 12)} -
- - {item.image} - - {getPortsHtml(item.ports)} - +
+
+ + {item.image} + + {getPortsHtml(item.ports)} + + {item.state == "exited" ? ( + + {item.state} + + ) : ( + + {item.state} + + )} + + + <> + {item.state == "running" && ( + + )} {item.state == "exited" ? ( - - {item.state} - + ) : ( - - {item.state} - - )} - - - <> - {item.state == "running" && ( - - )} - {item.state == "exited" ? ( - - ) : ( - - )} - { e.stopPropagation() - handleDeleteContainerConfirmation(item) + handleStopContainer(item.id) }} - /> - - -
- ))} + > + + + )} + { + e.stopPropagation() + handleDeleteContainerConfirmation(item) + }} + /> + + + + ))}
@@ -310,6 +409,7 @@ export default function ContainerList() { ) } + export function StaleStatusIcon({ status }: { status: string }) { let statusClassName = "" let title = "" diff --git a/web/src/app/images/image-list.tsx b/web/src/app/images/image-list.tsx index 5676ebff..bf41040f 100644 --- a/web/src/app/images/image-list.tsx +++ b/web/src/app/images/image-list.tsx @@ -14,7 +14,7 @@ import { TableRow, } from "@/components/ui/table" import { IImage } from "@/lib/api-models" -import { useState } from "react" +import { useMemo, useState } from "react" import useImages from "@/hooks/useImages" import { convertByteToMb, toastFailed, toastSuccess } from "@/lib/utils" import MainArea from "@/components/widgets/main-area" @@ -27,6 +27,10 @@ import TableButtonDelete from "@/components/widgets/table-button-delete" import { TableNoData } from "@/components/widgets/table-no-data" import apiBaseUrl from "@/lib/api-base-url" import DeleteDialog from "@/components/delete-dialog" +import { Input } from "@/components/ui/input" +import { SortableIcon } from "@/components/widgets/sortable-icon" + +type SortKey = "id" | "name" | "tag" | "status" | "size" export default function ImageList() { const { nodeId } = useParams() @@ -37,9 +41,69 @@ export default function ImageList() { useState(false) const [deleteInProgress, setDeleteInProgress] = useState(false) const [pruneInProgress, setPruneInProgress] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [sortKey, setSortKey] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") + + const sortedAndFilteredImages = useMemo(() => { + if (!images?.items) return [] + + const filtered = images.items.filter((image) => + image.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const sorted = [...filtered].sort((a, b) => { + let aValue: string | number = "" + let bValue: string | number = "" + + switch (sortKey) { + case "id": + aValue = a.id + bValue = b.id + break + case "name": + aValue = a.name + bValue = b.name + break + case "tag": + aValue = a.tag + bValue = b.tag + break + case "status": + aValue = a.inUse ? "In use" : "Unused" + bValue = b.inUse ? "In use" : "Unused" + break + case "size": + aValue = a.size + bValue = b.size + break + default: + break + } + + if (aValue < bValue) { + return sortOrder === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortOrder === "asc" ? 1 : -1 + } + return 0 + }) + + return sorted + }, [images, searchTerm, sortKey, sortOrder]) if (isLoading) return + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortKey(key) + setSortOrder("asc") + } + } + const handleDeleteImageConfirmation = (image: IImage) => { setImage({ ...image }) setDeleteImageConfirmationOpen(true) @@ -120,62 +184,126 @@ export default function ImageList() { Images - +
+ setSearchTerm(e.target.value)} + /> + +
- Id - Name - Tag - Status - Size + handleSort("id")} + > + Id + + + handleSort("name")} + > + Name + + + handleSort("tag")} + > + Tag + + + handleSort("status")} + > + Status + + + handleSort("size")} + > + Size + + Actions - {images?.items?.length === 0 && } - {images?.items && - images?.items.map((item) => ( - - {item.id.substring(7, 19)} - {item.name} - - {item.tag}{" "} - {item.dangling ? ( - (Dangling) - ) : ( - "" - )} - - {item.inUse ? "In use" : "Unused"} - {convertByteToMb(item.size)} - - {!item.inUse && ( - { - e.stopPropagation() - handleDeleteImageConfirmation(item) - }} - /> - )} - - - ))} + {sortedAndFilteredImages.length === 0 && } + {sortedAndFilteredImages.map((item) => ( + + {item.id.substring(7, 19)} + {item.name} + + {item.tag}{" "} + {item.dangling ? ( + (Dangling) + ) : ( + "" + )} + + {item.inUse ? "In use" : "Unused"} + {convertByteToMb(item.size)} + + {!item.inUse && ( + { + e.stopPropagation() + handleDeleteImageConfirmation(item) + }} + /> + )} + + + ))}
) } + diff --git a/web/src/app/networks/network-list.tsx b/web/src/app/networks/network-list.tsx index b2a123f3..8e6aa1cf 100644 --- a/web/src/app/networks/network-list.tsx +++ b/web/src/app/networks/network-list.tsx @@ -14,7 +14,7 @@ import { TableRow, } from "@/components/ui/table" import { INetwork } from "@/lib/api-models" -import { useState } from "react" +import { useMemo, useState } from "react" import useNetworks from "@/hooks/useNetworks" import MainArea from "@/components/widgets/main-area" import TopBar from "@/components/widgets/top-bar" @@ -27,6 +27,8 @@ import { TableNoData } from "@/components/widgets/table-no-data" import { toastFailed, toastSuccess } from "@/lib/utils" import apiBaseUrl from "@/lib/api-base-url" import DeleteDialog from "@/components/delete-dialog" +import { Input } from "@/components/ui/input" +import { SortableIcon } from "@/components/widgets/sortable-icon" const systemNetwoks = [ "none", @@ -37,6 +39,8 @@ const systemNetwoks = [ "docker_volumes-backup-extension-desktop-extension_default", ] +type SortKey = "id" | "name" | "driver" | "scope" | "status" + export default function NetworkList() { const { nodeId } = useParams() const { nodeHead } = useNodeHead(nodeId!) @@ -47,9 +51,69 @@ export default function NetworkList() { useState(false) const [deleteInProgress, setDeleteInProgress] = useState(false) const [pruneInProgress, setPruneInProgress] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [sortKey, setSortKey] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") + + const sortedAndFilteredNetworks = useMemo(() => { + if (!networks?.items) return [] + + const filtered = networks.items.filter((network) => + network.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const sorted = [...filtered].sort((a, b) => { + let aValue: string | number = "" + let bValue: string | number = "" + + switch (sortKey) { + case "id": + aValue = a.id + bValue = b.id + break + case "name": + aValue = a.name + bValue = b.name + break + case "driver": + aValue = a.driver + bValue = b.driver + break + case "scope": + aValue = a.scope + bValue = b.scope + break + case "status": + aValue = a.inUse ? "In use" : "Unused" + bValue = b.inUse ? "In use" : "Unused" + break + default: + break + } + + if (aValue < bValue) { + return sortOrder === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortOrder === "asc" ? 1 : -1 + } + return 0 + }) + + return sorted + }, [networks, searchTerm, sortKey, sortOrder]) if (isLoading) return + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortKey(key) + setSortOrder("asc") + } + } + const handleDeleteNetworkConfirmation = (network: INetwork) => { setNetwork({ ...network }) setDeleteNetworkOpenConfirmation(true) @@ -128,52 +192,117 @@ export default function NetworkList() { Networks - +
+ setSearchTerm(e.target.value)} + /> + +
- Id - Name - Driver - Scope - Status + handleSort("id")} + > + Id + + + handleSort("name")} + > + Name + + + handleSort("driver")} + > + Driver + + + handleSort("scope")} + > + Scope + + + handleSort("status")} + > + Status + + Actions - {networks?.items?.length === 0 && } - {networks?.items && - networks?.items.map((item) => ( - - {item.id.substring(0, 12)} - {item.name} - {item.driver} - {item.scope} - {item.inUse ? "In use" : "Unused"} - - {!systemNetwoks.includes(item.name) && !item.inUse && ( - { - e.stopPropagation() - handleDeleteNetworkConfirmation(item) - }} - /> - )} - - - ))} + {sortedAndFilteredNetworks.length === 0 && ( + + )} + {sortedAndFilteredNetworks.map((item) => ( + + {item.id.substring(0, 12)} + {item.name} + {item.driver} + {item.scope} + {item.inUse ? "In use" : "Unused"} + + {!systemNetwoks.includes(item.name) && !item.inUse && ( + { + e.stopPropagation() + handleDeleteNetworkConfirmation(item) + }} + /> + )} + + + ))}
diff --git a/web/src/app/volumes/volume-list.tsx b/web/src/app/volumes/volume-list.tsx index c9d09d06..f187a435 100644 --- a/web/src/app/volumes/volume-list.tsx +++ b/web/src/app/volumes/volume-list.tsx @@ -14,7 +14,7 @@ import { TableRow, } from "@/components/ui/table" import { IVolume } from "@/lib/api-models" -import { useState } from "react" +import { useMemo, useState } from "react" import MainArea from "@/components/widgets/main-area" import TopBar from "@/components/widgets/top-bar" import TopBarActions from "@/components/widgets/top-bar-actions" @@ -27,6 +27,10 @@ import { TableNoData } from "@/components/widgets/table-no-data" import DeleteDialog from "@/components/delete-dialog" import { convertByteToMb, toastFailed, toastSuccess } from "@/lib/utils" import apiBaseUrl from "@/lib/api-base-url" +import { Input } from "@/components/ui/input" +import { SortableIcon } from "@/components/widgets/sortable-icon" + +type SortKey = "driver" | "name" | "status" export default function VolumeList() { const { nodeId } = useParams() @@ -38,9 +42,61 @@ export default function VolumeList() { useState(false) const [deleteInProgress, setDeleteInProgress] = useState(false) const [pruneInProgress, setPruneInProgress] = useState(false) + const [searchTerm, setSearchTerm] = useState("") + const [sortKey, setSortKey] = useState("name") + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") + + const sortedAndFilteredVolumes = useMemo(() => { + if (!volumes?.items) return [] + + const filtered = volumes.items.filter((volume) => + volume.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + const sorted = [...filtered].sort((a, b) => { + let aValue: string | number = "" + let bValue: string | number = "" + + switch (sortKey) { + case "driver": + aValue = a.driver + bValue = b.driver + break + case "name": + aValue = a.name + bValue = b.name + break + case "status": + aValue = a.inUse ? "In use" : "Unused" + bValue = b.inUse ? "In use" : "Unused" + break + default: + break + } + + if (aValue < bValue) { + return sortOrder === "asc" ? -1 : 1 + } + if (aValue > bValue) { + return sortOrder === "asc" ? 1 : -1 + } + return 0 + }) + + return sorted + }, [volumes, searchTerm, sortKey, sortOrder]) if (isLoading) return + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc") + } else { + setSortKey(key) + setSortOrder("asc") + } + } + const handleDeleteVolumeConfirmation = (volume: IVolume) => { setVolume({ ...volume }) setDeleteVolumeOpenConfirmation(true) @@ -123,48 +179,91 @@ export default function VolumeList() { Volumes - +
+ setSearchTerm(e.target.value)} + /> + +
- Driver - Name - Status + handleSort("driver")} + > + Driver + + + handleSort("name")} + > + Name + + + handleSort("status")} + > + Status + + Actions - {volumes?.items?.length === 0 && } - {volumes?.items && - volumes?.items.map((item) => ( - - {item.driver} - {item.name} - {item.inUse ? "In use" : "Unused"} - - {!item.inUse && ( - { - e.stopPropagation() - handleDeleteVolumeConfirmation(item) - }} - /> - )} - - - ))} + {sortedAndFilteredVolumes.length === 0 && ( + + )} + {sortedAndFilteredVolumes.map((item) => ( + + {item.driver} + {item.name} + {item.inUse ? "In use" : "Unused"} + + {!item.inUse && ( + { + e.stopPropagation() + handleDeleteVolumeConfirmation(item) + }} + /> + )} + + + ))}
diff --git a/web/src/components/widgets/sortable-icon.tsx b/web/src/components/widgets/sortable-icon.tsx new file mode 100644 index 00000000..e358e7e8 --- /dev/null +++ b/web/src/components/widgets/sortable-icon.tsx @@ -0,0 +1,21 @@ +import { ChevronsUpDown, ChevronUp, ChevronDown } from "lucide-react" + +interface SortableIconProps { + sortKey: T + currentSortKey: T + sortOrder: "asc" | "desc" +} + +export function SortableIcon({ + sortKey, + currentSortKey, + sortOrder, +}: SortableIconProps) { + if (sortKey !== currentSortKey) { + return + } + if (sortOrder === "asc") { + return + } + return +}