diff --git a/changes/issue-5337-more-spinners b/changes/issue-5337-more-spinners new file mode 100644 index 0000000000..d526fbdd98 --- /dev/null +++ b/changes/issue-5337-more-spinners @@ -0,0 +1 @@ +- Add more loading spinners while asynchronous actions occur diff --git a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx index 1a29cf83c7..88c0aa72a2 100644 --- a/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx +++ b/frontend/pages/admin/AppSettingsPage/AppSettingsPage.tsx @@ -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("info"); - const { - data: appConfig, - isLoading: isLoadingConfig, - refetch: refetchConfig, - } = useQuery(["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 = ({

Set your organization information and configure SSO and SMTP

-
- + {renderSection()} +
+ )} ); }; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx index fed5affe48..24ef4024b7 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx @@ -70,7 +70,7 @@ const MembersPage = ({ const [showCreateUserModal, setShowCreateUserModal] = useState( false ); - const [isFormSubmitting, setIsFormSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [userEditing, setUserEditing] = useState(); const [searchString, setSearchString] = useState(""); const [createUserErrors, setCreateUserErrors] = useState( @@ -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 && ( diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/RemoveMemberModal/RemoveMemberModal.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/RemoveMemberModal/RemoveMemberModal.tsx index 347e8542ff..d4e0742310 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/RemoveMemberModal/RemoveMemberModal.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/RemoveMemberModal/RemoveMemberModal.tsx @@ -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 ( -
-

- You are about to remove{" "} - {memberName} from{" "} - {teamName}. -

-

- If {memberName} is not a member of any other team, they will lose - access to Fleet. -

-
- - -
-
+ {isLoading ? ( + + ) : ( +
+

+ You are about to remove{" "} + {memberName} from{" "} + {teamName}. +

+

+ If {memberName} is not a member of any other team, they will lose + access to Fleet. +

+
+ + +
+
+ )}
); }; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 494799f495..2e586e7054 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -129,6 +129,8 @@ const TeamDetailsWrapper = ({ const [backendValidators, setBackendValidators] = useState<{ [key: string]: string; }>({}); + const [teamIsRemoving, setTeamIsRemoving] = useState(false); + const [teamIsEditing, setTeamIsEditing] = useState(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} diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx index fe6e5ffd7d..c5a56d864e 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx @@ -28,6 +28,9 @@ const noTeamsClass = "no-teams"; const TeamManagementPage = (): JSX.Element => { const { renderFlash } = useContext(NotificationContext); + const [teamIsLoading, setTeamIsLoading] = useState(false); + const [teamIsEditing, setTeamIsEditing] = useState(false); + const [teamIsRemoving, setTeamIsRemoving] = useState(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( @@ -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 => { { 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} /> )} diff --git a/frontend/pages/admin/TeamManagementPage/components/CreateTeamModal/CreateTeamModal.tsx b/frontend/pages/admin/TeamManagementPage/components/CreateTeamModal/CreateTeamModal.tsx index 0bf095310d..debb3e4877 100644 --- a/frontend/pages/admin/TeamManagementPage/components/CreateTeamModal/CreateTeamModal.tsx +++ b/frontend/pages/admin/TeamManagementPage/components/CreateTeamModal/CreateTeamModal.tsx @@ -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 ( -
- - -

- Need to test queries and configurations before deploying? -

-

- 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. -

-
-
- - -
- + {isLoading ? ( + + ) : ( +
+ + +

+ Need to test queries and configurations before deploying? +

+

+ 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. +

+
+
+ + +
+ + )}
); }; diff --git a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx index 00e7ff3336..eccbe7e042 100644 --- a/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx +++ b/frontend/pages/admin/TeamManagementPage/components/DeleteTeamModal/DeleteTeamModal.tsx @@ -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 ( -
-

- You are about to delete{" "} - {name} from Fleet. -

-

- Members of this team who are not members of other teams will lose - access to Fleet. -

-

This action cannot be undone.

-
- - -
-
+ {isLoading ? ( + + ) : ( +
+

+ You are about to delete{" "} + {name} from Fleet. +

+

+ Members of this team who are not members of other teams will lose + access to Fleet. +

+

+ This action cannot be undone. +

+
+ + +
+
+ )}
); }; diff --git a/frontend/pages/admin/TeamManagementPage/components/EditTeamModal/EditTeamModal.tsx b/frontend/pages/admin/TeamManagementPage/components/EditTeamModal/EditTeamModal.tsx index de67933f10..b4103d8114 100644 --- a/frontend/pages/admin/TeamManagementPage/components/EditTeamModal/EditTeamModal.tsx +++ b/frontend/pages/admin/TeamManagementPage/components/EditTeamModal/EditTeamModal.tsx @@ -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 ( -
- -
- - -
- + {isLoading ? ( + + ) : ( +
+ +
+ + +
+ + )}
); }; diff --git a/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx b/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx index 50285a6481..dbeb7f6355 100644 --- a/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx +++ b/frontend/pages/admin/UserManagementPage/UserManagementPage.tsx @@ -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( false ); - const [isFormSubmitting, setIsFormSubmitting] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isEditingUser, setIsEditingUser] = useState(false); const [userEditing, setUserEditing] = useState(null); const [createUserErrors, setCreateUserErrors] = useState( 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( ["teams"], @@ -84,7 +85,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => { const { data: users, - isLoading: isLoadingUsers, + isFetching: isFetchingUsers, error: loadingUsersError, refetch: refetchUsers, } = useQuery( @@ -97,7 +98,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => { const { data: invites, - isLoading: isLoadingInvites, + isFetching: isFetchingInvites, error: loadingInvitesError, refetch: refetchInvites, } = useQuery( @@ -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} /> @@ -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); } diff --git a/frontend/pages/admin/UserManagementPage/components/CreateUserModal/CreateUserModal.tsx b/frontend/pages/admin/UserManagementPage/components/CreateUserModal/CreateUserModal.tsx index 41ffb49e01..e880e11688 100644 --- a/frontend/pages/admin/UserManagementPage/components/CreateUserModal/CreateUserModal.tsx +++ b/frontend/pages/admin/UserManagementPage/components/CreateUserModal/CreateUserModal.tsx @@ -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 ( <> - {isFormSubmitting && ( + {isLoading && (
diff --git a/frontend/pages/admin/UserManagementPage/components/EditUserModal/EditUserModal.tsx b/frontend/pages/admin/UserManagementPage/components/EditUserModal/EditUserModal.tsx index 31cdd479fe..b70ac1d269 100644 --- a/frontend/pages/admin/UserManagementPage/components/EditUserModal/EditUserModal.tsx +++ b/frontend/pages/admin/UserManagementPage/components/EditUserModal/EditUserModal.tsx @@ -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 ( - + {isLoading ? ( + + ) : ( + + )} ); }; diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index fdcf9e3015..7eb6def1ca 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -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( + false + ); + const [isRemovingPolicy, setIsRemovingPolicy] = useState(false); const [selectedPolicyIds, setSelectedPolicyIds] = useState([]); 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( @@ -117,7 +119,7 @@ const ManagePolicyPage = ({ const { data: teamPolicies, error: teamPoliciesError, - isLoading: isLoadingTeamPolicies, + isFetching: isFetchingTeamPolicies, refetch: refetchTeamPolicies, } = useQuery( ["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 && (
{canManageAutomations && - !isLoadingWebhooks && - !isLoadingGlobalPolicies && ( + !isFetchingWebhooks && + !isFetchingGlobalPolicies && (
- ) : ( -
- You have no policies. -

Add a policy to turn on automations.

-
- )} - - + {!policyAutomationEnabled && ( +
+ )} +
+
+ + +
- {!policyAutomationEnabled && ( -
- )} -
-
- - -
- + )} +
); }; diff --git a/frontend/pages/policies/ManagePoliciesPage/components/RemovePoliciesModal/RemovePoliciesModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/RemovePoliciesModal/RemovePoliciesModal.tsx index c5b7af545b..e8d404958d 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/RemovePoliciesModal/RemovePoliciesModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/RemovePoliciesModal/RemovePoliciesModal.tsx @@ -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 ( -
- Are you sure you want to delete the selected policies? -
- - -
-
+ <> + {isLoading ? ( + + ) : ( +
+ Are you sure you want to delete the selected policies? +
+ + +
+
+ )} +
); }; diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx index f93338e851..a006fa9a02 100644 --- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx +++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx @@ -97,8 +97,6 @@ const PolicyPage = ({ setShowOpenSchemaActionText, ] = useState(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, diff --git a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx index dd30ed8538..68b165ecb3 100644 --- a/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx +++ b/frontend/pages/policies/PolicyPage/components/NewPolicyModal/NewPolicyModal.tsx @@ -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 ( setIsNewPolicyModalOpen(false)}> -
- 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?" - /> - setDescription(value)} - value={description} - inputClassName={`${baseClass}__policy-save-modal-description`} - label="Description" - placeholder="Add a description here (optional)" - /> - 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()} -
- - - - - Select the platform(s) this -
- policy will be checked on -
- to save the policy. -
-
-
- + + + + + Select the platform(s) this +
+ policy will be checked on +
+ to save the policy. +
+
+ + + )} +
); }; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx index 59390b0717..76bcfe38b2 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyForm/PolicyForm.tsx @@ -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} /> )} diff --git a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx index 750af81ddc..58556f657f 100644 --- a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx +++ b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx @@ -67,6 +67,9 @@ const QueryEditor = ({ } }, []); + const [isCreatingNewPolicy, setIsCreatingNewPolicy] = useState( + 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} diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index a9a0f3ef24..1435f1272a 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -99,11 +99,12 @@ const ManageQueriesPage = ({ const [showRemoveQueryModal, setShowRemoveQueryModal] = useState( false ); + const [queryIsRemoving, setQueryIsRemoving] = useState(false); const { data: fleetQueries, error: fleetQueriesError, - isLoading: isLoadingFleetQueries, + isFetching: isFetchingFleetQueries, refetch: refetchFleetQueries, } = useQuery( "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 (
@@ -223,6 +227,7 @@ const ManageQueriesPage = ({
{showRemoveQueryModal && ( diff --git a/frontend/pages/queries/ManageQueriesPage/components/RemoveQueryModal/RemoveQueryModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/RemoveQueryModal/RemoveQueryModal.tsx index 78d20d25da..709506edc7 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/RemoveQueryModal/RemoveQueryModal.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/RemoveQueryModal/RemoveQueryModal.tsx @@ -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 ( -
- Are you sure you want to delete the selected queries? -
- - -
-
+ <> + {isLoading ? ( + + ) : ( +
+ Are you sure you want to delete the selected queries? +
+ + +
+
+ )} +
); }; diff --git a/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx b/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx index 3512840bfe..7e64b5853c 100644 --- a/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx +++ b/frontend/pages/queries/QueryPage/components/NewQueryModal/NewQueryModal.tsx @@ -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 ( setIsSaveModalOpen(false)}> -
- setName(value)} - value={name} - error={errors.name} - inputClassName={`${baseClass}__query-save-modal-name`} - label="Name" - placeholder="What is your query called?" - /> - setDescription(value)} - value={description} - inputClassName={`${baseClass}__query-save-modal-description`} - label="Description" - type="textarea" - placeholder="What information does your query reveal? (optional)" - /> - - Observers can run - -

- Users with the Observer role will be able to run this query on hosts - where they have access. -

-
-
- - -
- + setName(value)} + value={name} + error={errors.name} + inputClassName={`${baseClass}__query-save-modal-name`} + label="Name" + placeholder="What is your query called?" + /> + setDescription(value)} + value={description} + inputClassName={`${baseClass}__query-save-modal-description`} + label="Description" + type="textarea" + placeholder="What information does your query reveal? (optional)" + /> + + Observers can run + +

+ Users with the Observer role will be able to run this query on + hosts where they have access. +

+
+
+ + +
+ + )} +
); }; diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx index d3e4822122..baced09c75 100644 --- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx +++ b/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx @@ -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} /> )} diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx index b617376679..2ac340d019 100644 --- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx +++ b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx @@ -61,6 +61,8 @@ const QueryEditor = ({ lastEditedQueryObserverCanRun, } = useContext(QueryContext); + const [isQuerySaving, setIsQuerySaving] = useState(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} /> ); diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx index 2334a37577..ac521ba948 100644 --- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx @@ -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} /> @@ -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(false); + const [scheduleIsRemoving, setScheduleIsRemoving] = useState(false); const [showInheritedQueries, setShowInheritedQueries] = useState( 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 = ({
{isLoadingTeams || isLoadingFleetQueries || - isLoadingGlobalScheduledQueries || + isFetchingGlobalScheduledQueries || isLoadingTeamScheduledQueries ? ( ) : ( @@ -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 && ( )} {showRemoveScheduledQueryModal && ( )}
diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx index 550aef252c..2993166cb8 100644 --- a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx @@ -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} > -
- Are you sure you want to remove the selected queries from the schedule? -
- - + {isLoading ? ( + + ) : ( +
+ Are you sure you want to remove the selected queries from the + schedule? +
+ + +
-
+ )} ); }; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx index ee85059998..d6e48ec34f 100644 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx @@ -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 ( -
- {!editQuery && ( - - )} - - -

- Your configured log destination is {loggingConfig}. -

-

- {loggingConfig === "unknown" - ? "" - : `This means that when this query is run on your hosts, the data will - be sent to ${generateLoggingDestination(loggingConfig)}.`} -

-

- Check out the Fleet documentation on  - - how to configure a different log destination - - . -

-
-
- - {showAdvancedOptions && ( -
- - - - -
+ {isLoading ? ( + + ) : ( + + {!editQuery && ( + )} -
-
-
- + + +

+ Your configured log destination is {loggingConfig}. +

+

+ {loggingConfig === "unknown" + ? "" + : `This means that when this query is run on your hosts, the data will + be sent to ${generateLoggingDestination(loggingConfig)}.`} +

+

+ Check out the Fleet documentation on  + + how to configure a different log destination + + . +

+
+
+ + {showAdvancedOptions && ( +
+ + + + +
+ )}
-
- - +
+
+ +
+
+ + +
-
- + + )} ); };