Self-hosted dashboard and admin auth (#4837)

Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
Gabriel Stein
2026-04-23 19:06:36 +05:30
committed by GitHub
co-authored by kartik-mem0
parent 15feaa8ac4
commit db8ac61713
252 changed files with 20275 additions and 331 deletions
@@ -0,0 +1,192 @@
import { ReactNode } from "react";
import { LucideIcon } from "lucide-react";
interface Column<T> {
key: keyof T;
label: string;
icon?: LucideIcon;
render?(value: T[keyof T], row: T): ReactNode;
className?: string;
width?: number | "auto";
align?: "left" | "center" | "right";
cellVariant?: "default" | "flush";
headerVariant?: "default" | "check";
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
className?: string;
getRowKey?: (row: T, rowIndex: number) => string | number;
onRowClick?: (row: T, rowIndex: number) => void;
getRowClassName?: (row: T, rowIndex: number) => string | undefined;
}
const classes = {
tableHeaderRow: "h-[38px] border-b border-memBorder-primary",
tableHeaderCell:
"w-[230px] h-[38px] p-2 align-middle bg-surface-default-fg-secondary text-onSurface-default-secondary",
tableHeaderCheckCell:
"w-[40px] h-[38px] p-2 align-middle bg-surface-default-fg-secondary",
tableHeaderCheckWrap: "flex items-center gap-2",
tableHeaderCheckBox:
"box-border flex h-4 w-4 items-center gap-2.5 rounded-sm border border-memBorder-primary p-1",
tableHeaderDivider:
"w-px self-stretch border border-memBorder-primary shrink-0",
tableRow:
"h-[38px] border-t border-memBorder-primary bg-surface-default-primary hover:bg-surface-default-primary-hover",
tableCell:
"text-sm font-medium text-onSurface-default-primary px-4 py-2 justify-start align-middle font-[Fustat] leading-[140%] tracking-normal",
tableCellFlush: "align-middle",
tableCellBase: "text-sm px-6",
tableCellPadding: "",
} as const;
export function DataTable<T>({
data,
columns,
className = "",
getRowKey,
onRowClick,
getRowClassName,
}: DataTableProps<T>) {
const minHeight = data.length > 0 ? Math.max(76, 38 + data.length * 38) : 100;
// Proportional column widths so table fits container (width numbers treated as relative weights)
const totalWeight = columns.reduce(
(sum, col) => sum + (typeof col.width === "number" ? col.width : 100),
0,
);
return (
<div
className={`min-w-0 max-w-full overflow-hidden transition-all duration-300 ease-in-out ${className}`}
style={{ minHeight: `${minHeight}px` }}
>
<table className="table-fixed w-full">
<colgroup>
{columns.map((col, i) => {
const weight = typeof col.width === "number" ? col.width : 100;
const pct =
totalWeight > 0
? (weight / totalWeight) * 100
: 100 / columns.length;
return <col key={i} style={{ width: `${pct}%` }} />;
})}
</colgroup>
<thead>
<tr className={classes.tableHeaderRow}>
{columns.map((column, index) => {
const isLastColumn = index === columns.length - 1;
if (column.headerVariant === "check") {
return (
<th key={index} className={classes.tableHeaderCheckCell}>
<div className="flex h-full items-stretch justify-between">
<div className={classes.tableHeaderCheckWrap}>
<span className={classes.tableHeaderCheckBox} />
</div>
{!isLastColumn && (
<div className={classes.tableHeaderDivider} />
)}
</div>
</th>
);
}
const Icon = column.icon;
const relevantClasses = column.className
? column.className
.split(" ")
.filter(
(c) =>
c.startsWith("w-") ||
c.startsWith("min-w-") ||
c.startsWith("max-w-") ||
c.startsWith("text-center") ||
c.startsWith("text-left") ||
c.startsWith("text-right"),
)
.join(" ")
: "";
const baseHeaderClass = classes.tableHeaderCell;
const alignClass =
column.align === "center"
? "text-center"
: column.align === "right"
? "text-right"
: "";
const mergedRelevantClasses =
`${relevantClasses} ${alignClass}`.trim();
const hasCustomAlignment =
mergedRelevantClasses.includes("text-center") ||
mergedRelevantClasses.includes("text-right");
const headerClassName = hasCustomAlignment
? `${baseHeaderClass.replace("text-left", "")} ${mergedRelevantClasses}`.trim()
: mergedRelevantClasses
? `${baseHeaderClass} ${mergedRelevantClasses}`.trim()
: baseHeaderClass;
const headerCellClassName = `${headerClassName} min-w-0`;
const flexAlignment = mergedRelevantClasses.includes(
"text-center",
)
? "justify-center"
: mergedRelevantClasses.includes("text-right")
? "justify-end"
: "";
return (
<th key={index} className={headerCellClassName}>
<div className="flex h-full min-w-0 items-stretch justify-between">
<div
className={`flex min-w-0 flex-1 items-center gap-2 overflow-hidden ${flexAlignment}`}
>
{Icon && <Icon className="size-4 shrink-0" />}
<span className="truncate font-[Fustat] text-sm font-semibold leading-[18px] text-onSurface-default-secondary">
{column.label}
</span>
</div>
{!isLastColumn && (
<div className={classes.tableHeaderDivider} />
)}
</div>
</th>
);
})}
</tr>
</thead>
<tbody className="transition-all duration-300 ease-in-out">
{data.map((row, rowIndex) => (
<tr
key={getRowKey ? String(getRowKey(row, rowIndex)) : rowIndex}
className={`${classes.tableRow} ${
onRowClick ? "cursor-pointer" : ""
} ${getRowClassName ? (getRowClassName(row, rowIndex) ?? "") : ""} animate-fade-in`}
onClick={onRowClick ? () => onRowClick(row, rowIndex) : undefined}
>
{columns.map((column, colIndex) => {
const value = row[column.key];
const baseCellClass =
column.cellVariant === "flush"
? classes.tableCellFlush
: classes.tableCell;
const cellClassName = `${column.className || baseCellClass} min-w-0 overflow-hidden`;
return (
<td key={colIndex} className={cellClassName}>
<div className="min-w-0 overflow-hidden">
{column.render
? column.render(value, row)
: String(value)}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}
export { classes as tableClasses };
@@ -0,0 +1,77 @@
import { Plus, RefreshCw, SearchCode, Trash, UserRound } from "lucide-react";
import { cn } from "@/lib/utils";
interface EventBadgeProps {
event: string;
type?: string;
count?: number;
label?: string;
icon?: React.ElementType;
showIcon?: boolean;
variant?: "primary" | "secondary";
}
type BadgeVariant = "add" | "update" | "retrieved" | "delete" | "user";
const getBadgeConfig = (
type: string,
): { variant: BadgeVariant; icon: React.ElementType } => {
switch (type.toUpperCase()) {
case "ADD":
return { variant: "add", icon: Plus };
case "UPDATE":
return { variant: "update", icon: RefreshCw };
case "SEARCH":
case "GET_ALL":
case "GET":
return { variant: "retrieved", icon: SearchCode };
case "DELETE":
return { variant: "delete", icon: Trash };
case "USER":
case "USERS":
return { variant: "user", icon: UserRound };
default:
return { variant: "add", icon: Plus };
}
};
export function EventBadge({
event,
type,
count,
label,
icon,
showIcon = true,
variant = "primary",
}: EventBadgeProps) {
const resolvedType = (type ?? event).toUpperCase();
const { variant: badgeVariant, icon: DefaultIcon } =
getBadgeConfig(resolvedType);
const Icon = icon ?? DefaultIcon;
const content = label ?? (typeof count === "number" ? String(count) : "");
if (count === 0 && !label) {
return null;
}
return (
<div
className={cn(
"inline-flex min-w-0 max-w-full items-center justify-center gap-1 overflow-hidden rounded-sm px-1.5 py-0.5",
variant === "secondary"
? "bg-surface-default-fg-secondary"
: "bg-surface-default-tertiary",
)}
aria-label={`${event} ${badgeVariant} count`}
>
{showIcon && (
<Icon className="size-3.5 shrink-0 text-onSurface-default-secondary" />
)}
{content && (
<span className="min-w-0 truncate font-dm-mono text-xs font-normal leading-[18px] text-onSurface-default-secondary">
{content}
</span>
)}
</div>
);
}
@@ -0,0 +1,38 @@
import { Skeleton } from "@/components/ui/skeleton";
interface TableSkeletonProps {
rows?: number;
columns?: number;
}
export function TableSkeleton({ rows = 5, columns = 5 }: TableSkeletonProps) {
return (
<div className="w-full">
{/* Table Header */}
<div className="border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50">
<div className="flex items-center gap-4 px-4 py-3">
<Skeleton className="size-4 rounded" />
{[...Array(columns - 1)].map((_, i) => (
<Skeleton key={i} className="h-4 flex-1" />
))}
</div>
</div>
{/* Table Rows */}
<div className="divide-y divide-zinc-200 dark:divide-zinc-800">
{[...Array(rows)].map((_, rowIndex) => (
<div key={rowIndex} className="flex items-center gap-4 px-4 py-3">
<Skeleton className="size-4 rounded" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
{[...Array(columns - 2)].map((_, colIndex) => (
<Skeleton key={colIndex} className="h-4 w-24" />
))}
</div>
))}
</div>
</div>
);
}