diff --git a/package-lock.json b/package-lock.json index e5cdd8b27c..c086bf1c6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,9 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^14.6.1", + "@types/d3-cloud": "^1.2.9", "@types/d3-force": "^3.0.10", + "@types/d3-hierarchy": "^3.1.7", "@types/node": "^24.10.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -223,7 +225,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1853,7 +1854,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1877,7 +1877,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3279,6 +3278,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -3305,7 +3305,6 @@ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.17.8.tgz", "integrity": "sha512-42sfdLZSCpsCYmLCjSuntuPcDg3PLbakSmmYfz5Auea8gZYLr+8SS5k647doVu0BRAecqYOytkX2QC5/u/8VHw==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/react": "^0.26.28", "clsx": "^2.1.1", @@ -3341,7 +3340,6 @@ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.17.8.tgz", "integrity": "sha512-96qygbkTjRhdkzd5HDU8fMziemN/h758/EwrFu7TlWrEP10Vw076u+Ap/sG6OT4RGPZYYoHrTlT+mkCZblWHuw==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "^18.x || ^19.x" } @@ -4375,7 +4373,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4429,12 +4428,29 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "3.5.53", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.53.tgz", + "integrity": "sha512-8yKQA9cAS6+wGsJpBysmnhlaaxlN42Qizqkw+h2nILSlS+MAG2z4JdO6p+PJrJ+ACvimkmLJL281h157e52psQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", "license": "MIT" }, + "node_modules/@types/d3-cloud": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@types/d3-cloud/-/d3-cloud-1.2.9.tgz", + "integrity": "sha512-5EWJvnlCrqTThGp8lYHx+DL00sOjx2HTlXH1WRe93k5pfOIhPQaL63NttaKYIbT7bTXp/USiunjNS/N4ipttIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3": "^3" + } + }, "node_modules/@types/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", @@ -4469,6 +4485,13 @@ "@types/geojson": "*" } }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4558,7 +4581,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4578,7 +4600,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4590,7 +4611,6 @@ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4671,7 +4691,6 @@ "integrity": "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", @@ -5276,7 +5295,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5338,6 +5356,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -5672,7 +5691,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -5708,7 +5726,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -6319,8 +6338,7 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -6431,7 +6449,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -6745,7 +6764,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6806,7 +6824,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7785,7 +7802,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -8562,7 +8578,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8796,6 +8811,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -9526,7 +9542,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9555,6 +9570,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9569,6 +9585,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -9580,7 +9597,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/pretty-ms": { "version": "9.3.0", @@ -9647,7 +9665,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9659,7 +9676,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9732,7 +9748,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9961,8 +9976,7 @@ "node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "peer": true + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, "node_modules/redux-logger": { "version": "3.0.6", @@ -10155,7 +10169,6 @@ "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10502,6 +10515,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10512,6 +10526,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10903,7 +10918,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/tiny-case": { "version": "1.0.3", @@ -10973,7 +10989,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11192,7 +11207,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11472,7 +11486,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11612,7 +11625,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11655,7 +11667,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", diff --git a/package.json b/package.json index 2ab971606e..d47276a054 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,9 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^14.6.1", + "@types/d3-cloud": "^1.2.9", "@types/d3-force": "^3.0.10", + "@types/d3-hierarchy": "^3.1.7", "@types/node": "^24.10.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index ec177c1584..6a55a7c394 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -39,7 +39,7 @@ "{{name}} downloaded successfully.": "{{name}} erfolgreich heruntergeladen.", "{{name}} restored successfully.": "{{name}} erfolgreich wiederhergestellt.", "{{name}} Start: {{date}}": "{{name}} Start: {{date}}", - "{{name}} Start: {{startdate}} End: {{enddate}}": "{{name}} Start: {{startdate}} Ende: {{enddate}}", + "{{name}} Start: {{startDate}} End: {{endDate}}": "{{name}} Start: {{startDate}} Ende: {{endDate}}", "{{nr}} more times": "{{nr}} weitere Male", "{{nth}} {{weekday}} every {{interval}} months": "{{nth}} {{weekday}} alle {{interval}} Monate", "{{nth}} {{weekday}} every month": "{{nth}} {{weekday}} jeden Monat", diff --git a/public/locales/gsa-en.json b/public/locales/gsa-en.json index 9c7c259cd7..446312a619 100644 --- a/public/locales/gsa-en.json +++ b/public/locales/gsa-en.json @@ -39,7 +39,7 @@ "{{name}} downloaded successfully.": "{{name}} downloaded successfully.", "{{name}} restored successfully.": "{{name}} restored successfully.", "{{name}} Start: {{date}}": "{{name}} Start: {{date}}", - "{{name}} Start: {{startdate}} End: {{enddate}}": "{{name}} Start: {{startdate}} End: {{enddate}}", + "{{name}} Start: {{startDate}} End: {{endDate}}": "{{name}} Start: {{startDate}} End: {{endDate}}", "{{nr}} more times": "{{nr}} more times", "{{nth}} {{weekday}} every {{interval}} months": "{{nth}} {{weekday}} every {{interval}} months", "{{nth}} {{weekday}} every month": "{{nth}} {{weekday}} every month", diff --git a/public/locales/gsa-zh_CN.json b/public/locales/gsa-zh_CN.json index 18353cbef4..f9076bb9ae 100644 --- a/public/locales/gsa-zh_CN.json +++ b/public/locales/gsa-zh_CN.json @@ -39,7 +39,7 @@ "{{name}} downloaded successfully.": "", "{{name}} restored successfully.": "", "{{name}} Start: {{date}}": "{{name}} 开始: {{date}}", - "{{name}} Start: {{startdate}} End: {{enddate}}": "{{name}} 开始: {{startdate}} 结束: {{enddate}}", + "{{name}} Start: {{startDate}} End: {{endDate}}": "{{name}} 开始: {{startDate}} 结束: {{endDate}}", "{{nr}} more times": "{{nr}} more times", "{{nth}} {{weekday}} every {{interval}} months": "{{nth}} {{weekday}} 每 {{interval}} 月", "{{nth}} {{weekday}} every month": "{{nth}} {{weekday}} 每月", diff --git a/public/locales/gsa-zh_TW.json b/public/locales/gsa-zh_TW.json index a1acb36e29..c01416a604 100644 --- a/public/locales/gsa-zh_TW.json +++ b/public/locales/gsa-zh_TW.json @@ -39,7 +39,7 @@ "{{name}} downloaded successfully.": "", "{{name}} restored successfully.": "", "{{name}} Start: {{date}}": "", - "{{name}} Start: {{startdate}} End: {{enddate}}": "", + "{{name}} Start: {{startDate}} End: {{endDate}}": "", "{{nr}} more times": "", "{{nth}} {{weekday}} every {{interval}} months": "{{nth}} {{weekday}} 每 {{interval}} 月", "{{nth}} {{weekday}} every month": "{{nth}} {{weekday}} 每月", diff --git a/src/gmp/locale/index.ts b/src/gmp/locale/index.ts index 5230fab940..afca666a19 100644 --- a/src/gmp/locale/index.ts +++ b/src/gmp/locale/index.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import {_} from 'gmp/locale/lang'; +import {_, type TranslateFunc} from 'gmp/locale/lang'; + +export type {TranslateFunc}; export default _; diff --git a/src/gmp/locale/lang.ts b/src/gmp/locale/lang.ts index 704f339088..dcfafce09b 100644 --- a/src/gmp/locale/lang.ts +++ b/src/gmp/locale/lang.ts @@ -26,6 +26,8 @@ interface InitLocaleOptions { options?: InitOptions; } +export type TranslateFunc = (key: string, options?: TranslateOptions) => string; + const log = logger.getLogger('gmp.locale.lang'); declare module 'i18next' { diff --git a/src/gmp/types.ts b/src/gmp/types.ts new file mode 100644 index 0000000000..da8ea65e28 --- /dev/null +++ b/src/gmp/types.ts @@ -0,0 +1,8 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export interface ToString { + toString(): string; +} diff --git a/src/web/components/chart/Bar.jsx b/src/web/components/chart/Bar.tsx similarity index 78% rename from src/web/components/chart/Bar.jsx rename to src/web/components/chart/Bar.tsx index 2a4d5c245b..e94f97c486 100644 --- a/src/web/components/chart/Bar.jsx +++ b/src/web/components/chart/Bar.tsx @@ -7,15 +7,39 @@ import React from 'react'; import {scaleBand, scaleLinear} from 'd3-scale'; import styled from 'styled-components'; import {isDefined} from 'gmp/utils/identity'; -import Axis from 'web/components/chart/Axis'; -import Group from 'web/components/chart/Group'; -import Legend from 'web/components/chart/Legend'; -import Svg from 'web/components/chart/Svg'; -import ToolTip from 'web/components/chart/Tooltip'; +import Axis from 'web/components/chart/base/Axis'; +import Group from 'web/components/chart/base/Group'; +import Legend, { + type LegendData, + type LegendRef, +} from 'web/components/chart/base/Legend'; +import Svg from 'web/components/chart/base/Svg'; +import ToolTip from 'web/components/chart/base/Tooltip'; import {MENU_PLACEHOLDER_WIDTH} from 'web/components/chart/utils/Constants'; import {shouldUpdate} from 'web/components/chart/utils/Update'; import Layout from 'web/components/layout/Layout'; -import PropTypes from 'web/utils/PropTypes'; + +interface BarChartDataPoint extends LegendData { + x: number; + y: number; +} + +interface BarChartProps { + width: number; + height: number; + showLegend?: boolean; + horizontal?: boolean; + xLabel?: string; + yLabel?: string; + svgRef?: React.Ref; + data: BarChartDataPoint[]; + onDataClick?: (dataPoint: BarChartDataPoint) => void; + onLegendItemClick?: (dataPoint: BarChartDataPoint) => void; +} + +interface BarChartState { + width: number; +} const StyledLayout = styled(Layout)` overflow: hidden; @@ -35,8 +59,8 @@ const LABEL_HEIGHT = 20; const MIN_WIDTH = 250; const MIN_TICK_WIDTH = 20; -const tickFormat = val => { - const valStr = val.toString(); +const tickFormat = (val: number | string) => { + const valStr = String(val); if (valStr.length > MAX_LABEL_LENGTH) { // prevent cycling through the string return '...' + valStr.slice(valStr.length - MAX_LABEL_LENGTH); @@ -44,9 +68,11 @@ const tickFormat = val => { return valStr; }; -class BarChart extends React.Component { - constructor(...args) { - super(...args); +class BarChart extends React.Component { + legendRef: LegendRef; + + constructor(props: BarChartProps) { + super(props); this.legendRef = React.createRef(); @@ -55,7 +81,7 @@ class BarChart extends React.Component { }; } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: BarChartProps, nextState: BarChartState) { return ( shouldUpdate(nextProps, this.props) || nextState.width !== this.state.width @@ -132,7 +158,7 @@ class BarChart extends React.Component { maxHeight = maxHeight - LABEL_HEIGHT; } - const xScale = scaleBand() + const xScale = scaleBand() .rangeRound(horizontal ? [maxHeight, 0] : [0, maxWidth]) .domain(xValues) .padding(0.125); @@ -173,8 +199,8 @@ class BarChart extends React.Component { tickValues={tickValues} top={maxHeight} /> - {data.map((d, i) => ( - + {data.map(d => ( + {({targetRef, hide, show}) => ( } + fill={String(d.color)} height={ horizontal ? xScale.bandwidth() @@ -202,9 +228,9 @@ class BarChart extends React.Component { {showLegend && data.length > 0 && ( - data={data} + legendRef={this.legendRef} onItemClick={onLegendItemClick} /> )} @@ -213,36 +239,4 @@ class BarChart extends React.Component { } } -BarChart.propTypes = { - /* - Required array structure for data: - - [{ - x: ..., - y: ..., - toolTip: ..., - color: ..., - label: ..., - }] - */ - data: PropTypes.arrayOf( - PropTypes.shape({ - x: PropTypes.toString.isRequired, - y: PropTypes.number.isRequired, - label: PropTypes.any, - color: PropTypes.toString.isRequired, - toolTip: PropTypes.elementOrString, - }), - ).isRequired, - height: PropTypes.number.isRequired, - horizontal: PropTypes.bool, - showLegend: PropTypes.bool, - svgRef: PropTypes.ref, - width: PropTypes.number.isRequired, - xLabel: PropTypes.toString, - yLabel: PropTypes.toString, - onDataClick: PropTypes.func, - onLegendItemClick: PropTypes.func, -}; - export default BarChart; diff --git a/src/web/components/chart/Bubble.jsx b/src/web/components/chart/Bubble.tsx similarity index 67% rename from src/web/components/chart/Bubble.jsx rename to src/web/components/chart/Bubble.tsx index 657a0c01e9..65b19eda24 100644 --- a/src/web/components/chart/Bubble.jsx +++ b/src/web/components/chart/Bubble.tsx @@ -3,31 +3,60 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; import {pack, hierarchy} from 'd3-hierarchy'; import {isDefined} from 'gmp/utils/identity'; -import Group from 'web/components/chart/Group'; -import Svg from 'web/components/chart/Svg'; -import ToolTip from 'web/components/chart/Tooltip'; -import PropTypes from 'web/utils/PropTypes'; +import Group from 'web/components/chart/base/Group'; +import {type LegendData} from 'web/components/chart/base/Legend'; +import Svg from 'web/components/chart/base/Svg'; +import ToolTip from 'web/components/chart/base/Tooltip'; import Theme from 'web/utils/Theme'; +interface BubbleChartData extends LegendData { + value: number; +} + +interface BubbleChartProps { + data?: BubbleChartData[]; + width: number; + height: number; + svgRef?: React.Ref; + onDataClick?: (data: BubbleChartData) => void; +} + +interface BubbleChartHierarchyData extends BubbleChartData { + children: BubbleChartData[]; +} + const margin = { top: 5, right: 5, bottom: 5, left: 5, -}; +} as const; -const BubbleChart = ({data = [], width, height, svgRef, onDataClick}) => { +const BubbleChart = ({ + data = [], + width, + height, + svgRef, + onDataClick, +}: BubbleChartProps) => { const maxWidth = width - margin.left - margin.right; const maxHeight = height - margin.top - margin.bottom; const hasBubbles = data.length > 0; - const bubbles = pack().size([maxWidth, maxHeight]).padding(1.5); + const bubbles = pack() + .size([maxWidth, maxHeight]) + .padding(1.5); - const root = hierarchy({children: data}).sum(d => d.value); + const root = hierarchy({ + children: data, + // dummy root node + color: '', + label: '', + value: 0, + }).sum(d => d.value); const nodes = bubbles(root).leaves(); return ( @@ -52,7 +81,7 @@ const BubbleChart = ({data = [], width, height, svgRef, onDataClick}) => { onMouseEnter={show} onMouseLeave={hide} > - + {/* cut of text overflowing the circle */} @@ -60,7 +89,7 @@ const BubbleChart = ({data = [], width, height, svgRef, onDataClick}) => { } clipPath={`url(#${clippathId})`} dominantBaseline="middle" fontSize="10px" @@ -88,19 +117,4 @@ const BubbleChart = ({data = [], width, height, svgRef, onDataClick}) => { ); }; -BubbleChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.number.isRequired, - color: PropTypes.toString.isRequired, - label: PropTypes.toString.isRequired, - toolTip: PropTypes.elementOrString, - }), - ), - height: PropTypes.number.isRequired, - svgRef: PropTypes.ref, - width: PropTypes.number.isRequired, - onDataClick: PropTypes.func, -}; - export default BubbleChart; diff --git a/src/web/components/chart/Donut.jsx b/src/web/components/chart/Donut.tsx similarity index 78% rename from src/web/components/chart/Donut.jsx rename to src/web/components/chart/Donut.tsx index 1a2cf5e6fc..8244715b43 100644 --- a/src/web/components/chart/Donut.jsx +++ b/src/web/components/chart/Donut.tsx @@ -3,10 +3,16 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; -import {color as d3color} from 'd3-color'; +import React, {type Ref} from 'react'; +import {color as d3color, type HSLColor, type RGBColor} from 'd3-color'; import styled from 'styled-components'; -import {isDefined} from 'gmp/utils/identity'; +import {hasValue, isDefined} from 'gmp/utils/identity'; +import Group from 'web/components/chart/base/Group'; +import Legend, { + type LegendData, + type LegendRef, +} from 'web/components/chart/base/Legend'; +import Svg from 'web/components/chart/base/Svg'; import Arc2d from 'web/components/chart/donut/Arc2d'; import Arc3d from 'web/components/chart/donut/Arc3d'; import Labels from 'web/components/chart/donut/Labels'; @@ -16,18 +22,43 @@ import { PieOuterPath, } from 'web/components/chart/donut/Paths'; import Pie from 'web/components/chart/donut/Pie'; -import {DataPropType} from 'web/components/chart/donut/PropTypes'; -import Group from 'web/components/chart/Group'; -import Legend from 'web/components/chart/Legend'; -import Svg from 'web/components/chart/Svg'; import arc from 'web/components/chart/utils/Arc'; import {MENU_PLACEHOLDER_WIDTH} from 'web/components/chart/utils/Constants'; import {shouldUpdate} from 'web/components/chart/utils/Update'; import Layout from 'web/components/layout/Layout'; -import PropTypes from 'web/utils/PropTypes'; import {setRef} from 'web/utils/Render'; import Theme from 'web/utils/Theme'; +interface EmptyDonutProps { + left: number; + top: number; + innerRadiusX: number; + innerRadiusY: number; + outerRadiusX: number; + outerRadiusY: number; + donutHeight: number; +} + +export interface DonutChartData extends LegendData { + value: number; +} + +interface DonutChartProps { + width: number; + height: number; + data?: TData[]; + innerRadius?: number; + svgRef?: React.RefObject; + show3d?: boolean; + showLegend?: boolean; + onDataClick?: (data: TData) => void; + onLegendItemClick?: (item: TData) => void; +} + +interface DonutChartState { + width: number; +} + const LEGEND_MARGIN = 20; const MIN_RATIO = 2.0; const MIN_WIDTH = 200; @@ -44,7 +75,7 @@ const margin = { }; const emptyColor = Theme.lightGray; -const darkEmptyColor = d3color(emptyColor).darker(); +const darkEmptyColor = (d3color(emptyColor) as HSLColor | RGBColor).darker(); const EmptyDonut = ({ left, @@ -54,8 +85,8 @@ const EmptyDonut = ({ outerRadiusX, outerRadiusY, donutHeight, -}) => { - const donutarc = arc() +}: EmptyDonutProps) => { + const donutArc = arc() .innerRadiusX(innerRadiusX) .innerRadiusY(innerRadiusY) .outerRadiusX(outerRadiusX) @@ -68,7 +99,7 @@ const EmptyDonut = ({ innerRadiusX={innerRadiusX} innerRadiusY={innerRadiusY} /> - + extends React.Component, DonutChartState> { + legendRef: LegendRef; + svg: SVGSVGElement | null = null; -class DonutChart extends React.Component { - constructor(...args) { - super(...args); + constructor(props: DonutChartProps) { + super(props); this.legendRef = React.createRef(); @@ -118,9 +144,12 @@ class DonutChart extends React.Component { return width; } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate( + nextProps: DonutChartProps, + nextState: DonutChartState, + ) { return ( - shouldUpdate(nextProps, this.props) || + shouldUpdate>(nextProps, this.props) || nextState.width !== this.state.width || nextProps.show3d !== this.props.show3d ); @@ -160,7 +189,9 @@ class DonutChart extends React.Component { let comparisonY; const SPACING = 15; - const labels = [...this.svg.querySelectorAll('.pie-label')]; + const labels = hasValue(this.svg) + ? [...this.svg.querySelectorAll('.pie-label')] + : []; labels.forEach(label => { target = label; @@ -250,11 +281,11 @@ class DonutChart extends React.Component { innerRadiusY, }; - const Arc = show3d ? Arc3d : Arc2d; + const Arc = show3d ? Arc3d : Arc2d; return ( (this.svg = ref))} + ref={setRef(svgRef as Ref, ref => (this.svg = ref))} height={height} width={width} > @@ -281,7 +312,6 @@ class DonutChart extends React.Component { data={arcData} donutHeight={donutThickness} endAngle={endAngle} - index={index} path={arcPath} startAngle={startAngle} x={x} @@ -291,7 +321,7 @@ class DonutChart extends React.Component { /> )} - centerX={centerX} centerY={centerY} data={data} @@ -308,9 +338,9 @@ class DonutChart extends React.Component { )} {data.length > 0 && showLegend && ( - data={data} + legendRef={this.legendRef} onItemClick={onLegendItemClick} /> )} @@ -319,16 +349,4 @@ class DonutChart extends React.Component { } } -DonutChart.propTypes = { - data: DataPropType, - height: PropTypes.number.isRequired, - innerRadius: PropTypes.number, - show3d: PropTypes.bool, - showLegend: PropTypes.bool, - svgRef: PropTypes.ref, - width: PropTypes.number.isRequired, - onDataClick: PropTypes.func, - onLegendItemClick: PropTypes.func, -}; - export default DonutChart; diff --git a/src/web/components/chart/HostsTopologyChart.tsx b/src/web/components/chart/HostsTopologyChart.tsx index cced7259a6..a7031f06fa 100644 --- a/src/web/components/chart/HostsTopologyChart.tsx +++ b/src/web/components/chart/HostsTopologyChart.tsx @@ -20,7 +20,7 @@ import equal from 'fast-deep-equal'; import styled from 'styled-components'; import {isDefined} from 'gmp/utils/identity'; import {DEFAULT_SEVERITY_RATING, type SeverityRating} from 'gmp/utils/severity'; -import Group from 'web/components/chart/Group'; +import Group from 'web/components/chart/base/Group'; import Layout from 'web/components/layout/Layout'; import {type I18n, type TranslateFunc} from 'web/hooks/useTranslation'; import {setRef} from 'web/utils/Render'; diff --git a/src/web/components/chart/Label.jsx b/src/web/components/chart/Label.jsx deleted file mode 100644 index cf0311551c..0000000000 --- a/src/web/components/chart/Label.jsx +++ /dev/null @@ -1,32 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React from 'react'; -import PropTypes from 'web/utils/PropTypes'; -import Theme from 'web/utils/Theme'; - -const Label = React.forwardRef(({x, y, children, ...props}, ref) => ( - - {children} - -)); - -Label.propTypes = { - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, -}; - -export default Label; diff --git a/src/web/components/chart/Legend.jsx b/src/web/components/chart/Legend.jsx deleted file mode 100644 index abef3162fe..0000000000 --- a/src/web/components/chart/Legend.jsx +++ /dev/null @@ -1,138 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React from 'react'; -import {Line as VxLine} from '@visx/shape'; -import styled from 'styled-components'; -import {isDefined} from 'gmp/utils/identity'; -import ToolTip from 'web/components/chart/Tooltip'; -import PropTypes from 'web/utils/PropTypes'; -import Theme from 'web/utils/Theme'; - -const DEFAULT_SHAPE_SIZE = 15; - -const StyledLegend = styled.div` - padding: 5px 10px; - margin: 10px 5px; - display: flex; - flex-direction: column; - user-select: none; - color: ${Theme.black}; - opacity: 0.75; -`; - -export const Item = styled.div` - display: flex; - flex-direction: row; - align-items: center; - margin: 5px 0; - ${props => - isDefined(props.onClick) - ? { - cursor: 'pointer', - } - : undefined}; -`; - -export const Label = styled.div` - display: flex; - justify-content: start; - align-items: center; - flex-grow: 1; - margin-left: 10px; -`; - -export const Rect = styled.div` - display: flex; - align-items: center; - width: ${DEFAULT_SHAPE_SIZE}px; - height: 10px; - background-color: ${props => props.color}; -`; - -const StyledDiv = styled.div` - height: ${props => props.height}px; - background-color: ${Theme.white}; - padding: 0 2px; -`; - -export const Line = ({ - width = DEFAULT_SHAPE_SIZE + 5, - height = DEFAULT_SHAPE_SIZE, - color, - lineWidth = 1, - dashArray, -}) => { - const y = height / 2; - return ( - - - - - - ); -}; - -Line.propTypes = { - color: PropTypes.toString.isRequired, - dashArray: PropTypes.toString, - height: PropTypes.number, - lineWidth: PropTypes.number, - width: PropTypes.number, -}; - -const Legend = React.forwardRef(({data, children, onItemClick}, ref) => ( - - {data.map((d, i) => ( - - {({targetRef, hide, show}) => - isDefined(children) ? ( - children({ - d, - toolTipProps: { - ref: targetRef, - onMouseEnter: show, - onMouseLeave: hide, - }, - onItemClick, - }) - ) : ( - onItemClick(d) : undefined - } - onMouseEnter={show} - onMouseLeave={hide} - > - - - - ) - } - - ))} - -)); - -Legend.propTypes = { - children: PropTypes.func, - data: PropTypes.arrayOf( - PropTypes.shape({ - color: PropTypes.toString, - label: PropTypes.any, - toolTip: PropTypes.elementOrString, - }), - ).isRequired, - onItemClick: PropTypes.func, -}; - -export default Legend; diff --git a/src/web/components/chart/Schedule.jsx b/src/web/components/chart/Schedule.tsx similarity index 72% rename from src/web/components/chart/Schedule.jsx rename to src/web/components/chart/Schedule.tsx index 5a49dd9694..41a27cc007 100644 --- a/src/web/components/chart/Schedule.jsx +++ b/src/web/components/chart/Schedule.tsx @@ -6,19 +6,57 @@ import React from 'react'; import {LinearGradient} from '@visx/gradient'; import {scaleBand, scaleUtc} from 'd3-scale'; -import date from 'gmp/models/date'; +import {type TranslateFunc} from 'gmp/locale'; +import date, {type Date as GmpDate} from 'gmp/models/date'; import {shorten} from 'gmp/utils/string'; -import Axis from 'web/components/chart/Axis'; -import Group from 'web/components/chart/Group'; -import Svg from 'web/components/chart/Svg'; -import ToolTip from 'web/components/chart/Tooltip'; +import Axis from 'web/components/chart/base/Axis'; +import Group from 'web/components/chart/base/Group'; +import {type LegendData} from 'web/components/chart/base/Legend'; +import Svg from 'web/components/chart/base/Svg'; +import ToolTip from 'web/components/chart/base/Tooltip'; import path from 'web/components/chart/utils/Path'; import {shouldUpdate} from 'web/components/chart/utils/Update'; import Layout from 'web/components/layout/Layout'; -import PropTypes from 'web/utils/PropTypes'; import Theme from 'web/utils/Theme'; import {formattedUserSettingDateTimeWithTimeZone} from 'web/utils/user-setting-time-date-formatters'; -import withTranslation from 'web/utils/withTranslation'; +import withTranslation, { + type WithTranslationComponentProps, +} from 'web/utils/withTranslation'; + +interface FutureRun { + label: string; + futureRun: number; +} + +interface TriangleProps { + x?: number; + y?: number; + height: number; + width?: number; + toolTip?: React.ReactNode; +} + +interface ScheduleData extends LegendData { + starts: GmpDate[]; + isInfinite?: boolean; + duration?: number; + period?: number; +} + +interface ClonedScheduleData extends ScheduleData { + start: GmpDate; +} + +interface ScheduleChartProps extends WithTranslationComponentProps { + data: ScheduleData[]; + height: number; + svgRef?: React.Ref; + width: number; + yAxisLabel?: string; + startDate?: GmpDate; + endDate?: GmpDate; + _: TranslateFunc; +} const ONE_DAY = 60 * 60 * 24; @@ -28,17 +66,17 @@ const margin = { bottom: 40, left: 60, triangle: 10, -}; +} as const; const MAX_LABEL_LENGTH = 25; -const tickFormat = val => { - return shorten(val.toString(), MAX_LABEL_LENGTH); +const tickFormat = (value: string | number) => { + return shorten(String(value), MAX_LABEL_LENGTH); }; const STROKE_GRADIENT_ID = 'green_stroke_gradient'; -const getFutureRunLabel = (runs, _) => { +const getFutureRunLabel = (runs: number, _: TranslateFunc) => { if (runs === Number.POSITIVE_INFINITY) { return _('More runs not shown'); } @@ -48,26 +86,26 @@ const getFutureRunLabel = (runs, _) => { return _('{{num}} more runs not shown', {num: runs}); }; -const cloneSchedule = (d, start, _) => { +const cloneSchedule = (d: ScheduleData, start: GmpDate, _: TranslateFunc) => { const {duration = 0} = d; const toolTip = duration === 0 ? _('{{name}} Start: {{date}}', { name: d.label, - date: formattedUserSettingDateTimeWithTimeZone(start), + date: formattedUserSettingDateTimeWithTimeZone(start) as string, }) - : _('{{name}} Start: {{startdate}} End: {{enddate}}', { + : _('{{name}} Start: {{startDate}} End: {{endDate}}', { name: d.label, - startdate: formattedUserSettingDateTimeWithTimeZone(start), - enddate: formattedUserSettingDateTimeWithTimeZone( + startDate: formattedUserSettingDateTimeWithTimeZone(start) as string, + endDate: formattedUserSettingDateTimeWithTimeZone( start.clone().add(duration, 'seconds'), - ), + ) as string, }); return { ...d, start, toolTip, - }; + } as ClonedScheduleData; }; const StrokeGradient = () => ( @@ -94,7 +132,13 @@ const fillGradientUrl = `url(#${FILL_GRADIENT_ID})`; const TRIANGLE_WIDTH = 20; -const Triangle = ({x = 0, y = 0, height, width = TRIANGLE_WIDTH, toolTip}) => { +const Triangle = ({ + x = 0, + y = 0, + height, + width = TRIANGLE_WIDTH, + toolTip, +}: TriangleProps) => { const d = path() .move(x, y) .line(x, y + height) @@ -104,8 +148,8 @@ const Triangle = ({x = 0, y = 0, height, width = TRIANGLE_WIDTH, toolTip}) => { {({targetRef, hide, show}) => ( } + d={String(d)} fill={Theme.darkGreen} opacity="0.5" stroke={Theme.darkGreen} @@ -117,16 +161,8 @@ const Triangle = ({x = 0, y = 0, height, width = TRIANGLE_WIDTH, toolTip}) => { ); }; -Triangle.propTypes = { - height: PropTypes.number.isRequired, - toolTip: PropTypes.toString, - width: PropTypes.number, - x: PropTypes.number, - y: PropTypes.number, -}; - -class ScheduleChart extends React.Component { - shouldComponentUpdate(nextProps) { +class ScheduleChart extends React.Component { + shouldComponentUpdate(nextProps: ScheduleChartProps) { return shouldUpdate(nextProps, this.props); } @@ -167,8 +203,8 @@ class ScheduleChart extends React.Component { .domain(yValues) .padding(0.125); - const futureRuns = []; - let schedules = []; + const futureRuns: FutureRun[] = []; + let schedules: ClonedScheduleData[] = []; for (const d of data) { const {label, isInfinite = false, starts} = d; @@ -212,7 +248,7 @@ class ScheduleChart extends React.Component { /> - {schedules.map((d, i) => { + {schedules.map(d => { const {duration = 0, period = 0, start, label} = d; const startX = xScale(start); @@ -220,7 +256,7 @@ class ScheduleChart extends React.Component { let end = start.clone(); const hasDuration = duration > 0; if (hasDuration) { - end = end.add(d.duration, 'seconds'); + end = end.add(d.duration as number, 'seconds'); } else if (period > 0) { end = end.add(Math.min(period, ONE_DAY), 'seconds'); } else { @@ -234,10 +270,10 @@ class ScheduleChart extends React.Component { const endX = xScale(end.toDate()); const rwidth = endX - startX; return ( - + {({targetRef, show, hide}) => ( } fill={hasDuration ? Theme.lightGreen : fillGradientUrl} height={bandwidth} stroke={hasDuration ? Theme.darkGreen : strokeGradientUrl} @@ -271,23 +307,4 @@ class ScheduleChart extends React.Component { } } -ScheduleChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - starts: PropTypes.arrayOf(PropTypes.date).isRequired, - label: PropTypes.toString.isRequired, - isInfinite: PropTypes.bool, - duration: PropTypes.number, - period: PropTypes.number, - }), - ).isRequired, - endDate: PropTypes.date, - height: PropTypes.number.isRequired, - startDate: PropTypes.date, - svgRef: PropTypes.ref, - width: PropTypes.number.isRequired, - yAxisLabel: PropTypes.string, - _: PropTypes.func, -}; - -export default withTranslation(ScheduleChart); +export default withTranslation(ScheduleChart); diff --git a/src/web/components/chart/WordCloud.jsx b/src/web/components/chart/WordCloud.tsx similarity index 74% rename from src/web/components/chart/WordCloud.jsx rename to src/web/components/chart/WordCloud.tsx index c1f10b090c..6621ca82cf 100644 --- a/src/web/components/chart/WordCloud.jsx +++ b/src/web/components/chart/WordCloud.tsx @@ -4,32 +4,65 @@ */ import React from 'react'; -import d3cloud from 'd3-cloud'; +import type d3 from 'd3'; +import d3cloud, {type Word as d3Word} from 'd3-cloud'; import {scaleLinear} from 'd3-scale'; import {isDefined} from 'gmp/utils/identity'; -import Group from 'web/components/chart/Group'; -import Svg from 'web/components/chart/Svg'; -import PropTypes from 'web/utils/PropTypes'; +import Group from 'web/components/chart/base/Group'; +import {type LegendData} from 'web/components/chart/base/Legend'; +import Svg from 'web/components/chart/base/Svg'; + +interface Word extends d3Word { + color?: string; + filterValue: string; +} + +type Cloud = d3.layout.Cloud; + +interface WordCloudChartData extends LegendData { + value: number; + filterValue?: string; +} + +interface WordCloudChartProps { + data: WordCloudChartData[]; + width: number; + height: number; + svgRef?: React.RefObject; + onDataClick?: (filterValue: string) => void; +} + +interface WordCloudChartState { + width?: number; + height?: number; + words?: Word[]; + data?: WordCloudChartData[]; +} const margin = { top: 5, right: 5, bottom: 5, left: 5, -}; +} as const; const DEFAULT_MAX_WORDS = 50; const MIN_FONT_SIZE = 8; const MAX_FONT_SIZE = 20; -class WordCloudChart extends React.Component { - constructor(...args) { - super(...args); +class WordCloudChart extends React.Component< + WordCloudChartProps, + WordCloudChartState +> { + private cloud: Cloud; + + constructor(props: WordCloudChartProps) { + super(props); this.state = {}; - this.cloud = d3cloud() - .fontSize(d => d.size) + this.cloud = d3cloud() + .fontSize(d => d.size as number) .rotate(0) .padding(2) .font('Sans') @@ -53,7 +86,7 @@ class WordCloudChart extends React.Component { this.updateSize(); } if (this.state.data !== this.props.data) { - // data has been changed => recalcuate words + // data has been changed => recalculate words this.updateWords(); } if ( @@ -86,7 +119,7 @@ class WordCloudChart extends React.Component { text: word.label, color: word.color, filterValue: word.filterValue, - })); + })) as Word[]; // store to be rendered data in state // this allows to check if we need to update words on next render phase @@ -149,18 +182,4 @@ class WordCloudChart extends React.Component { } } -WordCloudChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.number.isRequired, - color: PropTypes.toString.isRequired, - label: PropTypes.toString.isRequired, - }), - ), - height: PropTypes.number.isRequired, - svgRef: PropTypes.ref, - width: PropTypes.number.isRequired, - onDataClick: PropTypes.func, -}; - export default WordCloudChart; diff --git a/src/web/components/chart/Axis.jsx b/src/web/components/chart/base/Axis.tsx similarity index 68% rename from src/web/components/chart/Axis.jsx rename to src/web/components/chart/base/Axis.tsx index 565ab71697..42e3a26f02 100644 --- a/src/web/components/chart/Axis.jsx +++ b/src/web/components/chart/base/Axis.tsx @@ -3,11 +3,28 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; -import {Axis as VxAxis} from '@visx/axis'; -import PropTypes from 'web/utils/PropTypes'; +import {type AxisScale, Axis as VxAxis} from '@visx/axis'; +import {type TextProps} from '@visx/text/lib/Text'; import Theme from 'web/utils/Theme'; +interface AxisProps { + hideTickLabels?: boolean; + label?: string; + labelOffset?: number; + left?: number; + orientation?: 'bottom' | 'top' | 'left' | 'right'; + numTicks?: number; + rangePadding?: number; + scale: AxisScale; + tickFormat?: (value: number) => string; + tickLabelProps?: () => Partial; + tickValues?: number[]; + tickLength?: number; + top?: number; +} + +type TextAnchor = 'start' | 'middle' | 'end' | 'inherit'; + const FONT_SIZE = 10; const DEFAULT_TICK_LENGTH = 8; @@ -21,26 +38,26 @@ const DEFAULT_TICK_PROPS = { const left = () => ({ dx: -0.25 * FONT_SIZE, dy: 0.25 * FONT_SIZE, - textAnchor: 'end', + textAnchor: 'end' as TextAnchor, ...DEFAULT_TICK_PROPS, }); const right = () => ({ dy: 0.25 * FONT_SIZE, dx: 0.25 * FONT_SIZE, - textAnchor: 'start', + textAnchor: 'start' as TextAnchor, ...DEFAULT_TICK_PROPS, }); const top = () => ({ dy: -0.25 * FONT_SIZE, - textAnchor: 'middle', + textAnchor: 'middle' as TextAnchor, ...DEFAULT_TICK_PROPS, }); const bottom = () => ({ dy: 0.25 * FONT_SIZE, - textAnchor: 'middle', + textAnchor: 'middle' as TextAnchor, ...DEFAULT_TICK_PROPS, }); @@ -61,7 +78,7 @@ const Axis = ({ ? tickLength : -tickLength, ...props -}) => ( +}: AxisProps) => ( ); -Axis.propTypes = { - hideTickLabels: PropTypes.bool, - labelOffset: PropTypes.number, - orientation: PropTypes.oneOf(['bottom', 'top', 'left', 'right']), - rangePadding: PropTypes.number, - tickLabelProps: PropTypes.func, - tickLength: PropTypes.number, -}; - export default Axis; diff --git a/src/web/components/chart/Group.jsx b/src/web/components/chart/base/Group.tsx similarity index 62% rename from src/web/components/chart/Group.jsx rename to src/web/components/chart/base/Group.tsx index 04fbe3da63..901fd9d606 100644 --- a/src/web/components/chart/Group.jsx +++ b/src/web/components/chart/base/Group.tsx @@ -6,9 +6,16 @@ import React from 'react'; import styled from 'styled-components'; import {isDefined} from 'gmp/utils/identity'; -import PropTypes from 'web/utils/PropTypes'; -const StyledGroup = styled.g` +type SVGGroupProps = React.SVGProps; + +interface GroupProps extends SVGGroupProps { + left?: number; + top?: number; + scale?: number; +} + +const StyledGroup = styled.g` ${props => isDefined(props.onClick) ? { @@ -17,17 +24,11 @@ const StyledGroup = styled.g` : undefined}; `; -const Group = ({left = 0, top = 0, scale = 1, ...props}) => ( +const Group = ({left = 0, top = 0, scale = 1, ...props}: GroupProps) => ( ); -Group.propTypes = { - left: PropTypes.number, - scale: PropTypes.number, - top: PropTypes.number, -}; - export default Group; diff --git a/src/web/components/chart/base/Label.tsx b/src/web/components/chart/base/Label.tsx new file mode 100644 index 0000000000..22784cb10b --- /dev/null +++ b/src/web/components/chart/base/Label.tsx @@ -0,0 +1,26 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React, {forwardRef} from 'react'; +import Theme from 'web/utils/Theme'; + +type LabelProps = React.SVGProps; + +const Label = forwardRef( + (props: LabelProps, ref?: React.Ref) => ( + } + className="pie-label" + dy=".33em" + fill={Theme.dialogGray} // to have labels a bit visible on white background + fontSize={Theme.Font.default} + fontWeight="bold" + textAnchor="middle" + {...props} + /> + ), +); + +export default Label; diff --git a/src/web/components/chart/base/LagendLabel.tsx b/src/web/components/chart/base/LagendLabel.tsx new file mode 100644 index 0000000000..1ff20ff1b5 --- /dev/null +++ b/src/web/components/chart/base/LagendLabel.tsx @@ -0,0 +1,16 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import styled from 'styled-components'; + +const LegendLabel = styled.div` + display: flex; + justify-content: start; + align-items: center; + flex-grow: 1; + margin-left: 10px; +`; + +export default LegendLabel; diff --git a/src/web/components/chart/base/Legend.tsx b/src/web/components/chart/base/Legend.tsx new file mode 100644 index 0000000000..a04766cbe5 --- /dev/null +++ b/src/web/components/chart/base/Legend.tsx @@ -0,0 +1,114 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {type RefObject, type ReactNode, type Ref} from 'react'; +import styled from 'styled-components'; +import {type ToString} from 'gmp/types'; +import {isDefined} from 'gmp/utils/identity'; +import LegendLabel from 'web/components/chart/base/LagendLabel'; +import {DEFAULT_SHAPE_SIZE} from 'web/components/chart/base/LegendLine'; +import ToolTip, {type ToolTipRef} from 'web/components/chart/base/Tooltip'; +import Theme from 'web/utils/Theme'; + +interface RectProps { + color: string; +} + +export interface LegendData { + color: ToString; + label: string; + toolTip?: ReactNode; +} + +interface LegendRenderProps { + d: TData; + toolTipProps: { + ref?: ToolTipRef; + onMouseEnter: () => void; + onMouseLeave: () => void; + }; + onItemClick?: (d: TData) => void; +} + +interface LegendProps { + children?: (props: LegendRenderProps) => ReactNode; + data: TData[]; + legendRef?: LegendRef; + onItemClick?: (d: TData) => void; +} + +export type LegendRef = RefObject; + +const StyledLegend = styled.div` + padding: 5px 10px; + margin: 10px 5px; + display: flex; + flex-direction: column; + user-select: none; + color: ${Theme.black}; + opacity: 0.75; +`; + +export const Item = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin: 5px 0; + ${props => + isDefined(props.onClick) + ? { + cursor: 'pointer', + } + : undefined}; +`; + +const Rect = styled.div` + display: flex; + align-items: center; + width: ${DEFAULT_SHAPE_SIZE}px; + height: 10px; + background-color: ${props => props.color}; +`; + +const Legend = ({ + data, + children, + onItemClick, + legendRef, +}: LegendProps) => ( + }> + {data.map(d => ( + + {({targetRef, hide, show}) => + isDefined(children) ? ( + children({ + d, + toolTipProps: { + ref: targetRef, + onMouseEnter: show, + onMouseLeave: hide, + }, + onItemClick, + }) + ) : ( + } + onClick={ + isDefined(onItemClick) ? () => onItemClick(d) : undefined + } + onMouseEnter={show} + onMouseLeave={hide} + > + + {d.label} + + ) + } + + ))} + +); + +export default Legend; diff --git a/src/web/components/chart/base/LegendLine.tsx b/src/web/components/chart/base/LegendLine.tsx new file mode 100644 index 0000000000..50e269be70 --- /dev/null +++ b/src/web/components/chart/base/LegendLine.tsx @@ -0,0 +1,54 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {Line as VxLine} from '@visx/shape'; +import styled from 'styled-components'; +import {type ToString} from 'gmp/types'; +import Theme from 'web/utils/Theme'; + +interface LegendLineProps { + width?: number; + height?: number; + color: ToString; + lineWidth?: number; + dashArray?: string; +} + +interface StyledDivProps { + height?: number; +} + +export const DEFAULT_SHAPE_SIZE = 15; + +const StyledDiv = styled.div` + height: ${props => props.height}px; + background-color: ${Theme.white}; + padding: 0 2px; +`; + +const LegendLine = ({ + width = DEFAULT_SHAPE_SIZE + 5, + height = DEFAULT_SHAPE_SIZE, + color, + lineWidth = 1, + dashArray, +}: LegendLineProps) => { + const y = height / 2; + return ( + + + + + + ); +}; + +export default LegendLine; diff --git a/src/web/components/chart/Line.jsx b/src/web/components/chart/base/Line.tsx similarity index 63% rename from src/web/components/chart/Line.jsx rename to src/web/components/chart/base/Line.tsx index 157268ca63..470d505db7 100644 --- a/src/web/components/chart/Line.jsx +++ b/src/web/components/chart/base/Line.tsx @@ -3,21 +3,24 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; +import React, {type MouseEvent} from 'react'; import {Line, LinePath} from '@visx/shape'; import {scaleLinear, scaleUtc} from 'd3-scale'; import memoize from 'memoize-one'; import styled from 'styled-components'; -import date from 'gmp/models/date'; +import date, {type Date as GmpDate} from 'gmp/models/date'; +import {type ToString} from 'gmp/types'; import {isDefined} from 'gmp/utils/identity'; -import Axis from 'web/components/chart/Axis'; -import Group from 'web/components/chart/Group'; +import Axis from 'web/components/chart/base/Axis'; +import Group from 'web/components/chart/base/Group'; +import LegendLabel from 'web/components/chart/base/LagendLabel'; import Legend, { Item, - Label, - Line as LegendLine, -} from 'web/components/chart/Legend'; -import Svg from 'web/components/chart/Svg'; + type LegendData, + type LegendRef, +} from 'web/components/chart/base/Legend'; +import LegendLine from 'web/components/chart/base/LegendLine'; +import Svg from 'web/components/chart/base/Svg'; import {MENU_PLACEHOLDER_WIDTH} from 'web/components/chart/utils/Constants'; import {shouldUpdate} from 'web/components/chart/utils/Update'; import Layout from 'web/components/layout/Layout'; @@ -25,6 +28,53 @@ import PropTypes from 'web/utils/PropTypes'; import {setRef} from 'web/utils/Render'; import Theme from 'web/utils/Theme'; +interface LineData { + x: number | GmpDate; + y: number; + y2: number; + label?: string; +} + +interface CrossProps { + x: number; + y: number; + color: ToString; + dashArray?: string; + lineWidth?: number; +} + +interface LineProps extends LegendData { + dashArray?: string; + lineWidth?: number; + width?: number; +} + +interface LineChartProps { + data: LineData[]; + height: number; + numTicks?: number; + showLegend?: boolean; + svgRef?: React.Ref; + timeline?: boolean; + width: number; + xAxisLabel?: ToString; + yAxisLabel?: ToString; + y2AxisLabel?: ToString; + yLine?: LineProps; + y2Line?: LineProps; + onRangeSelected?: (start: LineData, end: LineData) => void; +} + +interface LineChartState { + data?: LineData[]; + displayInfo: boolean; + infoX?: number | GmpDate; + mouseX?: number; + mouseY?: number; + rangeX?: number | GmpDate; + width: number; +} + const LEGEND_MARGIN = 20; const margin = { @@ -32,13 +82,23 @@ const margin = { right: 60, bottom: 55, left: 60, -}; +} as const; const MIN_WIDTH = 100 + margin.right + margin.left; const MIN_TICK_WIDTH = 75; -const findX = (timeline, value) => d => - timeline ? d.x.isSame(value) : d.x === value; +export const lineDataPropType = PropTypes.shape({ + color: PropTypes.toString.isRequired, + dashArray: PropTypes.string, + label: PropTypes.any.isRequired, + lineWidth: PropTypes.number, + width: PropTypes.number, +}); + +const findX = + (timeline: boolean, value: number | GmpDate) => + (d: LineData): d is LineData => + timeline ? (d.x as GmpDate).isSame(value) : d.x === value; const CrispEdgesLine = styled(Line)` shape-rendering: crisp-edges; @@ -57,55 +117,57 @@ const LabelTitle = styled.text` font-family: monospace; `; -const xValue = (d, timeline = false) => (timeline ? d.x.toDate() : d.x); +const xValue = (d: LineData) => Number(d.x); -const maxWidth = memoize(width => width - margin.left - margin.right); +const maxWidth = memoize((width: number) => width - margin.left - margin.right); -const maxHeight = memoize(height => height - margin.top - margin.bottom); +const maxHeight = memoize( + (height: number) => height - margin.top - margin.bottom, +); -const getXAxisTicks = memoize((width, numTicks = 10) => { +const getXAxisTicks = memoize((width: number, numTicks: number = 10) => { while (width / numTicks < MIN_TICK_WIDTH) { numTicks--; } return numTicks; }); -const getXValues = memoize((data = [], timeline = false) => - data.map(d => xValue(d, timeline)), -); - -const getXMin = memoize(xValues => Math.min(...xValues)); -const getXMax = memoize(xValues => Math.max(...xValues)); - -const getXScale = memoize((data = [], timeline = false, width) => { - const xValues = getXValues(data, timeline); - - const xMin = getXMin(xValues); - const xMax = getXMax(xValues); - - let xDomain; - if (timeline) { - xDomain = - data.length === 1 - ? [ - date(data[0].x).subtract(1, 'day').toDate(), - date(data[0].x).add(1, 'day').toDate(), - ] - : [date(xMin).toDate(), date(xMax).toDate()]; - } else { - xDomain = data.length > 1 ? [xMin, xMax] : [xMin - 1, xMax + 1]; - } +const getXValues = memoize((data: LineData[] = []) => data.map(d => xValue(d))); + +const getXMin = memoize((xValues: number[]) => Math.min(...xValues)); +const getXMax = memoize((xValues: number[]) => Math.max(...xValues)); + +const getXScale = memoize( + (data: LineData[] = [], timeline: boolean = false, width: number) => { + const xValues = getXValues(data); + + const xMin = getXMin(xValues); + const xMax = getXMax(xValues); + + let xDomain: [number, number]; + if (timeline) { + xDomain = + data.length === 1 + ? [ + Number(date(data[0].x).subtract(1, 'day')), + Number(date(data[0].x).add(1, 'day')), + ] + : [xMin, xMax]; + } else { + xDomain = data.length > 1 ? [xMin, xMax] : [xMin - 1, xMax + 1]; + } - return timeline - ? scaleUtc() - .range([0, maxWidth(width)]) - .domain(xDomain) - : scaleLinear() - .range([0, maxWidth(width)]) - .domain(xDomain); -}); + return timeline + ? scaleUtc() + .range([0, maxWidth(width)]) + .domain(xDomain) + : scaleLinear() + .range([0, maxWidth(width)]) + .domain(xDomain); + }, +); -const getYScale = memoize((data = [], height) => { +const getYScale = memoize((data: LineData[] = [], height: number) => { const yValues = data.map(d => d.y); const yMax = Math.max(...yValues); const yDomain = data.length > 1 ? [0, yMax] : [0, yMax * 2]; @@ -115,7 +177,7 @@ const getYScale = memoize((data = [], height) => { .nice(); }); -const getY2Scale = memoize((data = [], height) => { +const getY2Scale = memoize((data: LineData[] = [], height: number) => { const y2Values = data.map(d => d.y2); const y2Max = Math.max(...y2Values); @@ -126,34 +188,18 @@ const getY2Scale = memoize((data = [], height) => { .nice(); }); -export const lineDataPropType = PropTypes.shape({ - color: PropTypes.toString.isRequired, - dashArray: PropTypes.string, - label: PropTypes.any.isRequired, - lineWidth: PropTypes.number, - width: PropTypes.number, -}); - -const crossPropTypes = { - color: PropTypes.toString.isRequired, - dashArray: PropTypes.toString, - lineWidth: PropTypes.number, - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, -}; - -const Cross = ({x, y, color, dashArray, lineWidth = 1}) => ( +const Cross = ({x, y, color, dashArray, lineWidth = 1}: CrossProps) => ( ( ); -Cross.propTypes = crossPropTypes; - -const CrossY2 = ({x, y, color, lineWidth = 1}) => ( +const CrossY2 = ({x, y, color, dashArray, lineWidth = 1}: CrossProps) => ( ); -CrossY2.propTypes = crossPropTypes; +class LineChart extends React.Component { + legendRef: LegendRef; + svg: SVGSVGElement | null = null; -class LineChart extends React.Component { - constructor(...args) { - super(...args); + constructor(props: LineChartProps) { + super(props); this.legendRef = React.createRef(); @@ -212,7 +257,7 @@ class LineChart extends React.Component { this.update(); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: LineChartProps, nextState: LineChartState) { return ( shouldUpdate(nextProps, this.props) || nextState.width !== this.state.width || @@ -233,7 +278,7 @@ class LineChart extends React.Component { this.setState({displayInfo: true}); } - handleMouseMove(event) { + handleMouseMove(event: MouseEvent) { if (!this.svg) { return; } @@ -248,7 +293,11 @@ class LineChart extends React.Component { }); } - startRangeSelection(event) { + startRangeSelection(event: MouseEvent) { + if (!this.svg) { + return; + } + const box = this.svg.getBoundingClientRect(); const mouseX = event.clientX - box.left - margin.left - 1; @@ -259,10 +308,12 @@ class LineChart extends React.Component { const {rangeX, infoX} = this.state; const {onRangeSelected, timeline = false, data} = this.props; - if (onRangeSelected) { + if (onRangeSelected && isDefined(rangeX) && isDefined(infoX)) { const direction = infoX >= rangeX; - const start = {...data.find(findX(timeline, rangeX))}; - const end = {...data.find(findX(timeline, infoX))}; + const start = { + ...data.find(findX(timeline, rangeX)), + } as LineData; + const end = {...data.find(findX(timeline, infoX))} as LineData; if (direction) { onRangeSelected(start, end); @@ -274,11 +325,11 @@ class LineChart extends React.Component { this.setState({rangeX: undefined}); } - getXValueForPixel(px) { + getXValueForPixel(px: number) { const {data = [], timeline = false} = this.props; const {width} = this.state; - const xValues = getXValues(data, timeline); + const xValues = getXValues(data); if (xValues.length === 1) { return xValues[0]; @@ -295,7 +346,7 @@ class LineChart extends React.Component { const values = [...xValues].sort((a, b) => a - b); // sort copy of x values const xScale = getXScale(data, timeline, width); - const xV = xScale.invert(px); // x value for pixel position + const xV = Number(xScale.invert(px)); // x value for pixel position const index = values.findIndex(x => xV <= x); // get index of the first x value bigger then xV @@ -331,7 +382,7 @@ class LineChart extends React.Component { } renderInfo() { - const {data, height, timeline, yLine, y2Line} = this.props; + const {data, height, timeline = false, yLine, y2Line} = this.props; const {displayInfo, infoX, mouseY, width} = this.state; const lines = (isDefined(yLine) ? 1 : 0) + (isDefined(y2Line) ? 1 : 0); @@ -379,30 +430,34 @@ class LineChart extends React.Component { {label} - - - - {y} - - - - - - {y2} - - + {isDefined(yLine) && ( + + + + {y} + + + )} + {isDefined(y2Line) && ( + + + + {y2} + + + )} @@ -481,7 +536,7 @@ class LineChart extends React.Component { {isDefined(yLine) && ( )} - {y2Line && ( + {isDefined(y2Line) && ( - xScale(xValue(d, timeline))} - y={d => yScale(d.y)} - /> - xScale(xValue(d, timeline))} - y={d => y2Scale(d.y2)} - /> + {isDefined(yLine) && ( + xScale(xValue(d))} + y={d => yScale(d.y)} + /> + )} + {isDefined(y2Line) && ( + xScale(xValue(d))} + y={d => y2Scale(d.y2)} + /> + )} )} {hasOneValue && ( @@ -535,7 +596,7 @@ class LineChart extends React.Component { color={yLine.color} dashArray={yLine.dashArray} lineWidth={yLine.lineWidth} - x={xScale(xValue(data[0], timeline))} + x={xScale(xValue(data[0]))} y={yScale(data[0].y)} /> )} @@ -544,7 +605,7 @@ class LineChart extends React.Component { color={y2Line.color} dashArray={y2Line.dashArray} lineWidth={y2Line.lineWidth} - x={xScale(xValue(data[0], timeline))} + x={xScale(xValue(data[0]))} y={y2Scale(data[0].y2)} /> )} @@ -555,17 +616,20 @@ class LineChart extends React.Component { {hasLines && showLegend && ( - + data={[yLine, y2Line]} legendRef={this.legendRef}> {({d, toolTipProps}) => ( - + } + > - + )} @@ -575,26 +639,4 @@ class LineChart extends React.Component { } } -LineChart.propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - x: PropTypes.oneOfType([PropTypes.number, PropTypes.date]).isRequired, - y: PropTypes.number.isRequired, - y2: PropTypes.number.isRequired, - }), - ), - height: PropTypes.number.isRequired, - numTicks: PropTypes.number, - showLegend: PropTypes.bool, - svgRef: PropTypes.ref, - timeline: PropTypes.bool, - width: PropTypes.number.isRequired, - xAxisLabel: PropTypes.toString, - y2AxisLabel: PropTypes.toString, - y2Line: lineDataPropType, - yAxisLabel: PropTypes.toString, - yLine: lineDataPropType, - onRangeSelected: PropTypes.func, -}; - export default LineChart; diff --git a/src/web/components/chart/Svg.jsx b/src/web/components/chart/base/Svg.tsx similarity index 100% rename from src/web/components/chart/Svg.jsx rename to src/web/components/chart/base/Svg.tsx diff --git a/src/web/components/chart/Tooltip.jsx b/src/web/components/chart/base/Tooltip.tsx similarity index 69% rename from src/web/components/chart/Tooltip.jsx rename to src/web/components/chart/base/Tooltip.tsx index 6ccadccbd2..7d966f7691 100644 --- a/src/web/components/chart/Tooltip.jsx +++ b/src/web/components/chart/base/Tooltip.tsx @@ -7,9 +7,27 @@ import React from 'react'; import styled from 'styled-components'; import {hasValue} from 'gmp/utils/identity'; import Portal from 'web/components/portal/Portal'; -import PropTypes from 'web/utils/PropTypes'; import Theme from 'web/utils/Theme'; +type ToolTipDisplayProps = React.HTMLAttributes; +type ToolTipTargetElement = HTMLElement | SVGElement; +export type ToolTipRef = React.Ref; + +interface ToolTipRenderProps { + show: () => void; + hide: () => void; + targetRef?: ToolTipRef; +} + +interface ToolTipProps { + content?: React.ReactNode; + children: (args: ToolTipRenderProps) => React.ReactNode; +} + +interface ToolTipState { + visible: boolean; +} + const ToolTipText = styled.div` box-sizing: border-box; font-weight: bold; @@ -43,21 +61,21 @@ const ToolTipContainer = styled.div` ToolTipContainer.displayName = 'ToolTipContainer'; -const ToolTipDisplay = React.forwardRef(({children, ...props}, ref) => ( - - {children} - - -)); +const ToolTipDisplay = React.forwardRef( + ({children, ...props}: ToolTipDisplayProps, ref: React.Ref) => ( + } {...props}> + {children} + + + ), +); -class ToolTip extends React.Component { - static propTypes = { - children: PropTypes.func.isRequired, - content: PropTypes.elementOrString, - }; +class ToolTip extends React.Component { + target: React.RefObject; + tooltip: React.RefObject; - constructor(...args) { - super(...args); + constructor(props: ToolTipProps) { + super(props); this.state = { visible: false, @@ -106,7 +124,7 @@ class ToolTip extends React.Component { const {children, content} = this.props; const {visible} = this.state; return ( - + <> {content && visible && ( {content} @@ -117,7 +135,7 @@ class ToolTip extends React.Component { hide: this.hide, targetRef: this.target, })} - + ); } } diff --git a/src/web/components/chart/donut/Arc2d.jsx b/src/web/components/chart/donut/Arc2d.tsx similarity index 53% rename from src/web/components/chart/donut/Arc2d.jsx rename to src/web/components/chart/donut/Arc2d.tsx index bb0f56d74a..2368fcff4e 100644 --- a/src/web/components/chart/donut/Arc2d.jsx +++ b/src/web/components/chart/donut/Arc2d.tsx @@ -3,15 +3,31 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; import {isDefined} from 'gmp/utils/identity'; -import {ArcDataPropType} from 'web/components/chart/donut/PropTypes'; -import Group from 'web/components/chart/Group'; -import ToolTip from 'web/components/chart/Tooltip'; -import PropTypes from 'web/utils/PropTypes'; +import Group from 'web/components/chart/base/Group'; +import {type LegendData} from 'web/components/chart/base/Legend'; +import ToolTip from 'web/components/chart/base/Tooltip'; import Theme from 'web/utils/Theme'; -const Arc2d = ({data, path, x, y, onDataClick}) => { +interface Arc2dData extends LegendData { + value: number; +} + +interface Arc2dProps { + data: TData; + path: string; + x: number; + y: number; + onDataClick?: (data: TData) => void; +} + +const Arc2d = ({ + data, + path, + x, + y, + onDataClick, +}: Arc2dProps) => { const {color = Theme.lightGray, toolTip} = data; return ( @@ -21,9 +37,9 @@ const Arc2d = ({data, path, x, y, onDataClick}) => { onMouseEnter={show} onMouseLeave={hide} > - + } cx={x} cy={y} r="1" @@ -35,12 +51,4 @@ const Arc2d = ({data, path, x, y, onDataClick}) => { ); }; -Arc2d.propTypes = { - data: ArcDataPropType.isRequired, - path: PropTypes.toString.isRequired, - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - onDataClick: PropTypes.func, -}; - export default Arc2d; diff --git a/src/web/components/chart/donut/Arc3d.jsx b/src/web/components/chart/donut/Arc3d.tsx similarity index 62% rename from src/web/components/chart/donut/Arc3d.jsx rename to src/web/components/chart/donut/Arc3d.tsx index 0d62b524b5..a763c7feb7 100644 --- a/src/web/components/chart/donut/Arc3d.jsx +++ b/src/web/components/chart/donut/Arc3d.tsx @@ -3,21 +3,38 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; -import {color as d3color} from 'd3-color'; +import {color as d3color, type HSLColor, type RGBColor} from 'd3-color'; import {isDefined} from 'gmp/utils/identity'; +import Group from 'web/components/chart/base/Group'; +import {type LegendData} from 'web/components/chart/base/Legend'; +import ToolTip from 'web/components/chart/base/Tooltip'; import { PieOuterPath, PieTopPath, PieInnerPath, } from 'web/components/chart/donut/Paths'; -import {ArcDataPropType} from 'web/components/chart/donut/PropTypes'; -import Group from 'web/components/chart/Group'; -import ToolTip from 'web/components/chart/Tooltip'; -import PropTypes from 'web/utils/PropTypes'; import Theme from 'web/utils/Theme'; -const Arc3d = ({ +interface Arc3dData extends LegendData { + value: number; +} + +interface Arc3dProps { + data: TData; + innerRadiusX: number; + innerRadiusY: number; + outerRadiusX: number; + outerRadiusY: number; + donutHeight: number; + path: string; + startAngle: number; + endAngle: number; + x: number; + y: number; + onDataClick?: (data: TData) => void; +} + +const Arc3d = ({ data, innerRadiusX, innerRadiusY, @@ -30,9 +47,13 @@ const Arc3d = ({ x, y, onDataClick, -}) => { +}: Arc3dProps) => { const {color = Theme.lightGray, toolTip} = data; - const darker = d3color(color).darker(); + let d3Color = d3color(String(color)); + if (!d3Color) { + d3Color = d3color(Theme.lightGray); + } + const darker = (d3Color as HSLColor | RGBColor).darker(); return ( {({targetRef, hide, show}) => ( @@ -59,7 +80,7 @@ const Arc3d = ({ startAngle={startAngle} /> } cx={x} cy={y} r="1" @@ -71,19 +92,4 @@ const Arc3d = ({ ); }; -Arc3d.propTypes = { - data: ArcDataPropType, - donutHeight: PropTypes.number.isRequired, - endAngle: PropTypes.number.isRequired, - innerRadiusX: PropTypes.number.isRequired, - innerRadiusY: PropTypes.number.isRequired, - outerRadiusX: PropTypes.number.isRequired, - outerRadiusY: PropTypes.number.isRequired, - path: PropTypes.toString.isRequired, - startAngle: PropTypes.number.isRequired, - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, - onDataClick: PropTypes.func, -}; - export default Arc3d; diff --git a/src/web/components/chart/donut/Labels.jsx b/src/web/components/chart/donut/Labels.tsx similarity index 66% rename from src/web/components/chart/donut/Labels.jsx rename to src/web/components/chart/donut/Labels.tsx index 08f8a1951a..2a5a5f06f5 100644 --- a/src/web/components/chart/donut/Labels.jsx +++ b/src/web/components/chart/donut/Labels.tsx @@ -3,16 +3,29 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; +import {type ReactNode} from 'react'; +import Label from 'web/components/chart/base/Label'; +import ToolTip from 'web/components/chart/base/Tooltip'; import Pie from 'web/components/chart/donut/Pie'; -import {DataPropType} from 'web/components/chart/donut/PropTypes'; -import Label from 'web/components/chart/Label'; -import ToolTip from 'web/components/chart/Tooltip'; -import PropTypes from 'web/utils/PropTypes'; + +interface LabelData { + value: number; + toolTip?: ReactNode; +} + +interface LabelsProps { + data: TData[]; + centerX: number; + centerY: number; + innerRadiusX?: number; + outerRadiusX: number; + innerRadiusY?: number; + outerRadiusY?: number; +} const MIN_ANGLE_FOR_LABELS = 0.15; -const Labels = ({ +const Labels = ({ data, centerX, centerY, @@ -20,7 +33,7 @@ const Labels = ({ outerRadiusX, innerRadiusY, outerRadiusY, -}) => ( +}: LabelsProps) => ( ( ); -Labels.propTypes = { - centerX: PropTypes.number.isRequired, - centerY: PropTypes.number.isRequired, - data: DataPropType, - innerRadiusX: PropTypes.number, - innerRadiusY: PropTypes.number, - outerRadiusX: PropTypes.number.isRequired, - outerRadiusY: PropTypes.number, -}; - export default Labels; diff --git a/src/web/components/chart/donut/Paths.jsx b/src/web/components/chart/donut/Paths.tsx similarity index 61% rename from src/web/components/chart/donut/Paths.jsx rename to src/web/components/chart/donut/Paths.tsx index 83815693d5..017c33e084 100644 --- a/src/web/components/chart/donut/Paths.jsx +++ b/src/web/components/chart/donut/Paths.tsx @@ -3,22 +3,46 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; +import {forwardRef, type Ref} from 'react'; +import {type ToString} from 'gmp/types'; import path from 'web/components/chart/utils/Path'; -import PropTypes from 'web/utils/PropTypes'; + +interface PieToPathProps { + color: ToString; + path: ToString; +} + +interface PieInnerPathProps { + color: ToString; + donutHeight: number; + endAngle?: number; + innerRadiusX: number; + innerRadiusY: number; + startAngle?: number; +} + +interface PieOuterPathProps { + startAngle?: number; + endAngle?: number; + donutHeight: number; + color: ToString; + outerRadiusX: number; + outerRadiusY: number; +} const PI2 = 2 * Math.PI; -export const PieTopPath = ({color, path}) => ( - +export const PieTopPath = ({color, path}: PieToPathProps) => ( + ); -PieTopPath.propTypes = { - color: PropTypes.toString.isRequired, - path: PropTypes.toString.isRequired, -}; - -const pieInnerPath = (sa, ea, irx, iry, h) => { +const pieInnerPath = ( + sa: number, + ea: number, + irx: number, + iry: number, + h: number, +) => { const startAngle = sa < Math.PI ? Math.PI : sa; const endAngle = ea < Math.PI ? Math.PI : ea; const sx = irx * Math.cos(startAngle); @@ -34,7 +58,7 @@ const pieInnerPath = (sa, ea, irx, iry, h) => { paths.arc(irx, iry, sx, sy + h, {sweep: 0}); paths.close(); - return paths; + return String(paths); }; export const PieInnerPath = ({ @@ -44,7 +68,7 @@ export const PieInnerPath = ({ donutHeight, innerRadiusX, innerRadiusY, -}) => ( +}: PieInnerPathProps) => ( ); -PieInnerPath.propTypes = { - color: PropTypes.toString.isRequired, - donutHeight: PropTypes.number.isRequired, - endAngle: PropTypes.number, - innerRadiusX: PropTypes.number.isRequired, - innerRadiusY: PropTypes.number.isRequired, - startAngle: PropTypes.number, -}; - -const pieOuterPath = (sa, ea, rx, ry, h) => { +const pieOuterPath = ( + sa: number, + ea: number, + rx: number, + ry: number, + h: number, +) => { const startAngle = sa > Math.PI ? Math.PI : sa; const endAngle = ea > Math.PI ? Math.PI : ea; @@ -83,10 +104,10 @@ const pieOuterPath = (sa, ea, rx, ry, h) => { paths.arc(rx, ry, sx, sy, {sweep: 0}); paths.close(); - return paths; + return String(paths); }; -export const PieOuterPath = React.forwardRef( +export const PieOuterPath = forwardRef( ( { startAngle = 0, @@ -95,8 +116,8 @@ export const PieOuterPath = React.forwardRef( color, outerRadiusX, outerRadiusY, - }, - ref, + }: PieOuterPathProps, + ref: Ref, ) => ( ), ); - -PieOuterPath.propTypes = { - color: PropTypes.toString.isRequired, - donutHeight: PropTypes.number.isRequired, - endAngle: PropTypes.number, - outerRadiusX: PropTypes.number.isRequired, - outerRadiusY: PropTypes.number.isRequired, - startAngle: PropTypes.number, -}; diff --git a/src/web/components/chart/donut/Pie.jsx b/src/web/components/chart/donut/Pie.tsx similarity index 97% rename from src/web/components/chart/donut/Pie.jsx rename to src/web/components/chart/donut/Pie.tsx index c4a7181ed7..ab34daa944 100644 --- a/src/web/components/chart/donut/Pie.jsx +++ b/src/web/components/chart/donut/Pie.tsx @@ -6,7 +6,7 @@ import React from 'react'; import {pie as d3pie} from 'd3-shape'; import {isDefined} from 'gmp/utils/identity'; -import Group from 'web/components/chart/Group'; +import Group from 'web/components/chart/base/Group'; import arc from 'web/components/chart/utils/Arc'; import PropTypes from 'web/utils/PropTypes'; diff --git a/src/web/components/chart/donut/PropTypes.jsx b/src/web/components/chart/donut/PropTypes.jsx deleted file mode 100644 index 748a21823f..0000000000 --- a/src/web/components/chart/donut/PropTypes.jsx +++ /dev/null @@ -1,15 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import PropTypes from 'web/utils/PropTypes'; - -export const ArcDataPropType = PropTypes.shape({ - color: PropTypes.toString.isRequired, - value: PropTypes.numberOrNumberString.isRequired, - label: PropTypes.any, - toolTip: PropTypes.elementOrString, -}); - -export const DataPropType = PropTypes.arrayOf(ArcDataPropType); diff --git a/src/web/components/chart/utils/Arc.jsx b/src/web/components/chart/utils/Arc.tsx similarity index 87% rename from src/web/components/chart/utils/Arc.jsx rename to src/web/components/chart/utils/Arc.tsx index 719a1155b4..7215be6c43 100644 --- a/src/web/components/chart/utils/Arc.jsx +++ b/src/web/components/chart/utils/Arc.tsx @@ -10,27 +10,32 @@ const EPSILON = 1e-12; // 1 * 10^(-12) const PI2 = Math.PI * 2; -class Arc { +export class Arc { + _innerRadiusX: number; + _outerRadiusX?: number; + _innerRadiusY?: number; + _outerRadiusY?: number; + constructor() { this._innerRadiusX = 0; } - innerRadiusX(radius) { + innerRadiusX(radius: number) { this._innerRadiusX = radius; return this; } - outerRadiusX(radius) { + outerRadiusX(radius: number) { this._outerRadiusX = radius; return this; } - innerRadiusY(radius) { + innerRadiusY(radius: number) { this._innerRadiusY = radius; return this; } - outerRadiusY(radius) { + outerRadiusY(radius: number) { this._outerRadiusY = radius; return this; } @@ -38,7 +43,7 @@ class Arc { centroid({startAngle = 0, endAngle = PI2} = {}) { this._checkRadius(); - const outerRadiusX = this._outerRadiusX; + const outerRadiusX = this._outerRadiusX as number; const outerRadiusY = isDefined(this._outerRadiusY) ? this._outerRadiusY : outerRadiusX; @@ -75,7 +80,7 @@ class Arc { this._checkRadius(); - const outerRadiusX = this._outerRadiusX; + const outerRadiusX = this._outerRadiusX as number; const outerRadiusY = isDefined(this._outerRadiusY) ? this._outerRadiusY : outerRadiusX; diff --git a/src/web/components/chart/utils/Constants.jsx b/src/web/components/chart/utils/Constants.tsx similarity index 100% rename from src/web/components/chart/utils/Constants.jsx rename to src/web/components/chart/utils/Constants.tsx diff --git a/src/web/components/chart/utils/Path.jsx b/src/web/components/chart/utils/Path.tsx similarity index 62% rename from src/web/components/chart/utils/Path.jsx rename to src/web/components/chart/utils/Path.tsx index e5ddd760c0..fefd8913a6 100644 --- a/src/web/components/chart/utils/Path.jsx +++ b/src/web/components/chart/utils/Path.tsx @@ -3,13 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -class Path { +interface ArcOptions { + largeArc?: number; + sweep?: number; + rotationDegree?: number; +} + +export class Path { + paths: (string | number)[]; + closed: boolean; + constructor() { this.paths = []; this.closed = false; } - push(command, ...paths) { + push(command: string, ...paths: (string | number)[]) { if (!this.closed) { this.paths.push(command, ...paths); } @@ -22,20 +31,20 @@ class Path { return this; } - move(x, y) { + move(x: number, y: number) { return this.push('M', x, y); } - line(x, y) { + line(x: number, y: number) { return this.push('L', x, y); } arc( - radiusX, - radiusY, - x, - y, - {largeArc = 0, sweep = 0, rotationDegree = 0} = {}, + radiusX: number, + radiusY: number, + x: number, + y: number, + {largeArc = 0, sweep = 0, rotationDegree = 0}: ArcOptions = {}, ) { return this.push( 'A', diff --git a/src/web/components/chart/utils/Update.jsx b/src/web/components/chart/utils/Update.tsx similarity index 53% rename from src/web/components/chart/utils/Update.jsx rename to src/web/components/chart/utils/Update.tsx index 2d5bbe35f9..99e645aff6 100644 --- a/src/web/components/chart/utils/Update.jsx +++ b/src/web/components/chart/utils/Update.tsx @@ -3,17 +3,27 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +interface UpdateProps { + data?: TData; + width?: number; + height?: number; + showLegend?: boolean; +} + /** * Default implementation for checking if a chart component must be updated * * A chart must always be re-rendered if data, width or height has changed. * - * @param {Object} nextProps Next props to be set - * @param {Object} props Current set props + * @param nextProps Next props to be set + * @param props Current set props * - * @returns {Boolean} true if the chart component must be re-rendered + * @returns {boolean} true if the chart component must be re-rendered */ -export const shouldUpdate = (nextProps, props) => +export const shouldUpdate = ( + nextProps: TProps, + props: TProps, +): boolean => nextProps.data !== props.data || nextProps.width !== props.width || nextProps.height !== props.height || diff --git a/src/web/components/chart/utils/__tests__/Arc.test.jsx b/src/web/components/chart/utils/__tests__/Arc.test.tsx similarity index 69% rename from src/web/components/chart/utils/__tests__/Arc.test.jsx rename to src/web/components/chart/utils/__tests__/Arc.test.tsx index 4a8e5f7292..dbb8b8e510 100644 --- a/src/web/components/chart/utils/__tests__/Arc.test.jsx +++ b/src/web/components/chart/utils/__tests__/Arc.test.tsx @@ -6,7 +6,7 @@ import {describe, test, expect} from '@gsa/testing'; import arc from 'web/components/chart/utils/Arc'; -describe('arc class tests', () => { +describe('Arc class tests', () => { test('should throw if no outerRadiusX is set', () => { const a = arc(); expect(() => a.centroid()).toThrow(); @@ -28,55 +28,52 @@ describe('arc class tests', () => { expect(path).toMatchSnapshot(); a.innerRadiusX(50); - path = a.path().toString(); - expect(path).toMatchSnapshot(); + expect(String(a.path())).toMatchSnapshot(); a.outerRadiusY(120); - path = a.path().toString(); - expect(path).toMatchSnapshot(); + expect(String(a.path())).toMatchSnapshot(); a.innerRadiusY(120); - path = a.path().toString(); - expect(path).toMatchSnapshot(); + expect(String(a.path())).toMatchSnapshot(); }); test('should match paths for arc', () => { const a = arc().outerRadiusX(100); - const tarc = {startAngle: 1, endAngle: 2.5}; + const testArc = {startAngle: 1, endAngle: 2.5}; - let path = a.path(tarc).toString(); + let path = a.path(testArc).toString(); expect(path).toMatchSnapshot(); a.innerRadiusX(50); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toMatchSnapshot(); a.outerRadiusY(120); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toMatchSnapshot(); a.innerRadiusY(120); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toMatchSnapshot(); }); test('should draw empty path for no angle', () => { const a = arc().outerRadiusX(100); - const tarc = {startAngle: 1, endAngle: 1}; + const testArc = {startAngle: 1, endAngle: 1}; - let path = a.path(tarc).toString(); + let path = a.path(testArc).toString(); expect(path).toEqual('M 0 0'); a.innerRadiusX(50); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toEqual('M 0 0'); a.outerRadiusY(120); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toEqual('M 0 0'); a.innerRadiusY(120); - path = a.path(tarc).toString(); + path = a.path(testArc).toString(); expect(path).toEqual('M 0 0'); }); }); diff --git a/src/web/components/chart/utils/__tests__/Path.test.jsx b/src/web/components/chart/utils/__tests__/Path.test.tsx similarity index 100% rename from src/web/components/chart/utils/__tests__/Path.test.jsx rename to src/web/components/chart/utils/__tests__/Path.test.tsx diff --git a/src/web/components/chart/utils/__tests__/Update.test.jsx b/src/web/components/chart/utils/__tests__/Update.test.tsx similarity index 91% rename from src/web/components/chart/utils/__tests__/Update.test.jsx rename to src/web/components/chart/utils/__tests__/Update.test.tsx index 0dc7d1f7e2..5573725527 100644 --- a/src/web/components/chart/utils/__tests__/Update.test.jsx +++ b/src/web/components/chart/utils/__tests__/Update.test.tsx @@ -6,6 +6,11 @@ import {describe, test, expect} from '@gsa/testing'; import {shouldUpdate} from 'web/components/chart/utils/Update'; +interface FooProps { + foo: boolean; + data?: number; +} + describe('shouldUpdate tests', () => { test('should update if data identity has changed', () => { expect(shouldUpdate({data: {}}, {data: {}})).toEqual(true); @@ -41,6 +46,6 @@ describe('shouldUpdate tests', () => { }); test('should not update if unknown prop has changed', () => { - expect(shouldUpdate({foo: false}, {foo: true})).toEqual(false); + expect(shouldUpdate({foo: false}, {foo: true})).toEqual(false); }); }); diff --git a/src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.jsx.snap b/src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.jsx.snap deleted file mode 100644 index e88fb7e4ae..0000000000 --- a/src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.jsx.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`arc class tests > should match paths for arc 1`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L 0 0 A 0 0 0 0 0 0 0 Z"`; - -exports[`arc class tests > should match paths for arc 2`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; - -exports[`arc class tests > should match paths for arc 3`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; - -exports[`arc class tests > should match paths for arc 4`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 71.81665729247477 A 50 120 0 0 0 27.01511529340699 100.97651817694758 Z"`; - -exports[`arc class tests > should match paths for full circle 1`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 0 0 A 0 0 0 1 0 0 0 Z"`; - -exports[`arc class tests > should match paths for full circle 2`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 50 -1.2246467991473532e-14 A 50 50 0 1 0 50 0 Z"`; - -exports[`arc class tests > should match paths for full circle 3`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -1.2246467991473534e-14 A 50 50 0 1 0 50 0 Z"`; - -exports[`arc class tests > should match paths for full circle 4`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -2.939152317953648e-14 A 50 120 0 1 0 50 0 Z"`; diff --git a/src/web/components/chart/utils/__tests__/__snapshots__/arc.test.jsx.snap b/src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.tsx.snap similarity index 80% rename from src/web/components/chart/utils/__tests__/__snapshots__/arc.test.jsx.snap rename to src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.tsx.snap index e88fb7e4ae..3998834419 100644 --- a/src/web/components/chart/utils/__tests__/__snapshots__/arc.test.jsx.snap +++ b/src/web/components/chart/utils/__tests__/__snapshots__/Arc.test.tsx.snap @@ -1,17 +1,17 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`arc class tests > should match paths for arc 1`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L 0 0 A 0 0 0 0 0 0 0 Z"`; +exports[`Arc class tests > should match paths for arc 1`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L 0 0 A 0 0 0 0 0 0 0 Z"`; -exports[`arc class tests > should match paths for arc 2`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; +exports[`Arc class tests > should match paths for arc 2`] = `"M 54.03023058681398 84.14709848078965 A 100 100 0 0 1 -80.11436155469337 59.847214410395644 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; -exports[`arc class tests > should match paths for arc 3`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; +exports[`Arc class tests > should match paths for arc 3`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 29.923607205197822 A 50 50 0 0 0 27.01511529340699 42.073549240394826 Z"`; -exports[`arc class tests > should match paths for arc 4`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 71.81665729247477 A 50 120 0 0 0 27.01511529340699 100.97651817694758 Z"`; +exports[`Arc class tests > should match paths for arc 4`] = `"M 54.03023058681398 100.97651817694758 A 100 120 0 0 1 -80.11436155469337 71.81665729247477 L -40.05718077734669 71.81665729247477 A 50 120 0 0 0 27.01511529340699 100.97651817694758 Z"`; -exports[`arc class tests > should match paths for full circle 1`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 0 0 A 0 0 0 1 0 0 0 Z"`; +exports[`Arc class tests > should match paths for full circle 1`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 0 0 A 0 0 0 1 0 0 0 Z"`; -exports[`arc class tests > should match paths for full circle 2`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 50 -1.2246467991473532e-14 A 50 50 0 1 0 50 0 Z"`; +exports[`Arc class tests > should match paths for full circle 2`] = `"M 100 0 A 100 100 0 1 1 100 -2.4492935982947064e-14 L 50 -1.2246467991473532e-14 A 50 50 0 1 0 50 0 Z"`; -exports[`arc class tests > should match paths for full circle 3`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -1.2246467991473534e-14 A 50 50 0 1 0 50 0 Z"`; +exports[`Arc class tests > should match paths for full circle 3`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -1.2246467991473534e-14 A 50 50 0 1 0 50 0 Z"`; -exports[`arc class tests > should match paths for full circle 4`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -2.939152317953648e-14 A 50 120 0 1 0 50 0 Z"`; +exports[`Arc class tests > should match paths for full circle 4`] = `"M 100 0 A 100 120 0 1 1 100 -2.939152317953648e-14 L 50 -2.939152317953648e-14 A 50 120 0 1 0 50 0 Z"`; diff --git a/src/web/components/dashboard/display/created/CreatedDisplay.jsx b/src/web/components/dashboard/display/created/CreatedDisplay.jsx index 283e1dfd59..629f8703bf 100644 --- a/src/web/components/dashboard/display/created/CreatedDisplay.jsx +++ b/src/web/components/dashboard/display/created/CreatedDisplay.jsx @@ -7,7 +7,7 @@ import React from 'react'; import Filter from 'gmp/models/filter'; import FilterTerm from 'gmp/models/filter/filterterm'; import {isDefined} from 'gmp/utils/identity'; -import LineChart, {lineDataPropType} from 'web/components/chart/Line'; +import LineChart, {lineDataPropType} from 'web/components/chart/base/Line'; import transformCreated from 'web/components/dashboard/display/created/CreatedTransform'; import DataDisplay from 'web/components/dashboard/display/DataDisplay'; import PropTypes from 'web/utils/PropTypes'; diff --git a/src/web/components/dashboard/display/severity/severity-class-transform.ts b/src/web/components/dashboard/display/severity/severity-class-transform.ts index 763d4f492f..85ea31040e 100644 --- a/src/web/components/dashboard/display/severity/severity-class-transform.ts +++ b/src/web/components/dashboard/display/severity/severity-class-transform.ts @@ -7,6 +7,7 @@ import {parseSeverity, parseInt} from 'gmp/parser'; import {isDefined} from 'gmp/utils/identity'; import {severityValue} from 'gmp/utils/number'; import {type SeverityRating, DEFAULT_SEVERITY_RATING} from 'gmp/utils/severity'; +import {type LegendData} from 'web/components/chart/base/Legend'; import { totalCount, percent as percentFunc, @@ -48,11 +49,8 @@ interface FilterValue { end?: string; } -export interface SeverityClassData { +export interface SeverityClassData extends LegendData { value: number; - label: string; - toolTip: string; - color: string; filterValue: FilterValue; } diff --git a/src/web/components/label/Label.tsx b/src/web/components/label/Label.tsx index f999e70fce..37c2102b93 100644 --- a/src/web/components/label/Label.tsx +++ b/src/web/components/label/Label.tsx @@ -4,6 +4,7 @@ */ import styled from 'styled-components'; +import {type ToString} from 'gmp/types'; interface StyledLabelProps { $backgroundColor: string; @@ -11,10 +12,6 @@ interface StyledLabelProps { $textColor: string; } -interface ToString { - toString(): string; -} - const Label = styled.div` box-sizing: border-box; position: relative; @@ -56,7 +53,7 @@ const createLabel = ( borderColor: string, textColor: string, testId: string, - text: string | ToString, + text: ToString, ): React.FC> => { return (props: React.HTMLAttributes) => (