diff --git a/packages/app/src/app/components/AppRouteProvider.tsx b/packages/app/src/app/components/AppRouteProvider.tsx index a3ecf83..302ecad 100644 --- a/packages/app/src/app/components/AppRouteProvider.tsx +++ b/packages/app/src/app/components/AppRouteProvider.tsx @@ -1,5 +1,6 @@ -import { FC, lazy } from "react"; +import { FC, Suspense, lazy } from "react"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { Loader } from "../../report/components/Loader"; import { ErrorPage } from "./ErrorPage"; const LazyAnalysisPage = lazy(() => import("../../analysis/AnalysisPage")); @@ -18,5 +19,9 @@ const router = createBrowserRouter([ ]); export const AppRouteProvider: FC = () => { - return ; + return ( + }> + + + ); }; diff --git a/packages/app/src/common/AnalysisErrorDialog.tsx b/packages/app/src/common/AnalysisErrorDialog.tsx new file mode 100644 index 0000000..e15184c --- /dev/null +++ b/packages/app/src/common/AnalysisErrorDialog.tsx @@ -0,0 +1,89 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + SvgIcon, + Theme, + Typography, +} from "@mui/material"; +import { FC, useCallback, useEffect, useState } from "react"; +import { MdErrorOutline } from "react-icons/md"; +import { useNavigate } from "react-router-dom"; +import { makeStyles } from "tss-react/mui"; +import { resetReport } from "../report/actions/error-report-action"; +import { useReportStore } from "../report/stores/fe-report-store"; + +export const AnalysisErrorDialog: FC = () => { + const { classes } = useStyles(); + const { analysisError, reset } = useReportStore(); + const [open, setOpen] = useState(!!analysisError); + const navigate = useNavigate(); + + const handleClose = useCallback(() => { + setOpen(false); + resetReport(); + navigate("/"); + }, [navigate]); + + useEffect(() => { + !!analysisError && setOpen(true); + }, [analysisError]); + + return ( + + + + An error has occured + + + +
+ + Code + + + {analysisError?.errorCode} + +
+
+ + Message + + + {analysisError?.errorMessage} + +
+
+ + Details + + + {analysisError?.errorDetails} + +
+
+ + + + +
+ ); +}; + +const useStyles = makeStyles()((theme: Theme) => ({ + dialogContent: { + display: "flex", + flexDirection: "column", + gap: theme.spacing(2), + }, +})); diff --git a/packages/app/src/report/ReportPage.tsx b/packages/app/src/report/ReportPage.tsx index 18eac81..0322086 100644 --- a/packages/app/src/report/ReportPage.tsx +++ b/packages/app/src/report/ReportPage.tsx @@ -1,5 +1,6 @@ import { FC, useEffect, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { AnalysisErrorDialog } from "../common/AnalysisErrorDialog"; import { initProgress } from "./actions/init-report-action"; import { IssuesTable } from "./components/IssuesTable"; import { ReportDrawer } from "./components/ReportDrawer"; @@ -26,6 +27,7 @@ const ReportPage: FC = () => { + ); }; diff --git a/packages/app/src/report/actions/error-report-action.ts b/packages/app/src/report/actions/error-report-action.ts new file mode 100644 index 0000000..026edda --- /dev/null +++ b/packages/app/src/report/actions/error-report-action.ts @@ -0,0 +1,8 @@ +import { ReportStore } from "../stores/fe-report-store"; + +export const resetReport = () => { + const { unsubscribe, reset } = ReportStore.getState(); + + unsubscribe && unsubscribe(); + reset(); +}; diff --git a/packages/app/src/report/actions/init-report-action.ts b/packages/app/src/report/actions/init-report-action.ts index 9ce442a..8cd278c 100644 --- a/packages/app/src/report/actions/init-report-action.ts +++ b/packages/app/src/report/actions/init-report-action.ts @@ -3,7 +3,7 @@ import { NavigateFunction } from "react-router-dom"; import { AnalysisStatus } from "shared-types"; import config from "../../config"; import { ReportStore } from "../stores/fe-report-store"; -import { subscribeToLintProgress } from "./subscribe-report-action"; +import { subscribeToReportProgress } from "./subscribe-report-action"; export const initProgress = async ( requestId: string, @@ -17,7 +17,7 @@ export const initProgress = async ( switch (status) { case AnalysisStatus.Created: - subscribeToLintProgress(requestId); + subscribeToReportProgress(requestId); break; case AnalysisStatus.Completed: break; diff --git a/packages/app/src/report/actions/subscribe-report-action.ts b/packages/app/src/report/actions/subscribe-report-action.ts index 0cda637..8418a7b 100644 --- a/packages/app/src/report/actions/subscribe-report-action.ts +++ b/packages/app/src/report/actions/subscribe-report-action.ts @@ -1,32 +1,35 @@ import { AnalysisStatus, ReportState } from "shared-types"; -import { subscribe } from "../../ws-client"; import { ReportStore } from "../stores/fe-report-store"; +import { subscribe } from "../utils/ws-client"; -export const subscribeToLintProgress = (requestId: string) => { +export const subscribeToReportProgress = (requestId: string) => { console.log("subscribing to WS..."); // clear previous error - ReportStore.setState({ subscriptionError: undefined }); + ReportStore.setState({ wsError: undefined }); - const unsubscribe = subscribe( + const unsubscribe = subscribe({ requestId, - (msg: Partial) => { - if (msg.status && msg.status === AnalysisStatus.Completed) { + onMessage: (msg: Partial) => { + const completed = msg.status && msg.status === AnalysisStatus.Completed; + const error = !!msg.analysisError; + if (completed || error) { + // close ws connection on completed/error unsubscribe(); } ReportStore.setState({ ...msg }); }, - () => { + onError: () => { ReportStore.setState({ - subscriptionError: "Web socket connection error", + wsError: "Web socket connection error", }); }, - () => { + onClose: () => { const { inProgress } = ReportStore.getState(); inProgress && ReportStore.setState({ - subscriptionError: "Web socket connection closed while scanning", + wsError: "Web socket connection closed while scanning", }); - } - ); + }, + }); ReportStore.setState({ unsubscribe }); }; diff --git a/packages/app/src/report/stores/fe-report-store.ts b/packages/app/src/report/stores/fe-report-store.ts index 819823f..96dc9be 100644 --- a/packages/app/src/report/stores/fe-report-store.ts +++ b/packages/app/src/report/stores/fe-report-store.ts @@ -1,7 +1,7 @@ import { AnalysisStatus, ReportState } from "shared-types"; import { createStore, useStore } from "zustand"; import { ScoreColorKey } from "../fe-report-types"; -import { resolveScoreColor } from "../utils/report-utils"; +import { resolveScoreColor } from "../utils/score-utils"; const initialState: InitialState = { status: AnalysisStatus.Created, @@ -15,7 +15,8 @@ const initialState: InitialState = { unsubscribe: undefined, repoDetails: undefined, fileDetails: undefined, - subscriptionError: undefined, + wsError: undefined, + analysisError: undefined, }; export const ReportStore = createStore((set, get) => ({ @@ -55,6 +56,16 @@ export const ReportStore = createStore((set, get) => ({ }, })); +ReportStore.subscribe((next, prev) => { + if (next.analysisError !== prev.analysisError) { + console.log("analysis error has been set"); + console.log(next); + } + + // console.log("report store updated"); + // console.log(state); +}); + export const useReportStore = () => useStore(ReportStore); type InitialState = Omit< @@ -70,7 +81,7 @@ type InitialState = Omit< interface FeReportStoreState extends ReportState { selectedLinterName?: string; inProgress: boolean; - subscriptionError?: string; + wsError?: string; issuesCount(toolName: string): number; unsubscribe?: () => void; scoreColor(): ScoreColorKey; diff --git a/packages/app/src/report/utils/report-utils.ts b/packages/app/src/report/utils/score-utils.ts similarity index 100% rename from packages/app/src/report/utils/report-utils.ts rename to packages/app/src/report/utils/score-utils.ts diff --git a/packages/app/src/ws-client.ts b/packages/app/src/report/utils/ws-client.ts similarity index 59% rename from packages/app/src/ws-client.ts rename to packages/app/src/report/utils/ws-client.ts index b17c882..5171a84 100644 --- a/packages/app/src/ws-client.ts +++ b/packages/app/src/report/utils/ws-client.ts @@ -1,12 +1,12 @@ import { ReportState } from "shared-types"; -import config from "./config"; +import config from "../../config"; -export const subscribe = ( - requestId: string, - onMessage: (data: ReportState) => void, - onError: () => void, - onClose: () => void -) => { +export const subscribe = ({ + requestId, + onMessage, + onError, + onClose, +}: WSClientSubsciptionOptions) => { const ws = new WebSocket( `ws://${config.CODETOTAL_WS_HOST}:${config.CODETOTAL_WS_PORT}?requestId=${requestId}` ); @@ -24,3 +24,10 @@ export const subscribe = ( }; return unsubscribe; }; + +interface WSClientSubsciptionOptions { + requestId: string; + onMessage: (data: ReportState) => void; + onError: () => void; + onClose: () => void; +} diff --git a/packages/backend/src/megalinter/megalinter-types.ts b/packages/backend/src/megalinter/megalinter-types.ts index dd5b181..38f359c 100644 --- a/packages/backend/src/megalinter/megalinter-types.ts +++ b/packages/backend/src/megalinter/megalinter-types.ts @@ -95,13 +95,21 @@ export interface LinterCompleteMessage extends BaseMessage { export interface MegalinterErrorMessage extends BaseMessage { messageType: typeof MessageType.ServerError; message: string; - errorCode: string; + errorCode: MegalinterErrorCode; errorDetails: { error: string; }; requestId: string; } +export enum MegalinterErrorCode { + MissingAnalysisType = "missingAnalysisType", + GitClone = "gitCloneError", + UploadFileNotFound = "uploadedFileNotFound", + SnippetGuessError = "snippetGuessError", + SnippetBuildError = "snippetBuildError", +} + export interface RawLinter { descriptorId: string; linterId: string; diff --git a/packages/backend/src/megalinter/parsers/parse-errors.test.ts b/packages/backend/src/megalinter/parsers/parse-errors.test.ts new file mode 100644 index 0000000..90f1742 --- /dev/null +++ b/packages/backend/src/megalinter/parsers/parse-errors.test.ts @@ -0,0 +1,47 @@ +import { MegalinterErrorMessage, MessageType } from "../megalinter-types"; +import { parseMegalinterError } from "./parse-errors"; + +const storeMock = { + set: jest.fn(), + get: jest.fn().mockImplementation(() => ({ linters: [{ name: "devskim" }] })), + subscribe: jest.fn(), +}; + +describe("parse-errors", () => { + test("parseMegalinterError", () => { + const linterErrorMessage = { + messageType: MessageType.ServerError, + message: "Some error message", + errorCode: "gitCloneError", + errorDetails: { + error: "Some error details", + }, + requestId: "123", + }; + parseMegalinterError( + linterErrorMessage as MegalinterErrorMessage, + storeMock + ); + + expect(storeMock.set).toBeCalledWith({ + analysisError: { + errorCode: "gitCloneError", + errorMessage: "Some error message", + errorDetails: "Some error details", + }, + }); + }); + + test("parseMegalinterError should fail gracefully", () => { + const linterErrorMessage = {}; + + try { + parseMegalinterError( + linterErrorMessage as MegalinterErrorMessage, + storeMock + ); + } catch (err) { + expect(err.message).toBe("Unable to parse megalinter error message"); + } + }); +}); diff --git a/packages/backend/src/megalinter/parsers/parse-errors.ts b/packages/backend/src/megalinter/parsers/parse-errors.ts new file mode 100644 index 0000000..d2a2c2e --- /dev/null +++ b/packages/backend/src/megalinter/parsers/parse-errors.ts @@ -0,0 +1,23 @@ +import { ReportStore } from "../../stores/be-report-store"; +import { logger } from "../../utils/logger"; +import { MegalinterErrorMessage } from "../megalinter-types"; + +export const parseMegalinterError = ( + msg: MegalinterErrorMessage, + reportStore: ReportStore +) => { + try { + const { errorCode, message, errorDetails } = msg; + const analysisError = { + errorCode, + errorMessage: message, + errorDetails: errorDetails.error, + }; + reportStore.set({ + analysisError, + }); + } catch (err) { + logger.megalinter.error(err); + throw new Error("Unable to parse megalinter error message"); + } +}; diff --git a/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts b/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts index 9ed0cff..16f9ada 100644 --- a/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts +++ b/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts @@ -9,7 +9,7 @@ const storeMock = { }; describe("parse-linter-status", () => { - test("createWSServer", () => { + test("parseLinterStatus", () => { const linterCompleteMessage = { linterStatus: LinterStatus.Success, linterId: "devskim", diff --git a/packages/backend/src/megalinter/parsers/parser.ts b/packages/backend/src/megalinter/parsers/parser.ts index 76e642a..f514055 100644 --- a/packages/backend/src/megalinter/parsers/parser.ts +++ b/packages/backend/src/megalinter/parsers/parser.ts @@ -3,10 +3,12 @@ import { BaseMessage, LinterCompleteMessage, MegalinterCompleteMessage, + MegalinterErrorMessage, MegalinterStartMessage, MessageType, } from "../megalinter-types"; import { parseDetails } from "./parse-details"; +import { parseMegalinterError } from "./parse-errors"; import { parseLinterStatus } from "./parse-linter-status"; import { parseMegalinterComplete } from "./parse-megalinter-complete"; import { parseMegalinterStart } from "./parse-megalinter-start"; @@ -25,6 +27,9 @@ export const parseMessage = (msg: BaseMessage, reportStore: ReportStore) => { parseLinterStatus(msg as LinterCompleteMessage, reportStore); runSarif(msg as LinterCompleteMessage, reportStore); break; + case MessageType.ServerError: + parseMegalinterError(msg as MegalinterErrorMessage, reportStore); + break; } parsePackages(msg, reportStore); diff --git a/packages/backend/src/stores/be-report-store.test.ts b/packages/backend/src/stores/be-report-store.test.ts index a364c6c..e2df2ac 100644 --- a/packages/backend/src/stores/be-report-store.test.ts +++ b/packages/backend/src/stores/be-report-store.test.ts @@ -8,6 +8,7 @@ describe("report-store", () => { expect(getStore("123").get().requestId).toEqual("123"); expect(getStore("123").get().status).toEqual(AnalysisStatus.Created); expect(getStore("123").get().score).toEqual(0); + expect(getStore("123").get().analysisError).toEqual(undefined); expect(getStore("123")).toEqual(reportStore); }); }); diff --git a/packages/backend/src/stores/be-report-store.ts b/packages/backend/src/stores/be-report-store.ts index 8926afa..7773b24 100644 --- a/packages/backend/src/stores/be-report-store.ts +++ b/packages/backend/src/stores/be-report-store.ts @@ -5,7 +5,7 @@ import { addStore } from "./stores-map"; export const createReportStore = (requestId: string) => { logger.stores.log(`Creating report store for requestId: "${requestId}"`); - + const reportStore = createStore({ requestId, resourceType: undefined, @@ -16,8 +16,12 @@ export const createReportStore = (requestId: string) => { repoDetails: undefined, fileDetails: undefined, score: 0, + analysisError: undefined, }); + + // save the store instance for later use addStore(requestId, reportStore); + return reportStore; }; @@ -25,4 +29,5 @@ export type InitialReportStoreState = Pick< ReportState, "requestId" | "status" | "score" >; + export type ReportStore = Store; diff --git a/packages/backend/src/transport/http-server.ts b/packages/backend/src/transport/http-server.ts index 9c61842..7308f71 100644 --- a/packages/backend/src/transport/http-server.ts +++ b/packages/backend/src/transport/http-server.ts @@ -30,7 +30,7 @@ export const startHttpServer = ({ host, port }: HttpServerOptions) => { "/analysis", createFileUploadHandler(), async (req: Request, res: Response) => { - logger.transport.log("Receiver new analysis request"); + logger.transport.log("Received new analysis request"); const file = req.file; let action = req.body as Analysis; if (file) { diff --git a/packages/shared-types/src/report-types.ts b/packages/shared-types/src/report-types.ts index fc53d57..a27b35f 100644 --- a/packages/shared-types/src/report-types.ts +++ b/packages/shared-types/src/report-types.ts @@ -11,10 +11,13 @@ export interface ReportState { repoDetails?: RepoDetails; fileDetails?: FileDetails; score: number; + analysisError?: { + errorCode?: string; + errorMessage?: string; + errorDetails?: string; + }; } - - export interface RepoDetails { languages: ReportLanguage[]; readmeUrl: string;