diff --git a/api_app/visualizers_manager/classes.py b/api_app/visualizers_manager/classes.py index f95a4431e6..f595e920ae 100644 --- a/api_app/visualizers_manager/classes.py +++ b/api_app/visualizers_manager/classes.py @@ -146,7 +146,6 @@ def type(self) -> str: class VisualizableDownload(VisualizableObject): - def __init__( self, value: str, @@ -189,6 +188,63 @@ def attributes(self) -> List[str]: ] +class VisualizableImage(VisualizableObject): + """ + A visualizable component for displaying images in the visualizer. + + Supports two image sources: + - url: A URL to fetch the image from (e.g., URLscan screenshot) + - base64: Base64 encoded image data + + At least one of url or base64 must be provided. + If both are provided, url takes precedence. + """ + + def __init__( + self, + value: str = "", + url: str = "", + base64: str = "", + title: str = "", + description: str = "", + max_width: int = 500, + max_height: int = 400, + allow_expand: bool = True, + size: VisualizableSize = VisualizableSize.S_AUTO, + alignment: VisualizableAlignment = VisualizableAlignment.CENTER, + disable: bool = False, + ): + if not url and not base64: + raise ValueError("VisualizableImage requires either 'url' or 'base64'") + + super().__init__(size, alignment, disable) + self.value = value or title # value is used as alt text + self.url = url + self.base64 = base64 + self.title = title + self.description = description + self.max_width = max_width + self.max_height = max_height + self.allow_expand = allow_expand + + @property + def type(self) -> str: + return "image" + + @property + def attributes(self) -> List[str]: + return super().attributes + [ + "value", + "url", + "base64", + "title", + "description", + "max_width", + "max_height", + "allow_expand", + ] + + class VisualizableBool(VisualizableBase): def __init__( self, @@ -474,6 +530,7 @@ class Visualizer(Plugin, metaclass=abc.ABCMeta): VList = VisualizableVerticalList HList = VisualizableHorizontalList Table = VisualizableTable + Image = VisualizableImage TableColumn = VisualizableTableColumn @@ -553,7 +610,6 @@ def get_pivots_reports(self) -> QuerySet: return PivotReport.objects.filter(job=self._job) def get_data_models(self) -> QuerySet: - data_model_class = self._job.analyzable.get_data_model_class() analyzer_reports_pk = [report.pk for report in self.get_analyzer_reports()] return data_model_class.objects.filter(analyzers_report__in=analyzer_reports_pk) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a98548365b..34d3eefb73 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -126,6 +126,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -714,6 +715,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1509,6 +1511,7 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.7", "@babel/helper-module-imports": "^7.25.7", @@ -3828,6 +3831,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -4295,7 +4299,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4315,7 +4318,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4331,7 +4333,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4348,7 +4349,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4360,15 +4360,13 @@ "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, - "peer": true + "dev": true }, "node_modules/@testing-library/dom/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, - "peer": true, "engines": { "node": ">=8" } @@ -4378,7 +4376,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5175,6 +5172,7 @@ "version": "18.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.16.tgz", "integrity": "sha512-LLFWr12ZhBJ4YVw7neWLe6Pk7Ey5R9OCydfuMsz1L8bZxzaawJj2p06Q8/EFEHDeTBQNFLF62X+CG7B2zIyu0Q==", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5395,6 +5393,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5806,6 +5805,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5883,6 +5883,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6298,6 +6299,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -6308,6 +6310,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/axios-hooks/-/axios-hooks-3.1.5.tgz", "integrity": "sha512-mU4WZ9c6YiOTxgTIKbswoHvb/b9fWXa2FxNFKI/hVcfD9Qemz1r9KLfRSVZf1GZg8nFry7oTM5gxNmPSn5PG0Q==", + "peer": true, "dependencies": { "@babel/runtime": "7.18.9", "dequal": "2.0.3", @@ -6827,6 +6830,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "peer": true, "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -6874,6 +6878,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -7718,6 +7723,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -8088,6 +8094,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -8238,6 +8245,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -9027,6 +9035,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9219,6 +9228,7 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -9293,6 +9303,7 @@ "version": "6.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "peer": true, "dependencies": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -9322,6 +9333,7 @@ "version": "7.37.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -9353,6 +9365,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -9453,6 +9466,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10469,6 +10483,7 @@ "url": "https://opencollective.com/formik" } ], + "peer": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "deepmerge": "^2.1.1", @@ -15723,6 +15738,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -16627,6 +16643,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -17773,6 +17790,7 @@ "version": "6.0.13", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -18135,6 +18153,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18371,6 +18390,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -18609,6 +18629,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -18631,6 +18652,7 @@ "version": "6.27.0", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "peer": true, "dependencies": { "@remix-run/router": "1.20.0", "react-router": "6.27.0" @@ -19274,6 +19296,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -20354,6 +20377,7 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -20472,6 +20496,7 @@ "version": "9.2.3", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -20962,6 +20987,7 @@ "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -21136,6 +21162,7 @@ "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -22077,6 +22104,7 @@ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz", "integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==", "dev": true, + "peer": true, "dependencies": { "@csstools/selector-specificity": "^2.0.2", "balanced-match": "^2.0.0", @@ -22892,7 +22920,8 @@ "node_modules/tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -22936,6 +22965,7 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "peer": true, "engines": { "node": ">=10" }, @@ -23629,6 +23659,7 @@ "version": "5.88.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -23697,6 +23728,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -23746,6 +23778,7 @@ "version": "4.15.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -23804,6 +23837,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -24134,6 +24168,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -24574,6 +24609,7 @@ "version": "7.22.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -24971,6 +25007,7 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", + "peer": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } @@ -25454,6 +25491,7 @@ "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", + "peer": true, "requires": { "@babel/helper-annotate-as-pure": "^7.25.7", "@babel/helper-module-imports": "^7.25.7", @@ -26990,7 +27028,8 @@ "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true }, "@reactflow/background": { "version": "11.3.14", @@ -27297,7 +27336,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", "dev": true, - "peer": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -27314,7 +27352,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "peer": true, "requires": { "color-convert": "^2.0.1" } @@ -27324,7 +27361,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -27335,7 +27371,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "peer": true, "requires": { "color-name": "~1.1.4" } @@ -27344,22 +27379,19 @@ "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, - "peer": true + "dev": true }, "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, - "peer": true + "dev": true }, "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, - "peer": true, "requires": { "has-flag": "^4.0.0" } @@ -28085,6 +28117,7 @@ "version": "18.2.16", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.16.tgz", "integrity": "sha512-LLFWr12ZhBJ4YVw7neWLe6Pk7Ey5R9OCydfuMsz1L8bZxzaawJj2p06Q8/EFEHDeTBQNFLF62X+CG7B2zIyu0Q==", + "peer": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -28274,6 +28307,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -28584,7 +28618,8 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "peer": true }, "acorn-globals": { "version": "7.0.1", @@ -28640,6 +28675,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -28926,6 +28962,7 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "peer": true, "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -28936,6 +28973,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/axios-hooks/-/axios-hooks-3.1.5.tgz", "integrity": "sha512-mU4WZ9c6YiOTxgTIKbswoHvb/b9fWXa2FxNFKI/hVcfD9Qemz1r9KLfRSVZf1GZg8nFry7oTM5gxNmPSn5PG0Q==", + "peer": true, "requires": { "@babel/runtime": "7.18.9", "dequal": "2.0.3", @@ -29332,6 +29370,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "peer": true, "requires": {} }, "brace-expansion": { @@ -29360,6 +29399,7 @@ "version": "4.24.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "peer": true, "requires": { "caniuse-lite": "^1.0.30001663", "electron-to-chromium": "^1.5.28", @@ -29951,6 +29991,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -30211,7 +30252,8 @@ "d3-selection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true }, "d3-shape": { "version": "3.2.0", @@ -30315,7 +30357,8 @@ "date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "peer": true }, "date-fns-tz": { "version": "3.2.0", @@ -30912,6 +30955,7 @@ "version": "8.57.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -31161,6 +31205,7 @@ "version": "2.31.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "peer": true, "requires": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -31213,6 +31258,7 @@ "version": "6.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "peer": true, "requires": { "aria-query": "~5.1.3", "array-includes": "^3.1.8", @@ -31236,6 +31282,7 @@ "version": "7.37.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "peer": true, "requires": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -31281,6 +31328,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "peer": true, "requires": {} }, "eslint-plugin-testing-library": { @@ -31322,6 +31370,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -31955,6 +32004,7 @@ "version": "2.4.6", "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz", "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==", + "peer": true, "requires": { "@types/hoist-non-react-statics": "^3.3.1", "deepmerge": "^2.1.1", @@ -35729,6 +35779,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -36375,6 +36426,7 @@ "version": "8.4.27", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "peer": true, "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -37014,6 +37066,7 @@ "version": "6.0.13", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -37268,6 +37321,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -37444,6 +37498,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -37633,7 +37688,8 @@ "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", - "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" + "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "peer": true }, "react-router": { "version": "6.27.0", @@ -37647,6 +37703,7 @@ "version": "6.27.0", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "peer": true, "requires": { "@remix-run/router": "1.20.0", "react-router": "6.27.0" @@ -38131,6 +38188,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "peer": true, "requires": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -38953,6 +39011,7 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz", "integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==", + "peer": true, "requires": {} }, "react-textarea-autosize": { @@ -39038,6 +39097,7 @@ "version": "9.2.3", "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.2.3.tgz", "integrity": "sha512-1nXy7FIBIoOgXr3AIHOpgzcZXdj6rZE5YvNSPd1hYgwv8X64m6TAJsU0ExlieJdlRXhaRfTYRSZoTWa127b0gw==", + "peer": true, "requires": { "@babel/runtime": "^7.12.5", "@popperjs/core": "^2.6.0", @@ -39407,6 +39467,7 @@ "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "peer": true, "requires": { "fsevents": "~2.3.2" } @@ -39520,6 +39581,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.0.tgz", "integrity": "sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==", "devOptional": true, + "peer": true, "requires": { "@parcel/watcher": "^2.4.1", "chokidar": "^4.0.0", @@ -40237,6 +40299,7 @@ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz", "integrity": "sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==", "dev": true, + "peer": true, "requires": { "@csstools/selector-specificity": "^2.0.2", "balanced-match": "^2.0.0", @@ -40867,7 +40930,8 @@ "tslib": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "peer": true }, "tsutils": { "version": "3.21.0", @@ -40900,7 +40964,8 @@ "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "peer": true }, "type-is": { "version": "1.6.18", @@ -41374,6 +41439,7 @@ "version": "5.88.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", + "peer": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.0", @@ -41433,6 +41499,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -41470,6 +41537,7 @@ "version": "4.15.1", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "peer": true, "requires": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -41507,6 +41575,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -41747,6 +41816,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", diff --git a/frontend/src/components/common/visualizer/elements/const.js b/frontend/src/components/common/visualizer/elements/const.js index 71bb52d57b..827b90bd10 100644 --- a/frontend/src/components/common/visualizer/elements/const.js +++ b/frontend/src/components/common/visualizer/elements/const.js @@ -6,4 +6,5 @@ export const VisualizerComponentType = Object.freeze({ TITLE: "title", TABLE: "table", DOWNLOAD: "download", + IMAGE: "image", }); diff --git a/frontend/src/components/common/visualizer/elements/image.jsx b/frontend/src/components/common/visualizer/elements/image.jsx new file mode 100644 index 0000000000..b8f2f90b13 --- /dev/null +++ b/frontend/src/components/common/visualizer/elements/image.jsx @@ -0,0 +1,212 @@ +// This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl +// See the file 'LICENSE' for copying permission. + +import React from "react"; +import PropTypes from "prop-types"; +import { Modal, ModalBody, ModalHeader, UncontrolledTooltip } from "reactstrap"; +import { MdBrokenImage, MdZoomIn } from "react-icons/md"; + +/** + * ImageVisualizer component for displaying images in the visualizer. + * Supports both URL and base64 encoded images. + * Includes click-to-zoom functionality with a modal preview. + */ +export function ImageVisualizer({ + id, + url, + base64, + title, + description, + maxWidth, + maxHeight, + allowExpand, + disable, + size, +}) { + const [isModalOpen, setIsModalOpen] = React.useState(false); + const [hasError, setHasError] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(true); + + // Determine the image source - URL takes precedence over base64 + const imageSrc = React.useMemo(() => { + if (url) return url; + if (base64) { + // Handle base64 with or without data URI prefix + if (base64.startsWith("data:")) { + return base64; + } + // Default to PNG if no prefix provided + return `data:image/png;base64,${base64}`; + } + return null; + }, [url, base64]); + + const toggleModal = () => { + if (!disable && allowExpand && !hasError) { + setIsModalOpen(!isModalOpen); + } + }; + + const handleImageLoad = () => { + setIsLoading(false); + setHasError(false); + }; + + const handleImageError = () => { + setIsLoading(false); + setHasError(true); + }; + + // Reset states when image source changes + React.useEffect(() => { + setIsLoading(true); + setHasError(false); + }, [imageSrc]); + + const containerStyle = { + maxWidth: maxWidth || "500px", + display: "flex", + flexDirection: "column", + alignItems: "center", + }; + + const imageStyle = { + maxWidth: "100%", + maxHeight: maxHeight || "400px", + objectFit: "contain", + cursor: !disable && allowExpand && !hasError ? "pointer" : "default", + opacity: disable ? 0.5 : 1, + }; + + const renderImage = () => { + if (hasError) { + return ( +
+ + Failed to load image +
+ ); + } + + const imageElement = ( + {title + ); + + return ( + <> + {isLoading && ( +
+ + Loading... +
+ )} + {allowExpand && !disable ? ( + + ) : ( + imageElement + )} + + ); + }; + + if (!imageSrc) { + return ( +
+ No image source provided +
+ ); + } + + return ( +
+ {title &&
{title}
} +
+ {renderImage()} + {!hasError && !isLoading && allowExpand && !disable && ( + <> + + + Click to enlarge + + + )} +
+ {description && ( +
{description}
+ )} + + {/* Modal for expanded view */} + + + {title || "Image Preview"} + + + {title + {description &&

{description}

} +
+
+
+ ); +} + +ImageVisualizer.propTypes = { + id: PropTypes.string.isRequired, + url: PropTypes.string, + base64: PropTypes.string, + title: PropTypes.string, + description: PropTypes.string, + maxWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + maxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + allowExpand: PropTypes.bool, + disable: PropTypes.bool, + size: PropTypes.string, +}; + +ImageVisualizer.defaultProps = { + url: "", + base64: "", + title: "", + description: "", + maxWidth: "500px", + maxHeight: "400px", + allowExpand: true, + disable: false, + size: "col-auto", +}; diff --git a/frontend/src/components/common/visualizer/validators.js b/frontend/src/components/common/visualizer/validators.js index cf7a3070a5..73db9353c1 100644 --- a/frontend/src/components/common/visualizer/validators.js +++ b/frontend/src/components/common/visualizer/validators.js @@ -144,6 +144,16 @@ function parseElementFields(rawElement) { // validation for the elements switch (validatedFields.type) { + case VisualizerComponentType.IMAGE: { + validatedFields.url = parseString(rawElement.url); + validatedFields.base64 = parseString(rawElement.base64); + validatedFields.title = parseString(rawElement.title); + validatedFields.description = parseString(rawElement.description); + validatedFields.maxWidth = parseString(rawElement.max_width || "500px"); + validatedFields.maxHeight = parseString(rawElement.max_height || "400px"); + validatedFields.allowExpand = parseBool(rawElement.allow_expand ?? true); + break; + } case VisualizerComponentType.DOWNLOAD: { validatedFields.value = parseString(rawElement.value); validatedFields.mimetype = parseMimetype(rawElement.mimetype); diff --git a/frontend/src/components/common/visualizer/visualizer.jsx b/frontend/src/components/common/visualizer/visualizer.jsx index 76b6d00482..afeceec011 100644 --- a/frontend/src/components/common/visualizer/visualizer.jsx +++ b/frontend/src/components/common/visualizer/visualizer.jsx @@ -8,6 +8,7 @@ import { BooleanVisualizer } from "./elements/bool"; import { BaseVisualizer } from "./elements/base"; import { VerticalListVisualizer } from "./elements/verticalList"; import { TitleVisualizer } from "./elements/title"; +import { ImageVisualizer } from "./elements/image"; import { VisualizerComponentType } from "./elements/const"; import { getIcon } from "../icon/icons"; @@ -27,6 +28,24 @@ import { DownloadVisualizer } from "./elements/download"; function convertToElement(element, idElement, isChild = false) { let visualizerElement; switch (element.type) { + case VisualizerComponentType.IMAGE: { + visualizerElement = ( + + ); + break; + } case VisualizerComponentType.DOWNLOAD: { visualizerElement = ( { + test("renders image from URL", () => { + render( + , + ); + + expect(screen.getByText("Test Image")).toBeInTheDocument(); + expect(screen.getByText("A test description")).toBeInTheDocument(); + expect(screen.getByAltText("Test Image")).toBeInTheDocument(); + }); + + test("renders image from base64", () => { + const base64Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA"; + render( + , + ); + + const img = screen.getByAltText("Base64 Image"); + expect(img).toBeInTheDocument(); + expect(img.src).toContain("data:image/png;base64,"); + }); + + test("renders image from base64 with data URI prefix", () => { + const base64DataUri = "data:image/jpeg;base64,/9j/4AAQSkZJRg=="; + render( + , + ); + + const img = screen.getByAltText("JPEG Image"); + expect(img.src).toBe(base64DataUri); + }); + + test("shows error message when no source provided", () => { + render(); + + expect(screen.getByText("No image source provided")).toBeInTheDocument(); + }); + + test("renders with disabled state", () => { + render( + , + ); + + const img = screen.getByAltText("Disabled Image"); + expect(img).toHaveStyle({ opacity: "0.5" }); + }); + + test("opens modal on image click when allowExpand is true", async () => { + render( + , + ); + + // Simulate image load + const img = screen.getByAltText("Expandable Image"); + fireEvent.load(img); + + // Click the button wrapper (image is inside a button when allowExpand is true) + const button = img.closest("button"); + fireEvent.click(button); + + // Modal should be open - header shows the title + await waitFor(() => { + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + }); + + test("does not open modal when allowExpand is false", () => { + render( + , + ); + + // Simulate image load + const img = screen.getByAltText("Non-expandable Image"); + fireEvent.load(img); + + // Click the image + fireEvent.click(img); + + // Modal should not be open + expect(screen.queryByText("Image Preview")).not.toBeInTheDocument(); + }); + + test("shows loading state initially", () => { + render( + , + ); + + expect(screen.getByText("Loading...")).toBeInTheDocument(); + }); + + test("shows error state when image fails to load", async () => { + render( + , + ); + + // Simulate image error + const img = screen.getByAltText("Broken Image"); + fireEvent.error(img); + + await waitFor(() => { + expect(screen.getByText("Failed to load image")).toBeInTheDocument(); + }); + }); +}); diff --git a/tests/api_app/visualizers_manager/test_classes.py b/tests/api_app/visualizers_manager/test_classes.py index ff41fb21d5..e9b3e70023 100644 --- a/tests/api_app/visualizers_manager/test_classes.py +++ b/tests/api_app/visualizers_manager/test_classes.py @@ -14,6 +14,7 @@ VisualizableBool, VisualizableDownload, VisualizableHorizontalList, + VisualizableImage, VisualizableLevel, VisualizableLevelSize, VisualizableObject, @@ -677,3 +678,60 @@ def test_to_dict(self): "disable_sort_by": True, } self.assertEqual(expected_result, result) + + +class VisualizableImageTestCase(CustomTestCase): + def test_to_dict_with_url(self): + img = VisualizableImage( + url="https://example.com/image.png", + title="Test Image", + description="A test image", + max_width=400, + max_height=300, + allow_expand=True, + ) + result = img.to_dict() + + self.assertEqual(result["type"], "image") + self.assertEqual(result["url"], "https://example.com/image.png") + self.assertEqual(result["title"], "Test Image") + self.assertEqual(result["description"], "A test image") + self.assertEqual(result["max_width"], 400) + self.assertEqual(result["max_height"], 300) + self.assertEqual(result["allow_expand"], True) + + def test_to_dict_with_base64(self): + img = VisualizableImage( + base64="iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA", + title="Base64 Image", + ) + result = img.to_dict() + + self.assertEqual(result["type"], "image") + self.assertEqual(result["base64"], "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA") + self.assertEqual(result["url"], "") + self.assertEqual(result["title"], "Base64 Image") + + def test_requires_url_or_base64(self): + with self.assertRaises(ValueError) as context: + VisualizableImage(title="No source") + self.assertIn("url", str(context.exception).lower()) + + def test_default_values(self): + img = VisualizableImage(url="https://example.com/img.png") + result = img.to_dict() + + self.assertEqual(result["max_width"], 500) + self.assertEqual(result["max_height"], 400) + self.assertEqual(result["allow_expand"], True) + self.assertEqual(result["disable"], False) + self.assertEqual(result["alignment"], "center") + + def test_disabled_image(self): + img = VisualizableImage( + url="https://example.com/img.png", + disable=True, + ) + result = img.to_dict() + + self.assertEqual(result["disable"], True)