Skip to content

Commit 2bd55a5

Browse files
feat: FIT-1235: Reduce width of Active Sessions and Roles columns in Organization members table (#9222)
Co-authored-by: ricardoantoniocm <ricardoantoniocm@users.noreply.github.com>
1 parent d01e52a commit 2bd55a5

File tree

4 files changed

+135
-8
lines changed

4 files changed

+135
-8
lines changed

web/libs/ui/src/assets/icons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export { ReactComponent as IconMinus } from "./minus.svg";
179179
export { ReactComponent as IconModel } from "./model.svg";
180180
export { ReactComponent as IconModels } from "./models.svg";
181181
export { ReactComponent as IconModelVersion } from "./model-version.svg";
182+
export { ReactComponent as IconMonitors } from "./monitors.svg";
182183
export { ReactComponent as IconMoveLeft } from "./move-left.svg";
183184
export { ReactComponent as IconMoveRight } from "./move-right.svg";
184185
export { ReactComponent as IconMoveUp } from "./move-up.svg";
Lines changed: 4 additions & 0 deletions
Loading

web/libs/ui/src/lib/data-table/data-table.stories.tsx

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import type { ColumnDef, SortingState } from "@tanstack/react-table";
55
import type { ExtendedDataTableColumnDef } from "./data-table";
66
import { Badge } from "../badge/badge";
77
import { Button } from "../button/button";
8-
import { IconEdit, IconTrash } from "@humansignal/icons";
8+
import { Tooltip } from "../Tooltip/Tooltip";
9+
import { IconEdit, IconTrash, IconMonitors } from "@humansignal/icons";
910

1011
const meta: Meta<typeof DataTable> = {
1112
component: DataTable,
@@ -27,18 +28,36 @@ type User = {
2728
role: string;
2829
status: "active" | "inactive";
2930
lastActive: string;
31+
activeSessions?: number;
3032
};
3133

3234
const sampleData: User[] = [
33-
{ id: 1, name: "John Doe", email: "john@example.com", role: "Admin", status: "active", lastActive: "2024-01-15" },
34-
{ id: 2, name: "Jane Smith", email: "jane@example.com", role: "Editor", status: "active", lastActive: "2024-01-14" },
35+
{
36+
id: 1,
37+
name: "John Doe",
38+
email: "john@example.com",
39+
role: "Admin",
40+
status: "active",
41+
lastActive: "2024-01-15",
42+
activeSessions: 2,
43+
},
44+
{
45+
id: 2,
46+
name: "Jane Smith",
47+
email: "jane@example.com",
48+
role: "Editor",
49+
status: "active",
50+
lastActive: "2024-01-14",
51+
activeSessions: 1,
52+
},
3553
{
3654
id: 3,
3755
name: "Bob Johnson",
3856
email: "bob@example.com",
3957
role: "Viewer",
4058
status: "inactive",
4159
lastActive: "2024-01-10",
60+
activeSessions: 0,
4261
},
4362
{
4463
id: 4,
@@ -47,6 +66,7 @@ const sampleData: User[] = [
4766
role: "Editor",
4867
status: "active",
4968
lastActive: "2024-01-15",
69+
activeSessions: 1,
5070
},
5171
{
5272
id: 5,
@@ -55,6 +75,7 @@ const sampleData: User[] = [
5575
role: "Viewer",
5676
status: "active",
5777
lastActive: "2024-01-13",
78+
activeSessions: 3,
5879
},
5980
];
6081

@@ -516,3 +537,92 @@ export const ConditionalRowSelection: Story = {
516537
);
517538
},
518539
};
540+
541+
/**
542+
* Custom React Node Headers
543+
*
544+
* Demonstrates using custom React nodes as column headers instead of text.
545+
* The header property accepts any React node - icons, buttons, dropdowns, or any
546+
* custom component. Simply pass a function that returns your custom header content.
547+
*
548+
* This example shows an icon wrapped in a Tooltip for a compact column, but you can
549+
* use any React component as a header (buttons, dropdowns, badges, etc.).
550+
*
551+
* Hover over the monitors icon to see the tooltip explaining the column.
552+
*/
553+
export const WithCustomHeaders: Story = {
554+
render: () => {
555+
const [sorting, setSorting] = useState<SortingState>([]);
556+
557+
const columnsWithCustomHeaders: ColumnDef<User>[] = [
558+
{
559+
accessorKey: "name",
560+
header: "Name",
561+
enableSorting: true,
562+
},
563+
{
564+
accessorKey: "email",
565+
header: "Email",
566+
enableSorting: true,
567+
},
568+
{
569+
accessorKey: "role",
570+
header: "Role",
571+
cell: ({ getValue }) => {
572+
const role = getValue() as string;
573+
return (
574+
<Badge variant={role === "Admin" ? "primary" : role === "Editor" ? "success" : "info"} size="small">
575+
{role}
576+
</Badge>
577+
);
578+
},
579+
},
580+
{
581+
accessorKey: "activeSessions",
582+
// Icon-only header with tooltip - wrap icon in Tooltip directly
583+
header: () => (
584+
<Tooltip title="Active Sessions: Number of active browser sessions." alignment="top-center">
585+
<div className="flex items-center cursor-help">
586+
<IconMonitors width={24} height={24} />
587+
</div>
588+
</Tooltip>
589+
),
590+
size: 32,
591+
minSize: 30,
592+
cell: ({ getValue }) => {
593+
const sessions = getValue() as number;
594+
return (
595+
<div className="flex items-center justify-center">
596+
<Badge variant={sessions > 1 ? "warning" : "default"} size="small">
597+
{sessions}
598+
</Badge>
599+
</div>
600+
);
601+
},
602+
},
603+
{
604+
accessorKey: "lastActive",
605+
header: "Last Active",
606+
enableSorting: true,
607+
},
608+
];
609+
610+
return (
611+
<div className="flex flex-col gap-4">
612+
<div className="p-4 bg-neutral-surface rounded-md">
613+
<p className="text-sm text-neutral-content-subtle">
614+
💡 The column between "Role" and "Last Active" uses an icon wrapped in a Tooltip as the header. Hover over
615+
the monitors icon to see the tooltip.
616+
</p>
617+
</div>
618+
<DataTable
619+
data={sampleData}
620+
columns={columnsWithCustomHeaders}
621+
enableSorting
622+
sorting={sorting}
623+
onSortingChange={setSorting}
624+
/>
625+
</div>
626+
);
627+
},
628+
};

web/libs/ui/src/lib/data-table/data-table.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,13 @@ export const DataTable = <T extends DataShape>(props: DataTableProps<T>) => {
177177
// Determine if sorting is enabled for this column
178178
const columnSortingEnabled = enableSorting && col.enableSorting === true;
179179

180-
// Preserve original header - extract string if it's a string
181-
const originalHeader = typeof col.header === "string" ? col.header : undefined;
180+
// Preserve original header - extract string or call function to get React node
181+
const originalHeader =
182+
typeof col.header === "string"
183+
? col.header
184+
: typeof col.header === "function"
185+
? col.header({} as any) // Call the function to get the React node
186+
: undefined;
182187

183188
// Wrap all headers with unified Header component
184189
return {
@@ -725,12 +730,19 @@ export const Header = <T,>({
725730
return null;
726731
}
727732

733+
// Check if headerLabel is a string to wrap with Typography, or a React node to render directly
734+
const isStringHeader = typeof headerLabel === "string";
735+
728736
const headerContent = (
729737
<div className={cn(styles.headerContent, help && "gap-tighter")}>
730738
<div className="flex items-center gap-2">
731-
<Typography variant="label" size="small" className={cn(isSorted && styles.headerTextSorted)}>
732-
{headerLabel}
733-
</Typography>
739+
{isStringHeader ? (
740+
<Typography variant="label" size="small" className={cn(isSorted && styles.headerTextSorted)}>
741+
{headerLabel}
742+
</Typography>
743+
) : (
744+
headerLabel
745+
)}
734746
{help && (
735747
<Tooltip title={help} alignment="top-center">
736748
<IconInfoOutline width={18} height={18} className="text-neutral-content-subtler cursor-help shrink-0" />

0 commit comments

Comments
 (0)