fleet/frontend/pages/ManageControlsPage/ManageControlsPage.tsx
Scott Gress 2e1ce02796
UI for managing custom variables (#31875)
for #31054

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [ ] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [X] QA'd all new/changed functionality manually
2025-08-15 08:24:55 -05:00

213 lines
5.4 KiB
TypeScript

import React, { useCallback, useContext, useMemo } from "react";
import { Tab, Tabs, TabList } from "react-tabs";
import { InjectedRouter } from "react-router";
import PATHS from "router/paths";
import { AppContext } from "context/app";
import useTeamIdParam from "hooks/useTeamIdParam";
import TabNav from "components/TabNav";
import TabText from "components/TabText";
import MainContent from "components/MainContent";
import TeamsDropdown from "components/TeamsDropdown";
import { parseOSUpdatesCurrentVersionsQueryParams } from "./OSUpdates/components/CurrentVersionSection/CurrentVersionSection";
interface IControlsSubNavItem {
name: string;
pathname: string;
}
const controlsSubNav: IControlsSubNavItem[] = [
{
name: "OS updates",
pathname: PATHS.CONTROLS_OS_UPDATES,
},
{
name: "OS settings",
pathname: PATHS.CONTROLS_OS_SETTINGS,
},
{
name: "Setup experience",
pathname: PATHS.CONTROLS_SETUP_EXPERIENCE,
},
{
name: "Scripts",
pathname: PATHS.CONTROLS_SCRIPTS,
},
{
name: "Variables",
pathname: PATHS.CONTROLS_VARIABLES,
},
];
// params to strip when navigating between tabs
const subNavQueryParams = [
"page",
"order_key",
"order_direction",
"status",
] as const;
interface IManageControlsPageProps {
children: JSX.Element;
location: {
pathname: string;
search: string;
hash?: string;
query: {
team_id?: string;
page?: string;
order_key?: string;
order_direction?: "asc" | "desc";
};
};
router: InjectedRouter; // v3
}
const getTabIndex = (
permittedControlsSubNav: IControlsSubNavItem[],
path: string
): number => {
return permittedControlsSubNav.findIndex((navItem) => {
// tab stays highlighted for paths that start with same pathname
return path.startsWith(navItem.pathname);
});
};
const baseClass = "manage-controls-page";
const ManageControlsPage = ({
// TODO(sarah): decide on pattern to pass team id to subcomponents.
// using children makes it difficult to centralize page-level control
// over team id param
children,
location,
router,
}: IManageControlsPageProps): JSX.Element => {
const page = parseInt(location?.query?.page || "", 10) || 0;
const {
config,
isOnGlobalTeam,
isPremiumTier,
isGlobalAdmin,
isTeamAdmin,
} = useContext(AppContext);
const {
currentTeamId,
userTeams,
teamIdForApi,
handleTeamChange,
} = useTeamIdParam({
location,
router,
includeAllTeams: false,
includeNoTeam: true,
permittedAccessByTeamRole: {
admin: true,
maintainer: true,
observer: false,
observer_plus: false,
},
});
const permittedControlsSubNav = useMemo(() => {
let renderedSubNav = controlsSubNav;
if (!isGlobalAdmin && !isTeamAdmin) {
renderedSubNav = controlsSubNav.filter((navItem) => {
return navItem.name !== "OS updates";
});
}
return renderedSubNav;
}, [isGlobalAdmin, isTeamAdmin]);
const navigateToNav = useCallback(
(i: number): void => {
const navPath = permittedControlsSubNav[i].pathname;
// remove query params related to the prior tab
const newParams = new URLSearchParams(location?.search);
subNavQueryParams.forEach((p) => newParams.delete(p));
const newQuery = newParams.toString();
router.replace(
navPath
.concat(newQuery ? `?${newQuery}` : "")
.concat(location?.hash || "")
);
},
[location, router, permittedControlsSubNav]
);
const renderBody = () => {
return (
<div>
<TabNav>
<Tabs
selectedIndex={getTabIndex(
permittedControlsSubNav,
location?.pathname || ""
)}
onSelect={navigateToNav}
>
<TabList>
{permittedControlsSubNav.map((navItem) => {
return (
<Tab key={navItem.name} data-text={navItem.name}>
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
</TabList>
</Tabs>
</TabNav>
{React.cloneElement(children, {
teamIdForApi,
currentPage: page,
queryParams: parseOSUpdatesCurrentVersionsQueryParams(location.query),
})}
</div>
);
};
const renderHeaderContent = () => {
if (isPremiumTier && !config?.partnerships?.enable_primo && userTeams) {
if (userTeams.length > 1 || isOnGlobalTeam) {
return (
<TeamsDropdown
currentUserTeams={userTeams}
selectedTeamId={currentTeamId}
onChange={handleTeamChange}
includeAllTeams={false}
includeNoTeams
/>
);
}
if (!isOnGlobalTeam && userTeams.length === 1) {
return <h1>{userTeams[0].name}</h1>;
}
}
return <h1>Controls</h1>;
};
const renderHeader = () => (
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<div className={`${baseClass}__title`}>{renderHeaderContent()}</div>
</div>
</div>
);
return (
<MainContent>
<div className={`${baseClass}__wrapper`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header-wrap`}>{renderHeader()}</div>
</div>
{renderBody()}
</div>
</MainContent>
);
};
export default ManageControlsPage;