mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Add ability to filter software by "Vulnerable" on the Host details page (#3875)
- Add new "Software" tab to host details page - Add dropdown to filter vulnerable software - Extend DataTable client-side filtering to accommodate filter inputs that are controlled by parent components - Refactor host details software into separate component
This commit is contained in:
parent
4d5e3277ef
commit
d101ec7c18
2
changes/issue-3054-3208-host-details-software
Normal file
2
changes/issue-3054-3208-host-details-software
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
* Add new "Software" tab to Host details page
|
||||||
|
* Add ability to filter software by "Vulnerable" on the Host details page
|
@ -34,6 +34,7 @@ const baseClass = "data-table-container";
|
|||||||
interface IDataTableProps {
|
interface IDataTableProps {
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
data: any;
|
data: any;
|
||||||
|
filters?: Record<string, string | number | boolean>;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
manualSortBy?: boolean;
|
manualSortBy?: boolean;
|
||||||
sortHeader: any;
|
sortHeader: any;
|
||||||
@ -69,6 +70,7 @@ const CLIENT_SIDE_DEFAULT_PAGE_SIZE = 20;
|
|||||||
const DataTable = ({
|
const DataTable = ({
|
||||||
columns: tableColumns,
|
columns: tableColumns,
|
||||||
data: tableData,
|
data: tableData,
|
||||||
|
filters: tableFilters,
|
||||||
isLoading,
|
isLoading,
|
||||||
manualSortBy = false,
|
manualSortBy = false,
|
||||||
sortHeader,
|
sortHeader,
|
||||||
@ -129,6 +131,7 @@ const DataTable = ({
|
|||||||
previousPage,
|
previousPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
setAllFilters,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
@ -143,6 +146,29 @@ const DataTable = ({
|
|||||||
manualSortBy,
|
manualSortBy,
|
||||||
// Initializes as false, but changes briefly to true on successful notification
|
// Initializes as false, but changes briefly to true on successful notification
|
||||||
autoResetSelectedRows: resetSelectedRows,
|
autoResetSelectedRows: resetSelectedRows,
|
||||||
|
// Expands the enumerated `filterTypes` for react-table
|
||||||
|
// (see https://github.com/tannerlinsley/react-table/blob/master/src/filterTypes.js)
|
||||||
|
// with custom `filterTypes` defined for this `useTable` instance
|
||||||
|
filterTypes: React.useMemo(
|
||||||
|
() => ({
|
||||||
|
hasLength: (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
rows: Row[],
|
||||||
|
columnIds: string[],
|
||||||
|
filterValue: boolean
|
||||||
|
) => {
|
||||||
|
return !filterValue
|
||||||
|
? rows
|
||||||
|
: rows?.filter((row) => {
|
||||||
|
return columnIds?.some((id) => row?.values?.[id]?.length);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
// Expands the enumerated `sortTypes` for react-table
|
||||||
|
// (see https://github.com/tannerlinsley/react-table/blob/master/src/sortTypes.js)
|
||||||
|
// with custom `sortTypes` defined for this `useTable` instance
|
||||||
sortTypes: React.useMemo(
|
sortTypes: React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
caseInsensitive: (a: any, b: any, id: any) => {
|
caseInsensitive: (a: any, b: any, id: any) => {
|
||||||
@ -174,6 +200,16 @@ const DataTable = ({
|
|||||||
|
|
||||||
const { sortBy, selectedRowIds } = tableState;
|
const { sortBy, selectedRowIds } = tableState;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableFilters) {
|
||||||
|
const allFilters = Object.entries(tableFilters).map(([id, value]) => ({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
!!allFilters.length && setAllFilters(allFilters);
|
||||||
|
}
|
||||||
|
}, [tableFilters]);
|
||||||
|
|
||||||
// Listen for changes to filters if clientSideFilter is enabled
|
// Listen for changes to filters if clientSideFilter is enabled
|
||||||
|
|
||||||
const setDebouncedClientFilter = useDebouncedCallback(
|
const setDebouncedClientFilter = useDebouncedCallback(
|
||||||
|
@ -69,6 +69,7 @@ interface ITableContainerProps {
|
|||||||
onPrimarySelectActionClick?: (selectedItemIds: number[]) => void;
|
onPrimarySelectActionClick?: (selectedItemIds: number[]) => void;
|
||||||
customControl?: () => JSX.Element;
|
customControl?: () => JSX.Element;
|
||||||
onSelectSingleRow?: (value: Row) => void;
|
onSelectSingleRow?: (value: Row) => void;
|
||||||
|
filters?: Record<string, string | number | boolean>;
|
||||||
renderCount?: () => JSX.Element | null;
|
renderCount?: () => JSX.Element | null;
|
||||||
renderFooter?: () => JSX.Element | null;
|
renderFooter?: () => JSX.Element | null;
|
||||||
}
|
}
|
||||||
@ -81,6 +82,7 @@ const DEFAULT_PAGE_INDEX = 0;
|
|||||||
const TableContainer = ({
|
const TableContainer = ({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
|
filters,
|
||||||
isLoading,
|
isLoading,
|
||||||
manualSortBy = false,
|
manualSortBy = false,
|
||||||
defaultSortHeader = "name",
|
defaultSortHeader = "name",
|
||||||
@ -342,6 +344,7 @@ const TableContainer = ({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
|
filters={filters}
|
||||||
manualSortBy={manualSortBy}
|
manualSortBy={manualSortBy}
|
||||||
sortHeader={sortHeader}
|
sortHeader={sortHeader}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
@ -40,7 +40,6 @@ import InputField from "components/forms/fields/InputField";
|
|||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
import Modal from "components/Modal";
|
import Modal from "components/Modal";
|
||||||
import SoftwareVulnerabilities from "pages/hosts/HostDetailsPage/SoftwareVulnCount";
|
|
||||||
import TableContainer from "components/TableContainer";
|
import TableContainer from "components/TableContainer";
|
||||||
import TabsWrapper from "components/TabsWrapper";
|
import TabsWrapper from "components/TabsWrapper";
|
||||||
import InfoBanner from "components/InfoBanner";
|
import InfoBanner from "components/InfoBanner";
|
||||||
@ -59,6 +58,8 @@ import {
|
|||||||
humanHostDetailUpdated,
|
humanHostDetailUpdated,
|
||||||
secondsToHms,
|
secondsToHms,
|
||||||
} from "fleet/helpers";
|
} from "fleet/helpers";
|
||||||
|
|
||||||
|
import SoftwareTab from "./SoftwareTab/SoftwareTab";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import SelectQueryModal from "./SelectQueryModal";
|
import SelectQueryModal from "./SelectQueryModal";
|
||||||
import TransferHostModal from "./TransferHostModal";
|
import TransferHostModal from "./TransferHostModal";
|
||||||
@ -67,13 +68,11 @@ import {
|
|||||||
generatePolicyTableHeaders,
|
generatePolicyTableHeaders,
|
||||||
generatePolicyDataSet,
|
generatePolicyDataSet,
|
||||||
} from "./HostPoliciesTable/HostPoliciesTableConfig";
|
} from "./HostPoliciesTable/HostPoliciesTableConfig";
|
||||||
import generateSoftwareTableHeaders from "./SoftwareTable/SoftwareTableConfig";
|
|
||||||
import generateUsersTableHeaders from "./UsersTable/UsersTableConfig";
|
import generateUsersTableHeaders from "./UsersTable/UsersTableConfig";
|
||||||
import {
|
import {
|
||||||
generatePackTableHeaders,
|
generatePackTableHeaders,
|
||||||
generatePackDataSet,
|
generatePackDataSet,
|
||||||
} from "./PackTable/PackTableConfig";
|
} from "./PackTable/PackTableConfig";
|
||||||
import EmptySoftware from "./EmptySoftware";
|
|
||||||
import EmptyUsers from "./EmptyUsers";
|
import EmptyUsers from "./EmptyUsers";
|
||||||
import PolicyFailingCount from "./HostPoliciesTable/PolicyFailingCount";
|
import PolicyFailingCount from "./HostPoliciesTable/PolicyFailingCount";
|
||||||
import { isValidPolicyResponse } from "../ManageHostsPage/helpers";
|
import { isValidPolicyResponse } from "../ManageHostsPage/helpers";
|
||||||
@ -166,8 +165,7 @@ const HostDetailsPage = ({
|
|||||||
const [showRefetchSpinner, setShowRefetchSpinner] = useState<boolean>(false);
|
const [showRefetchSpinner, setShowRefetchSpinner] = useState<boolean>(false);
|
||||||
const [packsState, setPacksState] = useState<IPackStats[]>();
|
const [packsState, setPacksState] = useState<IPackStats[]>();
|
||||||
const [scheduleState, setScheduleState] = useState<IQueryStats[]>();
|
const [scheduleState, setScheduleState] = useState<IQueryStats[]>();
|
||||||
const [softwareState, setSoftwareState] = useState<ISoftware[]>([]);
|
const [hostSoftware, setHostSoftware] = useState<ISoftware[]>([]);
|
||||||
const [softwareSearchString, setSoftwareSearchString] = useState<string>("");
|
|
||||||
const [usersState, setUsersState] = useState<{ username: string }[]>([]);
|
const [usersState, setUsersState] = useState<{ username: string }[]>([]);
|
||||||
const [usersSearchString, setUsersSearchString] = useState<string>("");
|
const [usersSearchString, setUsersSearchString] = useState<string>("");
|
||||||
const [copyMessage, setCopyMessage] = useState<string>("");
|
const [copyMessage, setCopyMessage] = useState<string>("");
|
||||||
@ -293,7 +291,7 @@ const HostDetailsPage = ({
|
|||||||
}
|
}
|
||||||
return; // exit early because refectch is pending so we can avoid unecessary steps below
|
return; // exit early because refectch is pending so we can avoid unecessary steps below
|
||||||
}
|
}
|
||||||
setSoftwareState(returnedHost.software);
|
setHostSoftware(returnedHost.software);
|
||||||
setUsersState(returnedHost.users);
|
setUsersState(returnedHost.users);
|
||||||
if (returnedHost.pack_stats) {
|
if (returnedHost.pack_stats) {
|
||||||
const packStatsByType = returnedHost.pack_stats.reduce(
|
const packStatsByType = returnedHost.pack_stats.reduce(
|
||||||
@ -333,18 +331,6 @@ const HostDetailsPage = ({
|
|||||||
});
|
});
|
||||||
}, [usersSearchString]);
|
}, [usersSearchString]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSoftwareState(() => {
|
|
||||||
return (
|
|
||||||
host?.software.filter((softwareItem) => {
|
|
||||||
return softwareItem.name
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(softwareSearchString.toLowerCase());
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [softwareSearchString]);
|
|
||||||
|
|
||||||
// returns a mixture of props from host
|
// returns a mixture of props from host
|
||||||
const normalizeEmptyValues = (hostData: any): { [key: string]: any } => {
|
const normalizeEmptyValues = (hostData: any): { [key: string]: any } => {
|
||||||
return reduce(
|
return reduce(
|
||||||
@ -528,11 +514,6 @@ const HostDetailsPage = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSoftwareTableSearchChange = useCallback((queryData: any) => {
|
|
||||||
const { searchQuery } = queryData;
|
|
||||||
setSoftwareSearchString(searchQuery);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onUsersTableSearchChange = useCallback((queryData: any) => {
|
const onUsersTableSearchChange = useCallback((queryData: any) => {
|
||||||
const { searchQuery } = queryData;
|
const { searchQuery } = queryData;
|
||||||
setUsersSearchString(searchQuery);
|
setUsersSearchString(searchQuery);
|
||||||
@ -946,51 +927,7 @@ const HostDetailsPage = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSoftware = () => {
|
const renderSoftware = () => {
|
||||||
const tableHeaders = generateSoftwareTableHeaders();
|
return <SoftwareTab isLoading={isLoadingHost} software={hostSoftware} />;
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="section section--software">
|
|
||||||
<p className="section__header">Software</p>
|
|
||||||
|
|
||||||
{host?.software.length === 0 ? (
|
|
||||||
<div className="results">
|
|
||||||
<p className="results__header">
|
|
||||||
No installed software detected on this host.
|
|
||||||
</p>
|
|
||||||
<p className="results__data">
|
|
||||||
Expecting to see software? Try again in a few seconds as the
|
|
||||||
system catches up.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{host?.software && (
|
|
||||||
<SoftwareVulnerabilities softwareList={host?.software} />
|
|
||||||
)}
|
|
||||||
{host?.software && (
|
|
||||||
<TableContainer
|
|
||||||
columns={tableHeaders}
|
|
||||||
data={softwareState}
|
|
||||||
isLoading={isLoadingHost}
|
|
||||||
defaultSortHeader={"name"}
|
|
||||||
defaultSortDirection={"asc"}
|
|
||||||
inputPlaceHolder={"Filter software"}
|
|
||||||
onQueryChange={onSoftwareTableSearchChange}
|
|
||||||
resultsTitle={"software items"}
|
|
||||||
emptyComponent={EmptySoftware}
|
|
||||||
showMarkAllPages={false}
|
|
||||||
isAllPagesSelected={false}
|
|
||||||
searchable
|
|
||||||
wideSearch
|
|
||||||
filteredCount={softwareState.length}
|
|
||||||
isClientSidePagination
|
|
||||||
highlightOnHover
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderRefetch = () => {
|
const renderRefetch = () => {
|
||||||
@ -1267,6 +1204,7 @@ const HostDetailsPage = ({
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>Details</Tab>
|
<Tab>Details</Tab>
|
||||||
|
<Tab>Software</Tab>
|
||||||
<Tab>Schedule</Tab>
|
<Tab>Schedule</Tab>
|
||||||
<Tab>Policies</Tab>
|
<Tab>Policies</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
@ -1360,10 +1298,9 @@ const HostDetailsPage = ({
|
|||||||
</div>
|
</div>
|
||||||
{renderLabels()}
|
{renderLabels()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{host?.software && renderSoftware()}
|
|
||||||
{renderUsers()}
|
{renderUsers()}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>{renderSoftware()}</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{renderSchedule()}
|
{renderSchedule()}
|
||||||
{renderPacks()}
|
{renderPacks()}
|
||||||
|
106
frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTab.tsx
Normal file
106
frontend/pages/hosts/HostDetailsPage/SoftwareTab/SoftwareTab.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useDebouncedCallback } from "use-debounce/lib";
|
||||||
|
|
||||||
|
import { ISoftware } from "interfaces/software";
|
||||||
|
import { VULNERABLE_DROPDOWN_OPTIONS } from "utilities/constants";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import Dropdown from "components/forms/fields/Dropdown";
|
||||||
|
import TableContainer from "components/TableContainer";
|
||||||
|
|
||||||
|
import EmptySoftware from "./EmptySoftware";
|
||||||
|
import SoftwareVulnCount from "./SoftwareVulnCount";
|
||||||
|
|
||||||
|
import generateSoftwareTableHeaders from "./SoftwareTableConfig";
|
||||||
|
|
||||||
|
const baseClass = "host-details";
|
||||||
|
|
||||||
|
interface ISoftwareTableProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
software: ISoftware[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SoftwareTable = ({
|
||||||
|
isLoading,
|
||||||
|
software,
|
||||||
|
}: ISoftwareTableProps): JSX.Element => {
|
||||||
|
const [filterName, setFilterName] = useState("");
|
||||||
|
const [filterVuln, setFilterVuln] = useState(false);
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
name: filterName,
|
||||||
|
vulnerabilities: filterVuln,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters({ name: filterName, vulnerabilities: filterVuln });
|
||||||
|
}, [filterName, filterVuln]);
|
||||||
|
|
||||||
|
const onQueryChange = useDebouncedCallback(
|
||||||
|
({ searchQuery }: { searchQuery: string }) => {
|
||||||
|
setFilterName(searchQuery);
|
||||||
|
},
|
||||||
|
300
|
||||||
|
);
|
||||||
|
|
||||||
|
const onVulnFilterChange = (value: boolean) => {
|
||||||
|
setFilterVuln(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderVulnFilterDropdown = () => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
value={filters.vulnerabilities}
|
||||||
|
className={`${baseClass}__vuln_dropdown`}
|
||||||
|
options={VULNERABLE_DROPDOWN_OPTIONS}
|
||||||
|
searchable={false}
|
||||||
|
onChange={onVulnFilterChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = generateSoftwareTableHeaders();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section section--software">
|
||||||
|
<p className="section__header">Software</p>
|
||||||
|
|
||||||
|
{software.length ? (
|
||||||
|
<>
|
||||||
|
{software && <SoftwareVulnCount softwareList={software} />}
|
||||||
|
{software && (
|
||||||
|
<TableContainer
|
||||||
|
columns={tableHeaders}
|
||||||
|
data={software}
|
||||||
|
filters={filters}
|
||||||
|
isLoading={isLoading}
|
||||||
|
defaultSortHeader={"name"}
|
||||||
|
defaultSortDirection={"asc"}
|
||||||
|
inputPlaceHolder={"Filter software"}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
|
resultsTitle={"software items"}
|
||||||
|
emptyComponent={EmptySoftware}
|
||||||
|
showMarkAllPages={false}
|
||||||
|
isAllPagesSelected={false}
|
||||||
|
searchable
|
||||||
|
customControl={renderVulnFilterDropdown}
|
||||||
|
isClientSidePagination
|
||||||
|
isClientSideFilter
|
||||||
|
highlightOnHover
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="results">
|
||||||
|
<p className="results__header">
|
||||||
|
No installed software detected on this host.
|
||||||
|
</p>
|
||||||
|
<p className="results__data">
|
||||||
|
Expecting to see software? Try again in a few seconds as the system
|
||||||
|
catches up.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SoftwareTable;
|
@ -35,6 +35,11 @@ interface IDataColumn {
|
|||||||
disableHidden?: boolean;
|
disableHidden?: boolean;
|
||||||
disableSortBy?: boolean;
|
disableSortBy?: boolean;
|
||||||
sortType?: string;
|
sortType?: string;
|
||||||
|
// Filter can be used by react-table to render a filter input inside the column header
|
||||||
|
Filter?: () => null | JSX.Element;
|
||||||
|
filter?: string; // one of the enumerated `filterTypes` for react-table
|
||||||
|
// (see https://github.com/tannerlinsley/react-table/blob/master/src/filterTypes.js)
|
||||||
|
// or one of the custom `filterTypes` defined for the `useTable` instance (see `DataTable`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_CONVERSION: Record<string, string> = {
|
const TYPE_CONVERSION: Record<string, string> = {
|
||||||
@ -71,6 +76,8 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => {
|
|||||||
Header: "",
|
Header: "",
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
accessor: "vulnerabilities",
|
accessor: "vulnerabilities",
|
||||||
|
Filter: () => null, // input for this column filter outside of column header
|
||||||
|
filter: "hasLength", // filters out rows where vulnerabilities has no length if filter value is `true`
|
||||||
Cell: (cellProps) => {
|
Cell: (cellProps) => {
|
||||||
const vulnerabilities = cellProps.cell.value;
|
const vulnerabilities = cellProps.cell.value;
|
||||||
if (isEmpty(vulnerabilities)) {
|
if (isEmpty(vulnerabilities)) {
|
||||||
@ -113,6 +120,8 @@ const generateSoftwareTableHeaders = (): IDataColumn[] => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessor: "name",
|
accessor: "name",
|
||||||
|
Filter: () => null, // input for this column filter is rendered outside of column header
|
||||||
|
filter: "text", // filters name text based on the user's search query
|
||||||
Cell: (cellProps) => {
|
Cell: (cellProps) => {
|
||||||
const { name, bundle_identifier } = cellProps.row.original;
|
const { name, bundle_identifier } = cellProps.row.original;
|
||||||
if (bundle_identifier) {
|
if (bundle_identifier) {
|
@ -1,15 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { ISoftware } from "interfaces/software";
|
import { ISoftware } from "interfaces/software";
|
||||||
import IssueIcon from "../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
|
import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png";
|
||||||
|
|
||||||
const baseClass = "software-vuln-count";
|
const baseClass = "software-vuln-count";
|
||||||
|
|
||||||
const SoftwareVulnCount = (vulnProps: {
|
interface ISoftwareVulnCountProps {
|
||||||
softwareList: ISoftware[];
|
softwareList: ISoftware[];
|
||||||
}): JSX.Element | null => {
|
}
|
||||||
const { softwareList } = vulnProps;
|
|
||||||
|
|
||||||
|
const SoftwareVulnCount = ({
|
||||||
|
softwareList,
|
||||||
|
}: ISoftwareVulnCountProps): JSX.Element | null => {
|
||||||
const vulnCount = softwareList.reduce((sum, software) => {
|
const vulnCount = softwareList.reduce((sum, software) => {
|
||||||
return software.vulnerabilities
|
return software.vulnerabilities
|
||||||
? sum + software.vulnerabilities.length
|
? sum + software.vulnerabilities.length
|
@ -787,4 +787,37 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin: $pad-xxlarge 0 0;
|
margin: $pad-xxlarge 0 0;
|
||||||
}
|
}
|
||||||
|
.form-field--dropdown {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&__vuln_dropdown {
|
||||||
|
width: 219px;
|
||||||
|
|
||||||
|
.Select-menu-outer {
|
||||||
|
width: 364px;
|
||||||
|
max-height: 310px;
|
||||||
|
|
||||||
|
.Select-menu {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.Select-value {
|
||||||
|
padding-left: $pad-medium;
|
||||||
|
padding-right: $pad-medium;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
padding: 5px 0 0 0; // centers spin
|
||||||
|
content: url(../assets/images/icon-filter-black-16x16@2x.png);
|
||||||
|
transform: scale(0.5);
|
||||||
|
height: 26px;
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.Select-value-label {
|
||||||
|
padding-left: $pad-large;
|
||||||
|
font-size: $small !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user