Skip to content

Commit

Permalink
Display megalinter errors to the user
Browse files Browse the repository at this point in the history
  • Loading branch information
itayox committed Jul 24, 2023
1 parent d6fdf7e commit dee1d54
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 32 deletions.
9 changes: 7 additions & 2 deletions packages/app/src/app/components/AppRouteProvider.tsx
Original file line number Diff line number Diff line change
@@ -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"));
Expand All @@ -18,5 +19,9 @@ const router = createBrowserRouter([
]);

export const AppRouteProvider: FC = () => {
return <RouterProvider router={router} />;
return (
<Suspense fallback={<Loader />}>
<RouterProvider router={router} />
</Suspense>
);
};
89 changes: 89 additions & 0 deletions packages/app/src/common/AnalysisErrorDialog.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog
maxWidth="lg"
open={open}
onAnimationEnd={reset}
onClose={handleClose}
>
<DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<SvgIcon component={MdErrorOutline} color="error" />
An error has occured
</DialogTitle>
<Divider orientation="horizontal" />
<DialogContent className={classes.dialogContent}>
<div>
<Typography variant="body2" color="text.secondary">
Code
</Typography>
<Typography variant="body2" color="text.primary">
{analysisError?.errorCode}
</Typography>
</div>
<div>
<Typography variant="body2" color="text.secondary">
Message
</Typography>
<Typography variant="body2" color="text.primary">
{analysisError?.errorMessage}
</Typography>
</div>
<div>
<Typography variant="body2" color="text.secondary">
Details
</Typography>
<Typography variant="body2" color="text.primary">
{analysisError?.errorDetails}
</Typography>
</div>
</DialogContent>
<Divider orientation="horizontal" />
<DialogActions sx={{ padding: 2 }}>
<Button variant="contained" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
);
};

const useStyles = makeStyles()((theme: Theme) => ({
dialogContent: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(2),
},
}));
2 changes: 2 additions & 0 deletions packages/app/src/report/ReportPage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -26,6 +27,7 @@ const ReportPage: FC = () => {
<ReportDrawer>
<IssuesTable />
</ReportDrawer>
<AnalysisErrorDialog />
</div>
);
};
Expand Down
8 changes: 8 additions & 0 deletions packages/app/src/report/actions/error-report-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ReportStore } from "../stores/fe-report-store";

export const resetReport = () => {
const { unsubscribe, reset } = ReportStore.getState();

unsubscribe && unsubscribe();
reset();
};
4 changes: 2 additions & 2 deletions packages/app/src/report/actions/init-report-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,7 +17,7 @@ export const initProgress = async (

switch (status) {
case AnalysisStatus.Created:
subscribeToLintProgress(requestId);
subscribeToReportProgress(requestId);
break;
case AnalysisStatus.Completed:
break;
Expand Down
27 changes: 15 additions & 12 deletions packages/app/src/report/actions/subscribe-report-action.ts
Original file line number Diff line number Diff line change
@@ -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<ReportState>) => {
if (msg.status && msg.status === AnalysisStatus.Completed) {
onMessage: (msg: Partial<ReportState>) => {
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 });
};
17 changes: 14 additions & 3 deletions packages/app/src/report/stores/fe-report-store.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,7 +15,8 @@ const initialState: InitialState = {
unsubscribe: undefined,
repoDetails: undefined,
fileDetails: undefined,
subscriptionError: undefined,
wsError: undefined,
analysisError: undefined,
};

export const ReportStore = createStore<FeReportStoreState>((set, get) => ({
Expand Down Expand Up @@ -55,6 +56,16 @@ export const ReportStore = createStore<FeReportStoreState>((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<
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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}`
);
Expand All @@ -24,3 +24,10 @@ export const subscribe = (
};
return unsubscribe;
};

interface WSClientSubsciptionOptions {
requestId: string;
onMessage: (data: ReportState) => void;
onError: () => void;
onClose: () => void;
}
10 changes: 9 additions & 1 deletion packages/backend/src/megalinter/megalinter-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions packages/backend/src/megalinter/parsers/parse-errors.test.ts
Original file line number Diff line number Diff line change
@@ -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");
}
});
});
23 changes: 23 additions & 0 deletions packages/backend/src/megalinter/parsers/parse-errors.ts
Original file line number Diff line number Diff line change
@@ -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");
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const storeMock = {
};

describe("parse-linter-status", () => {
test("createWSServer", () => {
test("parseLinterStatus", () => {
const linterCompleteMessage = {
linterStatus: LinterStatus.Success,
linterId: "devskim",
Expand Down
Loading

0 comments on commit dee1d54

Please sign in to comment.