mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 17:05:18 +00:00
Refine and improve policy and query editing interface (#4004)
This commit is contained in:
parent
2084b7d310
commit
14d36d8e4d
1
changes/issue-3099-3195-improve-query-editing
Normal file
1
changes/issue-3099-3195-improve-query-editing
Normal file
@ -0,0 +1 @@
|
|||||||
|
* Refine and improve query and policy editing interface
|
@ -0,0 +1,49 @@
|
|||||||
|
import React, { KeyboardEvent } from "react";
|
||||||
|
import { Meta, Story } from "@storybook/react";
|
||||||
|
import { noop } from "lodash";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import AutoSizeInputField from ".";
|
||||||
|
|
||||||
|
import "../../../../index.scss";
|
||||||
|
|
||||||
|
interface IAutoSizeInputFieldProps {
|
||||||
|
name: string;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
hasError?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isFocused?: boolean;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
onChange: (newSelectedValue: string) => void;
|
||||||
|
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: AutoSizeInputField,
|
||||||
|
title: "Components/FormFields/Input",
|
||||||
|
args: {
|
||||||
|
autofocus: false,
|
||||||
|
disabled: false,
|
||||||
|
isFocused: false,
|
||||||
|
error: "",
|
||||||
|
inputClassName: "",
|
||||||
|
inputWrapperClass: "",
|
||||||
|
inputOptions: "",
|
||||||
|
name: "",
|
||||||
|
placeholder: "Type here...",
|
||||||
|
type: "",
|
||||||
|
value: "",
|
||||||
|
onFocus: noop,
|
||||||
|
onChange: noop,
|
||||||
|
onKeyPress: noop,
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
const Template: Story<IAutoSizeInputFieldProps> = (props) => (
|
||||||
|
<AutoSizeInputField {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Default = Template.bind({});
|
@ -0,0 +1,102 @@
|
|||||||
|
import React, {
|
||||||
|
ChangeEvent,
|
||||||
|
KeyboardEvent,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
interface IAutoSizeInputFieldProps {
|
||||||
|
name: string;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
hasError?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isFocused?: boolean;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
onChange: (newSelectedValue: string) => void;
|
||||||
|
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClass = "component__auto-size-input-field";
|
||||||
|
|
||||||
|
const TeamsDropdown = ({
|
||||||
|
name,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
inputClassName,
|
||||||
|
hasError,
|
||||||
|
isDisabled,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
onChange,
|
||||||
|
onKeyPress,
|
||||||
|
isFocused,
|
||||||
|
}: IAutoSizeInputFieldProps): JSX.Element => {
|
||||||
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
|
||||||
|
const inputClasses = classnames(baseClass, inputClassName, "no-hover", {
|
||||||
|
[`${baseClass}--disabled`]: isDisabled,
|
||||||
|
[`${baseClass}--error`]: hasError,
|
||||||
|
[`${baseClass}__textarea`]: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputElement = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange(inputValue);
|
||||||
|
}, [inputValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused && inputElement.current) {
|
||||||
|
inputElement.current.focus();
|
||||||
|
inputElement.current.selectionStart = inputValue.length;
|
||||||
|
inputElement.current.selectionEnd = inputValue.length;
|
||||||
|
}
|
||||||
|
}, [isFocused]);
|
||||||
|
|
||||||
|
const onInputChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setInputValue(event.currentTarget.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputFocus = () => {
|
||||||
|
isFocused = true;
|
||||||
|
onFocus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputBlur = () => {
|
||||||
|
isFocused = false;
|
||||||
|
onBlur();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputKeyPress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
onKeyPress(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={baseClass}>
|
||||||
|
<label className="input-sizer" data-value={inputValue} htmlFor={name}>
|
||||||
|
<textarea
|
||||||
|
name={name}
|
||||||
|
id={name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={inputValue}
|
||||||
|
className={inputClasses}
|
||||||
|
cols={12}
|
||||||
|
rows={1}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={onInputFocus}
|
||||||
|
onBlur={onInputBlur}
|
||||||
|
onKeyPress={onInputKeyPress}
|
||||||
|
ref={inputElement}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamsDropdown;
|
@ -0,0 +1,42 @@
|
|||||||
|
.component__auto-size-input-field {
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: $core-fleet-black;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $ui-fleet-black-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $core-vibrant-blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-sizer {
|
||||||
|
display: inline-grid;
|
||||||
|
vertical-align: top;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: auto;
|
||||||
|
grid-area: 1 / 2;
|
||||||
|
resize: none;
|
||||||
|
background: none;
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: attr(data-value) " ";
|
||||||
|
visibility: hidden;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./AutoSizeInputField";
|
@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
|
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
|
||||||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext, KeyboardEvent } from "react";
|
||||||
import { IAceEditor } from "react-ace/lib/types";
|
import { IAceEditor } from "react-ace/lib/types";
|
||||||
import ReactTooltip from "react-tooltip";
|
import ReactTooltip from "react-tooltip";
|
||||||
import { isUndefined } from "lodash";
|
import { isUndefined } from "lodash";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
import { addGravatarUrlToResource } from "fleet/helpers";
|
import { addGravatarUrlToResource } from "fleet/helpers";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -18,6 +19,7 @@ import FleetAce from "components/FleetAce";
|
|||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
import Checkbox from "components/forms/fields/Checkbox";
|
import Checkbox from "components/forms/fields/Checkbox";
|
||||||
import Spinner from "components/Spinner";
|
import Spinner from "components/Spinner";
|
||||||
|
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import InputField from "components/forms/fields/InputField";
|
import InputField from "components/forms/fields/InputField";
|
||||||
import NewPolicyModal from "../NewPolicyModal";
|
import NewPolicyModal from "../NewPolicyModal";
|
||||||
@ -52,7 +54,7 @@ const PolicyForm = ({
|
|||||||
onOpenSchemaSidebar,
|
onOpenSchemaSidebar,
|
||||||
renderLiveQueryWarning,
|
renderLiveQueryWarning,
|
||||||
}: IPolicyFormProps): JSX.Element => {
|
}: IPolicyFormProps): JSX.Element => {
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
const [errors, setErrors] = useState<{ [key: string]: any }>({});
|
||||||
const [isNewPolicyModalOpen, setIsNewPolicyModalOpen] = useState<boolean>(
|
const [isNewPolicyModalOpen, setIsNewPolicyModalOpen] = useState<boolean>(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
@ -87,7 +89,6 @@ const PolicyForm = ({
|
|||||||
} = useContext(PolicyContext);
|
} = useContext(PolicyContext);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentTeam,
|
|
||||||
currentUser,
|
currentUser,
|
||||||
isTeamObserver,
|
isTeamObserver,
|
||||||
isGlobalObserver,
|
isGlobalObserver,
|
||||||
@ -150,6 +151,16 @@ const PolicyForm = ({
|
|||||||
setLastEditedQueryBody(sqlString);
|
setLastEditedQueryBody(sqlString);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (event.key.toLowerCase() === "enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
setIsEditingName(false);
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
setIsEditingResolution(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const promptSavePolicy = (forceNew = false) => (
|
const promptSavePolicy = (forceNew = false) => (
|
||||||
evt: React.MouseEvent<HTMLButtonElement>
|
evt: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
@ -230,41 +241,44 @@ const PolicyForm = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const policyNameClasses = classnames("policy-name-wrapper", {
|
||||||
|
[`${baseClass}--editing`]: isEditingName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const policyDescriptionClasses = classnames("policy-description-wrapper", {
|
||||||
|
[`${baseClass}--editing`]: isEditingDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
const policyResolutionClasses = classnames("policy-resolution-wrapper", {
|
||||||
|
[`${baseClass}--editing`]: isEditingResolution,
|
||||||
|
});
|
||||||
|
|
||||||
const renderName = () => {
|
const renderName = () => {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
if (isEditingName) {
|
|
||||||
return (
|
|
||||||
<InputField
|
|
||||||
id="policy-name"
|
|
||||||
type="textarea"
|
|
||||||
name="policy-name"
|
|
||||||
error={errors.name}
|
|
||||||
value={lastEditedQueryName}
|
|
||||||
placeholder="Add name here"
|
|
||||||
inputClassName={`${baseClass}__policy-name`}
|
|
||||||
onChange={setLastEditedQueryName}
|
|
||||||
inputOptions={{
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
// sets cursor to end of inputfield
|
|
||||||
const val = e.target.value;
|
|
||||||
e.target.value = "";
|
|
||||||
e.target.value = val;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h1
|
<>
|
||||||
role="button"
|
<div className={policyNameClasses}>
|
||||||
className={`${baseClass}__policy-name`}
|
<AutoSizeInputField
|
||||||
onClick={() => setIsEditingName(true)}
|
name="policy-name"
|
||||||
>
|
placeholder="Add name here"
|
||||||
{lastEditedQueryName}
|
value={lastEditedQueryName}
|
||||||
<img alt="Edit name" src={PencilIcon} />
|
hasError={errors && errors.name}
|
||||||
</h1>
|
inputClassName={`${baseClass}__policy-name`}
|
||||||
|
onChange={setLastEditedQueryName}
|
||||||
|
onFocus={() => setIsEditingName(true)}
|
||||||
|
onBlur={() => setIsEditingName(false)}
|
||||||
|
onKeyPress={onInputKeypress}
|
||||||
|
isFocused={isEditingName}
|
||||||
|
/>
|
||||||
|
<a className="edit-link" onClick={() => setIsEditingName(true)}>
|
||||||
|
<img
|
||||||
|
className={`edit-icon ${isEditingName && "hide"}`}
|
||||||
|
alt="Edit name"
|
||||||
|
src={PencilIcon}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,38 +287,32 @@ const PolicyForm = ({
|
|||||||
|
|
||||||
const renderDescription = () => {
|
const renderDescription = () => {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
if (isEditingDescription) {
|
|
||||||
return (
|
|
||||||
<InputField
|
|
||||||
id="policy-description"
|
|
||||||
type="textarea"
|
|
||||||
name="policy-description"
|
|
||||||
value={lastEditedQueryDescription}
|
|
||||||
placeholder="Add description here."
|
|
||||||
inputClassName={`${baseClass}__policy-description`}
|
|
||||||
onChange={setLastEditedQueryDescription}
|
|
||||||
inputOptions={{
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
// sets cursor to end of inputfield
|
|
||||||
const val = e.target.value;
|
|
||||||
e.target.value = "";
|
|
||||||
e.target.value = val;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<>
|
||||||
role="button"
|
<div className={policyDescriptionClasses}>
|
||||||
className={`${baseClass}__policy-description`}
|
<AutoSizeInputField
|
||||||
onClick={() => setIsEditingDescription(true)}
|
name="policy-description"
|
||||||
>
|
placeholder="Add description here."
|
||||||
{lastEditedQueryDescription || "Add description here."}
|
value={lastEditedQueryDescription}
|
||||||
<img alt="Edit description" src={PencilIcon} />
|
inputClassName={`${baseClass}__policy-description`}
|
||||||
</span>
|
onChange={setLastEditedQueryDescription}
|
||||||
|
onFocus={() => setIsEditingDescription(true)}
|
||||||
|
onBlur={() => setIsEditingDescription(false)}
|
||||||
|
onKeyPress={onInputKeypress}
|
||||||
|
isFocused={isEditingDescription}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="edit-link"
|
||||||
|
onClick={() => setIsEditingDescription(true)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={`edit-icon ${isEditingDescription && "hide"}`}
|
||||||
|
alt="Edit name"
|
||||||
|
src={PencilIcon}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,52 +321,33 @@ const PolicyForm = ({
|
|||||||
|
|
||||||
const renderResolution = () => {
|
const renderResolution = () => {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
if (isEditingResolution) {
|
|
||||||
return (
|
|
||||||
<div className={`${baseClass}__policy-resolve`}>
|
|
||||||
{" "}
|
|
||||||
<b>Resolve:</b> <br />
|
|
||||||
<InputField
|
|
||||||
id="policy-resolution"
|
|
||||||
type="textarea"
|
|
||||||
name="policy-resolution"
|
|
||||||
value={lastEditedQueryResolution}
|
|
||||||
placeholder="Add resolution here."
|
|
||||||
inputClassName={`${baseClass}__policy-resolution`}
|
|
||||||
onChange={setLastEditedQueryResolution}
|
|
||||||
inputOptions={{
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
// sets cursor to end of inputfield
|
|
||||||
const val = e.target.value;
|
|
||||||
e.target.value = "";
|
|
||||||
e.target.value = val;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="resolve-text-wrapper">
|
<p className="resolve-title">
|
||||||
<b>Resolve:</b>{" "}
|
<strong>Resolve:</strong>
|
||||||
<span
|
</p>
|
||||||
role="button"
|
<div className={policyResolutionClasses}>
|
||||||
className={`${baseClass}__policy-resolution`}
|
<AutoSizeInputField
|
||||||
|
name="policy-resolution"
|
||||||
|
placeholder="Add resolution here."
|
||||||
|
value={lastEditedQueryResolution}
|
||||||
|
inputClassName={`${baseClass}__policy-resolution`}
|
||||||
|
onChange={setLastEditedQueryResolution}
|
||||||
|
onFocus={() => setIsEditingResolution(true)}
|
||||||
|
onBlur={() => setIsEditingResolution(false)}
|
||||||
|
onKeyPress={onInputKeypress}
|
||||||
|
isFocused={isEditingResolution}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="edit-link"
|
||||||
onClick={() => setIsEditingResolution(true)}
|
onClick={() => setIsEditingResolution(true)}
|
||||||
>
|
>
|
||||||
<img alt="Edit resolution" src={PencilIcon} />
|
<img
|
||||||
</span>
|
className={`edit-icon ${isEditingResolution && "hide"}`}
|
||||||
<br />
|
alt="Edit name"
|
||||||
<span
|
src={PencilIcon}
|
||||||
role="button"
|
/>
|
||||||
className={`${baseClass}__policy-resolution`}
|
</a>
|
||||||
onClick={() => setIsEditingResolution(true)}
|
|
||||||
>
|
|
||||||
{lastEditedQueryResolution || "Add resolution here."}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -39,27 +39,63 @@
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.name-description-resolve {
|
.name-description-resolve {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-right: 24px;
|
margin: $pad-medium 24px 0 0;
|
||||||
.policy-form__policy-name {
|
.policy-name-wrapper,
|
||||||
margin: $pad-large 0;
|
.policy-description-wrapper,
|
||||||
line-height: 2rem;
|
.policy-resolution-wrapper {
|
||||||
height: 2rem;
|
display: flex;
|
||||||
|
&:not(.policy-form--editing) {
|
||||||
|
textarea:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.policy-name-wrapper {
|
||||||
|
.edit-icon {
|
||||||
|
position: relative;
|
||||||
|
left: 3px;
|
||||||
|
top: 13px;
|
||||||
|
}
|
||||||
|
.policy-form__policy-name,
|
||||||
|
.input-sizer::after {
|
||||||
|
font-size: $large;
|
||||||
|
}
|
||||||
|
.component__auto-size-input-field {
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.policy-description-wrapper {
|
||||||
|
padding-top: $pad-small;
|
||||||
|
.edit-icon {
|
||||||
|
position: relative;
|
||||||
|
left: 3px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
&.hide {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.policy-form__policy-resolve,
|
.policy-form__policy-resolve,
|
||||||
.resolve-text-wrapper {
|
.resolve-text-wrapper {
|
||||||
margin-top: $pad-large;
|
margin-top: $pad-large;
|
||||||
}
|
}
|
||||||
#policy-description {
|
.resolve-title {
|
||||||
height: 38px;
|
margin-bottom: 0;
|
||||||
}
|
|
||||||
textarea.policy-form__policy-description {
|
|
||||||
margin-top: 0.8125rem;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
height: 14px;
|
|
||||||
padding-left: $pad-small;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +151,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__policy-name {
|
&__policy-name {
|
||||||
margin-top: $pad-large;
|
|
||||||
font-size: $large;
|
font-size: $large;
|
||||||
|
|
||||||
&.input-field--error {
|
&.input-field--error {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useContext, useEffect } from "react";
|
import React, { useState, useContext, useEffect, KeyboardEvent } from "react";
|
||||||
import { IAceEditor } from "react-ace/lib/types";
|
import { IAceEditor } from "react-ace/lib/types";
|
||||||
import ReactTooltip from "react-tooltip";
|
import ReactTooltip from "react-tooltip";
|
||||||
import { size } from "lodash";
|
import { size } from "lodash";
|
||||||
import { useDebouncedCallback } from "use-debounce/lib";
|
import { useDebouncedCallback } from "use-debounce/lib";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
import { addGravatarUrlToResource } from "fleet/helpers";
|
import { addGravatarUrlToResource } from "fleet/helpers";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -13,12 +14,14 @@ import { QueryContext } from "context/query";
|
|||||||
import { IQuery, IQueryFormData } from "interfaces/query";
|
import { IQuery, IQueryFormData } from "interfaces/query";
|
||||||
|
|
||||||
import Avatar from "components/Avatar";
|
import Avatar from "components/Avatar";
|
||||||
import FleetAce from "components/FleetAce"; // @ts-ignore
|
import FleetAce from "components/FleetAce";
|
||||||
|
// @ts-ignore
|
||||||
import validateQuery from "components/forms/validators/validate_query";
|
import validateQuery from "components/forms/validators/validate_query";
|
||||||
import Button from "components/buttons/Button";
|
import Button from "components/buttons/Button";
|
||||||
import Checkbox from "components/forms/fields/Checkbox";
|
import Checkbox from "components/forms/fields/Checkbox";
|
||||||
import Spinner from "components/Spinner"; // @ts-ignore
|
import Spinner from "components/Spinner";
|
||||||
import InputField from "components/forms/fields/InputField";
|
// @ts-ignore
|
||||||
|
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||||
import NewQueryModal from "../NewQueryModal";
|
import NewQueryModal from "../NewQueryModal";
|
||||||
import PlatformCompatibility from "../PlatformCompatibility";
|
import PlatformCompatibility from "../PlatformCompatibility";
|
||||||
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
|
import InfoIcon from "../../../../../../assets/images/icon-info-purple-14x14@2x.png";
|
||||||
@ -154,6 +157,15 @@ const QueryForm = ({
|
|||||||
setLastEditedQueryBody(sqlString);
|
setLastEditedQueryBody(sqlString);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (event.key.toLowerCase() === "enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
setIsEditingName(false);
|
||||||
|
setIsEditingDescription(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const promptSaveQuery = (forceNew = false) => (
|
const promptSaveQuery = (forceNew = false) => (
|
||||||
evt: React.MouseEvent<HTMLButtonElement>
|
evt: React.MouseEvent<HTMLButtonElement>
|
||||||
) => {
|
) => {
|
||||||
@ -233,48 +245,41 @@ const QueryForm = ({
|
|||||||
return <PlatformCompatibility compatiblePlatforms={compatiblePlatforms} />;
|
return <PlatformCompatibility compatiblePlatforms={compatiblePlatforms} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryNameClasses = classnames("query-name-wrapper", {
|
||||||
|
[`${baseClass}--editing`]: isEditingName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryDescriptionClasses = classnames("query-description-wrapper", {
|
||||||
|
[`${baseClass}--editing`]: isEditingDescription,
|
||||||
|
});
|
||||||
|
|
||||||
const renderName = () => {
|
const renderName = () => {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
if (isEditingName) {
|
|
||||||
return (
|
|
||||||
<InputField
|
|
||||||
id="query-name"
|
|
||||||
type="textarea"
|
|
||||||
name="query-name"
|
|
||||||
error={errors.name}
|
|
||||||
value={lastEditedQueryName}
|
|
||||||
placeholder="Add name here"
|
|
||||||
inputClassName={`${baseClass}__query-name`}
|
|
||||||
onChange={setLastEditedQueryName}
|
|
||||||
inputOptions={{
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
// sets cursor to end of inputfield
|
|
||||||
const val = e.target.value;
|
|
||||||
e.target.value = "";
|
|
||||||
e.target.value = val;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
// eslint complains about the button role
|
|
||||||
// applied to H1 - this is needed to avoid
|
|
||||||
// using a real button
|
|
||||||
// prettier-ignore
|
|
||||||
return (
|
return (
|
||||||
<h1
|
<>
|
||||||
role="button"
|
<div className={queryNameClasses}>
|
||||||
className={`${baseClass}__query-name`}
|
<AutoSizeInputField
|
||||||
onClick={() => setIsEditingName(true)}
|
name="query-name"
|
||||||
>
|
placeholder="Add name here"
|
||||||
{lastEditedQueryName}
|
value={lastEditedQueryName}
|
||||||
<img alt="Edit name" src={PencilIcon} />
|
hasError={errors && errors.name}
|
||||||
</h1>
|
inputClassName={`${baseClass}__query-name`}
|
||||||
|
onChange={setLastEditedQueryName}
|
||||||
|
onFocus={() => setIsEditingName(true)}
|
||||||
|
onBlur={() => setIsEditingName(false)}
|
||||||
|
onKeyPress={onInputKeypress}
|
||||||
|
isFocused={isEditingName}
|
||||||
|
/>
|
||||||
|
<a className="edit-link" onClick={() => setIsEditingName(true)}>
|
||||||
|
<img
|
||||||
|
className={`edit-icon ${isEditingName && "hide"}`}
|
||||||
|
alt="Edit name"
|
||||||
|
src={PencilIcon}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
/* eslint-enable */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
|
return <h1 className={`${baseClass}__query-name no-hover`}>New query</h1>;
|
||||||
@ -282,47 +287,34 @@ const QueryForm = ({
|
|||||||
|
|
||||||
const renderDescription = () => {
|
const renderDescription = () => {
|
||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
if (isEditingDescription) {
|
|
||||||
return (
|
|
||||||
<InputField
|
|
||||||
id="query-description"
|
|
||||||
type="textarea"
|
|
||||||
name="query-description"
|
|
||||||
value={lastEditedQueryDescription}
|
|
||||||
placeholder="Add description here."
|
|
||||||
inputClassName={`${baseClass}__query-description`}
|
|
||||||
onChange={setLastEditedQueryDescription}
|
|
||||||
inputOptions={{
|
|
||||||
autoFocus: true,
|
|
||||||
onFocus: (e: React.FocusEvent<HTMLInputElement>) => {
|
|
||||||
// sets cursor to end of inputfield
|
|
||||||
const val = e.target.value;
|
|
||||||
e.target.value = "";
|
|
||||||
e.target.value = val;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
// eslint complains about the button role
|
|
||||||
// applied to span - this is needed to avoid
|
|
||||||
// using a real button
|
|
||||||
// prettier-ignore
|
|
||||||
return (
|
return (
|
||||||
<span
|
<>
|
||||||
role="button"
|
<div className={queryDescriptionClasses}>
|
||||||
className={`${baseClass}__query-description`}
|
<AutoSizeInputField
|
||||||
onClick={() => setIsEditingDescription(true)}
|
name="query-description"
|
||||||
>
|
placeholder="Add description here."
|
||||||
{lastEditedQueryDescription}
|
value={lastEditedQueryDescription}
|
||||||
<img alt="Edit description" src={PencilIcon} />
|
inputClassName={`${baseClass}__query-description`}
|
||||||
</span>
|
onChange={setLastEditedQueryDescription}
|
||||||
|
onFocus={() => setIsEditingDescription(true)}
|
||||||
|
onBlur={() => setIsEditingDescription(false)}
|
||||||
|
onKeyPress={onInputKeypress}
|
||||||
|
isFocused={isEditingDescription}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="edit-link"
|
||||||
|
onClick={() => setIsEditingDescription(true)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={`edit-icon ${isEditingDescription && "hide"}`}
|
||||||
|
alt="Edit name"
|
||||||
|
src={PencilIcon}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
/* eslint-enable */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,24 +39,59 @@
|
|||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-link {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.name-description {
|
.name-description {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-right: 24px;
|
margin: $pad-medium 24px 0 0;
|
||||||
.query-form__query-name {
|
.query-name-wrapper {
|
||||||
margin-top: $pad-large;
|
display: flex;
|
||||||
line-height: 2rem;
|
&:not(.query-form--editing) {
|
||||||
height: 2rem;
|
textarea:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-icon {
|
||||||
|
position: relative;
|
||||||
|
left: 3px;
|
||||||
|
top: 13px;
|
||||||
|
}
|
||||||
|
.query-form__query-name,
|
||||||
|
.input-sizer::after {
|
||||||
|
font-size: $large;
|
||||||
|
}
|
||||||
|
.component__auto-size-input-field {
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 2.3rem;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.query-form__query-description:not(textarea) {
|
.query-description-wrapper {
|
||||||
margin: 0.25rem 0 1rem;
|
display: flex;
|
||||||
|
padding-top: $pad-small;
|
||||||
|
&:not(.query-form--editing) {
|
||||||
|
textarea:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: $core-vibrant-blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.edit-icon {
|
||||||
|
position: relative;
|
||||||
|
left: 3px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
textarea.query-form__query-description {
|
.edit-icon {
|
||||||
margin-top: 0.8125rem;
|
width: 14px;
|
||||||
}
|
height: 14px;
|
||||||
img {
|
opacity: 1;
|
||||||
transform: scale(0.5);
|
transition: opacity 0.2s;
|
||||||
position: relative;
|
&.hide {
|
||||||
top: 6px;
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +138,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__query-name {
|
&__query-name {
|
||||||
margin-top: $pad-large;
|
margin-top: 0;
|
||||||
font-size: $large;
|
font-size: $large;
|
||||||
|
|
||||||
&.input-field--error {
|
&.input-field--error {
|
||||||
|
Loading…
Reference in New Issue
Block a user