Skip to content

Commit

Permalink
Add advisory list page (#20)
Browse files Browse the repository at this point in the history
* Add advisory list page

* Add advisory details page
  • Loading branch information
carlosthe19916 authored Mar 8, 2024
1 parent e905565 commit b351d0d
Show file tree
Hide file tree
Showing 33 changed files with 1,316 additions and 30 deletions.
16 changes: 16 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,19 @@ Open browser at <http://localhost:3000>
| OIDC_Scope | Set Oidc Scope | openid |
| ANALYTICS_ENABLED | Enable/Disable analytics | false |
| ANALYTICS_WRITE_KEY | Set Segment Write key | null |

## Mock data

Enable mocks:

```shell
export MOCK=stub
```

Start app:

```shell
npm run start:dev
```

Mock data is defined at `client/src/mocks`
25 changes: 23 additions & 2 deletions frontend/client/src/app/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import React, { Suspense, lazy } from "react";
import { useRoutes } from "react-router-dom";
import { useParams, useRoutes } from "react-router-dom";

import { Bullseye, Spinner } from "@patternfly/react-core";

const Home = lazy(() => import("./pages/home"));
const AdvisoryList = lazy(() => import("./pages/advisory-list"));
const AdvisoryDetails = lazy(() => import("./pages/advisory-details"));

export enum PathParam {
ADVISORY_ID = "advisoryId",
CVE_ID = "cveId",
SBOM_ID = "sbomId",
PACKAGE_ID = "packageId",
}

export const AppRoutes = () => {
const allRoutes = useRoutes([{ path: "/", element: <Home /> }]);
const allRoutes = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/advisories", element: <AdvisoryList /> },
{
path: `/advisories/:${PathParam.ADVISORY_ID}`,
element: <AdvisoryDetails />,
},
]);

return (
<Suspense
Expand All @@ -20,3 +36,8 @@ export const AppRoutes = () => {
</Suspense>
);
};

export const useRouteParams = (pathParam: PathParam) => {
const params = useParams();
return params[pathParam];
};
47 changes: 47 additions & 0 deletions frontend/client/src/app/api/model-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
global_palette_purple_400 as criticalColor,
global_danger_color_100 as importantColor,
global_info_color_100 as lowColor,
global_warning_color_100 as moderateColor,
} from "@patternfly/react-tokens";

import { Severity } from "./models";
import { ProgressProps } from "@patternfly/react-core";

type ListType = {
[key in Severity]: {
shieldIconColor: { name: string; value: string; var: string };
progressProps: Pick<ProgressProps, "variant">;
};
};

export const severityList: ListType = {
low: {
shieldIconColor: lowColor,
progressProps: { variant: undefined },
},
moderate: {
shieldIconColor: moderateColor,
progressProps: { variant: "warning" },
},
important: {
shieldIconColor: importantColor,
progressProps: { variant: "danger" },
},
critical: {
shieldIconColor: criticalColor,
progressProps: { variant: "danger" },
},
};

export const severityFromNumber = (score: number): Severity => {
if (score >= 9.0) {
return "critical";
} else if (score >= 7.0) {
return "important";
} else if (score >= 4.0) {
return "moderate";
} else {
return "low";
}
};
41 changes: 41 additions & 0 deletions frontend/client/src/app/api/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,44 @@ export interface HubPaginatedResult<T> {
total: number;
params: HubRequestParams;
}

// Advisories

export type Severity = "low" | "moderate" | "important" | "critical";

export interface Advisory {
id: string;
aggregated_severity: Severity;
revision_date: string;
vulnerabilities_count: { [key in Severity]: number };
vulnerabilities: AdvisoryVulnerability[];
metadata: {
title: string;
category: string;
publisher: {
name: string;
namespace: string;
contact_details: string;
issuing_authority: string;
};
tracking: {
status: string;
initial_release_date: string;
current_release_date: string;
};
references: {
url: string;
label?: string;
}[];
notes: string[];
};
}

export interface AdvisoryVulnerability {
id: string;
title: string;
discovery_date: string;
release_date: string;
score: number;
cwe: string;
}
43 changes: 32 additions & 11 deletions frontend/client/src/app/api/rest.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import axios from "axios";
import { serializeRequestParamsForHub } from "@app/hooks/table-controls";
import { HubPaginatedResult, HubRequestParams } from "./models";
import axios from "axios";
import { Advisory, HubPaginatedResult, HubRequestParams } from "./models";

const HUB = "/hub";

interface ApiSearchResult<T> {
total: number;
result: T[];
}
export const ADVISORIES = HUB + "/advisories";

