diff --git a/package-lock.json b/package-lock.json index 063312f..c309f56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4310,6 +4310,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-csv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.3.tgz", + "integrity": "sha512-dkEdyRvRpygSnNg4cyzYWSUjukIQ5lAtXJwc7BqyUfzww/Cv2dcAFGYd+sWTFpGiDNZMVPp6vVPLcAPvJID8Kg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.6", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz", @@ -12687,6 +12696,11 @@ "react": ">= 16.3.0" } }, + "node_modules/react-csv": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz", + "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw==" + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -15207,6 +15221,7 @@ "monaco-editor": "^0.40.0", "react": "^18.2.0", "react-countup": "^6.4.2", + "react-csv": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-icons": "^4.10.1", @@ -15220,6 +15235,7 @@ "@types/lodash-es": "^4.17.8", "@types/md5": "^2.3.2", "@types/react": "^18.0.37", + "@types/react-csv": "^1.1.3", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", diff --git a/packages/app/package.json b/packages/app/package.json index 7680ea8..057a608 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -21,6 +21,7 @@ "monaco-editor": "^0.40.0", "react": "^18.2.0", "react-countup": "^6.4.2", + "react-csv": "^2.2.2", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-icons": "^4.10.1", @@ -34,6 +35,7 @@ "@types/lodash-es": "^4.17.8", "@types/md5": "^2.3.2", "@types/react": "^18.0.37", + "@types/react-csv": "^1.1.3", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", diff --git a/packages/app/src/report/actions/init-report-action.ts b/packages/app/src/report/actions/init-report-action.ts index 8cd278c..e56ec30 100644 --- a/packages/app/src/report/actions/init-report-action.ts +++ b/packages/app/src/report/actions/init-report-action.ts @@ -19,8 +19,6 @@ export const initProgress = async ( case AnalysisStatus.Created: subscribeToReportProgress(requestId); break; - case AnalysisStatus.Completed: - break; case AnalysisStatus.NotFound: navigate("/"); return; diff --git a/packages/app/src/report/components/ExportToCSV.tsx b/packages/app/src/report/components/ExportToCSV.tsx new file mode 100644 index 0000000..5a97d4e --- /dev/null +++ b/packages/app/src/report/components/ExportToCSV.tsx @@ -0,0 +1,47 @@ +import { IconButton, Theme, Tooltip, Zoom } from "@mui/material"; +import { FC } from "react"; +import { CSVLink } from "react-csv"; +import { MdCloudDownload } from "react-icons/md"; +import { AnalysisStatus } from "shared-types"; +import { makeStyles } from "tss-react/mui"; +import { useReportStore } from "../stores/fe-report-store"; + +export const ExportToCSV: FC = () => { + const { classes } = useStyles(); + const { status, allIssues, requestId } = useReportStore(); + + return ( + +
+ + + + + + + + + +
+
+ ); +}; + +const useStyles = makeStyles()((theme: Theme) => ({ + exportToCSV: { + display: "inline-flex", + alignItems: "center", + gap: theme.spacing(1), + color: theme.palette.primary.main, + fontWeight: 400, + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }, +})); diff --git a/packages/app/src/report/components/ReportHeader.tsx b/packages/app/src/report/components/ReportHeader.tsx index e285412..2db0713 100644 --- a/packages/app/src/report/components/ReportHeader.tsx +++ b/packages/app/src/report/components/ReportHeader.tsx @@ -14,6 +14,7 @@ import { NavLink } from "react-router-dom"; import { makeStyles } from "tss-react/mui"; import { LanguageIcon } from "../../common/LanguageIcon"; import { useReportStore } from "../stores/fe-report-store"; +import { ExportToCSV } from "./ExportToCSV"; import { ReportHeaderSection } from "./ReportHeaderSection"; import { Score } from "./Score"; @@ -82,6 +83,7 @@ export const ReportHeader: FC = ({ ready }) => { + ({ wordBreak: "keep-all", }, footer: { + display: "flex", + alignItems: "center", + gap: theme.spacing(2), paddingBlockStart: theme.spacing(1), + justifyContent: "space-between" }, })); diff --git a/packages/app/src/report/stores/fe-report-store.ts b/packages/app/src/report/stores/fe-report-store.ts index c921b06..6af6e91 100644 --- a/packages/app/src/report/stores/fe-report-store.ts +++ b/packages/app/src/report/stores/fe-report-store.ts @@ -1,4 +1,4 @@ -import { AnalysisStatus, ReportState } from "shared-types"; +import { AnalysisStatus, Issue, ReportState } from "shared-types"; import { createStore, useStore } from "zustand"; import { ScoreColorKey } from "../fe-report-types"; import { resolveScoreColor } from "../utils/score-utils"; @@ -54,6 +54,17 @@ export const ReportStore = createStore((set, get) => ({ reset: () => { set({ ...initialState }); }, + allIssues: () => { + const { linters } = get(); + return (linters || []) + .map((linter) => + (linter.issues || []).map((issue) => ({ + ...issue, + linter: linter.name, + })) + ) + .flat(); + }, })); export const useReportStore = () => useStore(ReportStore); @@ -66,6 +77,7 @@ type InitialState = Omit< | "reset" | "lintersCompleted" | "progress" + | "allIssues" >; interface FeReportStoreState extends ReportState { @@ -79,4 +91,5 @@ interface FeReportStoreState extends ReportState { lintersCompleted(): number; progress(): number; reset(): void; + allIssues(): Issue[]; }