refactor: switch to using Bun (#718)

This commit is contained in:
Dan Ditomaso
2025-07-16 22:30:10 -04:00
committed by GitHub
parent acfd7ebd2d
commit 704d06cfe7
401 changed files with 35294 additions and 39107 deletions
@@ -1,3 +1,4 @@
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
import {
AccordionContent,
AccordionHeader,
@@ -5,13 +6,12 @@ import {
AccordionTrigger,
} from "@components/UI/Accordion.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import { Slider } from "@components/UI/Slider.tsx";
import { ScrollArea } from "@components/UI/ScrollArea.tsx";
import { Slider } from "@components/UI/Slider.tsx";
import { ToggleGroup, ToggleGroupItem } from "@components/UI/ToggleGroup.tsx";
import { ReactNode } from "react";
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
import { cn } from "@core/utils/cn.ts";
import type { ReactNode } from "react";
import { useId } from "react";
interface FilterAccordionItemProps {
label: string;
@@ -66,9 +66,7 @@ export const FilterAccordionItem = ({
</AccordionTrigger>
</AccordionHeader>
<AccordionContent
className={cn(
"px-1 pb-4 pt-2 space-y-3 dark:border-slate-700",
)}
className={cn("px-1 pb-4 pt-2 space-y-3 dark:border-slate-700")}
>
{children}
</AccordionContent>
@@ -85,6 +83,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
label,
step,
}: FilterSliderProps<K>) => {
const sliderId = useId();
const value: [number, number] = filterState[filterKey];
const defaultValue: [number, number] = defaultFilterValues[filterKey];
@@ -98,7 +97,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
return (
<div className="space-y-2">
<label className="block text-sm font-medium">
<label htmlFor={sliderId} className="block text-sm font-medium">
{labelContent ?? defaultLabel}
</label>
<Slider
@@ -112,6 +111,7 @@ export const FilterSlider = <K extends RangeKeys<FilterState>>({
rangeClassName="bg-blue-500 dark:bg-blue-600"
thumbClassName="w-3 h-3 bg-white dark:bg-slate-300 border border-slate-400 dark:border-slate-100"
aria-label={label ?? String(filterKey)}
id={sliderId}
/>
</div>
);
@@ -121,7 +121,7 @@ function getNumberArray<T extends FilterState, K extends EnumArrayKeys<T>>(
state: T,
key: K,
): number[] {
return state[key] as unknown as number[];
return state[key] as number[];
}
export const FilterMulti = <K extends EnumArrayKeys<FilterState>>({
filterKey,
@@ -132,8 +132,8 @@ export const FilterMulti = <K extends EnumArrayKeys<FilterState>>({
}: FilterMultiProps<K>) => {
const selected = getNumberArray(filterState, filterKey);
const allSelected = options.length > 0 &&
options.every((opt) => selected.includes(opt));
const allSelected =
options.length > 0 && options.every((opt) => selected.includes(opt));
const toggleAll = () => {
setFilterState((prev) => ({
@@ -188,16 +188,19 @@ export const FilterToggle = <K extends keyof FilterState>({
onChange,
}: FilterToggleProps<K>) => (
<div className="space-y-1 pb-1">
<label className="block text-sm font-medium">
<label htmlFor={label} className="block text-sm font-medium">
{label}
</label>
<ToggleGroup
type="single"
aria-label={label}
id={label}
onValueChange={(value) => onChange(filterKey, value)}
value={typeof filterState[filterKey] === "undefined"
? ""
: filterState[filterKey].toString()}
value={
typeof filterState[filterKey] === "undefined"
? ""
: String(filterState[filterKey])
}
>
<ToggleGroupItem
value="false"
@@ -1,37 +1,32 @@
import {
type ComponentProps,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { Protobuf } from "@meshtastic/core";
import { FunnelIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { debounce } from "@core/utils/debounce.ts";
import { cn } from "@core/utils/cn.ts";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/UI/Popover.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Accordion } from "@components/UI/Accordion.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import {
FilterAccordionItem,
FilterMulti,
FilterSlider,
FilterToggle,
} from "@components/generic/Filter/FilterComponents.tsx";
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Accordion } from "@components/UI/Accordion.tsx";
import { Input } from "@components/UI/Input.tsx";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/UI/Popover.tsx";
import { cn } from "@core/utils/cn.ts";
import { debounce } from "@core/utils/debounce.ts";
import { Protobuf } from "@meshtastic/core";
import type { TFunction } from "i18next";
import { FunnelIcon } from "lucide-react";
import {
type ComponentProps,
type ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
const DEBOUNCE_DELAY_MS = 250;
const BATTERY_STATUS_PLUGGED_IN_VALUE = 101;
@@ -76,24 +71,25 @@ interface LastHeardLabelProps {
formatTS: (ts: number) => ReactNode;
t: TFunction<"ui", undefined>;
}
function LastHeardLabelContent(
{ lastHeardRange, defaultMaxLastHeard, formatTS, t }: LastHeardLabelProps,
) {
function LastHeardLabelContent({
lastHeardRange,
defaultMaxLastHeard,
formatTS,
t,
}: LastHeardLabelProps) {
const [start, end] = lastHeardRange;
return (
<>
{t("lastHeard.labelText", { value: "" })}
<br />
{start === 0
? (
t("lastHeard.nowLabel")
)
: (
<>
{start === defaultMaxLastHeard && ">"}
{formatTS(start)}
</>
)}
{start === 0 ? (
t("lastHeard.nowLabel")
) : (
<>
{start === defaultMaxLastHeard && ">"}
{formatTS(start)}
</>
)}
{start !== end && (
<>
{" — "}
@@ -109,13 +105,16 @@ interface BatteryLevelLabelProps {
batteryLevelRange: (number | undefined)[];
t: TFunction<"ui", undefined>;
}
function BatteryLevelLabelContent(
{ batteryLevelRange, t }: BatteryLevelLabelProps,
) {
function BatteryLevelLabelContent({
batteryLevelRange,
t,
}: BatteryLevelLabelProps) {
const [start, end] = batteryLevelRange;
const formatBatteryValue = (value: number | undefined) => {
if (value === undefined) return "";
if (value === undefined) {
return "";
}
return value === BATTERY_STATUS_PLUGGED_IN_VALUE
? t("batteryStatus.pluggedIn")
: `${value}%`;
@@ -158,12 +157,12 @@ export function FilterControl({
const handleTextChange = useCallback(
<K extends keyof FilterState>(key: K) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterState((prev) => ({
...prev,
[key]: e.target.value,
}));
},
(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterState((prev) => ({
...prev,
[key]: e.target.value,
}));
},
[setFilterState],
);
@@ -175,27 +174,29 @@ export function FilterControl({
[key]: value,
}));
}, DEBOUNCE_DELAY_MS),
[setFilterState],
[],
);
const handleRangeChange = useCallback(
<K extends keyof FilterState>(key: K) => (value: number[]) => {
setLocalFilterState((prev) => ({
...prev,
[key]: value,
}));
debouncedSetFilterState(key, value);
},
<K extends keyof FilterState>(key: K) =>
(value: number[]) => {
setLocalFilterState((prev) => ({
...prev,
[key]: value,
}));
debouncedSetFilterState(key, value);
},
[debouncedSetFilterState],
);
const handleBoolChange = useCallback(
<K extends keyof FilterState>(key: K, value: string | boolean) => {
const typedValue = value === true || value === "true"
? true
: value === false || value === "false"
? false
: undefined;
const typedValue =
value === true || value === "true"
? true
: value === false || value === "false"
? false
: undefined;
setFilterState((prev) => ({
...prev,
@@ -298,20 +299,14 @@ export function FilterControl({
<FilterToggle
label={t("favorites.label")}
filterKey="isFavorite"
alternativeLabels={[
t("hide.label"),
t("showOnly.label"),
]}
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
filterState={filterState}
onChange={handleBoolChange}
/>
<FilterToggle
label={t("viaMqtt.label")}
filterKey="viaMqtt"
alternativeLabels={[
t("hide.label"),
t("showOnly.label"),
]}
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
filterState={filterState}
onChange={handleBoolChange}
/>
@@ -370,9 +365,8 @@ export function FilterControl({
Protobuf.Config.Config_DeviceConfig_Role,
).filter((v): v is number => typeof v === "number")}
getLabel={(val) =>
formatEnumLabel(
Protobuf.Config.Config_DeviceConfig_Role[val],
)}
formatEnumLabel(Protobuf.Config.Config_DeviceConfig_Role[val])
}
/>
</FilterAccordionItem>
@@ -385,27 +379,22 @@ export function FilterControl({
(v): v is number => typeof v === "number",
)}
getLabel={(val) =>
formatEnumLabel(Protobuf.Mesh.HardwareModel[val])}
formatEnumLabel(Protobuf.Mesh.HardwareModel[val])
}
/>
</FilterAccordionItem>
<FilterAccordionItem label={t("advanced.label")}>
<FilterToggle
label={t("hopsUnknown.label")}
filterKey="hopsUnknown"
alternativeLabels={[
t("hide.label"),
t("showOnly.label"),
]}
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
filterState={filterState}
onChange={handleBoolChange}
/>
<FilterToggle
label={t("showUnheard.label")}
filterKey="showUnheard"
alternativeLabels={[
t("hide.label"),
t("showOnly.label"),
]}
alternativeLabels={[t("hide.label"), t("showOnly.label")]}
filterState={filterState}
onChange={handleBoolChange}
/>
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import { useFilterNode } from "@components/generic/Filter/useFilterNode.ts";
import { Protobuf } from "@meshtastic/core";
import { renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
export function createMockNode(): Protobuf.Mesh.NodeInfo {
function createMockNode(): Protobuf.Mesh.NodeInfo {
return {
$typeName: "meshtastic.NodeInfo",
num: 1234567890,
@@ -14,6 +14,7 @@ export function createMockNode(): Protobuf.Mesh.NodeInfo {
isFavorite: true,
isIgnored: false,
hopsAway: 2,
isKeyManuallyVerified: false,
user: {
$typeName: "meshtastic.User",
id: "!12345678",
@@ -29,7 +30,7 @@ export function createMockNode(): Protobuf.Mesh.NodeInfo {
$typeName: "meshtastic.DeviceMetrics",
batteryLevel: 101,
voltage: 4.21,
channelUtilization: 7.50,
channelUtilization: 7.5,
airUtilTx: 2.57,
uptimeSeconds: 528092,
},
@@ -67,7 +67,9 @@ export function useFilterNode() {
...filterOverrides,
};
if (!node.user) return false;
if (!node.user) {
return false;
}
const nodeName = filterState.nodeName.toLowerCase();
if (
@@ -89,7 +91,7 @@ export function useFilterNode() {
if (
(filterState.hopsUnknown === true && node.hopsAway !== undefined) ||
filterState.hopsUnknown === false && node.hopsAway === undefined
(filterState.hopsUnknown === false && node.hopsAway === undefined)
) {
return false;
}
@@ -105,7 +107,7 @@ export function useFilterNode() {
if (
(filterState.showUnheard === true && (node.lastHeard ?? 0) !== 0) ||
filterState.showUnheard === false && (node.lastHeard ?? 0) === 0
(filterState.showUnheard === false && (node.lastHeard ?? 0) === 0)
) {
return false;
}
@@ -125,7 +127,9 @@ export function useFilterNode() {
}
const snr = node.snr ?? -20;
if (snr < filterState.snr[0] || snr > filterState.snr[1]) return false;
if (snr < filterState.snr[0] || snr > filterState.snr[1]) {
return false;
}
const channelUtilization = node.deviceMetrics?.channelUtilization ?? 0;
if (
@@ -159,13 +163,17 @@ export function useFilterNode() {
return false;
}
const role: Protobuf.Config.Config_DeviceConfig_Role = node.user.role ??
Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
if (!filterState.role.includes(role)) return false;
const role: Protobuf.Config.Config_DeviceConfig_Role =
node.user.role ?? Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
if (!filterState.role.includes(role)) {
return false;
}
const hwModel: Protobuf.Mesh.HardwareModel = node.user.hwModel ??
Protobuf.Mesh.HardwareModel.UNSET;
if (!filterState.hwModel.includes(hwModel)) return false;
const hwModel: Protobuf.Mesh.HardwareModel =
node.user.hwModel ?? Protobuf.Mesh.HardwareModel.UNSET;
if (!filterState.hwModel.includes(hwModel)) {
return false;
}
return true;
},
@@ -173,10 +181,7 @@ export function useFilterNode() {
);
const isFilterDirty = useCallback(
(
current: FilterState,
overrides?: Partial<FilterState>,
): boolean => {
(current: FilterState, overrides?: Partial<FilterState>): boolean => {
const base: FilterState = overrides
? { ...defaultFilterValues, ...overrides }
: defaultFilterValues;
+1 -5
View File
@@ -4,11 +4,7 @@ interface MonoProps extends React.HTMLAttributes<HTMLSpanElement> {
children: React.ReactNode;
className?: string;
}
export const Mono = ({
children,
className,
...rest
}: MonoProps) => {
export const Mono = ({ children, className, ...rest }: MonoProps) => {
return (
<span
className={cn("font-mono text-sm text-text-secondary", className)}
@@ -1,8 +1,13 @@
import { describe, expect, it } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { DataRow, Heading, Table } from "@components/generic/Table/index.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { Mono } from "@components/generic/Mono.tsx";
import {
type DataRow,
type Heading,
Table,
} from "@components/generic/Table/index.tsx";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
// @ts-types="react"
describe("Generic Table", () => {
@@ -1,7 +1,6 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { useMemo, useState } from "react";
import React from "react";
import { cn } from "@core/utils/cn.ts";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
export interface Heading {
title: string;
@@ -49,10 +48,14 @@ export const Table = ({ headings, rows }: TableProps) => {
};
const sortedRows = useMemo(() => {
if (!sortColumn) return rows;
if (!sortColumn) {
return rows;
}
const columnIndex = headings.findIndex((h) => h.title === sortColumn);
if (columnIndex === -1) return rows;
if (columnIndex === -1) {
return rows;
}
return [...rows].sort((a, b) => {
if (a.isFavorite !== b.isFavorite) {
@@ -73,8 +76,12 @@ export const Table = ({ headings, rows }: TableProps) => {
bValue = bCell.sortValue;
}
if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
if (aValue < bValue) {
return sortOrder === "asc" ? -1 : 1;
}
if (aValue > bValue) {
return sortOrder === "asc" ? 1 : -1;
}
return 0;
});
}, [rows, sortColumn, sortOrder, headings]);
@@ -99,18 +106,23 @@ export const Table = ({ headings, rows }: TableProps) => {
}
}}
tabIndex={heading.sortable ? 0 : -1}
role="columnheader"
aria-sort={sortColumn === heading.title
? sortOrder === "asc" ? "ascending" : "descending"
: "none"}
aria-sort={
sortColumn === heading.title
? sortOrder === "asc"
? "ascending"
: "descending"
: "none"
}
>
<div className="flex items-center gap-2">
{heading.title}
{heading.sortable && sortColumn === heading.title && (
sortOrder === "asc"
? <ChevronUpIcon size={16} aria-hidden="true" />
: <ChevronDownIcon size={16} aria-hidden="true" />
)}
{heading.sortable &&
sortColumn === heading.title &&
(sortOrder === "asc" ? (
<ChevronUpIcon size={16} aria-hidden="true" />
) : (
<ChevronDownIcon size={16} aria-hidden="true" />
))}
</div>
</th>
))}
@@ -126,26 +138,25 @@ export const Table = ({ headings, rows }: TableProps) => {
: "bg-white dark:bg-slate-900 odd:bg-slate-200/40 dark:odd:bg-slate-800/40",
)}
>
{row.cells.map((cell, cellIndex) =>
cellIndex === 0
? (
<th
key={`${row.id}_${cellIndex}`}
className="whitespace-nowrap px-3 py-2 text-sm text-left text-text-secondary"
scope="row"
>
{cell.content}
</th>
)
: (
<td
key={`${row.id}_${cellIndex}`}
className="whitespace-nowrap px-3 py-2 text-sm text-text-secondary"
>
{cell.content}
</td>
)
)}
{row.cells.map((cell, cellIndex) => {
const key = `${row.id}_${cellIndex}`;
const isFirstCell = cellIndex === 0;
const cellElement = isFirstCell ? (
<th
className="whitespace-nowrap px-3 py-2 text-sm text-left text-text-secondary"
scope="row"
>
{cell.content}
</th>
) : (
<td className="whitespace-nowrap px-3 py-2 text-sm text-text-secondary">
{cell.content}
</td>
);
return React.cloneElement(cellElement, { key });
})}
</tr>
))}
</tbody>