react-auto-smart-table 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -0
- package/dist/index.css +404 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +1259 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1218 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
// src/analyzer/detectTypes.ts
|
|
2
|
+
var EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
|
|
3
|
+
var URL_REGEX = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6}\.?)(\/[\w.-]*)*\/?$/i;
|
|
4
|
+
var IMAGE_REGEX = /\.(jpeg|jpg|gif|png|webp|svg|bmp)(\?.*)?$/i;
|
|
5
|
+
var isEmail = (value) => EMAIL_REGEX.test(value);
|
|
6
|
+
var isImage = (value) => {
|
|
7
|
+
return URL_REGEX.test(value) && IMAGE_REGEX.test(value);
|
|
8
|
+
};
|
|
9
|
+
var isUrl = (value) => {
|
|
10
|
+
if (isImage(value)) return false;
|
|
11
|
+
return URL_REGEX.test(value);
|
|
12
|
+
};
|
|
13
|
+
var isDate = (value) => {
|
|
14
|
+
if (value instanceof Date) return !isNaN(value.getTime());
|
|
15
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
16
|
+
if (typeof value === "number") return false;
|
|
17
|
+
if (typeof value === "string" && value.trim() !== "" && !isNaN(Number(value))) return false;
|
|
18
|
+
const datePattern = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?Z?)?$|^\d{1,2}[-/]\d{1,2}[-/]\d{2,4}$/;
|
|
19
|
+
if (typeof value === "string" && !datePattern.test(value.trim())) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const d = new Date(value);
|
|
23
|
+
return !isNaN(d.getTime()) && String(d) !== "Invalid Date";
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
var isCurrency = (value) => {
|
|
28
|
+
return typeof value === "string" && /^[£$€¥]?\s*-?\d+(?:,\d{3})*(?:\.\d+)?\s*[£$€¥]?$/.test(value) && /[£$€¥]/.test(value);
|
|
29
|
+
};
|
|
30
|
+
var isPercentage = (value) => {
|
|
31
|
+
return typeof value === "string" && /^-?\d+(?:\.\d+)?%$/.test(value);
|
|
32
|
+
};
|
|
33
|
+
var isNumber = (value) => {
|
|
34
|
+
if (typeof value === "number") return true;
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return value.trim() !== "" && !isNaN(Number(value));
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
};
|
|
40
|
+
var isBoolean = (value) => {
|
|
41
|
+
if (typeof value === "boolean") return true;
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const lower = value.toLowerCase();
|
|
44
|
+
return lower === "true" || lower === "false" || lower === "yes" || lower === "no";
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
var detectValueType = (value) => {
|
|
49
|
+
if (value === null || value === void 0 || value === "") return "string";
|
|
50
|
+
const strVal = String(value);
|
|
51
|
+
if (isEmail(strVal)) return "email";
|
|
52
|
+
if (isUrl(strVal)) return "url";
|
|
53
|
+
if (isImage(strVal)) return "image";
|
|
54
|
+
if (isDate(value)) return "date";
|
|
55
|
+
if (isCurrency(strVal)) return "currency";
|
|
56
|
+
if (isPercentage(strVal)) return "percentage";
|
|
57
|
+
if (isNumber(value)) return "number";
|
|
58
|
+
if (isBoolean(value)) return "boolean";
|
|
59
|
+
return "string";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/analyzer/sampleDataset.ts
|
|
63
|
+
function sampleDataset(dataset, maxSampleSize = 200) {
|
|
64
|
+
if (!dataset || !dataset.length) return [];
|
|
65
|
+
if (dataset.length <= maxSampleSize) return [...dataset];
|
|
66
|
+
const sample = [];
|
|
67
|
+
const indices = /* @__PURE__ */ new Set();
|
|
68
|
+
while (sample.length < maxSampleSize) {
|
|
69
|
+
const idx = Math.floor(Math.random() * dataset.length);
|
|
70
|
+
if (!indices.has(idx)) {
|
|
71
|
+
indices.add(idx);
|
|
72
|
+
sample.push(dataset[idx]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return sample;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/analyzer/computeStats.ts
|
|
79
|
+
var computeColumnStats = (columnKey, dataset, type) => {
|
|
80
|
+
const stats = {};
|
|
81
|
+
const uniqueVals = /* @__PURE__ */ new Set();
|
|
82
|
+
let min = void 0;
|
|
83
|
+
let max = void 0;
|
|
84
|
+
for (const row of dataset) {
|
|
85
|
+
const val = row[columnKey];
|
|
86
|
+
if (val !== null && val !== void 0 && val !== "") {
|
|
87
|
+
uniqueVals.add(val);
|
|
88
|
+
if (type === "number" || type === "currency" || type === "percentage") {
|
|
89
|
+
let numVal = NaN;
|
|
90
|
+
if (type === "number") numVal = Number(val);
|
|
91
|
+
else if (type === "currency" || type === "percentage") {
|
|
92
|
+
const clean = String(val).replace(/[^0-9.-]/g, "");
|
|
93
|
+
numVal = Number(clean);
|
|
94
|
+
}
|
|
95
|
+
if (!isNaN(numVal)) {
|
|
96
|
+
if (min === void 0 || numVal < min) min = numVal;
|
|
97
|
+
if (max === void 0 || numVal > max) max = numVal;
|
|
98
|
+
}
|
|
99
|
+
} else if (type === "date") {
|
|
100
|
+
const d = new Date(val).getTime();
|
|
101
|
+
if (!isNaN(d)) {
|
|
102
|
+
if (min === void 0 || d < min) min = d;
|
|
103
|
+
if (max === void 0 || d > max) max = d;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
stats.uniqueValues = uniqueVals.size;
|
|
109
|
+
if (min !== void 0) stats.min = min;
|
|
110
|
+
if (max !== void 0) stats.max = max;
|
|
111
|
+
return stats;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/schema/buildSchema.ts
|
|
115
|
+
var buildSchema = (dataset, registry) => {
|
|
116
|
+
if (!dataset || dataset.length === 0) return {};
|
|
117
|
+
const sample = sampleDataset(dataset, 200);
|
|
118
|
+
const sampleSize = sample.length;
|
|
119
|
+
const keys = /* @__PURE__ */ new Set();
|
|
120
|
+
for (const row of sample) {
|
|
121
|
+
for (const key of Object.keys(row)) {
|
|
122
|
+
keys.add(key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const schema = {};
|
|
126
|
+
for (const key of keys) {
|
|
127
|
+
const values = sample.map((row) => row[key]);
|
|
128
|
+
let detectedTypeStr = null;
|
|
129
|
+
if (registry) {
|
|
130
|
+
detectedTypeStr = registry.runDetectors(key, values);
|
|
131
|
+
}
|
|
132
|
+
if (detectedTypeStr) {
|
|
133
|
+
const isNull = values.some((v) => v === null || v === void 0);
|
|
134
|
+
schema[key] = {
|
|
135
|
+
key,
|
|
136
|
+
type: detectedTypeStr,
|
|
137
|
+
nullable: isNull
|
|
138
|
+
};
|
|
139
|
+
const stats2 = computeColumnStats(key, sample, detectedTypeStr);
|
|
140
|
+
schema[key].uniqueValues = stats2.uniqueValues;
|
|
141
|
+
if (stats2.min !== void 0) schema[key].min = stats2.min;
|
|
142
|
+
if (stats2.max !== void 0) schema[key].max = stats2.max;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
let nullCount = 0;
|
|
146
|
+
const typeCounts = {};
|
|
147
|
+
for (const row of sample) {
|
|
148
|
+
const val = row[key];
|
|
149
|
+
if (val === null || val === void 0 || val === "") {
|
|
150
|
+
nullCount++;
|
|
151
|
+
} else {
|
|
152
|
+
const detectedType = detectValueType(val);
|
|
153
|
+
typeCounts[detectedType] = (typeCounts[detectedType] || 0) + 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const validValuesCount = sampleSize - nullCount;
|
|
157
|
+
let dominantType = "string";
|
|
158
|
+
if (validValuesCount > 0) {
|
|
159
|
+
let maxCount = 0;
|
|
160
|
+
let topType = "string";
|
|
161
|
+
for (const [type, count] of Object.entries(typeCounts)) {
|
|
162
|
+
if (count > maxCount) {
|
|
163
|
+
maxCount = count;
|
|
164
|
+
topType = type;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const confidence = maxCount / validValuesCount;
|
|
168
|
+
dominantType = confidence > 0.7 ? topType : "string";
|
|
169
|
+
}
|
|
170
|
+
const stats = computeColumnStats(key, sample, dominantType);
|
|
171
|
+
const columnSchema = {
|
|
172
|
+
key,
|
|
173
|
+
type: dominantType,
|
|
174
|
+
nullable: nullCount > 0,
|
|
175
|
+
uniqueValues: stats.uniqueValues
|
|
176
|
+
};
|
|
177
|
+
if (stats.min !== void 0) columnSchema.min = stats.min;
|
|
178
|
+
if (stats.max !== void 0) columnSchema.max = stats.max;
|
|
179
|
+
schema[key] = columnSchema;
|
|
180
|
+
}
|
|
181
|
+
return schema;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// src/hooks/useSorting.ts
|
|
185
|
+
import { useState, useMemo } from "react";
|
|
186
|
+
var useSorting = (data, schema, initialSortColumn = null, initialSortDirection = "asc") => {
|
|
187
|
+
const [sortColumn, setSortColumn] = useState(initialSortColumn);
|
|
188
|
+
const [sortDirection, setSortDirection] = useState(initialSortDirection);
|
|
189
|
+
const handleSort = (columnKey) => {
|
|
190
|
+
if (sortColumn === columnKey) {
|
|
191
|
+
if (sortDirection === "asc") {
|
|
192
|
+
setSortDirection("desc");
|
|
193
|
+
} else {
|
|
194
|
+
setSortColumn(null);
|
|
195
|
+
setSortDirection("asc");
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
setSortColumn(columnKey);
|
|
199
|
+
setSortDirection("asc");
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const sortedData = useMemo(() => {
|
|
203
|
+
if (!sortColumn || !data || data.length === 0) return data;
|
|
204
|
+
const columnType = schema[sortColumn]?.type || "string";
|
|
205
|
+
return [...data].sort((a, b) => {
|
|
206
|
+
let valA = a[sortColumn];
|
|
207
|
+
let valB = b[sortColumn];
|
|
208
|
+
if (valA === valB) return 0;
|
|
209
|
+
if (valA === null || valA === void 0) return sortDirection === "asc" ? 1 : -1;
|
|
210
|
+
if (valB === null || valB === void 0) return sortDirection === "asc" ? -1 : 1;
|
|
211
|
+
let comparison = 0;
|
|
212
|
+
switch (columnType) {
|
|
213
|
+
case "number":
|
|
214
|
+
case "currency":
|
|
215
|
+
case "percentage":
|
|
216
|
+
const numA = typeof valA === "number" ? valA : Number(String(valA).replace(/[^0-9.-]/g, ""));
|
|
217
|
+
const numB = typeof valB === "number" ? valB : Number(String(valB).replace(/[^0-9.-]/g, ""));
|
|
218
|
+
comparison = (isNaN(numA) ? 0 : numA) - (isNaN(numB) ? 0 : numB);
|
|
219
|
+
break;
|
|
220
|
+
case "date":
|
|
221
|
+
const dateA = new Date(valA).getTime();
|
|
222
|
+
const dateB = new Date(valB).getTime();
|
|
223
|
+
comparison = (isNaN(dateA) ? 0 : dateA) - (isNaN(dateB) ? 0 : dateB);
|
|
224
|
+
break;
|
|
225
|
+
case "boolean":
|
|
226
|
+
const boolA = valA === true || String(valA).toLowerCase() === "true" || String(valA).toLowerCase() === "yes" ? 1 : 0;
|
|
227
|
+
const boolB = valB === true || String(valB).toLowerCase() === "true" || String(valB).toLowerCase() === "yes" ? 1 : 0;
|
|
228
|
+
comparison = boolA - boolB;
|
|
229
|
+
break;
|
|
230
|
+
default:
|
|
231
|
+
comparison = String(valA).localeCompare(String(valB));
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
return sortDirection === "asc" ? comparison : -comparison;
|
|
235
|
+
});
|
|
236
|
+
}, [data, schema, sortColumn, sortDirection]);
|
|
237
|
+
return { sortedData, sortColumn, sortDirection, handleSort };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/hooks/useFilters.ts
|
|
241
|
+
import { useState as useState2, useMemo as useMemo2 } from "react";
|
|
242
|
+
var useFilters = (data, schema) => {
|
|
243
|
+
const [filters, setFilters] = useState2({});
|
|
244
|
+
const setFilter = (columnKey, value) => {
|
|
245
|
+
setFilters((prev) => {
|
|
246
|
+
const next = { ...prev };
|
|
247
|
+
if (value === null || value === void 0 || value === "") {
|
|
248
|
+
delete next[columnKey];
|
|
249
|
+
} else {
|
|
250
|
+
next[columnKey] = value;
|
|
251
|
+
}
|
|
252
|
+
return next;
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
const clearFilters = () => setFilters({});
|
|
256
|
+
const filteredData = useMemo2(() => {
|
|
257
|
+
if (!filters || Object.keys(filters).length === 0 || !data || data.length === 0) {
|
|
258
|
+
return data;
|
|
259
|
+
}
|
|
260
|
+
return data.filter((row) => {
|
|
261
|
+
for (const [key, filterValue] of Object.entries(filters)) {
|
|
262
|
+
const rowValue = row[key];
|
|
263
|
+
const type = schema[key]?.type || "string";
|
|
264
|
+
if (rowValue === null || rowValue === void 0) {
|
|
265
|
+
if (filterValue !== null && filterValue !== "") return false;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
switch (type) {
|
|
269
|
+
case "string":
|
|
270
|
+
case "email":
|
|
271
|
+
case "url":
|
|
272
|
+
if (typeof filterValue === "string") {
|
|
273
|
+
if (!String(rowValue).toLowerCase().includes(filterValue.toLowerCase())) return false;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
case "number":
|
|
277
|
+
case "currency":
|
|
278
|
+
case "percentage":
|
|
279
|
+
if (typeof filterValue === "object" && filterValue !== null) {
|
|
280
|
+
const numVal = typeof rowValue === "number" ? rowValue : Number(String(rowValue).replace(/[^0-9.-]/g, ""));
|
|
281
|
+
if (isNaN(numVal)) return false;
|
|
282
|
+
const { min, max } = filterValue;
|
|
283
|
+
if (min !== void 0 && numVal < min) return false;
|
|
284
|
+
if (max !== void 0 && numVal > max) return false;
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case "date":
|
|
288
|
+
if (typeof filterValue === "object" && filterValue !== null) {
|
|
289
|
+
const dateVal = new Date(rowValue).getTime();
|
|
290
|
+
if (isNaN(dateVal)) return false;
|
|
291
|
+
const { start, end } = filterValue;
|
|
292
|
+
if (start && dateVal < new Date(start).getTime()) return false;
|
|
293
|
+
if (end && dateVal > new Date(end).getTime()) return false;
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
case "boolean":
|
|
297
|
+
if (typeof filterValue === "boolean") {
|
|
298
|
+
const boolVal = rowValue === true || String(rowValue).toLowerCase() === "true" || String(rowValue).toLowerCase() === "yes";
|
|
299
|
+
if (boolVal !== filterValue) return false;
|
|
300
|
+
}
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
});
|
|
306
|
+
}, [data, schema, filters]);
|
|
307
|
+
return { filteredData, filters, setFilter, clearFilters };
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/hooks/usePagination.ts
|
|
311
|
+
import { useState as useState3, useMemo as useMemo3 } from "react";
|
|
312
|
+
var usePagination = (data, initialPageSize = 10) => {
|
|
313
|
+
const [currentPage, setCurrentPage] = useState3(1);
|
|
314
|
+
const [pageSize, setPageSize] = useState3(initialPageSize);
|
|
315
|
+
const totalPages = Math.max(1, Math.ceil(data.length / pageSize));
|
|
316
|
+
const safeCurrentPage = Math.min(currentPage, totalPages);
|
|
317
|
+
const paginatedData = useMemo3(() => {
|
|
318
|
+
const startIndex = (safeCurrentPage - 1) * pageSize;
|
|
319
|
+
return data.slice(startIndex, startIndex + pageSize);
|
|
320
|
+
}, [data, safeCurrentPage, pageSize]);
|
|
321
|
+
return {
|
|
322
|
+
paginatedData,
|
|
323
|
+
currentPage: safeCurrentPage,
|
|
324
|
+
pageSize,
|
|
325
|
+
totalPages,
|
|
326
|
+
setPage: setCurrentPage,
|
|
327
|
+
setPageSize
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/insights/aggregationEngine.ts
|
|
332
|
+
var aggregateData = (dataset, dimension, metrics) => {
|
|
333
|
+
if (!dataset || dataset.length === 0) return [];
|
|
334
|
+
if (!dimension) return [];
|
|
335
|
+
const groups = /* @__PURE__ */ new Map();
|
|
336
|
+
for (const row of dataset) {
|
|
337
|
+
let dimValue = row[dimension];
|
|
338
|
+
if (dimValue === null || dimValue === void 0) {
|
|
339
|
+
dimValue = "Unknown";
|
|
340
|
+
} else if (typeof dimValue === "boolean") {
|
|
341
|
+
dimValue = dimValue ? "Yes" : "No";
|
|
342
|
+
}
|
|
343
|
+
if (!groups.has(dimValue)) {
|
|
344
|
+
const initPoint = { [dimension]: dimValue };
|
|
345
|
+
for (const m of metrics) {
|
|
346
|
+
initPoint[m] = 0;
|
|
347
|
+
}
|
|
348
|
+
groups.set(dimValue, initPoint);
|
|
349
|
+
}
|
|
350
|
+
const group = groups.get(dimValue);
|
|
351
|
+
for (const m of metrics) {
|
|
352
|
+
const val = row[m];
|
|
353
|
+
let numVal = 0;
|
|
354
|
+
if (typeof val === "number") {
|
|
355
|
+
numVal = val;
|
|
356
|
+
} else if (typeof val === "string") {
|
|
357
|
+
numVal = Number(val.replace(/[^0-9.-]/g, ""));
|
|
358
|
+
}
|
|
359
|
+
if (!isNaN(numVal)) {
|
|
360
|
+
group[m] += numVal;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return Array.from(groups.values());
|
|
365
|
+
};
|
|
366
|
+
var aggregateDataByCount = (dataset, dimension) => {
|
|
367
|
+
if (!dataset || dataset.length === 0) return [];
|
|
368
|
+
const groups = /* @__PURE__ */ new Map();
|
|
369
|
+
for (const row of dataset) {
|
|
370
|
+
let dimValue = row[dimension];
|
|
371
|
+
if (dimValue === null || dimValue === void 0) {
|
|
372
|
+
dimValue = "Unknown";
|
|
373
|
+
} else if (typeof dimValue === "boolean") {
|
|
374
|
+
dimValue = dimValue ? "Yes" : "No";
|
|
375
|
+
}
|
|
376
|
+
groups.set(dimValue, (groups.get(dimValue) || 0) + 1);
|
|
377
|
+
}
|
|
378
|
+
return Array.from(groups.entries()).map(([dim, count]) => ({
|
|
379
|
+
[dimension]: dim,
|
|
380
|
+
count
|
|
381
|
+
}));
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/insights/chartDetector.ts
|
|
385
|
+
var detectChartFields = (schema) => {
|
|
386
|
+
const dimensions = [];
|
|
387
|
+
const metrics = [];
|
|
388
|
+
for (const [key, col] of Object.entries(schema)) {
|
|
389
|
+
const lowerKey = key.toLowerCase();
|
|
390
|
+
const isIdField = lowerKey === "id" || lowerKey === "_id" || lowerKey === "uuid" || lowerKey === "guid" || lowerKey.endsWith("id") || lowerKey.endsWith("_id") || lowerKey.endsWith("uuid");
|
|
391
|
+
if (isIdField) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const role = determineFieldRole(col.type, col.uniqueValues);
|
|
395
|
+
if (role === "dimension") {
|
|
396
|
+
dimensions.push(key);
|
|
397
|
+
} else if (role === "metric") {
|
|
398
|
+
metrics.push(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { dimensions, metrics };
|
|
402
|
+
};
|
|
403
|
+
var determineFieldRole = (type, uniqueValues) => {
|
|
404
|
+
switch (type) {
|
|
405
|
+
case "boolean":
|
|
406
|
+
return "dimension";
|
|
407
|
+
case "string":
|
|
408
|
+
if (uniqueValues !== void 0 && uniqueValues > 1 && uniqueValues <= 20) {
|
|
409
|
+
return "dimension";
|
|
410
|
+
}
|
|
411
|
+
return "discard";
|
|
412
|
+
case "date":
|
|
413
|
+
return "dimension";
|
|
414
|
+
case "number":
|
|
415
|
+
case "currency":
|
|
416
|
+
case "percentage":
|
|
417
|
+
return "metric";
|
|
418
|
+
default:
|
|
419
|
+
return "discard";
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/utils/stringUtils.ts
|
|
424
|
+
var ACRONYMS = ["ID", "URL", "API", "JSON", "UUID", "SKU", "IP", "VAT", "GST"];
|
|
425
|
+
var toTitleCase = (str) => {
|
|
426
|
+
if (!str) return "";
|
|
427
|
+
const spaced = str.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").trim();
|
|
428
|
+
return spaced.split(" ").filter(Boolean).map((word) => {
|
|
429
|
+
const upper = word.toUpperCase();
|
|
430
|
+
if (ACRONYMS.includes(upper)) {
|
|
431
|
+
return upper;
|
|
432
|
+
}
|
|
433
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
434
|
+
}).join(" ");
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/insights/insightBuilder.ts
|
|
438
|
+
var generateInsights = (dataset, schema) => {
|
|
439
|
+
if (!dataset || dataset.length === 0 || !schema) return [];
|
|
440
|
+
const { dimensions, metrics } = detectChartFields(schema);
|
|
441
|
+
const widgets = [];
|
|
442
|
+
const maxCharts = 4;
|
|
443
|
+
if (dimensions.length > 0 && metrics.length > 0) {
|
|
444
|
+
for (const dim of dimensions) {
|
|
445
|
+
if (widgets.length >= maxCharts) break;
|
|
446
|
+
const dimInfo = schema[dim];
|
|
447
|
+
const targetMetrics = metrics.slice(0, 2);
|
|
448
|
+
const aggregated = aggregateData(dataset, dim, targetMetrics);
|
|
449
|
+
if (dimInfo.type === "date") {
|
|
450
|
+
aggregated.sort((a, b) => new Date(a[dim]).getTime() - new Date(b[dim]).getTime());
|
|
451
|
+
} else {
|
|
452
|
+
const primaryMetric = targetMetrics[0];
|
|
453
|
+
aggregated.sort((a, b) => (b[primaryMetric] || 0) - (a[primaryMetric] || 0));
|
|
454
|
+
}
|
|
455
|
+
const isLine = dimInfo.type === "date";
|
|
456
|
+
const isPie = !isLine && aggregated.length > 0 && aggregated.length <= 5 && targetMetrics.length === 1;
|
|
457
|
+
widgets.push({
|
|
458
|
+
id: `chart-${dim}-metrics`,
|
|
459
|
+
title: `${targetMetrics.map(toTitleCase).join(" & ")} by ${toTitleCase(dim)}`,
|
|
460
|
+
type: isPie ? "pie" : isLine ? "line" : "bar",
|
|
461
|
+
data: aggregated,
|
|
462
|
+
xAxisKey: dim,
|
|
463
|
+
yAxisKeys: targetMetrics
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (dimensions.length > 0 && widgets.length < maxCharts) {
|
|
468
|
+
for (const dim of dimensions) {
|
|
469
|
+
if (widgets.length >= maxCharts) break;
|
|
470
|
+
if (widgets.some((w) => w.id.includes(dim))) continue;
|
|
471
|
+
const dimInfo = schema[dim];
|
|
472
|
+
const aggregated = aggregateDataByCount(dataset, dim);
|
|
473
|
+
if (dimInfo.type === "date") {
|
|
474
|
+
aggregated.sort((a, b) => new Date(a[dim]).getTime() - new Date(b[dim]).getTime());
|
|
475
|
+
} else {
|
|
476
|
+
aggregated.sort((a, b) => b.count - a.count);
|
|
477
|
+
}
|
|
478
|
+
const isLine = dimInfo.type === "date";
|
|
479
|
+
widgets.push({
|
|
480
|
+
id: `chart-${dim}-count`,
|
|
481
|
+
title: `Distribution of ${toTitleCase(dim)}`,
|
|
482
|
+
type: isLine ? "line" : aggregated.length <= 6 ? "pie" : "bar",
|
|
483
|
+
data: aggregated,
|
|
484
|
+
xAxisKey: dim,
|
|
485
|
+
yAxisKeys: ["count"]
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return widgets;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/components/SmartTable.tsx
|
|
493
|
+
import React3, { useMemo as useMemo5 } from "react";
|
|
494
|
+
|
|
495
|
+
// src/components/TableHeader.tsx
|
|
496
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
497
|
+
var getColumnWidthClass = (key, type) => {
|
|
498
|
+
const lowerKey = key.toLowerCase();
|
|
499
|
+
if (lowerKey === "id" || lowerKey === "_id" || type === "boolean") return "rst-col-xs";
|
|
500
|
+
if (type === "date" || type === "number" || lowerKey.includes("status")) return "rst-col-sm";
|
|
501
|
+
if (lowerKey.includes("role") || lowerKey.includes("country") || lowerKey.includes("category")) return "rst-col-md";
|
|
502
|
+
if (lowerKey.includes("name") || lowerKey.includes("email") || lowerKey.includes("url") || lowerKey.includes("description")) return "rst-col-flex";
|
|
503
|
+
return "rst-col-md";
|
|
504
|
+
};
|
|
505
|
+
var TableHeader = ({
|
|
506
|
+
schema,
|
|
507
|
+
sortable = false,
|
|
508
|
+
sortColumn = null,
|
|
509
|
+
sortDirection = "asc",
|
|
510
|
+
onSort
|
|
511
|
+
}) => {
|
|
512
|
+
const columns = Object.keys(schema);
|
|
513
|
+
return /* @__PURE__ */ jsx("thead", { className: "rst-header", children: /* @__PURE__ */ jsx("tr", { className: "rst-header-row", children: columns.map((colKey) => {
|
|
514
|
+
const colInfo = schema[colKey];
|
|
515
|
+
const widthClass = getColumnWidthClass(colKey, colInfo.type);
|
|
516
|
+
return /* @__PURE__ */ jsx(
|
|
517
|
+
"th",
|
|
518
|
+
{
|
|
519
|
+
className: `rst-th rst-th-col-${colKey} rst-th-type-${colInfo.type} ${widthClass} ${sortable && onSort ? "rst-sortable" : ""} ${sortColumn === colKey ? "rst-sort-active" : ""}`,
|
|
520
|
+
onClick: () => {
|
|
521
|
+
if (sortable && onSort) onSort(colKey);
|
|
522
|
+
},
|
|
523
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rst-header-content", children: [
|
|
524
|
+
/* @__PURE__ */ jsx("span", { children: toTitleCase(colKey) }),
|
|
525
|
+
sortable && onSort && /* @__PURE__ */ jsx("span", { className: "rst-sort-icon", children: sortColumn === colKey ? sortDirection === "asc" ? "\u2191" : "\u2193" : "\u21C5" })
|
|
526
|
+
] })
|
|
527
|
+
},
|
|
528
|
+
colKey
|
|
529
|
+
);
|
|
530
|
+
}) }) });
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/components/TableBody.tsx
|
|
534
|
+
import React, { useRef } from "react";
|
|
535
|
+
|
|
536
|
+
// src/renderers/numberRenderer.tsx
|
|
537
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
538
|
+
var NumberRenderer = ({ value }) => {
|
|
539
|
+
if (value === null || value === void 0) return null;
|
|
540
|
+
const num = Number(value);
|
|
541
|
+
if (!isNaN(num)) {
|
|
542
|
+
return /* @__PURE__ */ jsx2("span", { className: "rst-cell-number", children: new Intl.NumberFormat().format(num) });
|
|
543
|
+
}
|
|
544
|
+
return /* @__PURE__ */ jsx2("span", { className: "rst-cell-number", children: String(value) });
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// src/renderers/emailRenderer.tsx
|
|
548
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
549
|
+
var EmailRenderer = ({ value }) => {
|
|
550
|
+
if (!value) return null;
|
|
551
|
+
const email = String(value);
|
|
552
|
+
return /* @__PURE__ */ jsx3("a", { href: `mailto:${email}`, className: "rst-cell-email", onClick: (e) => e.stopPropagation(), children: email });
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/renderers/urlRenderer.tsx
|
|
556
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
557
|
+
var UrlRenderer = ({ value }) => {
|
|
558
|
+
if (!value) return null;
|
|
559
|
+
const url = String(value);
|
|
560
|
+
const href = /^https?:\/\//i.test(url) ? url : `https://${url}`;
|
|
561
|
+
return /* @__PURE__ */ jsx4(
|
|
562
|
+
"a",
|
|
563
|
+
{
|
|
564
|
+
href,
|
|
565
|
+
target: "_blank",
|
|
566
|
+
rel: "noopener noreferrer",
|
|
567
|
+
className: "rst-cell-url",
|
|
568
|
+
onClick: (e) => e.stopPropagation(),
|
|
569
|
+
children: url
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// src/renderers/imageRenderer.tsx
|
|
575
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
576
|
+
var ImageRenderer = ({ value }) => {
|
|
577
|
+
if (!value) return null;
|
|
578
|
+
const src = String(value);
|
|
579
|
+
return /* @__PURE__ */ jsx5(
|
|
580
|
+
"img",
|
|
581
|
+
{
|
|
582
|
+
src,
|
|
583
|
+
alt: "cell content",
|
|
584
|
+
className: "rst-cell-image",
|
|
585
|
+
style: { maxWidth: "50px", maxHeight: "50px", objectFit: "contain", borderRadius: "4px" },
|
|
586
|
+
loading: "lazy"
|
|
587
|
+
}
|
|
588
|
+
);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/renderers/percentageRenderer.tsx
|
|
592
|
+
import { jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
593
|
+
var PercentageRenderer = ({ value }) => {
|
|
594
|
+
if (value === null || value === void 0) return null;
|
|
595
|
+
const str = String(value);
|
|
596
|
+
if (str.includes("%")) {
|
|
597
|
+
return /* @__PURE__ */ jsx6("span", { className: "rst-cell-percentage", children: str });
|
|
598
|
+
}
|
|
599
|
+
const num = Number(value);
|
|
600
|
+
if (!isNaN(num)) {
|
|
601
|
+
return /* @__PURE__ */ jsxs2("span", { className: "rst-cell-percentage", children: [
|
|
602
|
+
(num * 100).toFixed(2),
|
|
603
|
+
"%"
|
|
604
|
+
] });
|
|
605
|
+
}
|
|
606
|
+
return /* @__PURE__ */ jsx6("span", { className: "rst-cell-percentage", children: str });
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// src/renderers/dateRenderer.tsx
|
|
610
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
611
|
+
var DateRenderer = ({ value }) => {
|
|
612
|
+
if (!value) return null;
|
|
613
|
+
const d = new Date(value);
|
|
614
|
+
if (isNaN(d.getTime())) {
|
|
615
|
+
return /* @__PURE__ */ jsx7("span", { className: "rst-cell-date", children: String(value) });
|
|
616
|
+
}
|
|
617
|
+
return /* @__PURE__ */ jsx7("span", { className: "rst-cell-date", title: d.toISOString(), children: d.toLocaleDateString(void 0, {
|
|
618
|
+
year: "numeric",
|
|
619
|
+
month: "short",
|
|
620
|
+
day: "numeric"
|
|
621
|
+
}) });
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/renderers/booleanRenderer.tsx
|
|
625
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
626
|
+
var BooleanRenderer = ({ value }) => {
|
|
627
|
+
if (value === null || value === void 0) return null;
|
|
628
|
+
let isTrue = false;
|
|
629
|
+
if (typeof value === "boolean") {
|
|
630
|
+
isTrue = value;
|
|
631
|
+
} else if (typeof value === "string") {
|
|
632
|
+
const lower = value.toLowerCase();
|
|
633
|
+
isTrue = lower === "true" || lower === "yes";
|
|
634
|
+
} else if (typeof value === "number") {
|
|
635
|
+
isTrue = value > 0;
|
|
636
|
+
}
|
|
637
|
+
return /* @__PURE__ */ jsx8(
|
|
638
|
+
"span",
|
|
639
|
+
{
|
|
640
|
+
className: `rst-cell-boolean rst-cell-boolean-${isTrue ? "true" : "false"}`,
|
|
641
|
+
style: {
|
|
642
|
+
display: "inline-block",
|
|
643
|
+
padding: "2px 8px",
|
|
644
|
+
borderRadius: "12px",
|
|
645
|
+
fontSize: "0.85em",
|
|
646
|
+
fontWeight: "bold",
|
|
647
|
+
backgroundColor: isTrue ? "#e6f4ea" : "#fce8e6",
|
|
648
|
+
color: isTrue ? "#137333" : "#c5221f"
|
|
649
|
+
},
|
|
650
|
+
children: isTrue ? "Yes" : "No"
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// src/renderers/defaultRenderer.tsx
|
|
656
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
657
|
+
var DefaultRenderer = ({ value }) => {
|
|
658
|
+
if (value === null || value === void 0) return null;
|
|
659
|
+
if (typeof value === "object") {
|
|
660
|
+
return /* @__PURE__ */ jsx9("span", { className: "rst-cell-default", children: JSON.stringify(value) });
|
|
661
|
+
}
|
|
662
|
+
return /* @__PURE__ */ jsx9("span", { className: "rst-cell-default", children: String(value) });
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/plugins/currencyPlugin.tsx
|
|
666
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
667
|
+
var currencyPlugin = {
|
|
668
|
+
detect: (columnKey, sample) => {
|
|
669
|
+
const lowerKey = columnKey.toLowerCase();
|
|
670
|
+
if (lowerKey.includes("amount") || lowerKey.includes("price") || lowerKey.includes("revenue")) {
|
|
671
|
+
return "currency";
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
},
|
|
675
|
+
render: ({ value }) => {
|
|
676
|
+
if (value === null || value === void 0) return null;
|
|
677
|
+
let num = NaN;
|
|
678
|
+
if (typeof value === "number") num = value;
|
|
679
|
+
else if (typeof value === "string") num = Number(value.replace(/[^0-9.-]/g, ""));
|
|
680
|
+
if (isNaN(num)) return /* @__PURE__ */ jsx10("span", { children: String(value) });
|
|
681
|
+
const formatted = new Intl.NumberFormat("en-US", {
|
|
682
|
+
style: "currency",
|
|
683
|
+
currency: "USD"
|
|
684
|
+
}).format(num);
|
|
685
|
+
return /* @__PURE__ */ jsx10("span", { style: { color: num < 0 ? "#d32f2f" : "#388e3c", fontWeight: "bold" }, children: formatted });
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/plugins/percentagePlugin.tsx
|
|
690
|
+
import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
691
|
+
var percentagePlugin = {
|
|
692
|
+
detect: (columnKey, sample) => {
|
|
693
|
+
const lowerKey = columnKey.toLowerCase();
|
|
694
|
+
if (lowerKey.includes("percent") || lowerKey.includes("rate")) {
|
|
695
|
+
return "percentage";
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
},
|
|
699
|
+
render: ({ value }) => {
|
|
700
|
+
if (value === null || value === void 0) return null;
|
|
701
|
+
let str = String(value);
|
|
702
|
+
let num = Number(value);
|
|
703
|
+
if (!str.includes("%") && !isNaN(num)) {
|
|
704
|
+
return /* @__PURE__ */ jsxs3("span", { style: {
|
|
705
|
+
backgroundColor: "#e3f2fd",
|
|
706
|
+
padding: "2px 6px",
|
|
707
|
+
borderRadius: "4px",
|
|
708
|
+
color: "#1565c0",
|
|
709
|
+
fontSize: "0.9em",
|
|
710
|
+
fontWeight: 600
|
|
711
|
+
}, children: [
|
|
712
|
+
(num * 100).toFixed(1),
|
|
713
|
+
"%"
|
|
714
|
+
] });
|
|
715
|
+
}
|
|
716
|
+
return /* @__PURE__ */ jsx11("span", { style: {
|
|
717
|
+
backgroundColor: "#e3f2fd",
|
|
718
|
+
padding: "2px 6px",
|
|
719
|
+
borderRadius: "4px",
|
|
720
|
+
color: "#1565c0",
|
|
721
|
+
fontSize: "0.9em",
|
|
722
|
+
fontWeight: 600
|
|
723
|
+
}, children: str });
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// src/renderers/cellRendererFactory.ts
|
|
728
|
+
var getCellRenderer = (type, registry) => {
|
|
729
|
+
if (registry) {
|
|
730
|
+
for (const plugin of registry.getPlugins()) {
|
|
731
|
+
if (type === "currency" && plugin.render && (plugin === currencyPlugin || plugin.detect?.("amount", []) === "currency")) {
|
|
732
|
+
return plugin.render;
|
|
733
|
+
}
|
|
734
|
+
if (type === "percentage" && plugin.render && (plugin === percentagePlugin || plugin.detect?.("percentage", []) === "percentage")) {
|
|
735
|
+
return plugin.render;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
switch (type) {
|
|
740
|
+
case "number":
|
|
741
|
+
case "currency":
|
|
742
|
+
return NumberRenderer;
|
|
743
|
+
case "email":
|
|
744
|
+
return EmailRenderer;
|
|
745
|
+
case "url":
|
|
746
|
+
return UrlRenderer;
|
|
747
|
+
case "image":
|
|
748
|
+
return ImageRenderer;
|
|
749
|
+
case "percentage":
|
|
750
|
+
return PercentageRenderer;
|
|
751
|
+
case "date":
|
|
752
|
+
return DateRenderer;
|
|
753
|
+
case "boolean":
|
|
754
|
+
return BooleanRenderer;
|
|
755
|
+
case "string":
|
|
756
|
+
default:
|
|
757
|
+
return DefaultRenderer;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/components/TableBody.tsx
|
|
762
|
+
import { useVirtual } from "react-virtual";
|
|
763
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
764
|
+
var TableBody = ({ data, schema, registry }) => {
|
|
765
|
+
const columns = Object.keys(schema);
|
|
766
|
+
const parentRef = useRef(null);
|
|
767
|
+
const rowVirtualizer = useVirtual({
|
|
768
|
+
size: data.length,
|
|
769
|
+
parentRef,
|
|
770
|
+
estimateSize: React.useCallback(() => 45, []),
|
|
771
|
+
overscan: 5
|
|
772
|
+
});
|
|
773
|
+
const isVirtualized = data.length > 20;
|
|
774
|
+
if (!isVirtualized) {
|
|
775
|
+
return /* @__PURE__ */ jsx12("tbody", { className: "rst-body", children: data.map((row, index) => /* @__PURE__ */ jsx12("tr", { className: "rst-row", children: columns.map((colKey) => {
|
|
776
|
+
const colInfo = schema[colKey];
|
|
777
|
+
const Renderer = getCellRenderer(colInfo.type, registry);
|
|
778
|
+
const widthClass = getColumnWidthClass(colKey, colInfo.type);
|
|
779
|
+
const val = row[colKey];
|
|
780
|
+
return /* @__PURE__ */ jsx12(
|
|
781
|
+
"td",
|
|
782
|
+
{
|
|
783
|
+
className: `rst-td rst-td-col-${colKey} rst-td-type-${colInfo.type} ${widthClass}`,
|
|
784
|
+
children: colInfo.type === "boolean" ? /* @__PURE__ */ jsx12("span", { className: `rst-badge ${val ? "rst-badge-yes" : "rst-badge-no"}`, children: val ? "Yes" : "No" }) : /* @__PURE__ */ jsx12(Renderer, { value: val })
|
|
785
|
+
},
|
|
786
|
+
colKey
|
|
787
|
+
);
|
|
788
|
+
}) }, index)) });
|
|
789
|
+
}
|
|
790
|
+
return /* @__PURE__ */ jsx12(
|
|
791
|
+
"tbody",
|
|
792
|
+
{
|
|
793
|
+
className: "rst-body",
|
|
794
|
+
ref: parentRef,
|
|
795
|
+
style: {
|
|
796
|
+
height: `${rowVirtualizer.totalSize}px`,
|
|
797
|
+
width: "100%",
|
|
798
|
+
position: "relative"
|
|
799
|
+
},
|
|
800
|
+
children: rowVirtualizer.virtualItems.map((virtualRow) => {
|
|
801
|
+
const row = data[virtualRow.index];
|
|
802
|
+
return /* @__PURE__ */ jsx12(
|
|
803
|
+
"tr",
|
|
804
|
+
{
|
|
805
|
+
className: "rst-row",
|
|
806
|
+
style: {
|
|
807
|
+
position: "absolute",
|
|
808
|
+
top: 0,
|
|
809
|
+
left: 0,
|
|
810
|
+
width: "100%",
|
|
811
|
+
height: `${virtualRow.size}px`,
|
|
812
|
+
transform: `translateY(${virtualRow.start}px)`
|
|
813
|
+
},
|
|
814
|
+
children: columns.map((colKey) => {
|
|
815
|
+
const val = row[colKey];
|
|
816
|
+
const colInfo = schema[colKey];
|
|
817
|
+
const Renderer = getCellRenderer(colInfo.type, registry);
|
|
818
|
+
const widthClass = getColumnWidthClass(colKey, colInfo.type);
|
|
819
|
+
return /* @__PURE__ */ jsx12(
|
|
820
|
+
"td",
|
|
821
|
+
{
|
|
822
|
+
className: `rst-td rst-td-col-${colKey} rst-td-type-${colInfo.type} ${widthClass}`,
|
|
823
|
+
children: colInfo.type === "boolean" ? /* @__PURE__ */ jsx12("span", { className: `rst-badge ${val ? "rst-badge-yes" : "rst-badge-no"}`, children: val ? "Yes" : "No" }) : /* @__PURE__ */ jsx12(Renderer, { value: val })
|
|
824
|
+
},
|
|
825
|
+
colKey
|
|
826
|
+
);
|
|
827
|
+
})
|
|
828
|
+
},
|
|
829
|
+
virtualRow.index
|
|
830
|
+
);
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// src/components/FilterPanel.tsx
|
|
837
|
+
import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
838
|
+
var FilterPanel = ({ schema, filters, onFilterChange, onClear }) => {
|
|
839
|
+
const handleStringChange = (key, e) => {
|
|
840
|
+
onFilterChange(key, e.target.value || null);
|
|
841
|
+
};
|
|
842
|
+
const handleNumberChange = (key, field, value) => {
|
|
843
|
+
const current = filters[key] || {};
|
|
844
|
+
const num = parseFloat(value);
|
|
845
|
+
const next = { ...current };
|
|
846
|
+
if (isNaN(num)) {
|
|
847
|
+
delete next[field];
|
|
848
|
+
} else {
|
|
849
|
+
next[field] = num;
|
|
850
|
+
}
|
|
851
|
+
if (Object.keys(next).length === 0) {
|
|
852
|
+
onFilterChange(key, null);
|
|
853
|
+
} else {
|
|
854
|
+
onFilterChange(key, next);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
const handleDateChange = (key, field, value) => {
|
|
858
|
+
const current = filters[key] || {};
|
|
859
|
+
const next = { ...current };
|
|
860
|
+
if (!value) {
|
|
861
|
+
delete next[field];
|
|
862
|
+
} else {
|
|
863
|
+
next[field] = value;
|
|
864
|
+
}
|
|
865
|
+
if (Object.keys(next).length === 0) {
|
|
866
|
+
onFilterChange(key, null);
|
|
867
|
+
} else {
|
|
868
|
+
onFilterChange(key, next);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const handleBooleanToggle = (key) => {
|
|
872
|
+
const current = filters[key];
|
|
873
|
+
if (current === true) {
|
|
874
|
+
onFilterChange(key, false);
|
|
875
|
+
} else if (current === false) {
|
|
876
|
+
onFilterChange(key, null);
|
|
877
|
+
} else {
|
|
878
|
+
onFilterChange(key, true);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
return /* @__PURE__ */ jsxs4("div", { className: "rst-filter-panel", children: [
|
|
882
|
+
Object.entries(schema).map(([key, colInfo]) => {
|
|
883
|
+
const type = colInfo.type;
|
|
884
|
+
const currentFilter = filters[key];
|
|
885
|
+
return /* @__PURE__ */ jsxs4("div", { className: `rst-filter-group rst-filter-type-${type}`, children: [
|
|
886
|
+
/* @__PURE__ */ jsxs4("label", { children: [
|
|
887
|
+
toTitleCase(key),
|
|
888
|
+
" Filter"
|
|
889
|
+
] }),
|
|
890
|
+
(type === "string" || type === "email" || type === "url") && /* @__PURE__ */ jsx13(
|
|
891
|
+
"input",
|
|
892
|
+
{
|
|
893
|
+
type: "text",
|
|
894
|
+
placeholder: `Search ${toTitleCase(key)}...`,
|
|
895
|
+
value: currentFilter || "",
|
|
896
|
+
onChange: (e) => handleStringChange(key, e)
|
|
897
|
+
}
|
|
898
|
+
),
|
|
899
|
+
(type === "number" || type === "currency" || type === "percentage") && /* @__PURE__ */ jsxs4("div", { className: "rst-filter-range", children: [
|
|
900
|
+
/* @__PURE__ */ jsx13(
|
|
901
|
+
"input",
|
|
902
|
+
{
|
|
903
|
+
type: "number",
|
|
904
|
+
placeholder: "Min",
|
|
905
|
+
value: currentFilter?.min ?? "",
|
|
906
|
+
onChange: (e) => handleNumberChange(key, "min", e.target.value)
|
|
907
|
+
}
|
|
908
|
+
),
|
|
909
|
+
/* @__PURE__ */ jsx13(
|
|
910
|
+
"input",
|
|
911
|
+
{
|
|
912
|
+
type: "number",
|
|
913
|
+
placeholder: "Max",
|
|
914
|
+
value: currentFilter?.max ?? "",
|
|
915
|
+
onChange: (e) => handleNumberChange(key, "max", e.target.value)
|
|
916
|
+
}
|
|
917
|
+
)
|
|
918
|
+
] }),
|
|
919
|
+
type === "date" && /* @__PURE__ */ jsxs4("div", { className: "rst-filter-range", children: [
|
|
920
|
+
/* @__PURE__ */ jsx13(
|
|
921
|
+
"input",
|
|
922
|
+
{
|
|
923
|
+
type: "date",
|
|
924
|
+
title: "Start Date",
|
|
925
|
+
value: currentFilter?.start || "",
|
|
926
|
+
onChange: (e) => handleDateChange(key, "start", e.target.value)
|
|
927
|
+
}
|
|
928
|
+
),
|
|
929
|
+
/* @__PURE__ */ jsx13(
|
|
930
|
+
"input",
|
|
931
|
+
{
|
|
932
|
+
type: "date",
|
|
933
|
+
title: "End Date",
|
|
934
|
+
value: currentFilter?.end || "",
|
|
935
|
+
onChange: (e) => handleDateChange(key, "end", e.target.value)
|
|
936
|
+
}
|
|
937
|
+
)
|
|
938
|
+
] }),
|
|
939
|
+
type === "boolean" && /* @__PURE__ */ jsx13(
|
|
940
|
+
"button",
|
|
941
|
+
{
|
|
942
|
+
className: "rst-filter-toggle",
|
|
943
|
+
onClick: () => handleBooleanToggle(key),
|
|
944
|
+
style: {
|
|
945
|
+
backgroundColor: currentFilter === true ? "#e6f4ea" : currentFilter === false ? "#fce8e6" : "#fff"
|
|
946
|
+
},
|
|
947
|
+
children: currentFilter === true ? "Only Yes" : currentFilter === false ? "Only No" : "All"
|
|
948
|
+
}
|
|
949
|
+
)
|
|
950
|
+
] }, key);
|
|
951
|
+
}),
|
|
952
|
+
Object.keys(filters).length > 0 && /* @__PURE__ */ jsx13(
|
|
953
|
+
"button",
|
|
954
|
+
{
|
|
955
|
+
className: "rst-filter-clear-btn",
|
|
956
|
+
onClick: onClear,
|
|
957
|
+
children: "Clear All Filters"
|
|
958
|
+
}
|
|
959
|
+
)
|
|
960
|
+
] });
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// src/components/InsightsPanel.tsx
|
|
964
|
+
import { useMemo as useMemo4 } from "react";
|
|
965
|
+
import {
|
|
966
|
+
BarChart,
|
|
967
|
+
Bar,
|
|
968
|
+
LineChart,
|
|
969
|
+
Line,
|
|
970
|
+
PieChart,
|
|
971
|
+
Pie,
|
|
972
|
+
Cell,
|
|
973
|
+
XAxis,
|
|
974
|
+
YAxis,
|
|
975
|
+
CartesianGrid,
|
|
976
|
+
Tooltip,
|
|
977
|
+
Legend,
|
|
978
|
+
ResponsiveContainer
|
|
979
|
+
} from "recharts";
|
|
980
|
+
import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
981
|
+
var COLORS = ["#8884d8", "#82ca9d", "#ffc658", "#ff8042", "#a4de6c", "#d0ed57"];
|
|
982
|
+
var InsightsPanel = ({ data, schema }) => {
|
|
983
|
+
const insights = useMemo4(() => {
|
|
984
|
+
return generateInsights(data, schema);
|
|
985
|
+
}, [data, schema]);
|
|
986
|
+
if (!insights || insights.length === 0) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
return /* @__PURE__ */ jsxs5("div", { className: "rst-insights-panel", children: [
|
|
990
|
+
/* @__PURE__ */ jsx14("h3", { children: "AI Generated Insights" }),
|
|
991
|
+
/* @__PURE__ */ jsx14("div", { className: "rst-insights-grid", children: insights.map((widget) => /* @__PURE__ */ jsxs5("div", { className: "rst-insight-card", children: [
|
|
992
|
+
/* @__PURE__ */ jsx14("h4", { className: "rst-insight-title", children: widget.title }),
|
|
993
|
+
/* @__PURE__ */ jsx14("div", { className: "rst-insight-chart-wrapper", style: { height: "240px" }, children: /* @__PURE__ */ jsx14(ResponsiveContainer, { width: "100%", height: "100%", children: widget.type === "bar" ? /* @__PURE__ */ jsxs5(BarChart, { data: widget.data, margin: { top: 5, right: 10, left: 0, bottom: 5 }, children: [
|
|
994
|
+
/* @__PURE__ */ jsx14(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }),
|
|
995
|
+
/* @__PURE__ */ jsx14(XAxis, { dataKey: widget.xAxisKey, tick: { fontSize: 11, fill: "#64748b" }, axisLine: { stroke: "#e2e8f0" }, tickLine: false }),
|
|
996
|
+
/* @__PURE__ */ jsx14(YAxis, { tick: { fontSize: 11, fill: "#64748b" }, axisLine: { stroke: "#e2e8f0" }, tickLine: false }),
|
|
997
|
+
/* @__PURE__ */ jsx14(
|
|
998
|
+
Tooltip,
|
|
999
|
+
{
|
|
1000
|
+
contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" },
|
|
1001
|
+
cursor: { fill: "#f1f5f9" }
|
|
1002
|
+
}
|
|
1003
|
+
),
|
|
1004
|
+
/* @__PURE__ */ jsx14(Legend, { wrapperStyle: { fontSize: "11px", paddingTop: "10px" }, iconType: "circle" }),
|
|
1005
|
+
widget.yAxisKeys.map((yKey, index) => /* @__PURE__ */ jsx14(Bar, { dataKey: yKey, fill: COLORS[index % COLORS.length], radius: [4, 4, 0, 0], barSize: 32 }, yKey))
|
|
1006
|
+
] }) : widget.type === "line" ? /* @__PURE__ */ jsxs5(LineChart, { data: widget.data, margin: { top: 5, right: 10, left: 0, bottom: 5 }, children: [
|
|
1007
|
+
/* @__PURE__ */ jsx14(CartesianGrid, { strokeDasharray: "3 3", vertical: false, stroke: "#e2e8f0" }),
|
|
1008
|
+
/* @__PURE__ */ jsx14(XAxis, { dataKey: widget.xAxisKey, tick: { fontSize: 11, fill: "#64748b" }, axisLine: { stroke: "#e2e8f0" }, tickLine: false }),
|
|
1009
|
+
/* @__PURE__ */ jsx14(YAxis, { tick: { fontSize: 11, fill: "#64748b" }, axisLine: { stroke: "#e2e8f0" }, tickLine: false }),
|
|
1010
|
+
/* @__PURE__ */ jsx14(Tooltip, { contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" } }),
|
|
1011
|
+
/* @__PURE__ */ jsx14(Legend, { wrapperStyle: { fontSize: "11px", paddingTop: "10px" }, iconType: "circle" }),
|
|
1012
|
+
widget.yAxisKeys.map((yKey, index) => /* @__PURE__ */ jsx14(Line, { type: "monotone", dataKey: yKey, stroke: COLORS[index % COLORS.length], strokeWidth: 2.5, dot: { r: 4, strokeWidth: 2, fill: "#fff" }, activeDot: { r: 6, strokeWidth: 0 } }, yKey))
|
|
1013
|
+
] }) : /* @__PURE__ */ jsxs5(PieChart, { children: [
|
|
1014
|
+
/* @__PURE__ */ jsx14(Tooltip, { contentStyle: { borderRadius: "8px", border: "none", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)" } }),
|
|
1015
|
+
/* @__PURE__ */ jsx14(Legend, { wrapperStyle: { fontSize: "11px" }, iconType: "circle" }),
|
|
1016
|
+
/* @__PURE__ */ jsx14(
|
|
1017
|
+
Pie,
|
|
1018
|
+
{
|
|
1019
|
+
data: widget.data,
|
|
1020
|
+
cx: "50%",
|
|
1021
|
+
cy: "45%",
|
|
1022
|
+
innerRadius: 60,
|
|
1023
|
+
outerRadius: 80,
|
|
1024
|
+
fill: "#8884d8",
|
|
1025
|
+
paddingAngle: 5,
|
|
1026
|
+
dataKey: widget.yAxisKeys[0],
|
|
1027
|
+
nameKey: widget.xAxisKey,
|
|
1028
|
+
label: false,
|
|
1029
|
+
children: widget.data.map((entry, index) => /* @__PURE__ */ jsx14(Cell, { fill: COLORS[index % COLORS.length] }, `cell-${index}`))
|
|
1030
|
+
}
|
|
1031
|
+
)
|
|
1032
|
+
] }) }) })
|
|
1033
|
+
] }, widget.id)) })
|
|
1034
|
+
] });
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/plugins/pluginRegistry.ts
|
|
1038
|
+
var PluginRegistry = class {
|
|
1039
|
+
constructor() {
|
|
1040
|
+
this.plugins = [];
|
|
1041
|
+
}
|
|
1042
|
+
register(pluginOrPlugins) {
|
|
1043
|
+
if (Array.isArray(pluginOrPlugins)) {
|
|
1044
|
+
this.plugins.push(...pluginOrPlugins);
|
|
1045
|
+
} else {
|
|
1046
|
+
this.plugins.push(pluginOrPlugins);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
clear() {
|
|
1050
|
+
this.plugins = [];
|
|
1051
|
+
}
|
|
1052
|
+
runDetectors(columnKey, sample) {
|
|
1053
|
+
for (const plugin of this.plugins) {
|
|
1054
|
+
if (plugin.detect) {
|
|
1055
|
+
const detectedType = plugin.detect(columnKey, sample);
|
|
1056
|
+
if (detectedType) {
|
|
1057
|
+
return detectedType;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
getRenderers() {
|
|
1064
|
+
const renderers = {};
|
|
1065
|
+
for (const plugin of this.plugins) {
|
|
1066
|
+
}
|
|
1067
|
+
return renderers;
|
|
1068
|
+
}
|
|
1069
|
+
getPlugins() {
|
|
1070
|
+
return this.plugins;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
var globalPluginRegistry = new PluginRegistry();
|
|
1074
|
+
|
|
1075
|
+
// src/components/SmartTable.tsx
|
|
1076
|
+
import { jsx as jsx15, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1077
|
+
var SmartTable = ({
|
|
1078
|
+
data,
|
|
1079
|
+
sortable = false,
|
|
1080
|
+
filterable = false,
|
|
1081
|
+
pagination = false,
|
|
1082
|
+
insights = false,
|
|
1083
|
+
plugins = []
|
|
1084
|
+
}) => {
|
|
1085
|
+
const registry = useMemo5(() => {
|
|
1086
|
+
const reg = new PluginRegistry();
|
|
1087
|
+
if (plugins && plugins.length > 0) {
|
|
1088
|
+
reg.register(plugins);
|
|
1089
|
+
}
|
|
1090
|
+
return reg;
|
|
1091
|
+
}, [plugins]);
|
|
1092
|
+
const schema = useMemo5(() => {
|
|
1093
|
+
return buildSchema(data, registry);
|
|
1094
|
+
}, [data, registry]);
|
|
1095
|
+
const { filteredData, filters, setFilter, clearFilters } = useFilters(data, schema);
|
|
1096
|
+
const dataToFilter = filterable ? filteredData : data;
|
|
1097
|
+
const { sortedData, sortColumn, sortDirection, handleSort } = useSorting(dataToFilter, schema);
|
|
1098
|
+
const dataToRenderOrPaginate = sortable ? sortedData : dataToFilter;
|
|
1099
|
+
const { paginatedData, currentPage, totalPages, setPage } = usePagination(dataToRenderOrPaginate, 10);
|
|
1100
|
+
const finalData = pagination ? paginatedData : dataToRenderOrPaginate;
|
|
1101
|
+
const [showFilters, setShowFilters] = React3.useState(true);
|
|
1102
|
+
if (!data || data.length === 0) {
|
|
1103
|
+
return /* @__PURE__ */ jsx15("div", { className: "rst-container", children: /* @__PURE__ */ jsx15("div", { className: "rst-empty", children: "No data to display." }) });
|
|
1104
|
+
}
|
|
1105
|
+
const summaryStats = /* @__PURE__ */ jsxs6("div", { className: "rst-summary-strip", children: [
|
|
1106
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1107
|
+
data.length,
|
|
1108
|
+
" Records"
|
|
1109
|
+
] }),
|
|
1110
|
+
/* @__PURE__ */ jsx15("span", { className: "rst-dot", children: "\u2022" }),
|
|
1111
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1112
|
+
Object.keys(schema).length,
|
|
1113
|
+
" Columns"
|
|
1114
|
+
] }),
|
|
1115
|
+
/* @__PURE__ */ jsx15("span", { className: "rst-dot", children: "\u2022" }),
|
|
1116
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1117
|
+
"Types: ",
|
|
1118
|
+
Array.from(new Set(Object.values(schema).map((c) => c.type))).join(", ")
|
|
1119
|
+
] })
|
|
1120
|
+
] });
|
|
1121
|
+
return /* @__PURE__ */ jsxs6("div", { className: "rst-container", children: [
|
|
1122
|
+
/* @__PURE__ */ jsx15("header", { className: "rst-main-header", children: /* @__PURE__ */ jsxs6("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-end" }, children: [
|
|
1123
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1124
|
+
/* @__PURE__ */ jsx15("h2", { children: "Smart Table" }),
|
|
1125
|
+
/* @__PURE__ */ jsx15("p", { children: "Auto-generated insights, filters, and records" })
|
|
1126
|
+
] }),
|
|
1127
|
+
filterable && /* @__PURE__ */ jsxs6(
|
|
1128
|
+
"button",
|
|
1129
|
+
{
|
|
1130
|
+
className: "rst-toggle-filters-btn",
|
|
1131
|
+
onClick: () => setShowFilters(!showFilters),
|
|
1132
|
+
children: [
|
|
1133
|
+
"Filters ",
|
|
1134
|
+
showFilters ? "\u25B2" : "\u25BC"
|
|
1135
|
+
]
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
] }) }),
|
|
1139
|
+
insights && /* @__PURE__ */ jsx15(InsightsPanel, { data: dataToRenderOrPaginate, schema }),
|
|
1140
|
+
summaryStats,
|
|
1141
|
+
filterable && showFilters && /* @__PURE__ */ jsx15("section", { className: "rst-card", children: /* @__PURE__ */ jsx15(
|
|
1142
|
+
FilterPanel,
|
|
1143
|
+
{
|
|
1144
|
+
schema,
|
|
1145
|
+
filters,
|
|
1146
|
+
onFilterChange: setFilter,
|
|
1147
|
+
onClear: clearFilters
|
|
1148
|
+
}
|
|
1149
|
+
) }),
|
|
1150
|
+
data.length > 0 && finalData.length === 0 && /* @__PURE__ */ jsxs6("div", { className: "rst-empty-results-card", children: [
|
|
1151
|
+
/* @__PURE__ */ jsx15("div", { className: "rst-empty-icon", children: "\u{1F50D}" }),
|
|
1152
|
+
/* @__PURE__ */ jsx15("h3", { children: "No results found" }),
|
|
1153
|
+
/* @__PURE__ */ jsx15("p", { children: "Try adjusting your filters to find what you're looking for." }),
|
|
1154
|
+
/* @__PURE__ */ jsx15("button", { className: "btn btn-primary", onClick: clearFilters, children: "Clear All Filters" })
|
|
1155
|
+
] }),
|
|
1156
|
+
data.length > 0 && finalData.length > 0 && /* @__PURE__ */ jsxs6("section", { className: "rst-card", children: [
|
|
1157
|
+
/* @__PURE__ */ jsx15("div", { className: "rst-table-container", children: /* @__PURE__ */ jsxs6("table", { className: "rst-table", children: [
|
|
1158
|
+
/* @__PURE__ */ jsx15(
|
|
1159
|
+
TableHeader,
|
|
1160
|
+
{
|
|
1161
|
+
schema,
|
|
1162
|
+
sortable,
|
|
1163
|
+
sortColumn,
|
|
1164
|
+
sortDirection,
|
|
1165
|
+
onSort: handleSort
|
|
1166
|
+
}
|
|
1167
|
+
),
|
|
1168
|
+
/* @__PURE__ */ jsx15(TableBody, { data: finalData, schema, registry })
|
|
1169
|
+
] }) }),
|
|
1170
|
+
pagination && /* @__PURE__ */ jsxs6("div", { className: "rst-pagination", children: [
|
|
1171
|
+
/* @__PURE__ */ jsx15(
|
|
1172
|
+
"button",
|
|
1173
|
+
{
|
|
1174
|
+
disabled: currentPage === 1,
|
|
1175
|
+
onClick: () => setPage(currentPage - 1),
|
|
1176
|
+
children: "Previous"
|
|
1177
|
+
}
|
|
1178
|
+
),
|
|
1179
|
+
/* @__PURE__ */ jsxs6("span", { children: [
|
|
1180
|
+
"Page ",
|
|
1181
|
+
currentPage,
|
|
1182
|
+
" of ",
|
|
1183
|
+
totalPages
|
|
1184
|
+
] }),
|
|
1185
|
+
/* @__PURE__ */ jsx15(
|
|
1186
|
+
"button",
|
|
1187
|
+
{
|
|
1188
|
+
disabled: currentPage === totalPages,
|
|
1189
|
+
onClick: () => setPage(currentPage + 1),
|
|
1190
|
+
children: "Next"
|
|
1191
|
+
}
|
|
1192
|
+
)
|
|
1193
|
+
] })
|
|
1194
|
+
] })
|
|
1195
|
+
] });
|
|
1196
|
+
};
|
|
1197
|
+
export {
|
|
1198
|
+
FilterPanel,
|
|
1199
|
+
InsightsPanel,
|
|
1200
|
+
PluginRegistry,
|
|
1201
|
+
SmartTable,
|
|
1202
|
+
aggregateData,
|
|
1203
|
+
aggregateDataByCount,
|
|
1204
|
+
buildSchema,
|
|
1205
|
+
computeColumnStats,
|
|
1206
|
+
currencyPlugin,
|
|
1207
|
+
detectChartFields,
|
|
1208
|
+
detectValueType,
|
|
1209
|
+
generateInsights,
|
|
1210
|
+
getCellRenderer,
|
|
1211
|
+
globalPluginRegistry,
|
|
1212
|
+
percentagePlugin,
|
|
1213
|
+
sampleDataset,
|
|
1214
|
+
useFilters,
|
|
1215
|
+
usePagination,
|
|
1216
|
+
useSorting
|
|
1217
|
+
};
|
|
1218
|
+
//# sourceMappingURL=index.mjs.map
|