Add more loading spinners to async actions (#5742)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Add more loading spinners while asynchronous actions occur
|
||||
@@ -9,7 +9,7 @@ import configAPI from "services/entities/config";
|
||||
import deepDifference from "utilities/deep_difference";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
|
||||
import Spinner from "components/Spinner";
|
||||
import PATHS from "router/paths";
|
||||
import Info from "./cards/Info";
|
||||
import WebAddress from "./cards/WebAddress";
|
||||
@@ -34,11 +34,11 @@ const AppSettingsPage = ({
|
||||
|
||||
const [activeSection, setActiveSection] = useState<string>("info");
|
||||
|
||||
const {
|
||||
data: appConfig,
|
||||
isLoading: isLoadingConfig,
|
||||
refetch: refetchConfig,
|
||||
} = useQuery<IConfig, Error, IConfig>(["config"], () => configAPI.loadAll(), {
|
||||
const { data: appConfig, isLoading, refetch: refetchConfig } = useQuery<
|
||||
IConfig,
|
||||
Error,
|
||||
IConfig
|
||||
>(["config"], () => configAPI.loadAll(), {
|
||||
select: (data: IConfig) => data,
|
||||
onSuccess: (data) => {
|
||||
setConfig(data);
|
||||
@@ -93,7 +93,7 @@ const AppSettingsPage = ({
|
||||
}, [sectionTitle]);
|
||||
|
||||
const renderSection = () => {
|
||||
if (!isLoadingConfig && appConfig) {
|
||||
if (!isLoading && appConfig) {
|
||||
return (
|
||||
<>
|
||||
{activeSection === "info" && (
|
||||
@@ -135,88 +135,94 @@ const AppSettingsPage = ({
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Set your organization information and configure SSO and SMTP
|
||||
</p>
|
||||
<div className={`${baseClass}__settings-form`}>
|
||||
<nav>
|
||||
<ul className={`${baseClass}__form-nav-list`}>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive("info")}
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={`${baseClass}__settings-form`}>
|
||||
<nav>
|
||||
<ul className={`${baseClass}__form-nav-list`}>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive("info")}
|
||||
}`}
|
||||
to={PATHS.ADMIN_SETTINGS_INFO}
|
||||
>
|
||||
Organization info
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"webaddress"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_WEBADDRESS}
|
||||
>
|
||||
Fleet web address
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive("sso")}`}
|
||||
to={PATHS.ADMIN_SETTINGS_SSO}
|
||||
>
|
||||
Single sign-on options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link$ ${isNavItemActive("smtp")}`}
|
||||
to={PATHS.ADMIN_SETTINGS_SMTP}
|
||||
>
|
||||
SMTP options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"agents"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_AGENTS}
|
||||
>
|
||||
Global agent options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"host-status-webhook"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_HOST_STATUS_WEBHOOK}
|
||||
>
|
||||
Host status webhook
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"statistics"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_STATISTICS}
|
||||
>
|
||||
Usage statistics
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"advanced"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_ADVANCED}
|
||||
>
|
||||
Advanced options
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{renderSection()}
|
||||
</div>
|
||||
to={PATHS.ADMIN_SETTINGS_INFO}
|
||||
>
|
||||
Organization info
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"webaddress"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_WEBADDRESS}
|
||||
>
|
||||
Fleet web address
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive("sso")}`}
|
||||
to={PATHS.ADMIN_SETTINGS_SSO}
|
||||
>
|
||||
Single sign-on options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link$ ${isNavItemActive(
|
||||
"smtp"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_SMTP}
|
||||
>
|
||||
SMTP options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"agents"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_AGENTS}
|
||||
>
|
||||
Global agent options
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"host-status-webhook"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_HOST_STATUS_WEBHOOK}
|
||||
>
|
||||
Host status webhook
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"statistics"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_STATISTICS}
|
||||
>
|
||||
Usage statistics
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className={`${baseClass}__nav-link ${isNavItemActive(
|
||||
"advanced"
|
||||
)}`}
|
||||
to={PATHS.ADMIN_SETTINGS_ADVANCED}
|
||||
>
|
||||
Advanced options
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{renderSection()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-7
@@ -70,7 +70,7 @@ const MembersPage = ({
|
||||
const [showCreateUserModal, setShowCreateUserModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [userEditing, setUserEditing] = useState<IUser>();
|
||||
const [searchString, setSearchString] = useState<string>("");
|
||||
const [createUserErrors, setCreateUserErrors] = useState<IUserFormErrors>(
|
||||
@@ -147,6 +147,7 @@ const MembersPage = ({
|
||||
|
||||
const onRemoveMemberSubmit = useCallback(() => {
|
||||
const removedUsers = { users: [{ id: userEditing?.id }] };
|
||||
setIsLoading(true);
|
||||
teamsAPI
|
||||
.removeMembers(teamId, removedUsers)
|
||||
.then(() => {
|
||||
@@ -163,6 +164,7 @@ const MembersPage = ({
|
||||
renderFlash("error", "Unable to remove members. Please try again.")
|
||||
)
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
toggleRemoveMemberModal();
|
||||
refetchUsers();
|
||||
});
|
||||
@@ -199,7 +201,7 @@ const MembersPage = ({
|
||||
);
|
||||
|
||||
const onCreateMemberSubmit = (formData: IFormData) => {
|
||||
setIsFormSubmitting(true);
|
||||
setIsLoading(true);
|
||||
|
||||
if (formData.newUserType === NewUserType.AdminInvited) {
|
||||
const requestData = {
|
||||
@@ -240,7 +242,7 @@ const MembersPage = ({
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
const requestData = {
|
||||
@@ -271,7 +273,7 @@ const MembersPage = ({
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -283,7 +285,7 @@ const MembersPage = ({
|
||||
formData
|
||||
);
|
||||
|
||||
setIsFormSubmitting(true);
|
||||
setIsLoading(true);
|
||||
|
||||
const userName = userEditing?.name;
|
||||
|
||||
@@ -312,7 +314,7 @@ const MembersPage = ({
|
||||
} else {
|
||||
refetchUsers();
|
||||
}
|
||||
setIsFormSubmitting(false);
|
||||
setIsLoading(false);
|
||||
toggleEditMemberModal();
|
||||
})
|
||||
.catch((userErrors: { data: IApiError }) => {
|
||||
@@ -453,6 +455,7 @@ const MembersPage = ({
|
||||
isSsoEnabled={userEditing?.sso_enabled}
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
currentTeam={currentTeam}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
{showCreateUserModal && (
|
||||
@@ -469,13 +472,14 @@ const MembersPage = ({
|
||||
canUseSso={canUseSso}
|
||||
currentTeam={currentTeam}
|
||||
isModifiedByGlobalAdmin={isGlobalAdmin}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
{showRemoveMemberModal && currentTeam && (
|
||||
<RemoveMemberModal
|
||||
memberName={userEditing?.name || ""}
|
||||
teamName={currentTeam.name}
|
||||
isLoading={isLoading}
|
||||
onCancel={toggleRemoveMemberModal}
|
||||
onSubmit={onRemoveMemberSubmit}
|
||||
/>
|
||||
|
||||
+35
-28
@@ -2,12 +2,14 @@ import React, { useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "remove-member-modal";
|
||||
|
||||
interface IDeleteTeamModalProps {
|
||||
memberName: string;
|
||||
teamName: string;
|
||||
isLoading: boolean;
|
||||
onSubmit: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -15,6 +17,7 @@ interface IDeleteTeamModalProps {
|
||||
const RemoveMemberModal = ({
|
||||
memberName,
|
||||
teamName,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: IDeleteTeamModalProps): JSX.Element => {
|
||||
@@ -33,34 +36,38 @@ const RemoveMemberModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Remove team member"} onExit={onCancel} className={baseClass}>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p>
|
||||
You are about to remove{" "}
|
||||
<span className={`${baseClass}__name`}>{memberName}</span> from{" "}
|
||||
<span className={`${baseClass}__team-name`}>{teamName}</span>.
|
||||
</p>
|
||||
<p>
|
||||
If {memberName} is not a member of any other team, they will lose
|
||||
access to Fleet.
|
||||
</p>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse-alert"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p>
|
||||
You are about to remove{" "}
|
||||
<span className={`${baseClass}__name`}>{memberName}</span> from{" "}
|
||||
<span className={`${baseClass}__team-name`}>{teamName}</span>.
|
||||
</p>
|
||||
<p>
|
||||
If {memberName} is not a member of any other team, they will lose
|
||||
access to Fleet.
|
||||
</p>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse-alert"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -129,6 +129,8 @@ const TeamDetailsWrapper = ({
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const [teamIsRemoving, setTeamIsRemoving] = useState<boolean>(false);
|
||||
const [teamIsEditing, setTeamIsEditing] = useState<boolean>(false);
|
||||
|
||||
const { refetch: refetchMe } = useQuery(["me"], () => usersAPI.me(), {
|
||||
enabled: false,
|
||||
@@ -284,7 +286,7 @@ const TeamDetailsWrapper = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleDeleteTeamModal();
|
||||
setTeamIsRemoving(true);
|
||||
|
||||
try {
|
||||
await teamsAPI.destroy(currentTeam.id);
|
||||
@@ -294,6 +296,9 @@ const TeamDetailsWrapper = ({
|
||||
renderFlash("error", "Something went wrong removing the team");
|
||||
console.error(response);
|
||||
return false;
|
||||
} finally {
|
||||
toggleDeleteTeamModal();
|
||||
setTeamIsRemoving(false);
|
||||
}
|
||||
}, [toggleDeleteTeamModal, currentTeam?.id]);
|
||||
|
||||
@@ -312,6 +317,8 @@ const TeamDetailsWrapper = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setTeamIsEditing(true);
|
||||
|
||||
try {
|
||||
await teamsAPI.update(updatedAttrs, currentTeam.id);
|
||||
await teamsAPI.loadAll({ perPage: 500 });
|
||||
@@ -338,6 +345,8 @@ const TeamDetailsWrapper = ({
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
setTeamIsEditing(false);
|
||||
}
|
||||
},
|
||||
[toggleEditTeamModal, currentTeam, setBackendValidators]
|
||||
@@ -519,6 +528,7 @@ const TeamDetailsWrapper = ({
|
||||
onCancel={toggleDeleteTeamModal}
|
||||
onSubmit={onDeleteSubmit}
|
||||
name={currentTeam.name}
|
||||
isLoading={teamIsRemoving}
|
||||
/>
|
||||
)}
|
||||
{showEditTeamModal && (
|
||||
@@ -527,6 +537,7 @@ const TeamDetailsWrapper = ({
|
||||
onSubmit={onEditSubmit}
|
||||
defaultName={currentTeam.name}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={teamIsEditing}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -28,6 +28,9 @@ const noTeamsClass = "no-teams";
|
||||
|
||||
const TeamManagementPage = (): JSX.Element => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [teamIsLoading, setTeamIsLoading] = useState<boolean>(false);
|
||||
const [teamIsEditing, setTeamIsEditing] = useState<boolean>(false);
|
||||
const [teamIsRemoving, setTeamIsRemoving] = useState<boolean>(false);
|
||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||
const [showDeleteTeamModal, setShowDeleteTeamModal] = useState(false);
|
||||
const [showEditTeamModal, setShowEditTeamModal] = useState(false);
|
||||
@@ -40,7 +43,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
|
||||
const {
|
||||
data: teams,
|
||||
isLoading: isLoadingTeams,
|
||||
isFetching: isFetchingTeams,
|
||||
error: loadingTeamsError,
|
||||
refetch: refetchTeams,
|
||||
} = useQuery<ITeamsResponse, Error, ITeam[]>(
|
||||
@@ -96,6 +99,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
|
||||
const onCreateSubmit = useCallback(
|
||||
(formData: ICreateTeamFormData) => {
|
||||
setTeamIsLoading(true);
|
||||
teamsAPI
|
||||
.create(formData)
|
||||
.then(() => {
|
||||
@@ -113,6 +117,9 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
renderFlash("error", "Could not create team. Please try again.");
|
||||
toggleCreateTeamModal();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setTeamIsLoading(false);
|
||||
});
|
||||
},
|
||||
[toggleCreateTeamModal]
|
||||
@@ -120,6 +127,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
|
||||
const onDeleteSubmit = useCallback(() => {
|
||||
if (teamEditing) {
|
||||
setTeamIsRemoving(true);
|
||||
teamsAPI
|
||||
.destroy(teamEditing.id)
|
||||
.then(() => {
|
||||
@@ -132,6 +140,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setTeamIsRemoving(true);
|
||||
refetchTeams();
|
||||
toggleDeleteTeamModal();
|
||||
});
|
||||
@@ -143,6 +152,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
if (formData.name === teamEditing?.name) {
|
||||
toggleEditTeamModal();
|
||||
} else if (teamEditing) {
|
||||
setTeamIsEditing(true);
|
||||
teamsAPI
|
||||
.update(formData, teamEditing.id)
|
||||
.then(() => {
|
||||
@@ -166,6 +176,9 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
`Could not edit ${teamEditing.name}. Please try again.`
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setTeamIsEditing(false);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -232,7 +245,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
<TableContainer
|
||||
columns={tableHeaders}
|
||||
data={tableData}
|
||||
isLoading={isLoadingTeams}
|
||||
isLoading={isFetchingTeams}
|
||||
defaultSortHeader={"name"}
|
||||
defaultSortDirection={"asc"}
|
||||
inputPlaceHolder={"Search"}
|
||||
@@ -254,6 +267,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
onCancel={toggleCreateTeamModal}
|
||||
onSubmit={onCreateSubmit}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={teamIsLoading}
|
||||
/>
|
||||
)}
|
||||
{showDeleteTeamModal && (
|
||||
@@ -261,6 +275,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
onCancel={toggleDeleteTeamModal}
|
||||
onSubmit={onDeleteSubmit}
|
||||
name={teamEditing?.name || ""}
|
||||
isLoading={teamIsRemoving}
|
||||
/>
|
||||
)}
|
||||
{showEditTeamModal && (
|
||||
@@ -269,6 +284,7 @@ const TeamManagementPage = (): JSX.Element => {
|
||||
onSubmit={onEditSubmit}
|
||||
defaultName={teamEditing?.name || ""}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={teamIsEditing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+40
-33
@@ -5,6 +5,7 @@ import Button from "components/buttons/Button";
|
||||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "create-team-modal";
|
||||
|
||||
@@ -16,12 +17,14 @@ interface ICreateTeamModalProps {
|
||||
onCancel: () => void;
|
||||
onSubmit: (formData: ICreateTeamFormData) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const CreateTeamModal = ({
|
||||
onCancel,
|
||||
onSubmit,
|
||||
backendValidators,
|
||||
isLoading,
|
||||
}: ICreateTeamModalProps): JSX.Element => {
|
||||
const [name, setName] = useState("");
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
@@ -52,39 +55,43 @@ const CreateTeamModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Create team"} onExit={onCancel} className={baseClass}>
|
||||
<form
|
||||
className={`${baseClass}__form`}
|
||||
onSubmit={onFormSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
autofocus
|
||||
name="name"
|
||||
onChange={onInputChange}
|
||||
label="Team name"
|
||||
placeholder="Team name"
|
||||
value={name}
|
||||
error={errors.name}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p className={`${baseClass}__info-header`}>
|
||||
Need to test queries and configurations before deploying?
|
||||
</p>
|
||||
<p>
|
||||
A popular pattern is to end a team’s name with “- Sandbox”, then you
|
||||
can use this to test new queries and configuration with staging
|
||||
hosts or volunteers acting as canaries.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="brand" disabled={name === ""}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
className={`${baseClass}__form`}
|
||||
onSubmit={onFormSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
autofocus
|
||||
name="name"
|
||||
onChange={onInputChange}
|
||||
label="Team name"
|
||||
placeholder="Team name"
|
||||
value={name}
|
||||
error={errors.name}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p className={`${baseClass}__info-header`}>
|
||||
Need to test queries and configurations before deploying?
|
||||
</p>
|
||||
<p>
|
||||
A popular pattern is to end a team’s name with “- Sandbox”, then
|
||||
you can use this to test new queries and configuration with
|
||||
staging hosts or volunteers acting as canaries.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="brand" disabled={name === ""}>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+28
-19
@@ -2,17 +2,20 @@ import React, { useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "delete-team-modal";
|
||||
|
||||
interface IDeleteTeamModalProps {
|
||||
name: string;
|
||||
isLoading: boolean;
|
||||
onSubmit: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const DeleteTeamModal = ({
|
||||
name,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: IDeleteTeamModalProps): JSX.Element => {
|
||||
@@ -31,25 +34,31 @@ const DeleteTeamModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Delete team"} onExit={onCancel} className={baseClass}>
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p>
|
||||
You are about to delete{" "}
|
||||
<span className={`${baseClass}__name`}>{name}</span> from Fleet.
|
||||
</p>
|
||||
<p>
|
||||
Members of this team who are not members of other teams will lose
|
||||
access to Fleet.
|
||||
</p>
|
||||
<p className={`${baseClass}__warning`}>This action cannot be undone.</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="button" onClick={onSubmit} variant="alert">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form className={`${baseClass}__form`}>
|
||||
<p>
|
||||
You are about to delete{" "}
|
||||
<span className={`${baseClass}__name`}>{name}</span> from Fleet.
|
||||
</p>
|
||||
<p>
|
||||
Members of this team who are not members of other teams will lose
|
||||
access to Fleet.
|
||||
</p>
|
||||
<p className={`${baseClass}__warning`}>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="button" onClick={onSubmit} variant="alert">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+39
-32
@@ -4,6 +4,7 @@ import Modal from "components/Modal";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "edit-team-modal";
|
||||
|
||||
@@ -16,6 +17,7 @@ interface IEditTeamModalProps {
|
||||
onSubmit: (formData: IEditTeamFormData) => void;
|
||||
defaultName: string;
|
||||
backendValidators: { [key: string]: string };
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const EditTeamModal = ({
|
||||
@@ -23,6 +25,7 @@ const EditTeamModal = ({
|
||||
onSubmit,
|
||||
defaultName,
|
||||
backendValidators,
|
||||
isLoading,
|
||||
}: IEditTeamModalProps): JSX.Element => {
|
||||
const [name, setName] = useState(defaultName);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
@@ -48,38 +51,42 @@ const EditTeamModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Edit team"} onExit={onCancel} className={baseClass}>
|
||||
<form
|
||||
className={`${baseClass}__form`}
|
||||
onSubmit={onFormSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
autofocus
|
||||
name="name"
|
||||
onChange={onInputChange}
|
||||
label="Team name"
|
||||
placeholder="Team name"
|
||||
value={name}
|
||||
error={errors.name}
|
||||
/>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
disabled={name === ""}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
className={`${baseClass}__form`}
|
||||
onSubmit={onFormSubmit}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
autofocus
|
||||
name="name"
|
||||
onChange={onInputChange}
|
||||
label="Team name"
|
||||
placeholder="Team name"
|
||||
value={name}
|
||||
error={errors.name}
|
||||
/>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
disabled={name === ""}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useContext } from "react";
|
||||
import React, { useState, useCallback, useContext, useEffect } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import memoize from "memoize-one";
|
||||
@@ -58,7 +58,8 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
const [showResetSessionsModal, setShowResetSessionsModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isEditingUser, setIsEditingUser] = useState<boolean>(false);
|
||||
const [userEditing, setUserEditing] = useState<any>(null);
|
||||
const [createUserErrors, setCreateUserErrors] = useState<IUserFormErrors>(
|
||||
DEFAULT_CREATE_USER_ERRORS
|
||||
@@ -71,7 +72,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
// API CALLS
|
||||
const {
|
||||
data: teams,
|
||||
isLoading: isLoadingTeams,
|
||||
isFetching: isFetchingTeams,
|
||||
error: loadingTeamsError,
|
||||
} = useQuery<ITeamsResponse, Error, ITeam[]>(
|
||||
["teams"],
|
||||
@@ -84,7 +85,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
|
||||
const {
|
||||
data: users,
|
||||
isLoading: isLoadingUsers,
|
||||
isFetching: isFetchingUsers,
|
||||
error: loadingUsersError,
|
||||
refetch: refetchUsers,
|
||||
} = useQuery<IUser[], Error, IUser[]>(
|
||||
@@ -97,7 +98,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
|
||||
const {
|
||||
data: invites,
|
||||
isLoading: isLoadingInvites,
|
||||
isFetching: isFetchingInvites,
|
||||
error: loadingInvitesError,
|
||||
refetch: refetchInvites,
|
||||
} = useQuery<IInvite[], Error, IInvite[]>(
|
||||
@@ -216,7 +217,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
};
|
||||
|
||||
const onCreateUserSubmit = (formData: any) => {
|
||||
setIsFormSubmitting(true);
|
||||
setIsLoading(true);
|
||||
|
||||
if (formData.newUserType === NewUserType.AdminInvited) {
|
||||
// Do some data formatting adding `invited_by` for the request to be correct and deleteing uncessary fields
|
||||
@@ -247,7 +248,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
// Do some data formatting deleting unnecessary fields
|
||||
@@ -273,7 +274,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFormSubmitting(false);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -281,6 +282,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
const onEditUser = (formData: any) => {
|
||||
const userData = getUser(userEditing.type, userEditing.id);
|
||||
|
||||
setIsEditingUser(true);
|
||||
if (userEditing.type === "invite") {
|
||||
return (
|
||||
userData &&
|
||||
@@ -303,6 +305,9 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsEditingUser(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -461,6 +466,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
isModifiedByGlobalAdmin
|
||||
isInvitePending={userEditing.type === "invite"}
|
||||
editUserErrors={editUserErrors}
|
||||
isLoading={isEditingUser}
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
@@ -479,7 +485,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
isPremiumTier={isPremiumTier || false}
|
||||
smtpConfigured={config?.smtp_settings.configured || false}
|
||||
canUseSso={config?.sso_settings.enable_sso || false}
|
||||
isFormSubmitting={isFormSubmitting}
|
||||
isLoading={isLoading}
|
||||
isModifiedByGlobalAdmin
|
||||
/>
|
||||
);
|
||||
@@ -528,11 +534,12 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
isPremiumTier || false
|
||||
);
|
||||
|
||||
const loadingTableData = isLoadingUsers || isLoadingInvites || isLoadingTeams;
|
||||
const loadingTableData =
|
||||
isFetchingUsers || isFetchingInvites || isFetchingTeams;
|
||||
const tableDataError =
|
||||
loadingUsersError || loadingInvitesError || loadingTeamsError;
|
||||
|
||||
let tableData: any = [];
|
||||
let tableData: unknown = [];
|
||||
if (!loadingTableData) {
|
||||
tableData = combineUsersAndInvites(users, invites, currentUser?.id);
|
||||
}
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ interface ICreateUserModalProps {
|
||||
currentTeam?: ITeam;
|
||||
canUseSso: boolean; // corresponds to whether SSO is enabled for the organization
|
||||
isModifiedByGlobalAdmin?: boolean | false;
|
||||
isFormSubmitting?: boolean | false;
|
||||
isLoading?: boolean | false;
|
||||
serverErrors?: { base: string; email: string };
|
||||
createUserErrors?: IUserFormErrors;
|
||||
}
|
||||
@@ -38,14 +38,14 @@ const CreateUserModal = ({
|
||||
smtpConfigured,
|
||||
canUseSso,
|
||||
isModifiedByGlobalAdmin,
|
||||
isFormSubmitting,
|
||||
isLoading,
|
||||
serverErrors,
|
||||
createUserErrors,
|
||||
}: ICreateUserModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal title="Create user" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
{isFormSubmitting && (
|
||||
{isLoading && (
|
||||
<div className="loading-spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
+26
-19
@@ -3,6 +3,7 @@ import React from "react";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { IUserFormErrors } from "interfaces/user";
|
||||
import Modal from "components/Modal";
|
||||
import Spinner from "components/Spinner";
|
||||
import UserForm from "../UserForm";
|
||||
import { IFormData } from "../UserForm/UserForm";
|
||||
|
||||
@@ -23,6 +24,7 @@ interface IEditUserModalProps {
|
||||
editUserErrors?: IUserFormErrors;
|
||||
isModifiedByGlobalAdmin?: boolean | false;
|
||||
isInvitePending?: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "edit-user-modal";
|
||||
@@ -44,6 +46,7 @@ const EditUserModal = ({
|
||||
editUserErrors,
|
||||
isModifiedByGlobalAdmin,
|
||||
isInvitePending,
|
||||
isLoading,
|
||||
}: IEditUserModalProps): JSX.Element => {
|
||||
return (
|
||||
<Modal
|
||||
@@ -51,25 +54,29 @@ const EditUserModal = ({
|
||||
onExit={onCancel}
|
||||
className={`${baseClass}__edit-user-modal`}
|
||||
>
|
||||
<UserForm
|
||||
createOrEditUserErrors={editUserErrors}
|
||||
defaultName={defaultName}
|
||||
defaultEmail={defaultEmail}
|
||||
defaultGlobalRole={defaultGlobalRole}
|
||||
defaultTeamRole={defaultTeamRole}
|
||||
defaultTeams={defaultTeams}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
availableTeams={availableTeams}
|
||||
submitText={"Save"}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
isSsoEnabled={isSsoEnabled}
|
||||
isModifiedByGlobalAdmin={isModifiedByGlobalAdmin}
|
||||
isInvitePending={isInvitePending}
|
||||
currentTeam={currentTeam}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<UserForm
|
||||
createOrEditUserErrors={editUserErrors}
|
||||
defaultName={defaultName}
|
||||
defaultEmail={defaultEmail}
|
||||
defaultGlobalRole={defaultGlobalRole}
|
||||
defaultTeamRole={defaultTeamRole}
|
||||
defaultTeams={defaultTeams}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
availableTeams={availableTeams}
|
||||
submitText={"Save"}
|
||||
isPremiumTier={isPremiumTier}
|
||||
smtpConfigured={smtpConfigured}
|
||||
canUseSso={canUseSso}
|
||||
isSsoEnabled={isSsoEnabled}
|
||||
isModifiedByGlobalAdmin={isModifiedByGlobalAdmin}
|
||||
isInvitePending={isInvitePending}
|
||||
currentTeam={currentTeam}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,8 +56,6 @@ const ManagePolicyPage = ({
|
||||
isTeamAdmin,
|
||||
isTeamMaintainer,
|
||||
currentTeam,
|
||||
setAvailableTeams,
|
||||
setCurrentUser,
|
||||
setCurrentTeam,
|
||||
setConfig,
|
||||
} = useContext(AppContext);
|
||||
@@ -76,6 +74,10 @@ const ManagePolicyPage = ({
|
||||
|
||||
const { setResetSelectedRows } = useContext(TableContext);
|
||||
|
||||
const [isAutomationsLoading, setIsAutomationsLoading] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isRemovingPolicy, setIsRemovingPolicy] = useState<boolean>(false);
|
||||
const [selectedPolicyIds, setSelectedPolicyIds] = useState<number[]>([]);
|
||||
const [showManageAutomationsModal, setShowManageAutomationsModal] = useState(
|
||||
false
|
||||
@@ -99,7 +101,7 @@ const ManagePolicyPage = ({
|
||||
const {
|
||||
data: globalPolicies,
|
||||
error: globalPoliciesError,
|
||||
isLoading: isLoadingGlobalPolicies,
|
||||
isFetching: isFetchingGlobalPolicies,
|
||||
isStale: isStaleGlobalPolicies,
|
||||
refetch: refetchGlobalPolicies,
|
||||
} = useQuery<ILoadAllPoliciesResponse, Error, IPolicyStats[]>(
|
||||
@@ -117,7 +119,7 @@ const ManagePolicyPage = ({
|
||||
const {
|
||||
data: teamPolicies,
|
||||
error: teamPoliciesError,
|
||||
isLoading: isLoadingTeamPolicies,
|
||||
isFetching: isFetchingTeamPolicies,
|
||||
refetch: refetchTeamPolicies,
|
||||
} = useQuery<ILoadAllPoliciesResponse, Error, IPolicyStats[]>(
|
||||
["teamPolicies", teamId],
|
||||
@@ -132,7 +134,7 @@ const ManagePolicyPage = ({
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainer || isTeamAdmin;
|
||||
const canManageAutomations = isGlobalAdmin || isTeamAdmin;
|
||||
|
||||
const { isLoading: isLoadingWebhooks, refetch: refetchWebhooks } = useQuery<
|
||||
const { isFetching: isFetchingWebhooks, refetch: refetchWebhooks } = useQuery<
|
||||
IConfig | ILoadTeamResponse,
|
||||
Error,
|
||||
IConfig | ITeam
|
||||
@@ -210,6 +212,7 @@ const ManagePolicyPage = ({
|
||||
policy_ids,
|
||||
enable_failing_policies_webhook,
|
||||
}: IWebhookFailingPolicies) => {
|
||||
setIsAutomationsLoading(true);
|
||||
try {
|
||||
const api = teamId ? teamsAPI : configAPI;
|
||||
const secondParam = teamId || undefined;
|
||||
@@ -222,6 +225,7 @@ const ManagePolicyPage = ({
|
||||
},
|
||||
},
|
||||
};
|
||||
setIsAutomationsLoading(true);
|
||||
|
||||
const request = api.update(data, secondParam);
|
||||
await request.then(() => {
|
||||
@@ -234,6 +238,7 @@ const ManagePolicyPage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleManageAutomationsModal();
|
||||
setIsAutomationsLoading(false);
|
||||
refetchWebhooks();
|
||||
}
|
||||
};
|
||||
@@ -252,6 +257,7 @@ const ManagePolicyPage = ({
|
||||
|
||||
const onRemovePoliciesSubmit = async () => {
|
||||
const id = currentTeam?.id;
|
||||
setIsRemovingPolicy(true);
|
||||
try {
|
||||
const request = id
|
||||
? teamPoliciesAPI.destroy(id, selectedPolicyIds)
|
||||
@@ -276,6 +282,7 @@ const ManagePolicyPage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleRemovePoliciesModal();
|
||||
setIsRemovingPolicy(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -292,9 +299,9 @@ const ManagePolicyPage = ({
|
||||
|
||||
const showInheritedPoliciesButton =
|
||||
!!teamId &&
|
||||
!isLoadingTeamPolicies &&
|
||||
!isFetchingTeamPolicies &&
|
||||
!teamPoliciesError &&
|
||||
!isLoadingGlobalPolicies &&
|
||||
!isFetchingGlobalPolicies &&
|
||||
!globalPoliciesError &&
|
||||
!!globalPolicies?.length;
|
||||
|
||||
@@ -362,8 +369,8 @@ const ManagePolicyPage = ({
|
||||
{showCtaButtons && (
|
||||
<div className={`${baseClass} button-wrap`}>
|
||||
{canManageAutomations &&
|
||||
!isLoadingWebhooks &&
|
||||
!isLoadingGlobalPolicies && (
|
||||
!isFetchingWebhooks &&
|
||||
!isFetchingGlobalPolicies && (
|
||||
<Button
|
||||
onClick={toggleManageAutomationsModal}
|
||||
className={`${baseClass}__manage-automations button`}
|
||||
@@ -403,12 +410,12 @@ const ManagePolicyPage = ({
|
||||
{!!teamId && teamPoliciesError && <TableDataError />}
|
||||
{!!teamId &&
|
||||
!teamPoliciesError &&
|
||||
(isLoadingTeamPolicies && isLoadingWebhooks ? (
|
||||
(isFetchingTeamPolicies && isFetchingWebhooks ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
policiesList={teamPolicies || []}
|
||||
isLoading={isLoadingTeamPolicies && isLoadingWebhooks}
|
||||
isLoading={isFetchingTeamPolicies && isFetchingWebhooks}
|
||||
onAddPolicyClick={onAddPolicyClick}
|
||||
onRemovePoliciesClick={onRemovePoliciesClick}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy}
|
||||
@@ -419,12 +426,12 @@ const ManagePolicyPage = ({
|
||||
{!teamId && globalPoliciesError && <TableDataError />}
|
||||
{!teamId &&
|
||||
!globalPoliciesError &&
|
||||
(isLoadingGlobalPolicies ? (
|
||||
(isFetchingGlobalPolicies ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
policiesList={globalPolicies || []}
|
||||
isLoading={isLoadingGlobalPolicies && isLoadingWebhooks}
|
||||
isLoading={isFetchingGlobalPolicies && isFetchingWebhooks}
|
||||
onAddPolicyClick={onAddPolicyClick}
|
||||
onRemovePoliciesClick={onRemovePoliciesClick}
|
||||
canAddOrRemovePolicy={canAddOrRemovePolicy}
|
||||
@@ -456,11 +463,11 @@ const ManagePolicyPage = ({
|
||||
<div className={`${baseClass}__inherited-policies-table`}>
|
||||
{globalPoliciesError && <TableDataError />}
|
||||
{!globalPoliciesError &&
|
||||
(isLoadingGlobalPolicies ? (
|
||||
(isFetchingGlobalPolicies ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<PoliciesListWrapper
|
||||
isLoading={isLoadingGlobalPolicies && isLoadingWebhooks}
|
||||
isLoading={isFetchingGlobalPolicies && isFetchingWebhooks}
|
||||
policiesList={globalPolicies || []}
|
||||
onRemovePoliciesClick={noop}
|
||||
resultsTitle="policies"
|
||||
@@ -490,6 +497,7 @@ const ManagePolicyPage = ({
|
||||
failingPoliciesWebhook.enable_failing_policies_webhook) ||
|
||||
false
|
||||
}
|
||||
isAutomationsLoading={isAutomationsLoading}
|
||||
/>
|
||||
)}
|
||||
{showAddPolicyModal && (
|
||||
@@ -502,6 +510,7 @@ const ManagePolicyPage = ({
|
||||
)}
|
||||
{showRemovePoliciesModal && (
|
||||
<RemovePoliciesModal
|
||||
isLoading={isRemovingPolicy}
|
||||
onCancel={toggleRemovePoliciesModal}
|
||||
onSubmit={onRemovePoliciesSubmit}
|
||||
/>
|
||||
|
||||
+93
-86
@@ -12,6 +12,7 @@ import Slider from "components/forms/fields/Slider";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Spinner from "components/Spinner";
|
||||
import PreviewPayloadModal from "../PreviewPayloadModal";
|
||||
|
||||
interface IManageAutomationsModalProps {
|
||||
@@ -23,6 +24,7 @@ interface IManageAutomationsModalProps {
|
||||
currentAutomatedPolicies?: number[];
|
||||
currentDestinationUrl?: string;
|
||||
enableFailingPoliciesWebhook: boolean;
|
||||
isAutomationsLoading: boolean;
|
||||
}
|
||||
|
||||
interface ICheckedPolicy {
|
||||
@@ -95,6 +97,7 @@ const ManageAutomationsModal = ({
|
||||
currentAutomatedPolicies,
|
||||
currentDestinationUrl,
|
||||
enableFailingPoliciesWebhook,
|
||||
isAutomationsLoading,
|
||||
}: IManageAutomationsModalProps): JSX.Element => {
|
||||
const [destination_url, setDestinationUrl] = useState<string>(
|
||||
currentDestinationUrl || ""
|
||||
@@ -144,8 +147,6 @@ const ManageAutomationsModal = ({
|
||||
policy_ids,
|
||||
enable_failing_policies_webhook: policyAutomationEnabled,
|
||||
});
|
||||
|
||||
onReturnToApp();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,92 +173,98 @@ const ManageAutomationsModal = ({
|
||||
title={"Manage automations"}
|
||||
className={baseClass}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__software-select-items`}>
|
||||
<Slider
|
||||
value={policyAutomationEnabled}
|
||||
onChange={() =>
|
||||
setPolicyAutomationEnabled(!policyAutomationEnabled)
|
||||
}
|
||||
inactiveText={"Policy automations disabled"}
|
||||
activeText={"Policy automations enabled"}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__overlay-container`}>
|
||||
<div className={`${baseClass}__policy-automation-enabled`}>
|
||||
{availablePolicies && availablePolicies.length > 0 ? (
|
||||
<div className={`${baseClass}__policy-select-items`}>
|
||||
<p>
|
||||
<strong>
|
||||
Choose which policies you would like to listen to:
|
||||
</strong>
|
||||
</p>
|
||||
{policyItems &&
|
||||
policyItems.map((policyItem) => {
|
||||
const { isChecked, name, id } = policyItem;
|
||||
return (
|
||||
<div key={id} className={`${baseClass}__team-item`}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
onChange={() => updatePolicyItems(policyItem.id)}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<>
|
||||
{isAutomationsLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__software-select-items`}>
|
||||
<Slider
|
||||
value={policyAutomationEnabled}
|
||||
onChange={() =>
|
||||
setPolicyAutomationEnabled(!policyAutomationEnabled)
|
||||
}
|
||||
inactiveText={"Policy automations disabled"}
|
||||
activeText={"Policy automations enabled"}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__overlay-container`}>
|
||||
<div className={`${baseClass}__policy-automation-enabled`}>
|
||||
{availablePolicies && availablePolicies.length > 0 ? (
|
||||
<div className={`${baseClass}__policy-select-items`}>
|
||||
<p>
|
||||
<strong>
|
||||
Choose which policies you would like to listen to:
|
||||
</strong>
|
||||
</p>
|
||||
{policyItems &&
|
||||
policyItems.map((policyItem) => {
|
||||
const { isChecked, name, id } = policyItem;
|
||||
return (
|
||||
<div key={id} className={`${baseClass}__team-item`}>
|
||||
<Checkbox
|
||||
value={isChecked}
|
||||
name={name}
|
||||
onChange={() => updatePolicyItems(policyItem.id)}
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__no-policies`}>
|
||||
<b>You have no policies.</b>
|
||||
<p>Add a policy to turn on automations.</p>
|
||||
</div>
|
||||
)}
|
||||
<InputField
|
||||
inputWrapperClass={`${baseClass}__url-input`}
|
||||
name="webhook-url"
|
||||
label={"Destination URL"}
|
||||
type={"text"}
|
||||
value={destination_url}
|
||||
onChange={onURLChange}
|
||||
error={errors.url}
|
||||
hint={
|
||||
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
|
||||
}
|
||||
placeholder={"https://server.com/example"}
|
||||
tooltip="Provide a URL to deliver a webhook request to."
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text-link"
|
||||
onClick={togglePreviewPayloadModal}
|
||||
>
|
||||
Preview payload
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${baseClass}__no-policies`}>
|
||||
<b>You have no policies.</b>
|
||||
<p>Add a policy to turn on automations.</p>
|
||||
</div>
|
||||
)}
|
||||
<InputField
|
||||
inputWrapperClass={`${baseClass}__url-input`}
|
||||
name="webhook-url"
|
||||
label={"Destination URL"}
|
||||
type={"text"}
|
||||
value={destination_url}
|
||||
onChange={onURLChange}
|
||||
error={errors.url}
|
||||
hint={
|
||||
'For each policy, Fleet will send a JSON payload to this URL with a list of the hosts that updated their answer to "No."'
|
||||
}
|
||||
placeholder={"https://server.com/example"}
|
||||
tooltip="Provide a URL to deliver a webhook request to."
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="text-link"
|
||||
onClick={togglePreviewPayloadModal}
|
||||
>
|
||||
Preview payload
|
||||
</Button>
|
||||
{!policyAutomationEnabled && (
|
||||
<div className={`${baseClass}__overlay`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onReturnToApp}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{!policyAutomationEnabled && (
|
||||
<div className={`${baseClass}__overlay`} />
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onReturnToApp}
|
||||
variant="inverse"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+29
-20
@@ -2,15 +2,18 @@ import React, { useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "remove-policies-modal";
|
||||
|
||||
interface IRemovePoliciesModalProps {
|
||||
isLoading: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const RemovePoliciesModal = ({
|
||||
isLoading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: IRemovePoliciesModalProps): JSX.Element => {
|
||||
@@ -29,26 +32,32 @@ const RemovePoliciesModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Delete policies"} onExit={onCancel} className={baseClass}>
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to delete the selected policies?
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse-alert"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to delete the selected policies?
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="button"
|
||||
variant="alert"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={onCancel}
|
||||
variant="inverse-alert"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -97,8 +97,6 @@ const PolicyPage = ({
|
||||
setShowOpenSchemaActionText,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
// disabled on page load so we can control the number of renders
|
||||
// else it will re-populate the context on occasion
|
||||
const {
|
||||
isLoading: isStoredPolicyLoading,
|
||||
data: storedPolicy,
|
||||
|
||||
@@ -12,6 +12,7 @@ import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
export interface INewPolicyModalProps {
|
||||
baseClass: string;
|
||||
@@ -20,6 +21,7 @@ export interface INewPolicyModalProps {
|
||||
setIsNewPolicyModalOpen: (isOpen: boolean) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
platformSelector: IPlatformSelector;
|
||||
policyIsLoading: boolean;
|
||||
}
|
||||
|
||||
const validatePolicyName = (name: string) => {
|
||||
@@ -40,6 +42,7 @@ const NewPolicyModal = ({
|
||||
setIsNewPolicyModalOpen,
|
||||
backendValidators,
|
||||
platformSelector,
|
||||
policyIsLoading,
|
||||
}: INewPolicyModalProps): JSX.Element => {
|
||||
const {
|
||||
lastEditedQueryName,
|
||||
@@ -98,80 +101,86 @@ const NewPolicyModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Save policy"} onExit={() => setIsNewPolicyModalOpen(false)}>
|
||||
<form
|
||||
onSubmit={handleSavePolicy}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__policy-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What yes or no question does your policy ask about your devices?"
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__policy-save-modal-description`}
|
||||
label="Description"
|
||||
placeholder="Add a description here (optional)"
|
||||
/>
|
||||
<InputField
|
||||
name="resolution"
|
||||
onChange={(value: string) => setResolution(value)}
|
||||
value={resolution}
|
||||
inputClassName={`${baseClass}__policy-save-modal-resolution`}
|
||||
label="Resolution"
|
||||
type="textarea"
|
||||
placeholder="What steps should a device owner take to resolve a host that fails this policy? (optional)"
|
||||
/>
|
||||
{platformSelector.render()}
|
||||
<div
|
||||
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--modal`}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__button--modal-cancel`}
|
||||
onClick={() => setIsNewPolicyModalOpen(false)}
|
||||
variant="text-link"
|
||||
<>
|
||||
{policyIsLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSavePolicy}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--modal-save`}
|
||||
data-tip
|
||||
data-for={`${baseClass}__button--modal-save-tooltip`}
|
||||
data-tip-disable={!disableSave}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__button--modal-save`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSavePolicy}
|
||||
disabled={disableSave}
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__policy-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What yes or no question does your policy ask about your devices?"
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__policy-save-modal-description`}
|
||||
label="Description"
|
||||
placeholder="Add a description here (optional)"
|
||||
/>
|
||||
<InputField
|
||||
name="resolution"
|
||||
onChange={(value: string) => setResolution(value)}
|
||||
value={resolution}
|
||||
inputClassName={`${baseClass}__policy-save-modal-resolution`}
|
||||
label="Resolution"
|
||||
type="textarea"
|
||||
placeholder="What steps should a device owner take to resolve a host that fails this policy? (optional)"
|
||||
/>
|
||||
{platformSelector.render()}
|
||||
<div
|
||||
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--modal`}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button--modal-save-tooltip`}
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`${baseClass}__button--modal-save-tooltip`}
|
||||
backgroundColor="#3e4771"
|
||||
>
|
||||
Select the platform(s) this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save the policy.
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<Button
|
||||
className={`${baseClass}__button--modal-cancel`}
|
||||
onClick={() => setIsNewPolicyModalOpen(false)}
|
||||
variant="text-link"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--modal-save`}
|
||||
data-tip
|
||||
data-for={`${baseClass}__button--modal-save-tooltip`}
|
||||
data-tip-disable={!disableSave}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__button--modal-save`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSavePolicy}
|
||||
disabled={disableSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button--modal-save-tooltip`}
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`${baseClass}__button--modal-save-tooltip`}
|
||||
backgroundColor="#3e4771"
|
||||
>
|
||||
Select the platform(s) this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save the policy.
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@ interface IPolicyFormProps {
|
||||
showOpenSchemaActionText: boolean;
|
||||
storedPolicy: IPolicy | undefined;
|
||||
isStoredPolicyLoading: boolean;
|
||||
isCreatingNewPolicy: boolean;
|
||||
onCreatePolicy: (formData: IPolicyFormData) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
@@ -62,6 +63,7 @@ const PolicyForm = ({
|
||||
showOpenSchemaActionText,
|
||||
storedPolicy,
|
||||
isStoredPolicyLoading,
|
||||
isCreatingNewPolicy,
|
||||
onCreatePolicy,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
@@ -520,6 +522,7 @@ const PolicyForm = ({
|
||||
setIsNewPolicyModalOpen={setIsNewPolicyModalOpen}
|
||||
backendValidators={backendValidators}
|
||||
platformSelector={platformSelector}
|
||||
policyIsLoading={isCreatingNewPolicy}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -67,6 +67,9 @@ const QueryEditor = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [isCreatingNewPolicy, setIsCreatingNewPolicy] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
@@ -75,11 +78,12 @@ const QueryEditor = ({
|
||||
if (policyTeamId) {
|
||||
formData.team_id = policyTeamId;
|
||||
}
|
||||
|
||||
setIsCreatingNewPolicy(true);
|
||||
try {
|
||||
const policy: IPolicy = await createPolicy(formData).then(
|
||||
(data) => data.policy
|
||||
);
|
||||
setIsCreatingNewPolicy(false);
|
||||
router.push(PATHS.EDIT_POLICY(policy));
|
||||
renderFlash("success", "Policy created!");
|
||||
} catch (createError: any) {
|
||||
@@ -94,6 +98,8 @@ const QueryEditor = ({
|
||||
"Something went wrong creating your policy. Please try again."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsCreatingNewPolicy(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -163,6 +169,7 @@ const QueryEditor = ({
|
||||
storedPolicy={storedPolicy}
|
||||
policyIdForEdit={policyIdForEdit}
|
||||
isStoredPolicyLoading={isStoredPolicyLoading}
|
||||
isCreatingNewPolicy={isCreatingNewPolicy}
|
||||
showOpenSchemaActionText={showOpenSchemaActionText}
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
|
||||
@@ -99,11 +99,12 @@ const ManageQueriesPage = ({
|
||||
const [showRemoveQueryModal, setShowRemoveQueryModal] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [queryIsRemoving, setQueryIsRemoving] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
data: fleetQueries,
|
||||
error: fleetQueriesError,
|
||||
isLoading: isLoadingFleetQueries,
|
||||
isFetching: isFetchingFleetQueries,
|
||||
refetch: refetchFleetQueries,
|
||||
} = useQuery<IFleetQueriesResponse, Error, IQuery[]>(
|
||||
"fleet queries by platform",
|
||||
@@ -124,10 +125,10 @@ const ManageQueriesPage = ({
|
||||
}, [fleetQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoadingFleetQueries && enhancedQueriesList) {
|
||||
if (!isFetchingFleetQueries && enhancedQueriesList) {
|
||||
setQueriesList(enhancedQueriesList);
|
||||
}
|
||||
}, [enhancedQueriesList, isLoadingFleetQueries]);
|
||||
}, [enhancedQueriesList, isFetchingFleetQueries]);
|
||||
|
||||
const onCreateQueryClick = () => router.push(PATHS.NEW_QUERY);
|
||||
|
||||
@@ -143,6 +144,8 @@ const ManageQueriesPage = ({
|
||||
const onRemoveQuerySubmit = useCallback(async () => {
|
||||
const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries";
|
||||
|
||||
setQueryIsRemoving(true);
|
||||
|
||||
const removeQueries = selectedQueryIds.map((id) =>
|
||||
fleetQueriesAPI.destroy(id)
|
||||
);
|
||||
@@ -161,6 +164,7 @@ const ManageQueriesPage = ({
|
||||
);
|
||||
} finally {
|
||||
toggleRemoveQueryModal();
|
||||
setQueryIsRemoving(false);
|
||||
}
|
||||
}, [refetchFleetQueries, selectedQueryIds, toggleRemoveQueryModal]);
|
||||
|
||||
@@ -176,7 +180,7 @@ const ManageQueriesPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
const isTableDataLoading = isLoadingFleetQueries || queriesList === null;
|
||||
const isTableDataLoading = isFetchingFleetQueries || queriesList === null;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
@@ -223,6 +227,7 @@ const ManageQueriesPage = ({
|
||||
</div>
|
||||
{showRemoveQueryModal && (
|
||||
<RemoveQueryModal
|
||||
isLoading={queryIsRemoving}
|
||||
onCancel={toggleRemoveQueryModal}
|
||||
onSubmit={onRemoveQuerySubmit}
|
||||
/>
|
||||
|
||||
+20
-11
@@ -2,15 +2,18 @@ import React, { useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "remove-query-modal";
|
||||
|
||||
interface IRemoveQueryModalProps {
|
||||
isLoading: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const RemoveQueryModal = ({
|
||||
isLoading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: IRemoveQueryModalProps): JSX.Element => {
|
||||
@@ -29,17 +32,23 @@ const RemoveQueryModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Delete query"} onExit={onCancel} className={baseClass}>
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to delete the selected queries?
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="alert" onClick={onSubmit}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to delete the selected queries?
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="alert" onClick={onSubmit}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,10 +9,12 @@ import Checkbox from "components/forms/fields/Checkbox";
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
export interface INewQueryModalProps {
|
||||
baseClass: string;
|
||||
queryValue: string;
|
||||
isLoading: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
setIsSaveModalOpen: (isOpen: boolean) => void;
|
||||
backendValidators: { [key: string]: string };
|
||||
@@ -32,6 +34,7 @@ const validateQueryName = (name: string) => {
|
||||
const NewQueryModal = ({
|
||||
baseClass,
|
||||
queryValue,
|
||||
isLoading,
|
||||
onCreateQuery,
|
||||
setIsSaveModalOpen,
|
||||
backendValidators,
|
||||
@@ -74,57 +77,67 @@ const NewQueryModal = ({
|
||||
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={() => setIsSaveModalOpen(false)}>
|
||||
<form
|
||||
onSubmit={handleUpdate}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__query-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__query-save-modal-description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__query-save-modal-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the Observer role will be able to run this query on hosts
|
||||
where they have access.
|
||||
</p>
|
||||
<hr />
|
||||
<div
|
||||
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--modal`}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={() => setIsSaveModalOpen(false)}
|
||||
variant="text-link"
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleUpdate}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
||||
Save query
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__query-save-modal-name`}
|
||||
label="Name"
|
||||
placeholder="What is your query called?"
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
inputClassName={`${baseClass}__query-save-modal-description`}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="What information does your query reveal? (optional)"
|
||||
/>
|
||||
<Checkbox
|
||||
name="observerCanRun"
|
||||
onChange={setObserverCanRun}
|
||||
value={observerCanRun}
|
||||
wrapperClassName={`${baseClass}__query-save-modal-observer-can-run-wrapper`}
|
||||
>
|
||||
Observers can run
|
||||
</Checkbox>
|
||||
<p>
|
||||
Users with the Observer role will be able to run this query on
|
||||
hosts where they have access.
|
||||
</p>
|
||||
<hr />
|
||||
<div
|
||||
className={`${baseClass}__button-wrap ${baseClass}__button-wrap--modal`}
|
||||
>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
onClick={() => setIsSaveModalOpen(false)}
|
||||
variant="text-link"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
>
|
||||
Save query
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ interface IQueryFormProps {
|
||||
showOpenSchemaActionText: boolean;
|
||||
storedQuery: IQuery | undefined;
|
||||
isStoredQueryLoading: boolean;
|
||||
isQuerySaving: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
@@ -64,6 +65,7 @@ const QueryForm = ({
|
||||
showOpenSchemaActionText,
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
isQuerySaving,
|
||||
onCreateQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
@@ -555,6 +557,7 @@ const QueryForm = ({
|
||||
onCreateQuery={onCreateQuery}
|
||||
setIsSaveModalOpen={setIsSaveModalOpen}
|
||||
backendValidators={backendValidators}
|
||||
isLoading={isQuerySaving}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -61,6 +61,8 @@ const QueryEditor = ({
|
||||
lastEditedQueryObserverCanRun,
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [isQuerySaving, setIsQuerySaving] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (storedQueryError) {
|
||||
renderFlash(
|
||||
@@ -75,6 +77,7 @@ const QueryEditor = ({
|
||||
}>({});
|
||||
|
||||
const onSaveQueryFormSubmit = debounce(async (formData: IQueryFormData) => {
|
||||
setIsQuerySaving(true);
|
||||
try {
|
||||
const { query }: { query: IQuery } = await createQuery(formData);
|
||||
router.push(PATHS.EDIT_QUERY(query));
|
||||
@@ -90,6 +93,8 @@ const QueryEditor = ({
|
||||
"Something went wrong creating your query. Please try again."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsQuerySaving(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -146,6 +151,7 @@ const QueryEditor = ({
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
backendValidators={backendValidators}
|
||||
isQuerySaving={isQuerySaving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ const renderTable = (
|
||||
toggleScheduleEditorModal: () => void,
|
||||
isOnGlobalTeam: boolean,
|
||||
selectedTeamData: ITeam | undefined,
|
||||
isLoadingGlobalScheduledQueries: boolean,
|
||||
isFetchingGlobalScheduledQueries: boolean,
|
||||
isLoadingTeamScheduledQueries: boolean
|
||||
): JSX.Element => {
|
||||
return allScheduledQueriesError ? (
|
||||
@@ -56,7 +56,7 @@ const renderTable = (
|
||||
toggleScheduleEditorModal={toggleScheduleEditorModal}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
selectedTeamData={selectedTeamData}
|
||||
loadingInheritedQueriesTableData={isLoadingGlobalScheduledQueries}
|
||||
loadingInheritedQueriesTableData={isFetchingGlobalScheduledQueries}
|
||||
loadingTeamQueriesTableData={isLoadingTeamScheduledQueries}
|
||||
/>
|
||||
);
|
||||
@@ -68,7 +68,7 @@ const renderAllTeamsTable = (
|
||||
allTeamsScheduledQueriesError: Error | null,
|
||||
isOnGlobalTeam: boolean,
|
||||
selectedTeamData: ITeam | undefined,
|
||||
isLoadingGlobalScheduledQueries: boolean,
|
||||
isFetchingGlobalScheduledQueries: boolean,
|
||||
isLoadingTeamScheduledQueries: boolean
|
||||
): JSX.Element => {
|
||||
return allTeamsScheduledQueriesError ? (
|
||||
@@ -81,7 +81,7 @@ const renderAllTeamsTable = (
|
||||
allScheduledQueriesList={allTeamsScheduledQueriesList}
|
||||
isOnGlobalTeam={isOnGlobalTeam}
|
||||
selectedTeamData={selectedTeamData}
|
||||
loadingInheritedQueriesTableData={isLoadingGlobalScheduledQueries}
|
||||
loadingInheritedQueriesTableData={isFetchingGlobalScheduledQueries}
|
||||
loadingTeamQueriesTableData={isLoadingTeamScheduledQueries}
|
||||
/>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ const ManageSchedulePage = ({
|
||||
const {
|
||||
data: globalScheduledQueries,
|
||||
error: globalScheduledQueriesError,
|
||||
isLoading: isLoadingGlobalScheduledQueries,
|
||||
isFetching: isFetchingGlobalScheduledQueries,
|
||||
refetch: refetchGlobalScheduledQueries,
|
||||
} = useQuery<
|
||||
ILoadAllGlobalScheduledQueriesResponse,
|
||||
@@ -280,6 +280,8 @@ const ManageSchedulePage = ({
|
||||
const selectedTeamData =
|
||||
teams?.find((team: ITeam) => selectedTeam === team.id) || undefined;
|
||||
|
||||
const [scheduleIsLoading, setScheduleIsLoading] = useState<boolean>(false);
|
||||
const [scheduleIsRemoving, setScheduleIsRemoving] = useState<boolean>(false);
|
||||
const [showInheritedQueries, setShowInheritedQueries] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
@@ -329,6 +331,7 @@ const ManageSchedulePage = ({
|
||||
};
|
||||
|
||||
const onRemoveScheduledQuerySubmit = useCallback(() => {
|
||||
setScheduleIsRemoving(true);
|
||||
const promises = selectedQueryIds.map((id: number) => {
|
||||
return selectedTeamId
|
||||
? teamScheduledQueriesAPI.destroy(selectedTeamId, id)
|
||||
@@ -350,6 +353,10 @@ const ManageSchedulePage = ({
|
||||
`Unable to remove scheduled ${queryOrQueries}. Please try again.`
|
||||
);
|
||||
toggleRemoveScheduledQueryModal();
|
||||
})
|
||||
.finally(() => {
|
||||
refetchGlobalScheduledQueries();
|
||||
setScheduleIsRemoving(false);
|
||||
});
|
||||
}, [
|
||||
selectedTeamId,
|
||||
@@ -360,6 +367,7 @@ const ManageSchedulePage = ({
|
||||
|
||||
const onAddScheduledQuerySubmit = useCallback(
|
||||
(formData: IFormData, editQuery: IEditScheduledQuery | undefined) => {
|
||||
setScheduleIsLoading(true);
|
||||
if (editQuery) {
|
||||
const updatedAttributes = deepDifference(formData, editQuery);
|
||||
|
||||
@@ -375,12 +383,17 @@ const ManageSchedulePage = ({
|
||||
`Successfully updated ${formData.name} in the schedule.`
|
||||
);
|
||||
refetchScheduledQueries();
|
||||
toggleScheduleEditorModal();
|
||||
})
|
||||
.catch(() => {
|
||||
renderFlash(
|
||||
"error",
|
||||
"Could not update scheduled query. Please try again."
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setScheduleIsLoading(false);
|
||||
refetchGlobalScheduledQueries();
|
||||
});
|
||||
} else {
|
||||
const createResponse = selectedTeamId
|
||||
@@ -394,12 +407,16 @@ const ManageSchedulePage = ({
|
||||
`Successfully added ${formData.name} to the schedule.`
|
||||
);
|
||||
refetchScheduledQueries();
|
||||
toggleScheduleEditorModal();
|
||||
})
|
||||
.catch(() => {
|
||||
renderFlash("error", "Could not schedule query. Please try again.");
|
||||
})
|
||||
.finally(() => {
|
||||
setScheduleIsLoading(false);
|
||||
refetchGlobalScheduledQueries();
|
||||
});
|
||||
}
|
||||
toggleScheduleEditorModal();
|
||||
},
|
||||
[selectedTeamId, toggleScheduleEditorModal]
|
||||
);
|
||||
@@ -471,7 +488,7 @@ const ManageSchedulePage = ({
|
||||
<div>
|
||||
{isLoadingTeams ||
|
||||
isLoadingFleetQueries ||
|
||||
isLoadingGlobalScheduledQueries ||
|
||||
isFetchingGlobalScheduledQueries ||
|
||||
isLoadingTeamScheduledQueries ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
@@ -484,7 +501,7 @@ const ManageSchedulePage = ({
|
||||
toggleScheduleEditorModal,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData,
|
||||
isLoadingGlobalScheduledQueries,
|
||||
isFetchingGlobalScheduledQueries,
|
||||
isLoadingTeamScheduledQueries
|
||||
)
|
||||
)}
|
||||
@@ -513,24 +530,26 @@ const ManageSchedulePage = ({
|
||||
inheritedScheduledQueriesError,
|
||||
isOnGlobalTeam || false,
|
||||
selectedTeamData,
|
||||
isLoadingGlobalScheduledQueries,
|
||||
isFetchingGlobalScheduledQueries,
|
||||
isLoadingTeamScheduledQueries
|
||||
)}
|
||||
{showScheduleEditorModal && (
|
||||
<ScheduleEditorModal
|
||||
onCancel={toggleScheduleEditorModal}
|
||||
onClose={toggleScheduleEditorModal}
|
||||
onScheduleSubmit={onAddScheduledQuerySubmit}
|
||||
allQueries={fleetQueries}
|
||||
editQuery={selectedScheduledQuery}
|
||||
teamId={selectedTeamId}
|
||||
togglePreviewDataModal={togglePreviewDataModal}
|
||||
showPreviewDataModal={showPreviewDataModal}
|
||||
isLoading={scheduleIsLoading}
|
||||
/>
|
||||
)}
|
||||
{showRemoveScheduledQueryModal && (
|
||||
<RemoveScheduledQueryModal
|
||||
onCancel={toggleRemoveScheduledQueryModal}
|
||||
onSubmit={onRemoveScheduledQuerySubmit}
|
||||
isLoading={scheduleIsRemoving}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+18
-10
@@ -2,15 +2,18 @@ import React, { useEffect } from "react";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
|
||||
const baseClass = "remove-scheduled-query-modal";
|
||||
|
||||
interface IRemoveScheduledQueryModalProps {
|
||||
isLoading: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: () => void;
|
||||
}
|
||||
|
||||
const RemoveScheduledQueryModal = ({
|
||||
isLoading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: IRemoveScheduledQueryModalProps): JSX.Element => {
|
||||
@@ -33,17 +36,22 @@ const RemoveScheduledQueryModal = ({
|
||||
onExit={onCancel}
|
||||
className={baseClass}
|
||||
>
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to remove the selected queries from the schedule?
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="alert" onClick={onSubmit}>
|
||||
Remove
|
||||
</Button>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className={baseClass}>
|
||||
Are you sure you want to remove the selected queries from the
|
||||
schedule?
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" variant="alert" onClick={onSubmit}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+121
-114
@@ -15,6 +15,7 @@ import InfoBanner from "components/InfoBanner/InfoBanner";
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Spinner from "components/Spinner";
|
||||
import {
|
||||
FREQUENCY_DROPDOWN_OPTIONS,
|
||||
PLATFORM_DROPDOWN_OPTIONS,
|
||||
@@ -40,7 +41,7 @@ interface IFormData {
|
||||
|
||||
interface IScheduleEditorModalProps {
|
||||
allQueries: IQuery[];
|
||||
onCancel: () => void;
|
||||
onClose: () => void;
|
||||
onScheduleSubmit: (
|
||||
formData: IFormData,
|
||||
editQuery: IEditScheduledQuery | undefined
|
||||
@@ -49,6 +50,7 @@ interface IScheduleEditorModalProps {
|
||||
teamId?: number;
|
||||
togglePreviewDataModal: () => void;
|
||||
showPreviewDataModal: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
interface INoQueryOption {
|
||||
id: number;
|
||||
@@ -85,13 +87,14 @@ const generateLoggingDestination = (loggingConfig: string): string => {
|
||||
};
|
||||
|
||||
const ScheduleEditorModal = ({
|
||||
onCancel,
|
||||
onClose,
|
||||
onScheduleSubmit,
|
||||
allQueries,
|
||||
editQuery,
|
||||
teamId,
|
||||
togglePreviewDataModal,
|
||||
showPreviewDataModal,
|
||||
isLoading,
|
||||
}: IScheduleEditorModalProps): JSX.Element => {
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
@@ -240,123 +243,127 @@ const ScheduleEditorModal = ({
|
||||
return (
|
||||
<Modal
|
||||
title={editQuery?.query_name || "Schedule editor"}
|
||||
onExit={onCancel}
|
||||
onExit={onClose}
|
||||
className={baseClass}
|
||||
>
|
||||
<form className={`${baseClass}__form`}>
|
||||
{!editQuery && (
|
||||
<Dropdown
|
||||
searchable
|
||||
options={createQueryDropdownOptions()}
|
||||
onChange={onChangeSelectQuery}
|
||||
placeholder={"Select query"}
|
||||
value={selectedQuery?.id}
|
||||
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={onChangeSelectFrequency}
|
||||
placeholder={"Every day"}
|
||||
value={selectedFrequency}
|
||||
label={"Choose a frequency and then run this query on a schedule"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your configured log destination is <b>{loggingConfig}</b>.
|
||||
</p>
|
||||
<p>
|
||||
{loggingConfig === "unknown"
|
||||
? ""
|
||||
: `This means that when this query is run on your hosts, the data will
|
||||
be sent to ${generateLoggingDestination(loggingConfig)}.`}
|
||||
</p>
|
||||
<p>
|
||||
Check out the Fleet documentation on
|
||||
<a
|
||||
href="https://fleetdm.com/docs/deploying/configuration#osquery-result-log-plugin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
how to configure a different log destination
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
baseClass={baseClass}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={onChangeSelectLoggingType}
|
||||
placeholder="Select"
|
||||
value={selectedLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platform"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={onChangeMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={selectedMinOsqueryVersionOptions}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<InputField
|
||||
onChange={onChangeShard}
|
||||
inputWrapperClass={`${baseClass}__form-field ${baseClass}__form-field--shard`}
|
||||
value={selectedShard}
|
||||
placeholder="- - -"
|
||||
label="Shard"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form className={`${baseClass}__form`}>
|
||||
{!editQuery && (
|
||||
<Dropdown
|
||||
searchable
|
||||
options={createQueryDropdownOptions()}
|
||||
onChange={onChangeSelectQuery}
|
||||
placeholder={"Select query"}
|
||||
value={selectedQuery?.id}
|
||||
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div className={`${baseClass}__preview-btn-wrap`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="inverse"
|
||||
onClick={togglePreviewDataModal}
|
||||
>
|
||||
Preview data
|
||||
</Button>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={FREQUENCY_DROPDOWN_OPTIONS}
|
||||
onChange={onChangeSelectFrequency}
|
||||
placeholder={"Every day"}
|
||||
value={selectedFrequency}
|
||||
label={"Choose a frequency and then run this query on a schedule"}
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
|
||||
/>
|
||||
<InfoBanner className={`${baseClass}__sandbox-info`}>
|
||||
<p>
|
||||
Your configured log destination is <b>{loggingConfig}</b>.
|
||||
</p>
|
||||
<p>
|
||||
{loggingConfig === "unknown"
|
||||
? ""
|
||||
: `This means that when this query is run on your hosts, the data will
|
||||
be sent to ${generateLoggingDestination(loggingConfig)}.`}
|
||||
</p>
|
||||
<p>
|
||||
Check out the Fleet documentation on
|
||||
<a
|
||||
href="https://fleetdm.com/docs/deploying/configuration#osquery-result-log-plugin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
how to configure a different log destination
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</InfoBanner>
|
||||
<div>
|
||||
<RevealButton
|
||||
isShowing={showAdvancedOptions}
|
||||
baseClass={baseClass}
|
||||
hideText={"Hide advanced options"}
|
||||
showText={"Show advanced options"}
|
||||
caretPosition={"after"}
|
||||
onClick={toggleAdvancedOptions}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div>
|
||||
<Dropdown
|
||||
options={LOGGING_TYPE_OPTIONS}
|
||||
onChange={onChangeSelectLoggingType}
|
||||
placeholder="Select"
|
||||
value={selectedLoggingType}
|
||||
label="Logging"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={PLATFORM_DROPDOWN_OPTIONS}
|
||||
placeholder="Select"
|
||||
label="Platform"
|
||||
onChange={onChangeSelectPlatformOptions}
|
||||
value={selectedPlatformOptions}
|
||||
multi
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
|
||||
/>
|
||||
<Dropdown
|
||||
options={MIN_OSQUERY_VERSION_OPTIONS}
|
||||
onChange={onChangeMinOsqueryVersionOptions}
|
||||
placeholder="Select"
|
||||
value={selectedMinOsqueryVersionOptions}
|
||||
label="Minimum osquery version"
|
||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||
/>
|
||||
<InputField
|
||||
onChange={onChangeShard}
|
||||
inputWrapperClass={`${baseClass}__form-field ${baseClass}__form-field--shard`}
|
||||
value={selectedShard}
|
||||
placeholder="- - -"
|
||||
label="Shard"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="brand"
|
||||
onClick={onFormSubmit}
|
||||
disabled={!selectedQuery && !editQuery}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
<div className={`${baseClass}__btn-wrap`}>
|
||||
<div className={`${baseClass}__preview-btn-wrap`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="inverse"
|
||||
onClick={togglePreviewDataModal}
|
||||
>
|
||||
Preview data
|
||||
</Button>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onClose} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="brand"
|
||||
onClick={onFormSubmit}
|
||||
disabled={!selectedQuery && !editQuery}
|
||||
>
|
||||
Schedule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user