UI - GitOps Mode: Core abstractions, first batch of applications (#26401)

## For #26229 – Part 1


![ezgif-6bbe6d60c12ed4](https://github.com/user-attachments/assets/37a04b64-abd7-4605-b4ac-9542836ff562)

- 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:
jacobshandling 2025-02-20 08:41:07 -08:00 committed by GitHub
parent c22f575150
commit 5d9026b7e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 1361 additions and 553 deletions

View file

@ -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 => {

View file

@ -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">

View file

@ -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 (

View file

@ -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}

View file

@ -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;

View file

@ -0,0 +1,7 @@
.gitops-mode-tooltip-wrapper {
.component__tooltip-wrapper {
&__tip-text {
cursor: default;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./GitOpsModeTooltipWrapper";

View file

@ -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) {

View file

@ -23,6 +23,9 @@
border: 1px solid $core-vibrant-red;
}
}
&--disabled {
@include disabled;
}
}
.ace_content {

View file

@ -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;

View file

@ -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}

View file

@ -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}

View file

@ -25,6 +25,9 @@
display: flex;
align-items: center;
height: 24px; // Noticeable with help text below slider
&--disabled {
@include disabled;
}
}
&__dot {

View 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;

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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",

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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>
);

View file

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

View file

@ -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>
);

View file

@ -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>
)}
</>

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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={

View file

@ -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)}
/>
);

View file

@ -49,6 +49,7 @@ const ScriptPackageUploader = ({
accept=".sh,.ps1"
onFileUpload={onUploadFile}
isLoading={showLoading}
gitopsCompatible
/>
);
};

View file

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

View file

@ -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} />
)}
/>
);
},
},
{

View file

@ -56,6 +56,7 @@ const SetupExperienceScriptUploader = ({
buttonMessage="Upload"
onFileUpload={onUploadFile}
isLoading={showLoading}
gitopsCompatible
/>
);
};

View file

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

View file

@ -23,6 +23,9 @@
display: flex;
flex-direction: column;
gap: $pad-xxlarge;
&--disabled {
@include disabled;
}
}
&__form-fields {

View file

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

View file

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

View file

@ -20,4 +20,7 @@
&__no-integrations a {
display: block;
}
.component__tooltip-wrapper__tip-text {
text-align: center;
}
}

View file

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

View file

@ -35,4 +35,10 @@
.info-banner {
margin-top: $pad-small;
}
&__form-fields {
&--gitops-disabled {
@include disabled;
}
}
}

View file

@ -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;

View file

@ -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}

View file

@ -1,8 +1,2 @@
.integrations {
&__page-description {
font-size: $x-small;
color: $core-fleet-black;
@include sticky-settings-description;
}
}

View file

@ -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;

View file

@ -0,0 +1,5 @@
.change-management {
.custom-link {
margin-bottom: 1rem;
}
}

View file

@ -0,0 +1 @@
export { default } from "./ChangeManagement";

View file

@ -1,9 +1,4 @@
.integrations-management {
&__page-description {
font-size: $x-small;
color: $core-fleet-black;
}
.no-integrations {
display: flex;
flex-direction: column;

View file

@ -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}

View file

@ -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;
}

View file

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

View file

@ -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 {

View file

@ -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>
);

View file

@ -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;
}

View file

@ -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;
}
}

View file

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

View file

@ -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();
});
});

View file

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

View file

@ -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,
});

View file

@ -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;

View file

@ -85,6 +85,9 @@
}
}
}
&--gitops-mode-disabled {
@include disabled;
}
}
.policy-name-wrapper {
.no-value {

View file

@ -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";

View file

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

View file

@ -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;

View file

@ -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>
</>
)}

View file

@ -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;
}
}

View file

@ -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"

View file

@ -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`,

View file

@ -45,6 +45,7 @@ const config: Config = {
transformIgnorePatterns: [`/node_modules/(?!(${esModules})/)`],
globals: {
TransformStream,
featureFlags: {},
},
};

View file

@ -21,3 +21,7 @@ declare module "*.pdf" {
const value: string;
export = value;
}
declare const featureFlags: {
[key: string]: type;
};

View file

@ -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") {