export const getHubPaginatedResult = <T>(
url: string,
params: HubRequestParams = {}
): Promise<HubPaginatedResult<T>> =>
axios
.get<ApiSearchResult<T>>(url, {
.get<T[]>(url, {
params: serializeRequestParamsForHub(params),
})
.then(({ data }) => ({
data: data.result,
total: data.total,
.then(({ data, headers }) => ({
data,
total: headers["x-total"]
? parseInt(headers["x-total"], 10)
: data.length,
params,
}));

export const getAdvisories = (params: HubRequestParams = {}) => {
return getHubPaginatedResult<Advisory>(ADVISORIES, params);
};

export const getAdvisoryById = (id: number | string) => {
return axios
.get<Advisory>(`${ADVISORIES}/${id}`)
.then((response) => response.data);
};

export const getAdvisorySourceById = (id: number | string) => {
return axios
.get<string>(`${ADVISORIES}/${id}/source`)
.then((response) => response.data);
};

export const downloadAdvisoryById = (id: number | string) => {
return axios.get<string>(`${ADVISORIES}/${id}/source`, {
responseType: "arraybuffer",
headers: { Accept: "text/plain", responseType: "blob" },
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from "react";

import { Flex, FlexItem } from "@patternfly/react-core";
import ShieldIcon from "@patternfly/react-icons/dist/esm/icons/shield-alt-icon";

import { Severity } from "@app/api/models";
import { severityList } from "@app/api/model-utils";

interface SeverityRendererProps {
value: Severity;
showLabel?: boolean;
}

export const SeverityRenderer: React.FC<SeverityRendererProps> = ({
value,
showLabel,
}) => {
const severityProps = severityList[value];

return (
<Flex
spaceItems={{ default: "spaceItemsXs" }}
alignItems={{ default: "alignItemsCenter" }}
flexWrap={{ default: "nowrap" }}
style={{ whiteSpace: "nowrap" }}
>
<FlexItem>
<ShieldIcon color={severityProps.shieldIconColor.value} />
</FlexItem>
{showLabel && (
<FlexItem>{value.charAt(0).toUpperCase() + value.slice(1)}</FlexItem>
)}
</Flex>
);
};
5 changes: 3 additions & 2 deletions frontend/client/src/app/components/AnalyticsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export const AnalyticsContextProvider: React.FC<IAnalyticsProviderProps> = ({
if (auth) {
const claims = auth.user?.profile;
analytics.identify(claims?.sub, {
organization_id: (claims?.organization as any)?.id,
/* eslint-disable @typescript-eslint/no-explicit-any */
organization_id: ((claims as any)?.organization as any)?.id,
domain: claims?.email?.split("@")[1],
});
}
Expand All @@ -45,7 +46,7 @@ export const AnalyticsContextProvider: React.FC<IAnalyticsProviderProps> = ({
const location = useLocation();
useEffect(() => {
analytics.page();
}, [location]);
}, [analytics, location]);

return (
<AnalyticsContext.Provider value={analytics}>
Expand Down
1 change: 1 addition & 0 deletions frontend/client/src/app/components/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ConfirmDialogProps {
| "danger"
| "warning"
| "info"
/* eslint-disable @typescript-eslint/no-explicit-any */
| React.ComponentType<any>;
message: string | React.ReactNode;

Expand Down
21 changes: 21 additions & 0 deletions frontend/client/src/app/components/LoadingWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from "react";
import ErrorState from "@patternfly/react-component-groups/dist/esm/ErrorState";
import { Bullseye, Spinner } from "@patternfly/react-core";

export const LoadingWrapper = (props: {
isFetching: boolean;
fetchError?: Error;
children: React.ReactNode;
}) => {
if (props.isFetching) {
return (
<Bullseye>
<Spinner />
</Bullseye>
);
} else if (props.fetchError) {
return <ErrorState errorTitle="Error" />;
} else {
return props.children;
}
};
1 change: 0 additions & 1 deletion frontend/client/src/app/components/PageDrawerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ export const PageDrawerContent: React.FC<IPageDrawerContentProps> = ({
header = null,
children,
drawerPanelContentProps,
focusKey,
pageKey: localPageKeyProp,
}) => {
const {
Expand Down
30 changes: 30 additions & 0 deletions frontend/client/src/app/components/SeverityProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";

import { Progress } from "@patternfly/react-core";

import { severityFromNumber, severityList } from "@app/api/model-utils";

interface SeverityRendererProps {
value: number;
showLabel?: boolean;
}

export const SeverityProgressBar: React.FC<SeverityRendererProps> = ({
value,
}) => {
const severityType = severityFromNumber(value);
const severityProps = severityList[severityType];

return (
<>
<Progress
aria-labelledby="severity"
size="sm"
max={10}
value={value}
label={`${value}/10`}
{...severityProps.progressProps}
/>
</>
);
};
46 changes: 46 additions & 0 deletions frontend/client/src/app/components/SeverityShieldAndText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";

import { Flex, FlexItem } from "@patternfly/react-core";
import ShieldIcon from "@patternfly/react-icons/dist/esm/icons/shield-alt-icon";

import { severityFromNumber, severityList } from "@app/api/model-utils";
import { Severity } from "@app/api/models";

interface SeverityShieldAndTextProps {
value: Severity | number;
showLabel?: boolean;
}

export const SeverityShieldAndText: React.FC<SeverityShieldAndTextProps> = ({
value,
showLabel,
}) => {
let severity: Severity;
if (typeof value === "number") {
severity = severityFromNumber(value);
} else {
severity = value;
}

const severityProps = severityList[severity];

return (
<>
<Flex
spaceItems={{ default: "spaceItemsXs" }}
alignItems={{ default: "alignItemsCenter" }}
flexWrap={{ default: "nowrap" }}
style={{ whiteSpace: "nowrap" }}
>
<FlexItem>
<ShieldIcon color={severityProps.shieldIconColor.value} />
</FlexItem>
{showLabel && (
<FlexItem>
{severity.charAt(0).toUpperCase() + severity.slice(1)}
</FlexItem>
)}
</Flex>
</>
);
};
Loading

0 comments on commit b351d0d

Please sign in to comment.