fleet/frontend/pages/SoftwarePage/SoftwarePage.tsx
Jacob Shandling a10aac29c6
UI – Calendar events modal follow up (#17788)
## Follow-up work to #17717 

**Finalize disabled options and tooltips:**
<img width="697" alt="Screenshot 2024-03-21 at 5 14 40 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/ea5d880f-75f6-48ef-85cc-b807812c9a50">
<img width="697" alt="Screenshot 2024-03-21 at 5 15 13 PM"
src="https://github.com/fleetdm/fleet/assets/61553566/bdd33118-933e-4676-9e1e-680ebcddbc7a">

**Only update policies and settings when there's a diff:**

![1(1)](https://github.com/fleetdm/fleet/assets/61553566/183d1834-3c54-4fef-a208-dfbb0354e507)

**Reorganize onChange handlers, types**

- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-03-26 13:39:37 -05:00

394 lines
12 KiB
TypeScript

import React, { useCallback, useContext, useState } from "react";
import { InjectedRouter } from "react-router";
import { useQuery } from "react-query";
import { Tab, TabList, Tabs } from "react-tabs";
import PATHS from "router/paths";
import {
IConfig,
CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS,
} from "interfaces/config";
import {
IJiraIntegration,
IZendeskIntegration,
IZendeskJiraIntegrations,
} from "interfaces/integration";
import { ITeamConfig } from "interfaces/team";
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
import configAPI from "services/entities/config";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import { AppContext } from "context/app";
import { NotificationContext } from "context/notification";
import useTeamIdParam from "hooks/useTeamIdParam";
import { buildQueryStringFromParams } from "utilities/url";
import Button from "components/buttons/Button";
import MainContent from "components/MainContent";
import TeamsHeader from "components/TeamsHeader";
import TabsWrapper from "components/TabsWrapper";
import ManageAutomationsModal from "./components/ManageSoftwareAutomationsModal";
interface ISoftwareSubNavItem {
name: string;
pathname: string;
}
const softwareSubNav: ISoftwareSubNavItem[] = [
{
name: "Software",
pathname: PATHS.SOFTWARE_TITLES,
},
{
name: "OS",
pathname: PATHS.SOFTWARE_OS,
},
{
name: "Vulnerabilities",
pathname: PATHS.SOFTWARE_VULNERABILITIES,
},
];
const getTabIndex = (path: string): number => {
return softwareSubNav.findIndex((navItem) => {
// This check ensures that for software versions path we still
// highlight the software tab.
if (navItem.name === "Software" && PATHS.SOFTWARE_VERSIONS === path) {
return true;
}
// tab stays highlighted for paths that start with same pathname
return path.startsWith(navItem.pathname);
});
};
// default values for query params used on this page if not provided
const DEFAULT_SORT_DIRECTION = "desc";
const DEFAULT_SORT_HEADER = "hosts_count";
const DEFAULT_PAGE_SIZE = 20;
const DEFAULT_PAGE = 0;
const baseClass = "software-page";
interface ISoftwareAutomations {
webhook_settings: {
vulnerabilities_webhook: IWebhookSoftwareVulnerabilities;
};
integrations: {
jira: IJiraIntegration[];
zendesk: IZendeskIntegration[];
};
}
interface ISoftwareConfigQueryKey {
scope: string;
teamId?: number;
}
interface ISoftwarePageProps {
children: JSX.Element;
location: {
pathname: string;
search: string;
query: {
team_id?: string;
vulnerable?: string;
exploit?: string;
page?: string;
query?: string;
order_key?: string;
order_direction?: "asc" | "desc";
};
hash?: string;
};
router: InjectedRouter; // v3
}
const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const {
config: globalConfig,
isFreeTier,
isGlobalAdmin,
isGlobalMaintainer,
isOnGlobalTeam,
isPremiumTier,
isSandboxMode,
} = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const queryParams = location.query;
// initial values for query params used on this page
const sortHeader =
queryParams && queryParams.order_key
? queryParams.order_key
: DEFAULT_SORT_HEADER;
const sortDirection =
queryParams?.order_direction === undefined
? DEFAULT_SORT_DIRECTION
: queryParams.order_direction;
const page =
queryParams && queryParams.page
? parseInt(queryParams.page, 10)
: DEFAULT_PAGE;
// TODO: move these down into the Software Titles component.
const query = queryParams && queryParams.query ? queryParams.query : "";
const showVulnerableSoftware =
queryParams !== undefined && queryParams.vulnerable === "true";
const showExploitedVulnerabilitiesOnly =
queryParams !== undefined && queryParams.exploit === "true";
const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
false
);
const [showPreviewPayloadModal, setShowPreviewPayloadModal] = useState(false);
const [showPreviewTicketModal, setShowPreviewTicketModal] = useState(false);
const {
currentTeamId,
isAnyTeamSelected,
isRouteOk,
teamIdForApi,
userTeams,
handleTeamChange,
} = useTeamIdParam({
location,
router,
includeAllTeams: true,
includeNoTeam: false,
});
// softwareConfig is either the global config or the team config of the
// currently selected team depending on the page team context selected
// by the user.
const {
data: softwareConfig,
error: softwareConfigError,
isFetching: isFetchingSoftwareConfig,
refetch: refetchSoftwareConfig,
} = useQuery<
IConfig | ILoadTeamResponse,
Error,
IConfig | ITeamConfig,
ISoftwareConfigQueryKey[]
>(
[{ scope: "softwareConfig", teamId: teamIdForApi }],
({ queryKey }) => {
const { teamId } = queryKey[0];
return teamId ? teamsAPI.load(teamId) : configAPI.loadAll();
},
{
enabled: isRouteOk,
select: (data) => ("team" in data ? data.team : data),
}
);
// TODO: move into manage automations modal
const vulnWebhookSettings =
softwareConfig?.webhook_settings?.vulnerabilities_webhook;
const isVulnWebhookEnabled = !!vulnWebhookSettings?.enable_vulnerabilities_webhook;
const isVulnIntegrationEnabled = (
integrations?: IZendeskJiraIntegrations
) => {
return (
!!integrations?.jira?.some((j) => j.enable_software_vulnerabilities) ||
!!integrations?.zendesk?.some((z) => z.enable_software_vulnerabilities)
);
};
// TODO: move into manage automations modal
const isAnyVulnAutomationEnabled =
isVulnWebhookEnabled ||
isVulnIntegrationEnabled(softwareConfig?.integrations);
// TODO: move into manage automations modal
const recentVulnerabilityMaxAge = (() => {
let maxAgeInNanoseconds: number | undefined;
if (softwareConfig && "vulnerabilities" in softwareConfig) {
maxAgeInNanoseconds =
softwareConfig.vulnerabilities.recent_vulnerability_max_age;
} else {
maxAgeInNanoseconds =
globalConfig?.vulnerabilities.recent_vulnerability_max_age;
}
return maxAgeInNanoseconds
? Math.round(maxAgeInNanoseconds / 86400000000000) // convert from nanoseconds to days
: CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS;
})();
const isSoftwareConfigLoaded =
!isFetchingSoftwareConfig && !softwareConfigError && !!softwareConfig;
const canManageAutomations =
isGlobalAdmin && (!isPremiumTier || !isAnyTeamSelected);
const toggleManageAutomationsModal = useCallback(() => {
setShowManageAutomationsModal(!showManageAutomationsModal);
}, [setShowManageAutomationsModal, showManageAutomationsModal]);
const togglePreviewPayloadModal = useCallback(() => {
setShowPreviewPayloadModal(!showPreviewPayloadModal);
}, [setShowPreviewPayloadModal, showPreviewPayloadModal]);
const togglePreviewTicketModal = useCallback(() => {
setShowPreviewTicketModal(!showPreviewTicketModal);
}, [setShowPreviewTicketModal, showPreviewTicketModal]);
// TODO: move into manage automations modal
const onCreateWebhookSubmit = async (
configSoftwareAutomations: ISoftwareAutomations
) => {
try {
const request = configAPI.update(configSoftwareAutomations);
await request.then(() => {
renderFlash(
"success",
"Successfully updated vulnerability automations."
);
refetchSoftwareConfig();
});
} catch {
renderFlash(
"error",
"Could not update vulnerability automations. Please try again."
);
} finally {
toggleManageAutomationsModal();
}
};
const onTeamChange = useCallback(
(teamId: number) => {
handleTeamChange(teamId);
// TODO: reset page to 0 when changing teams
},
[handleTeamChange]
);
const navigateToNav = useCallback(
(i: number): void => {
// Only query param to persist between tabs is team id
const teamIdParam = buildQueryStringFromParams({
team_id: location?.query.team_id,
});
const navPath = softwareSubNav[i].pathname.concat(`?${teamIdParam}`);
router.replace(navPath);
},
[location, router]
);
const renderTitle = () => {
return (
<>
{isFreeTier && <h1>Software</h1>}
{isPremiumTier && (
<TeamsHeader
isOnGlobalTeam={isOnGlobalTeam}
currentTeamId={currentTeamId}
userTeams={userTeams}
onTeamChange={onTeamChange}
isSandboxMode={isSandboxMode}
/>
)}
</>
);
};
const renderHeaderDescription = () => {
return (
<p>
Search for installed software{" "}
{(isGlobalAdmin || isGlobalMaintainer) &&
(!isPremiumTier || !isAnyTeamSelected) &&
"and manage automations for detected vulnerabilities (CVEs)"}{" "}
on{" "}
{isPremiumTier && isAnyTeamSelected
? "all hosts assigned to this team"
: "all of your hosts"}
.
</p>
);
};
const renderBody = () => {
return (
<div>
<TabsWrapper>
<Tabs
selectedIndex={getTabIndex(location?.pathname || "")}
onSelect={navigateToNav}
>
<TabList>
{softwareSubNav.map((navItem) => {
return (
<Tab key={navItem.name} data-text={navItem.name}>
{navItem.name}
</Tab>
);
})}
</TabList>
</Tabs>
</TabsWrapper>
{React.cloneElement(children, {
router,
isSoftwareEnabled: Boolean(
softwareConfig?.features?.enable_software_inventory
),
perPage: DEFAULT_PAGE_SIZE,
orderDirection: sortDirection,
orderKey: sortHeader,
currentPage: page,
teamId: teamIdForApi,
// TODO: move down into the Software Titles component
query,
showVulnerableSoftware,
showExploitedVulnerabilitiesOnly,
})}
</div>
);
};
return (
<MainContent>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>{renderTitle()}</div>
</div>
</div>
{canManageAutomations && isSoftwareConfigLoaded && (
<Button
onClick={toggleManageAutomationsModal}
className={`${baseClass}__manage-automations button`}
variant="brand"
>
<span>Manage automations</span>
</Button>
)}
</div>
<div className={`${baseClass}__description`}>
{renderHeaderDescription()}
</div>
{renderBody()}
{showManageAutomationsModal && (
<ManageAutomationsModal
onCancel={toggleManageAutomationsModal}
onCreateWebhookSubmit={onCreateWebhookSubmit}
togglePreviewPayloadModal={togglePreviewPayloadModal}
togglePreviewTicketModal={togglePreviewTicketModal}
showPreviewPayloadModal={showPreviewPayloadModal}
showPreviewTicketModal={showPreviewTicketModal}
softwareVulnerabilityAutomationEnabled={isAnyVulnAutomationEnabled}
softwareVulnerabilityWebhookEnabled={isVulnWebhookEnabled}
currentDestinationUrl={vulnWebhookSettings?.destination_url || ""}
recentVulnerabilityMaxAge={recentVulnerabilityMaxAge}
/>
)}
</div>
</MainContent>
);
};
export default SoftwarePage;