Fleet UI: Split out inventory vs. library team level views (#44467)
This commit is contained in:
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
software/library Library tab > Table
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { Row } from "react-table";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
|
||||
import { getPathWithQueryParams } from "utilities/url";
|
||||
import { ISoftwareTitlesResponse } from "services/entities/software";
|
||||
import { ISoftwareTitle } from "interfaces/software";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import LastUpdatedText from "components/LastUpdatedText";
|
||||
import Slider from "components/forms/fields/Slider";
|
||||
import { ITableQueryData } from "components/TableContainer/TableContainer";
|
||||
import TableCount from "components/TableContainer/TableCount";
|
||||
|
||||
import EmptySoftwareTable from "pages/SoftwarePage/components/tables/EmptySoftwareTable";
|
||||
|
||||
import generateLibraryTableConfig from "./SoftwareLibraryTableConfig";
|
||||
|
||||
interface IRowProps extends Row {
|
||||
original: {
|
||||
id?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ISoftwareLibraryTableProps {
|
||||
router: InjectedRouter;
|
||||
data?: ISoftwareTitlesResponse;
|
||||
isSoftwareEnabled: boolean;
|
||||
query: string;
|
||||
perPage: number;
|
||||
orderDirection: "asc" | "desc";
|
||||
orderKey: string;
|
||||
selfServiceOnly: boolean;
|
||||
currentPage: number;
|
||||
teamId?: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "software-library-table";
|
||||
|
||||
const SoftwareLibraryTable = ({
|
||||
router,
|
||||
data,
|
||||
isSoftwareEnabled,
|
||||
query,
|
||||
perPage,
|
||||
orderDirection,
|
||||
orderKey,
|
||||
selfServiceOnly,
|
||||
currentPage,
|
||||
teamId,
|
||||
isLoading,
|
||||
}: ISoftwareLibraryTableProps) => {
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isTeamAdmin,
|
||||
isTeamMaintainer,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const canAddSoftware =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer;
|
||||
|
||||
const determineQueryParamChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedEntry = Object.entries(newTableQuery).find(([key, val]) => {
|
||||
switch (key) {
|
||||
case "searchQuery":
|
||||
return val !== query;
|
||||
case "sortDirection":
|
||||
return val !== orderDirection;
|
||||
case "sortHeader":
|
||||
return val !== orderKey;
|
||||
case "pageIndex":
|
||||
return val !== currentPage;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return changedEntry?.[0] ?? "";
|
||||
},
|
||||
[currentPage, orderDirection, orderKey, query]
|
||||
);
|
||||
|
||||
const generateNewQueryParams = useCallback(
|
||||
(newTableQuery: ITableQueryData, changedParam: string) => {
|
||||
const newQueryParam: Record<string, string | number | undefined> = {
|
||||
query: newTableQuery.searchQuery,
|
||||
fleet_id: teamId,
|
||||
order_direction: newTableQuery.sortDirection,
|
||||
order_key: newTableQuery.sortHeader,
|
||||
page:
|
||||
changedParam === "pageIndex" || changedParam === ""
|
||||
? newTableQuery.pageIndex
|
||||
: 0,
|
||||
};
|
||||
if (selfServiceOnly) {
|
||||
newQueryParam.self_service = "true";
|
||||
}
|
||||
|
||||
return newQueryParam;
|
||||
},
|
||||
[selfServiceOnly, teamId]
|
||||
);
|
||||
|
||||
// NOTE: this is called once on initial render and every time the query changes
|
||||
const onQueryChange = useCallback(
|
||||
(newTableQuery: ITableQueryData) => {
|
||||
const changedParam = determineQueryParamChange(newTableQuery);
|
||||
|
||||
const newRoute = getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_LIBRARY,
|
||||
routeTemplate: "",
|
||||
queryParams: generateNewQueryParams(newTableQuery, changedParam),
|
||||
});
|
||||
|
||||
router.replace(newRoute);
|
||||
},
|
||||
[determineQueryParamChange, generateNewQueryParams, router]
|
||||
);
|
||||
|
||||
const tableData: ISoftwareTitle[] | undefined = data?.software_titles;
|
||||
|
||||
const softwareTableHeaders = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return generateLibraryTableConfig(router, teamId);
|
||||
}, [data, router, teamId]);
|
||||
|
||||
// Determines if a user should be able to filter or search in the table
|
||||
const hasData = tableData && tableData.length > 0;
|
||||
const hasQuery = query !== "";
|
||||
|
||||
const showFilterHeaders =
|
||||
isSoftwareEnabled && (hasData || hasQuery || selfServiceOnly);
|
||||
|
||||
const handleSelfServiceToggle = () => {
|
||||
const queryParams: Record<string, string | number | undefined> = {
|
||||
query,
|
||||
fleet_id: teamId,
|
||||
order_direction: orderDirection,
|
||||
order_key: orderKey,
|
||||
page: 0,
|
||||
};
|
||||
if (!selfServiceOnly) {
|
||||
queryParams.self_service = "true";
|
||||
}
|
||||
|
||||
router.replace(
|
||||
getNextLocationPath({
|
||||
pathPrefix: PATHS.SOFTWARE_LIBRARY,
|
||||
routeTemplate: "",
|
||||
queryParams,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRowSelect = (row: IRowProps) => {
|
||||
if (!row.original.id) return;
|
||||
|
||||
const detailsPath = PATHS.SOFTWARE_TITLE_DETAILS(
|
||||
row.original.id.toString()
|
||||
);
|
||||
|
||||
router.push(getPathWithQueryParams(detailsPath, { fleet_id: teamId }));
|
||||
};
|
||||
|
||||
const renderSoftwareCount = () => {
|
||||
return (
|
||||
<>
|
||||
<TableCount name="items" count={data?.count} />
|
||||
{tableData && data?.counts_updated_at && (
|
||||
<LastUpdatedText
|
||||
lastUpdatedAt={data.counts_updated_at}
|
||||
customTooltipText={
|
||||
<>
|
||||
The last time software data was <br />
|
||||
updated, including vulnerabilities <br />
|
||||
and host counts.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomControls = () => {
|
||||
return (
|
||||
<Slider
|
||||
value={selfServiceOnly}
|
||||
onChange={handleSelfServiceToggle}
|
||||
inactiveText="Self-service only"
|
||||
activeText="Self-service only"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableHelpText = () => (
|
||||
<div>
|
||||
Seeing unexpected software?{" "}
|
||||
<CustomLink
|
||||
url={GITHUB_NEW_ISSUE_LINK}
|
||||
text="File an issue on GitHub"
|
||||
newTab
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<TableContainer
|
||||
columnConfigs={softwareTableHeaders}
|
||||
data={tableData ?? []}
|
||||
isLoading={isLoading}
|
||||
resultsTitle="items"
|
||||
emptyComponent={() => {
|
||||
if (!isSoftwareEnabled) {
|
||||
return <EmptySoftwareTable isSoftwareDisabled />;
|
||||
}
|
||||
if (query !== "" || selfServiceOnly) {
|
||||
return (
|
||||
<EmptyState
|
||||
header="No items match the current search criteria"
|
||||
info="Expecting to see software? Check back later."
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EmptyState
|
||||
header="No software available"
|
||||
info={
|
||||
canAddSoftware
|
||||
? "Add software to your library to get started."
|
||||
: "Software added to this fleet's library will appear here."
|
||||
}
|
||||
primaryButton={
|
||||
canAddSoftware ? (
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(
|
||||
getPathWithQueryParams(
|
||||
PATHS.SOFTWARE_ADD_FLEET_MAINTAINED,
|
||||
{ fleet_id: teamId }
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
Add software
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
defaultSortHeader={orderKey}
|
||||
defaultSortDirection={orderDirection}
|
||||
pageIndex={currentPage}
|
||||
defaultSearchQuery={query}
|
||||
manualSortBy
|
||||
pageSize={perPage}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
disableNextPage={!data?.meta.has_next_results}
|
||||
searchable={showFilterHeaders}
|
||||
inputPlaceHolder="Search by name"
|
||||
onQueryChange={onQueryChange}
|
||||
additionalQueries={String(selfServiceOnly)}
|
||||
customControl={showFilterHeaders ? renderCustomControls : undefined}
|
||||
stackControls
|
||||
renderCount={renderSoftwareCount}
|
||||
renderTableHelpText={renderTableHelpText}
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={handleRowSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareLibraryTable;
|
||||
Reference in New Issue
Block a user