mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Improve loading states (#6471)
This commit is contained in:
parent
5cd845a15e
commit
1c6c379f4d
@ -29,6 +29,6 @@ In today’s episode of the Future of Device Management podcast, we speak with [
|
||||
<meta name="category" value="podcasts">
|
||||
<meta name="authorGitHubUsername" value="zwass">
|
||||
<meta name="authorFullName" value="Zach Wasserman">
|
||||
<meta name="publishedOn" value="2022-06-30">
|
||||
<meta name="publishedOn" value="2099-06-30">
|
||||
<meta name="articleTitle" value="Future of device management episode 2">
|
||||
<meta name="articleImageUrl" value="../website/assets/images/articles/future-of-device-management-ep2-cover-1600x900@2x.jpg">
|
||||
|
1
changes/issue-5829-improve-loading-states
Normal file
1
changes/issue-5829-improve-loading-states
Normal file
@ -0,0 +1 @@
|
||||
* Improve UI loading states.
|
@ -26,7 +26,7 @@ describe("Policies flow (empty)", () => {
|
||||
.type(
|
||||
"{selectall}SELECT 1 FROM users WHERE username = 'backup' LIMIT 1;"
|
||||
);
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
cy.getAttached(".policy-form__policy-save-modal-name")
|
||||
.click()
|
||||
.type("Does the device have a user named 'backup'?");
|
||||
@ -36,7 +36,7 @@ describe("Policies flow (empty)", () => {
|
||||
cy.getAttached(".policy-form__policy-save-modal-resolution")
|
||||
.click()
|
||||
.type("Create a user named 'backup'");
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
});
|
||||
|
||||
@ -45,9 +45,9 @@ describe("Policies flow (empty)", () => {
|
||||
cy.findByText(/add a policy/i).click();
|
||||
});
|
||||
cy.findByText(/gatekeeper enabled/i).click();
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
cy.getAttached(".policy-form__button-wrap--modal").within(() => {
|
||||
cy.findAllByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
});
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
});
|
||||
@ -188,7 +188,7 @@ describe("Policies flow (empty)", () => {
|
||||
testCompatibility(el, i, [true, false, false]);
|
||||
});
|
||||
});
|
||||
cy.findByRole("button", { name: /save policy/i }).click(); // open save policy modal
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
|
||||
cy.getAttached(".platform-selector").within(() => {
|
||||
cy.getAttached(".fleet-checkbox__input").each((el, i) => {
|
||||
@ -204,7 +204,7 @@ describe("Policies flow (empty)", () => {
|
||||
cy.getAttached(".add-policy-modal__modal").within(() => {
|
||||
cy.findByText("Automatic login disabled (macOS)").click();
|
||||
});
|
||||
cy.findByRole("button", { name: /save policy/i }).click(); // open save policy modal
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
|
||||
cy.getAttached(".platform-selector").within(() => {
|
||||
cy.getAttached(".fleet-checkbox__input").each((el, i) => {
|
||||
@ -215,7 +215,7 @@ describe("Policies flow (empty)", () => {
|
||||
testSelections(el, i, [false, false, false]);
|
||||
});
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).should("be.disabled");
|
||||
cy.getAttached(".policy-form__button--modal-save").should("be.disabled");
|
||||
});
|
||||
|
||||
it("allows user to overide preselected platforms when saving new policy", () => {
|
||||
@ -231,7 +231,7 @@ describe("Policies flow (empty)", () => {
|
||||
testCompatibility(el, i, [true, false, false]);
|
||||
});
|
||||
});
|
||||
cy.findByRole("button", { name: /save policy/i }).click(); // open save policy modal
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
|
||||
cy.getAttached(".platform-selector").within(() => {
|
||||
cy.getAttached(".fleet-checkbox__input").each((el, i) => {
|
||||
@ -243,7 +243,7 @@ describe("Policies flow (empty)", () => {
|
||||
testSelections(el, i, [false, false, true]);
|
||||
});
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
|
||||
// confirm that new policy was saved with user-selected platforms
|
||||
@ -268,8 +268,8 @@ describe("Policies flow (empty)", () => {
|
||||
cy.getAttached(".add-policy-modal__modal").within(() => {
|
||||
cy.findByText("Antivirus healthy (macOS)").click();
|
||||
});
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
|
||||
// edit platform selections for policy
|
||||
|
@ -554,9 +554,9 @@ describe("Premium tier - Global Admin user", () => {
|
||||
cy.findByText(/gatekeeper enabled/i).click();
|
||||
cy.getAttached(".policy-form__button-wrap").within(() => {
|
||||
cy.findByRole("button", { name: /run/i }).should("exist");
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
cy.findByText(/gatekeeper enabled/i).should("exist");
|
||||
});
|
||||
|
@ -241,9 +241,9 @@ describe("Premium tier - Maintainer user", () => {
|
||||
cy.findByText(/gatekeeper enabled/i).click();
|
||||
cy.getAttached(".policy-form__button-wrap").within(() => {
|
||||
cy.findByRole("button", { name: /run/i }).should("exist");
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
});
|
||||
it("allows global maintainer to delete a team policy", () => {
|
||||
|
@ -301,9 +301,9 @@ describe("Premium tier - Team Admin user", () => {
|
||||
cy.findByText(/gatekeeper enabled/i).click();
|
||||
cy.getAttached(".policy-form__button-wrap").within(() => {
|
||||
cy.findByRole("button", { name: /run/i }).should("exist");
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
});
|
||||
it("allows team admin to edit a team policy", () => {
|
||||
|
@ -289,10 +289,8 @@ describe("Premium tier - Team observer/maintainer user", () => {
|
||||
|
||||
// Add a default policy
|
||||
cy.findByText(/gatekeeper enabled/i).click();
|
||||
cy.getAttached(".policy-form__button-wrap").within(() => {
|
||||
cy.findByRole("button", { name: /save policy/i }).click();
|
||||
});
|
||||
cy.findByRole("button", { name: /^Save$/ }).click();
|
||||
cy.getAttached(".policy-form__save").click();
|
||||
cy.getAttached(".policy-form__button--modal-save").click();
|
||||
cy.findByText(/policy created/i).should("exist");
|
||||
|
||||
// On maintaining team, should see "save" and "run" for a new policy
|
||||
|
@ -73,7 +73,6 @@ const DEBOUNCE_DELAY = 500;
|
||||
const STALE_TIME = 60000;
|
||||
|
||||
const isLabel = (entity: ISelectTargetsEntity) => "label_type" in entity;
|
||||
const isHost = (entity: ISelectTargetsEntity) => "hostname" in entity;
|
||||
|
||||
const parseLabels = (list?: ILabelSummary[]) => {
|
||||
const allHosts = list?.filter((l) => l.name === "All Hosts") || [];
|
||||
@ -324,13 +323,18 @@ const SelectTargets = ({
|
||||
|
||||
const renderTargetsCount = (): JSX.Element | null => {
|
||||
if (isFetchingCounts) {
|
||||
return <i style={{ color: "#8b8fa2" }}>Checking for online hosts...</i>;
|
||||
return (
|
||||
<>
|
||||
<Spinner small />
|
||||
<i style={{ color: "#8b8fa2" }}>Counting hosts</i>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorCounts) {
|
||||
return (
|
||||
<b style={{ color: "#d66c7b", margin: 0 }}>
|
||||
There was a problem checking online hosts. Please try again later.
|
||||
There was a problem counting hosts. Please try again later.
|
||||
</b>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import React from "react";
|
||||
import { Meta, Story } from "@storybook/react";
|
||||
|
||||
import Spinner from ".";
|
||||
import { ISpinnerProps } from "./Spinner";
|
||||
|
||||
import "../../index.scss";
|
||||
|
||||
@ -14,6 +13,6 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<ISpinnerProps> = (props) => <Spinner {...props} />;
|
||||
const Template: Story = (props) => <Spinner {...props} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
@ -1,25 +1,14 @@
|
||||
import React from "react";
|
||||
|
||||
export interface ISpinnerProps {
|
||||
isInButton?: boolean;
|
||||
interface ISpinnerProps {
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "loading-spinner";
|
||||
|
||||
const Spinner = ({ isInButton }: ISpinnerProps): JSX.Element => {
|
||||
if (isInButton) {
|
||||
return (
|
||||
<div className="ring ring-for-button">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Spinner = ({ small }: ISpinnerProps): JSX.Element => {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass} ${small ? "small" : ""}`}>
|
||||
<div className={`${baseClass}__ring`}>
|
||||
<div />
|
||||
<div />
|
||||
|
@ -25,29 +25,13 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: -4px;
|
||||
border: 4px solid $core-white;
|
||||
border: 4px solid;
|
||||
border-radius: 50%;
|
||||
animation: ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: $ui-vibrant-blue-25 transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__ring-for-button {
|
||||
margin-right: $pad-small;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-width: 3px;
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
|
||||
div {
|
||||
margin: -3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-width: 3px;
|
||||
border-color: $core-white transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
@ -61,6 +45,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner.small {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
.loading-spinner {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
transform: scale(0.7);
|
||||
position: absolute;
|
||||
&__ring {
|
||||
border-color: $core-white;
|
||||
|
||||
div {
|
||||
border-color: $core-vibrant-blue transparent transparent transparent;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-wrap {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
@ -157,6 +157,11 @@
|
||||
margin-left: 16px;
|
||||
font-size: $x-small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.loading-spinner {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
|
@ -102,84 +102,80 @@ const NewPolicyModal = ({
|
||||
return (
|
||||
<Modal title={"Save policy"} onExit={() => setIsNewPolicyModalOpen(false)}>
|
||||
<>
|
||||
{policyIsLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSavePolicy}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
<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`}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
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-cancel`}
|
||||
onClick={() => setIsNewPolicyModalOpen(false)}
|
||||
variant="text-link"
|
||||
className={`${baseClass}__button--modal-save`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSavePolicy}
|
||||
disabled={disableSave}
|
||||
>
|
||||
Cancel
|
||||
{policyIsLoading ? <Spinner /> : "Save policy"}
|
||||
</Button>
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--modal-save`}
|
||||
data-tip
|
||||
data-for={`${baseClass}__button--modal-save-tooltip`}
|
||||
data-tip-disable={!disableSave}
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button--modal-save-tooltip`}
|
||||
place="bottom"
|
||||
type="dark"
|
||||
effect="solid"
|
||||
id={`${baseClass}__button--modal-save-tooltip`}
|
||||
backgroundColor="#3e4771"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
Select the platform(s) this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save the policy.
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -37,6 +37,7 @@ interface IPolicyFormProps {
|
||||
storedPolicy: IPolicy | undefined;
|
||||
isStoredPolicyLoading: boolean;
|
||||
isCreatingNewPolicy: boolean;
|
||||
isUpdatingPolicy: boolean;
|
||||
onCreatePolicy: (formData: IPolicyFormData) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
@ -64,6 +65,7 @@ const PolicyForm = ({
|
||||
storedPolicy,
|
||||
isStoredPolicyLoading,
|
||||
isCreatingNewPolicy,
|
||||
isUpdatingPolicy,
|
||||
onCreatePolicy,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
@ -84,6 +86,7 @@ const PolicyForm = ({
|
||||
const [isEditingResolution, setIsEditingResolution] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isPolicySaving, setIsPolicySaving] = useState<boolean>(false);
|
||||
|
||||
// Note: The PolicyContext values should always be used for any mutable policy data such as query name
|
||||
// The storedPolicy prop should only be used to access immutable metadata such as author id
|
||||
@ -486,7 +489,7 @@ const PolicyForm = ({
|
||||
onClick={promptSavePolicy()}
|
||||
disabled={isEditMode && !isAnyPlatformSelected}
|
||||
>
|
||||
<>{!isEditMode ? "Save policy" : "Save"}</>
|
||||
<>{isUpdatingPolicy ? <Spinner /> : "Save"}</>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
|
@ -228,6 +228,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap--modal-save {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__button--modal-save {
|
||||
width: 106px;
|
||||
}
|
||||
|
||||
&__save {
|
||||
display: flex;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
&__platform-error {
|
||||
font-size: $small;
|
||||
font-weight: $bold;
|
||||
|
@ -239,10 +239,7 @@ const QueryResults = ({
|
||||
onClick={onStopQuery}
|
||||
variant="alert"
|
||||
>
|
||||
<>
|
||||
<Spinner isInButton />
|
||||
Stop
|
||||
</>
|
||||
<>Stop</>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
@ -70,6 +70,7 @@ const QueryEditor = ({
|
||||
const [isCreatingNewPolicy, setIsCreatingNewPolicy] = useState<boolean>(
|
||||
false
|
||||
);
|
||||
const [isUpdatingPolicy, setIsUpdatingPolicy] = useState<boolean>(false);
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
@ -108,6 +109,8 @@ const QueryEditor = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsUpdatingPolicy(true);
|
||||
|
||||
const updatedPolicy = deepDifference(formData, {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
@ -141,6 +144,8 @@ const QueryEditor = ({
|
||||
"Something went wrong updating your policy. Please try again."
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsUpdatingPolicy(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -174,6 +179,7 @@ const QueryEditor = ({
|
||||
onOpenSchemaSidebar={onOpenSchemaSidebar}
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
backendValidators={backendValidators}
|
||||
isUpdatingPolicy={isUpdatingPolicy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -160,6 +160,11 @@
|
||||
margin-left: 16px;
|
||||
font-size: $x-small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.loading-spinner {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
|
@ -78,65 +78,61 @@ const NewQueryModal = ({
|
||||
return (
|
||||
<Modal title={"Save query"} onExit={() => setIsSaveModalOpen(false)}>
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleUpdate}
|
||||
className={`${baseClass}__save-modal-form`}
|
||||
autoComplete="off"
|
||||
<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`}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
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`}
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__btn ${baseClass}__save-modal__btn`}
|
||||
type="submit"
|
||||
variant="brand"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
{isLoading ? <Spinner /> : "Save query"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -38,6 +38,7 @@ interface IQueryFormProps {
|
||||
storedQuery: IQuery | undefined;
|
||||
isStoredQueryLoading: boolean;
|
||||
isQuerySaving: boolean;
|
||||
isQueryUpdating: boolean;
|
||||
onCreateQuery: (formData: IQueryFormData) => void;
|
||||
onOsqueryTableSelect: (tableName: string) => void;
|
||||
goToSelectTargets: () => void;
|
||||
@ -66,6 +67,7 @@ const QueryForm = ({
|
||||
storedQuery,
|
||||
isStoredQueryLoading,
|
||||
isQuerySaving,
|
||||
isQueryUpdating,
|
||||
onCreateQuery,
|
||||
onOsqueryTableSelect,
|
||||
goToSelectTargets,
|
||||
@ -440,11 +442,6 @@ const QueryForm = ({
|
||||
const renderForGlobalAdminOrAnyMaintainer = (
|
||||
<>
|
||||
<form className={`${baseClass}__wrapper`} autoComplete="off">
|
||||
{isSaveAsNewLoading && (
|
||||
<div className={`${baseClass}__loading-overlay`}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__title-bar`}>
|
||||
<div className="name-description">
|
||||
{renderName()}
|
||||
@ -496,7 +493,7 @@ const QueryForm = ({
|
||||
onClick={promptSaveAsNewQuery()}
|
||||
disabled={false}
|
||||
>
|
||||
Save as new
|
||||
{isSaveAsNewLoading ? <Spinner /> : "Save as new"}
|
||||
</Button>
|
||||
)}
|
||||
<div className="query-form__button-wrap--save-query-button">
|
||||
@ -519,7 +516,7 @@ const QueryForm = ({
|
||||
!hasTeamMaintainerPermissions
|
||||
}
|
||||
>
|
||||
Save
|
||||
{isQueryUpdating ? <Spinner /> : "Save"}
|
||||
</Button>
|
||||
</div>{" "}
|
||||
<ReactTooltip
|
||||
|
@ -197,6 +197,19 @@
|
||||
margin-right: $pad-xsmall;
|
||||
}
|
||||
|
||||
&__save {
|
||||
display: flex;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
&__save-as-new {
|
||||
width: 82px;
|
||||
}
|
||||
|
||||
&__save-modal__btn {
|
||||
width: 106px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: $core-fleet-black;
|
||||
display: inline-block;
|
||||
|
@ -241,10 +241,7 @@ const QueryResults = ({
|
||||
onClick={onStopQuery}
|
||||
variant="alert"
|
||||
>
|
||||
<>
|
||||
<Spinner isInButton />
|
||||
Stop
|
||||
</>
|
||||
<>Stop</>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
@ -62,6 +62,7 @@ const QueryEditor = ({
|
||||
} = useContext(QueryContext);
|
||||
|
||||
const [isQuerySaving, setIsQuerySaving] = useState<boolean>(false);
|
||||
const [isQueryUpdating, setIsQueryUpdating] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (storedQueryError) {
|
||||
@ -103,6 +104,8 @@ const QueryEditor = ({
|
||||
return false;
|
||||
}
|
||||
|
||||
setIsQueryUpdating(true);
|
||||
|
||||
const updatedQuery = deepDifference(formData, {
|
||||
lastEditedQueryName,
|
||||
lastEditedQueryDescription,
|
||||
@ -125,6 +128,8 @@ const QueryEditor = ({
|
||||
}
|
||||
}
|
||||
|
||||
setIsQueryUpdating(false);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@ -152,6 +157,7 @@ const QueryEditor = ({
|
||||
renderLiveQueryWarning={renderLiveQueryWarning}
|
||||
backendValidators={backendValidators}
|
||||
isQuerySaving={isQuerySaving}
|
||||
isQueryUpdating={isQueryUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user