mirror of
https://github.com/fleetdm/fleet
synced 2026-04-24 15:07:29 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38088 ## Testing - [x] 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 <img width="1163" height="494" alt="Screenshot 2026-01-16 at 11 00 53 AM" src="https://github.com/user-attachments/assets/46664267-2295-4690-97aa-e6ec16ef5e78" /> <img width="800" height="223" alt="Screenshot 2026-01-16 at 11 01 04 AM" src="https://github.com/user-attachments/assets/95116b23-a72f-45ba-a1ea-d3909053a827" /> <img width="1248" height="543" alt="Screenshot 2026-01-16 at 11 01 15 AM" src="https://github.com/user-attachments/assets/597976f2-07ed-4ce8-a299-27f8b1ad5cd3" /> <img width="1066" height="507" alt="Screenshot 2026-01-16 at 11 36 45 AM" src="https://github.com/user-attachments/assets/c5647a86-2723-4734-8d70-44db7f16cd0d" /> <img width="1476" height="349" alt="Screenshot 2026-01-16 at 11 42 53 AM" src="https://github.com/user-attachments/assets/c7097473-12e5-4011-88bd-c8208ef62325" />
313 lines
8.2 KiB
TypeScript
313 lines
8.2 KiB
TypeScript
import React from "react";
|
|
|
|
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
|
|
import StatusIndicator from "components/StatusIndicator";
|
|
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
|
|
import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell";
|
|
import TooltipWrapper from "components/TooltipWrapper";
|
|
import { IInvite } from "interfaces/invite";
|
|
import { IUser, UserRole } from "interfaces/user";
|
|
import { IDropdownOption } from "interfaces/dropdownOption";
|
|
import { generateRole, generateTeam, greyCell } from "utilities/helpers";
|
|
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
|
import { renderApiUserIndicator } from "pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPageTableConfig";
|
|
import ActionsDropdown from "../../../../../components/ActionsDropdown";
|
|
|
|
interface IHeaderProps {
|
|
column: {
|
|
title: string;
|
|
isSortedDesc: boolean;
|
|
};
|
|
}
|
|
|
|
interface IRowProps {
|
|
row: {
|
|
original: IUser | IInvite;
|
|
};
|
|
}
|
|
|
|
interface ICellProps extends IRowProps {
|
|
cell: {
|
|
value: string;
|
|
};
|
|
}
|
|
|
|
interface IActionsDropdownProps extends IRowProps {
|
|
cell: {
|
|
value: IDropdownOption[];
|
|
};
|
|
}
|
|
|
|
interface IDataColumn {
|
|
title: string;
|
|
Header: ((props: IHeaderProps) => JSX.Element) | string;
|
|
accessor: string;
|
|
Cell:
|
|
| ((props: ICellProps) => JSX.Element)
|
|
| ((props: IActionsDropdownProps) => JSX.Element);
|
|
disableHidden?: boolean;
|
|
disableSortBy?: boolean;
|
|
}
|
|
|
|
export interface IUserTableData {
|
|
name: string;
|
|
status: string;
|
|
email: string;
|
|
teams: string;
|
|
role: UserRole;
|
|
actions: IDropdownOption[];
|
|
id: number;
|
|
type: string;
|
|
api_only: boolean;
|
|
}
|
|
|
|
// NOTE: cellProps come from react-table
|
|
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
|
const generateTableHeaders = (
|
|
actionSelectHandler: (value: string, user: IUser | IInvite) => void,
|
|
isPremiumTier: boolean | undefined
|
|
): IDataColumn[] => {
|
|
const tableHeaders: IDataColumn[] = [
|
|
{
|
|
title: "Name",
|
|
Header: "Name",
|
|
disableSortBy: true,
|
|
accessor: "name",
|
|
Cell: (cellProps: ICellProps) => {
|
|
const apiOnlyUser =
|
|
"api_only" in cellProps.row.original
|
|
? cellProps.row.original.api_only
|
|
: false;
|
|
|
|
return (
|
|
<TooltipTruncatedTextCell
|
|
value={cellProps.cell.value}
|
|
suffix={apiOnlyUser && renderApiUserIndicator()}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: "Role",
|
|
Header: "Role",
|
|
accessor: "role",
|
|
disableSortBy: true,
|
|
Cell: (cellProps: ICellProps) => {
|
|
if (cellProps.cell.value === "GitOps") {
|
|
return (
|
|
<TooltipWrapper
|
|
tipContent={
|
|
<>
|
|
The GitOps role is only available on the command-line
|
|
<br />
|
|
when creating an API-only user. This user has no
|
|
<br />
|
|
access to the UI.
|
|
</>
|
|
}
|
|
>
|
|
GitOps
|
|
</TooltipWrapper>
|
|
);
|
|
}
|
|
if (cellProps.cell.value === "Observer+") {
|
|
return (
|
|
<TooltipWrapper
|
|
tipContent={
|
|
<>
|
|
Users with the Observer+ role have access to all of
|
|
<br />
|
|
the same functions as an Observer, with the added
|
|
<br />
|
|
ability to run any live query against all hosts.
|
|
</>
|
|
}
|
|
>
|
|
{cellProps.cell.value}
|
|
</TooltipWrapper>
|
|
);
|
|
}
|
|
const greyAndItalic = greyCell(cellProps.cell.value);
|
|
return (
|
|
<TextCell
|
|
value={cellProps.cell.value}
|
|
grey={greyAndItalic}
|
|
italic={greyAndItalic}
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: "Status",
|
|
Header: (cellProps) => (
|
|
<HeaderCell
|
|
value={cellProps.column.title}
|
|
isSortedDesc={cellProps.column.isSortedDesc}
|
|
/>
|
|
),
|
|
accessor: "status",
|
|
Cell: (cellProps: ICellProps) => (
|
|
<StatusIndicator value={cellProps.cell.value} />
|
|
),
|
|
},
|
|
{
|
|
title: "Email",
|
|
Header: "Email",
|
|
disableSortBy: true,
|
|
accessor: "email",
|
|
Cell: (cellProps: ICellProps) => (
|
|
<TextCell value={cellProps.cell.value} />
|
|
),
|
|
},
|
|
{
|
|
title: "Actions",
|
|
Header: "",
|
|
disableSortBy: true,
|
|
accessor: "actions",
|
|
Cell: (cellProps: IActionsDropdownProps) => (
|
|
<ActionsDropdown
|
|
options={cellProps.cell.value}
|
|
onChange={(value: string) =>
|
|
actionSelectHandler(value, cellProps.row.original)
|
|
}
|
|
placeholder="Actions"
|
|
menuAlign="right"
|
|
variant="small-button"
|
|
/>
|
|
),
|
|
},
|
|
];
|
|
|
|
// Add Teams column for premium tier
|
|
if (isPremiumTier) {
|
|
tableHeaders.splice(2, 0, {
|
|
title: "Teams",
|
|
Header: "Teams",
|
|
accessor: "teams",
|
|
disableSortBy: true,
|
|
Cell: (cellProps: ICellProps) => (
|
|
<TextCell value={cellProps.cell.value} />
|
|
),
|
|
});
|
|
}
|
|
|
|
return tableHeaders;
|
|
};
|
|
|
|
const generateStatus = (type: string, data: IUser | IInvite): string => {
|
|
const { teams, global_role } = data;
|
|
if (global_role === null && teams.length === 0) {
|
|
return "No access";
|
|
}
|
|
|
|
return type === "invite" ? "Invite pending" : "Active";
|
|
};
|
|
|
|
const generateActionDropdownOptions = (
|
|
isCurrentUser: boolean,
|
|
isInvitePending: boolean,
|
|
isSsoEnabled: boolean
|
|
): IDropdownOption[] => {
|
|
const disableDelete = isCurrentUser;
|
|
|
|
let dropdownOptions = [
|
|
{
|
|
label: "Edit",
|
|
disabled: false,
|
|
value: isCurrentUser ? "editMyAccount" : "edit",
|
|
},
|
|
{
|
|
label: "Require password reset",
|
|
disabled: isInvitePending,
|
|
value: "passwordReset",
|
|
},
|
|
{
|
|
label: "Reset sessions",
|
|
disabled: isInvitePending,
|
|
value: "resetSessions",
|
|
},
|
|
{
|
|
label: "Delete",
|
|
disabled: disableDelete,
|
|
value: "delete",
|
|
tooltipContent: disableDelete ? (
|
|
<>
|
|
There must be at least one Admin
|
|
<br />
|
|
user on the account. To delete this
|
|
<br />
|
|
user, add or set existing user with
|
|
<br />
|
|
role of "Admin".
|
|
</>
|
|
) : undefined,
|
|
},
|
|
];
|
|
|
|
if (isCurrentUser) {
|
|
// remove "Reset sessions" from dropdownOptions
|
|
dropdownOptions = dropdownOptions.filter(
|
|
(option) => option.label !== "Reset sessions"
|
|
);
|
|
}
|
|
|
|
if (isSsoEnabled) {
|
|
// remove "Require password reset" from dropdownOptions
|
|
dropdownOptions = dropdownOptions.filter(
|
|
(option) => option.label !== "Require password reset"
|
|
);
|
|
}
|
|
return dropdownOptions;
|
|
};
|
|
|
|
const enhanceUserData = (
|
|
users: IUser[],
|
|
currentUserId: number
|
|
): IUserTableData[] => {
|
|
return users.map((user) => {
|
|
return {
|
|
name: user.name || DEFAULT_EMPTY_CELL_VALUE,
|
|
status: generateStatus("user", user),
|
|
email: user.email,
|
|
teams: generateTeam(user.teams, user.global_role),
|
|
role: generateRole(user.teams, user.global_role),
|
|
actions: generateActionDropdownOptions(
|
|
user.id === currentUserId,
|
|
false,
|
|
user.sso_enabled
|
|
),
|
|
id: user.id,
|
|
type: "user",
|
|
api_only: user.api_only,
|
|
};
|
|
});
|
|
};
|
|
|
|
const enhanceInviteData = (invites: IInvite[]): IUserTableData[] => {
|
|
return invites.map((invite) => {
|
|
return {
|
|
name: invite.name || DEFAULT_EMPTY_CELL_VALUE,
|
|
status: generateStatus("invite", invite),
|
|
email: invite.email,
|
|
teams: generateTeam(invite.teams, invite.global_role),
|
|
role: generateRole(invite.teams, invite.global_role),
|
|
actions: generateActionDropdownOptions(false, true, invite.sso_enabled),
|
|
id: invite.id,
|
|
type: "invite",
|
|
api_only: false, // api only users are created through fleetctl and not invites
|
|
};
|
|
});
|
|
};
|
|
|
|
const combineDataSets = (
|
|
users: IUser[],
|
|
invites: IInvite[],
|
|
currentUserId: number
|
|
): IUserTableData[] => {
|
|
return [
|
|
...enhanceUserData(users, currentUserId),
|
|
...enhanceInviteData(invites),
|
|
];
|
|
};
|
|
|
|
export { generateTableHeaders, combineDataSets };
|