mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 11:50:44 +08:00
Ship gated eye polish, fabric levels, and collection UI refresh.
Topology MCP adds pull/compact/bundle/suggest-hubs with a no-template skill path; API gains fabric level and NE collection policy; web list pages get paging and denser collect/network workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b1aee86701
commit
b81e5869a6
91 changed files with 10395 additions and 1513 deletions
62
web/src/utils/csvExport.ts
Normal file
62
web/src/utils/csvExport.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
export type CsvColumn<T> = {
|
||||
key: string;
|
||||
header: string;
|
||||
value?: (row: T) => string | number | null | undefined;
|
||||
};
|
||||
|
||||
function escapeCsvCell(raw: string): string {
|
||||
if (/[",\r\n]/.test(raw)) return `"${raw.replace(/"/g, '""')}"`;
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** Build CSV text (UTF-8 BOM for Excel). */
|
||||
export function toCsv<T>(rows: T[], columns: CsvColumn<T>[]): string {
|
||||
const header = columns.map((c) => escapeCsvCell(c.header)).join(",");
|
||||
const lines = rows.map((row) =>
|
||||
columns
|
||||
.map((c) => {
|
||||
const v = c.value
|
||||
? c.value(row)
|
||||
: (row as Record<string, unknown>)[c.key];
|
||||
return escapeCsvCell(v == null ? "" : String(v));
|
||||
})
|
||||
.join(","),
|
||||
);
|
||||
return `\uFEFF${[header, ...lines].join("\r\n")}\r\n`;
|
||||
}
|
||||
|
||||
export function downloadTextFile(filename: string, content: string, mime = "text/csv;charset=utf-8") {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function downloadCsv<T>(filename: string, rows: T[], columns: CsvColumn<T>[]) {
|
||||
downloadTextFile(filename, toCsv(rows, columns));
|
||||
}
|
||||
|
||||
/** Page through a list API up to maxRows (default 2000). */
|
||||
export async function fetchAllPages<T>(opts: {
|
||||
pageSize?: number;
|
||||
maxRows?: number;
|
||||
fetchPage: (page: number, pageSize: number) => Promise<{ total?: number; items: T[] }>;
|
||||
}): Promise<T[]> {
|
||||
const pageSize = Math.max(1, Math.min(500, opts.pageSize || 200));
|
||||
const maxRows = Math.max(1, opts.maxRows || 2000);
|
||||
const out: T[] = [];
|
||||
let page = 1;
|
||||
while (out.length < maxRows) {
|
||||
const res = await opts.fetchPage(page, pageSize);
|
||||
const batch = res.items || [];
|
||||
if (!batch.length) break;
|
||||
out.push(...batch);
|
||||
if (batch.length < pageSize) break;
|
||||
if (typeof res.total === "number" && out.length >= res.total) break;
|
||||
page += 1;
|
||||
}
|
||||
return out.slice(0, maxRows);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue