Skip to content

Commit b990b3c

Browse files
jacobshandlingJacob Shandling
and
Jacob Shandling
authored
UI - GitOps Mode, 3/3 (#26537)
## For #26229 - Remove feature flag - Undo updates to 4 Policies automation modals to facilitate refactor being implemented in parallel - Remaining specs: **Manage teams:** ![manage-teams](https://github.com/user-attachments/assets/af8d8d10-2add-4d8d-8961-61d0de44b067) Empty: <img width="1464" alt="Screenshot 2025-02-21 at 4 27 30 PM" src="https://github.com/user-attachments/assets/17cf4fc2-cc4e-4f63-8276-3db79b44e9e1" /> **Team users:** ![team-users](https://github.com/user-attachments/assets/1bf106c1-bdf7-442c-a957-6c9eea6af14d) Empty: <img width="1464" alt="Screenshot 2025-02-21 at 4 29 01 PM" src="https://github.com/user-attachments/assets/46dd0e44-2af3-4ca7-a0be-628e358a61d7" /> **Team agent options:** ![team-agent-options](https://github.com/user-attachments/assets/7d4ee8b6-03c7-48d2-8337-b2c33e50abe9) **Team settings:** ![team-settings](https://github.com/user-attachments/assets/a67b45fc-a5ce-4267-b8fd-2f1e300d1fd8) - [x] Changes file added for user-visible changes in `changes/` - [x] Added/updated automated tests - [ ] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling <[email protected]>
1 parent 0595485 commit b990b3c

File tree

25 files changed

+273
-280
lines changed

25 files changed

+273
-280
lines changed

changes/25478-GitOps-Mode

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Implement GitOps Mode

frontend/components/TableContainer/TableContainer.tsx

+52-29
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import SearchField from "components/forms/fields/SearchField";
1010
import Pagination from "components/Pagination";
1111
import Button from "components/buttons/Button";
1212
import Icon from "components/Icon/Icon";
13+
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
14+
1315
import { COLORS } from "styles/var/colors";
1416

1517
import DataTable from "./DataTable/DataTable";
@@ -30,6 +32,10 @@ interface IRowProps extends Row {
3032
};
3133
}
3234

