mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fleet UI: *New* policy details page + updates to the edit policy and edit report pages (#43102)
This commit is contained in:
parent
a06415174e
commit
fe5e537b22
25 changed files with 914 additions and 824 deletions
|
|
@ -237,7 +237,7 @@
|
|||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: sub;
|
||||
vertical-align: center;
|
||||
}
|
||||
}
|
||||
.linkToFilteredHosts__header {
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ const LabelChooser = ({
|
|||
customTargetOptions = [],
|
||||
onSelectCustomTarget,
|
||||
onSelectLabel,
|
||||
disableOptions,
|
||||
}: ILabelChooserProps) => {
|
||||
const getHelpText = (value?: string) => {
|
||||
if (dropdownHelpText) return dropdownHelpText;
|
||||
|
|
@ -138,6 +139,7 @@ const LabelChooser = ({
|
|||
options={customTargetOptions}
|
||||
searchable={false}
|
||||
onChange={onSelectCustomTarget}
|
||||
disabled={disableOptions}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__description`}>
|
||||
|
|
@ -155,8 +157,10 @@ const LabelChooser = ({
|
|||
value={!!selectedLabels[label.name]}
|
||||
onChange={onSelectLabel}
|
||||
parseTarget
|
||||
/>
|
||||
<div className={`${baseClass}__label-name`}>{label.name}</div>
|
||||
disabled={disableOptions}
|
||||
>
|
||||
{label.name}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
&__checkboxes {
|
||||
display: flex;
|
||||
max-height: 187px;
|
||||
max-height: 189px; // Fits 5 options before scrolling
|
||||
flex-direction: column;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
|
|
@ -38,6 +38,6 @@
|
|||
}
|
||||
|
||||
&__label-name {
|
||||
padding-left: $pad-large;
|
||||
padding-left: $pad-small;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export type ButtonVariant =
|
|||
| "default"
|
||||
| "alert"
|
||||
| "pill"
|
||||
| "grey-pill"
|
||||
| "text-link" // Underlines on hover
|
||||
| "text-link-dark" // underline on hover, dark text
|
||||
| "brand-inverse-icon" // Green icon with text, no underline on hover
|
||||
|
|
@ -146,7 +147,8 @@ class Button extends React.Component<IButtonProps, IButtonState> {
|
|||
variant === "inverse" ||
|
||||
variant === "brand-inverse-icon" ||
|
||||
variant === "text-icon" ||
|
||||
variant === "pill";
|
||||
variant === "pill" ||
|
||||
variant === "grey-pill";
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -190,6 +190,33 @@ $base-class: "button";
|
|||
}
|
||||
}
|
||||
|
||||
&--grey-pill {
|
||||
@include button-variant(
|
||||
$core-fleet-white,
|
||||
$ui-off-white,
|
||||
null,
|
||||
$inverse: true
|
||||
);
|
||||
color: $ui-fleet-black-75;
|
||||
border: 1px solid $ui-fleet-black-25;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: $xx-small;
|
||||
font-weight: $bold;
|
||||
padding: 0 $pad-small;
|
||||
height: 28px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px solid $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: inset 2px 2px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&--text-link {
|
||||
@include button-variant(transparent);
|
||||
border: 0;
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@
|
|||
|
||||
&.is-disabled {
|
||||
> .Select-control .Select-value .Select-value-label {
|
||||
color: $ui-fleet-black-5;
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,11 +401,15 @@ describe("Edit Auto Update Config Modal", () => {
|
|||
await screen.findByLabelText("Custom");
|
||||
|
||||
// Now wait specifically for one label to appear
|
||||
const freshLabel = await screen.findByLabelText(mockLabels[1].name);
|
||||
const freshLabel = await screen.findByRole("checkbox", {
|
||||
name: mockLabels[1].name,
|
||||
});
|
||||
expect(freshLabel).toBeInTheDocument();
|
||||
expect(freshLabel).toBeChecked();
|
||||
|
||||
const funLabel = screen.getByLabelText(mockLabels[0].name);
|
||||
const funLabel = screen.getByRole("checkbox", {
|
||||
name: mockLabels[0].name,
|
||||
});
|
||||
expect(funLabel).toBeInTheDocument();
|
||||
expect(funLabel).not.toBeChecked();
|
||||
});
|
||||
|
|
@ -426,10 +430,12 @@ describe("Edit Auto Update Config Modal", () => {
|
|||
/>
|
||||
);
|
||||
// Wait for labels to load
|
||||
await screen.findByLabelText(mockLabels[1].name);
|
||||
await screen.findByRole("checkbox", { name: mockLabels[1].name });
|
||||
const customOption = screen.getByLabelText("Custom");
|
||||
expect(customOption).toBeChecked();
|
||||
const labelOption = screen.getByLabelText(mockLabels[1].name);
|
||||
const labelOption = screen.getByRole("checkbox", {
|
||||
name: mockLabels[1].name,
|
||||
});
|
||||
expect(labelOption).toBeChecked();
|
||||
await user.click(labelOption);
|
||||
expect(labelOption).not.toBeChecked();
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const generateInstallerPoliciesTableConfig = ({
|
|||
value={cellProps.cell.value}
|
||||
tooltipTruncate
|
||||
path={getPathWithQueryParams(
|
||||
PATHS.EDIT_POLICY(cellProps.row.original.id),
|
||||
PATHS.POLICY_DETAILS(cellProps.row.original.id),
|
||||
{
|
||||
fleet_id: teamId,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,8 +131,8 @@ const generateTableHeaders = (
|
|||
)}
|
||||
</>
|
||||
}
|
||||
path={getPathWithQueryParams(PATHS.EDIT_POLICY(id), {
|
||||
fleet_id: team_id,
|
||||
path={getPathWithQueryParams(PATHS.POLICY_DETAILS(id), {
|
||||
team_id,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
411
frontend/pages/policies/PolicyDetailsPage/PolicyDetailsPage.tsx
Normal file
411
frontend/pages/policies/PolicyDetailsPage/PolicyDetailsPage.tsx
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter, Params } from "react-router/lib/Router";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { noop } from "lodash";
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { IPolicy, IStoredPolicyResponse } from "interfaces/policy";
|
||||
import { ILabelPolicy } from "interfaces/label";
|
||||
import { API_ALL_TEAMS_ID, APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
|
||||
import { PLATFORM_DISPLAY_NAMES, Platform } from "interfaces/platform";
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
|
||||
import { addGravatarUrlToResource } from "utilities/helpers";
|
||||
import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants";
|
||||
import { getPathWithQueryParams } from "utilities/url";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
|
||||
import BackButton from "components/BackButton";
|
||||
import Button from "components/buttons/Button";
|
||||
import DataSet from "components/DataSet";
|
||||
import Icon from "components/Icon";
|
||||
import MainContent from "components/MainContent";
|
||||
import PageDescription from "components/PageDescription";
|
||||
import Spinner from "components/Spinner";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Avatar from "components/Avatar";
|
||||
import ShowQueryModal from "components/modals/ShowQueryModal";
|
||||
import PolicyAutomations from "pages/policies/PolicyPage/components/PolicyAutomations";
|
||||
|
||||
interface IPolicyDetailsPageProps {
|
||||
router: InjectedRouter;
|
||||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
search: string;
|
||||
query: { team_id?: string };
|
||||
};
|
||||
}
|
||||
|
||||
const baseClass = "policy-details-page";
|
||||
|
||||
const PolicyDetailsPage = ({
|
||||
router,
|
||||
params: { id: paramsPolicyId },
|
||||
location,
|
||||
}: IPolicyDetailsPageProps): JSX.Element => {
|
||||
const policyId = paramsPolicyId ? parseInt(paramsPolicyId, 10) : null;
|
||||
const handlePageError = useErrorHandler();
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isGlobalObserver,
|
||||
isGlobalTechnician,
|
||||
isOnGlobalTeam,
|
||||
config,
|
||||
currentTeam,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
lastEditedQueryResolution,
|
||||
lastEditedQueryBody,
|
||||
lastEditedQueryPlatform,
|
||||
setLastEditedQueryId,
|
||||
setLastEditedQueryName,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryBody,
|
||||
setLastEditedQueryResolution,
|
||||
setLastEditedQueryCritical,
|
||||
setLastEditedQueryPlatform,
|
||||
setLastEditedQueryLabelsIncludeAny,
|
||||
setLastEditedQueryLabelsExcludeAny,
|
||||
setPolicyTeamId,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
const {
|
||||
isRouteOk,
|
||||
teamIdForApi,
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
isTeamTechnician,
|
||||
isObserverPlus,
|
||||
} = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
includeAllTeams: true,
|
||||
includeNoTeam: true,
|
||||
permittedAccessByTeamRole: {
|
||||
admin: true,
|
||||
maintainer: true,
|
||||
observer: true,
|
||||
observer_plus: true,
|
||||
technician: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [showQueryModal, setShowQueryModal] = useState(false);
|
||||
|
||||
if (policyId === null || isNaN(policyId)) {
|
||||
router.push(PATHS.MANAGE_POLICIES);
|
||||
}
|
||||
|
||||
const { isLoading, data: storedPolicy, error: apiError } = useQuery<
|
||||
IStoredPolicyResponse,
|
||||
Error,
|
||||
IPolicy
|
||||
>(["policy", policyId], () => globalPoliciesAPI.load(policyId as number), {
|
||||
enabled: isRouteOk && !!policyId,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
select: (data: IStoredPolicyResponse) => data.policy,
|
||||
onSuccess: (returnedPolicy) => {
|
||||
setLastEditedQueryId(returnedPolicy.id);
|
||||
setLastEditedQueryName(returnedPolicy.name);
|
||||
setLastEditedQueryDescription(returnedPolicy.description);
|
||||
setLastEditedQueryBody(returnedPolicy.query);
|
||||
setLastEditedQueryResolution(returnedPolicy.resolution);
|
||||
setLastEditedQueryCritical(returnedPolicy.critical);
|
||||
setLastEditedQueryPlatform(returnedPolicy.platform);
|
||||
setLastEditedQueryLabelsIncludeAny(
|
||||
returnedPolicy.labels_include_any || []
|
||||
);
|
||||
setLastEditedQueryLabelsExcludeAny(
|
||||
returnedPolicy.labels_exclude_any || []
|
||||
);
|
||||
const deNulledTeamId = returnedPolicy.team_id ?? undefined;
|
||||
setPolicyTeamId(
|
||||
deNulledTeamId === API_ALL_TEAMS_ID
|
||||
? APP_CONTEXT_ALL_TEAMS_ID
|
||||
: deNulledTeamId
|
||||
);
|
||||
},
|
||||
onError: (error) => handlePageError(error),
|
||||
});
|
||||
|
||||
const { data: teamData } = useQuery<ILoadTeamResponse>(
|
||||
["team", teamIdForApi],
|
||||
() => teamsAPI.load(teamIdForApi as number),
|
||||
{
|
||||
enabled: !!teamIdForApi && teamIdForApi > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
let currentAutomatedPolicies: number[] = [];
|
||||
if (teamData?.team) {
|
||||
const {
|
||||
webhook_settings: { failing_policies_webhook: webhook },
|
||||
integrations,
|
||||
} = teamData.team;
|
||||
const isIntegrationEnabled =
|
||||
(integrations?.jira?.some((j: any) => j.enable_failing_policies) ||
|
||||
integrations?.zendesk?.some((z: any) => z.enable_failing_policies)) ??
|
||||
false;
|
||||
if (isIntegrationEnabled || webhook?.enable_failing_policies_webhook) {
|
||||
currentAutomatedPolicies = webhook?.policy_ids || [];
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (storedPolicy?.name) {
|
||||
document.title = `${storedPolicy.name} | Policies | ${DOCUMENT_TITLE_SUFFIX}`;
|
||||
} else {
|
||||
document.title = `Policies | ${DOCUMENT_TITLE_SUFFIX}`;
|
||||
}
|
||||
}, [location.pathname, storedPolicy?.name]);
|
||||
|
||||
const isInheritedPolicy = storedPolicy?.team_id === null;
|
||||
|
||||
const canEditPolicy =
|
||||
(isGlobalAdmin || isGlobalMaintainer || isTeamMaintainerOrTeamAdmin) &&
|
||||
// Team users cannot edit inherited (global) policies
|
||||
!(isInheritedPolicy && !isOnGlobalTeam);
|
||||
|
||||
const canRunPolicy =
|
||||
isObserverPlus ||
|
||||
isTeamMaintainerOrTeamAdmin ||
|
||||
isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isGlobalTechnician ||
|
||||
isTeamTechnician;
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
|
||||
const backToPoliciesPath = getPathWithQueryParams(PATHS.MANAGE_POLICIES, {
|
||||
team_id: teamIdForApi,
|
||||
});
|
||||
|
||||
const renderAuthor = (): JSX.Element | null => {
|
||||
if (!storedPolicy) return null;
|
||||
return (
|
||||
<DataSet
|
||||
className={`${baseClass}__author`}
|
||||
title="Author"
|
||||
value={
|
||||
<div className={`${baseClass}__author-info`}>
|
||||
<Avatar
|
||||
user={addGravatarUrlToResource({
|
||||
email: storedPolicy.author_email,
|
||||
})}
|
||||
size="xsmall"
|
||||
/>
|
||||
<span>
|
||||
{storedPolicy.author_name === currentUser?.name
|
||||
? "You"
|
||||
: storedPolicy.author_name}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPlatforms = (): JSX.Element | null => {
|
||||
if (!lastEditedQueryPlatform) return null;
|
||||
const platforms = lastEditedQueryPlatform
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter((p): p is Platform => p in PLATFORM_DISPLAY_NAMES);
|
||||
if (platforms.length === 0) return null;
|
||||
|
||||
return (
|
||||
<DataSet
|
||||
className={`${baseClass}__platforms`}
|
||||
title="Platforms"
|
||||
value={
|
||||
<div className={`${baseClass}__platform-list`}>
|
||||
{platforms.map((platform) => (
|
||||
<span key={platform} className={`${baseClass}__platform-item`}>
|
||||
<Icon name={platform} color="ui-fleet-black-75" />
|
||||
{PLATFORM_DISPLAY_NAMES[platform] || platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onLabelClick = (label: ILabelPolicy) => {
|
||||
router.push(PATHS.MANAGE_HOSTS_LABEL(label.id));
|
||||
};
|
||||
|
||||
const renderLabels = (): JSX.Element | null => {
|
||||
const includeAny = storedPolicy?.labels_include_any;
|
||||
const excludeAny = storedPolicy?.labels_exclude_any;
|
||||
|
||||
if (!includeAny?.length && !excludeAny?.length) return null;
|
||||
|
||||
const isInclude = !!includeAny?.length;
|
||||
const labels = isInclude ? includeAny : excludeAny;
|
||||
|
||||
return (
|
||||
<DataSet
|
||||
className={`${baseClass}__labels`}
|
||||
title="Labels"
|
||||
value={
|
||||
<div className={`${baseClass}__labels-section`}>
|
||||
<p>
|
||||
Policy will target hosts that{" "}
|
||||
<b>{isInclude ? "have any" : "exclude any"}</b> of these labels:
|
||||
</p>
|
||||
<ul className={`${baseClass}__labels-list`}>
|
||||
{labels?.map((label: ILabelPolicy) => (
|
||||
<li key={label.id}>
|
||||
<Button
|
||||
onClick={() => onLabelClick(label)}
|
||||
variant="grey-pill"
|
||||
className={`${baseClass}__label-pill`}
|
||||
>
|
||||
{label.name}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
<BackButton text="Back to policies" path={backToPoliciesPath} />
|
||||
</div>
|
||||
{!isLoading && !apiError && (
|
||||
<>
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className={`${baseClass}__name-description`}>
|
||||
<h1 className={`${baseClass}__policy-name`}>
|
||||
{lastEditedQueryName}
|
||||
{storedPolicy?.critical && (
|
||||
<TooltipWrapper
|
||||
tipContent="This policy has been marked as critical."
|
||||
showArrow
|
||||
underline={false}
|
||||
>
|
||||
<Icon
|
||||
className="critical-policy-icon"
|
||||
name="policy"
|
||||
color="ui-fleet-black-50"
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</h1>
|
||||
<PageDescription
|
||||
className={`${baseClass}__policy-description`}
|
||||
content={lastEditedQueryDescription}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-button-container`}>
|
||||
<Button
|
||||
className={`${baseClass}__show-query-btn`}
|
||||
onClick={() => setShowQueryModal(true)}
|
||||
variant="inverse"
|
||||
>
|
||||
Show query
|
||||
</Button>
|
||||
{canRunPolicy && (
|
||||
<Button
|
||||
className={`${baseClass}__run`}
|
||||
variant="inverse"
|
||||
onClick={() => {
|
||||
policyId &&
|
||||
router.push(
|
||||
`${getPathWithQueryParams(
|
||||
PATHS.EDIT_POLICY(policyId),
|
||||
{
|
||||
team_id: teamIdForApi,
|
||||
}
|
||||
)}#targets`
|
||||
);
|
||||
}}
|
||||
disabled={!!disabledLiveQuery}
|
||||
>
|
||||
Run policy <Icon name="run" />
|
||||
</Button>
|
||||
)}
|
||||
{canEditPolicy && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
policyId &&
|
||||
router.push(
|
||||
getPathWithQueryParams(PATHS.EDIT_POLICY(policyId), {
|
||||
team_id: teamIdForApi,
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`${baseClass}__edit-policy-btn`}
|
||||
>
|
||||
Edit policy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{lastEditedQueryResolution && (
|
||||
<DataSet
|
||||
className={`${baseClass}__resolve`}
|
||||
title="Resolve"
|
||||
value={lastEditedQueryResolution}
|
||||
/>
|
||||
)}
|
||||
{renderAuthor()}
|
||||
{currentTeam && (
|
||||
<DataSet
|
||||
className={`${baseClass}__fleet`}
|
||||
title="Fleet"
|
||||
value={currentTeam.name}
|
||||
/>
|
||||
)}
|
||||
{renderPlatforms()}
|
||||
{renderLabels()}
|
||||
{storedPolicy && (
|
||||
<PolicyAutomations
|
||||
storedPolicy={storedPolicy}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
onAddAutomation={noop}
|
||||
isAddingAutomation={false}
|
||||
gitOpsModeEnabled={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (!isRouteOk) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MainContent className={baseClass}>
|
||||
{isLoading ? <Spinner /> : renderHeader()}
|
||||
{showQueryModal && (
|
||||
<ShowQueryModal
|
||||
query={lastEditedQueryBody}
|
||||
onCancel={() => setShowQueryModal(false)}
|
||||
/>
|
||||
)}
|
||||
</MainContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default PolicyDetailsPage;
|
||||
74
frontend/pages/policies/PolicyDetailsPage/_styles.scss
Normal file
74
frontend/pages/policies/PolicyDetailsPage/_styles.scss
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
.policy-details-page {
|
||||
@include vertical-page-layout;
|
||||
|
||||
p {
|
||||
margin: 0; // Until this is an app-wide style
|
||||
}
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-xlarge;
|
||||
}
|
||||
|
||||
&__name-description,
|
||||
&__platform-list,
|
||||
&__labels-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__platform-list {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__action-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: max-content;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__policy-name {
|
||||
font-size: $large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
|
||||
.critical-policy-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Override DataSet 4px gap to 8px for all instances on this page
|
||||
.data-set {
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__platform-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
|
||||
&__labels-help-text {
|
||||
margin: 0;
|
||||
font-weight: $regular;
|
||||
}
|
||||
|
||||
&__labels-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $pad-small;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
1
frontend/pages/policies/PolicyDetailsPage/index.ts
Normal file
1
frontend/pages/policies/PolicyDetailsPage/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PolicyDetailsPage";
|
||||
|
|
@ -144,7 +144,9 @@ const PolicyPage = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
const [step, setStep] = useState(LIVE_POLICY_STEPS[1]);
|
||||
const [step, setStep] = useState(
|
||||
location.hash === "#targets" ? LIVE_POLICY_STEPS[2] : LIVE_POLICY_STEPS[1]
|
||||
);
|
||||
const [selectedTargets, setSelectedTargets] = useState<ITarget[]>([]);
|
||||
const [targetedHosts, setTargetedHosts] = useState<IHost[]>([]);
|
||||
const [targetedLabels, setTargetedLabels] = useState<ILabel[]>([]);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.policy-automations {
|
||||
max-width: 600px;
|
||||
font-size: $x-small;
|
||||
|
||||
&__cta-card {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import { createCustomRenderer, createMockRouter } from "test/test-utils";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import mockServer from "test/mock-server";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
|
@ -42,6 +42,8 @@ const labelSummariesHandler = http.get(baseUrl("/labels/summary"), () => {
|
|||
|
||||
describe("PolicyForm - component", () => {
|
||||
const defaultProps = {
|
||||
router: createMockRouter(),
|
||||
teamIdForApi: 3,
|
||||
policyIdForEdit: mockPolicy.id,
|
||||
showOpenSchemaActionText: false,
|
||||
storedPolicy: createMockPolicy({ name: "Foo" }),
|
||||
|
|
@ -127,6 +129,8 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
render(
|
||||
<PolicyForm
|
||||
router={createMockRouter()}
|
||||
teamIdForApi={3}
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ name: "" })}
|
||||
|
|
@ -190,6 +194,8 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
const { user } = render(
|
||||
<PolicyForm
|
||||
router={createMockRouter()}
|
||||
teamIdForApi={3}
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy({ platform: undefined })}
|
||||
|
|
@ -266,6 +272,8 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
const { user } = render(
|
||||
<PolicyForm
|
||||
router={createMockRouter()}
|
||||
teamIdForApi={3}
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
storedPolicy={createMockPolicy()}
|
||||
|
|
@ -360,7 +368,9 @@ describe("PolicyForm - component", () => {
|
|||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const funButton = screen.getByLabelText("Fun");
|
||||
const funButton = await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
});
|
||||
expect(funButton).not.toBeChecked();
|
||||
await userEvent.click(funButton);
|
||||
await waitFor(() => {
|
||||
|
|
@ -378,7 +388,11 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
})
|
||||
);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeEnabled();
|
||||
|
|
@ -398,7 +412,11 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
})
|
||||
);
|
||||
|
||||
// Click "Include any" to open the dropdown.
|
||||
const includeAnyOption = screen.getByRole("option", {
|
||||
|
|
@ -433,7 +451,11 @@ describe("PolicyForm - component", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
})
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText("All hosts"));
|
||||
|
||||
|
|
@ -655,53 +677,6 @@ describe("PolicyForm - component", () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'Viewing policy' when existing policy and user has no save permissions", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
context: {
|
||||
app: {
|
||||
currentUser: createMockUser(),
|
||||
currentTeam: createMockTeamSummary(),
|
||||
isGlobalObserver: true, // no save perms
|
||||
isGlobalAdmin: false,
|
||||
isGlobalMaintainer: false,
|
||||
isTeamMaintainerOrTeamAdmin: false,
|
||||
isOnGlobalTeam: true,
|
||||
isPremiumTier: true,
|
||||
isSandboxMode: false,
|
||||
isFreeTier: false,
|
||||
config: createMockConfig(),
|
||||
},
|
||||
policy: {
|
||||
policyTeamId: undefined,
|
||||
lastEditedQueryId: mockPolicy.id,
|
||||
lastEditedQueryName: mockPolicy.name,
|
||||
lastEditedQueryDescription: mockPolicy.description,
|
||||
lastEditedQueryBody: mockPolicy.query,
|
||||
lastEditedQueryResolution: mockPolicy.resolution,
|
||||
lastEditedQueryCritical: mockPolicy.critical,
|
||||
lastEditedQueryPlatform: mockPolicy.platform,
|
||||
lastEditedQueryLabelsIncludeAny: [],
|
||||
lastEditedQueryLabelsExcludeAny: [],
|
||||
defaultPolicy: false,
|
||||
setLastEditedQueryName: jest.fn(),
|
||||
setLastEditedQueryDescription: jest.fn(),
|
||||
setLastEditedQueryBody: jest.fn(),
|
||||
setLastEditedQueryResolution: jest.fn(),
|
||||
setLastEditedQueryCritical: jest.fn(),
|
||||
setLastEditedQueryPlatform: jest.fn(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<PolicyForm {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/Viewing policy for/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(createMockTeamSummary().name)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows 'Creating a new policy' when there is no existing policy", () => {
|
||||
const render = createCustomRenderer({
|
||||
withBackendMock: true,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
|
||||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||
import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
import { useQuery, useQueryClient } from "react-query";
|
||||
|
||||
import { Ace } from "ace-builds";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import { addGravatarUrlToResource } from "utilities/helpers";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import usePlatformCompatibility from "hooks/usePlatformCompatibility";
|
||||
import usePlatformSelector from "hooks/usePlatformSelector";
|
||||
import PATHS from "router/paths";
|
||||
import CUSTOM_TARGET_OPTIONS from "pages/policies/helpers";
|
||||
import { getPathWithQueryParams } from "utilities/url";
|
||||
|
||||
import { IPolicy, IPolicyFormData } from "interfaces/policy";
|
||||
import { CommaSeparatedPlatformString } from "interfaces/platform";
|
||||
|
|
@ -25,22 +26,19 @@ import {
|
|||
LEARN_MORE_ABOUT_BASE_LINK,
|
||||
} from "utilities/constants";
|
||||
|
||||
import Avatar from "components/Avatar";
|
||||
import SQLEditor from "components/SQLEditor";
|
||||
// @ts-ignore
|
||||
import { validateQuery } from "components/forms/validators/validate_query";
|
||||
import Button from "components/buttons/Button";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import PageDescription from "components/PageDescription";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
import DataSet from "components/DataSet";
|
||||
|
||||
import labelsAPI, {
|
||||
getCustomLabels,
|
||||
|
|
@ -55,6 +53,8 @@ import PolicyAutomations from "../PolicyAutomations";
|
|||
const baseClass = "policy-form";
|
||||
|
||||
interface IPolicyFormProps {
|
||||
router: InjectedRouter;
|
||||
teamIdForApi?: number;
|
||||
policyIdForEdit: number | null;
|
||||
showOpenSchemaActionText: boolean;
|
||||
storedPolicy: IPolicy | undefined;
|
||||
|
|
@ -74,6 +74,7 @@ interface IPolicyFormProps {
|
|||
onClickAutofillResolution: () => Promise<void>;
|
||||
resetAiAutofillData: () => void;
|
||||
currentAutomatedPolicies: number[];
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const validateQuerySQL = (query: string) => {
|
||||
|
|
@ -89,6 +90,8 @@ const validateQuerySQL = (query: string) => {
|
|||
};
|
||||
|
||||
const PolicyForm = ({
|
||||
router,
|
||||
teamIdForApi,
|
||||
policyIdForEdit,
|
||||
showOpenSchemaActionText,
|
||||
storedPolicy,
|
||||
|
|
@ -108,15 +111,13 @@ const PolicyForm = ({
|
|||
onClickAutofillResolution,
|
||||
resetAiAutofillData,
|
||||
currentAutomatedPolicies,
|
||||
onCancel,
|
||||
}: IPolicyFormProps): JSX.Element => {
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
||||
const [isSaveNewPolicyModalOpen, setIsSaveNewPolicyModalOpen] = useState(
|
||||
false
|
||||
);
|
||||
const [showQueryEditor, setShowQueryEditor] = useState(false);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [isEditingResolution, setIsEditingResolution] = useState(false);
|
||||
|
||||
const [selectedTargetType, setSelectedTargetType] = useState("All hosts");
|
||||
const [selectedCustomTarget, setSelectedCustomTarget] = useState(
|
||||
|
|
@ -239,12 +240,39 @@ const PolicyForm = ({
|
|||
|
||||
policyIdForEdit = policyIdForEdit || 0;
|
||||
|
||||
const isExistingPolicy = !!policyIdForEdit;
|
||||
const isEditMode = !!policyIdForEdit && !isTeamObserver && !isGlobalObserver;
|
||||
|
||||
const isNewTemplatePolicy =
|
||||
!isExistingPolicy &&
|
||||
!policyIdForEdit &&
|
||||
DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName);
|
||||
|
||||
/* - Observer/Observer+ and Technicians cannot edit existing policies
|
||||
- Team users cannot edit inherited policies
|
||||
Reroute edit existing policy page (/:policyId/edit) to policy details page (/:policyId) */
|
||||
useEffect(() => {
|
||||
const isInheritedPolicy = isEditMode && storedPolicy?.team_id === null;
|
||||
|
||||
const noEditPermissions =
|
||||
isTeamObserver ||
|
||||
isGlobalObserver ||
|
||||
isTeamTechnician ||
|
||||
isGlobalTechnician ||
|
||||
(!isOnGlobalTeam && isInheritedPolicy); // Team user viewing inherited policy
|
||||
|
||||
if (
|
||||
!isStoredPolicyLoading && // Confirms teamId for storedQuery before RBAC reroute
|
||||
policyIdForEdit &&
|
||||
policyIdForEdit > 0 &&
|
||||
noEditPermissions
|
||||
) {
|
||||
router.push(
|
||||
getPathWithQueryParams(PATHS.POLICY_DETAILS(policyIdForEdit), {
|
||||
fleet_id: teamIdForApi,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [policyIdForEdit, isTeamMaintainerOrTeamAdmin, isStoredPolicyLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTargetType(
|
||||
!lastEditedQueryLabelsIncludeAny.length &&
|
||||
|
|
@ -286,9 +314,6 @@ const PolicyForm = ({
|
|||
setCompatiblePlatforms(lastEditedQueryBody);
|
||||
}, [lastEditedQueryBody, lastEditedQueryId]);
|
||||
|
||||
const hasSavePermissions =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainerOrTeamAdmin;
|
||||
|
||||
const onLoad = (editor: Ace.Editor) => {
|
||||
editor.setOptions({
|
||||
enableLinking: true,
|
||||
|
|
@ -313,16 +338,6 @@ const PolicyForm = ({
|
|||
resetAiAutofillData(); // Allows retry of AI autofill API if the SQL has changed
|
||||
};
|
||||
|
||||
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key.toLowerCase() === "enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.blur();
|
||||
setIsEditingName(false);
|
||||
setIsEditingDescription(false);
|
||||
setIsEditingResolution(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onAddPatchAutomation = async () => {
|
||||
if (
|
||||
!storedPolicy?.patch_software?.software_title_id ||
|
||||
|
|
@ -348,21 +363,21 @@ const PolicyForm = ({
|
|||
const promptSavePolicy = () => (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
if (isExistingPolicy && !lastEditedQueryName) {
|
||||
if (isEditMode && !lastEditedQueryName) {
|
||||
return setErrors({
|
||||
...errors,
|
||||
name: "Policy name must be present",
|
||||
});
|
||||
}
|
||||
|
||||
if (isExistingPolicy && !isPatchPolicy && !isAnyPlatformSelected) {
|
||||
if (isEditMode && !isPatchPolicy && !isAnyPlatformSelected) {
|
||||
return setErrors({
|
||||
...errors,
|
||||
name: "At least one platform must be selected",
|
||||
});
|
||||
}
|
||||
|
||||
if (isPatchPolicy && isExistingPolicy) {
|
||||
if (isPatchPolicy && isEditMode) {
|
||||
// Patch policies: only send editable fields, not query/platform
|
||||
const payload: IPolicyFormData = {
|
||||
name: lastEditedQueryName,
|
||||
|
|
@ -373,14 +388,11 @@ const PolicyForm = ({
|
|||
payload.critical = lastEditedQueryCritical;
|
||||
}
|
||||
onUpdate(payload);
|
||||
setIsEditingName(false);
|
||||
setIsEditingDescription(false);
|
||||
setIsEditingResolution(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedPlatforms = getSelectedPlatforms();
|
||||
if (selectedPlatforms.length === 0 && !isExistingPolicy && !defaultPolicy) {
|
||||
if (selectedPlatforms.length === 0 && !isEditMode && !defaultPolicy) {
|
||||
// If no platforms are selected, default to all compatible platforms
|
||||
selectedPlatforms = getCompatiblePlatforms();
|
||||
setSelectedPlatforms(selectedPlatforms);
|
||||
|
|
@ -394,7 +406,7 @@ const PolicyForm = ({
|
|||
setLastEditedQueryPlatform(newPlatformString);
|
||||
}
|
||||
|
||||
if (!isExistingPolicy) {
|
||||
if (!isEditMode) {
|
||||
setIsSaveNewPolicyModalOpen(true);
|
||||
} else {
|
||||
const payload: IPolicyFormData = {
|
||||
|
|
@ -423,34 +435,6 @@ const PolicyForm = ({
|
|||
}
|
||||
onUpdate(payload);
|
||||
}
|
||||
|
||||
setIsEditingName(false);
|
||||
setIsEditingDescription(false);
|
||||
setIsEditingResolution(false);
|
||||
};
|
||||
|
||||
const renderAuthor = (): JSX.Element | null => {
|
||||
return storedPolicy ? (
|
||||
<DataSet
|
||||
className={`${baseClass}__author`}
|
||||
title="Author"
|
||||
value={
|
||||
<>
|
||||
<Avatar
|
||||
user={addGravatarUrlToResource({
|
||||
email: storedPolicy.author_email,
|
||||
})}
|
||||
size="xsmall"
|
||||
/>
|
||||
<span>
|
||||
{storedPolicy.author_name === currentUser?.name
|
||||
? "You"
|
||||
: storedPolicy.author_name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderLabelComponent = (): JSX.Element | null => {
|
||||
|
|
@ -476,88 +460,17 @@ const PolicyForm = ({
|
|||
);
|
||||
};
|
||||
|
||||
const editName = () => {
|
||||
if (!isEditingName) {
|
||||
setIsEditingName(true);
|
||||
}
|
||||
};
|
||||
|
||||
const editDescription = () => {
|
||||
if (!isEditingDescription) {
|
||||
setIsEditingDescription(true);
|
||||
}
|
||||
};
|
||||
|
||||
const editResolution = () => {
|
||||
if (!isEditingResolution) {
|
||||
setIsEditingResolution(true);
|
||||
}
|
||||
};
|
||||
|
||||
const policyNameWrapperBase = `${baseClass}__policy-name-wrapper`;
|
||||
const policyNameWrapperClasses = classnames(policyNameWrapperBase, {
|
||||
[`${baseClass}--editing`]: isEditingName,
|
||||
});
|
||||
|
||||
const policyDescriptionWrapperBase = `${baseClass}__policy-description-wrapper`;
|
||||
const policyDescriptionWrapperClasses = classnames(
|
||||
policyDescriptionWrapperBase,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingDescription,
|
||||
}
|
||||
);
|
||||
|
||||
const policyResolutionWrapperBase = `${baseClass}__policy-resolution-wrapper`;
|
||||
const policyResolutionWrapperClasses = classnames(
|
||||
policyResolutionWrapperBase,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingResolution,
|
||||
}
|
||||
);
|
||||
|
||||
const renderName = () => {
|
||||
if (isExistingPolicy) {
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(disableChildren) => {
|
||||
const classes = classnames(policyNameWrapperClasses, {
|
||||
[`${policyNameWrapperBase}--disabled-by-gitops-mode`]: disableChildren,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-name"
|
||||
placeholder="Add name here"
|
||||
value={lastEditedQueryName}
|
||||
hasError={errors && errors.name}
|
||||
inputClassName={`${baseClass}__policy-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}
|
||||
`}
|
||||
maxLength={160}
|
||||
onChange={setLastEditedQueryName}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
disableTabability={disableChildren}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`${baseClass}__edit-icon ${
|
||||
isEditingName ? `${baseClass}__edit-icon--hide` : ""
|
||||
}`}
|
||||
size="small-medium"
|
||||
color="core-fleet-green"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
<InputField
|
||||
name="policy-name"
|
||||
label="Name"
|
||||
placeholder="Add name here"
|
||||
value={lastEditedQueryName}
|
||||
error={errors && errors.name}
|
||||
onChange={(value: string) => setLastEditedQueryName(value)}
|
||||
disabled={gitOpsModeEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -572,46 +485,17 @@ const PolicyForm = ({
|
|||
};
|
||||
|
||||
const renderDescription = () => {
|
||||
if (isExistingPolicy) {
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(disableChildren) => {
|
||||
const classes = classnames(policyDescriptionWrapperClasses, {
|
||||
[`${policyDescriptionWrapperBase}--disabled-by-gitops-mode`]: disableChildren,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-description"
|
||||
placeholder="Add description here."
|
||||
value={lastEditedQueryDescription}
|
||||
inputClassName={`${baseClass}__policy-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={250}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
disableTabability={disableChildren}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`${baseClass}__edit-icon ${
|
||||
isEditingDescription ? `${baseClass}__edit-icon--hide` : ""
|
||||
}`}
|
||||
size="small-medium"
|
||||
color="core-fleet-green"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
<InputField
|
||||
name="policy-description"
|
||||
label="Description"
|
||||
placeholder="Add description here."
|
||||
value={lastEditedQueryDescription}
|
||||
type="textarea"
|
||||
helpText="How does this policy's failure put the organization at risk?"
|
||||
onChange={(value: string) => setLastEditedQueryDescription(value)}
|
||||
disabled={gitOpsModeEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -620,50 +504,18 @@ const PolicyForm = ({
|
|||
};
|
||||
|
||||
const renderResolution = () => {
|
||||
if (isExistingPolicy) {
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<div className={`form-field ${baseClass}__policy-resolve`}>
|
||||
<div className="form-field__label">Resolve</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(disableChildren) => {
|
||||
const classes = classnames(policyResolutionWrapperClasses, {
|
||||
[`${policyResolutionWrapperBase}--disabled-by-gitops-mode`]: disableChildren,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingResolution(true)}
|
||||
onBlur={() => setIsEditingResolution(false)}
|
||||
onClick={editResolution}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-resolution"
|
||||
placeholder="Add resolution here."
|
||||
value={lastEditedQueryResolution}
|
||||
inputClassName={`${baseClass}__policy-resolution ${
|
||||
!lastEditedQueryResolution ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={500}
|
||||
onChange={setLastEditedQueryResolution}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingResolution}
|
||||
disableTabability={disableChildren}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`${baseClass}__edit-icon ${
|
||||
isEditingResolution ? `${baseClass}__edit-icon--hide` : ""
|
||||
}`}
|
||||
size="small-medium"
|
||||
color="core-fleet-green"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
name="policy-resolution"
|
||||
label="Resolution"
|
||||
placeholder="Add resolution here."
|
||||
value={lastEditedQueryResolution}
|
||||
type="textarea"
|
||||
helpText="If this policy fails, what should the end user expect?"
|
||||
onChange={(value: string) => setLastEditedQueryResolution(value)}
|
||||
disabled={gitOpsModeEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -672,7 +524,7 @@ const PolicyForm = ({
|
|||
|
||||
const renderPlatformCompatibility = () => {
|
||||
if (
|
||||
isExistingPolicy &&
|
||||
isEditMode &&
|
||||
(isStoredPolicyLoading || policyIdForEdit !== lastEditedQueryId)
|
||||
) {
|
||||
return null;
|
||||
|
|
@ -711,99 +563,22 @@ const PolicyForm = ({
|
|||
const renderPolicyFleetName = () => {
|
||||
if (isFreeTier || !currentTeam?.name) return null;
|
||||
|
||||
if (isExistingPolicy) {
|
||||
return hasSavePermissions ? (
|
||||
<p>
|
||||
Editing policy for <strong>{currentTeam?.name}</strong>.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Viewing policy for <strong>{currentTeam?.name}</strong>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
return isEditMode ? (
|
||||
<p>
|
||||
Editing policy for <strong>{currentTeam?.name}</strong>.
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
Creating a new policy for <strong>{currentTeam?.name}</strong>.
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
// Non-editable form used for:
|
||||
// - Team observers and team observer+ viewing any of their team's policies and any inherited policies
|
||||
// - Team admins and team maintainers viewing any inherited policy
|
||||
// - Global observers and global observer+ viewing any team's policies and any inherited policies
|
||||
// - Global technicians and team technicians viewing any team's policies and any inherited policies
|
||||
const renderNonEditableForm = (
|
||||
<form className={`${baseClass}__wrapper`}>
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<h1
|
||||
className={`${baseClass}__policy-name ${baseClass}__policy-name--no-hover`}
|
||||
>
|
||||
{lastEditedQueryName}
|
||||
</h1>
|
||||
{renderAuthor()}
|
||||
</div>
|
||||
{renderPolicyFleetName()}
|
||||
{lastEditedQueryDescription && (
|
||||
<PageDescription
|
||||
className={`${baseClass}__policy-description no-hover`}
|
||||
content={lastEditedQueryDescription}
|
||||
/>
|
||||
)}
|
||||
{lastEditedQueryResolution && (
|
||||
<>
|
||||
<p className="resolve-title">
|
||||
<strong>Resolve:</strong>
|
||||
</p>
|
||||
<p className={`${baseClass}__policy-resolution no-hover`}>
|
||||
{lastEditedQueryResolution}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<RevealButton
|
||||
isShowing={showQueryEditor}
|
||||
className={baseClass}
|
||||
hideText="Hide SQL"
|
||||
showText="Show SQL"
|
||||
onClick={() => setShowQueryEditor(!showQueryEditor)}
|
||||
/>
|
||||
{showQueryEditor && (
|
||||
<SQLEditor
|
||||
value={lastEditedQueryBody}
|
||||
name="query editor"
|
||||
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
|
||||
wrapEnabled
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
{renderLiveQueryWarning()}
|
||||
{(isObserverPlus ||
|
||||
isTeamMaintainerOrTeamAdmin ||
|
||||
isGlobalTechnician ||
|
||||
isTeamTechnician) && ( // Team admin, team maintainer, any Observer+ and any Technician can run a policy
|
||||
<div className="button-wrap">
|
||||
<Button
|
||||
className={`${baseClass}__run`}
|
||||
onClick={goToSelectTargets}
|
||||
disabled={isExistingPolicy && !isAnyPlatformSelected}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
||||
// Editable form is used for:
|
||||
// Global admins and global maintainers
|
||||
// Team admins and team maintainers viewing any of their team's policies
|
||||
const renderEditablePolicyForm = () => {
|
||||
const renderPolicyForm = () => {
|
||||
// Save disabled for no platforms selected, query name blank on existing query, or sql errors
|
||||
const disableSaveFormErrors =
|
||||
isAddingAutomation ||
|
||||
(isExistingPolicy && !isPatchPolicy && !isAnyPlatformSelected) ||
|
||||
(isEditMode && !isPatchPolicy && !isAnyPlatformSelected) ||
|
||||
(lastEditedQueryName === "" && !!lastEditedQueryId) ||
|
||||
(selectedTargetType === "Custom" &&
|
||||
!Object.entries(selectedLabels).some(([, value]) => {
|
||||
|
|
@ -814,13 +589,57 @@ const PolicyForm = ({
|
|||
return (
|
||||
<>
|
||||
<form className={`${baseClass}__wrapper`} autoComplete="off">
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className={`${baseClass}__policy-name`}>{renderName()}</div>
|
||||
{isExistingPolicy && renderAuthor()}
|
||||
</div>
|
||||
{renderPolicyFleetName()}
|
||||
{isEditMode ? (
|
||||
<div className={`${baseClass}__page-header`}>
|
||||
<h1 className={`${baseClass}__page-title`}>Edit policy</h1>
|
||||
{renderPolicyFleetName()}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className={`${baseClass}__policy-name-fleet-name`}>
|
||||
{renderName()}
|
||||
{renderPolicyFleetName()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isEditMode && renderName()}
|
||||
{renderDescription()}
|
||||
{renderResolution()}
|
||||
{isEditMode && !isPatchPolicy && platformSelector.render()}
|
||||
{isEditMode && isPremiumTier && !isPatchPolicy && (
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={selectedTargetType}
|
||||
selectedCustomTarget={selectedCustomTarget}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
onSelectCustomTarget={setSelectedCustomTarget}
|
||||
selectedLabels={selectedLabels}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={setSelectedTargetType}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
customHelpText={
|
||||
<span className="form-field__help-text">
|
||||
Policy will target hosts on selected platforms that{" "}
|
||||
<b>have any</b> of these labels:
|
||||
</span>
|
||||
}
|
||||
disableOptions={gitOpsModeEnabled}
|
||||
suppressTitle
|
||||
/>
|
||||
)}
|
||||
{isEditMode && storedPolicy && (
|
||||
<PolicyAutomations
|
||||
storedPolicy={storedPolicy}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
onAddAutomation={onAddPatchAutomation}
|
||||
isAddingAutomation={isAddingAutomation}
|
||||
gitOpsModeEnabled={!!gitOpsModeEnabled}
|
||||
/>
|
||||
)}
|
||||
{isEditMode &&
|
||||
isPremiumTier &&
|
||||
!isPatchPolicy &&
|
||||
renderCriticalPolicy()}
|
||||
<SQLEditor
|
||||
value={lastEditedQueryBody}
|
||||
error={errors.query}
|
||||
|
|
@ -846,75 +665,47 @@ const PolicyForm = ({
|
|||
onChange={onChangePolicySql}
|
||||
handleSubmit={promptSavePolicy}
|
||||
wrapEnabled
|
||||
focus={!isExistingPolicy}
|
||||
focus={!isEditMode}
|
||||
readOnly={isPatchPolicy}
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
{isExistingPolicy && !isPatchPolicy && platformSelector.render()}
|
||||
{isExistingPolicy && isPremiumTier && !isPatchPolicy && (
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={selectedTargetType}
|
||||
selectedCustomTarget={selectedCustomTarget}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
onSelectCustomTarget={setSelectedCustomTarget}
|
||||
selectedLabels={selectedLabels}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={setSelectedTargetType}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
customHelpText={
|
||||
<span className="form-field__help-text">
|
||||
Policy will target hosts on selected platforms that{" "}
|
||||
<b>have any</b> of these labels:
|
||||
</span>
|
||||
}
|
||||
suppressTitle
|
||||
/>
|
||||
)}
|
||||
{isExistingPolicy && storedPolicy && (
|
||||
<PolicyAutomations
|
||||
storedPolicy={storedPolicy}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies}
|
||||
onAddAutomation={onAddPatchAutomation}
|
||||
isAddingAutomation={isAddingAutomation}
|
||||
gitOpsModeEnabled={!!gitOpsModeEnabled}
|
||||
/>
|
||||
)}
|
||||
{isExistingPolicy && isPremiumTier && renderCriticalPolicy()}
|
||||
{renderLiveQueryWarning()}
|
||||
<div className="button-wrap">
|
||||
{hasSavePermissions && (
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Select the platforms this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save or run the policy.
|
||||
</>
|
||||
}
|
||||
tooltipClass={`${baseClass}__button-wrap--tooltip`}
|
||||
position="top"
|
||||
disableTooltip={!isExistingPolicy || isAnyPlatformSelected}
|
||||
underline={false}
|
||||
>
|
||||
<span className={`${baseClass}__button-wrap--tooltip`}>
|
||||
<Button
|
||||
onClick={promptSavePolicy()}
|
||||
disabled={disableSaveFormErrors || disableChildren}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingPolicy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
/>
|
||||
{isEditMode && onCancel && (
|
||||
<Button variant="inverse" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Select the platforms this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save or run the policy.
|
||||
</>
|
||||
}
|
||||
tooltipClass={`${baseClass}__button-wrap--tooltip`}
|
||||
position="top"
|
||||
disableTooltip={!isEditMode || isAnyPlatformSelected}
|
||||
underline={false}
|
||||
>
|
||||
<span className={`${baseClass}__button-wrap--tooltip`}>
|
||||
<Button
|
||||
onClick={promptSavePolicy()}
|
||||
disabled={disableSaveFormErrors || disableChildren}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingPolicy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
/>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
disabledLiveQuery ? (
|
||||
|
|
@ -931,8 +722,7 @@ const PolicyForm = ({
|
|||
)
|
||||
}
|
||||
disableTooltip={
|
||||
(!isExistingPolicy || isAnyPlatformSelected) &&
|
||||
!disabledLiveQuery
|
||||
(!isEditMode || isAnyPlatformSelected) && !disabledLiveQuery
|
||||
}
|
||||
underline={false}
|
||||
showArrow
|
||||
|
|
@ -943,7 +733,7 @@ const PolicyForm = ({
|
|||
onClick={goToSelectTargets}
|
||||
disabled={
|
||||
isAddingAutomation ||
|
||||
(isExistingPolicy && !isAnyPlatformSelected) ||
|
||||
(isEditMode && !isAnyPlatformSelected) ||
|
||||
disabledLiveQuery
|
||||
}
|
||||
variant="inverse"
|
||||
|
|
@ -979,22 +769,7 @@ const PolicyForm = ({
|
|||
return <Spinner />;
|
||||
}
|
||||
|
||||
const isInheritedPolicy = isExistingPolicy && storedPolicy?.team_id === null;
|
||||
|
||||
const noEditPermissions =
|
||||
isTeamObserver ||
|
||||
isGlobalObserver ||
|
||||
isTeamTechnician ||
|
||||
isGlobalTechnician ||
|
||||
(!isOnGlobalTeam && isInheritedPolicy); // Team user viewing inherited policy
|
||||
|
||||
// Render non-editable form only
|
||||
if (noEditPermissions) {
|
||||
return renderNonEditableForm;
|
||||
}
|
||||
|
||||
// Render default editable form
|
||||
return renderEditablePolicyForm();
|
||||
return renderPolicyForm();
|
||||
};
|
||||
|
||||
export default PolicyForm;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__page-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__page-title {
|
||||
font-size: $large;
|
||||
font-weight: $bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__page-subtitle {
|
||||
font-size: $x-small;
|
||||
color: $ui-fleet-black-75;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__title-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -34,10 +52,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.input-field,
|
||||
.input-field__text-area {
|
||||
min-height: auto;
|
||||
white-space: normal;
|
||||
.input-field__textarea {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
|
|
@ -56,7 +72,7 @@
|
|||
height: 18px;
|
||||
}
|
||||
|
||||
&__policy-name,
|
||||
&__policy-name-fleet-name,
|
||||
&__description,
|
||||
&__resolution {
|
||||
.button--text-icon {
|
||||
|
|
@ -65,90 +81,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__policy-name-wrapper,
|
||||
&__policy-description-wrapper,
|
||||
&__policy-resolution-wrapper {
|
||||
&__policy-name-fleet-name {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
width: fit-content;
|
||||
|
||||
&:not(&--editing) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
* {
|
||||
color: $core-fleet-green;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--disabled-by-gitops-mode {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&__policy-name-wrapper {
|
||||
line-height: $line-height-large;
|
||||
|
||||
.no-value {
|
||||
min-width: 112px;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 0;
|
||||
/** Designed to be aligned with first line of text, not center of multiple
|
||||
lines of text, without this, the icon will vertically center on multiline */
|
||||
align-self: initial;
|
||||
height: 42px; // 24px font-size * 1.75 line height
|
||||
align-items: center; // Centers the icon in the height area
|
||||
|
||||
&--hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__policy-description-wrapper,
|
||||
&__policy-resolution-wrapper {
|
||||
.no-value {
|
||||
min-width: 106px;
|
||||
}
|
||||
.policy-form__edit-icon {
|
||||
height: 21px; // 14px font-size * 1.5 line height
|
||||
}
|
||||
}
|
||||
|
||||
&__policy-name,
|
||||
&__policy-description,
|
||||
&__policy-resolution {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
resize: none;
|
||||
white-space: normal;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__policy-name {
|
||||
font-size: $large;
|
||||
|
||||
&.input-field--error {
|
||||
border: 1px solid $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
&__autofill-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.autofill-tooltip-wrapper {
|
||||
display: flex; // Required for vertical centering
|
||||
}
|
||||
|
||||
.autofill-button-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
&--tooltip {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.policy-form__run {
|
||||
min-width: 64px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ describe("SaveNewPolicyModal", () => {
|
|||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const funButton = screen.getByLabelText("Fun");
|
||||
const funButton = await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
});
|
||||
expect(funButton).not.toBeChecked();
|
||||
await userEvent.click(funButton);
|
||||
expect(saveButton).toBeEnabled();
|
||||
|
|
@ -149,7 +151,9 @@ describe("SaveNewPolicyModal", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", { name: "Fun" })
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(onCreatePolicy.mock.calls[0][0].labels_include_any).toEqual([
|
||||
|
|
@ -175,7 +179,9 @@ describe("SaveNewPolicyModal", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", { name: "Fun" })
|
||||
);
|
||||
|
||||
// Click "Include any" to open the dropdown.
|
||||
const includeAnyOption = screen.getByRole("option", {
|
||||
|
|
|
|||
|
|
@ -259,12 +259,22 @@ const QueryEditor = ({
|
|||
);
|
||||
};
|
||||
|
||||
const backPath = policyIdForEdit
|
||||
? getPathWithQueryParams(PATHS.POLICY_DETAILS(policyIdForEdit), {
|
||||
team_id: teamIdForApi,
|
||||
})
|
||||
: backToPoliciesPath();
|
||||
|
||||
const backText = policyIdForEdit ? "Back to policy" : "Back to policies";
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__form`}>
|
||||
<div className={`${baseClass}__header-links`}>
|
||||
<BackButton text="Back to policies" path={backToPoliciesPath()} />
|
||||
<BackButton text={backText} path={backPath} />
|
||||
</div>
|
||||
<PolicyForm
|
||||
router={router}
|
||||
teamIdForApi={teamIdForApi}
|
||||
onCreatePolicy={onCreatePolicy}
|
||||
goToSelectTargets={goToSelectTargets}
|
||||
onOsqueryTableSelect={onOsqueryTableSelect}
|
||||
|
|
@ -284,6 +294,17 @@ const QueryEditor = ({
|
|||
onClickAutofillResolution={onClickAutofillResolution}
|
||||
resetAiAutofillData={() => setPolicyAutofillData(null)}
|
||||
currentAutomatedPolicies={currentAutomatedPolicies || []}
|
||||
onCancel={
|
||||
policyIdForEdit
|
||||
? () =>
|
||||
router.push(
|
||||
getPathWithQueryParams(
|
||||
PATHS.POLICY_DETAILS(policyIdForEdit),
|
||||
{ fleet_id: teamIdForApi }
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -429,8 +429,10 @@ describe("EditQueryForm - component", () => {
|
|||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeChecked();
|
||||
expect(screen.getByLabelText("Fun")).toBeChecked();
|
||||
expect(screen.getByLabelText("Fresh")).not.toBeChecked();
|
||||
expect(screen.getByRole("checkbox", { name: "Fun" })).toBeChecked();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Fresh" })
|
||||
).not.toBeChecked();
|
||||
expect(screen.getByRole("button", { name: "Save" })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
|
@ -449,9 +451,11 @@ describe("EditQueryForm - component", () => {
|
|||
expect(screen.getByLabelText("All hosts")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Custom")).toBeChecked();
|
||||
funButton = screen.getByLabelText("Fun");
|
||||
funButton = screen.getByRole("checkbox", { name: "Fun" });
|
||||
expect(funButton).toBeChecked();
|
||||
expect(screen.getByLabelText("Fresh")).not.toBeChecked();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Fresh" })
|
||||
).not.toBeChecked();
|
||||
saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React, {
|
|||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
|
|
@ -11,7 +10,6 @@ import { Location } from "history";
|
|||
import { useQuery } from "react-query";
|
||||
|
||||
import { size } from "lodash";
|
||||
import classnames from "classnames";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { Ace } from "ace-builds";
|
||||
|
||||
|
|
@ -65,7 +63,8 @@ import Slider from "components/forms/fields/Slider";
|
|||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
|
|
@ -192,6 +191,7 @@ const EditQueryForm = ({
|
|||
config,
|
||||
isPremiumTier,
|
||||
isFreeTier,
|
||||
currentTeam,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const isExistingQuery = !!queryIdForEdit;
|
||||
|
|
@ -207,8 +207,6 @@ const EditQueryForm = ({
|
|||
const [showQueryEditor, setShowQueryEditor] = useState(
|
||||
isObserverPlus || isAnyTeamObserverPlus || false
|
||||
);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [queryWasChanged, setQueryWasChanged] = useState(false);
|
||||
const [selectedTargetType, setSelectedTargetType] = useState("");
|
||||
|
|
@ -327,14 +325,6 @@ const EditQueryForm = ({
|
|||
setLastEditedQueryBody(sqlString);
|
||||
};
|
||||
|
||||
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (event.key.toLowerCase() === "enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.currentTarget.blur();
|
||||
setIsEditingName(false);
|
||||
setIsEditingDescription(false);
|
||||
}
|
||||
};
|
||||
const frequencyOptions = useMemo(
|
||||
() =>
|
||||
getCustomDropdownOptions(
|
||||
|
|
@ -413,30 +403,6 @@ const EditQueryForm = ({
|
|||
}
|
||||
};
|
||||
|
||||
const renderAuthor = (): JSX.Element | null => {
|
||||
return storedQuery ? (
|
||||
<DataSet
|
||||
className={`${baseClass}__author`}
|
||||
title="Author"
|
||||
value={
|
||||
<>
|
||||
<Avatar
|
||||
user={addGravatarUrlToResource({
|
||||
email: storedQuery.author_email,
|
||||
})}
|
||||
size="xsmall"
|
||||
/>
|
||||
<span>
|
||||
{storedQuery.author_name === currentUser?.name
|
||||
? "You"
|
||||
: storedQuery.author_name}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderLabelComponent = (): JSX.Element | null => {
|
||||
if (!showOpenSchemaActionText) {
|
||||
return null;
|
||||
|
|
@ -460,70 +426,20 @@ const EditQueryForm = ({
|
|||
return platformCompatibility.render();
|
||||
};
|
||||
|
||||
const editName = () => {
|
||||
if (!isEditingName) {
|
||||
setIsEditingName(true);
|
||||
}
|
||||
};
|
||||
|
||||
const queryNameWrapperClass = `${baseClass}__query-name-wrapper`;
|
||||
const queryNameWrapperClasses = classnames(queryNameWrapperClass, {
|
||||
[`${baseClass}--editing`]: isEditingName,
|
||||
});
|
||||
|
||||
const queryDescriptionWrapperClass = `${baseClass}__query-description-wrapper`;
|
||||
const queryDescriptionWrapperClasses = classnames(
|
||||
queryDescriptionWrapperClass,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingDescription,
|
||||
}
|
||||
);
|
||||
|
||||
const renderName = () => {
|
||||
if (isExistingQuery) {
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(disableChildren) => {
|
||||
const classes = classnames(queryNameWrapperClasses, {
|
||||
[`${queryNameWrapperClass}--disabled-by-gitops-mode`]: disableChildren,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-name"
|
||||
placeholder="Add name"
|
||||
value={lastEditedQueryName}
|
||||
inputClassName={`${baseClass}__query-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={160}
|
||||
hasError={errors && errors.name}
|
||||
onChange={setLastEditedQueryName}
|
||||
onBlur={() => {
|
||||
setLastEditedQueryName(lastEditedQueryName.trim());
|
||||
}}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
disableTabability={disableChildren}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`${baseClass}__edit-icon ${
|
||||
isEditingName ? `${baseClass}__edit-icon--hide` : ""
|
||||
}`}
|
||||
size="small-medium"
|
||||
color="core-fleet-green"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<InputField
|
||||
name="query-name"
|
||||
label="Name"
|
||||
placeholder="Add name"
|
||||
value={lastEditedQueryName}
|
||||
error={errors && errors.name}
|
||||
onChange={(value: string) => setLastEditedQueryName(value)}
|
||||
onBlur={() => {
|
||||
setLastEditedQueryName(lastEditedQueryName.trim());
|
||||
}}
|
||||
disabled={gitOpsModeEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -531,53 +447,18 @@ const EditQueryForm = ({
|
|||
return <h1 className={`${baseClass}__query-name no-hover`}>New report</h1>;
|
||||
};
|
||||
|
||||
const editDescription = () => {
|
||||
if (!isEditingDescription) {
|
||||
setIsEditingDescription(true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderDescription = () => {
|
||||
if (isExistingQuery) {
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(disableChildren) => {
|
||||
const classes = classnames(queryDescriptionWrapperClasses, {
|
||||
[`${queryDescriptionWrapperClass}--disabled-by-gitops-mode`]: disableChildren,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-description"
|
||||
placeholder="Add description"
|
||||
value={lastEditedQueryDescription}
|
||||
maxLength={250}
|
||||
inputClassName={`${baseClass}__query-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
disableTabability={disableChildren}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`${baseClass}__edit-icon ${
|
||||
isEditingDescription ? `${baseClass}__edit-icon--hide` : ""
|
||||
}`}
|
||||
size="small-medium"
|
||||
color="core-fleet-green"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
<InputField
|
||||
name="query-description"
|
||||
label="Description"
|
||||
placeholder="Add description"
|
||||
value={lastEditedQueryDescription}
|
||||
type="textarea"
|
||||
helpText="What information does your report reveal? (Optional)"
|
||||
onChange={(value: string) => setLastEditedQueryDescription(value)}
|
||||
disabled={gitOpsModeEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -616,12 +497,9 @@ const EditQueryForm = ({
|
|||
// Observers and observer+ of existing query
|
||||
const renderNonEditableForm = (
|
||||
<form className={`${baseClass}`}>
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<h1 className={`${baseClass}__query-name no-hover`}>
|
||||
{lastEditedQueryName}
|
||||
</h1>
|
||||
{renderAuthor()}
|
||||
</div>
|
||||
<h1 className={`${baseClass}__query-name no-hover`}>
|
||||
{lastEditedQueryName}
|
||||
</h1>
|
||||
{renderQueryTeam()}
|
||||
<PageDescription
|
||||
className={`${baseClass}__query-description no-hover`}
|
||||
|
|
@ -657,6 +535,7 @@ const EditQueryForm = ({
|
|||
isAnyTeamObserverPlus) && (
|
||||
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
|
||||
<TooltipWrapper
|
||||
className="live-query-button-tooltip"
|
||||
tipContent="Live reports are disabled in organization settings"
|
||||
disableTooltip={!disabledLiveQuery}
|
||||
position="top"
|
||||
|
|
@ -740,28 +619,19 @@ const EditQueryForm = ({
|
|||
return (
|
||||
<>
|
||||
<form className={baseClass} autoComplete="off">
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
{renderName()}
|
||||
{isExistingQuery && renderAuthor()}
|
||||
</div>
|
||||
{renderQueryTeam()}
|
||||
{isExistingQuery ? (
|
||||
<div className={`${baseClass}__page-header`}>
|
||||
<h1 className={`${baseClass}__page-title`}>Edit report</h1>
|
||||
{renderQueryTeam()}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__query-name-fleet-name`}>
|
||||
{renderName()}
|
||||
{renderQueryTeam()}
|
||||
</div>
|
||||
)}
|
||||
{isExistingQuery && renderName()}
|
||||
{renderDescription()}
|
||||
<SQLEditor
|
||||
value={lastEditedQueryBody}
|
||||
error={errors.query}
|
||||
label="Query"
|
||||
labelActionComponent={renderLabelComponent()}
|
||||
name="query editor"
|
||||
onLoad={onLoad}
|
||||
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
|
||||
onChange={onChangeQuery}
|
||||
handleSubmit={
|
||||
confirmChanges ? toggleConfirmSaveChangesModal : handleSaveQuery
|
||||
}
|
||||
wrapEnabled
|
||||
focus={!isExistingQuery}
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
|
||||
{isExistingQuery && (
|
||||
<div
|
||||
|
|
@ -858,6 +728,26 @@ const EditQueryForm = ({
|
|||
suppressTitle
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<SQLEditor
|
||||
value={lastEditedQueryBody}
|
||||
error={errors.query}
|
||||
label="Query"
|
||||
labelActionComponent={renderLabelComponent()}
|
||||
name="query editor"
|
||||
onLoad={onLoad}
|
||||
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
|
||||
onChange={onChangeQuery}
|
||||
handleSubmit={
|
||||
confirmChanges ? toggleConfirmSaveChangesModal : handleSaveQuery
|
||||
}
|
||||
wrapEnabled
|
||||
focus={!isExistingQuery}
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
{isExistingQuery && (
|
||||
<>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
className="advanced-options-toggle"
|
||||
|
|
@ -894,7 +784,7 @@ const EditQueryForm = ({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{renderLiveQueryWarning()}
|
||||
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
|
||||
|
|
@ -935,6 +825,7 @@ const EditQueryForm = ({
|
|||
</>
|
||||
)}
|
||||
<TooltipWrapper
|
||||
className="live-query-button-tooltip"
|
||||
tipContent="Live reports are disabled in organization settings"
|
||||
disableTooltip={!disabledLiveQuery}
|
||||
position="top"
|
||||
|
|
|
|||
|
|
@ -6,20 +6,37 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
&__title-bar {
|
||||
&__page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
&__page-title {
|
||||
font-size: $large;
|
||||
font-weight: $bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__page-subtitle {
|
||||
font-size: $x-small;
|
||||
color: $ui-fleet-black-75;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__query-name-fleet-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-small;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.input-field,
|
||||
.input-field__text-area {
|
||||
min-height: auto;
|
||||
line-height: normal;
|
||||
white-space: normal;
|
||||
.input-field__textarea {
|
||||
min-width: 100%;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
|
|
@ -33,57 +50,6 @@
|
|||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
&__query-name-wrapper,
|
||||
&__query-description-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
width: fit-content;
|
||||
&:not(.edit-query-form--editing) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
* {
|
||||
color: $core-fleet-green;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
&--disabled-by-gitops-mode {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 0;
|
||||
/** Designed to be aligned with first line of text, not center of multiple
|
||||
lines of text, without this, the icon will vertically center on multiline */
|
||||
align-self: initial;
|
||||
height: 42px; // 24px font-size * 1.75 line height
|
||||
align-items: center; // Centers the icon in the height area
|
||||
|
||||
&--hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-name-wrapper {
|
||||
.no-value {
|
||||
min-width: 168px;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-description-wrapper {
|
||||
.no-value {
|
||||
min-width: 144px;
|
||||
}
|
||||
|
||||
.edit-query-form__edit-icon {
|
||||
height: 21px; // 14px font-size * 1.5 line height
|
||||
}
|
||||
}
|
||||
|
||||
// Author section
|
||||
&__author {
|
||||
align-items: flex-end;
|
||||
|
|
@ -96,49 +62,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__query-name,
|
||||
&__query-description {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
resize: none;
|
||||
white-space: normal;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
&.focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Future iteration: move size="large" into AutoSizeInputField isntead of styling here
|
||||
&__query-name-wrapper {
|
||||
color: $core-fleet-black;
|
||||
font-size: $large;
|
||||
line-height: $line-height-large;
|
||||
|
||||
.no-value {
|
||||
min-width: 168px;
|
||||
}
|
||||
.edit-query-form__query-name,
|
||||
.input-sizer::after {
|
||||
font-size: $large;
|
||||
line-height: $line-height-large;
|
||||
}
|
||||
.component__auto-size-input-field {
|
||||
font-size: $large;
|
||||
line-height: $line-height-large;
|
||||
}
|
||||
|
||||
&.input-field--error {
|
||||
border: 1px solid $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
|
||||
&__query-description {
|
||||
color: $ui-fleet-black-75;
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
&--new-query {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -261,7 +261,9 @@ describe("SaveNewQueryModal", () => {
|
|||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
expect(saveButton).toBeDisabled();
|
||||
|
||||
const funButton = screen.getByLabelText("Fun");
|
||||
const funButton = await screen.findByRole("checkbox", {
|
||||
name: "Fun",
|
||||
});
|
||||
expect(funButton).not.toBeChecked();
|
||||
await userEvent.click(funButton);
|
||||
expect(saveButton).toBeEnabled();
|
||||
|
|
@ -280,7 +282,9 @@ describe("SaveNewQueryModal", () => {
|
|||
|
||||
// Set a label.
|
||||
await userEvent.click(screen.getByLabelText("Custom"));
|
||||
await userEvent.click(screen.getByLabelText("Fun"));
|
||||
await userEvent.click(
|
||||
await screen.findByRole("checkbox", { name: "Fun" })
|
||||
);
|
||||
await userEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
expect(saveQuery.mock.calls[0][0].labels_include_any).toEqual(["Fun"]);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import ManagePacksPage from "pages/packs/ManagePacksPage";
|
|||
import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
|
||||
import NoAccessPage from "pages/NoAccessPage";
|
||||
import PackComposerPage from "pages/packs/PackComposerPage";
|
||||
import PolicyDetailsPage from "pages/policies/PolicyDetailsPage";
|
||||
import PolicyPage from "pages/policies/PolicyPage";
|
||||
import QueryDetailsPage from "pages/queries/details/QueryDetailsPage";
|
||||
import LiveQueryPage from "pages/queries/live/LiveQueryPage";
|
||||
|
|
@ -411,7 +412,10 @@ const routes = (
|
|||
<Route component={AuthAnyMaintainerAnyAdminRoutes}>
|
||||
<Route path="new" component={PolicyPage} />
|
||||
</Route>
|
||||
<Route path=":id" component={PolicyPage} />
|
||||
<Route path=":id">
|
||||
<IndexRoute component={PolicyDetailsPage} />
|
||||
<Route path="edit" component={PolicyPage} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Redirect from="profile" to="account" /> {/* deprecated URL */}
|
||||
<Route path="account" component={AccountPage} />
|
||||
|
|
|
|||
|
|
@ -124,8 +124,10 @@ export default {
|
|||
`${URL_PREFIX}/reports/${queryId || "new"}/live`,
|
||||
REPORT_DETAILS: (queryId: number): string =>
|
||||
`${URL_PREFIX}/reports/${queryId}`,
|
||||
EDIT_POLICY: (policyId: number): string =>
|
||||
POLICY_DETAILS: (policyId: number): string =>
|
||||
`${URL_PREFIX}/policies/${policyId}`,
|
||||
EDIT_POLICY: (policyId: number): string =>
|
||||
`${URL_PREFIX}/policies/${policyId}/edit`,
|
||||
FORGOT_PASSWORD: `${URL_PREFIX}/login/forgot`,
|
||||
MFA: `${URL_PREFIX}/login/mfa`,
|
||||
NO_ACCESS: `${URL_PREFIX}/login/denied`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue