diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs deleted file mode 100644 index 0809b5424..000000000 --- a/frontend/.eslintrc.cjs +++ /dev/null @@ -1,80 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react/recommended', - 'plugin:storybook/recommended', - ], - overrides: [ - { - env: { - node: true, - }, - files: ['.eslintrc.{js,cjs}'], - parserOptions: { - sourceType: 'script', - }, - }, - ], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - }, - plugins: ['@typescript-eslint', 'react'], - rules: { - 'semi': ['error', 'never'], - 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': true }], - 'jsx-quotes': ['error', 'prefer-single'], - 'no-trailing-spaces': 'error', - 'no-multiple-empty-lines': ['error', { 'max': 1, 'maxEOF': 0, 'maxBOF': 0 }], - 'eol-last': ['error', 'always'], - 'max-len': ['error', { 'code': 120, 'tabWidth': 4, 'ignoreUrls': true, 'ignoreComments': false, 'ignoreRegExpLiterals': true, 'ignoreStrings': true, 'ignoreTemplateLiterals': true }], - 'react/react-in-jsx-scope': 'off', - 'react/jsx-uses-react': 'off', - 'func-style': ['error', 'expression'], - 'react/prop-types': 'off', - '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], - '@typescript-eslint/ban-ts-comment': [ - 'error', - { - 'ts-ignore': 'allow-with-description', - }, - ], - 'indent': ['error', 2, { - 'SwitchCase': 1, - 'VariableDeclarator': 1, - 'outerIIFEBody': 1, - 'MemberExpression': 1, - 'FunctionDeclaration': { - 'parameters': 1, - 'body': 1, - }, - 'FunctionExpression': { - 'parameters': 1, - 'body': 1, - }, - 'CallExpression': { - 'arguments': 1, - }, - 'ArrayExpression': 1, - 'ObjectExpression': 1, - 'ImportDeclaration': 1, - 'flatTernaryExpressions': false, - 'ignoreComments': false, - }], - 'react/jsx-indent': ['error', 2], - 'react/jsx-indent-props': ['error', 2], - 'no-multi-spaces': ['error'], - }, - settings: { - react: { - version: 'detect', - }, - }, -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dd7d3543c..64f341782 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2882,6 +2882,175 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "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, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", @@ -2958,6 +3127,36 @@ "react": ">= 16" } }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6242,6 +6441,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-visitor-keys": { + "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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@tanstack/query-core": { "version": "5.51.21", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", @@ -7653,6 +7865,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -8804,6 +9026,14 @@ "node": ">=6" } }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -8913,6 +9143,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -9204,6 +9447,254 @@ "source-map": "~0.6.1" } }, + "node_modules/eslint": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.9.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "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, + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "license": "MIT", + "peer": true + }, + "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, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "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, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -9216,6 +9707,20 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -9390,6 +9895,14 @@ "dev": true, "peer": true }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/fast-shallow-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", @@ -9417,6 +9930,20 @@ "walk-up-path": "^3.0.1" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/file-system-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", @@ -9527,6 +10054,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC", + "peer": true + }, "node_modules/flow-parser": { "version": "0.242.1", "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.242.1.tgz", @@ -9864,6 +10414,27 @@ "node": ">=4" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -10345,6 +10916,17 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "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, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -10628,6 +11210,14 @@ "node": ">=4" } }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10703,6 +11293,14 @@ "dev": true, "peer": true }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10734,6 +11332,17 @@ "node": ">=12.0.0" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10865,6 +11474,21 @@ "node": ">=6" } }, + "node_modules/levn": { + "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, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lezer-json5": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lezer-json5/-/lezer-json5-2.0.2.tgz", @@ -11007,6 +11631,14 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11582,6 +12214,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/nearley": { "version": "2.20.1", "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", @@ -11847,6 +12487,25 @@ "format-util": "^1.0.3" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -12570,6 +13229,17 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", @@ -13697,6 +14367,16 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", @@ -14675,6 +15355,14 @@ "devOptional": true, "peer": true }, + "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, + "license": "MIT", + "peer": true + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -14755,6 +15443,19 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-dedent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", @@ -14799,6 +15500,20 @@ "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", "dev": true }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", @@ -15522,6 +16237,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/frontend/src/api/modules/use-modules.ts b/frontend/src/api/modules/use-modules.ts index dbfe02d95..5bbbe201f 100644 --- a/frontend/src/api/modules/use-modules.ts +++ b/frontend/src/api/modules/use-modules.ts @@ -1,45 +1,45 @@ -import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect' -import { GetModulesResponse } from '../../protos/xyz/block/ftl/v1/console/console_pb' - import { Code, ConnectError } from '@connectrpc/connect' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useEffect } from 'react' import { useClient } from '../../hooks/use-client' +import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect' +import { useSchema } from '../schema/use-schema' -const fetchModules = async (client: ConsoleService, isVisible: boolean): Promise => { - if (!isVisible) { - throw new Error('Component is not visible') - } +const useModulesKey = 'modules' - const abortController = new AbortController() +export const useModules = () => { + const client = useClient(ConsoleService) + const queryClient = useQueryClient() + const { data: streamingData } = useSchema() - try { - const modules = await client.getModules({}, { signal: abortController.signal }) - return modules ?? [] - } catch (error) { - if (error instanceof ConnectError) { - if (error.code !== Code.Canceled) { - console.error('fetchModules - Connect error:', error) + useEffect(() => { + if (streamingData) { + queryClient.invalidateQueries({ + queryKey: [useModulesKey], + }) + } + }, [streamingData, queryClient]) + + const fetchModules = async (signal: AbortSignal) => { + try { + console.debug('fetching modules from FTL') + const modules = await client.getModules({}, { signal }) + return modules ?? [] + } catch (error) { + if (error instanceof ConnectError) { + if (error.code !== Code.Canceled) { + console.error('fetchModules - Connect error:', error) + } + } else { + console.error('fetchModules:', error) } - } else { - console.error('fetchModules:', error) + throw error } - throw error - } finally { - abortController.abort() } -} - -export const useModules = () => { - const client = useClient(ConsoleService) - const isVisible = useVisibility() - const schema = useSchema() - return useQuery( - ['modules', schema, isVisible], // The query key, include schema and isVisible as dependencies - () => fetchModules(client, isVisible), - { - enabled: isVisible, // Only run the query when the component is visible - refetchOnWindowFocus: false, // Optional: Disable refetching on window focus - staleTime: 1000 * 60 * 5, // Optional: Cache data for 5 minutes - } - ) + return useQuery({ + queryKey: [useModulesKey], + queryFn: async ({ signal }) => fetchModules(signal), + enabled: !!streamingData, + }) } diff --git a/frontend/src/api/schema/use-schema.ts b/frontend/src/api/schema/use-schema.ts index e1d979a89..c554b6ce9 100644 --- a/frontend/src/api/schema/use-schema.ts +++ b/frontend/src/api/schema/use-schema.ts @@ -1,65 +1,53 @@ import { Code, ConnectError } from '@connectrpc/connect' -import { useEffect, useState } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useClient } from '../../hooks/use-client.ts' import { useVisibility } from '../../hooks/use-visibility.ts' import { ControllerService } from '../../protos/xyz/block/ftl/v1/ftl_connect.ts' import { DeploymentChangeType, type PullSchemaResponse } from '../../protos/xyz/block/ftl/v1/ftl_pb.ts' +const streamingSchemaKey = 'streamingSchema' + export const useSchema = () => { const client = useClient(ControllerService) - const [schema, setSchema] = useState([]) + const queryClient = useQueryClient() const isVisible = useVisibility() - useEffect(() => { - const abortController = new AbortController() - - const fetchSchema = async () => { - try { - if (!isVisible) { - abortController.abort() - return + const streamSchema = async (signal: AbortSignal) => { + try { + const schemaMap = new Map() + for await (const response of client.pullSchema({}, { signal })) { + const moduleName = response.moduleName ?? '' + console.log(`schema changed: ${DeploymentChangeType[response.changeType]} ${moduleName}`) + switch (response.changeType) { + case DeploymentChangeType.DEPLOYMENT_ADDED: + schemaMap.set(moduleName, response) + break + case DeploymentChangeType.DEPLOYMENT_CHANGED: + schemaMap.set(moduleName, response) + break + case DeploymentChangeType.DEPLOYMENT_REMOVED: + schemaMap.delete(moduleName) } - const schemaMap = new Map() - for await (const response of client.pullSchema( - {}, - { - signal: abortController.signal, - }, - )) { - const moduleName = response.moduleName ?? '' - console.log(`${response.changeType} ${moduleName}`) - switch (response.changeType) { - case DeploymentChangeType.DEPLOYMENT_ADDED: - schemaMap.set(moduleName, response) - break - case DeploymentChangeType.DEPLOYMENT_CHANGED: - schemaMap.set(moduleName, response) - break - case DeploymentChangeType.DEPLOYMENT_REMOVED: - schemaMap.delete(moduleName) - } - - if (!response.more) { - setSchema(Array.from(schemaMap.values()).sort((a, b) => a.schema?.name?.localeCompare(b.schema?.name ?? '') ?? 0)) - } + if (!response.more) { + const schema = Array.from(schemaMap.values()).sort((a, b) => a.schema?.name?.localeCompare(b.schema?.name ?? '') ?? 0) + queryClient.setQueryData([streamingSchemaKey], schema) } - } catch (error) { - if (error instanceof ConnectError) { - if (error.code !== Code.Canceled) { - console.error('Console service - streamEvents - Connect error:', error) - } - } else { - console.error('Console service - streamEvents:', error) + } + } catch (error) { + if (error instanceof ConnectError) { + if (error.code !== Code.Canceled) { + console.error('useSchema - streamSchema - Connect error:', error) } + } else { + console.error('useSchema - streamSchema:', error) } } + } - fetchSchema() - return () => { - abortController.abort() - } - }, [client, isVisible]) - - return schema + return useQuery({ + queryKey: [streamingSchemaKey], + queryFn: async ({ signal }) => streamSchema(signal), + enabled: isVisible, + }) } diff --git a/frontend/src/api/timeline/index.ts b/frontend/src/api/timeline/index.ts new file mode 100644 index 000000000..c69bc9aa4 --- /dev/null +++ b/frontend/src/api/timeline/index.ts @@ -0,0 +1,5 @@ +export * from './stream-verb-calls' +export * from './timeline-filters' +export * from './use-request-calls' +export * from './use-timeline-calls' +export * from './use-timeline' diff --git a/frontend/src/api/timeline/stream-verb-calls.ts b/frontend/src/api/timeline/stream-verb-calls.ts new file mode 100644 index 000000000..0a5d6b4dd --- /dev/null +++ b/frontend/src/api/timeline/stream-verb-calls.ts @@ -0,0 +1,6 @@ +import { callFilter } from './timeline-filters.ts' +import { useTimelineCalls } from './use-timeline-calls.ts' + +export const useStreamVerbCalls = (moduleName?: string, verbName?: string, enabled = true) => { + return useTimelineCalls(true, [callFilter(moduleName || '', verbName)], enabled) +} diff --git a/frontend/src/services/console.service.ts b/frontend/src/api/timeline/timeline-filters.ts similarity index 51% rename from frontend/src/services/console.service.ts rename to frontend/src/api/timeline/timeline-filters.ts index 31da05972..c75042cdc 100644 --- a/frontend/src/services/console.service.ts +++ b/frontend/src/api/timeline/timeline-filters.ts @@ -1,24 +1,16 @@ import type { Timestamp } from '@bufbuild/protobuf' -import { Code, ConnectError } from '@connectrpc/connect' -import { createClient } from '../hooks/use-client' -import { ConsoleService } from '../protos/xyz/block/ftl/v1/console/console_connect' import { - type CallEvent, - type Event, - EventType, + type EventType, EventsQuery_CallFilter, EventsQuery_DeploymentFilter, EventsQuery_EventTypeFilter, EventsQuery_Filter, EventsQuery_IDFilter, EventsQuery_LogLevelFilter, - EventsQuery_Order, EventsQuery_RequestFilter, EventsQuery_TimeFilter, type LogLevel, -} from '../protos/xyz/block/ftl/v1/console/console_pb' - -const client = createClient(ConsoleService) +} from '../../protos/xyz/block/ftl/v1/console/console_pb' export const requestKeysFilter = (requestKeys: string[]): EventsQuery_Filter => { const filter = new EventsQuery_Filter() @@ -106,88 +98,3 @@ export const eventIdFilter = ({ } return filter } - -export const getRequestCalls = async ({ - abortControllerSignal, - requestKey, -}: { - abortControllerSignal: AbortSignal - requestKey: string -}): Promise => { - const allEvents = await getEvents({ - abortControllerSignal, - filters: [requestKeysFilter([requestKey]), eventTypesFilter([EventType.CALL])], - }) - return allEvents.map((e) => e.entry.value) as CallEvent[] -} - -export const getCalls = async ({ - abortControllerSignal, - destModule, - destVerb, - sourceModule, -}: { - abortControllerSignal: AbortSignal - destModule: string - destVerb?: string - sourceModule?: string -}): Promise => { - const allEvents = await getEvents({ - abortControllerSignal, - filters: [callFilter(destModule, destVerb, sourceModule), eventTypesFilter([EventType.CALL])], - }) - return allEvents.map((e) => e.entry.value) as CallEvent[] -} - -export const getEvents = async ({ - abortControllerSignal, - limit = 1000, - order = EventsQuery_Order.DESC, - filters = [], -}: { - abortControllerSignal: AbortSignal - limit?: number - order?: EventsQuery_Order - filters?: EventsQuery_Filter[] -}): Promise => { - try { - const response = await client.getEvents({ filters, limit, order }, { signal: abortControllerSignal }) - return response.events - } catch (error) { - if (error instanceof ConnectError) { - if (error.code === Code.Canceled) { - return [] - } - } - throw error - } -} - -export const streamEvents = async ({ - abortControllerSignal, - filters, - onEventsReceived, -}: { - abortControllerSignal: AbortSignal - filters: EventsQuery_Filter[] - onEventsReceived: (events: Event[]) => void -}) => { - try { - for await (const response of client.streamEvents( - { updateInterval: { seconds: BigInt(1) }, query: { limit: 200, filters, order: EventsQuery_Order.DESC } }, - { signal: abortControllerSignal }, - )) { - if (response.events) { - onEventsReceived(response.events) - } - } - } catch (error) { - if (error instanceof ConnectError) { - if (error.code !== Code.Canceled) { - console.error('Console service - streamEvents - Connect error:', error) - } - } else { - console.error('Console service - streamEvents:', error) - } - } -} diff --git a/frontend/src/api/timeline/use-request-calls.ts b/frontend/src/api/timeline/use-request-calls.ts new file mode 100644 index 000000000..e787d5963 --- /dev/null +++ b/frontend/src/api/timeline/use-request-calls.ts @@ -0,0 +1,6 @@ +import { requestKeysFilter } from './timeline-filters' +import { useTimelineCalls } from './use-timeline-calls' + +export const useRequestCalls = (requestKey?: string) => { + return useTimelineCalls(true, [requestKeysFilter([requestKey || ''])], !!requestKey) +} diff --git a/frontend/src/api/timeline/use-timeline-calls.ts b/frontend/src/api/timeline/use-timeline-calls.ts new file mode 100644 index 000000000..870d9a859 --- /dev/null +++ b/frontend/src/api/timeline/use-timeline-calls.ts @@ -0,0 +1,16 @@ +import { type CallEvent, EventType, type EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' +import { eventTypesFilter } from './timeline-filters.ts' +import { useTimeline } from './use-timeline.ts' + +export const useTimelineCalls = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => { + const allFilters = [...filters, eventTypesFilter([EventType.CALL])] + const timelineQuery = useTimeline(isStreaming, allFilters, enabled) + + // Map the events to CallEvent for ease of use + const data = timelineQuery.data?.map((event) => event.entry.value as CallEvent) || [] + + return { + ...timelineQuery, + data, + } +} diff --git a/frontend/src/api/timeline/use-timeline.ts b/frontend/src/api/timeline/use-timeline.ts new file mode 100644 index 000000000..63b30f86b --- /dev/null +++ b/frontend/src/api/timeline/use-timeline.ts @@ -0,0 +1,63 @@ +import { Code, ConnectError } from '@connectrpc/connect' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useClient } from '../../hooks/use-client' +import { useVisibility } from '../../hooks/use-visibility' +import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect' +import { type EventsQuery_Filter, EventsQuery_Order } from '../../protos/xyz/block/ftl/v1/console/console_pb' + +const timelineKey = 'timeline' +const maxTimelineEntries = 1000 + +export const useTimeline = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => { + const client = useClient(ConsoleService) + const queryClient = useQueryClient() + const isVisible = useVisibility() + + const order = EventsQuery_Order.DESC + const limit = isStreaming ? 200 : 1000 + + const queryKey = [timelineKey, isStreaming, filters, order, limit] + + const fetchTimeline = async ({ signal }: { signal: AbortSignal }) => { + try { + console.log('fetching timeline') + const response = await client.getEvents({ filters, limit, order }, { signal }) + return response.events + } catch (error) { + if (error instanceof ConnectError) { + if (error.code === Code.Canceled) { + return [] + } + } + throw error + } + } + + const streamTimeline = async ({ signal }: { signal: AbortSignal }) => { + try { + console.log('streaming timeline') + console.log('filters:', filters) + for await (const response of client.streamEvents({ updateInterval: { seconds: BigInt(1) }, query: { limit, filters, order } }, { signal })) { + if (response.events) { + const prev = queryClient.getQueryData(queryKey) ?? [] + const allEvents = [...response.events, ...prev].slice(0, maxTimelineEntries) + queryClient.setQueryData(queryKey, allEvents) + } + } + } catch (error) { + if (error instanceof ConnectError) { + if (error.code !== Code.Canceled) { + console.error('Console service - streamEvents - Connect error:', error) + } + } else { + console.error('Console service - streamEvents:', error) + } + } + } + + return useQuery({ + queryKey: queryKey, + queryFn: async ({ signal }) => (isStreaming ? streamTimeline({ signal }) : fetchTimeline({ signal })), + enabled: enabled && isVisible, + }) +} diff --git a/frontend/src/components/CodeEditor.tsx b/frontend/src/components/CodeEditor.tsx index cd4646af5..3d3c53d0a 100644 --- a/frontend/src/components/CodeEditor.tsx +++ b/frontend/src/components/CodeEditor.tsx @@ -14,7 +14,7 @@ import { defaultKeymap } from '@codemirror/commands' import { handleRefresh, jsonSchemaHover, jsonSchemaLinter, stateExtensions } from 'codemirror-json-schema' import { json5, json5ParseLinter } from 'codemirror-json5' import { useCallback, useEffect, useRef } from 'react' -import { useDarkMode } from '../hooks/use-dark-mode' +import { useUserPreferences } from '../providers/user-preferences-provider' const commonExtensions = [ gutter({ class: 'CodeMirror-lint-markers' }), @@ -33,7 +33,7 @@ export interface InitialState { } export const CodeEditor = ({ initialState, onTextChanged }: { initialState: InitialState; onTextChanged?: (text: string) => void }) => { - const { isDarkMode } = useDarkMode() + const { isDarkMode } = useUserPreferences() const editorContainerRef = useRef(null) const editorViewRef = useRef(null) diff --git a/frontend/src/components/DarkModeSwitch.tsx b/frontend/src/components/DarkModeSwitch.tsx index 741e3e777..ca6489f76 100644 --- a/frontend/src/components/DarkModeSwitch.tsx +++ b/frontend/src/components/DarkModeSwitch.tsx @@ -1,10 +1,10 @@ import { Switch } from '@headlessui/react' import { MoonIcon, SunIcon } from '@heroicons/react/20/solid' +import { useUserPreferences } from '../providers/user-preferences-provider' import { classNames } from '../utils/react.utils' -import { useDarkMode } from '../hooks/use-dark-mode' export const DarkModeSwitch = () => { - const { isDarkMode, setDarkMode } = useDarkMode() + const { isDarkMode, setDarkMode } = useUserPreferences() return ( { + return ( +
+ + Loading... +
+ ) +} diff --git a/frontend/src/features/console/ConsolePage.tsx b/frontend/src/features/console/ConsolePage.tsx index 79479914e..17b4bce26 100644 --- a/frontend/src/features/console/ConsolePage.tsx +++ b/frontend/src/features/console/ConsolePage.tsx @@ -1,10 +1,10 @@ import { CubeTransparentIcon } from '@heroicons/react/24/outline' -import { useContext, useState } from 'react' +import { useState } from 'react' import { type NavigateFunction, useNavigate } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' import { ResizablePanels } from '../../components/ResizablePanels' import { Page } from '../../layout' import { Config, Module, Secret, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../providers/modules-provider' import { type FTLNode, GraphPane } from '../graph/GraphPane' import BottomPanel from './BottomPanel' import type { ExpandablePanelProps } from './ExpandablePanel' @@ -15,10 +15,14 @@ import { secretPanels } from './right-panel/SecretPanels' import { verbPanels } from './right-panel/VerbPanels' export const ConsolePage = () => { - const modules = useContext(modulesContext) + const modules = useModules() const navigate = useNavigate() const [selectedNode, setSelectedNode] = useState(null) + if (!modules.isSuccess) { + return Loading... + } + return ( } title='Console' /> @@ -26,7 +30,7 @@ export const ConsolePage = () => { } rightPanelHeader={headerForNode(selectedNode)} - rightPanelPanels={panelsForNode(modules.modules, selectedNode, navigate)} + rightPanelPanels={panelsForNode(modules.data.modules, selectedNode, navigate)} bottomPanelContent={} /> diff --git a/frontend/src/features/deployments/DeploymentCard.tsx b/frontend/src/features/deployments/DeploymentCard.tsx index 80fb9a48f..6379a0a61 100644 --- a/frontend/src/features/deployments/DeploymentCard.tsx +++ b/frontend/src/features/deployments/DeploymentCard.tsx @@ -1,23 +1,23 @@ -import { useContext, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' import { Badge } from '../../components/Badge' import { Card } from '../../components/Card' import { Chip } from '../../components/Chip' import type { Module } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../providers/modules-provider' import { deploymentTextColor } from './deployment.utils' export const DeploymentCard = ({ deploymentKey, className }: { deploymentKey: string; className?: string }) => { const navigate = useNavigate() - const { modules } = useContext(modulesContext) + const modules = useModules() const [module, setModule] = useState() useEffect(() => { - if (modules) { - const module = modules.find((module) => module.deploymentKey === deploymentKey) + if (modules.isSuccess) { + const module = modules.data.modules.find((module) => module.deploymentKey === deploymentKey) setModule(module) } - }, [modules]) + }, [modules.data]) return ( navigate(`/deployments/${deploymentKey}`)}> diff --git a/frontend/src/features/deployments/DeploymentPage.tsx b/frontend/src/features/deployments/DeploymentPage.tsx index ac9c0782b..f898bde54 100644 --- a/frontend/src/features/deployments/DeploymentPage.tsx +++ b/frontend/src/features/deployments/DeploymentPage.tsx @@ -1,14 +1,14 @@ import { RocketLaunchIcon } from '@heroicons/react/24/outline' import { useContext, useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' +import { modulesFilter } from '../../api/timeline' import { Badge } from '../../components/Badge' import { Card } from '../../components/Card' import { Page } from '../../layout' import type { Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../providers/modules-provider' import { NotificationType, NotificationsContext } from '../../providers/notifications-provider' import { SidePanelProvider } from '../../providers/side-panel-provider' -import { modulesFilter } from '../../services/console.service' import { deploymentKeyModuleName } from '../modules/module.utils' import { Timeline } from '../timeline/Timeline' import { isCron, isExported, isHttpIngress } from '../verbs/verb.utils' @@ -18,7 +18,7 @@ const timeSettings = { isTailing: true, isPaused: false } export const DeploymentPage = () => { const navigate = useNavigate() const { deploymentKey } = useParams() - const modules = useContext(modulesContext) + const modules = useModules() const notification = useContext(NotificationsContext) const navgation = useNavigate() const [module, setModule] = useState() @@ -30,12 +30,12 @@ export const DeploymentPage = () => { }, [module?.deploymentKey]) useEffect(() => { - if (modules.modules.length > 0 && deploymentKey) { - let module = modules.modules.find((module) => module.deploymentKey === deploymentKey) + if (modules.isSuccess && modules.data.modules.length > 0 && deploymentKey) { + let module = modules.data.modules.find((module) => module.deploymentKey === deploymentKey) if (!module) { const moduleName = deploymentKeyModuleName(deploymentKey) if (moduleName) { - module = modules.modules.find((module) => module.name === moduleName) + module = modules.data.modules.find((module) => module.name === moduleName) navgation(`/deployments/${module?.deploymentKey}`) notification.showNotification({ title: 'Showing latest deployment', @@ -48,7 +48,7 @@ export const DeploymentPage = () => { setModule(module) } } - }, [modules, deploymentKey]) + }, [modules.data, deploymentKey]) return ( diff --git a/frontend/src/features/deployments/DeploymentsPage.tsx b/frontend/src/features/deployments/DeploymentsPage.tsx index 218dd39c8..e0456efe3 100644 --- a/frontend/src/features/deployments/DeploymentsPage.tsx +++ b/frontend/src/features/deployments/DeploymentsPage.tsx @@ -1,21 +1,27 @@ import { RocketLaunchIcon } from '@heroicons/react/24/outline' -import { useContext } from 'react' +import { useModules } from '../../api/modules/use-modules' import { Page } from '../../layout' -import { modulesContext } from '../../providers/modules-provider' import { DeploymentCard } from './DeploymentCard' export const DeploymentsPage = () => { - const modules = useContext(modulesContext) + const modules = useModules() + + if (!modules.isSuccess) { + return Loading... + } return ( } title='Deployments' /> -
- {modules.modules.map((module) => ( - - ))} -
+ {modules.isLoading &&
Loading...
} + {modules.isSuccess && ( +
+ {modules.data.modules.map((module) => ( + + ))} +
+ )}
) diff --git a/frontend/src/features/graph/GraphPane.tsx b/frontend/src/features/graph/GraphPane.tsx index 9f4cfc84d..1942aff46 100644 --- a/frontend/src/features/graph/GraphPane.tsx +++ b/frontend/src/features/graph/GraphPane.tsx @@ -1,9 +1,9 @@ -import { useContext, useEffect } from 'react' +import { useEffect } from 'react' import ReactFlow, { Background, Controls, useEdgesState, useNodesState } from 'reactflow' import 'reactflow/dist/style.css' import React from 'react' +import { useModules } from '../../api/modules/use-modules' import type { Config, Module, Secret, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../providers/modules-provider' import { ConfigNode } from './ConfigNode' import { GroupNode } from './GroupNode' import { SecretNode } from './SecretNode' @@ -18,20 +18,21 @@ interface GraphPaneProps { } export const GraphPane: React.FC = ({ onTapped }) => { - const modules = useContext(modulesContext) + const modules = useModules() const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [selectedNode, setSelectedNode] = React.useState(null) useEffect(() => { - const { nodes: newNodes, edges: newEdges } = layoutNodes(modules.modules, modules.topology) + if (!modules.isSuccess) return + const { nodes: newNodes, edges: newEdges } = layoutNodes(modules.data.modules, modules.data.topology) // Need to update after render loop for ReactFlow to pick up the changes setTimeout(() => { setNodes(newNodes) setEdges(newEdges) }, 0) - }, [modules.modules]) + }, [modules.data?.modules]) useEffect(() => { const currentNodes = nodes.map((node) => { diff --git a/frontend/src/features/requests/RequestGraph.tsx b/frontend/src/features/requests/RequestGraph.tsx index d5465eed2..f2736a288 100644 --- a/frontend/src/features/requests/RequestGraph.tsx +++ b/frontend/src/features/requests/RequestGraph.tsx @@ -1,4 +1,6 @@ import type { Duration, Timestamp } from '@bufbuild/protobuf' +import { useRequestCalls } from '../../api/timeline/use-request-calls' +import { Loader } from '../../components/Loader' import type { CallEvent } from '../../protos/xyz/block/ftl/v1/console/console_pb' import { verbRefString } from '../verbs/verb.utils' @@ -53,12 +55,23 @@ const CallBlock = ({ } interface Props { - calls: CallEvent[] call?: CallEvent setSelectedCall: React.Dispatch> } -export const RequestGraph = ({ calls, call, setSelectedCall }: Props) => { +export const RequestGraph = ({ call, setSelectedCall }: Props) => { + const requestCalls = useRequestCalls(call?.requestKey) + + if (requestCalls.isLoading) { + return ( +
+ +
+ ) + } + + const calls = requestCalls.data?.reverse() || [] + if (calls.length === 0) { return <> } diff --git a/frontend/src/features/timeline/Timeline.tsx b/frontend/src/features/timeline/Timeline.tsx index 009f5106a..f28204f41 100644 --- a/frontend/src/features/timeline/Timeline.tsx +++ b/frontend/src/features/timeline/Timeline.tsx @@ -1,10 +1,10 @@ import type { Timestamp } from '@bufbuild/protobuf' import { useContext, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' -import { useVisibility } from '../../hooks/use-visibility.ts' +import { timeFilter, useTimeline } from '../../api/timeline/index.ts' +import { Loader } from '../../components/Loader.tsx' import type { Event, EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' import { SidePanelContext } from '../../providers/side-panel-provider.tsx' -import { eventIdFilter, getEvents, streamEvents, timeFilter } from '../../services/console.service.ts' import { formatTimestampShort } from '../../utils/date.utils.ts' import { panelColor } from '../../utils/style.utils.ts' import { deploymentTextColor } from '../deployments/deployment.utils.ts' @@ -19,62 +19,19 @@ import { TimelineDeploymentUpdatedDetails } from './details/TimelineDeploymentUp import { TimelineLogDetails } from './details/TimelineLogDetails.tsx' import type { TimeSettings } from './filters/TimelineTimeControls.tsx' -const maxTimelineEntries = 1000 - export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings; filters: EventsQuery_Filter[] }) => { const [searchParams, setSearchParams] = useSearchParams() const { openPanel, closePanel, isOpen } = useContext(SidePanelContext) - const [entries, setEntries] = useState([]) const [selectedEntry, setSelectedEntry] = useState(null) - const isVisible = useVisibility() - useEffect(() => { - const eventId = searchParams.get('id') - const abortController = new AbortController() - if (!isVisible) { - abortController.abort() - return - } - - const fetchEvents = async () => { - let eventFilters = filters - if (timeSettings.newerThan || timeSettings.olderThan) { - eventFilters = [timeFilter(timeSettings.olderThan, timeSettings.newerThan), ...filters] - } - - if (eventId) { - const id = BigInt(eventId) - eventFilters = [eventIdFilter({ higherThan: id }), ...filters] - } - const events = await getEvents({ abortControllerSignal: abortController.signal, filters: eventFilters }) - setEntries(events) + let eventFilters = filters + if (timeSettings.newerThan || timeSettings.olderThan) { + eventFilters = [timeFilter(timeSettings.olderThan, timeSettings.newerThan), ...filters] + } - if (eventId) { - const entry = events.find((event) => event.id.toString() === eventId) - if (entry) { - handleEntryClicked(entry) - } - } - } + const streamTimeline = timeSettings.isTailing && !timeSettings.isPaused - if (timeSettings.isTailing && !timeSettings.isPaused && !eventId) { - setEntries([]) - streamEvents({ - abortControllerSignal: abortController.signal, - filters, - onEventsReceived: (events) => { - if (!timeSettings.isPaused) { - setEntries((prev) => [...events, ...prev].slice(0, maxTimelineEntries)) - } - }, - }) - } else { - fetchEvents() - } - return () => { - abortController.abort() - } - }, [filters, timeSettings, isVisible]) + const timeline = useTimeline(streamTimeline, eventFilters) useEffect(() => { if (!isOpen) { @@ -130,6 +87,16 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings } } + if (timeline.isLoading) { + return ( +
+ +
+ ) + } + + const entries = timeline.data || [] + return (
diff --git a/frontend/src/features/timeline/details/TimelineCallDetails.tsx b/frontend/src/features/timeline/details/TimelineCallDetails.tsx index 226451644..7f43dd4c2 100644 --- a/frontend/src/features/timeline/details/TimelineCallDetails.tsx +++ b/frontend/src/features/timeline/details/TimelineCallDetails.tsx @@ -3,11 +3,8 @@ import { useContext, useEffect, useState } from 'react' import { AttributeBadge } from '../../../components/AttributeBadge' import { CloseButton } from '../../../components/CloseButton' import { CodeBlock } from '../../../components/CodeBlock' -import { useClient } from '../../../hooks/use-client' -import { ConsoleService } from '../../../protos/xyz/block/ftl/v1/console/console_connect' import type { CallEvent } from '../../../protos/xyz/block/ftl/v1/console/console_pb' import { SidePanelContext } from '../../../providers/side-panel-provider' -import { getRequestCalls } from '../../../services/console.service' import { formatDuration } from '../../../utils/date.utils' import { DeploymentCard } from '../../deployments/DeploymentCard' import { RequestGraph } from '../../requests/RequestGraph' @@ -15,35 +12,13 @@ import { verbRefString } from '../../verbs/verb.utils' import { TimelineTimestamp } from './TimelineTimestamp' export const TimelineCallDetails = ({ timestamp, call }: { timestamp?: Timestamp; call: CallEvent }) => { - const client = useClient(ConsoleService) const { closePanel } = useContext(SidePanelContext) - const [requestCalls, setRequestCalls] = useState([]) const [selectedCall, setSelectedCall] = useState(call) useEffect(() => { setSelectedCall(call) }, [call]) - useEffect(() => { - const abortController = new AbortController() - const fetchRequestCalls = async () => { - if (selectedCall.requestKey === undefined) { - return - } - const calls = await getRequestCalls({ - abortControllerSignal: abortController.signal, - requestKey: selectedCall.requestKey, - }) - setRequestCalls(calls.reverse()) - } - - fetchRequestCalls() - - return () => { - abortController.abort() - } - }, [client, selectedCall]) - return (
@@ -61,7 +36,7 @@ export const TimelineCallDetails = ({ timestamp, call }: { timestamp?: Timestamp
- +
Request
diff --git a/frontend/src/features/timeline/filters/TimelineFilterPanel.tsx b/frontend/src/features/timeline/filters/TimelineFilterPanel.tsx index 95d64010d..03f391ec9 100644 --- a/frontend/src/features/timeline/filters/TimelineFilterPanel.tsx +++ b/frontend/src/features/timeline/filters/TimelineFilterPanel.tsx @@ -1,9 +1,9 @@ import { PhoneIcon, RocketLaunchIcon } from '@heroicons/react/24/outline' import type React from 'react' -import { useContext, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' +import { useModules } from '../../../api/modules/use-modules' +import { eventTypesFilter, logLevelFilter, modulesFilter } from '../../../api/timeline' import { EventType, type EventsQuery_Filter, LogLevel } from '../../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../../providers/modules-provider' -import { eventTypesFilter, logLevelFilter, modulesFilter } from '../../../services/console.service' import { textColor } from '../../../utils' import { LogLevelBadgeSmall } from '../../logs/LogLevelBadgeSmall' import { logLevelBgColor, logLevelColor, logLevelRingColor } from '../../logs/log.utils' @@ -43,24 +43,24 @@ export const TimelineFilterPanel = ({ }: { onFiltersChanged: (filters: EventsQuery_Filter[]) => void }) => { - const modules = useContext(modulesContext) + const modules = useModules() const [selectedEventTypes, setSelectedEventTypes] = useState(Object.keys(EVENT_TYPES)) const [selectedModules, setSelectedModules] = useState([]) const [previousModules, setPreviousModules] = useState([]) const [selectedLogLevel, setSelectedLogLevel] = useState(1) useEffect(() => { - if (modules.modules.length === 0) { + if (!modules.isSuccess || modules.data.modules.length === 0) { return } - const newModules = modules.modules.map((module) => module.deploymentKey) + const newModules = modules.data.modules.map((module) => module.deploymentKey) const addedModules = newModules.filter((name) => !previousModules.includes(name)) if (addedModules.length > 0) { setSelectedModules((prevSelected) => [...prevSelected, ...addedModules]) } setPreviousModules(newModules) - }, [modules]) + }, [modules.data]) useEffect(() => { const filter: EventsQuery_Filter[] = [] @@ -144,40 +144,42 @@ export const TimelineFilterPanel = ({ - -
- - | - -
- {modules.modules.map((module) => ( -
-
- handleModuleChanged(module.deploymentKey, e.target.checked)} - className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer' - /> -
-
- -
+ {modules.isSuccess && ( + +
+ + | +
- ))} -
+ {modules.data.modules.map((module) => ( +
+
+ handleModuleChanged(module.deploymentKey, e.target.checked)} + className='h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer' + /> +
+
+ +
+
+ ))} + + )}
diff --git a/frontend/src/features/verbs/VerbCalls.tsx b/frontend/src/features/verbs/VerbCalls.tsx deleted file mode 100644 index 419877ad7..000000000 --- a/frontend/src/features/verbs/VerbCalls.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Timestamp } from '@bufbuild/protobuf' -import { useContext, useEffect, useState } from 'react' -import { useClient } from '../../hooks/use-client.ts' -import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect.ts' -import type { CallEvent, Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { SidePanelContext } from '../../providers/side-panel-provider.tsx' -import { getCalls } from '../../services/console.service.ts' -import { formatDuration, formatTimestamp } from '../../utils/date.utils.ts' -import { TimelineCallDetails } from '../timeline/details/TimelineCallDetails.tsx' - -export const VerbCalls = ({ module, verb }: { module?: Module; verb?: Verb }) => { - const client = useClient(ConsoleService) - const [calls, setCalls] = useState([]) - const { openPanel } = useContext(SidePanelContext) - - useEffect(() => { - const abortController = new AbortController() - const fetchCalls = async () => { - if (module === undefined) { - return - } - - const calls = await getCalls({ - abortControllerSignal: abortController.signal, - destModule: module.name, - destVerb: verb?.verb?.name, - }) - setCalls(calls) - } - - fetchCalls() - - return () => { - abortController.abort() - } - }, [client, module, verb]) - - const handleClick = (call: CallEvent) => { - openPanel() - } - - return ( - <> -
- - - - - - - - - - - {calls.map((call, index) => ( - handleClick(call)} className='cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800'> - - - - - - ))} - -
- Request - - Source - - Time - - Duration(ms) -
-
-
{call.requestKey?.toString()}
-
-
-
-
- {call.sourceVerbRef && [call.sourceVerbRef.module, call.sourceVerbRef.name].join(':')} -
-
-
-
-
{formatTimestamp(call.timeStamp)}
-
-
- {formatDuration(call.duration)} -
-
- - ) -} diff --git a/frontend/src/features/verbs/VerbPage.tsx b/frontend/src/features/verbs/VerbPage.tsx index 0d703f875..18071e1ce 100644 --- a/frontend/src/features/verbs/VerbPage.tsx +++ b/frontend/src/features/verbs/VerbPage.tsx @@ -1,13 +1,14 @@ import { BoltIcon, Square3Stack3DIcon } from '@heroicons/react/24/outline' import { useContext, useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' +import { useStreamVerbCalls } from '../../api/timeline/stream-verb-calls' +import { Loader } from '../../components/Loader' import { ResizablePanels } from '../../components/ResizablePanels' import { Page } from '../../layout' -import { type CallEvent, EventType, type Module, type Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' -import { modulesContext } from '../../providers/modules-provider' +import type { CallEvent, Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' import { NotificationType, NotificationsContext } from '../../providers/notifications-provider' import { SidePanelProvider } from '../../providers/side-panel-provider' -import { callFilter, eventTypesFilter, streamEvents } from '../../services/console.service' import { CallList } from '../calls/CallList' import { deploymentKeyModuleName } from '../modules/module.utils' import { VerbRequestForm } from './VerbRequestForm' @@ -17,19 +18,19 @@ export const VerbPage = () => { const { deploymentKey, verbName } = useParams() const notification = useContext(NotificationsContext) const navgation = useNavigate() - const modules = useContext(modulesContext) + const modules = useModules() const [module, setModule] = useState() const [verb, setVerb] = useState() - const [calls, setCalls] = useState([]) useEffect(() => { - if (modules.modules.length === 0 || !deploymentKey || !verbName) return + if (!modules.isSuccess) return + if (modules.data.modules.length === 0 || !deploymentKey || !verbName) return - let module = modules.modules.find((module) => module.deploymentKey === deploymentKey) + let module = modules.data.modules.find((module) => module.deploymentKey === deploymentKey) if (!module) { const moduleName = deploymentKeyModuleName(deploymentKey) if (moduleName) { - module = modules.modules.find((module) => module.name === moduleName) + module = modules.data.modules.find((module) => module.name === moduleName) navgation(`/deployments/${module?.deploymentKey}/verbs/${verbName}`) notification.showNotification({ title: 'Showing latest deployment', @@ -41,30 +42,18 @@ export const VerbPage = () => { setModule(module) const verb = module?.verbs.find((verb) => verb.verb?.name.toLocaleLowerCase() === verbName?.toLocaleLowerCase()) setVerb(verb) - }, [modules, deploymentKey]) + }, [modules.data, deploymentKey]) - useEffect(() => { - const abortController = new AbortController() - if (!module) return - - const streamCalls = async () => { - setCalls([]) - streamEvents({ - abortControllerSignal: abortController.signal, - filters: [callFilter(module.name, verb?.verb?.name), eventTypesFilter([EventType.CALL])], - onEventsReceived: (events) => { - const callEvents = events.map((event) => event.entry.value as CallEvent) - setCalls((prev) => [...callEvents, ...prev]) - }, - }) - } + const callEvents = useStreamVerbCalls(module?.name, verb?.verb?.name) + const calls: CallEvent[] = callEvents.data || [] - streamCalls() - - return () => { - abortController.abort() - } - }, [module]) + if (!module || !verb || callEvents.isLoading) { + return ( +
+ +
+ ) + } const header = (
diff --git a/frontend/src/features/verbs/VerbRequestForm.tsx b/frontend/src/features/verbs/VerbRequestForm.tsx index c478cb42e..b81d2b9e6 100644 --- a/frontend/src/features/verbs/VerbRequestForm.tsx +++ b/frontend/src/features/verbs/VerbRequestForm.tsx @@ -84,8 +84,10 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb const requestBytes = createVerbRequest(path, verb, editorText, headersText) const response = await client.call({ verb: verbRef, body: requestBytes }) + if (response.response.case === 'body') { - const jsonString = Buffer.from(response.response.value).toString('utf-8') + const textDecoder = new TextDecoder('utf-8') + const jsonString = textDecoder.decode(response.response.value) setResponse(JSON.stringify(JSON.parse(jsonString), null, 2)) } else if (response.response.case === 'error') { diff --git a/frontend/src/features/verbs/verb.utils.ts b/frontend/src/features/verbs/verb.utils.ts index b3dd8c1b2..83cb4b9b0 100644 --- a/frontend/src/features/verbs/verb.utils.ts +++ b/frontend/src/features/verbs/verb.utils.ts @@ -171,8 +171,9 @@ export const createVerbRequest = (path: string, verb?: Verb, editorText?: string requestJson = newRequestJson } - const buffer = Buffer.from(JSON.stringify(requestJson)) - return new Uint8Array(buffer) + const textEncoder = new TextEncoder() + const encoded = textEncoder.encode(JSON.stringify(requestJson)) + return encoded } export const verbCalls = (verb?: Verb) => { diff --git a/frontend/src/hooks/use-dark-mode.ts b/frontend/src/hooks/use-dark-mode.ts deleted file mode 100644 index 75c3792b3..000000000 --- a/frontend/src/hooks/use-dark-mode.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from 'react' -import { useLocalStorage } from 'react-use' - -export const useDarkMode = () => { - const [isDarkMode, setDarkMode] = useLocalStorage('dark-mode', false) - - useEffect(() => { - if (isDarkMode) { - document.documentElement.classList.add('dark') - } else { - document.documentElement.classList.remove('dark') - } - }, [isDarkMode]) - - return { isDarkMode, setDarkMode } -} diff --git a/frontend/src/layout/VSCodeLayout.tsx b/frontend/src/layout/VSCodeLayout.tsx deleted file mode 100644 index ae46126bd..000000000 --- a/frontend/src/layout/VSCodeLayout.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useState } from 'react' - -export const Layout = () => { - const [leftPaneWidth, setLeftPaneWidth] = useState(200) - const [rightPaneWidth, setRightPaneWidth] = useState(200) - const [bottomPaneHeight, setBottomPaneHeight] = useState(100) - - const handleMouseDown = (e: React.MouseEvent, direction: string) => { - const startX = e.clientX - const startY = e.clientY - const startLeftPaneWidth = leftPaneWidth - const startRightPaneWidth = rightPaneWidth - const startBottomPaneHeight = bottomPaneHeight - - const handleMouseMove = (e: MouseEvent) => { - if (direction === 'left') { - const newLeftPaneWidth = startLeftPaneWidth + e.clientX - startX - setLeftPaneWidth(newLeftPaneWidth > 100 ? newLeftPaneWidth : 100) - } else if (direction === 'right') { - const newRightPaneWidth = startRightPaneWidth - (e.clientX - startX) - setRightPaneWidth(newRightPaneWidth > 100 ? newRightPaneWidth : 100) - } else if (direction === 'bottom') { - const newBottomPaneHeight = startBottomPaneHeight - (e.clientY - startY) - setBottomPaneHeight(newBottomPaneHeight > 50 ? newBottomPaneHeight : 50) - } - } - - const handleMouseUp = () => { - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - } - - return ( -
-
-
- {/* Activity Bar */} -
-
-
- {/* Left Pane */} -
handleMouseDown(e, 'left')} - /> -
-
-
-
- {/* Main Editor Area */} -
-
- {/* Right Pane */} -
handleMouseDown(e, 'right')} - /> -
-
-
- {/* Bottom Pane */} -
handleMouseDown(e, 'bottom')} - /> -
-
-
-
-
- ) -} diff --git a/frontend/src/layout/navigation/MobileNavigation.tsx b/frontend/src/layout/navigation/MobileNavigation.tsx index fa54ca8f4..169789f61 100644 --- a/frontend/src/layout/navigation/MobileNavigation.tsx +++ b/frontend/src/layout/navigation/MobileNavigation.tsx @@ -1,13 +1,12 @@ import { XMarkIcon } from '@heroicons/react/24/outline' -import { useContext } from 'react' import { NavLink } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' import { DarkModeSwitch } from '../../components' -import { modulesContext } from '../../providers/modules-provider' import { classNames } from '../../utils' import { navigation } from './navigation-items' const MobileNavigation = ({ onClose }: { onClose: () => void }) => { - const modules = useContext(modulesContext) + const modules = useModules() return (
@@ -40,7 +39,7 @@ const MobileNavigation = ({ onClose }: { onClose: () => void }) => { className='ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-indigo-600 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-white ring-1 ring-inset ring-indigo-500' aria-hidden='true' > - {modules.modules.length} + {modules.isSuccess && modules.data.modules.length} )}
diff --git a/frontend/src/layout/navigation/Navigation.tsx b/frontend/src/layout/navigation/Navigation.tsx index 952be361a..8aae20f8b 100644 --- a/frontend/src/layout/navigation/Navigation.tsx +++ b/frontend/src/layout/navigation/Navigation.tsx @@ -1,8 +1,7 @@ import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon } from '@heroicons/react/24/outline' -import { useContext } from 'react' import { Link, NavLink } from 'react-router-dom' +import { useModules } from '../../api/modules/use-modules' import { DarkModeSwitch } from '../../components/DarkModeSwitch' -import { modulesContext } from '../../providers/modules-provider' import { classNames } from '../../utils' import { navigation } from './navigation-items' @@ -13,7 +12,7 @@ export const Navigation = ({ isCollapsed: boolean setIsCollapsed: React.Dispatch> }) => { - const modules = useContext(modulesContext) + const modules = useModules() return (
@@ -77,7 +76,7 @@ export const Navigation = ({ className='ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-indigo-600 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-white ring-1 ring-inset ring-indigo-500' aria-hidden='true' > - {modules.modules.length} + {modules.isSuccess && modules.data.modules.length} )} diff --git a/frontend/src/providers/app-providers.tsx b/frontend/src/providers/app-providers.tsx index 18af33157..1be306849 100644 --- a/frontend/src/providers/app-providers.tsx +++ b/frontend/src/providers/app-providers.tsx @@ -1,16 +1,16 @@ -import { ModulesProvider } from './modules-provider' import { NotificationsProvider } from './notifications-provider' import { ReactQueryProvider } from './react-query-provider' import { RoutingProvider } from './routing-provider' +import { UserPreferencesProvider } from './user-preferences-provider' export const AppProvider = () => { return ( - + - + ) } diff --git a/frontend/src/providers/dark-mode-provider.tsx b/frontend/src/providers/dark-mode-provider.tsx deleted file mode 100644 index c30adaf7c..000000000 --- a/frontend/src/providers/dark-mode-provider.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { type PropsWithChildren, useContext } from 'react' -import useLocalStorage from '../hooks/use-local-storage' - -const DarkModeContext = React.createContext({ - isDarkMode: false, - setDarkMode: (_: boolean) => {}, -}) - -export const useDarkMode = () => { - return useContext(DarkModeContext) -} - -export const DarkModeProvider = ({ children }: PropsWithChildren) => { - const [isDarkMode, setDarkMode] = useLocalStorage('dark-mode', false) - - return {children} -} diff --git a/frontend/src/providers/modules-provider.tsx b/frontend/src/providers/modules-provider.tsx deleted file mode 100644 index a1f8e9887..000000000 --- a/frontend/src/providers/modules-provider.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Code, ConnectError } from '@connectrpc/connect' -import { type PropsWithChildren, createContext, useEffect, useState } from 'react' -import { useClient } from '../hooks/use-client' -import { useSchema } from '../api/schema/use-schema' -import { useVisibility } from '../hooks/use-visibility' -import { ConsoleService } from '../protos/xyz/block/ftl/v1/console/console_connect' -import { GetModulesResponse } from '../protos/xyz/block/ftl/v1/console/console_pb' - -export const modulesContext = createContext(new GetModulesResponse()) - -export const ModulesProvider = ({ children }: PropsWithChildren) => { - const schema = useSchema() - const client = useClient(ConsoleService) - const [modules, setModules] = useState(new GetModulesResponse()) - const isVisible = useVisibility() - - useEffect(() => { - const abortController = new AbortController() - - const fetchModules = async () => { - if (!isVisible) { - abortController.abort() - return - } - - try { - const modules = await client.getModules({}, { signal: abortController.signal }) - setModules(modules ?? []) - } catch (error) { - if (error instanceof ConnectError) { - if (error.code !== Code.Canceled) { - console.error('ModulesProvider - Connect error:', error) - } - } else { - console.error('ModulesProvider:', error) - } - } - - return - } - - fetchModules() - return () => { - abortController.abort() - } - }, [client, schema, isVisible]) - - return {children} -} diff --git a/frontend/src/providers/react-query-provider.tsx b/frontend/src/providers/react-query-provider.tsx index 6a2b5e07c..1b3c98408 100644 --- a/frontend/src/providers/react-query-provider.tsx +++ b/frontend/src/providers/react-query-provider.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import { PropsWithChildren } from 'react' +import type { PropsWithChildren } from 'react' const queryClient = new QueryClient() diff --git a/frontend/src/providers/routing-provider.tsx b/frontend/src/providers/routing-provider.tsx index 6e2c2a2fa..b73019ddb 100644 --- a/frontend/src/providers/routing-provider.tsx +++ b/frontend/src/providers/routing-provider.tsx @@ -1,10 +1,10 @@ -import { createBrowserRouter, createRoutesFromElements, Navigate, Route, RouterProvider } from 'react-router-dom' -import { Layout } from '../layout' -import { TimelinePage } from '../features/timeline/TimelinePage' -import { DeploymentsPage } from '../features/deployments/DeploymentsPage' +import { Navigate, Route, RouterProvider, createBrowserRouter, createRoutesFromElements } from 'react-router-dom' +import { ConsolePage } from '../features/console/ConsolePage' import { DeploymentPage } from '../features/deployments/DeploymentPage' +import { DeploymentsPage } from '../features/deployments/DeploymentsPage' +import { TimelinePage } from '../features/timeline/TimelinePage' import { VerbPage } from '../features/verbs/VerbPage' -import { ConsolePage } from '../features/console/ConsolePage' +import { Layout } from '../layout' import { NotFoundPage } from '../layout/NotFoundPage' const router = createBrowserRouter( @@ -20,8 +20,8 @@ const router = createBrowserRouter( } /> } /> - - ) + , + ), ) export const RoutingProvider = () => { diff --git a/frontend/src/providers/settings-provider.tsx b/frontend/src/providers/settings-provider.tsx deleted file mode 100644 index a681c306c..000000000 --- a/frontend/src/providers/settings-provider.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import useLocalStorage from '../hooks/use-local-storage' -import { createContext, PropsWithChildren, useContext, useEffect } from 'react' - -interface SettingsContextProps { - isDarkMode: boolean; - setDarkMode: (value: boolean) => void; - language: string; - setLanguage: (value: string) => void; - // Add more settings as needed -} - -const SettingsContext = createContext(undefined) - -export const useSettings = () => { - const context = useContext(SettingsContext) - if (!context) { - throw new Error('useSettings must be used within a SettingsProvider') - } - return context -} - -export const SettingsProvider = ({ children }: PropsWithChildren) => { - const [isDarkMode, setDarkMode] = useLocalStorage('dark-mode', false) - const [language, setLanguage] = useLocalStorage('language', 'en') // Example of another setting - - // You can use useEffect to sync settings with other parts of the app if necessary - useEffect(() => { - document.documentElement.classList.toggle('dark', isDarkMode) - }, [isDarkMode]) - - return ( - - {children} - - ) -} diff --git a/frontend/src/providers/user-preferences-provider.tsx b/frontend/src/providers/user-preferences-provider.tsx new file mode 100644 index 000000000..b98d219ec --- /dev/null +++ b/frontend/src/providers/user-preferences-provider.tsx @@ -0,0 +1,31 @@ +import { type PropsWithChildren, createContext, useContext, useEffect } from 'react' +import useLocalStorage from '../hooks/use-local-storage' + +interface UserPreferencesContextProps { + isDarkMode: boolean + setDarkMode: (value: boolean) => void +} + +const UserPreferencesContext = createContext(undefined) + +export const useUserPreferences = () => { + const context = useContext(UserPreferencesContext) + if (!context) { + throw new Error('useSettings must be used within a UserPreferencesProvider') + } + return context +} + +export const UserPreferencesProvider = ({ children }: PropsWithChildren) => { + const [isDarkMode, setDarkMode] = useLocalStorage('dark-mode', false) + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } + }, [isDarkMode]) + + return {children} +}