35+
interface ITableContainerActionButtonProps extends IActionButtonProps {
36+
gitOpsModeCompatible?: boolean;
37+
}
38+
3339
interface ITableContainerProps<T = any> {
3440
columnConfigs: any; // TODO: Figure out type
3541
data: any; // TODO: Figure out type
@@ -41,7 +47,7 @@ interface ITableContainerProps<T = any> {
4147
defaultPageIndex?: number;
4248
defaultSelectedRows?: Record<string, boolean>;
4349
/** Button visible above the table container next to search bar */
44-
actionButton?: IActionButtonProps;
50+
actionButton?: ITableContainerActionButtonProps;
4551
inputPlaceHolder?: string;
4652
disableActionButton?: boolean;
4753
disableMultiRowSelect?: boolean;
@@ -284,6 +290,47 @@ const TableContainer = <T,>({
284290
onPaginationChange,
285291
]);
286292

293+
const renderFilterActionButton = () => {
294+
// always !!actionButton here, this is for type checker
295+
if (actionButton) {
296+
if (actionButton.gitOpsModeCompatible) {
297+
return (
298+
<GitOpsModeTooltipWrapper
299+
tipOffset={8}
300+
renderChildren={(disableChildren) => (
301+
<Button
302+
disabled={disableActionButton || disableChildren}
303+
onClick={actionButton.onActionButtonClick}
304+
variant={actionButton.variant || "brand"}
305+
className={`${baseClass}__table-action-button`}
306+
>
307+
<>
308+
{actionButton.buttonText}
309+
{actionButton.iconSvg && <Icon name={actionButton.iconSvg} />}
310+
</>
311+
</Button>
312+
)}
313+
/>
314+
);
315+
}
316+
return (
317+
<Button
318+
disabled={disableActionButton}
319+
onClick={actionButton.onActionButtonClick}
320+
variant={actionButton.variant || "brand"}
321+
className={`${baseClass}__table-action-button`}
322+
>
323+
<>
324+
{actionButton.buttonText}
325+
{actionButton.iconSvg && <Icon name={actionButton.iconSvg} />}
326+
</>
327+
</Button>
328+
);
329+
}
330+
// should never reach here
331+
return null;
332+
};
333+
287334
const renderFilters = useCallback(() => {
288335
const opacity = isLoading ? { opacity: 0.4 } : { opacity: 1 };
289336

@@ -306,19 +353,7 @@ const TableContainer = <T,>({
306353
</div>
307354

308355
{actionButton && !actionButton.hideButton && (
309-
<div className="stackable-header">
310-
<Button
311-
disabled={disableActionButton}
312-
onClick={actionButton.onActionButtonClick}
313-
variant={actionButton.variant || "brand"}
314-
className={`${baseClass}__table-action-button`}
315-
>
316-
<>
317-
{actionButton.buttonText}
318-
{actionButton.iconSvg && <Icon name={actionButton.iconSvg} />}
319-
</>
320-
</Button>
321-
</div>
356+
<div className="stackable-header">{renderFilterActionButton()}</div>
322357
)}
323358
<div className="stackable-header top-shift-header">
324359
{customControl && customControl()}
@@ -386,21 +421,9 @@ const TableContainer = <T,>({
386421
</div>
387422
)}
388423
<span className="controls">
389-
{actionButton && !actionButton.hideButton && (
390-
<Button
391-
disabled={disableActionButton}
392-
onClick={actionButton.onActionButtonClick}
393-
variant={actionButton.variant || "brand"}
394-
className={`${baseClass}__table-action-button`}
395-
>
396-
<>
397-
{actionButton.buttonText}
398-
{actionButton.iconSvg && (
399-
<Icon name={actionButton.iconSvg} />
400-
)}
401-
</>
402-
</Button>
403-
)}
424+
{actionButton &&
425+
!actionButton.hideButton &&
426+
renderFilterActionButton()}
404427
{customControl && customControl()}
405428
</span>
406429
</div>

frontend/components/buttons/ActionButtons/ActionButtons.tsx

+49-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ButtonVariant } from "components/buttons/Button/Button";
66
import DropdownButton from "components/buttons/DropdownButton";
77
import Icon from "components/Icon/Icon";
88
import { IconNames } from "components/icons";
9+
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
910

1011
export interface IActionButtonProps {
1112
type: "primary" | "secondary";
@@ -15,6 +16,7 @@ export interface IActionButtonProps {
1516
iconSvg?: IconNames;
1617
hideAction?: boolean;
1718
onClick: () => void;
19+
gitOpsModeCompatible?: boolean;
1820
}
1921

2022
interface IProps {
@@ -52,22 +54,56 @@ const ActionButtons = ({ baseClass, actions }: IProps): JSX.Element => {
5254
<div
5355
className={`${baseClass}__action-buttons--secondary-buttons action-buttons__secondary-buttons`}
5456
>
55-
{secondaryActions.map(
56-
(action) =>
57-
!action.hideAction &&
58-
(action.buttonVariant !== "text-icon" ? (
57+
{secondaryActions.map((action) => {
58+
if (!action.hideAction && action.buttonVariant !== "text-icon") {
59+
if (action.gitOpsModeCompatible) {
60+
return (
61+
<GitOpsModeTooltipWrapper
62+
renderChildren={(disableChildren) => (
63+
<Button
64+
variant={action.buttonVariant}
65+
onClick={action.onClick}
66+
disabled={disableChildren}
67+
>
68+
{action.label}
69+
</Button>
70+
)}
71+
/>
72+
);
73+
}
74+
return (
5975
<Button variant={action.buttonVariant} onClick={action.onClick}>
6076
{action.label}
6177
</Button>
62-
) : (
63-
<Button variant="text-icon" onClick={action.onClick}>
64-
<>
65-
{action.label}
66-
{action.iconSvg && <Icon name={action.iconSvg} />}
67-
</>
68-
</Button>
69-
))
70-
)}
78+
);
79+
}
80+
if (action.gitOpsModeCompatible) {
81+
return (
82+
<GitOpsModeTooltipWrapper
83+
renderChildren={(disableChildren) => (
84+
<Button
85+
variant="text-icon"
86+
onClick={action.onClick}
87+
disabled={disableChildren}
88+
>
89+
<>
90+
{action.label}
91+
{action.iconSvg && <Icon name={action.iconSvg} />}
92+
</>
93+
</Button>
94+
)}
95+
/>
96+
);
97+
}
98+
return (
99+
<Button variant="text-icon" onClick={action.onClick}>
100+
<>
101+
{action.label}
102+
{action.iconSvg && <Icon name={action.iconSvg} />}
103+
</>
104+
</Button>
105+
);
106+
})}
71107
</div>
72108
<div
73109
className={`${baseClass}__action-buttons--secondary-dropdown action-buttons__secondary-dropdown`}

frontend/pages/admin/IntegrationsPage/IntegrationNavItems.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,12 @@ const integrationSettingsNavItems: ISideNavItem<any>[] = [
2626
path: PATHS.ADMIN_INTEGRATIONS_CALENDARS,
2727
Card: Calendars,
2828
},
29-
];
30-
31-
if (featureFlags.allowGitOpsMode === "true") {
32-
integrationSettingsNavItems.push({
29+
{
3330
title: "Change management",
3431
urlSection: "change-management",
3532
path: PATHS.ADMIN_INTEGRATIONS_CHANGE_MANAGEMENT,
3633
Card: ChangeManagement,
37-
});
38-
}
34+
},
35+
];
3936

4037
export default integrationSettingsNavItems;

frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -428,13 +428,15 @@ const TeamDetailsWrapper = ({
428428
buttonVariant: "text-icon",
429429
iconSvg: "eye",
430430
onClick: toggleManageEnrollSecretsModal,
431+
gitOpsModeCompatible: true,
431432
},
432433
{
433434
type: "secondary",
434435
label: "Rename team",
435436
buttonVariant: "text-icon",
436437
iconSvg: "pencil",
437438
onClick: toggleRenameTeamModal,
439+
gitOpsModeCompatible: true,
438440
},
439441
{
440442
type: "secondary",
@@ -443,6 +445,7 @@ const TeamDetailsWrapper = ({
443445
iconSvg: "trash",
444446
hideAction: !isGlobalAdmin,
445447
onClick: toggleDeleteTeamModal,
448+
gitOpsModeCompatible: true,
446449
},
447450
]}
448451
/>

frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/TeamSettings.tsx

+23-10
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import SectionHeader from "components/SectionHeader";
3636
// @ts-ignore
3737
import Dropdown from "components/forms/fields/Dropdown";
3838
import Checkbox from "components/forms/fields/Checkbox";
39+
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
3940

4041
import TeamHostExpiryToggle from "./components/TeamHostExpiryToggle";
4142

@@ -149,7 +150,8 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
149150
host_expiry_enabled: globalHostExpiryEnabled,
150151
host_expiry_window: globalHostExpiryWindow,
151152
},
152-
} = appConfig ?? { host_expiry_settings: {} };
153+
gitops: { gitops_mode_enabled: gitopsModeEnabled },
154+
} = appConfig ?? { host_expiry_settings: {}, gitops: {} };
153155

154156
const {
155157
data: teamConfig,
@@ -296,6 +298,7 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
296298
value={formData.teamHostStatusWebhookEnabled}
297299
helpText="This will trigger webhooks specific to this team, separate from the global host status webhook."
298300
tooltipContent="Send an alert if a portion of your hosts go offline."
301+
disabled={gitopsModeEnabled}
299302
>
300303
Enable host status webhook
301304
</Checkbox>
@@ -316,6 +319,7 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
316319
value={formData.teamHostStatusWebhookDestinationUrl}
317320
parseTarget
318321
error={formErrors.host_status_webhook_destination_url}
322+
disabled={gitopsModeEnabled}
319323
tooltip={
320324
<p>
321325
Provide a URL to deliver <br />
@@ -331,6 +335,7 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
331335
value={formData.teamHostStatusWebhookHostPercentage}
332336
parseTarget
333337
searchable={false}
338+
disabled={gitopsModeEnabled}
334339
tooltip={
335340
<p>
336341
Select the minimum percentage of hosts that
@@ -348,6 +353,7 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
348353
name="teamHostStatusWebhookWindow"
349354
value={formData.teamHostStatusWebhookWindow}
350355
parseTarget
356+
disabled={gitopsModeEnabled}
351357
searchable={false}
352358
tooltip={
353359
<p>
@@ -372,6 +378,7 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
372378
setTeamExpiryEnabled={(isEnabled: boolean) =>
373379
onInputChange({ name: "teamHostExpiryEnabled", value: isEnabled })
374380
}
381+
gitopsModeEnabled={gitopsModeEnabled}
375382
/>
376383
)}
377384
{formData.teamHostExpiryEnabled && (
@@ -385,17 +392,23 @@ const TeamSettings = ({ location, router }: ITeamSubnavProps) => {
385392
name="teamHostExpiryWindow"
386393
value={formData.teamHostExpiryWindow}
387394
error={formErrors.host_expiry_window}
395+
disabled={gitopsModeEnabled}
388396
/>
389397
)}
390-
<Button
391-
type="submit"
392-
variant="brand"
393-
className="button-wrap"
394-
isLoading={updatingTeamSettings}
395-
disabled={Object.keys(formErrors).length > 0}
396-
>
397-
Save
398-
</Button>
398+
<GitOpsModeTooltipWrapper
399+
tipOffset={-8}
400+
renderChildren={(disableChildren) => (
401+
<Button
402+
type="submit"
403+
variant="brand"
404+
className="button-wrap"
405+
isLoading={updatingTeamSettings}
406+
disabled={Object.keys(formErrors).length > 0 || disableChildren}
407+
>
408+
Save
409+
</Button>
410+
)}
411+
/>
399412
</form>
400413
);
401414
};

frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamSettings/components/TeamHostExpiryToggle/TeamHostExpiryToggle.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ interface ITeamHostExpiryToggle {
1010
globalHostExpiryWindow?: number;
1111
teamExpiryEnabled: boolean;
1212
setTeamExpiryEnabled: (value: boolean) => void;
13+
gitopsModeEnabled?: boolean;
1314
}
1415

1516
const TeamHostExpiryToggle = ({
1617
globalHostExpiryEnabled,
1718
globalHostExpiryWindow,
1819
teamExpiryEnabled,
1920
setTeamExpiryEnabled,
21+
gitopsModeEnabled,
2022
}: ITeamHostExpiryToggle) => {
2123
const renderHelpText = () =>
2224
// this will never be rendered while globalHostExpiryWindow is undefined
@@ -49,7 +51,7 @@ const TeamHostExpiryToggle = ({
4951
name="enableHostExpiry"
5052
onChange={setTeamExpiryEnabled}
5153
value={teamExpiryEnabled || globalHostExpiryEnabled} // Still shows checkmark if global expiry is enabled though the checkbox will be disabled.
52-
disabled={globalHostExpiryEnabled}
54+
disabled={globalHostExpiryEnabled || gitopsModeEnabled}
5355
helpText={renderHelpText()}
5456
tooltipContent={
5557
<>

frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
414414
? toggleAddUserModal
415415
: toggleCreateUserModal,
416416
hideButton: userIds.length === 0 && searchString === "",
417+
gitOpsModeCompatible: true,
417418
}}
418419
onQueryChange={({ searchQuery }) => setSearchString(searchQuery)}
419420
inputPlaceHolder="Search"

0 commit comments

Comments
 (0)