Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add advisory list page #20

Merged
merged 2 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to get words converted from numbers from the backend? Would future localizations be easier/harder? Should l10n stuff be only in the front-end, leaving the backend simpler?

I don't know nor do I have an opinion, just questions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to get words converted from numbers from the backend?

Definitely it would be easier. The current Trustification returns a number. But you're right, this is thing to enhance and leave it to the backend to interpret/convert scores (numbers).

So I'll leave it to the backend .

It seems I will need to redefine my DTOs (mock DTOs, not linked to the backend yet). https://github.com/carlosthe19916/trustify/blob/advisory-list/frontend/client/src/app/api/models.ts#L73

Would future localizations be easier/harder
localyzation should not be affected. It won't make it easier nor harder, just not affect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mock DTOs

+1

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 @@ -25,7 +25,7 @@
export const AnalyticsContextProvider: React.FC<IAnalyticsProviderProps> = ({
children,
}) => {
const auth = (isAuthRequired && useAuth()) || undefined;

Check warning on line 28 in frontend/client/src/app/components/AnalyticsProvider.tsx

View workflow job for this annotation

GitHub Actions / test

React Hook "useAuth" is called conditionally. React Hooks must be called in the exact same order in every component render
const analytics = React.useMemo(() => {
return AnalyticsBrowser.load(analyticsSettings);
}, []);
Expand All @@ -35,7 +35,8 @@
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 @@
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 */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw others eslint inline here and there and again I think those things will be solved in the future so no problem for me.

| 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
Loading