UI: Couple Setup experience > Install software selected platform with URL (#33327)

## For #33299 

- Couple card logic to URL param
- Validate param, push to default macos if invalid or missing
- Validate that other setup experience cards don't have a platform
param, push to no param if present


![ezgif-7ab225faff3840](https://github.com/user-attachments/assets/ea1c0382-a928-4855-a083-c8c52ec2ab4f)


- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling 2025-09-23 09:54:39 -07:00 committed by GitHub
parent c99aa8cbff
commit 477f3cbaec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 72 additions and 20 deletions

View file

@ -215,3 +215,9 @@ export const SETUP_EXPERIENCE_PLATFORMS = [
] as const;
export type SetupExperiencePlatform = typeof SETUP_EXPERIENCE_PLATFORMS[number];
export const isSetupExperiencePlatform = (
s: string | undefined
): s is SetupExperiencePlatform => {
return SETUP_EXPERIENCE_PLATFORMS.includes(s as SetupExperiencePlatform);
};

View file

@ -23,7 +23,7 @@ const SetupExperience = ({
router,
teamIdForApi,
}: ISetupExperienceProps) => {
const { section } = params;
const { section, platform: urlPlatformParam } = params;
const { isPremiumTier } = useContext(AppContext);
// Not premium shows premium message
@ -41,6 +41,14 @@ const SetupExperience = ({
SETUP_EXPERIENCE_NAV_ITEMS.find((item) => item.urlSection === section) ??
DEFAULT_SETTINGS_SECTION;
if (
currentFormSection.urlSection !== "install-software" &&
urlPlatformParam
) {
router.replace(
currentFormSection.path + queryString // current card doesn't support platforms yet
);
}
const CurrentCard = currentFormSection.Card;
return (
@ -58,6 +66,7 @@ const SetupExperience = ({
key={teamIdForApi}
currentTeamId={teamIdForApi}
router={router}
urlPlatformParam={urlPlatformParam}
/>
}
/>

View file

@ -13,6 +13,7 @@ import RunScript from "./cards/RunScript";
export interface ISetupExperienceCardProps {
currentTeamId: number;
router: InjectedRouter;
urlPlatformParam?: string; // not yet guaranteed to be a valid platform
}
const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<ISetupExperienceCardProps>[] = [
@ -31,7 +32,7 @@ const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<ISetupExperienceCardProps>[] = [
{
title: "3. Install software",
urlSection: "install-software",
path: PATHS.CONTROLS_INSTALL_SOFTWARE,
path: PATHS.CONTROLS_INSTALL_SOFTWARE("macos"),
Card: InstallSoftware,
},
{

View file

@ -3,16 +3,21 @@ import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import PATHS from "router/paths";
import mdmAPI, {
IGetSetupExperienceSoftwareResponse,
} from "services/entities/mdm";
import configAPI from "services/entities/config";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import { ISoftwareTitle } from "interfaces/software";
import { DEFAULT_USE_QUERY_OPTIONS, SUPPORT_LINK } from "utilities/constants";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import { IConfig } from "interfaces/config";
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
import { SetupExperiencePlatform } from "interfaces/platform";
import {
isSetupExperiencePlatform,
SetupExperiencePlatform,
} from "interfaces/platform";
import SectionHeader from "components/SectionHeader";
import DataError from "components/DataError";
@ -34,23 +39,30 @@ const baseClass = "install-software";
// available for install so we can correctly display the selected count.
const PER_PAGE_SIZE = 3000;
const DEFAULT_PLATFORM: SetupExperiencePlatform = "macos";
export const PLATFORM_BY_INDEX: SetupExperiencePlatform[] = [
"macos",
"windows",
"linux",
];
export interface InstallSoftwareLocation {
search: string;
pathname: string;
query: {
team_id?: string;
};
}
const InstallSoftware = ({
currentTeamId,
router,
urlPlatformParam,
}: ISetupExperienceCardProps) => {
const isValidPlatform = isSetupExperiencePlatform(urlPlatformParam);
// all uses of selectedPlatform are gated by above boolean
const selectedPlatform = urlPlatformParam as SetupExperiencePlatform;
const [showSelectSoftwareModal, setShowSelectSoftwareModal] = useState(false);
const [
selectedPlatform,
setSelectedPlatform,
] = useState<SetupExperiencePlatform>(DEFAULT_PLATFORM);
const {
data: softwareTitles,
@ -72,6 +84,7 @@ const InstallSoftware = ({
{
...DEFAULT_USE_QUERY_OPTIONS,
select: (res) => res.software_titles,
enabled: isValidPlatform,
}
);
@ -80,6 +93,7 @@ const InstallSoftware = ({
Error
>(["config", currentTeamId], () => configAPI.loadAll(), {
...DEFAULT_USE_QUERY_OPTIONS,
enabled: isValidPlatform,
});
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
@ -88,19 +102,33 @@ const InstallSoftware = ({
ITeamConfig
>(["team", currentTeamId], () => teamsAPI.load(currentTeamId), {
...DEFAULT_USE_QUERY_OPTIONS,
enabled: currentTeamId !== API_NO_TEAM_ID,
enabled: isValidPlatform && currentTeamId !== API_NO_TEAM_ID,
select: (res) => res.team,
});
const handleTabChange = useCallback(
(index: number) => {
const newPlatform = PLATFORM_BY_INDEX[index];
router.push(
PATHS.CONTROLS_INSTALL_SOFTWARE(newPlatform).concat(
location?.search ?? ""
)
);
},
[router]
);
if (!isValidPlatform) {
router.replace(
PATHS.CONTROLS_INSTALL_SOFTWARE("macos").concat(location?.search ?? "")
);
}
const onSave = async () => {
setShowSelectSoftwareModal(false);
refetchSoftwareTitles();
};
const handleTabChange = useCallback((index: number) => {
setSelectedPlatform(PLATFORM_BY_INDEX[index]);
}, []);
const hasManualAgentInstall = getManualAgentInstallSetting(
currentTeamId,
globalConfig,

View file

@ -288,16 +288,22 @@ const routes = (
<Route path="os-updates" component={OSUpdates} />
<Route path="os-settings" component={OSSettings} />
<Route path="os-settings/:section" component={OSSettings} />
<Route path="setup-experience" component={SetupExperience} />
<Route
path="setup-experience/:section"
component={SetupExperience}
/>
<Route
path="setup-experience/:section/:platform"
component={SetupExperience}
/>
<Route path="scripts">
<IndexRedirect to="library" />
<Route path=":section" component={Scripts} />
</Route>
<Route path="variables" component={Secrets} />
<Route
path="setup-experience/:section"
component={SetupExperience}
/>
</Route>
</Route>
<Route

View file

@ -1,3 +1,4 @@
import { SetupExperiencePlatform } from "interfaces/platform";
import URL_PREFIX from "./url_prefix";
const INTEGRATIONS_PREFIX = `${URL_PREFIX}/settings/integrations`;
@ -16,7 +17,8 @@ export default {
CONTROLS_END_USER_AUTHENTICATION: `${URL_PREFIX}/controls/setup-experience/end-user-auth`,
CONTROLS_BOOTSTRAP_PACKAGE: `${URL_PREFIX}/controls/setup-experience/bootstrap-package`,
CONTROLS_SETUP_ASSITANT: `${URL_PREFIX}/controls/setup-experience/setup-assistant`,
CONTROLS_INSTALL_SOFTWARE: `${URL_PREFIX}/controls/setup-experience/install-software`,
CONTROLS_INSTALL_SOFTWARE: (platform: SetupExperiencePlatform) =>
`${URL_PREFIX}/controls/setup-experience/install-software/${platform}`,
CONTROLS_RUN_SCRIPT: `${URL_PREFIX}/controls/setup-experience/run-script`,
CONTROLS_SCRIPTS: `${URL_PREFIX}/controls/scripts`,
CONTROLS_SCRIPTS_LIBRARY: `${URL_PREFIX}/controls/scripts/library`,