feat(grid): add column formatters

This commit is contained in:
t8y2
2026-05-15 23:27:09 +08:00
parent a14a6977fa
commit 1b934e72e0
8 changed files with 579 additions and 11 deletions
+138
View File
@@ -0,0 +1,138 @@
import { displayCellValue, type CellValue } from "@/lib/cellValue";
export type DateTimeFormatterUnit = "seconds" | "milliseconds" | "auto";
export type ColumnFormatterConfig =
| { kind: "datetime"; unit: DateTimeFormatterUnit }
| { kind: "json-path"; path: string }
| { kind: "mask"; prefix: number; suffix: number };
export interface ColumnFormatterKeyParts {
connectionId: string;
database?: string;
schema?: string;
tableName: string;
column: string;
}
export function buildColumnFormatterKey(parts: ColumnFormatterKeyParts): string {
return [parts.connectionId, parts.database ?? "", parts.schema ?? "", parts.tableName, parts.column].join("::");
}
export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig | undefined {
if (!value || typeof value !== "object") return undefined;
const config = value as Record<string, unknown>;
if (config.kind === "datetime") {
return config.unit === "seconds" || config.unit === "milliseconds" || config.unit === "auto"
? { kind: "datetime", unit: config.unit }
: undefined;
}
if (config.kind === "json-path") {
return typeof config.path === "string" && isSupportedJsonPath(config.path)
? { kind: "json-path", path: config.path }
: undefined;
}
if (config.kind === "mask") {
if (!Number.isInteger(config.prefix) || !Number.isInteger(config.suffix)) return undefined;
if ((config.prefix as number) < 0 || (config.suffix as number) < 0) return undefined;
return { kind: "mask", prefix: config.prefix as number, suffix: config.suffix as number };
}
return undefined;
}
export function applyColumnFormatter(value: CellValue, formatter: ColumnFormatterConfig | undefined): string {
if (!formatter) return displayCellValue(value);
if (value === null) return displayCellValue(value);
try {
if (formatter.kind === "datetime") return formatDateTime(value, formatter.unit);
if (formatter.kind === "json-path") return formatJsonPath(value, formatter.path);
if (formatter.kind === "mask") return formatMask(value, formatter);
return displayCellValue(value);
} catch {
return displayCellValue(value);
}
}
function formatDateTime(value: Exclude<CellValue, null>, unit: DateTimeFormatterUnit): string {
const numeric = typeof value === "number" ? value : Number(String(value).trim());
if (!Number.isFinite(numeric)) return displayCellValue(value);
const timestamp =
unit === "seconds" || (unit === "auto" && Math.abs(numeric) < 100_000_000_000) ? numeric * 1000 : numeric;
const date = new Date(timestamp);
return Number.isNaN(date.getTime()) ? displayCellValue(value) : date.toLocaleString();
}
function formatJsonPath(value: Exclude<CellValue, null>, path: string): string {
if (typeof value !== "string") return displayCellValue(value);
const parsed = JSON.parse(value);
const tokens = parseJsonPath(path);
let current: unknown = parsed;
for (const token of tokens) {
if (current == null) return "";
if (typeof token === "number") {
if (!Array.isArray(current)) return "";
current = current[token];
} else {
if (typeof current !== "object" || Array.isArray(current)) return "";
current = (current as Record<string, unknown>)[token];
}
}
if (current === undefined) return "";
if (current === null) return "NULL";
if (typeof current === "object") return JSON.stringify(current);
return String(current);
}
function formatMask(
value: Exclude<CellValue, null>,
formatter: Extract<ColumnFormatterConfig, { kind: "mask" }>,
): string {
const text = displayCellValue(value);
const visibleCount = formatter.prefix + formatter.suffix;
if (text.length <= visibleCount) return "*".repeat(text.length);
return `${text.slice(0, formatter.prefix)}${"*".repeat(text.length - visibleCount)}${text.slice(
text.length - formatter.suffix,
)}`;
}
function isSupportedJsonPath(path: string): boolean {
if (!path.startsWith("$")) return false;
try {
parseJsonPath(path);
return true;
} catch {
return false;
}
}
function parseJsonPath(path: string): Array<string | number> {
const tokens: Array<string | number> = [];
let index = 1;
while (index < path.length) {
if (path[index] === ".") {
const match = path.slice(index + 1).match(/^[A-Za-z_$][\w$]*/);
if (!match) throw new Error("Invalid JSON path");
tokens.push(match[0]);
index += match[0].length + 1;
continue;
}
if (path[index] === "[") {
const match = path.slice(index).match(/^\[(\d+)\]/);
if (!match) throw new Error("Invalid JSON path");
tokens.push(Number(match[1]));
index += match[0].length;
continue;
}
throw new Error("Invalid JSON path");
}
return tokens;
}