mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
UI - GitOps Mode: Core abstractions, first batch of applications (#26401)
## For #26229 – Part 1

- This PR contains the core abstractions, routes, API updates, and types
for GitOps mode in the UI. Since this work will touch essentially every
part of the Fleet UI, it is ripe for merge conflicts. To mitigate such
conflicts, I'll be merging this work in a number of iterative PRs. ~To
effectively gate any of this work from showing until it is all merged to
`main`, [this commit](feedbb2d4c) hides
the settings section that allows enabling/disabling this setting,
effectively feature flagging the entire thing. In the last of these
iterative PRs, that commit will be reverted to engage the entire
feature. For testing purposes, reviewers can `git revert
feedbb2d4c25ec2e304e1f18d409cee62f6752ed` locally~ The new settings
section for this feature is feature flagged until all PRs are merged -
to show the setting section while testing, run `ALLOW_GITOPS_MODE=true
NODE_ENV=development yarn run webpack --progress --watch` in place of
`make generate-dev`
- Changes file will be added and feature flag removed in the last PR
- [x] Settings page with routing, form, API integration (hidden until
last PR)
- [x] Activities
- [x] Navbar indicator
- Apply GOM conditional UI to:
- [x] Manage enroll secret modal: .5
- Controls >
- [x] Scripts:
- Setup experience >
- [x] Install software > Select software modal
- [x] OS Settings >
- [x] Custom settings
- [x] Disk encryption
- [x] OS Updates
2/18/25, added to this PR:
- [x] Controls > Setup experience > Run script
- [x] Software >
- [x] Manage automations modal
- [x] Add software >
- [x] App Store (VPP)
- [x] Custom package
- [x] Queries
- [x] Manage
- [x] Automations modal
- [x] New
- [x] Edit
- [x] Policies
- [x] Manage
- [x] New
- [x] Edit
- Manage automations
- [x] Calendar events
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
parent
c22f575150
commit
5d9026b7e5
69 changed files with 1361 additions and 553 deletions
|
|
@ -193,6 +193,10 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
|
|||
},
|
||||
fleet_desktop: { transparency_url: "https://fleetdm.com/transparency" },
|
||||
mdm: createMockMdmConfig(),
|
||||
gitops: {
|
||||
gitops_mode_enabled: false,
|
||||
repository_url: "",
|
||||
},
|
||||
};
|
||||
|
||||
const createMockConfig = (overrides?: Partial<IConfig>): IConfig => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React from "react";
|
|||
import { ITeam } from "interfaces/team";
|
||||
import { IEnrollSecret } from "interfaces/enroll_secret";
|
||||
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon/Icon";
|
||||
|
|
@ -81,15 +82,20 @@ const EnrollSecretModal = ({
|
|||
</>
|
||||
)}
|
||||
<div className={`${baseClass}__add-secret`}>
|
||||
<Button
|
||||
onClick={addNewSecretClick}
|
||||
className={`${baseClass}__add-secret-btn`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<>
|
||||
Add secret <Icon name="plus" />
|
||||
</>
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
onClick={addNewSecretClick}
|
||||
className={`${baseClass}__add-secret-btn`}
|
||||
variant="text-icon"
|
||||
>
|
||||
Add secret <Icon name="plus" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onReturnToApp} variant="brand">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Button from "components/buttons/Button";
|
|||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Icon from "components/Icon";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "enroll-secrets";
|
||||
|
||||
|
|
@ -93,22 +94,29 @@ const EnrollSecretRow = ({
|
|||
};
|
||||
|
||||
const renderEditDeleteButtons = () => (
|
||||
<div className="buttons">
|
||||
<Button
|
||||
onClick={onEditSecretClick}
|
||||
className={`${baseClass}__edit-secret-icon`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDeleteSecretClick}
|
||||
className={`${baseClass}__delete-secret-icon`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<div className="buttons">
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
onClick={onEditSecretClick}
|
||||
className={`${baseClass}__edit-secret-icon`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDeleteSecretClick}
|
||||
disabled={disableChildren}
|
||||
className={`${baseClass}__delete-secret-icon`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { GraphicNames } from "components/graphics";
|
|||
import Icon from "components/Icon";
|
||||
import Graphic from "components/Graphic";
|
||||
import FileDetails from "components/FileDetails";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "file-uploader";
|
||||
|
||||
|
|
@ -54,6 +55,8 @@ interface IFileUploaderProps {
|
|||
name: string;
|
||||
platform?: string;
|
||||
};
|
||||
/** Indicates that this file uploader deals with an entity that can be managed by GitOps, and so should be disabled when gitops mode is enabled */
|
||||
gitopsCompatible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -72,6 +75,7 @@ export const FileUploader = ({
|
|||
onFileUpload,
|
||||
canEdit = false,
|
||||
fileDetails,
|
||||
gitopsCompatible = false,
|
||||
}: IFileUploaderProps) => {
|
||||
const [isFileSelected, setIsFileSelected] = useState(!!fileDetails);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -122,19 +126,40 @@ export const FileUploader = ({
|
|||
{additionalInfo && (
|
||||
<p className={`${baseClass}__additional-info`}>{additionalInfo}</p>
|
||||
)}
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
customOnKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<label htmlFor="upload-file">
|
||||
{buttonType === "link" && <Icon name="upload" />}
|
||||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
{gitopsCompatible ? (
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled || disableChildren}
|
||||
customOnKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<label htmlFor="upload-file">
|
||||
{buttonType === "link" && <Icon name="upload" />}
|
||||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant={buttonVariant}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
customOnKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
>
|
||||
<label htmlFor="upload-file">
|
||||
{buttonType === "link" && <Icon name="upload" />}
|
||||
<span>{buttonMessage}</span>
|
||||
</label>
|
||||
</Button>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
accept={accept}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import CustomLink from "components/CustomLink";
|
||||
import TooltipWrapper, {
|
||||
ITooltipWrapper,
|
||||
} from "components/TooltipWrapper/TooltipWrapper";
|
||||
import { AppContext } from "context/app";
|
||||
import React, { useContext } from "react";
|
||||
|
||||
interface IGitOpsModeTooltipWrapper {
|
||||
renderChildren: (disableChildren?: boolean) => React.ReactNode;
|
||||
position?: ITooltipWrapper["position"];
|
||||
tipOffset?: ITooltipWrapper["tipOffset"];
|
||||
fixedPositionStrategy?: ITooltipWrapper["fixedPositionStrategy"];
|
||||
}
|
||||
|
||||
const baseClass = "gitops-mode-tooltip-wrapper";
|
||||
|
||||
const GitOpsModeTooltipWrapper = ({
|
||||
position = "top",
|
||||
tipOffset,
|
||||
renderChildren,
|
||||
fixedPositionStrategy,
|
||||
}: IGitOpsModeTooltipWrapper) => {
|
||||
const { config } = useContext(AppContext);
|
||||
const gomEnabled = config?.gitops.gitops_mode_enabled;
|
||||
const repoURL = config?.gitops.repository_url;
|
||||
|
||||
if (!gomEnabled) {
|
||||
return <>{renderChildren()}</>;
|
||||
}
|
||||
|
||||
const tipContent = (
|
||||
// at this point repoURL will always be defined
|
||||
<>
|
||||
{repoURL && (
|
||||
<>
|
||||
Manage in{" "}
|
||||
<CustomLink newTab text="YAML" variant="tooltip-link" url={repoURL} />
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
(GitOps mode enabled)
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<TooltipWrapper
|
||||
className={baseClass}
|
||||
position={position}
|
||||
tipOffset={tipOffset}
|
||||
tipContent={tipContent}
|
||||
underline={false}
|
||||
showArrow
|
||||
fixedPositionStrategy={fixedPositionStrategy}
|
||||
>
|
||||
{renderChildren(true)}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitOpsModeTooltipWrapper;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.gitops-mode-tooltip-wrapper {
|
||||
.component__tooltip-wrapper {
|
||||
&__tip-text {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
frontend/components/GitOpsModeTooltipWrapper/index.ts
Normal file
1
frontend/components/GitOpsModeTooltipWrapper/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./GitOpsModeTooltipWrapper";
|
||||
|
|
@ -44,6 +44,7 @@ export interface ISQLEditorProps {
|
|||
onLoad?: (editor: IAceEditor) => void;
|
||||
onChange?: (value: string) => void;
|
||||
handleSubmit?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "sql-editor";
|
||||
|
|
@ -57,7 +58,7 @@ const SQLEditor = ({
|
|||
name = "query-editor",
|
||||
value,
|
||||
placeholder,
|
||||
readOnly,
|
||||
readOnly: _readOnly = false,
|
||||
maxLines = 20,
|
||||
showGutter = true,
|
||||
wrapEnabled = false,
|
||||
|
|
@ -69,10 +70,12 @@ const SQLEditor = ({
|
|||
onLoad,
|
||||
onChange,
|
||||
handleSubmit = noop,
|
||||
disabled = false,
|
||||
}: ISQLEditorProps): JSX.Element => {
|
||||
const editorRef = useRef<ReactAce>(null);
|
||||
const wrapperClass = classnames(className, wrapperClassName, baseClass, {
|
||||
[`${baseClass}__wrapper--error`]: !!error,
|
||||
[`${baseClass}__wrapper--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const fixHotkeys = (editor: IAceEditor) => {
|
||||
|
|
@ -82,6 +85,8 @@ const SQLEditor = ({
|
|||
|
||||
const langTools = ace.require("ace/ext/language_tools");
|
||||
|
||||
const readOnly = disabled || _readOnly;
|
||||
|
||||
// Error handling within checkTableValues
|
||||
|
||||
if (!readOnly) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
border: 1px solid $core-vibrant-red;
|
||||
}
|
||||
}
|
||||
&--disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_content {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Tooltip as ReactTooltip5, PlacesType } from "react-tooltip-5";
|
|||
|
||||
import { uniqueId } from "lodash";
|
||||
|
||||
interface ITooltipWrapper {
|
||||
export interface ITooltipWrapper {
|
||||
children: React.ReactNode;
|
||||
// default is bottom-start
|
||||
position?: PlacesType;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ interface IAutoSizeInputFieldProps {
|
|||
onBlur?: () => void;
|
||||
onChange: (newSelectedValue: string) => void;
|
||||
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
disableTabability?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "component__auto-size-input-field";
|
||||
|
|
@ -31,6 +32,7 @@ const AutoSizeInputField = ({
|
|||
onBlur = () => null,
|
||||
onChange,
|
||||
onKeyPress,
|
||||
disableTabability = false,
|
||||
}: IAutoSizeInputFieldProps): JSX.Element => {
|
||||
const inputClasses = classnames(baseClass, inputClassName, "no-hover", {
|
||||
[`${baseClass}--disabled`]: isDisabled,
|
||||
|
|
@ -79,7 +81,7 @@ const AutoSizeInputField = ({
|
|||
className={inputClasses}
|
||||
cols={1}
|
||||
rows={1}
|
||||
tabIndex={0}
|
||||
tabIndex={disableTabability ? -1 : 0}
|
||||
onFocus={onInputFocus}
|
||||
onBlur={onInputBlur}
|
||||
onKeyPress={onInputKeyPress}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,20 @@ interface ISliderProps {
|
|||
className?: string;
|
||||
helpText?: JSX.Element | string;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const baseClass = "fleet-slider";
|
||||
|
||||
const Slider = (props: ISliderProps): JSX.Element => {
|
||||
const { onChange, value, inactiveText, activeText, autoFocus } = props;
|
||||
const {
|
||||
onChange,
|
||||
value,
|
||||
inactiveText,
|
||||
activeText,
|
||||
autoFocus,
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const sliderRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
|
|
@ -38,8 +46,9 @@ const Slider = (props: ISliderProps): JSX.Element => {
|
|||
|
||||
const handleClick = (evt: React.MouseEvent) => {
|
||||
evt.preventDefault();
|
||||
if (disabled) return;
|
||||
|
||||
return onChange();
|
||||
onChange();
|
||||
};
|
||||
|
||||
const formFieldProps = pick(props, [
|
||||
|
|
@ -48,11 +57,15 @@ const Slider = (props: ISliderProps): JSX.Element => {
|
|||
"error",
|
||||
"name",
|
||||
"className",
|
||||
"disabled",
|
||||
]) as IFormFieldProps;
|
||||
|
||||
const wrapperClassNames = classnames(`${baseClass}__wrapper`, {
|
||||
[`${baseClass}__wrapper--disabled`]: disabled,
|
||||
});
|
||||
return (
|
||||
<FormField {...formFieldProps} type="slider">
|
||||
<div className={`${baseClass}__wrapper`}>
|
||||
<div className={wrapperClassNames}>
|
||||
<button
|
||||
className={`button button--unstyled ${sliderBtnClass}`}
|
||||
onClick={handleClick}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px; // Noticeable with help text below slider
|
||||
&--disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
|
|
|
|||
36
frontend/components/icons/GitOpsMode.tsx
Normal file
36
frontend/components/icons/GitOpsMode.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
// import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
|
||||
|
||||
interface IGitOpsModeIconProps {
|
||||
// size?: IconSizes;
|
||||
color?: Colors;
|
||||
}
|
||||
|
||||
const GitOpsMode = ({
|
||||
// size = "small",
|
||||
color = "core-fleet-white",
|
||||
}: IGitOpsModeIconProps) => {
|
||||
// const iconSize = ICON_SIZES[size];
|
||||
return (
|
||||
<svg
|
||||
// TODO - product redesign icon with square aspect ratio to allow dynamic sizing
|
||||
// width={iconSize}
|
||||
// height={iconSize}
|
||||
// viewBox={`0 0 ${iconSize} ${iconSize}`}
|
||||
width="12"
|
||||
height="13"
|
||||
viewBox="0 0 12 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.375 4.87499L2.625 4.87499V4.87499H3.375ZM3.75 10.875C3.75 11.0821 3.58211 11.25 3.375 11.25V12.75C4.41053 12.75 5.25 11.9105 5.25 10.875H3.75ZM3.375 11.25C3.16789 11.25 3 11.0821 3 10.875H1.5C1.5 11.9105 2.33947 12.75 3.375 12.75V11.25ZM3 10.875C3 10.6679 3.16789 10.5 3.375 10.5V9C2.33947 9 1.5 9.83947 1.5 10.875H3ZM3.375 10.5C3.58211 10.5 3.75 10.6679 3.75 10.875H5.25C5.25 9.83947 4.41053 9 3.375 9V10.5ZM10.125 7.875C10.125 8.08211 9.95711 8.25 9.75 8.25V9.75C10.7855 9.75 11.625 8.91053 11.625 7.875H10.125ZM9.75 8.25C9.54289 8.25 9.375 8.08211 9.375 7.875H7.875C7.875 8.91053 8.71447 9.75 9.75 9.75V8.25ZM9.375 7.875C9.375 7.66789 9.54289 7.5 9.75 7.5V6C8.71447 6 7.875 6.83947 7.875 7.875H9.375ZM9.75 7.5C9.95711 7.5 10.125 7.66789 10.125 7.875H11.625C11.625 6.83947 10.7855 6 9.75 6V7.5ZM3.75 2.625C3.75 2.83211 3.58211 3 3.375 3V4.5C4.41053 4.5 5.25 3.66053 5.25 2.625H3.75ZM3.375 3C3.16789 3 3 2.83211 3 2.625H1.5C1.5 3.66053 2.33947 4.5 3.375 4.5V3ZM3 2.625C3 2.41789 3.16789 2.25 3.375 2.25V0.75C2.33947 0.75 1.5 1.58947 1.5 2.625H3ZM3.375 2.25C3.58211 2.25 3.75 2.41789 3.75 2.625H5.25C5.25 1.58947 4.41053 0.75 3.375 0.75V2.25ZM3.375 4.09038C4.125 4.09038 4.125 4.09037 4.125 4.09036C4.125 4.09035 4.125 4.09034 4.125 4.09032C4.125 4.09027 4.125 4.09021 4.125 4.09013C4.125 4.08997 4.125 4.08973 4.125 4.0894C4.125 4.08876 4.125 4.08781 4.125 4.08656C4.125 4.08406 4.125 4.08039 4.125 4.07569C4.125 4.0663 4.125 4.0528 4.125 4.03637C4.125 4.00353 4.125 3.95899 4.125 3.91223C4.125 3.81871 4.125 3.71629 4.125 3.68071C4.125 3.67626 4.125 3.67286 4.125 3.67065C4.125 3.66954 4.125 3.66873 4.125 3.66825C4.125 3.668 4.125 3.66784 4.125 3.66777C4.125 3.6673 4.125 3.66795 4.125 3.66845C3.37468 4.41767 2.625 3.66777 2.625 3.66775C2.625 3.66775 2.625 3.66774 2.625 3.66776C2.625 3.66838 2.625 3.67436 2.625 3.68692C2.625 3.712 2.625 3.76327 2.625 3.85017C2.625 4.02397 2.625 4.34034 2.625 4.87499L4.125 4.87499C4.125 4.34034 4.125 4.02397 4.125 3.85017C4.125 3.76327 4.125 3.712 4.125 3.68691C4.125 3.67439 4.125 3.66836 4.125 3.6677C4.125 3.66768 4.125 3.66764 4.125 3.66762C4.125 3.66758 3.37532 2.91767 2.625 3.6669C2.625 3.66724 2.625 3.6676 2.625 3.66763C2.625 3.66765 2.625 3.66768 2.625 3.66771C2.625 3.66781 2.625 3.66798 2.625 3.66823C2.625 3.66872 2.625 3.66953 2.625 3.67064C2.625 3.67285 2.625 3.67625 2.625 3.6807C2.625 3.71629 2.625 3.81871 2.625 3.91223C2.625 3.95899 2.625 4.00353 2.625 4.03637C2.625 4.0528 2.625 4.0663 2.625 4.07569C2.625 4.08039 2.625 4.08406 2.625 4.08656C2.625 4.0878 2.625 4.08876 2.625 4.0894C2.625 4.08972 2.625 4.08997 2.625 4.09013C2.625 4.09021 2.625 4.09027 2.625 4.09031C2.625 4.09033 2.625 4.09035 2.625 4.09036C2.625 4.09037 2.625 4.09037 3.375 4.09038ZM2.625 4.87499V9.89513H4.125V4.87499H2.625ZM2.625 4.87499C2.625 6.22752 3.34275 7.39654 4.48217 8.09712C5.61733 8.79507 7.14267 9.01866 8.8069 8.60261L8.4431 7.14739C7.10733 7.48134 6.00767 7.27422 5.26783 6.81933C4.53225 6.36705 4.125 5.66106 4.125 4.87499L2.625 4.87499Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitOpsMode;
|
||||
|
|
@ -68,6 +68,7 @@ import Settings from "./Settings";
|
|||
import AutomaticSelfService from "./AutomaticSelfService";
|
||||
import User from "./User";
|
||||
import InfoOutline from "./InfoOutline";
|
||||
import GitOpsMode from "./GitOpsMode";
|
||||
|
||||
// a mapping of the usable names of icons to the icon source.
|
||||
export const ICON_MAP = {
|
||||
|
|
@ -142,6 +143,7 @@ export const ICON_MAP = {
|
|||
settings: Settings,
|
||||
"automatic-self-service": AutomaticSelfService,
|
||||
user: User,
|
||||
"gitops-mode": GitOpsMode,
|
||||
};
|
||||
|
||||
export type IconNames = keyof typeof ICON_MAP;
|
||||
|
|
|
|||
|
|
@ -2,15 +2,21 @@ import React, { useContext } from "react";
|
|||
import { Link } from "react-router";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { QueryParams } from "utilities/url";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { API_ALL_TEAMS_ID, APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { QueryParams } from "utilities/url";
|
||||
|
||||
import LinkWithContext from "components/LinkWithContext";
|
||||
// @ts-ignore
|
||||
import OrgLogoIcon from "components/icons/OrgLogoIcon";
|
||||
import Icon from "components/Icon";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import UserMenu from "../UserMenu";
|
||||
import getNavItems, { INavItem } from "./navItems";
|
||||
|
|
@ -70,6 +76,35 @@ const isGlobalPage = (path: string) => {
|
|||
return Object.values(REGEX_GLOBAL_PAGES).some((re) => path.match(re));
|
||||
};
|
||||
|
||||
const GitOpsModeIndicator = () => {
|
||||
const baseClass = "gitops-mode-indicator";
|
||||
const tipContent = (
|
||||
<>
|
||||
Items managed in YAML are read-only.
|
||||
<br />
|
||||
<CustomLink
|
||||
newTab
|
||||
text="Learn more"
|
||||
variant="tooltip-link"
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/ui-gitops-mode`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<TooltipWrapper
|
||||
position="bottom"
|
||||
underline={false}
|
||||
showArrow
|
||||
tipContent={tipContent}
|
||||
className={baseClass}
|
||||
tipOffset={2}
|
||||
>
|
||||
<Icon name="gitops-mode" />
|
||||
<div className={`${baseClass}__text`}>GitOps mode</div>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const SiteTopNav = ({
|
||||
config,
|
||||
currentUser,
|
||||
|
|
@ -203,18 +238,21 @@ const SiteTopNav = ({
|
|||
const renderNavItems = () => {
|
||||
return (
|
||||
<div className="site-nav-content">
|
||||
<ul className="site-nav-list">
|
||||
<ul className="site-nav-left">
|
||||
{userNavItems.map((navItem) => {
|
||||
return renderNavItem(navItem);
|
||||
})}
|
||||
</ul>
|
||||
<UserMenu
|
||||
onLogout={onLogoutUser}
|
||||
onUserMenuItemClick={onUserMenuItemClick}
|
||||
currentUser={currentUser}
|
||||
isAnyTeamAdmin={isAnyTeamAdmin}
|
||||
isGlobalAdmin={isGlobalAdmin}
|
||||
/>
|
||||
<div className="site-nav-right">
|
||||
{config.gitops.gitops_mode_enabled && <GitOpsModeIndicator />}
|
||||
<UserMenu
|
||||
onLogout={onLogoutUser}
|
||||
onUserMenuItemClick={onUserMenuItemClick}
|
||||
currentUser={currentUser}
|
||||
isAnyTeamAdmin={isAnyTeamAdmin}
|
||||
isGlobalAdmin={isGlobalAdmin}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.site-nav-list {
|
||||
.site-nav-left {
|
||||
list-style: none;
|
||||
height: 50px;
|
||||
width: 671px;
|
||||
|
|
@ -15,6 +15,34 @@
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.site-nav-right {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $pad-large;
|
||||
}
|
||||
|
||||
.gitops-mode-indicator {
|
||||
.component__tooltip-wrapper {
|
||||
&__element {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
color: $core-white;
|
||||
font-weight: $regular;
|
||||
font-size: $xxx-small;
|
||||
cursor: default;
|
||||
}
|
||||
&__tip-text {
|
||||
text-align: center;
|
||||
color: $core-white;
|
||||
font-size: $xx-small;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.site-nav-item {
|
||||
position: relative;
|
||||
transition: color 200ms ease-in-out;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export enum ActivityType {
|
|||
TransferredHosts = "transferred_hosts",
|
||||
EnabledWindowsMdm = "enabled_windows_mdm",
|
||||
DisabledWindowsMdm = "disabled_windows_mdm",
|
||||
EnabledGitOpsMode = "enabled_gitops_mode",
|
||||
DisabledGitOpsMode = "disabled_gitops_mode",
|
||||
EnabledWindowsMdmMigration = "enabled_windows_mdm_migration",
|
||||
DisabledWindowsMdmMigration = "disabled_windows_mdm_migration",
|
||||
RanScript = "ran_script",
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ export interface IConfig {
|
|||
};
|
||||
};
|
||||
mdm: IMdmConfig;
|
||||
gitops: IGitOpsModeConfig;
|
||||
}
|
||||
|
||||
export interface IWebhookSettings {
|
||||
|
|
@ -226,3 +227,7 @@ export const CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS = 30;
|
|||
export interface IUserSettings {
|
||||
hidden_host_columns: string[];
|
||||
}
|
||||
export interface IGitOpsModeConfig {
|
||||
gitops_mode_enabled: boolean;
|
||||
repository_url: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -648,6 +648,8 @@ const TAGGED_TEMPLATES = {
|
|||
disabledWindowsMdm: () => {
|
||||
return <> told Fleet to turn off Windows MDM features.</>;
|
||||
},
|
||||
enabledGitOpsMode: () => "enabled GitOps mode in the UI.",
|
||||
disabledGitOpsMode: () => "disabled GitOps mode in the UI.",
|
||||
enabledWindowsMdmMigration: () => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -1210,6 +1212,12 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
|
|||
case ActivityType.DisabledWindowsMdm: {
|
||||
return TAGGED_TEMPLATES.disabledWindowsMdm();
|
||||
}
|
||||
case ActivityType.EnabledGitOpsMode: {
|
||||
return TAGGED_TEMPLATES.enabledGitOpsMode();
|
||||
}
|
||||
case ActivityType.DisabledGitOpsMode: {
|
||||
return TAGGED_TEMPLATES.disabledGitOpsMode();
|
||||
}
|
||||
case ActivityType.EnabledWindowsMdmMigration: {
|
||||
return TAGGED_TEMPLATES.enabledWindowsMdmMigration();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
import React from "react";
|
||||
|
|
@ -17,16 +18,22 @@ const ProfileListHeading = ({
|
|||
Configuration profile
|
||||
</span>
|
||||
<span className={`${baseClass}__actions-heading`}>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
className={`${baseClass}__add-button`}
|
||||
onClick={onClickAddProfile}
|
||||
>
|
||||
<span className={`${baseClass}__icon-wrap`}>
|
||||
<Icon name="plus" />
|
||||
Add profile
|
||||
</span>
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="left"
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
variant="text-icon"
|
||||
className={`${baseClass}__add-button`}
|
||||
onClick={onClickAddProfile}
|
||||
>
|
||||
<span className={`${baseClass}__icon-wrap`}>
|
||||
<Icon name="plus" />
|
||||
Add profile
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Graphic from "components/Graphic";
|
|||
import Icon from "components/Icon";
|
||||
|
||||
import strUtils from "utilities/strings";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "profile-list-item";
|
||||
|
||||
|
|
@ -153,13 +154,18 @@ const ProfileListItem = ({
|
|||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${subClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(profile)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
className={`${subClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(profile)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import Card from "components/Card";
|
||||
import Button from "components/buttons/Button";
|
||||
import ProfileGraphic from "../AddProfileGraphic";
|
||||
|
|
@ -14,14 +15,20 @@ const AddProfileCard = ({ setShowModal }: IAddProfileCardProps) => (
|
|||
<Card color="grey" className={baseClass}>
|
||||
<div className={`${baseClass}__card--content-wrap`}>
|
||||
<ProfileGraphic baseClass={baseClass} showMessage />
|
||||
<Button
|
||||
className={`${baseClass}__card--add-button`}
|
||||
variant="brand"
|
||||
type="button"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Add profile
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
className={`${baseClass}__card--add-button`}
|
||||
variant="brand"
|
||||
type="button"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
Add profile
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import PremiumFeatureMessage from "components/PremiumFeatureMessage";
|
|||
import Spinner from "components/Spinner";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import DiskEncryptionTable from "./components/DiskEncryptionTable";
|
||||
|
||||
|
|
@ -197,6 +198,7 @@ const DiskEncryption = ({
|
|||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
disabled={config?.gitops.gitops_mode_enabled}
|
||||
onChange={onToggleCheckbox}
|
||||
value={diskEncryptionEnabled}
|
||||
className={`${baseClass}__checkbox`}
|
||||
|
|
@ -212,12 +214,18 @@ const DiskEncryption = ({
|
|||
newTab
|
||||
/>
|
||||
</p>
|
||||
<Button
|
||||
className={`${baseClass}__save-button`}
|
||||
onClick={onUpdateDiskEncryption}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={-12}
|
||||
renderChildren={(d) => (
|
||||
<Button
|
||||
disabled={d}
|
||||
className={`${baseClass}__save-button`}
|
||||
onClick={onUpdateDiskEncryption}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import InputField from "components/forms/fields/InputField";
|
|||
import Button from "components/buttons/Button";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { AppContext } from "context/app";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "apple-os-target-form";
|
||||
|
||||
|
|
@ -115,6 +117,7 @@ const AppleOSTargetForm = ({
|
|||
refetchTeamConfig,
|
||||
}: IAppleOSTargetFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [minOsVersion, setMinOsVersion] = useState(defaultMinOsVersion);
|
||||
|
|
@ -185,6 +188,7 @@ const AppleOSTargetForm = ({
|
|||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
label="Minimum version"
|
||||
disabled={gomEnabled}
|
||||
tooltip={getMinimumVersionTooltip()}
|
||||
helpText={
|
||||
<>
|
||||
|
|
@ -201,6 +205,7 @@ const AppleOSTargetForm = ({
|
|||
onChange={handleMinVersionChange}
|
||||
/>
|
||||
<InputField
|
||||
disabled={gomEnabled}
|
||||
label="Deadline"
|
||||
tooltip="The end user can't dismiss the OS update once they reach this deadline. Deadline is 12:00 (Noon), the host's local time."
|
||||
helpText="YYYY-MM-DD format only (e.g., “2024-07-01”)."
|
||||
|
|
@ -208,9 +213,14 @@ const AppleOSTargetForm = ({
|
|||
error={deadlineError}
|
||||
onChange={handleDeadlineChange}
|
||||
/>
|
||||
<Button type="submit" isLoading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
renderChildren={(dC) => (
|
||||
<Button disabled={dC} type="submit" isLoading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import React, { useContext, useState } from "react";
|
|||
import { isEmpty } from "lodash";
|
||||
|
||||
import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
import teamsAPI from "services/entities/teams";
|
||||
|
||||
|
|
@ -10,6 +13,7 @@ import teamsAPI from "services/entities/teams";
|
|||
import InputField from "components/forms/fields/InputField";
|
||||
import Button from "components/buttons/Button";
|
||||
import validatePresence from "components/forms/validators/validate_presence";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "windows-target-form";
|
||||
|
||||
|
|
@ -96,6 +100,8 @@ const WindowsTargetForm = ({
|
|||
refetchTeamConfig,
|
||||
}: IWindowsTargetFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [deadlineDays, setDeadlineDays] = useState(
|
||||
defaultDeadlineDays.toString()
|
||||
|
|
@ -155,6 +161,7 @@ const WindowsTargetForm = ({
|
|||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
disabled={gomEnabled}
|
||||
label="Deadline"
|
||||
tooltip="Number of days the end user has before updates are installed and the host is forced to restart."
|
||||
helpText="Number of days from 0 to 30."
|
||||
|
|
@ -163,6 +170,7 @@ const WindowsTargetForm = ({
|
|||
onChange={handleDeadlineDaysChange}
|
||||
/>
|
||||
<InputField
|
||||
disabled={gomEnabled}
|
||||
label="Grace period"
|
||||
tooltip="Number of days after the deadline the end user has before the host is forced to restart (only if end user was offline when deadline passed)."
|
||||
helpText="Number of days from 0 to 7."
|
||||
|
|
@ -170,9 +178,14 @@ const WindowsTargetForm = ({
|
|||
error={gracePeriodDaysError}
|
||||
onChange={handleGracePeriodDays}
|
||||
/>
|
||||
<Button type="submit" isLoading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
renderChildren={(dC) => (
|
||||
<Button disabled={dC} type="submit" isLoading={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import DataError from "components/DataError";
|
|||
import paths from "router/paths";
|
||||
import ActionsDropdown from "components/ActionsDropdown";
|
||||
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "script-details-modal";
|
||||
|
||||
|
|
@ -203,13 +204,19 @@ const ScriptDetailsModal = ({
|
|||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="bottom"
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
primaryButtons={
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Button from "components/buttons/Button";
|
|||
import Icon from "components/Icon";
|
||||
import ListItem from "components/ListItem";
|
||||
import { ISupportedGraphicNames } from "components/ListItem/ListItem";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
const baseClass = "script-list-item";
|
||||
|
||||
|
|
@ -95,6 +96,42 @@ const ScriptListItem = ({
|
|||
onDelete(script);
|
||||
};
|
||||
|
||||
const actions = (
|
||||
<>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
onClick={onClickEdit}
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="pencil" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
disabled={disableChildren}
|
||||
onClick={onClickDelete}
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
className={baseClass}
|
||||
|
|
@ -106,31 +143,7 @@ const ScriptListItem = ({
|
|||
createdAt={script.created_at}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickEdit}
|
||||
>
|
||||
<Icon name="pencil" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
actions={actions}
|
||||
onClick={() => onClickScript(script)}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const ScriptPackageUploader = ({
|
|||
accept=".sh,.ps1"
|
||||
onFileUpload={onUploadFile}
|
||||
isLoading={showLoading}
|
||||
gitopsCompatible
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import mdmAPI from "services/entities/mdm";
|
|||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import SelectSoftwareTable from "../SelectSoftwareTable";
|
||||
|
||||
|
|
@ -92,13 +93,19 @@ const SelectSoftwareModal = ({
|
|||
onChangeSelectAll={onChangeSelectAll}
|
||||
/>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={onSaveSelectedSoftware}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={6}
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
disabled={dC}
|
||||
variant="brand"
|
||||
onClick={onSaveSelectedSoftware}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button variant="inverse" onClick={onExit}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { APPLE_PLATFORM_DISPLAY_NAMES } from "interfaces/platform";
|
|||
import TextCell from "components/TableContainer/DataTable/TextCell";
|
||||
import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
export interface EnhancedSoftwareTitle extends ISoftwareTitle {
|
||||
isSelected: boolean;
|
||||
|
|
@ -40,7 +41,16 @@ const generateTableConfig = (
|
|||
cellProps.toggleAllRowsSelected();
|
||||
},
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={6}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: (cellProps: ISelectionCellProps) => {
|
||||
const { checked } = cellProps.row.getToggleRowSelectedProps();
|
||||
|
|
@ -51,7 +61,16 @@ const generateTableConfig = (
|
|||
cellProps.row.toggleRowSelected();
|
||||
},
|
||||
};
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={6}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ const SetupExperienceScriptUploader = ({
|
|||
buttonMessage="Upload"
|
||||
onFileUpload={onUploadFile}
|
||||
isLoading={showLoading}
|
||||
gitopsCompatible
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import { ILabelSummary } from "interfaces/label";
|
||||
import { PLATFORM_DISPLAY_NAMES } from "interfaces/platform";
|
||||
import { IAppStoreApp } from "interfaces/software";
|
||||
|
|
@ -12,7 +14,10 @@ import Button from "components/buttons/Button";
|
|||
import FileDetails from "components/FileDetails";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
|
||||
|
||||
import {
|
||||
CUSTOM_TARGET_OPTIONS,
|
||||
generateHelpText,
|
||||
|
|
@ -120,6 +125,8 @@ const SoftwareVppForm = ({
|
|||
isLoading = false,
|
||||
onCancel,
|
||||
}: ISoftwareVppFormProps) => {
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const [formData, setFormData] = useState<ISoftwareVppFormData>(
|
||||
softwareVppForEdit
|
||||
? {
|
||||
|
|
@ -211,7 +218,7 @@ const SoftwareVppForm = ({
|
|||
return (
|
||||
<div className={`${baseClass}__form-fields`}>
|
||||
<FileDetails
|
||||
graphicNames={"app-store"}
|
||||
graphicNames="app-store"
|
||||
fileDetails={{ name: softwareVppForEdit.name, platform: "macOS" }}
|
||||
canEdit={false}
|
||||
/>
|
||||
|
|
@ -278,6 +285,10 @@ const SoftwareVppForm = ({
|
|||
[`${baseClass}__content-disabled`]: isLoading,
|
||||
});
|
||||
|
||||
const formContentClasses = classnames(`${baseClass}__form-content`, {
|
||||
[`${baseClass}__form-content--disabled`]: gomEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={onFormSubmit}>
|
||||
{isLoading && <div className={`${baseClass}__overlay`} />}
|
||||
|
|
@ -285,21 +296,28 @@ const SoftwareVppForm = ({
|
|||
{!softwareVppForEdit && (
|
||||
<p>Apple App Store apps purchased via Apple Business Manager:</p>
|
||||
)}
|
||||
<div className={`${baseClass}__form-content`}>
|
||||
<div className={formContentClasses}>
|
||||
<>{renderContent()}</>
|
||||
<div className={`${baseClass}__action-buttons`}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
disabled={isSubmitDisabled}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{softwareVppForEdit ? "Save" : "Add software"}
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__action-buttons`}>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="bottom"
|
||||
tipOffset={8}
|
||||
renderChildren={(disableChildren) => (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
disabled={disableChildren || isSubmitDisabled}
|
||||
isLoading={isLoading}
|
||||
className={`${baseClass}__add-secret-btn`}
|
||||
>
|
||||
{softwareVppForEdit ? "Save" : "Add software"}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $pad-xxlarge;
|
||||
&--disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&__form-fields {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ const SoftwareCustomPackage = ({
|
|||
className={`${baseClass}__package-form`}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
// TODO - unnecessary if all uses of `PackageForm` are gitops compatible - TBD by product
|
||||
gitopsCompatible
|
||||
/>
|
||||
{uploadDetails && (
|
||||
<FileProgressModal
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { Link } from "react-router";
|
||||
import PATHS from "router/paths";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import {
|
||||
IJiraIntegration,
|
||||
|
|
@ -18,7 +19,6 @@ import {
|
|||
import configAPI from "services/entities/config";
|
||||
import { SUPPORT_LINK } from "utilities/constants";
|
||||
|
||||
import ReactTooltip from "react-tooltip";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import Modal from "components/Modal";
|
||||
|
|
@ -29,11 +29,12 @@ import Radio from "components/forms/fields/Radio";
|
|||
import InputField from "components/forms/fields/InputField";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import { IWebhookSoftwareVulnerabilities } from "interfaces/webhook";
|
||||
import useDeepEffect from "hooks/useDeepEffect";
|
||||
import { isEmpty, omit } from "lodash";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
import PreviewPayloadModal from "../PreviewPayloadModal";
|
||||
import PreviewTicketModal from "../PreviewTicketModal";
|
||||
|
|
@ -113,6 +114,8 @@ const ManageAutomationsModal = ({
|
|||
setSelectedIntegration,
|
||||
] = useState<IIntegration>();
|
||||
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
useDeepEffect(() => {
|
||||
setSoftwareAutomationsEnabled(
|
||||
softwareVulnerabilityAutomationEnabled || false
|
||||
|
|
@ -351,6 +354,7 @@ const ManageAutomationsModal = ({
|
|||
(zendeskIntegrationsIndexed &&
|
||||
zendeskIntegrationsIndexed.length > 0) ? (
|
||||
<Dropdown
|
||||
disabled={gomEnabled}
|
||||
searchable
|
||||
options={createIntegrationDropdownOptions()}
|
||||
onChange={onChangeSelectIntegration}
|
||||
|
|
@ -408,7 +412,7 @@ const ManageAutomationsModal = ({
|
|||
}
|
||||
placeholder="https://server.com/example"
|
||||
tooltip="Provide a URL to deliver a webhook request to."
|
||||
disabled={!softwareAutomationsEnabled}
|
||||
disabled={!softwareAutomationsEnabled || gomEnabled}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -435,6 +439,54 @@ const ManageAutomationsModal = ({
|
|||
return <PreviewPayloadModal onCancel={togglePreviewPayloadModal} />;
|
||||
}
|
||||
|
||||
const renderSaveButton = () => {
|
||||
const hasIntegrations = !(
|
||||
((jiraIntegrationsIndexed && jiraIntegrationsIndexed.length === 0) ||
|
||||
(zendeskIntegrationsIndexed &&
|
||||
zendeskIntegrationsIndexed.length === 0)) &&
|
||||
integrationEnabled &&
|
||||
softwareAutomationsEnabled
|
||||
);
|
||||
const renderRawButton = (gomDisabled = false) => (
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
Add an integration to create
|
||||
<br /> tickets for vulnerability automations.
|
||||
</>
|
||||
}
|
||||
disableTooltip={hasIntegrations || gomDisabled}
|
||||
position="bottom"
|
||||
underline={false}
|
||||
showArrow
|
||||
tipOffset={6}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
disabled={
|
||||
(softwareAutomationsEnabled &&
|
||||
integrationEnabled &&
|
||||
!selectedIntegration) ||
|
||||
(softwareAutomationsEnabled &&
|
||||
!integrationEnabled &&
|
||||
destinationUrl === "") ||
|
||||
gomDisabled
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
);
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={renderRawButton}
|
||||
tipOffset={6}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onExit={onReturnToApp}
|
||||
|
|
@ -444,6 +496,7 @@ const ManageAutomationsModal = ({
|
|||
>
|
||||
<div className={`${baseClass} form`}>
|
||||
<Slider
|
||||
disabled={gomEnabled}
|
||||
value={softwareAutomationsEnabled}
|
||||
onChange={() =>
|
||||
setSoftwareAutomationsEnabled(!softwareAutomationsEnabled)
|
||||
|
|
@ -466,7 +519,7 @@ const ManageAutomationsModal = ({
|
|||
value="ticket"
|
||||
name="workflow-type"
|
||||
onChange={onRadioChange(true)}
|
||||
disabled={!softwareAutomationsEnabled}
|
||||
disabled={!softwareAutomationsEnabled || gomEnabled}
|
||||
/>
|
||||
<Radio
|
||||
className={`${baseClass}__radio-input`}
|
||||
|
|
@ -476,7 +529,7 @@ const ManageAutomationsModal = ({
|
|||
value="webhook"
|
||||
name="workflow-type"
|
||||
onChange={onRadioChange(false)}
|
||||
disabled={!softwareAutomationsEnabled}
|
||||
disabled={!softwareAutomationsEnabled || gomEnabled}
|
||||
/>
|
||||
</div>
|
||||
{integrationEnabled ? renderTicket() : renderWebhook()}
|
||||
|
|
@ -492,49 +545,7 @@ const ManageAutomationsModal = ({
|
|||
</p>
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<div
|
||||
data-tip
|
||||
data-for="save-automation-button"
|
||||
data-tip-disable={
|
||||
!(
|
||||
((jiraIntegrationsIndexed &&
|
||||
jiraIntegrationsIndexed.length === 0) ||
|
||||
(zendeskIntegrationsIndexed &&
|
||||
zendeskIntegrationsIndexed.length === 0)) &&
|
||||
integrationEnabled &&
|
||||
softwareAutomationsEnabled
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={handleSaveAutomation}
|
||||
disabled={
|
||||
(softwareAutomationsEnabled &&
|
||||
integrationEnabled &&
|
||||
!selectedIntegration) ||
|
||||
(softwareAutomationsEnabled &&
|
||||
!integrationEnabled &&
|
||||
destinationUrl === "")
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
className={`save-automation-button-tooltip`}
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
id="save-automation-button"
|
||||
data-html
|
||||
>
|
||||
<>
|
||||
Add an integration to create
|
||||
<br /> tickets for vulnerability automations.
|
||||
</>
|
||||
</ReactTooltip>
|
||||
{renderSaveButton()}
|
||||
<Button onClick={onReturnToApp} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -20,4 +20,7 @@
|
|||
&__no-integrations a {
|
||||
display: block;
|
||||
}
|
||||
.component__tooltip-wrapper__tip-text {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import React, { useContext, useState, useEffect, useCallback } from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { getFileDetails } from "utilities/file/fileUtils";
|
||||
import getDefaultInstallScript from "utilities/software_install_scripts";
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
InstallTypeSection,
|
||||
} from "pages/SoftwarePage/helpers";
|
||||
import TargetLabelSelector from "components/TargetLabelSelector";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import PackageAdvancedOptions from "../PackageAdvancedOptions";
|
||||
|
||||
|
|
@ -64,6 +66,8 @@ interface IPackageFormProps {
|
|||
defaultUninstallScript?: string;
|
||||
defaultSelfService?: boolean;
|
||||
className?: string;
|
||||
/** Indicates that this PackageFOrm deals with an entity that can be managed by GitOps, and so should be disabled when gitops mode is enabled */
|
||||
gitopsCompatible?: boolean;
|
||||
}
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb,.rpm";
|
||||
|
|
@ -82,8 +86,10 @@ const PackageForm = ({
|
|||
defaultUninstallScript,
|
||||
defaultSelfService,
|
||||
className,
|
||||
gitopsCompatible = false,
|
||||
}: IPackageFormProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const initialFormData: IPackageFormData = {
|
||||
software: defaultSoftware || null,
|
||||
|
|
@ -238,66 +244,86 @@ const PackageForm = ({
|
|||
fileDetails={
|
||||
formData.software ? getFileDetails(formData.software) : undefined
|
||||
}
|
||||
gitopsCompatible={gitopsCompatible}
|
||||
/>
|
||||
{!isEditingSoftware && (
|
||||
<InstallTypeSection
|
||||
className={baseClass}
|
||||
isCustomPackage
|
||||
isExeCustomPackage={isExePackage}
|
||||
installType={formData.installType}
|
||||
onChangeInstallType={onChangeInstallType}
|
||||
/>
|
||||
)}
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={formData.targetType}
|
||||
selectedCustomTarget={formData.customTarget}
|
||||
selectedLabels={formData.labelTargets}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={onSelectTargetType}
|
||||
onSelectCustomTarget={onSelectCustomTarget}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
dropdownHelpText={
|
||||
formData.targetType === "Custom" &&
|
||||
generateHelpText(formData.installType, formData.customTarget)
|
||||
<div
|
||||
// including `form` class here keeps the children fields subject to the global form
|
||||
// children styles
|
||||
className={
|
||||
gitopsCompatible && gomEnabled
|
||||
? `${baseClass}__form-fields--gitops-disabled form`
|
||||
: "form"
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
value={formData.selfService}
|
||||
onChange={onToggleSelfServiceCheckbox}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
End users can install from{" "}
|
||||
<b>Fleet Desktop {">"} Self-service</b>.
|
||||
</>
|
||||
{!isEditingSoftware && (
|
||||
<InstallTypeSection
|
||||
className={`${baseClass}__install-type`}
|
||||
isCustomPackage
|
||||
isExeCustomPackage={isExePackage}
|
||||
installType={formData.installType}
|
||||
onChangeInstallType={onChangeInstallType}
|
||||
/>
|
||||
)}
|
||||
<TargetLabelSelector
|
||||
selectedTargetType={formData.targetType}
|
||||
selectedCustomTarget={formData.customTarget}
|
||||
selectedLabels={formData.labelTargets}
|
||||
customTargetOptions={CUSTOM_TARGET_OPTIONS}
|
||||
className={`${baseClass}__target`}
|
||||
onSelectTargetType={onSelectTargetType}
|
||||
onSelectCustomTarget={onSelectCustomTarget}
|
||||
onSelectLabel={onSelectLabel}
|
||||
labels={labels || []}
|
||||
dropdownHelpText={
|
||||
formData.targetType === "Custom" &&
|
||||
generateHelpText(formData.installType, formData.customTarget)
|
||||
}
|
||||
/>
|
||||
<Checkbox
|
||||
value={formData.selfService}
|
||||
onChange={onToggleSelfServiceCheckbox}
|
||||
>
|
||||
Self-service
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<PackageAdvancedOptions
|
||||
showSchemaButton={showSchemaButton}
|
||||
selectedPackage={formData.software}
|
||||
errors={{
|
||||
preInstallQuery: formValidation.preInstallQuery?.message,
|
||||
}}
|
||||
preInstallQuery={formData.preInstallQuery}
|
||||
installScript={formData.installScript}
|
||||
postInstallScript={formData.postInstallScript}
|
||||
uninstallScript={formData.uninstallScript}
|
||||
onClickShowSchema={onClickShowSchema}
|
||||
onChangePreInstallQuery={onChangePreInstallQuery}
|
||||
onChangeInstallScript={onChangeInstallScript}
|
||||
onChangePostInstallScript={onChangePostInstallScript}
|
||||
onChangeUninstallScript={onChangeUninstallScript}
|
||||
/>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
<>
|
||||
End users can install from{" "}
|
||||
<b>Fleet Desktop {">"} Self-service</b>.
|
||||
</>
|
||||
}
|
||||
>
|
||||
Self-service
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<PackageAdvancedOptions
|
||||
showSchemaButton={showSchemaButton}
|
||||
selectedPackage={formData.software}
|
||||
errors={{
|
||||
preInstallQuery: formValidation.preInstallQuery?.message,
|
||||
}}
|
||||
preInstallQuery={formData.preInstallQuery}
|
||||
installScript={formData.installScript}
|
||||
postInstallScript={formData.postInstallScript}
|
||||
uninstallScript={formData.uninstallScript}
|
||||
onClickShowSchema={onClickShowSchema}
|
||||
onChangePreInstallQuery={onChangePreInstallQuery}
|
||||
onChangeInstallScript={onChangeInstallScript}
|
||||
onChangePostInstallScript={onChangePostInstallScript}
|
||||
onChangeUninstallScript={onChangeUninstallScript}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-buttons">
|
||||
<Button type="submit" variant="brand" disabled={isSubmitDisabled}>
|
||||
{isEditingSoftware ? "Save" : "Add software"}
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={6}
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
disabled={dC || isSubmitDisabled}
|
||||
>
|
||||
{isEditingSoftware ? "Save" : "Add software"}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -35,4 +35,10 @@
|
|||
.info-banner {
|
||||
margin-top: $pad-small;
|
||||
}
|
||||
|
||||
&__form-fields {
|
||||
&--gitops-disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ISideNavItem } from "../components/SideNav/SideNav";
|
|||
import Integrations from "./cards/Integrations";
|
||||
import MdmSettings from "./cards/MdmSettings";
|
||||
import Calendars from "./cards/Calendars";
|
||||
import ChangeManagement from "./cards/ChangeManagement";
|
||||
|
||||
const integrationSettingsNavItems: ISideNavItem<any>[] = [
|
||||
// TODO: types
|
||||
|
|
@ -27,4 +28,13 @@ const integrationSettingsNavItems: ISideNavItem<any>[] = [
|
|||
},
|
||||
];
|
||||
|
||||
if (featureFlags.allowGitOpsMode === "true") {
|
||||
integrationSettingsNavItems.push({
|
||||
title: "Change management",
|
||||
urlSection: "change-management",
|
||||
path: PATHS.ADMIN_INTEGRATIONS_CHANGE_MANAGEMENT,
|
||||
Card: ChangeManagement,
|
||||
});
|
||||
}
|
||||
|
||||
export default integrationSettingsNavItems;
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ const IntegrationsPage = ({
|
|||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Add ticket destinations and turn on mobile device management features.
|
||||
</p>
|
||||
<SideNav
|
||||
className={`${baseClass}__side-nav`}
|
||||
navItems={navItems}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,2 @@
|
|||
.integrations {
|
||||
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@include sticky-settings-description;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
import Button from "components/buttons/Button";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import SectionHeader from "components/SectionHeader";
|
||||
import React, { useContext, useState } from "react";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import validUrl from "components/forms/validators/valid_url";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import { IConfig } from "interfaces/config";
|
||||
import { IFormField } from "interfaces/form_field";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import configAPI from "services/entities/config";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import Spinner from "components/Spinner";
|
||||
import DataError from "components/DataError";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
const baseClass = "change-management";
|
||||
|
||||
interface IChangeManagementFormData {
|
||||
gomEnabled: boolean;
|
||||
repoURL: string;
|
||||
}
|
||||
|
||||
interface IChangeManagementFormErrors {
|
||||
repository_url?: string | null;
|
||||
}
|
||||
|
||||
const validate = (formData: IChangeManagementFormData) => {
|
||||
const errs: IChangeManagementFormErrors = {};
|
||||
const { gomEnabled, repoURL } = formData;
|
||||
if (gomEnabled) {
|
||||
if (!repoURL) {
|
||||
errs.repository_url =
|
||||
"Git repository URL is required when GitOps mode is enabled";
|
||||
} else if (!validUrl({ url: repoURL })) {
|
||||
errs.repository_url = "Git repository URL must be a valid URL";
|
||||
}
|
||||
}
|
||||
return errs;
|
||||
};
|
||||
|
||||
const ChangeManagement = () => {
|
||||
const { setConfig } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [formData, setFormData] = useState<IChangeManagementFormData>({
|
||||
// dummy 0 values, will be populated with fresh config API response
|
||||
gomEnabled: false,
|
||||
repoURL: "",
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState<IChangeManagementFormErrors>({});
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
const {
|
||||
isLoading: isLoadingConfig,
|
||||
error: isLoadingConfigError,
|
||||
refetch: refetchConfig,
|
||||
} = useQuery<IConfig, Error, IConfig>(
|
||||
["integrations"],
|
||||
() => configAPI.loadAll(),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
const {
|
||||
gitops: { gitops_mode_enabled: gomEnabled, repository_url: repoURL },
|
||||
} = data;
|
||||
setFormData({ gomEnabled, repoURL });
|
||||
setConfig(data);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { gomEnabled, repoURL } = formData;
|
||||
|
||||
if (isLoadingConfig) {
|
||||
return <Spinner />;
|
||||
}
|
||||
if (isLoadingConfigError) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
const handleSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const errs = validate(formData);
|
||||
if (Object.keys(errs).length > 0) {
|
||||
setFormErrors(errs);
|
||||
return;
|
||||
}
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await configAPI.update({
|
||||
gitops: {
|
||||
gitops_mode_enabled: formData.gomEnabled,
|
||||
repository_url: formData.repoURL,
|
||||
},
|
||||
});
|
||||
renderFlash("success", "Successfully updated settings");
|
||||
setIsUpdating(false);
|
||||
refetchConfig();
|
||||
} catch (e) {
|
||||
const message = getErrorReason(e);
|
||||
renderFlash("error", message || "Failed ot update settings");
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onInputChange = ({ name, value }: IFormField) => {
|
||||
const newFormData = { ...formData, [name]: value };
|
||||
setFormData(newFormData);
|
||||
const newErrs = validate(newFormData);
|
||||
// only set errors that are updates of existing errors
|
||||
// new errors are only set onBlur or submit
|
||||
const errsToSet: Record<string, string> = {};
|
||||
Object.keys(formErrors).forEach((k) => {
|
||||
// @ts-ignore
|
||||
if (newErrs[k]) {
|
||||
// @ts-ignore
|
||||
errsToSet[k] = newErrs[k];
|
||||
}
|
||||
});
|
||||
setFormErrors(errsToSet);
|
||||
};
|
||||
|
||||
const onInputBlur = () => {
|
||||
setFormErrors(validate(formData));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SectionHeader title="Change management" />
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
When using a git repository to manage Fleet, you can optionally put the
|
||||
UI in GitOps mode. This prevents you from making changes in the UI that
|
||||
would be overridden by GitOps workflows.
|
||||
</p>
|
||||
<CustomLink
|
||||
newTab
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/gitops`}
|
||||
text="Learn more about GitOps"
|
||||
/>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Checkbox
|
||||
onChange={onInputChange}
|
||||
name="gomEnabled"
|
||||
value={gomEnabled}
|
||||
parseTarget
|
||||
>
|
||||
<TooltipWrapper tipContent="GitOps mode is a UI-only setting. API permissions are restricted based on user role.">
|
||||
Enable GitOps mode
|
||||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
{/* Git repository URL */}
|
||||
<InputField
|
||||
label="Git repository URL"
|
||||
onChange={onInputChange}
|
||||
name="repoURL"
|
||||
value={repoURL}
|
||||
parseTarget
|
||||
onBlur={onInputBlur}
|
||||
error={formErrors.repository_url}
|
||||
helpText="When GitOps mode is enabled, you will be directed here to make changes."
|
||||
disabled={!gomEnabled}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!!Object.keys(formErrors).length}
|
||||
isLoading={isUpdating}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeManagement;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.change-management {
|
||||
.custom-link {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./ChangeManagement";
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
.integrations-management {
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
}
|
||||
|
||||
.no-integrations {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -128,9 +128,6 @@ const OrgSettingsPage = ({ params, router }: IOrgSettingsPageProps) => {
|
|||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Set your organization information and configure SSO and SMTP.
|
||||
</p>
|
||||
<SideNav
|
||||
className={`${baseClass}__side-nav`}
|
||||
navItems={navItems}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
.org-settings {
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@include sticky-settings-description;
|
||||
}
|
||||
|
||||
&__sandbox-demo-message {
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,9 +252,6 @@ const TeamManagementPage = (): JSX.Element => {
|
|||
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Create, customize, and remove teams from Fleet.
|
||||
</p>
|
||||
<SandboxGate
|
||||
fallbackComponent={() => (
|
||||
<SandboxMessage
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
.team-management {
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@include sticky-settings-description;
|
||||
padding-bottom: $pad-medium;
|
||||
}
|
||||
.data-table-block {
|
||||
.data-table__table {
|
||||
thead {
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ interface IUserManagementProps {
|
|||
const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
return (
|
||||
<div className={`${baseClass}`}>
|
||||
<p className={`${baseClass}__page-description`}>
|
||||
Create new users, customize user permissions, and remove users from
|
||||
Fleet.
|
||||
</p>
|
||||
<UsersTable router={router} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
.user-management {
|
||||
&__page-description {
|
||||
font-size: $x-small;
|
||||
color: $core-fleet-black;
|
||||
@include sticky-settings-description;
|
||||
padding-bottom: $pad-medium;
|
||||
}
|
||||
|
||||
&__api-only-user {
|
||||
@include grey-badge;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,17 @@
|
|||
.component__tabs-wrapper {
|
||||
top: $pad-xxlarge; // for sticky
|
||||
z-index: 3;
|
||||
padding-bottom: $pad-xxlarge;
|
||||
}
|
||||
|
||||
.side-nav__card-container {
|
||||
|
||||
// all side navs in the admin section we want to limit the max width
|
||||
>* {
|
||||
> * {
|
||||
width: 100%;
|
||||
max-width: $settings-form-max-width;
|
||||
}
|
||||
}
|
||||
.side-nav__nav-list {
|
||||
position: initial;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -562,19 +562,14 @@ const ManagePolicyPage = ({
|
|||
renderFlash("success", "No changes detected.");
|
||||
return;
|
||||
}
|
||||
const responses: Promise<
|
||||
ReturnType<typeof teamPoliciesAPI.update>
|
||||
>[] = [];
|
||||
responses.concat(
|
||||
changedPolicies.map((changedPolicy) => {
|
||||
return teamPoliciesAPI.update(changedPolicy.id, {
|
||||
// "software_title_id": null will unset software install for the policy
|
||||
// "software_title_id": X will set the value to the given integer (except 0).
|
||||
software_title_id: changedPolicy.swIdToInstall || null,
|
||||
team_id: teamIdForApi,
|
||||
});
|
||||
})
|
||||
);
|
||||
const responses = changedPolicies.map((changedPolicy) => {
|
||||
return teamPoliciesAPI.update(changedPolicy.id, {
|
||||
// "software_title_id": null will unset software install for the policy
|
||||
// "software_title_id": X will set the value to the given integer (except 0).
|
||||
software_title_id: changedPolicy.swIdToInstall || null,
|
||||
team_id: teamIdForApi,
|
||||
});
|
||||
});
|
||||
await Promise.all(responses);
|
||||
await wait(100); // prevent race
|
||||
refetchTeamPolicies();
|
||||
|
|
@ -897,6 +892,8 @@ const ManagePolicyPage = ({
|
|||
config?.integrations.google_calendar.length > 0) ??
|
||||
false;
|
||||
|
||||
const gomEnabled = config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const isCalEventsEnabled =
|
||||
teamConfig?.integrations.google_calendar?.enable_calendar_events ?? false;
|
||||
|
||||
|
|
@ -1107,6 +1104,7 @@ const ManagePolicyPage = ({
|
|||
url={teamConfig?.integrations.google_calendar?.webhook_url || ""}
|
||||
policies={policiesAvailableToAutomate}
|
||||
isUpdating={isUpdatingPolicies}
|
||||
gomEnabled={gomEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,18 @@
|
|||
import React from "react";
|
||||
|
||||
import { noop } from "lodash";
|
||||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import createMockPolicy from "__mocks__/policyMock";
|
||||
|
||||
import CalendarEventsModal from "./CalendarEventsModal";
|
||||
|
||||
const testGlobalPolicy = [
|
||||
const testGlobalPolicies = [
|
||||
createMockPolicy({ team_id: null, name: "Inherited policy 1" }),
|
||||
createMockPolicy({ id: 2, team_id: null, name: "Inherited policy 2" }),
|
||||
createMockPolicy({ id: 3, team_id: null, name: "Inherited policy 3" }),
|
||||
];
|
||||
|
||||
const testTeamPolicies = [
|
||||
createMockPolicy({ id: 4, team_id: 2, name: "Team policy 1" }),
|
||||
createMockPolicy({ id: 5, team_id: 2, name: "Team policy 2" }),
|
||||
];
|
||||
|
||||
describe("CalendarEventsModal - component", () => {
|
||||
it("renders components for admin", async () => {
|
||||
const render = createCustomRenderer({
|
||||
|
|
@ -37,7 +32,7 @@ describe("CalendarEventsModal - component", () => {
|
|||
configured
|
||||
enabled
|
||||
url="https://server.com/example"
|
||||
policies={testGlobalPolicy}
|
||||
policies={testGlobalPolicies}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -70,7 +65,7 @@ describe("CalendarEventsModal - component", () => {
|
|||
configured
|
||||
enabled
|
||||
url="https://server.com/example"
|
||||
policies={testGlobalPolicy}
|
||||
policies={testGlobalPolicies}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -88,4 +83,72 @@ describe("CalendarEventsModal - component", () => {
|
|||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("disables submission in GitOps mode", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
isTeamAdmin: false,
|
||||
// @ts-ignore
|
||||
config: {
|
||||
gitops: {
|
||||
gitops_mode_enabled: true,
|
||||
repository_url: "a.b.cc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
const { user } = render(
|
||||
<CalendarEventsModal
|
||||
onExit={noop}
|
||||
onSubmit={onSubmit}
|
||||
isUpdating={false}
|
||||
configured
|
||||
enabled
|
||||
url="https://server.com/example"
|
||||
policies={testGlobalPolicies}
|
||||
gomEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Resolution webhook URL/i)).toBeInTheDocument();
|
||||
const save = screen.getByRole("button", { name: /Save/i });
|
||||
expect(save).toBeInTheDocument();
|
||||
await user.click(save);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
it("allows submission", async () => {
|
||||
const render = createCustomRenderer({
|
||||
context: {
|
||||
app: {
|
||||
isGlobalAdmin: true,
|
||||
isTeamAdmin: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
const { user } = render(
|
||||
<CalendarEventsModal
|
||||
onExit={noop}
|
||||
onSubmit={onSubmit}
|
||||
isUpdating={false}
|
||||
configured
|
||||
enabled
|
||||
url="https://server.com/example"
|
||||
policies={testGlobalPolicies}
|
||||
/>
|
||||
);
|
||||
|
||||
const save = screen.getByRole("button", { name: /Save/i });
|
||||
expect(save).toBeInTheDocument();
|
||||
await user.click(save);
|
||||
expect(onSubmit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import Modal from "components/Modal";
|
|||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import Icon from "components/Icon";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
import CalendarEventPreviewModal from "../CalendarEventPreviewModal";
|
||||
import CalendarPreview from "../../../../../../assets/images/calendar-preview-720x436@2x.png";
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ interface ICalendarEventsModal {
|
|||
enabled: boolean;
|
||||
url: string;
|
||||
policies: IPolicyStats[];
|
||||
gomEnabled?: boolean;
|
||||
}
|
||||
|
||||
// allows any policy name to be the name of a form field, one of the checkboxes
|
||||
|
|
@ -52,6 +54,7 @@ const CalendarEventsModal = ({
|
|||
enabled,
|
||||
url,
|
||||
policies,
|
||||
gomEnabled = false,
|
||||
}: ICalendarEventsModal) => {
|
||||
const { isGlobalAdmin, isTeamAdmin } = useContext(AppContext);
|
||||
|
||||
|
|
@ -203,7 +206,7 @@ const CalendarEventsModal = ({
|
|||
onChange={() => {
|
||||
onPolicyEnabledChange({ name, value: !isChecked });
|
||||
}}
|
||||
disabled={!formData.enabled}
|
||||
disabled={!formData.enabled || gomEnabled}
|
||||
>
|
||||
<TooltipTruncatedText value={name} />
|
||||
</Checkbox>
|
||||
|
|
@ -273,6 +276,7 @@ const CalendarEventsModal = ({
|
|||
onChange={onFeatureEnabledChange}
|
||||
inactiveText="Disabled"
|
||||
activeText="Enabled"
|
||||
disabled={gomEnabled}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -337,7 +341,7 @@ const CalendarEventsModal = ({
|
|||
error={formErrors.url}
|
||||
tooltip="Provide a URL to deliver a webhook request to."
|
||||
helpText="A request will be sent to this URL during the calendar event. Use it to trigger auto-remediation."
|
||||
disabled={!formData.enabled}
|
||||
disabled={!formData.enabled || gomEnabled}
|
||||
/>
|
||||
<RevealButton
|
||||
isShowing={showExamplePayload}
|
||||
|
|
@ -356,16 +360,20 @@ const CalendarEventsModal = ({
|
|||
{renderPolicies()}
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onUpdateCalendarEvents}
|
||||
className="save-loading"
|
||||
isLoading={isUpdating}
|
||||
disabled={Object.keys(formErrors).length > 0}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onUpdateCalendarEvents}
|
||||
className="save-loading"
|
||||
isLoading={isUpdating}
|
||||
disabled={Object.keys(formErrors).length > 0 || dC}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button onClick={onExit} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { PolicyResponse } from "utilities/constants";
|
|||
import { createHostsByPolicyPath } from "utilities/helpers";
|
||||
import InheritedBadge from "components/InheritedBadge";
|
||||
import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import PassingColumnHeader from "../PassingColumnHeader";
|
||||
|
||||
interface IGetToggleAllRowsSelectedProps {
|
||||
|
|
@ -268,7 +270,16 @@ const generateTableHeaders = (
|
|||
const checkboxProps = viewingTeamPolicies
|
||||
? teamCheckboxProps
|
||||
: regularCheckboxProps;
|
||||
return <Checkbox {...checkboxProps} enableEnterToCheck />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={8}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} enableEnterToCheck {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
const inheritedPolicy = cellProps.row.original.team_id === null;
|
||||
|
|
@ -283,7 +294,16 @@ const generateTableHeaders = (
|
|||
return <></>;
|
||||
}
|
||||
|
||||
return <Checkbox {...checkboxProps} enableEnterToCheck />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={8}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} enableEnterToCheck {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disableHidden: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import TooltipWrapper from "components/TooltipWrapper";
|
|||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import SaveNewPolicyModal from "../SaveNewPolicyModal";
|
||||
|
||||
const baseClass = "policy-form";
|
||||
|
|
@ -127,6 +129,11 @@ const PolicyForm = ({
|
|||
config,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
const aiFeaturesDisabled =
|
||||
config?.server_settings.ai_features_disabled || false;
|
||||
const gomEnabled = config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const debounceSQL = useDebouncedCallback((sql: string) => {
|
||||
const { errors: newErrors } = validateQuerySQL(sql);
|
||||
|
||||
|
|
@ -142,7 +149,7 @@ const PolicyForm = ({
|
|||
} = platformCompatibility;
|
||||
|
||||
const platformSelectorDisabled =
|
||||
isFetchingAutofillDescription || isFetchingAutofillResolution;
|
||||
isFetchingAutofillDescription || isFetchingAutofillResolution || gomEnabled;
|
||||
|
||||
const platformSelector = usePlatformSelector(
|
||||
lastEditedQueryPlatform,
|
||||
|
|
@ -166,10 +173,6 @@ const PolicyForm = ({
|
|||
!policyIdForEdit &&
|
||||
DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName);
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
const aiFeaturesDisabled =
|
||||
config?.server_settings.ai_features_disabled || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewTemplatePolicy) {
|
||||
setCompatiblePlatforms(lastEditedQueryBody);
|
||||
|
|
@ -334,19 +337,22 @@ const PolicyForm = ({
|
|||
}
|
||||
};
|
||||
|
||||
const policyNameWrapperClasses = classnames("policy-name-wrapper", {
|
||||
const policyNameWrapperBase = "policy-name-wrapper";
|
||||
const policyNameWrapperClasses = classnames(policyNameWrapperBase, {
|
||||
[`${baseClass}--editing`]: isEditingName,
|
||||
});
|
||||
|
||||
const policyDescriptionWrapperBase = "policy-description-wrapper";
|
||||
const policyDescriptionWrapperClasses = classnames(
|
||||
"policy-description-wrapper",
|
||||
policyDescriptionWrapperBase,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingDescription,
|
||||
}
|
||||
);
|
||||
|
||||
const policyResolutionWrapperBase = "policy-resolution-wrapper";
|
||||
const policyResolutionWrapperClasses = classnames(
|
||||
"policy-resolution-wrapper",
|
||||
policyResolutionWrapperBase,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingResolution,
|
||||
}
|
||||
|
|
@ -355,34 +361,44 @@ const PolicyForm = ({
|
|||
const renderName = () => {
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={policyNameWrapperClasses}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-name"
|
||||
placeholder="Add name here"
|
||||
value={lastEditedQueryName}
|
||||
hasError={errors && errors.name}
|
||||
inputClassName={`${baseClass}__policy-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(dC) => {
|
||||
const classes = classnames(policyNameWrapperClasses, {
|
||||
[`${policyNameWrapperBase}--gitops-mode-disabled`]: dC,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-name"
|
||||
placeholder="Add name here"
|
||||
value={lastEditedQueryName}
|
||||
hasError={errors && errors.name}
|
||||
inputClassName={`${baseClass}__policy-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}
|
||||
`}
|
||||
maxLength={160}
|
||||
onChange={setLastEditedQueryName}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingName ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
maxLength={160}
|
||||
onChange={setLastEditedQueryName}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
disableTabability={dC}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingName ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -398,32 +414,42 @@ const PolicyForm = ({
|
|||
const renderDescription = () => {
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={policyDescriptionWrapperClasses}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-description"
|
||||
placeholder="Add description here."
|
||||
value={lastEditedQueryDescription}
|
||||
inputClassName={`${baseClass}__policy-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={250}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(dC) => {
|
||||
const classes = classnames(policyDescriptionWrapperClasses, {
|
||||
[`${policyDescriptionWrapperBase}--gitops-mode-disabled`]: dC,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-description"
|
||||
placeholder="Add description here."
|
||||
value={lastEditedQueryDescription}
|
||||
inputClassName={`${baseClass}__policy-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={250}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
disableTabability={dC}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -435,30 +461,42 @@ const PolicyForm = ({
|
|||
return (
|
||||
<div className={`form-field ${baseClass}__policy-resolve`}>
|
||||
<div className="form-field__label">Resolve:</div>
|
||||
<div
|
||||
className={policyResolutionWrapperClasses}
|
||||
onFocus={() => setIsEditingResolution(true)}
|
||||
onBlur={() => setIsEditingResolution(false)}
|
||||
onClick={editResolution}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-resolution"
|
||||
placeholder="Add resolution here."
|
||||
value={lastEditedQueryResolution}
|
||||
inputClassName={`${baseClass}__policy-resolution ${
|
||||
!lastEditedQueryResolution ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={500}
|
||||
onChange={setLastEditedQueryResolution}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingResolution}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingResolution ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(dC) => {
|
||||
const classes = classnames(policyResolutionWrapperClasses, {
|
||||
[`${policyResolutionWrapperBase}--gitops-mode-disabled`]: dC,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingResolution(true)}
|
||||
onBlur={() => setIsEditingResolution(false)}
|
||||
onClick={editResolution}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="policy-resolution"
|
||||
placeholder="Add resolution here."
|
||||
value={lastEditedQueryResolution}
|
||||
inputClassName={`${baseClass}__policy-resolution ${
|
||||
!lastEditedQueryResolution ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={500}
|
||||
onChange={setLastEditedQueryResolution}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingResolution}
|
||||
disableTabability={dC}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingResolution ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -486,6 +524,7 @@ const PolicyForm = ({
|
|||
onChange={(value: boolean) => setLastEditedQueryCritical(value)}
|
||||
value={lastEditedQueryCritical}
|
||||
isLeftLabel
|
||||
disabled={gomEnabled}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
|
|
@ -560,7 +599,7 @@ const PolicyForm = ({
|
|||
// Editable form is used for:
|
||||
// Global admins and global maintainers
|
||||
// Team admins and team maintainers viewing any of their team's policies
|
||||
const renderEditableQueryForm = () => {
|
||||
const renderEditablePolicyForm = () => {
|
||||
// Save disabled for no platforms selected, query name blank on existing query, or sql errors
|
||||
const disableSaveFormErrors =
|
||||
(isEditMode && !isAnyPlatformSelected) ||
|
||||
|
|
@ -590,6 +629,7 @@ const PolicyForm = ({
|
|||
handleSubmit={promptSavePolicy}
|
||||
wrapEnabled
|
||||
focus={!isEditMode}
|
||||
disabled={gomEnabled}
|
||||
/>
|
||||
{renderPlatformCompatibility()}
|
||||
{isEditMode && platformSelector.render()}
|
||||
|
|
@ -597,38 +637,44 @@ const PolicyForm = ({
|
|||
{renderLiveQueryWarning()}
|
||||
<div className="button-wrap">
|
||||
{hasSavePermissions && (
|
||||
<>
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--tooltip`}
|
||||
data-tip
|
||||
data-for="save-policy-button"
|
||||
data-tip-disable={!isEditMode || isAnyPlatformSelected}
|
||||
>
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={promptSavePolicy()}
|
||||
disabled={disableSaveFormErrors}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingPolicy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button-wrap--tooltip`}
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
id="save-policy-button"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
Select the platforms this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save or run the policy.
|
||||
</ReactTooltip>
|
||||
</>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(dC) => (
|
||||
// TODO - update to use TooltipWrapper
|
||||
<>
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--tooltip`}
|
||||
data-tip
|
||||
data-for="save-policy-button"
|
||||
data-tip-disable={!isEditMode || isAnyPlatformSelected}
|
||||
>
|
||||
<Button
|
||||
variant="brand"
|
||||
onClick={promptSavePolicy()}
|
||||
disabled={disableSaveFormErrors || dC}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingPolicy}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</span>
|
||||
<ReactTooltip
|
||||
className={`${baseClass}__button-wrap--tooltip`}
|
||||
place="bottom"
|
||||
effect="solid"
|
||||
id="save-policy-button"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
>
|
||||
Select the platforms this
|
||||
<br />
|
||||
policy will be checked on
|
||||
<br />
|
||||
to save or run the policy.
|
||||
</ReactTooltip>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* TODO - update to use TooltipWrapper */}
|
||||
<span
|
||||
className={`${baseClass}__button-wrap--tooltip`}
|
||||
data-tip
|
||||
|
|
@ -705,7 +751,7 @@ const PolicyForm = ({
|
|||
}
|
||||
|
||||
// Render default editable form
|
||||
return renderEditableQueryForm();
|
||||
return renderEditablePolicyForm();
|
||||
};
|
||||
|
||||
export default PolicyForm;
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&--gitops-mode-disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
.policy-name-wrapper {
|
||||
.no-value {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import React, {
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import React, { useContext, useCallback, useEffect, useState } from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import { useQuery } from "react-query";
|
||||
import { pick } from "lodash";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
|
@ -11,6 +13,7 @@ import LogDestinationIndicator from "components/LogDestinationIndicator/LogDesti
|
|||
import { ISchedulableQuery } from "interfaces/schedulable_query";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import { CONTACT_FLEET_LINK } from "utilities/constants";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
interface IManageQueryAutomationsModalProps {
|
||||
isUpdatingAutomations: boolean;
|
||||
|
|
@ -69,6 +72,8 @@ const ManageQueryAutomationsModal = ({
|
|||
// TODO: Error handling, if any
|
||||
// const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
|
||||
const gomEnabled = useContext(AppContext).config?.gitops.gitops_mode_enabled;
|
||||
|
||||
// Client side sort queries alphabetically
|
||||
const sortedAvailableQueries =
|
||||
availableQueries?.sort((a, b) =>
|
||||
|
|
@ -136,6 +141,7 @@ const ManageQueryAutomationsModal = ({
|
|||
// !isChecked &&
|
||||
// setErrors((errs) => omit(errs, "queryItems"));
|
||||
}}
|
||||
disabled={gomEnabled}
|
||||
>
|
||||
<TooltipTruncatedText value={name} />
|
||||
</Checkbox>
|
||||
|
|
@ -184,15 +190,21 @@ const ManageQueryAutomationsModal = ({
|
|||
Preview data
|
||||
</Button>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onSubmitQueryAutomations}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingAutomations}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={6}
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="brand"
|
||||
onClick={onSubmitQueryAutomations}
|
||||
className="save-loading"
|
||||
isLoading={isUpdatingAutomations}
|
||||
disabled={dC}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<Button onClick={onCancel} variant="inverse">
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import React from "react";
|
|||
import { formatDistanceToNow } from "date-fns";
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
|
||||
import permissionsUtils from "utilities/permissions";
|
||||
import { IUser } from "interfaces/user";
|
||||
import { secondsToDhms } from "utilities/helpers";
|
||||
|
|
@ -29,7 +31,8 @@ import TextCell from "components/TableContainer/DataTable/TextCell";
|
|||
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import InheritedBadge from "components/InheritedBadge";
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator";
|
||||
|
||||
interface IQueryRow {
|
||||
|
|
@ -105,7 +108,7 @@ interface IDataColumn {
|
|||
sortType?: string;
|
||||
}
|
||||
|
||||
interface IGenerateTableHeaders {
|
||||
interface IGenerateColumnConfigs {
|
||||
currentUser: IUser;
|
||||
currentTeamId?: number;
|
||||
omitSelectionColumn?: boolean;
|
||||
|
|
@ -113,11 +116,11 @@ interface IGenerateTableHeaders {
|
|||
|
||||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
const generateTableHeaders = ({
|
||||
const generateColumnConfigs = ({
|
||||
currentUser,
|
||||
currentTeamId,
|
||||
omitSelectionColumn = false,
|
||||
}: IGenerateTableHeaders): IDataColumn[] => {
|
||||
}: IGenerateColumnConfigs): IDataColumn[] => {
|
||||
const isCurrentTeamObserverOrGlobalObserver = currentTeamId
|
||||
? permissionsUtils.isTeamObserver(currentUser, currentTeamId)
|
||||
: permissionsUtils.isOnlyObserver(currentUser);
|
||||
|
|
@ -290,7 +293,16 @@ const generateTableHeaders = ({
|
|||
(row.original.team_id ?? undefined) === currentTeamId,
|
||||
});
|
||||
|
||||
return <Checkbox {...checkboxProps} enableEnterToCheck />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={8}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} enableEnterToCheck {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: (cellProps: ICellProps): JSX.Element => {
|
||||
const isInheritedQuery =
|
||||
|
|
@ -306,7 +318,16 @@ const generateTableHeaders = ({
|
|||
onChange: () => row.toggleRowSelected(),
|
||||
};
|
||||
// v4.35.0 Any team admin or maintainer now can add, edit, delete their team's queries
|
||||
return <Checkbox {...checkboxProps} enableEnterToCheck />;
|
||||
return (
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={8}
|
||||
fixedPositionStrategy
|
||||
renderChildren={(dC) => (
|
||||
<Checkbox disabled={dC} enableEnterToCheck {...checkboxProps} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
disableHidden: true,
|
||||
});
|
||||
|
|
@ -314,4 +335,4 @@ const generateTableHeaders = ({
|
|||
return tableHeaders;
|
||||
};
|
||||
|
||||
export default generateTableHeaders;
|
||||
export default generateColumnConfigs;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import Spinner from "components/Spinner";
|
|||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import LogDestinationIndicator from "components/LogDestinationIndicator";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import SaveQueryModal from "../SaveQueryModal";
|
||||
import ConfirmSaveChangesModal from "../ConfirmSaveChangesModal";
|
||||
|
|
@ -171,6 +172,8 @@ const EditQueryForm = ({
|
|||
|
||||
const savedQueryMode = !!queryIdForEdit;
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
const gomEnabled = config?.gitops.gitops_mode_enabled;
|
||||
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
||||
// NOTE: SaveQueryModal is only being used to create a new query in this component.
|
||||
// It's easy to confuse with other names like promptSaveQuery, promptSaveAsNewQuery, etc.,
|
||||
|
|
@ -487,12 +490,15 @@ const EditQueryForm = ({
|
|||
setIsEditingName(true);
|
||||
}
|
||||
};
|
||||
const queryNameWrapperClasses = classnames("query-name-wrapper", {
|
||||
|
||||
const queryNameWrapperClass = "query-name-wrapper";
|
||||
const queryNameWrapperClasses = classnames(queryNameWrapperClass, {
|
||||
[`${baseClass}--editing`]: isEditingName,
|
||||
});
|
||||
|
||||
const queryDescriptionWrapperClass = "query-description-wrapper";
|
||||
const queryDescriptionWrapperClasses = classnames(
|
||||
"query-description-wrapper",
|
||||
queryDescriptionWrapperClass,
|
||||
{
|
||||
[`${baseClass}--editing`]: isEditingDescription,
|
||||
}
|
||||
|
|
@ -501,34 +507,46 @@ const EditQueryForm = ({
|
|||
const renderName = () => {
|
||||
if (savedQueryMode) {
|
||||
return (
|
||||
<div
|
||||
className={queryNameWrapperClasses}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-name"
|
||||
placeholder="Add name"
|
||||
value={lastEditedQueryName}
|
||||
inputClassName={`${baseClass}__query-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={160}
|
||||
hasError={errors && errors.name}
|
||||
onChange={setLastEditedQueryName}
|
||||
onBlur={() => {
|
||||
setLastEditedQueryName(lastEditedQueryName.trim());
|
||||
}}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingName ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(dC) => {
|
||||
const classes = classnames(queryNameWrapperClasses, {
|
||||
[`${queryNameWrapperClass}--gitops-mode-disabled`]: dC,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingName(true)}
|
||||
onBlur={() => setIsEditingName(false)}
|
||||
onClick={editName}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-name"
|
||||
placeholder="Add name"
|
||||
value={lastEditedQueryName}
|
||||
inputClassName={`${baseClass}__query-name ${
|
||||
!lastEditedQueryName ? "no-value" : ""
|
||||
}`}
|
||||
maxLength={160}
|
||||
hasError={errors && errors.name}
|
||||
onChange={setLastEditedQueryName}
|
||||
onBlur={() => {
|
||||
setLastEditedQueryName(lastEditedQueryName.trim());
|
||||
}}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingName}
|
||||
disableTabability={dC}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingName ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -544,30 +562,42 @@ const EditQueryForm = ({
|
|||
const renderDescription = () => {
|
||||
if (savedQueryMode) {
|
||||
return (
|
||||
<div
|
||||
className={queryDescriptionWrapperClasses}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-description"
|
||||
placeholder="Add description"
|
||||
value={lastEditedQueryDescription}
|
||||
maxLength={250}
|
||||
inputClassName={`${baseClass}__query-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
<GitOpsModeTooltipWrapper
|
||||
position="right"
|
||||
tipOffset={16}
|
||||
renderChildren={(dC) => {
|
||||
const classes = classnames(queryDescriptionWrapperClasses, {
|
||||
[`${queryDescriptionWrapperClass}--gitops-mode-disabled`]: dC,
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
onFocus={() => setIsEditingDescription(true)}
|
||||
onBlur={() => setIsEditingDescription(false)}
|
||||
onClick={editDescription}
|
||||
>
|
||||
<AutoSizeInputField
|
||||
name="query-description"
|
||||
placeholder="Add description"
|
||||
value={lastEditedQueryDescription}
|
||||
maxLength={250}
|
||||
inputClassName={`${baseClass}__query-description ${
|
||||
!lastEditedQueryDescription ? "no-value" : ""
|
||||
}`}
|
||||
onChange={setLastEditedQueryDescription}
|
||||
onKeyPress={onInputKeypress}
|
||||
isFocused={isEditingDescription}
|
||||
disableTabability={dC}
|
||||
/>
|
||||
<Icon
|
||||
name="pencil"
|
||||
className={`edit-icon ${isEditingDescription ? "hide" : ""}`}
|
||||
size="small-medium"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
@ -730,7 +760,11 @@ const EditQueryForm = ({
|
|||
{renderPlatformCompatibility()}
|
||||
|
||||
{savedQueryMode && (
|
||||
<>
|
||||
<div
|
||||
// including `form` class here keeps the children fields subject to the global form
|
||||
// children styles
|
||||
className={gomEnabled ? "gitops-mode-disabled form" : "form"}
|
||||
>
|
||||
<Dropdown
|
||||
searchable={false}
|
||||
options={frequencyOptions}
|
||||
|
|
@ -840,37 +874,46 @@ const EditQueryForm = ({
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
{renderLiveQueryWarning()}
|
||||
<div className={`button-wrap ${baseClass}__button-wrap--new-query`}>
|
||||
{hasSavePermissions && (
|
||||
<>
|
||||
{savedQueryMode && (
|
||||
<Button
|
||||
variant="text-link"
|
||||
onClick={promptSaveAsNewQuery()}
|
||||
disabled={disableSaveFormErrors}
|
||||
className="save-as-new-loading"
|
||||
isLoading={isSaveAsNewLoading}
|
||||
>
|
||||
Save as new
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
variant="text-link"
|
||||
onClick={promptSaveAsNewQuery()}
|
||||
disabled={disableSaveFormErrors || dC}
|
||||
className="save-as-new-loading"
|
||||
isLoading={isSaveAsNewLoading}
|
||||
>
|
||||
Save as new
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__button-wrap--save-query-button`}>
|
||||
<Button
|
||||
className="save-loading"
|
||||
variant="brand"
|
||||
onClick={
|
||||
confirmChanges
|
||||
? toggleConfirmSaveChangesModal
|
||||
: promptSaveQuery()
|
||||
}
|
||||
disabled={disableSaveFormErrors}
|
||||
isLoading={isQueryUpdating}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<GitOpsModeTooltipWrapper
|
||||
tipOffset={8}
|
||||
renderChildren={(dC) => (
|
||||
<Button
|
||||
className="save-loading"
|
||||
variant="brand"
|
||||
onClick={
|
||||
confirmChanges
|
||||
? toggleConfirmSaveChangesModal
|
||||
: promptSaveQuery()
|
||||
}
|
||||
disabled={disableSaveFormErrors || dC}
|
||||
isLoading={isQueryUpdating}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&--gitops-mode-disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
.query-name-wrapper {
|
||||
.no-value {
|
||||
|
|
@ -158,4 +161,8 @@
|
|||
z-index: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gitops-mode-disabled {
|
||||
@include disabled;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -288,8 +288,8 @@ const routes = (
|
|||
path="fleet-maintained"
|
||||
component={SoftwareFleetMaintained}
|
||||
/>
|
||||
<Route path="package" component={SoftwareCustomPackage} />
|
||||
<Route path="app-store" component={SoftwareAppStore} />
|
||||
<Route path="package" component={SoftwareCustomPackage} />
|
||||
</Route>
|
||||
<Route
|
||||
path="add/fleet-maintained/:id"
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export default {
|
|||
ADMIN_INTEGRATIONS_AUTOMATIC_ENROLLMENT_WINDOWS: `${URL_PREFIX}/settings/integrations/automatic-enrollment/windows`,
|
||||
ADMIN_INTEGRATIONS_SCEP: `${URL_PREFIX}/settings/integrations/mdm/scep`,
|
||||
ADMIN_INTEGRATIONS_CALENDARS: `${URL_PREFIX}/settings/integrations/calendars`,
|
||||
ADMIN_INTEGRATIONS_CHANGE_MANAGEMENT: `${URL_PREFIX}/settings/integrations/change-management`,
|
||||
ADMIN_INTEGRATIONS_VPP: `${URL_PREFIX}/settings/integrations/mdm/vpp`,
|
||||
ADMIN_INTEGRATIONS_VPP_SETUP: `${URL_PREFIX}/settings/integrations/vpp/setup`,
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const config: Config = {
|
|||
transformIgnorePatterns: [`/node_modules/(?!(${esModules})/)`],
|
||||
globals: {
|
||||
TransformStream,
|
||||
featureFlags: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
4
frontend/typings/index.d.ts
vendored
4
frontend/typings/index.d.ts
vendored
|
|
@ -21,3 +21,7 @@ declare module "*.pdf" {
|
|||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare const featureFlags: {
|
||||
[key: string]: type;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ let plugins = [
|
|||
new WebpackNotifierPlugin({
|
||||
excludeWarnings: true,
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
featureFlags: {
|
||||
allowGitOpsMode: JSON.stringify(process.env.ALLOW_GITOPS_MODE),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
|
|
|
|||
Loading…
Reference in a new issue