Update default fleet selected on dashboard and controls (#42688)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**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 `<Link>`
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
This commit is contained in:
Scott Gress 2026-03-31 09:11:51 -05:00 committed by GitHub
parent e62bdf17b6
commit 29aa39a392
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 80 additions and 26 deletions

View file

@ -0,0 +1 @@
- Updated the default fleet selected when navigating to the dashboard and to Controls

View file

@ -151,16 +151,14 @@ const SiteTopNav = ({
if (iconName && iconName === "logo") {
return (
<li className={navItemClasses} key={`nav-item-${name}`}>
<LinkWithContext
<Link
className={`${navItemBaseClass}__logo-wrapper`}
currentQueryParams={currentQueryParams}
to={navItem.location.pathname}
withParams={{ type: "query", names: ["fleet_id"] }}
>
<div className={`${navItemBaseClass}__logo`}>
<OrgLogoIcon className="logo" src={orgLogoURL} />
</div>
</LinkWithContext>
</Link>
</li>
);
}

View file

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

View file

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

View file

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