From 29aa39a3928792c57f323912b16ba990aabe87bc Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Tue, 31 Mar 2026 09:11:51 -0500 Subject: [PATCH] Update default fleet selected on dashboard and controls (#42688) **Related issue:** Resolves #40317 # 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 With the current router we have in place, we can't really test `` elements, so our ability to make useful automated tests is pretty limited here. I extracted the fleet name sorting code into an exported function and added some tests for that. - [X] QA'd all new/changed functionality manually - [X] verified that when All Fleets is selected in dropdown, navigating to Controls switches to Workstations - [X] verified that when another fleet is selected in dropdown, navigating to Controls maintains that selection - [X] verified that when a fleet is selected in dropdown, navigating to the dashboard changes to All Fleets - [X] verified that when "Unassigned" is present in the fleets dropdown, it is at the bottom - [X] verified that when using a permalink to the dashboard with a fleet selected (e.g. `?fleet_id=1`), the correct fleet shows as selected --- changes/40137-update-default-fleet | 1 + .../top_nav/SiteTopNav/SiteTopNav.tsx | 6 +-- frontend/context/app.tests.ts | 48 +++++++++++++++++++ frontend/context/app.tsx | 38 +++++++-------- frontend/hooks/useTeamIdParam.ts | 13 +++-- 5 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 changes/40137-update-default-fleet create mode 100644 frontend/context/app.tests.ts diff --git a/changes/40137-update-default-fleet b/changes/40137-update-default-fleet new file mode 100644 index 0000000000..640fb016d5 --- /dev/null +++ b/changes/40137-update-default-fleet @@ -0,0 +1 @@ +- Updated the default fleet selected when navigating to the dashboard and to Controls diff --git a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx index c780b0150b..d4ae4299dd 100644 --- a/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx +++ b/frontend/components/top_nav/SiteTopNav/SiteTopNav.tsx @@ -151,16 +151,14 @@ const SiteTopNav = ({ if (iconName && iconName === "logo") { return (
  • -
    -
    +
  • ); } diff --git a/frontend/context/app.tests.ts b/frontend/context/app.tests.ts new file mode 100644 index 0000000000..ef2de6d757 --- /dev/null +++ b/frontend/context/app.tests.ts @@ -0,0 +1,48 @@ +import createMockUser from "__mocks__/userMock"; + +import { sortAvailableTeams } from "./app"; + +describe("sortAvailableTeams", () => { + it("places Unassigned last for global team users", () => { + const teams = [ + { id: 0, name: "Unassigned" }, + { id: 2, name: "Zebra" }, + { id: 1, name: "Alpha" }, + { id: -1, name: "All fleets" }, + ]; + const result = sortAvailableTeams(teams, createMockUser()); + expect(result.map((t) => t.name)).toEqual([ + "All fleets", + "Alpha", + "Zebra", + "Unassigned", + ]); + }); + + it("does not include All fleets or Unassigned for non-global users", () => { + const teams = [ + { id: 0, name: "Unassigned" }, + { id: 2, name: "Zebra" }, + { id: 1, name: "Alpha" }, + { id: -1, name: "All fleets" }, + ]; + const result = sortAvailableTeams( + teams, + createMockUser({ global_role: null }) + ); + expect(result.map((t) => t.name)).toEqual(["Alpha", "Zebra"]); + }); + + it("sorts named teams alphabetically (case-insensitive)", () => { + const teams = [ + { id: 3, name: "charlie" }, + { id: 1, name: "Alpha" }, + { id: 2, name: "Bravo" }, + ]; + const result = sortAvailableTeams( + teams, + createMockUser({ global_role: null }) + ); + expect(result.map((t) => t.name)).toEqual(["Alpha", "Bravo", "charlie"]); + }); +}); diff --git a/frontend/context/app.tsx b/frontend/context/app.tsx index 3a7d1816c8..c18f706cae 100644 --- a/frontend/context/app.tsx +++ b/frontend/context/app.tsx @@ -339,6 +339,24 @@ const setPermissions = ( }; }; +export const sortAvailableTeams = ( + availableFleets: ITeamSummary[], + user: IUser | null +): ITeamSummary[] => { + const sortedFleets = [...availableFleets] + .sort((a, b) => sort.caseInsensitiveAsc(a.name, b.name)) + .filter( + (t) => + t.name !== APP_CONTEXT_ALL_TEAMS_SUMMARY.name && + t.name !== APP_CONTEXT_NO_TEAM_SUMMARY.name + ); + if (user && permissions.isOnGlobalTeam(user)) { + sortedFleets.unshift(APP_CONTEXT_ALL_TEAMS_SUMMARY); + sortedFleets.push(APP_CONTEXT_NO_TEAM_SUMMARY); + } + return sortedFleets; +}; + const reducer = (state: InitialStateType, action: IAction) => { switch (action.type) { case ACTIONS.SET_USER_SETTINGS: { @@ -349,27 +367,9 @@ const reducer = (state: InitialStateType, action: IAction) => { }; } case ACTIONS.SET_AVAILABLE_TEAMS: { - const { user, availableTeams } = action; - - let sortedTeams = availableTeams.sort( - (a: ITeamSummary, b: ITeamSummary) => - sort.caseInsensitiveAsc(a.name, b.name) - ); - sortedTeams = sortedTeams.filter( - (t) => - t.name !== APP_CONTEXT_ALL_TEAMS_SUMMARY.name && - t.name !== APP_CONTEXT_NO_TEAM_SUMMARY.name - ); - if (user && permissions.isOnGlobalTeam(user)) { - sortedTeams.unshift( - APP_CONTEXT_ALL_TEAMS_SUMMARY, - APP_CONTEXT_NO_TEAM_SUMMARY - ); - } - return { ...state, - availableTeams: sortedTeams, + availableTeams: sortAvailableTeams(action.availableTeams, action.user), }; } case ACTIONS.SET_CURRENT_USER: { diff --git a/frontend/hooks/useTeamIdParam.ts b/frontend/hooks/useTeamIdParam.ts index ff6523ee2b..eced195bf0 100644 --- a/frontend/hooks/useTeamIdParam.ts +++ b/frontend/hooks/useTeamIdParam.ts @@ -1,6 +1,6 @@ import { useCallback, useContext, useEffect, useMemo } from "react"; import { InjectedRouter } from "react-router"; -import { findLastIndex, trimStart } from "lodash"; +import { findLastIndex, sortBy, trimStart } from "lodash"; import { AppContext } from "context/app"; import { TableContext } from "context/table"; @@ -221,8 +221,15 @@ const getDefaultTeam = ({ defaultTeam = userTeams.find((t) => t.id === APP_CONTEXT_ALL_TEAMS_ID); } if (!defaultTeam && includeNoTeam) { - // default to No team when "All teams" not included and no team is included - defaultTeam = userTeams.find((t) => t.id === APP_CONTEXT_NO_TEAM_ID); + // prefer the real fleet with the lowest ID over "Unassigned" + const realFleets = userTeams.filter( + (t) => t.id > APP_CONTEXT_NO_TEAM_ID + ); + if (realFleets.length > 0) { + defaultTeam = sortBy(realFleets, (t) => t.id)[0]; + } else { + defaultTeam = userTeams.find((t) => t.id === APP_CONTEXT_NO_TEAM_ID); + } } }