From fb5820ae23d98f8cc219cba14344a448c1beec8c Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 12:21:37 +0100 Subject: [PATCH 1/9] refactor: extract --- src/components/AlertsTable.tsx | 204 +++++++++++++++++++++++++++++++++ src/components/Dashboard.tsx | 195 +------------------------------ 2 files changed, 210 insertions(+), 189 deletions(-) create mode 100644 src/components/AlertsTable.tsx diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx new file mode 100644 index 0000000..d2ed58e --- /dev/null +++ b/src/components/AlertsTable.tsx @@ -0,0 +1,204 @@ +import { format } from "date-fns"; +import { + Cell, + Column, + Input, + Row, + SearchField, + Table, + TableBody, + FieldGroup, + TableHeader, + SearchFieldClearButton, + Badge, +} 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"; + +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, + } = useAlertSearch(); + const [searchParams, setSearchParams] = useSearchParams(); + const { data: filteredAlerts = [] } = useFilteredAlerts(); + + 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 + + + + {filteredAlerts + .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 8b54337..173ebee 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")} -
-
-
- ))} -
-
-
+
); } From be31981a090731b32562d8ea8bf3e8d42fb9046e Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:00:54 +0100 Subject: [PATCH 2/9] limit the number of items displayed on one page --- package-lock.json | 17 ++ package.json | 1 + src/components/AlertsTable.tsx | 2 + src/components/__tests__/Dashboard.test.tsx | 164 ++++++++++++-------- src/hooks/useAlertsData.ts | 7 +- 5 files changed, 122 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81df323..59fc1e3 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 1721b36..019f2eb 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 index d2ed58e..e1b8735 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -73,6 +73,8 @@ export function AlertsTable() { const [searchParams, setSearchParams] = useSearchParams(); const { data: filteredAlerts = [] } = useFilteredAlerts(); + console.log(); + const handleToggleFilter = useCallback( (isChecked: boolean) => { if (isChecked) { diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index 5a4d8dc..a101db3 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,93 @@ 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", }, - { - 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", + 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", + 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, + ].map((alert) => ({ ...alert, alert_id: faker.string.uuid() })), + ); }), ); } @@ -296,4 +314,16 @@ 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(15); + }); + }); }); diff --git a/src/hooks/useAlertsData.ts b/src/hooks/useAlertsData.ts index 72a251b..05783a2 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 } = {}) => { @@ -68,7 +70,8 @@ export const useFilteredAlerts = () => { typeof alert.trigger_string === "object" && (alert.trigger_type as TriggerType) === "codegate-context-retriever" ); - }), + }) + .slice(0, 14), }); }; From 706d6eb331419ced6424055caa90128f53ebe399 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:34:19 +0100 Subject: [PATCH 3/9] allow pagination --- src/components/AlertsTable.tsx | 14 ++++++++++++- src/components/__tests__/Dashboard.test.tsx | 22 +++++++++++++++++++++ src/hooks/useAlertSearch.ts | 10 +++++++++- src/types.ts | 3 +++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index e1b8735..166269d 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -11,6 +11,7 @@ import { TableHeader, SearchFieldClearButton, Badge, + Button, } from "@stacklok/ui-kit"; import { Switch } from "@stacklok/ui-kit"; import { AlertConversation } from "@/api/generated"; @@ -69,11 +70,15 @@ export function AlertsTable() { setIsMaliciousFilterActive, setSearch, search, + page, + nextPage, + prevPage, } = useAlertSearch(); const [searchParams, setSearchParams] = useSearchParams(); const { data: filteredAlerts = [] } = useFilteredAlerts(); - console.log(); + const hasPreviousPage = page > 0; + const hasNextPage = true; const handleToggleFilter = useCallback( (isChecked: boolean) => { @@ -201,6 +206,13 @@ export function AlertsTable() {
+ + + ); } diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index a101db3..67664c9 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -326,4 +326,26 @@ describe("Dashboard", () => { ).toHaveLength(15); }); }); + + it("allows pagination", async () => { + mockManyAlerts(); + + render(); + + expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + + await userEvent.click(screen.getByRole("button", { name: /next/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + + await userEvent.click(screen.getByRole("button", { name: /next/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + + // back at the first page + await userEvent.click(screen.getByRole("button", { name: /previous/i })); + expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); + }); }); diff --git a/src/hooks/useAlertSearch.ts b/src/hooks/useAlertSearch.ts index fd4560f..cfc1fb0 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/types.ts b/src/types.ts index 2549273..c1fa2dd 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 = From ea7f3743ea94a8505ee54159a6ee9a129ea6c4a5 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:38:25 +0100 Subject: [PATCH 4/9] implement page slicing --- src/components/__tests__/Dashboard.test.tsx | 14 ++++++++++++++ src/hooks/useAlertsData.ts | 8 ++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index 67664c9..aa4e5d3 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -348,4 +348,18 @@ describe("Dashboard", () => { await userEvent.click(screen.getByRole("button", { name: /previous/i })); expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); }); + + it("displays the correct rows when using 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(15); + }); + }); }); diff --git a/src/hooks/useAlertsData.ts b/src/hooks/useAlertsData.ts index 05783a2..f062e02 100644 --- a/src/hooks/useAlertsData.ts +++ b/src/hooks/useAlertsData.ts @@ -35,8 +35,12 @@ export const useAlertsData = ({ ...args } = {}) => { }); }; +const PAGE_SIZE = 15; + export const useFilteredAlerts = () => { - const { isMaliciousFilterActive, search } = useAlertSearch(); + const { isMaliciousFilterActive, search, page } = useAlertSearch(); + + const page_start = page * PAGE_SIZE; return useAlertsData({ select: ( @@ -71,7 +75,7 @@ export const useFilteredAlerts = () => { (alert.trigger_type as TriggerType) === "codegate-context-retriever" ); }) - .slice(0, 14), + .slice(page_start, page_start + PAGE_SIZE - 1), }); }; From 277cf72e1c61a9b0ddcca5d4c6ad1112b3452577 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:51:42 +0100 Subject: [PATCH 5/9] fully implement pagination --- src/components/AlertsTable.tsx | 19 ++++++++++-- src/components/__tests__/Dashboard.test.tsx | 34 ++++++++++----------- src/hooks/useAlertsData.ts | 14 +++------ 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index 166269d..ccee054 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -24,6 +24,8 @@ import { useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { useFilteredAlerts } from "@/hooks/useAlertsData"; +const PAGE_SIZE = 15; + const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { const data = getMaliciousPackage(input); if (data === null) return "N/A"; @@ -77,8 +79,21 @@ export function AlertsTable() { const [searchParams, setSearchParams] = useSearchParams(); const { data: filteredAlerts = [] } = useFilteredAlerts(); + const pageStart = page * PAGE_SIZE; + const pageEnd = page * PAGE_SIZE + PAGE_SIZE - 1; + + const dataView = filteredAlerts.slice(pageStart, pageEnd); + const hasPreviousPage = page > 0; - const hasNextPage = true; + const hasNextPage = pageEnd + 1 < filteredAlerts.length; + + console.log({ + pageStart, + pageEnd, + hasPreviousPage, + hasNextPage, + length: filteredAlerts.length, + }); const handleToggleFilter = useCallback( (isChecked: boolean) => { @@ -169,7 +184,7 @@ export function AlertsTable() { - {filteredAlerts + {dataView .sort( (a, b) => new Date(b.timestamp).getTime() - diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index aa4e5d3..ca4d1a7 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -104,6 +104,7 @@ function mockManyAlerts() { ...mockedAlerts, ...mockedAlerts, ...mockedAlerts, + ...mockedAlerts, ].map((alert) => ({ ...alert, alert_id: faker.string.uuid() })), ); }), @@ -332,34 +333,31 @@ describe("Dashboard", () => { render(); - expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); - expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); + await waitFor(async () => { + await userEvent.click(screen.getByRole("button", { name: /next/i })); - await userEvent.click(screen.getByRole("button", { name: /next/i })); - expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + expect( + within(screen.getByTestId("alerts-table")).getAllByRole("row").length, + ).toBeLessThan(15); + }); - await userEvent.click(screen.getByRole("button", { name: /next/i })); - expect(screen.getByRole("button", { name: /previous/i })).toBeEnabled(); + // 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(); - - // back at the first page - await userEvent.click(screen.getByRole("button", { name: /previous/i })); - expect(screen.getByRole("button", { name: /previous/i })).toBeDisabled(); - }); - - it("displays the correct rows when using pagination", async () => { - mockManyAlerts(); - - render(); + expect(screen.getByRole("button", { name: /next/i })).toBeEnabled(); await waitFor(async () => { - await userEvent.click(screen.getByRole("button", { name: /next/i })); + 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, - ).toBeLessThan(15); + ).toEqual(15); }); }); }); diff --git a/src/hooks/useAlertsData.ts b/src/hooks/useAlertsData.ts index f062e02..71e3270 100644 --- a/src/hooks/useAlertsData.ts +++ b/src/hooks/useAlertsData.ts @@ -35,18 +35,14 @@ export const useAlertsData = ({ ...args } = {}) => { }); }; -const PAGE_SIZE = 15; - export const useFilteredAlerts = () => { - const { isMaliciousFilterActive, search, page } = useAlertSearch(); - - const page_start = page * PAGE_SIZE; + const { isMaliciousFilterActive, search } = useAlertSearch(); return useAlertsData({ select: ( data: Exclude["data"], undefined>, - ) => - data + ) => { + return data .filter((alert) => { const maliciousPkg = getMaliciousPackage(alert.trigger_string); const maliciousPkgName = @@ -74,8 +70,8 @@ export const useFilteredAlerts = () => { typeof alert.trigger_string === "object" && (alert.trigger_type as TriggerType) === "codegate-context-retriever" ); - }) - .slice(page_start, page_start + PAGE_SIZE - 1), + }); + }, }); }; From 05c7da3b461fb48656653aedd3e55171ca677d21 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:52:18 +0100 Subject: [PATCH 6/9] remove unnecessary debug print --- src/components/AlertsTable.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index ccee054..c7a9d26 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -87,14 +87,6 @@ export function AlertsTable() { const hasPreviousPage = page > 0; const hasNextPage = pageEnd + 1 < filteredAlerts.length; - console.log({ - pageStart, - pageEnd, - hasPreviousPage, - hasNextPage, - length: filteredAlerts.length, - }); - const handleToggleFilter = useCallback( (isChecked: boolean) => { if (isChecked) { From 7c7d044ff266c2b416d49737d256dcc70f3f6c77 Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 14:59:18 +0100 Subject: [PATCH 7/9] extract hook for client side pagination --- src/components/AlertsTable.tsx | 15 ++++++--------- src/hooks/useClientSidePagination.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useClientSidePagination.ts diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index c7a9d26..aa82ab0 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -23,8 +23,7 @@ import { useAlertSearch } from "@/hooks/useAlertSearch"; import { useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { useFilteredAlerts } from "@/hooks/useAlertsData"; - -const PAGE_SIZE = 15; +import { useClientSidePagination } from "@/hooks/useClientSidePagination"; const wrapObjectOutput = (input: AlertConversation["trigger_string"]) => { const data = getMaliciousPackage(input); @@ -79,13 +78,11 @@ export function AlertsTable() { const [searchParams, setSearchParams] = useSearchParams(); const { data: filteredAlerts = [] } = useFilteredAlerts(); - const pageStart = page * PAGE_SIZE; - const pageEnd = page * PAGE_SIZE + PAGE_SIZE - 1; - - const dataView = filteredAlerts.slice(pageStart, pageEnd); - - const hasPreviousPage = page > 0; - const hasNextPage = pageEnd + 1 < filteredAlerts.length; + const { dataView, hasNextPage, hasPreviousPage } = useClientSidePagination( + filteredAlerts, + page, + 15, + ); const handleToggleFilter = useCallback( (isChecked: boolean) => { diff --git a/src/hooks/useClientSidePagination.ts b/src/hooks/useClientSidePagination.ts new file mode 100644 index 0000000..531472b --- /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 - 1; + + const dataView = data.slice(pageStart, pageEnd); + + const hasPreviousPage = page > 0; + const hasNextPage = pageEnd + 1 < data.length; + + return { pageStart, pageEnd, dataView, hasPreviousPage, hasNextPage }; +} From 415b4e925f6ea329aabee4b997791f6718a9503b Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 15:02:48 +0100 Subject: [PATCH 8/9] improve pagination layout --- src/components/AlertsTable.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/AlertsTable.tsx b/src/components/AlertsTable.tsx index aa82ab0..e67e6cd 100644 --- a/src/components/AlertsTable.tsx +++ b/src/components/AlertsTable.tsx @@ -211,12 +211,16 @@ export function AlertsTable() { - - +
+
+ + +
+
); } From 3a1060db3862d1bfb2de91295dcf8e9239e2d03f Mon Sep 17 00:00:00 2001 From: Daniel Kantor Date: Fri, 17 Jan 2025 15:07:45 +0100 Subject: [PATCH 9/9] fixes for pagination logic --- src/components/__tests__/Dashboard.test.tsx | 19 +++++++++++-------- src/hooks/useClientSidePagination.ts | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/__tests__/Dashboard.test.tsx b/src/components/__tests__/Dashboard.test.tsx index ca4d1a7..c359e91 100644 --- a/src/components/__tests__/Dashboard.test.tsx +++ b/src/components/__tests__/Dashboard.test.tsx @@ -324,7 +324,7 @@ describe("Dashboard", () => { await waitFor(() => { expect( within(screen.getByTestId("alerts-table")).getAllByRole("row"), - ).toHaveLength(15); + ).toHaveLength(16); }); }); @@ -333,13 +333,16 @@ describe("Dashboard", () => { render(); - await waitFor(async () => { - await userEvent.click(screen.getByRole("button", { name: /next/i })); + await waitFor( + async () => { + await userEvent.click(screen.getByRole("button", { name: /next/i })); - expect( - within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toBeLessThan(15); - }); + 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(); @@ -357,7 +360,7 @@ describe("Dashboard", () => { expect( within(screen.getByTestId("alerts-table")).getAllByRole("row").length, - ).toEqual(15); + ).toEqual(16); }); }); }); diff --git a/src/hooks/useClientSidePagination.ts b/src/hooks/useClientSidePagination.ts index 531472b..2529025 100644 --- a/src/hooks/useClientSidePagination.ts +++ b/src/hooks/useClientSidePagination.ts @@ -4,12 +4,12 @@ export function useClientSidePagination( pageSize: number, ) { const pageStart = page * pageSize; - const pageEnd = page * pageSize + pageSize - 1; + const pageEnd = page * pageSize + pageSize; const dataView = data.slice(pageStart, pageEnd); const hasPreviousPage = page > 0; - const hasNextPage = pageEnd + 1 < data.length; + const hasNextPage = pageEnd < data.length; return { pageStart, pageEnd, dataView, hasPreviousPage, hasNextPage }; }