From a38e2cdd2ced63de130fe3a88118cc00a977b41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20K=C3=A1ntor?= Date: Fri, 17 Jan 2025 16:53:52 +0100 Subject: [PATCH] feat: add pagination for alerts table (#104) * refactor: extract * limit the number of items displayed on one page * allow pagination * implement page slicing * fully implement pagination * remove unnecessary debug print * extract hook for client side pagination * improve pagination layout * fixes for pagination logic --- package-lock.json | 17 ++ package.json | 1 + src/components/AlertsTable.tsx | 226 ++++++++++++++++++++ src/components/Dashboard.tsx | 195 +---------------- src/components/__tests__/Dashboard.test.tsx | 201 +++++++++++------ src/hooks/useAlertSearch.ts | 10 +- src/hooks/useAlertsData.ts | 11 +- src/hooks/useClientSidePagination.ts | 15 ++ src/types.ts | 3 + 9 files changed, 418 insertions(+), 261 deletions(-) create mode 100644 src/components/AlertsTable.tsx create mode 100644 src/hooks/useClientSidePagination.ts diff --git a/package-lock.json b/package-lock.json index c8287eca..e171cfd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@eslint/js": "^9.15.0", + "@faker-js/faker": "^9.4.0", "@hey-api/openapi-ts": "^0.61.2", "@tailwindcss/typography": "^0.5.16", "@testing-library/jest-dom": "^6.6.3", @@ -957,6 +958,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.4.0.tgz", + "integrity": "sha512-85+k0AxaZSTowL0gXp8zYWDIrWclTbRPg/pm/V0dSFZ6W6D4lhcG3uuZl4zLsEKfEvs69xDbLN2cHQudwp95JA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@fontsource-variable/figtree": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@fontsource-variable/figtree/-/figtree-5.1.2.tgz", diff --git a/package.json b/package.json index 6d0b47b7..91d40866 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@eslint/js": "^9.15.0", + "@faker-js/faker": "^9.4.0", "@hey-api/openapi-ts": "^0.61.2", "@tailwindcss/typography": "^0.5.16", "@testing-library/jest-dom": "^6.6.3", diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx new file mode 100644 index 00000000..e67e6cdd --- /dev/null +++ b/src/components/AlertsTable.tsx @@ -0,0 +1,226 @@ +import { format } from "date-fns"; +import { + Cell, + Column, + Input, + Row, + SearchField, + Table, + TableBody, + FieldGroup, + TableHeader, + SearchFieldClearButton, + Badge, + Button, +} from "@stacklok/ui-kit"; +import { Switch } from "@stacklok/ui-kit"; +import { AlertConversation } from "@/api/generated"; +import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit"; +import { getMaliciousPackage } from "@/lib/utils"; +import { Search } from "lucide-react"; +import { Markdown } from "./Markdown"; +import { useAlertSearch } from "@/hooks/useAlertSearch"; +import { useCallback } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useFilteredAlerts } from "@/hooks/useAlertsData"; +import { useClientSidePagination } from "@/hooks/useClientSidePagination"; + +const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { + const data = getMaliciousPackage(input); + if (data === null) return "N/A"; + if (typeof data === "string") { + return ( +
+ {data} +
+ ); + } + if (!data.type || !data.name) return "N/A"; + + return ( +
+ +   + + {data.type}/{data.name} + + {data.status && ( + <> +
+ {data.status} + + )} + {data.description && ( + <> +
+ {data.description} + + )} +
+ ); +}; + +export function AlertsTable() { + const { + isMaliciousFilterActive, + setIsMaliciousFilterActive, + setSearch, + search, + page, + nextPage, + prevPage, + } = useAlertSearch(); + const [searchParams, setSearchParams] = useSearchParams(); + const { data: filteredAlerts = [] } = useFilteredAlerts(); + + const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( + filteredAlerts, + page, + 15, + ); + + const handleToggleFilter = useCallback( + (isChecked: boolean) => { + if (isChecked) { + searchParams.set("maliciousPkg", "true"); + searchParams.delete("search"); + setSearch(""); + } else { + searchParams.delete("maliciousPkg"); + } + setSearchParams(searchParams); + setIsMaliciousFilterActive(isChecked); + }, + [setSearchParams, setSearch, searchParams, setIsMaliciousFilterActive], + ); + + const handleSearch = useCallback( + (value: string) => { + if (value) { + searchParams.set("search", value); + searchParams.delete("maliciousPkg"); + setSearch(value); + setIsMaliciousFilterActive(false); + } else { + searchParams.delete("search"); + setSearch(""); + } + setSearchParams(searchParams); + }, + [searchParams, setIsMaliciousFilterActive, setSearch, setSearchParams], + ); + + return ( + <> +
+
+

All Alerts

+ + {filteredAlerts.length} + +
+ +
+
+ + + Malicious Packages + + + +

Filter by malicious packages

+
+
+
+ handleSearch(value.toLowerCase().trim())} + > + + } + /> + + + +
+
+
+ + + + + Trigger Type + + Trigger Token + File + Code + Timestamp + + + + {dataView + .sort( + (a, b) => + new Date(b.timestamp).getTime() - + new Date(a.timestamp).getTime(), + ) + .map((alert) => ( + + {alert.trigger_type} + + {wrapObjectOutput(alert.trigger_string)} + + + {alert.code_snippet?.filepath || "N/A"} + + + {alert.code_snippet?.code ? ( +
+                        {alert.code_snippet.code}
+                      
+ ) : ( + "N/A" + )} +
+ +
+ {format(new Date(alert.timestamp ?? ""), "y/MM/dd")} +
+
+ {format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")} +
+
+
+ ))} +
+
+
+ +
+
+ + +
+
+ + ); +} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 8b54337e..173ebee7 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,89 +1,23 @@ -import { format } from "date-fns"; -import { - Cell, - Column, - Input, - Row, - SearchField, - Table, - TableBody, - FieldGroup, - TableHeader, - SearchFieldClearButton, - Badge, - Separator, -} from "@stacklok/ui-kit"; -import { useCallback, useEffect } from "react"; +import { Separator } from "@stacklok/ui-kit"; +import { useEffect } from "react"; import { BarChart } from "@/viz/BarChart"; import { LineChart } from "@/viz/LineChart"; -import { Markdown } from "./Markdown"; import { PieChart } from "@/viz/PieChart"; -import { Switch } from "@stacklok/ui-kit"; -import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit"; import { useSearchParams } from "react-router-dom"; -import { AlertConversation } from "@/api/generated"; -import { getMaliciousPackage } from "@/lib/utils"; import { CodegateStatus } from "@/features/dashboard-codegate-status/components/codegate-status"; -import { Search } from "lucide-react"; import { useAlertsData, - useFilteredAlerts, useMaliciousPackagesChartData, } from "@/hooks/useAlertsData"; import { useAlertSearch } from "@/hooks/useAlertSearch"; - -const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { - const data = getMaliciousPackage(input); - if (data === null) return "N/A"; - if (typeof data === "string") { - return ( -
- {data} -
- ); - } - if (!data.type || !data.name) return "N/A"; - - return ( -
- -   - - {data.type}/{data.name} - - {data.status && ( - <> -
- {data.status} - - )} - {data.description && ( - <> -
- {data.description} - - )} -
- ); -}; +import { AlertsTable } from "./AlertsTable"; export function Dashboard() { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); - const { - isMaliciousFilterActive, - setIsMaliciousFilterActive, - setSearch, - search, - } = useAlertSearch(); + const { setIsMaliciousFilterActive, setSearch } = useAlertSearch(); const { data: alerts = [], isLoading } = useAlertsData(); - const { data: filteredAlerts = [] } = useFilteredAlerts(); useEffect(() => { const isMaliciousFilterActive = searchParams.get("maliciousPkg") === "true"; @@ -98,37 +32,6 @@ export function Dashboard() { const maliciousPackages = useMaliciousPackagesChartData(); - const handleToggleFilter = useCallback( - (isChecked: boolean) => { - if (isChecked) { - searchParams.set("maliciousPkg", "true"); - searchParams.delete("search"); - setSearch(""); - } else { - searchParams.delete("maliciousPkg"); - } - setSearchParams(searchParams); - setIsMaliciousFilterActive(isChecked); - }, - [setSearchParams, setSearch, searchParams, setIsMaliciousFilterActive], - ); - - const handleSearch = useCallback( - (value: string) => { - if (value) { - searchParams.set("search", value); - searchParams.delete("maliciousPkg"); - setSearch(value); - setIsMaliciousFilterActive(false); - } else { - searchParams.delete("search"); - setSearch(""); - } - setSearchParams(searchParams); - }, - [searchParams, setIsMaliciousFilterActive, setSearch, setSearchParams], - ); - return (
@@ -140,93 +43,7 @@ export function Dashboard() { -
-
-

All Alerts

- - {filteredAlerts.length} - -
- -
-
- - - Malicious Packages - - - -

Filter by malicious packages

-
-
-
- handleSearch(value.toLowerCase().trim())} - > - - } - /> - - - -
-
-
- - - - - Trigger Type - - Trigger Token - File - Code - Timestamp - - - - {filteredAlerts.map((alert) => ( - - {alert.trigger_type} - - {wrapObjectOutput(alert.trigger_string)} - - - {alert.code_snippet?.filepath || "N/A"} - - - {alert.code_snippet?.code ? ( -
-                      {alert.code_snippet.code}
-                    
- ) : ( - "N/A" - )} -
- -
- {format(new Date(alert.timestamp ?? ""), "y/MM/dd")} -
-
- {format(new Date(alert.timestamp ?? ""), "hh:mm:ss a")} -
-
-
- ))} -
-
-
+
); } diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index 5a4d8dc4..c359e91e 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -2,9 +2,11 @@ import { render } from "@/lib/test-utils"; import { screen, waitFor, within } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { Dashboard } from "../Dashboard"; +import { faker } from "@faker-js/faker"; import React from "react"; import { server } from "@/mocks/msw/node"; import { HttpResponse, http } from "msw"; +import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json"; import userEvent from "@testing-library/user-event"; vi.mock("recharts", async (importOriginal) => { @@ -17,77 +19,94 @@ vi.mock("recharts", async (importOriginal) => { }; }); -function mockAlertsWithMaliciousPkg() { - server.use( - http.get("*/dashboard/alerts", () => { - return HttpResponse.json([ - { - conversation: { - question_answers: [ - { - question: { - message: - "\n\n```tsx src/App.tsx (23-29)\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n\n\n```\nPlease explain the cause of this error and how to solve it: 'test' is declared but its value is never read.", - timestamp: "2025-01-02T13:42:48.617799Z", - message_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", - }, - answer: { - message: - 'The error message "\'test\' is declared but its value is never read" indicates that the variable `test` is defined in your code, but it is never used anywhere. This can lead to confusion for other developers reading the code and might indicate that the intended functionality has not been implemented.\n\n### Cause of the Error\n\n1. **Unused Variable**: The variable `test` is declared but not used in the rest of the code within the `App` component. This can be a result of incomplete implementation or a placeholder that was meant to be used later but was forgotten.\n\n### How to Solve It\n\nYou have a few options to resolve this issue:\n\n1. **Remove the Unused Variable**:\n - If the variable is not needed, you can safely remove it to clean up the code and eliminate the warning.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n2. **Use the Variable**:\n - If the variable is intended to be used, ensure that it is utilized somewhere in the component. For example, you could log it for debugging purposes or use it in some part of the application logic.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n console.log(test); // Example usage\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n3. **Suppress the Warning (Not Recommended)**:\n - If you are sure that the variable is intentionally unused and do not want to remove it, you can suppress the warning. However, this is generally not recommended as it can lead to oversight of real issues.\n\n ```tsx\n // @ts-ignore\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n ```\n\n### Best Practices\n\n- **Keep Code Clean**: Regularly clean up unused variables to maintain a clean and understandable codebase.\n- **Use Linters**: Utilize tools like ESLint to automatically detect and flag unused variables, helping you maintain code quality.\n\nBy addressing this issue, you can improve the maintainability and readability of your code.', - timestamp: "2025-01-02T13:42:49.361714Z", - message_id: "a542408b-d0bc-4f6f-8b71-64c93c1e388a", - }, - }, - ], - provider: "vllm", - type: "chat", - chat_id: "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", - conversation_timestamp: "2025-01-02T13:42:48.617799Z", - }, - alert_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", - code_snippet: null, - trigger_string: - 'GitHub - Access Token:\n useSse();\n\n const test = "ghp_token";\n const breadcrumb = useBreadcrumb();\n\n', - trigger_type: "codegate-secrets", - trigger_category: "critical", +const fakeConversionation1 = { + conversation: { + question_answers: [ + { + question: { + message: + "\n\n```tsx src/App.tsx (23-29)\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = \"REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>\";\n const breadcrumb = useBreadcrumb();\n\n\n```\nPlease explain the cause of this error and how to solve it: 'test' is declared but its value is never read.", timestamp: "2025-01-02T13:42:48.617799Z", + message_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", + }, + answer: { + message: + 'The error message "\'test\' is declared but its value is never read" indicates that the variable `test` is defined in your code, but it is never used anywhere. This can lead to confusion for other developers reading the code and might indicate that the intended functionality has not been implemented.\n\n### Cause of the Error\n\n1. **Unused Variable**: The variable `test` is declared but not used in the rest of the code within the `App` component. This can be a result of incomplete implementation or a placeholder that was meant to be used later but was forgotten.\n\n### How to Solve It\n\nYou have a few options to resolve this issue:\n\n1. **Remove the Unused Variable**:\n - If the variable is not needed, you can safely remove it to clean up the code and eliminate the warning.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n2. **Use the Variable**:\n - If the variable is intended to be used, ensure that it is utilized somewhere in the component. For example, you could log it for debugging purposes or use it in some part of the application logic.\n\n ```tsx\n const { prompts, loading, fetchPrompts } = usePromptsStore();\n useSse();\n\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n\n useEffect(() => {\n console.log(test); // Example usage\n fetchPrompts();\n }, [fetchPrompts]);\n ```\n\n3. **Suppress the Warning (Not Recommended)**:\n - If you are sure that the variable is intentionally unused and do not want to remove it, you can suppress the warning. However, this is generally not recommended as it can lead to oversight of real issues.\n\n ```tsx\n // @ts-ignore\n const test = "REDACTED<$lCzMsqxUWElmZqd/qX4llR3pqsrBJVxkt6SB6WXXKKhYG37ZnscxPawjfZmgPVoSpcSR975hOIE2s/6iVr2JETpVdwdaG4hAYlzJ4tteJg==>";\n const breadcrumb = useBreadcrumb();\n ```\n\n### Best Practices\n\n- **Keep Code Clean**: Regularly clean up unused variables to maintain a clean and understandable codebase.\n- **Use Linters**: Utilize tools like ESLint to automatically detect and flag unused variables, helping you maintain code quality.\n\nBy addressing this issue, you can improve the maintainability and readability of your code.', + timestamp: "2025-01-02T13:42:49.361714Z", + message_id: "a542408b-d0bc-4f6f-8b71-64c93c1e388a", }, - { - conversation: { - question_answers: [ - { - question: { - message: - '\n\n```py testing_file.py (1-15)\nimport invokehttp\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="ghp_token"\n\nif __name__ == "__main__":\n app.run()\n\n```\nare there malicious?', - timestamp: "2024-12-09T17:50:04.418017Z", - message_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", - }, - answer: { - message: - 'Warning: CodeGate detected one or more potentially malicious or archived packages.\n- Pkg 1: [trustypkg.dev/pypi/invokehttp](https://www.trustypkg.dev/pypi/invokehttp)\n\nYes, there is a security concern in your code. The `invokehttp` package is known to be malicious and should not be used in your project. Here\'s how to make your code more secure:\n\n1. Remove the malicious package and replace it with safe alternatives:\n - Instead of `invokehttp`, you can use well-maintained packages like `requests` or `httpx` for HTTP requests\n \nHere\'s the corrected version:\n\n```python testing_file.py\nimport requests # or httpx\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="REDACTED<$SoFD847FAe6yVl6NWtk/p5+X8sQW5eU2NO0gHwbcCH4JtuEeS3Mi1IZ0BTM3oU4028c7+Vn9kkCvHQQvp8YnevjxIbZftv0xtK1iH7BgRqrAFTyh1A==>"\n\nif __name__ == "__main__":\n app.run()\n```\n\nAdditional security recommendations:\n1. Make sure to manage your dependencies using a requirements.txt or Pipfile\n2. Regularly update your dependencies to get security patches\n3. Consider using a virtual environment for your project\n4. Use package security scanning tools like `safety` or `bandit` to check for known vulnerabilities in your dependencies', - timestamp: "2024-12-09T17:50:20.846946Z", - message_id: "91b84560-2d36-4aab-807a-c6f4612dd5fd", - }, - }, - ], - provider: "anthropic", - type: "chat", - chat_id: "chatcmpl-0f3c7eb8-4306-452b-a415-b2de8867e734", - conversation_timestamp: "2024-12-09T17:50:04.418017Z", - }, - alert_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", - code_snippet: null, - trigger_string: { - name: "invokehttp", - type: "pypi", - status: "malicious", - description: "Python HTTP for Humans.", - }, - trigger_type: "codegate-context-retriever", - trigger_category: "critical", + }, + ], + provider: "vllm", + type: "chat", + chat_id: "chatcmpl-7d87679de7ed41639eb91d8ebbaa6f72", + conversation_timestamp: "2025-01-02T13:42:48.617799Z", + }, + alert_id: "c8a6afd1-98d7-43c4-97ff-7b183b8f6a5f", + code_snippet: null, + trigger_string: + 'GitHub - Access Token:\n useSse();\n\n const test = "ghp_token";\n const breadcrumb = useBreadcrumb();\n\n', + trigger_type: "codegate-secrets", + trigger_category: "critical", + timestamp: "2025-01-02T13:42:48.617799Z", +}; + +const fakeConversionation2 = { + conversation: { + question_answers: [ + { + question: { + message: + '\n\n```py testing_file.py (1-15)\nimport invokehttp\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="ghp_token"\n\nif __name__ == "__main__":\n app.run()\n\n```\nare there malicious?', timestamp: "2024-12-09T17:50:04.418017Z", + message_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", }, - ]); + answer: { + message: + 'Warning: CodeGate detected one or more potentially malicious or archived packages.\n- Pkg 1: [trustypkg.dev/pypi/invokehttp](https://www.trustypkg.dev/pypi/invokehttp)\n\nYes, there is a security concern in your code. The `invokehttp` package is known to be malicious and should not be used in your project. Here\'s how to make your code more secure:\n\n1. Remove the malicious package and replace it with safe alternatives:\n - Instead of `invokehttp`, you can use well-maintained packages like `requests` or `httpx` for HTTP requests\n \nHere\'s the corrected version:\n\n```python testing_file.py\nimport requests # or httpx\nimport fastapi\nfrom fastapi import FastAPI, Request, Response, HTTPException\nimport numpy\n\n\n@app.route("/providers", methods=["GET"])\ndef get_providers():\n return jsonify([provider.to_dict() for provider in registry.providers])\n\nGITHUB_TOKEN="REDACTED<$SoFD847FAe6yVl6NWtk/p5+X8sQW5eU2NO0gHwbcCH4JtuEeS3Mi1IZ0BTM3oU4028c7+Vn9kkCvHQQvp8YnevjxIbZftv0xtK1iH7BgRqrAFTyh1A==>"\n\nif __name__ == "__main__":\n app.run()\n```\n\nAdditional security recommendations:\n1. Make sure to manage your dependencies using a requirements.txt or Pipfile\n2. Regularly update your dependencies to get security patches\n3. Consider using a virtual environment for your project\n4. Use package security scanning tools like `safety` or `bandit` to check for known vulnerabilities in your dependencies', + timestamp: "2024-12-09T17:50:20.846946Z", + message_id: "91b84560-2d36-4aab-807a-c6f4612dd5fd", + }, + }, + ], + provider: "anthropic", + type: "chat", + chat_id: "chatcmpl-0f3c7eb8-4306-452b-a415-b2de8867e734", + conversation_timestamp: "2024-12-09T17:50:04.418017Z", + }, + alert_id: "fa53ba0d-4fb0-4770-a563-88e504af621f", + code_snippet: null, + trigger_string: { + name: "invokehttp", + type: "pypi", + status: "malicious", + description: "Python HTTP for Humans.", + }, + trigger_type: "codegate-context-retriever", + trigger_category: "critical", + timestamp: "2024-12-09T17:50:04.418017Z", +}; + +function mockAlertsWithMaliciousPkg() { + server.use( + http.get("*/dashboard/alerts", () => { + return HttpResponse.json([fakeConversionation1, fakeConversionation2]); + }), + ); +} + +function mockManyAlerts() { + server.use( + http.get("*/dashboard/alerts", () => { + return HttpResponse.json( + [ + ...mockedAlerts, + ...mockedAlerts, + ...mockedAlerts, + ...mockedAlerts, + ...mockedAlerts, + ].map((alert) => ({ ...alert, alert_id: faker.string.uuid() })), + ); }), ); } @@ -296,4 +315,52 @@ describe("Dashboard", () => { expect(within(firstRow).getByText(/2025\/01\/14/i)).toBeVisible(); expect(within(secondRow).getByText(/2025\/01\/07/i)).toBeVisible(); }); + + it("only displays a limited number of items in the table", async () => { + mockManyAlerts(); + + render(); + + await waitFor(() => { + expect( + within(screen.getByTestId("alerts-table")).getAllByRole("row"), + ).toHaveLength(16); + }); + }); + + it("allows pagination", async () => { + mockManyAlerts(); + + render(); + + await waitFor( + async () => { + await userEvent.click(screen.getByRole("button", { name: /next/i })); + + expect( + within(screen.getByTestId("alerts-table")).getAllByRole("row").length, + ).toBeLessThan(16); + }, + { timeout: 5000 }, + ); + + // on the last page, we cannot go further + expect(screen.getByRole("button", { name: /next/i })).toBeDisabled(); + + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + await waitFor(async () => { + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + + // once we reach the first page, we cannot paginate backwards anymore + expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + expect( + within(screen.getByTestId("alerts-table")).getAllByRole("row").length, + ).toEqual(16); + }); + }); }); diff --git a/src/hooks/useAlertSearch.ts b/src/hooks/useAlertSearch.ts index fd4560f7..cfc1fb08 100644 --- a/src/hooks/useAlertSearch.ts +++ b/src/hooks/useAlertSearch.ts @@ -5,11 +5,19 @@ export const useAlertSearch = create((set) => ({ isMaliciousFilterActive: false, search: "", setSearch: (search: string) => { - set({ search }); + set({ search, page: 0 }); }, setIsMaliciousFilterActive: (isActive: boolean) => { set({ isMaliciousFilterActive: isActive, + page: 0, }); }, + page: 0, + nextPage: () => { + set((state) => ({ page: state.page + 1 })); + }, + prevPage: () => { + set((state) => ({ page: state.page - 1 })); + }, })); diff --git a/src/hooks/useAlertsData.ts b/src/hooks/useAlertsData.ts index 72a251b6..71e32706 100644 --- a/src/hooks/useAlertsData.ts +++ b/src/hooks/useAlertsData.ts @@ -11,7 +11,7 @@ import { getAlertsDashboardAlertsGetQueryKey } from "@/api/generated/@tanstack/r const fetchAlerts = async (): Promise => { const { data } = await getAlertsDashboardAlertsGet(); - return (data ?? []) + const results = (data ?? []) .filter((alert): alert is AlertConversation => alert !== null) .filter((alert) => alert.trigger_category === "critical") .filter((alert) => @@ -23,6 +23,8 @@ const fetchAlerts = async (): Promise => { (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), ); + + return results; }; export const useAlertsData = ({ ...args } = {}) => { @@ -39,8 +41,8 @@ export const useFilteredAlerts = () => { return useAlertsData({ select: ( data: Exclude["data"], undefined>, - ) => - data + ) => { + return data .filter((alert) => { const maliciousPkg = getMaliciousPackage(alert.trigger_string); const maliciousPkgName = @@ -68,7 +70,8 @@ export const useFilteredAlerts = () => { typeof alert.trigger_string === "object" && (alert.trigger_type as TriggerType) === "codegate-context-retriever" ); - }), + }); + }, }); }; diff --git a/src/hooks/useClientSidePagination.ts b/src/hooks/useClientSidePagination.ts new file mode 100644 index 00000000..25290258 --- /dev/null +++ b/src/hooks/useClientSidePagination.ts @@ -0,0 +1,15 @@ +export function useClientSidePagination( + data: T[], + page: number, + pageSize: number, +) { + const pageStart = page * pageSize; + const pageEnd = page * pageSize + pageSize; + + const dataView = data.slice(pageStart, pageEnd); + + const hasPreviousPage = page > 0; + const hasNextPage = pageEnd < data.length; + + return { pageStart, pageEnd, dataView, hasPreviousPage, hasNextPage }; +} diff --git a/src/types.ts b/src/types.ts index 25492735..c1fa2ddf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,9 @@ export type AlertSearchState = { search: string; setSearch: (search: string) => void; setIsMaliciousFilterActive: (isChecked: boolean) => void; + page: number; + nextPage: () => void; + prevPage: () => void; }; export type TriggerType =