Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[WIP] serial number lookup + filter to only "All" group
Browse files Browse the repository at this point in the history
Signed-off-by: Edouard Vanbelle <edouard.vanbelle@shadow.tech>
EdouardVanbelle committed Jan 29, 2025
1 parent cd7f433 commit b3db19e
Showing 4 changed files with 235 additions and 102 deletions.
2 changes: 1 addition & 1 deletion src/modules/groups/AssignPeerToGroupModal.tsx
Original file line number Diff line number Diff line change
@@ -370,6 +370,6 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
header: ({ column }) => {
return <DataTableHeader column={column}>OS</DataTableHeader>;
},
cell: ({ row }) => <PeerOSCell os={row.original.os} />,
cell: ({ row }) => <PeerOSCell os={row.original.os} serial={row.original.serial_number} />,
},
];
81 changes: 70 additions & 11 deletions src/modules/groups/GroupSelector.tsx
Original file line number Diff line number Diff line change
@@ -21,17 +21,23 @@ import { Group } from "@/interfaces/Group";

interface MultiSelectProps {
values: string[];
onChange: (items: string[]) => void;
exactValue?: string;
onChange: (items: string[], exactItem?: string) => void;
disabled?: boolean;
popoverWidth?: "auto" | number;
groups: Group[] | undefined;
unassignedCount?: number;
defaultGroupName?: string;
}
export function GroupSelector({
onChange,
values,
exactValue,
disabled = false,
popoverWidth = 400,
groups,
unassignedCount,
defaultGroupName = "All", //defined as a property, no clue if this value may change in the future
}: MultiSelectProps) {
const searchRef = React.useRef<HTMLInputElement>(null);
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
@@ -40,9 +46,20 @@ export function GroupSelector({
const toggle = (code: string) => {
const isSelected = values.find((c) => c == code) != undefined;
if (isSelected) {
onChange && onChange(values.filter((c) => c != code));
onChange && onChange(values.filter((c) => c != code), undefined);
} else {
onChange && onChange([...values, code]);
onChange && onChange([...values, code], undefined);
setSearch("");
}
};

const toggleExactGroup = (code: string) => {
const isSelected = exactValue == code;
if (isSelected) {
onChange && onChange([], undefined);
setSearch("");
} else {
onChange && onChange([], code);
setSearch("");
}
};
@@ -62,14 +79,16 @@ export function GroupSelector({
}}
>
<PopoverTrigger asChild={true}>
<Button variant={"secondary"} disabled={disabled} ref={inputRef}>
<Button variant={"secondary"} disabled={disabled} ref={inputRef} className="w-[200px] justify-between">
<FolderGit2 size={16} className={"shrink-0"} />
<div className={"w-full flex justify-between"}>
{values.length > 0 ? (
<div>{values.length} Group(s)</div>
) : (
"All Groups"
)}
{
exactValue != undefined
? ("Unassigned peers")
: values.length > 0
? (`${values.length} Group(s)`)
: ("All Groups")
}
<div className={"pl-2"}>
<ChevronsUpDown size={18} className={"shrink-0"} />
</div>
@@ -132,7 +151,6 @@ export function GroupSelector({
</div>
</div>
</div>

<ScrollArea
className={
"max-h-[380px] overflow-y-auto flex flex-col gap-1 pl-2 py-2 pr-3"
@@ -141,7 +159,48 @@ export function GroupSelector({
<CommandGroup>
<div className={""}>
<div className={"grid grid-cols-1 gap-1"}>
{orderBy(groups, "name")?.map((item) => {
<CommandItem
className={"p-1"}
onSelect={() => {
toggleExactGroup( defaultGroupName);
searchRef.current?.focus();
}}
onClick={(e) => e.preventDefault()}
>
<div
className={
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-3 py-1 px-1 w-full"
}
>
<Checkbox checked={exactValue == defaultGroupName}/>
<div
className={
"flex justify-between items-center w-full"
}
>
<div
className={
"flex items-center gap-2 whitespace-nowrap text-sm"
}
>
<FolderGit2 size={13} className={"shrink-0"} />
<TextWithTooltip text={"Unassigned peers"} />
</div>
<div
className={
"flex items-center gap-2 text-xs text-nb-gray-200/60"
}
>
<MonitorSmartphoneIcon size={13} />
{unassignedCount} Peer(s)
</div>
</div>
</div>
</CommandItem>
<hr />
{orderBy(groups, "name")
?.filter((group) => group.name != defaultGroupName) // Ignore default group
?.map((item) => {
const value = item?.name || "";
if (value === "") return null;
const isSelected =
42 changes: 40 additions & 2 deletions src/modules/peers/PeerOSCell.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@components/Tooltip";
import { Barcode, Laptop } from "lucide-react";
import Image from "next/image";
import React, { useMemo } from "react";
import { FaWindows } from "react-icons/fa6";
@@ -14,7 +15,7 @@ import FreeBSDLogo from "@/assets/os-icons/FreeBSD.png";
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
import { OperatingSystem } from "@/interfaces/OperatingSystem";

export function PeerOSCell({ os }: { os: string }) {
export function PeerOSCell({ os, serial }: { os: string, serial?: string }) {
return (
<TooltipProvider>
<Tooltip delayDuration={1}>
@@ -34,13 +35,50 @@ export function PeerOSCell({ os }: { os: string }) {
</div>
</TooltipTrigger>
<TooltipContent>
<div className={"text-neutral-300 flex flex-col gap-1"}>{os}</div>
<ListItem
icon={<Laptop size={14} />}
label={"OS"}
value={os}
/>
{ (serial !== undefined) &&
<ListItem

icon={<Barcode size={14} />}
label={"Serial"}
value={serial}
/>
}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

const ListItem = ({
icon,
label,
value,
}: {
icon: React.ReactNode;
label: string;
value: string | React.ReactNode;
}) => {
return (
<div
className={
"flex justify-between gap-5 border-b border-nb-gray-920 py-2 px-4 last:border-b-0 text-xs"
}
>
<div className={"flex items-center gap-2 text-nb-gray-100 font-medium"}>
{icon}
{label}
</div>
<div className={"text-nb-gray-400"}>{value}</div>
</div>
);
};


export function OSLogo({ os }: { os: string }) {
const icon = useMemo(() => {
return getOperatingSystem(os);
212 changes: 124 additions & 88 deletions src/modules/peers/PeersTable.tsx
Original file line number Diff line number Diff line change
@@ -12,8 +12,11 @@ import GetStartedTest from "@components/ui/GetStartedTest";
import { NotificationCountBadge } from "@components/ui/NotificationCountBadge";
import {
ColumnDef,
ColumnFiltersState,
RowSelectionState,
SortingState,
Table,
VisibilityState,
} from "@tanstack/react-table";
import { uniqBy } from "lodash";
import { ExternalLinkIcon } from "lucide-react";
@@ -40,6 +43,7 @@ import PeerVersionCell from "@/modules/peers/PeerVersionCell";
const PeersTableColumns: ColumnDef<Peer>[] = [
{
id: "select",
enableHiding: false,
header: ({ table }) => (
<div className={"min-w-[20px] max-w-[20px]"}>
<Checkbox
@@ -60,11 +64,11 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
</div>
),
enableSorting: false,
enableHiding: false,
},
{
id: "name",
accessorFn: (peer) => `${peer?.name}${peer?.dns_label}`,
enableHiding: false,
header: ({ column }) => {
return <DataTableHeader column={column}>Name</DataTableHeader>;
},
@@ -83,6 +87,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
accessorFn: (peer) => peer.connected,
},
{
id: "ip",
accessorKey: "ip",
sortingFn: "text",
},
@@ -96,18 +101,29 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
},
{
id: "dns_label",
enableHiding: false,
accessorKey: "dns_label",
header: ({ column }) => {
return <DataTableHeader column={column}>Address</DataTableHeader>;
},
cell: ({ row }) => <PeerAddressCell peer={row.original} />,
},
{
id: "group_name_strings",
accessorKey: "group_name_strings",
accessorFn: (peer) => peer.groups?.map((g) => g?.name || "").join(", "),
sortingFn: "text",
},
{
// used for exact group matching
id: "exact_group_name_strings",
accessorKey: "exact_group_name_strings",
accessorFn: (peer) => peer.groups?.map((g) => g?.name || "").join("|"),
sortingFn: "text",
filterFn: "equals"
},
{
id: "group_names",
accessorKey: "group_names",
accessorFn: (peer) => peer.groups?.map((g) => g?.name || ""),
sortingFn: "text",
@@ -126,6 +142,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
),
},
{
id: "last_seen",
accessorKey: "last_seen",
header: ({ column }) => {
return <DataTableHeader column={column}>Last seen</DataTableHeader>;
@@ -134,13 +151,23 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
cell: ({ row }) => <PeerLastSeenCell peer={row.original} />,
},
{
id: "os",
accessorKey: "os",
header: ({ column }) => {
return <DataTableHeader column={column}>OS</DataTableHeader>;
},
cell: ({ row }) => <PeerOSCell os={row.original.os} />,
cell: ({ row }) => <PeerOSCell os={row.original.os} serial={row.original.serial_number} />,
},
{
id: "serial",
header: ({ column }) => {
return <DataTableHeader column={column}>Serial number</DataTableHeader>;
},
accessorFn: (peer) => peer.serial_number,
sortingFn: "text",
},
{
id: "version",
accessorKey: "version",
header: ({ column }) => {
return <DataTableHeader column={column}>Version</DataTableHeader>;
@@ -167,8 +194,10 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
},
{
id: "actions",
enableHiding: false,
accessorKey: "id",
header: "",

cell: ({ row }) => (
<PeerProvider peer={row.original}>
<PeerActionCell />
@@ -206,9 +235,19 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
],
);

// Caveat: no clue if this name may change in the future
// By default server assign every peers to `All` group
const DEFAULT_GROUP_NAME = "All";

const pendingApprovalCount =
peers?.filter((p) => p.approval_required).length || 0;

// Count peers that are not assigned to other group than the default group
const unassignedCount = peers?.reduce(
(acc, p) => acc + ((p.groups?.length == 1 && p.groups.at(0)?.name == DEFAULT_GROUP_NAME) ? 1: 0),
0
);

const tableGroups =
(uniqBy(
peers?.map((p) => p.groups?.map((g) => g)).flatMap((g) => g),
@@ -219,41 +258,80 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {

const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});

const colVisibility: VisibilityState = {
select: !isUser,
actions: !isUser,
groups: !isUser,
connected: false,
approval_required: false,

// hidden, but usefull for lookup
serial: false,
group_name_strings: false,
exact_group_name_strings: false,
group_names: false,
ip: false,
user_name: false,
user_email: false,
}

const [resultingColumnVisibility, setColumnVisibility] = useState(colVisibility);

const resetSelectedRows = () => {
if (Object.keys(selectedRows).length > 0) {
setSelectedRows({});
}
};

/**
* This function overrides the given column filters and reuses the previous filters
*
* Table index and selection is also cleared
*/
const overrideTableFilter = (table: Table<Peer>, change: ColumnFiltersState) => {
let filters = [] as ColumnFiltersState;

// List of all used column filters
[
"connected",
"approval_required",
"exact_group_name_strings",
"group_names",
].forEach((columnId) => {
let columnFilter = change.find((entry) => entry.id == columnId);
if (columnFilter === undefined) {
columnFilter = {
id: columnId,
value: table.getColumn(columnId)?.getFilterValue()
}
}
filters.push(columnFilter);
})
table.setPageIndex(0);
table.setColumnFilters( filters);
resetSelectedRows();
};

return (
<>
<PeerMultiSelect
selectedPeers={selectedRows}
onCanceled={() => setSelectedRows({})}
/>
<DataTable
keepStateInLocalStorage={true}
headingTarget={headingTarget}
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
useRowId={true}
text={"Peers"}
sorting={sorting}
setSorting={setSorting}
setColumnVisibility={setColumnVisibility}
columns={PeersTableColumns}
data={peers}
searchPlaceholder={"Search by name, IP, owner or group..."}
columnVisibility={{
select: !isUser,
connected: false,
approval_required: false,
group_name_strings: false,
group_names: false,
ip: false,
user_name: false,
user_email: false,
actions: !isUser,
groups: !isUser,
}}
searchPlaceholder={"Search by name, IP, Serial, owner or group..."}
columnVisibility={resultingColumnVisibility}
isLoading={isLoading}
getStartedCard={
<GetStartedTest
@@ -292,29 +370,12 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
<ButtonGroup.Button
disabled={peers?.length == 0}
onClick={() => {
table.setPageIndex(0);
let groupFilters = table
.getColumn("group_names")
?.getFilterValue();
table.setColumnFilters([
overrideTableFilter( table, [
{
id: "connected",
value: undefined,
},
{
id: "approval_required",
value: undefined,
},
{
id: "group_names",
value: groupFilters ?? [],
},
{
id: "group_names",
value: groupFilters ?? [],
},
]);
resetSelectedRows();
}}
variant={
table.getColumn("connected")?.getFilterValue() == undefined
@@ -326,29 +387,12 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
let groupFilters = table
.getColumn("group_names")
?.getFilterValue();
table.setColumnFilters([
overrideTableFilter( table, [
{
id: "connected",
value: true,
},
{
id: "approval_required",
value: undefined,
},
{
id: "group_names",
value: groupFilters ?? [],
},
{
id: "group_names",
value: groupFilters ?? [],
},
]);
resetSelectedRows();
}}
disabled={peers?.length == 0}
variant={
@@ -361,25 +405,12 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
</ButtonGroup.Button>
<ButtonGroup.Button
onClick={() => {
table.setPageIndex(0);
let groupFilters = table
.getColumn("group_names")
?.getFilterValue();
table.setColumnFilters([
overrideTableFilter( table, [
{
id: "connected",
value: false,
},
{
id: "approval_required",
value: undefined,
},
{
id: "group_names",
value: groupFilters ?? [],
},
]);
resetSelectedRows();
}}
disabled={peers?.length == 0}
variant={
@@ -396,29 +427,21 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
<Button
disabled={peers?.length == 0}
onClick={() => {
table.setPageIndex(0);
let current =
table.getColumn("approval_required")?.getFilterValue() ===
undefined
undefined
? true
: undefined;

table.setColumnFilters([
{
id: "connected",
value: undefined,
},
overrideTableFilter( table, [
{
id: "approval_required",
value: current,
},
]);

resetSelectedRows();
}}
variant={
table.getColumn("approval_required")?.getFilterValue() ===
true
true
? "tertiary"
: "secondary"
}
@@ -428,30 +451,43 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
</Button>
)}

<DataTableRowsPerPage table={table} disabled={peers?.length == 0} />

{!isUser && (
{
!isUser
&& tableGroups.length > 1 // if length == 1, it means only "All" group exists, case not relevant
&& (
<GroupSelector
disabled={peers?.length == 0}
values={
(table
.getColumn("group_names")
?.getFilterValue() as string[]) || []
}
onChange={(groups) => {
table.setPageIndex(0);
if (groups.length == 0) {
table.getColumn("group_names")?.setFilterValue(undefined);
return;
} else {
table.getColumn("group_names")?.setFilterValue(groups);
}
resetSelectedRows();
exactValue={
(table
.getColumn("exact_group_name_strings")
?.getFilterValue() as string) || undefined
}
onChange={(groups, exactItem) => {
const group_filter = groups.length == 0 ? undefined : groups;
overrideTableFilter( table, [
{
id: "group_names",
value: group_filter
},
{
id: "exact_group_name_strings",
value: exactItem
}
]);
}}
groups={tableGroups}
unassignedCount={unassignedCount}
defaultGroupName={DEFAULT_GROUP_NAME}
/>
)}

<DataTableRowsPerPage table={table} disabled={peers?.length == 0} />

<DataTableRefreshButton
isDisabled={peers?.length == 0}
onClick={() => {

0 comments on commit b3db19e

Please sign in to comment.