Skip to content

Commit

Permalink
feat: add advisory list (#216)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosthe19916 authored Nov 5, 2024
1 parent bc9e661 commit 87cb954
Show file tree
Hide file tree
Showing 10 changed files with 409 additions and 1 deletion.
2 changes: 2 additions & 0 deletions client/src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const PackageList = lazy(() => import("./pages/package-list"));
const PackageDetails = lazy(() => import("./pages/package-details"));
const SBOMList = lazy(() => import("./pages/sbom-list"));
const SBOMDetails = lazy(() => import("./pages/sbom-details"));
const AdvisoryList = lazy(() => import("./pages/advisory-list"));
const Search = lazy(() => import("./pages/search"));
const ImporterList = lazy(() => import("./pages/importer-list"));
const Upload = lazy(() => import("./pages/upload"));
Expand Down Expand Up @@ -46,6 +47,7 @@ export const AppRoutes = () => {
path: `/sboms/:${PathParam.SBOM_ID}`,
element: <SBOMDetails />,
},
{ path: "/advisories", element: <AdvisoryList /> },
{
path: `/importers`,
element: <ImporterList />,
Expand Down
10 changes: 10 additions & 0 deletions client/src/app/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export const SidebarApp: React.FC = () => {
Packages
</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink
to="/advisories"
className={({ isActive }) => {
return css(LINK_CLASS, isActive ? ACTIVE_LINK_CLASS : "");
}}
>
Advisories
</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink
to="/importers"
Expand Down
119 changes: 119 additions & 0 deletions client/src/app/pages/advisory-list/advisory-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from "react";

import { AxiosError } from "axios";

import { AdvisorySummary } from "@app/client";
import { FilterType } from "@app/components/FilterToolbar";
import { TablePersistenceKeyPrefixes } from "@app/Constants";
import {
getHubRequestParams,
ITableControls,
useTableControlProps,
useTableControlState,
} from "@app/hooks/table-controls";
import { useSelectionState } from "@app/hooks/useSelectionState";
import { useFetchAdvisories } from "@app/queries/advisories";

interface IAdvisorySearchContext {
tableControls: ITableControls<
AdvisorySummary,
"identifier" | "title" | "severity" | "revision" | "vulnerabilities",
"identifier" | "severity",
"" | "average_severity" | "revision",
string
>;

totalItemCount: number;
isFetching: boolean;
fetchError: AxiosError | null;
}

const contextDefaultValue = {} as IAdvisorySearchContext;

export const AdvisorySearchContext =
React.createContext<IAdvisorySearchContext>(contextDefaultValue);

interface IAdvisoryProvider {
children: React.ReactNode;
}

export const AdvisorySearchProvider: React.FunctionComponent<
IAdvisoryProvider
> = ({ children }) => {
const tableControlState = useTableControlState({
tableName: "advisory",
persistenceKeyPrefix: TablePersistenceKeyPrefixes.advisories,
columnNames: {
identifier: "ID",
title: "Title",
severity: "Aggregated Severity",
revision: "Revision",
vulnerabilities: "Vulnerabilities",
},
isPaginationEnabled: true,
isSortEnabled: true,
sortableColumns: ["identifier", "severity"],
isFilterEnabled: true,
filterCategories: [
{
categoryKey: "",
title: "Filter text",
placeholderText: "Search",
type: FilterType.search,
},
{
categoryKey: "average_severity",
title: "Severity",
placeholderText: "Severity",
type: FilterType.multiselect,
selectOptions: [
{ value: "none", label: "None" },
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
{ value: "critical", label: "Critical" },
],
},
{
categoryKey: "revision",
title: "Revision",
type: FilterType.dateRange,
},
],
isExpansionEnabled: false,
});

const {
result: { data: advisories, total: totalItemCount },
isFetching,
fetchError,
} = useFetchAdvisories(
getHubRequestParams({
...tableControlState,
hubSortFieldKeys: {
identifier: "identifier",
severity: "average_score",
},
})
);

const tableControls = useTableControlProps({
...tableControlState,
idProperty: "identifier",
currentPageItems: advisories,
totalItemCount,
isLoading: isFetching,
selectionState: useSelectionState({
items: advisories,
isEqual: (a, b) => a.identifier === b.identifier,
}),
});

return (
<AdvisorySearchContext.Provider
value={{ totalItemCount, isFetching, fetchError, tableControls }}
>
{children}
</AdvisorySearchContext.Provider>
);
};
36 changes: 36 additions & 0 deletions client/src/app/pages/advisory-list/advisory-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";

import {
PageSection,
PageSectionVariants,
Text,
TextContent,
} from "@patternfly/react-core";

import { AdvisorySearchProvider } from "./advisory-context";
import { AdvisoryTable } from "./advisory-table";
import { AdvisoryToolbar } from "./advisory-toolbar";

export const AdvisoryList: React.FC = () => {
return (
<>
<PageSection variant={PageSectionVariants.light}>
<TextContent>
<Text component="h1">Advisories</Text>
</TextContent>
</PageSection>
<PageSection>
<div
style={{
backgroundColor: "var(--pf-v5-global--BackgroundColor--100)",
}}
>
<AdvisorySearchProvider>
<AdvisoryToolbar />
<AdvisoryTable />
</AdvisorySearchProvider>
</div>
</PageSection>
</>
);
};
164 changes: 164 additions & 0 deletions client/src/app/pages/advisory-list/advisory-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React from "react";
import { NavLink } from "react-router-dom";

import {
ActionsColumn,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";

import { Severity } from "@app/client";
import { NotificationsContext } from "@app/components/NotificationsContext";
import { SimplePagination } from "@app/components/SimplePagination";
import {
ConditionalTableBody,
TableHeaderContentWithControls,
TableRowContentWithControls,
} from "@app/components/TableControls";
import { useDownload } from "@app/hooks/domain-controls/useDownload";

import { SeverityShieldAndText } from "@app/components/SeverityShieldAndText";
import { VulnerabilityGallery } from "@app/components/VulnerabilityGallery";
import { AdvisorySearchContext } from "./advisory-context";

export const AdvisoryTable: React.FC = ({}) => {
const { isFetching, fetchError, totalItemCount, tableControls } =
React.useContext(AdvisorySearchContext);

const { pushNotification } = React.useContext(NotificationsContext);

const {
numRenderedColumns,
currentPageItems,
propHelpers: {
paginationProps,
tableProps,
getThProps,
getTrProps,
getTdProps,
},
expansionDerivedState: { isCellExpanded },
} = tableControls;

const { downloadAdvisory } = useDownload();

return (
<>
<Table {...tableProps} aria-label="advisory-table">
<Thead>
<Tr>
<TableHeaderContentWithControls {...tableControls}>
<Th {...getThProps({ columnKey: "identifier" })} />
<Th {...getThProps({ columnKey: "title" })} />
<Th {...getThProps({ columnKey: "severity" })} />
<Th {...getThProps({ columnKey: "revision" })} />
<Th {...getThProps({ columnKey: "vulnerabilities" })} />
</TableHeaderContentWithControls>
</Tr>
</Thead>
<ConditionalTableBody
isLoading={isFetching}
isError={!!fetchError}
isNoData={totalItemCount === 0}
numRenderedColumns={numRenderedColumns}
>
{currentPageItems.map((item, rowIndex) => {
type SeverityGroup = { [key in Severity]: number };
const defaultSeverityGroup: SeverityGroup = {
critical: 0,
high: 0,
medium: 0,
low: 0,
none: 0,
};

const severiries = item.vulnerabilities.reduce((prev, current) => {
return {
...prev,
[current.severity]: prev[current.severity] + 1,
};
}, defaultSeverityGroup);

return (
<Tbody key={item.identifier} isExpanded={isCellExpanded(item)}>
<Tr {...getTrProps({ item })}>
<TableRowContentWithControls
{...tableControls}
item={item}
rowIndex={rowIndex}
>
<Td
width={15}
{...getTdProps({
columnKey: "identifier",
isCompoundExpandToggle: true,
item: item,
rowIndex,
})}
>
<NavLink to={`/advisories/${item.uuid}`}>
{item.identifier}
</NavLink>
</Td>
<Td
width={40}
modifier="truncate"
{...getTdProps({ columnKey: "title" })}
>
{item.title}
</Td>
<Td
width={15}
modifier="truncate"
{...getTdProps({ columnKey: "severity" })}
>
<SeverityShieldAndText
value={item.average_severity as Severity}
/>
</Td>
<Td width={10} {...getTdProps({ columnKey: "revision" })}>
<a href="https://github.com/trustification/trustify/issues/967">
issue-967
</a>
</Td>
<Td
width={20}
{...getTdProps({ columnKey: "vulnerabilities" })}
>
<VulnerabilityGallery severities={severiries} />
</Td>
<Td isActionCell>
<ActionsColumn
items={[
{
title: "Download",
onClick: () => {
downloadAdvisory(
item.uuid,
`${item.identifier}.json`
);
},
},
]}
/>
</Td>
</TableRowContentWithControls>
</Tr>
</Tbody>
);
})}
</ConditionalTableBody>
</Table>
<SimplePagination
idPrefix="advisory-table"
isTop={false}
isCompact
paginationProps={paginationProps}
/>
</>
);
};
40 changes: 40 additions & 0 deletions client/src/app/pages/advisory-list/advisory-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";

import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core";

import { FilterToolbar } from "@app/components/FilterToolbar";
import { SimplePagination } from "@app/components/SimplePagination";

import { AdvisorySearchContext } from "./advisory-context";

interface IAdvisoryToolbar {}

export const AdvisoryToolbar: React.FC<IAdvisoryToolbar> = ({}) => {
const { tableControls } = React.useContext(AdvisorySearchContext);

const {
propHelpers: {
toolbarProps,
filterToolbarProps,
paginationToolbarItemProps,
paginationProps,
},
} = tableControls;

return (
<>
<Toolbar {...toolbarProps}>
<ToolbarContent>
<FilterToolbar {...filterToolbarProps} />
<ToolbarItem {...paginationToolbarItemProps}>
<SimplePagination
idPrefix="advisory-table"
isTop
paginationProps={paginationProps}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
</>
);
};
1 change: 1 addition & 0 deletions client/src/app/pages/advisory-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AdvisoryList as default } from "./advisory-list";
Loading

0 comments on commit 87cb954

Please sign in to comment.