Add more loading spinners to async actions (#5742)

This commit is contained in:
Luke Heath
2022-05-17 14:14:09 -05:00
committed by GitHub
parent 34dccc7b16
commit 8eeeebb19f
27 changed files with 832 additions and 638 deletions
+1
View File
@@ -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>
);
};
@@ -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}
/>
@@ -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>
@@ -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 teams 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 teams 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>
);
};
@@ -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>
);
};
@@ -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);
}
@@ -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>
@@ -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}
/>
@@ -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>
);
};
@@ -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}
/>
@@ -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>
@@ -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>
);
};
@@ -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&nbsp;
<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&nbsp;
<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>
);
};