kmod-cli 1.5.0 → 1.7.1
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 +35 -33
- package/bin/gen-readme-name-key.js +6 -3
- package/component-templates/components/list-map.tsx +60 -0
- package/component-templates/components/table/data-table.tsx +117 -76
- package/component-templates/components/table/readme.tsx +173 -68
- package/component-templates/providers/rbac.tsx +255 -0
- package/components.json +11 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,64 +41,66 @@ npx kumod add button # add button component
|
|
|
41
41
|
## Components
|
|
42
42
|
|
|
43
43
|
- access-denied
|
|
44
|
+
- api-service
|
|
44
45
|
- breadcumb
|
|
45
|
-
- count-down
|
|
46
|
-
- count-input
|
|
47
46
|
- button
|
|
47
|
+
- calculate
|
|
48
48
|
- calendar
|
|
49
|
+
- color-by-text
|
|
50
|
+
- column-table
|
|
51
|
+
- config
|
|
52
|
+
- count-down
|
|
53
|
+
- count-input
|
|
54
|
+
- data-table
|
|
49
55
|
- date-input
|
|
50
56
|
- date-range-picker
|
|
51
|
-
- label
|
|
52
|
-
- popover
|
|
53
|
-
- select
|
|
54
|
-
- switch
|
|
55
57
|
- datetime-picker
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
- time-picker-utils
|
|
60
|
-
- time-picker
|
|
58
|
+
- fade-on-scroll
|
|
59
|
+
- feature-config
|
|
60
|
+
- feature-guard
|
|
61
61
|
- gradient-outline
|
|
62
62
|
- gradient-svg
|
|
63
63
|
- grid-layout
|
|
64
|
+
- hash-aes
|
|
64
65
|
- hydrate-guard
|
|
66
|
+
- idb
|
|
65
67
|
- image
|
|
68
|
+
- input
|
|
69
|
+
- keys
|
|
70
|
+
- kookies
|
|
71
|
+
- label
|
|
72
|
+
- lib
|
|
73
|
+
- list-map
|
|
66
74
|
- loader-slash-gradient
|
|
67
75
|
- masonry-gallery
|
|
68
76
|
- modal
|
|
69
77
|
- multi-select
|
|
70
78
|
- non-hydration
|
|
79
|
+
- period-input
|
|
80
|
+
- popover
|
|
71
81
|
- portal
|
|
82
|
+
- query
|
|
83
|
+
- rbac
|
|
84
|
+
- readme
|
|
85
|
+
- refine-provider
|
|
86
|
+
- safe-action
|
|
72
87
|
- segments-circle
|
|
88
|
+
- select
|
|
89
|
+
- simple-validate
|
|
73
90
|
- single-select
|
|
91
|
+
- spam-guard
|
|
92
|
+
- storage
|
|
93
|
+
- stripe-effect
|
|
74
94
|
- stroke-circle
|
|
75
|
-
-
|
|
76
|
-
- data-table
|
|
77
|
-
- readme
|
|
95
|
+
- switch
|
|
78
96
|
- table
|
|
79
97
|
- text-hover-effect
|
|
98
|
+
- time-picker
|
|
99
|
+
- time-picker-input
|
|
100
|
+
- time-picker-utils
|
|
80
101
|
- timout-loader
|
|
81
102
|
- toast
|
|
82
|
-
- config
|
|
83
|
-
- feature-config
|
|
84
|
-
- keys
|
|
85
|
-
- api-service
|
|
86
|
-
- calculate
|
|
87
|
-
- idb
|
|
88
|
-
- lib
|
|
89
|
-
- storage
|
|
90
|
-
- fade-on-scroll
|
|
91
|
-
- safe-action
|
|
92
|
-
- spam-guard
|
|
93
103
|
- utils
|
|
94
|
-
- feature-guard
|
|
95
|
-
- refine-provider
|
|
96
|
-
- query
|
|
97
|
-
- color-by-text
|
|
98
|
-
- stripe-effect
|
|
99
|
-
- kookies
|
|
100
|
-
- hash-aes
|
|
101
|
-
- simple-validate
|
|
102
104
|
## Contributing
|
|
103
105
|
|
|
104
106
|
Contributions are welcome! Please open issues or pull requests.
|
|
@@ -13,10 +13,13 @@ const componentsConfig = JSON.parse(
|
|
|
13
13
|
fs.readFileSync(componentsPath, 'utf8')
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
+
// sort a-z
|
|
17
|
+
const componentSorted = Object.keys(componentsConfig).sort(
|
|
18
|
+
(a, b) => a.localeCompare(b)
|
|
19
|
+
)
|
|
20
|
+
|
|
16
21
|
// build list
|
|
17
|
-
const componentList =
|
|
18
|
-
.map((k) => `- ${k}`)
|
|
19
|
-
.join('\n');
|
|
22
|
+
const componentList = componentSorted.map((k) => `- ${k}`).join('\n');
|
|
20
23
|
|
|
21
24
|
// đọc README
|
|
22
25
|
const readmeContent = fs.readFileSync(readmePath, 'utf8');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ListProps<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
keyExtractor: (item: T) => React.Key;
|
|
6
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function List<T>({ items, keyExtractor, renderItem }: ListProps<T>) {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
{items.map((item, index) => (
|
|
13
|
+
<ListItem
|
|
14
|
+
key={keyExtractor(item)}
|
|
15
|
+
item={item}
|
|
16
|
+
index={index}
|
|
17
|
+
renderItem={renderItem}
|
|
18
|
+
/>
|
|
19
|
+
))}
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ListItemProps<T> {
|
|
25
|
+
item: T;
|
|
26
|
+
index: number;
|
|
27
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generic memo component
|
|
31
|
+
function _ListItem<T>({ item, index, renderItem }: ListItemProps<T>) {
|
|
32
|
+
return <>{renderItem(item, index)}</>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ListItem = React.memo(_ListItem) as <T>(
|
|
36
|
+
props: ListItemProps<T>
|
|
37
|
+
) => React.ReactNode;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// Example usage of the List component
|
|
41
|
+
// interface User {
|
|
42
|
+
// id: number;
|
|
43
|
+
// name: string;
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// const users: User[] = [
|
|
47
|
+
// { id: 1, name: 'Alice' },
|
|
48
|
+
// { id: 2, name: 'Bob' },
|
|
49
|
+
// { id: 3, name: 'Charlie' },
|
|
50
|
+
// ];
|
|
51
|
+
|
|
52
|
+
// export function UserList() {
|
|
53
|
+
// return (
|
|
54
|
+
// <List
|
|
55
|
+
// items={users}
|
|
56
|
+
// keyExtractor={(user) => user.id}
|
|
57
|
+
// renderItem={(user) => <div>{user.name}</div>}
|
|
58
|
+
// />
|
|
59
|
+
// );
|
|
60
|
+
// }
|
|
@@ -55,7 +55,7 @@ export type TableClassNames = {
|
|
|
55
55
|
};
|
|
56
56
|
export type TableHeaderProps<TData> =
|
|
57
57
|
HTMLAttributes<HTMLTableSectionElement> & {
|
|
58
|
-
handleClick
|
|
58
|
+
handleClick?: ({
|
|
59
59
|
e,
|
|
60
60
|
table,
|
|
61
61
|
}: {
|
|
@@ -64,7 +64,7 @@ export type TableHeaderProps<TData> =
|
|
|
64
64
|
}) => void;
|
|
65
65
|
};
|
|
66
66
|
export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
|
|
67
|
-
handleClick
|
|
67
|
+
handleClick?: ({
|
|
68
68
|
e,
|
|
69
69
|
table,
|
|
70
70
|
}: {
|
|
@@ -73,7 +73,8 @@ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
|
|
|
73
73
|
}) => void;
|
|
74
74
|
};
|
|
75
75
|
export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
|
|
76
|
-
|
|
76
|
+
classNameCondition?: | (({cell, table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) => string) | string;
|
|
77
|
+
handleClick?: ({
|
|
77
78
|
e,
|
|
78
79
|
table,
|
|
79
80
|
cell,
|
|
@@ -85,18 +86,21 @@ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
|
|
|
85
86
|
};
|
|
86
87
|
export type TableCellProps<TData, TValue> =
|
|
87
88
|
HTMLAttributes<HTMLTableCellElement> & {
|
|
88
|
-
|
|
89
|
+
classNameCondition?: | (({cell, table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) => string) | string;
|
|
90
|
+
handleClick?: ({
|
|
89
91
|
e,
|
|
90
92
|
table,
|
|
91
93
|
cell,
|
|
94
|
+
row
|
|
92
95
|
}: {
|
|
93
96
|
e: React.MouseEvent<HTMLTableCellElement>;
|
|
94
97
|
cell: Cell<TData, TValue>;
|
|
98
|
+
row: Row<TData>;
|
|
95
99
|
table: ITable<TData>;
|
|
96
100
|
}) => void;
|
|
97
101
|
};
|
|
98
102
|
export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
99
|
-
handleClick
|
|
103
|
+
handleClick?: ({
|
|
100
104
|
e,
|
|
101
105
|
table,
|
|
102
106
|
row,
|
|
@@ -107,7 +111,8 @@ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
|
107
111
|
}) => void;
|
|
108
112
|
};
|
|
109
113
|
export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
110
|
-
|
|
114
|
+
classNameCondition?: | (({row, table}: {row?: Row<TData>; table?: ITable<TData>}) => string) | string;
|
|
115
|
+
handleClick?: ({
|
|
111
116
|
e,
|
|
112
117
|
table,
|
|
113
118
|
row,
|
|
@@ -118,7 +123,7 @@ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
|
118
123
|
}) => void;
|
|
119
124
|
};
|
|
120
125
|
export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
|
|
121
|
-
handleClick
|
|
126
|
+
handleClick?: ({
|
|
122
127
|
e,
|
|
123
128
|
table,
|
|
124
129
|
}: {
|
|
@@ -173,6 +178,15 @@ export type DataTableProps<TData, TValue> = {
|
|
|
173
178
|
// handles?: Handles
|
|
174
179
|
};
|
|
175
180
|
|
|
181
|
+
export interface UseTablePropsFn<TData, TValue> {
|
|
182
|
+
table: ITable<TData>;
|
|
183
|
+
row?: Row<TData>;
|
|
184
|
+
cell?: Cell<TData, TValue>;
|
|
185
|
+
header?: Header<TData, TValue>;
|
|
186
|
+
headerGroup?: HeaderGroup<TData>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
176
190
|
export type DataTableToolbarFns<TData> = {
|
|
177
191
|
globalFilter: string;
|
|
178
192
|
setGlobalFilter: (value: string) => void;
|
|
@@ -204,7 +218,7 @@ export function DataTable<TData, TValue>({
|
|
|
204
218
|
useTableProps,
|
|
205
219
|
initialState,
|
|
206
220
|
alternate = "even",
|
|
207
|
-
alternateColor = "#
|
|
221
|
+
alternateColor = "#fbfbfb",
|
|
208
222
|
// handles
|
|
209
223
|
}: DataTableProps<TData, TValue>) {
|
|
210
224
|
const table = useReactTable({
|
|
@@ -241,47 +255,74 @@ export function DataTable<TData, TValue>({
|
|
|
241
255
|
};
|
|
242
256
|
|
|
243
257
|
const {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} = useTableProps?.tableProps || {};
|
|
258
|
+
handleClick: tableHandleClick,
|
|
259
|
+
onClick: tableOnClick,
|
|
260
|
+
...tableDomProps
|
|
261
|
+
} = useTableProps?.tableProps || {};
|
|
248
262
|
|
|
249
|
-
const {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} = useTableProps?.headerProps || {};
|
|
254
|
-
const {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} = useTableProps?.rowHeadProps || {};
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
} = useTableProps?.
|
|
270
|
-
const {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
263
|
+
const {
|
|
264
|
+
handleClick: headerHandleClick,
|
|
265
|
+
onClick: headerOnClick,
|
|
266
|
+
...headerDomProps
|
|
267
|
+
} = useTableProps?.headerProps || {};
|
|
268
|
+
const {
|
|
269
|
+
handleClick: rowHeadHandleClick,
|
|
270
|
+
onClick: rowHeadOnClick,
|
|
271
|
+
...rowHeadDomProps
|
|
272
|
+
} = useTableProps?.rowHeadProps || {};
|
|
273
|
+
const {
|
|
274
|
+
handleClick: cellHeadHandleClick,
|
|
275
|
+
onClick: cellHeadOnClick,
|
|
276
|
+
classNameCondition: cellHeadClassNameCondition,
|
|
277
|
+
...cellHeadDomProps
|
|
278
|
+
} = useTableProps?.cellHeadProps || {};
|
|
279
|
+
const {
|
|
280
|
+
handleClick: bodyHandleClick,
|
|
281
|
+
onClick: bodyOnClick,
|
|
282
|
+
...bodyDomProps
|
|
283
|
+
} = useTableProps?.bodyProps || {};
|
|
284
|
+
const {
|
|
285
|
+
handleClick: rowBodyHandleClick,
|
|
286
|
+
onClick: rowBodyOnClick,
|
|
287
|
+
style: rowBodyStyle,
|
|
288
|
+
classNameCondition: rowBodyClassNameCondition,
|
|
289
|
+
...rowBodyDomProps
|
|
290
|
+
} = useTableProps?.rowBodyProps || {};
|
|
291
|
+
const {
|
|
292
|
+
handleClick: cellBodyHandleClick,
|
|
293
|
+
onClick: cellBodyOnClick,
|
|
294
|
+
classNameCondition: cellBodyClassNameCondition,
|
|
295
|
+
...cellBodyDomProps
|
|
296
|
+
} = useTableProps?.cellBodyProps || {};
|
|
275
297
|
|
|
276
|
-
const {
|
|
277
|
-
|
|
278
|
-
...skRowDomProps
|
|
279
|
-
} = useTableProps?.rowBodyProps || {};
|
|
298
|
+
const { handleClick: skRowHandleClick, classNameCondition: skRowClassNameCondition, ...skRowDomProps } =
|
|
299
|
+
useTableProps?.rowBodyProps || {};
|
|
280
300
|
|
|
281
|
-
const {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
301
|
+
const { handleClick: skCellHandleClick, classNameCondition: skCellClassNameCondition, ...skCellDomProps } =
|
|
302
|
+
useTableProps?.cellBodyProps || {};
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
function getCellHeadClassNameByCondition({cell,table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) {
|
|
306
|
+
if(!cell || !table) return "";
|
|
307
|
+
const classNameCondition = useTableProps?.cellHeadProps?.classNameCondition;
|
|
308
|
+
if(!classNameCondition) return "";
|
|
309
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
310
|
+
return classNameCondition({cell,table});
|
|
311
|
+
}
|
|
312
|
+
function getCellBodyClassNameByCondition({cell,table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) {
|
|
313
|
+
if(!cell || !table) return "";
|
|
314
|
+
const classNameCondition = useTableProps?.cellBodyProps?.classNameCondition;
|
|
315
|
+
if(!classNameCondition) return "";
|
|
316
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
317
|
+
return classNameCondition({cell,table});
|
|
318
|
+
}
|
|
319
|
+
function getRowBodyClassNameByCondition({row,table}: {row?: Row<TData>; table?: ITable<TData>}) {
|
|
320
|
+
if(!row || !table) return "";
|
|
321
|
+
const classNameCondition = useTableProps?.rowBodyProps?.classNameCondition;
|
|
322
|
+
if(!classNameCondition) return "";
|
|
323
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
324
|
+
return classNameCondition({row,table});
|
|
325
|
+
}
|
|
285
326
|
|
|
286
327
|
|
|
287
328
|
return (
|
|
@@ -321,35 +362,28 @@ const {
|
|
|
321
362
|
{headerGroup.headers.map((header) => (
|
|
322
363
|
<TableHead
|
|
323
364
|
key={header.id}
|
|
324
|
-
{...
|
|
325
|
-
const { handleClick, onClick, ...rest } =
|
|
326
|
-
useTableProps?.cellHeadProps || {};
|
|
327
|
-
return rest;
|
|
328
|
-
})()}
|
|
365
|
+
{...cellHeadDomProps}
|
|
329
366
|
className={cn(
|
|
330
367
|
"cursor-pointer select-none",
|
|
331
368
|
classNames?.header?.head
|
|
332
369
|
)}
|
|
333
370
|
style={{
|
|
334
|
-
width: header.getSize()
|
|
371
|
+
width: header.getSize()
|
|
372
|
+
? `${header.getSize()}px !important`
|
|
373
|
+
: "auto",
|
|
335
374
|
}}
|
|
336
375
|
onClick={(e) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
useTableProps.cellHeadProps.onClick(e);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Just call the parent's handleClick if provided
|
|
343
|
-
if (useTableProps?.cellHeadProps?.handleClick) {
|
|
344
|
-
useTableProps.cellHeadProps.handleClick({
|
|
345
|
-
e,
|
|
346
|
-
cell: header,
|
|
347
|
-
table,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
376
|
+
cellHeadOnClick?.(e);
|
|
377
|
+
cellHeadHandleClick?.({ e, table, cell: header });
|
|
350
378
|
}}
|
|
351
379
|
>
|
|
352
|
-
<div
|
|
380
|
+
<div
|
|
381
|
+
className={cn(
|
|
382
|
+
"flex items-center gap-1 w-fit",
|
|
383
|
+
classNames?.header?.content,
|
|
384
|
+
getCellHeadClassNameByCondition({ cell: header, table })
|
|
385
|
+
)}
|
|
386
|
+
>
|
|
353
387
|
{flexRender(
|
|
354
388
|
header.column.columnDef.header,
|
|
355
389
|
header.getContext()
|
|
@@ -384,15 +418,18 @@ const {
|
|
|
384
418
|
{!isLoading &&
|
|
385
419
|
table.getRowModel().rows.length > 0 &&
|
|
386
420
|
table.getRowModel().rows.map((row, index) => {
|
|
387
|
-
const { handleClick, onClick, ...rest } =
|
|
388
|
-
useTableProps?.rowBodyProps || {};
|
|
389
|
-
|
|
390
421
|
return (
|
|
391
422
|
<TableRow
|
|
392
423
|
{...rowBodyDomProps}
|
|
393
424
|
key={row.id}
|
|
394
|
-
style={{
|
|
395
|
-
|
|
425
|
+
style={{
|
|
426
|
+
...rowBodyStyle,
|
|
427
|
+
backgroundColor: getAlternateColor(index),
|
|
428
|
+
}}
|
|
429
|
+
className={cn(
|
|
430
|
+
classNames?.body?.row,
|
|
431
|
+
getRowBodyClassNameByCondition({ row, table })
|
|
432
|
+
)}
|
|
396
433
|
data-state={row.getIsSelected() && "selected"}
|
|
397
434
|
onClick={(e) => {
|
|
398
435
|
rowBodyOnClick?.(e);
|
|
@@ -403,10 +440,13 @@ const {
|
|
|
403
440
|
<TableCell
|
|
404
441
|
{...cellBodyDomProps}
|
|
405
442
|
key={cell.id}
|
|
406
|
-
className={cn(
|
|
443
|
+
className={cn(
|
|
444
|
+
classNames?.body?.cell,
|
|
445
|
+
getCellBodyClassNameByCondition({ cell, table })
|
|
446
|
+
)}
|
|
407
447
|
onClick={(e) => {
|
|
408
448
|
cellBodyOnClick?.(e);
|
|
409
|
-
cellBodyHandleClick?.({ e, cell, table });
|
|
449
|
+
cellBodyHandleClick?.({ e, cell, row, table });
|
|
410
450
|
}}
|
|
411
451
|
>
|
|
412
452
|
{flexRender(
|
|
@@ -480,26 +520,27 @@ export const TableSkeleton = <TData, TValue>({
|
|
|
480
520
|
const {
|
|
481
521
|
handleClick: _rowHandleClick,
|
|
482
522
|
onClick: _rowOnClick,
|
|
523
|
+
classNameCondition: rowClassNameCondition,
|
|
483
524
|
...rowDomProps
|
|
484
525
|
} = props?.rowBodyProps || {};
|
|
485
526
|
|
|
486
527
|
const {
|
|
487
528
|
handleClick: _cellHandleClick,
|
|
488
529
|
onClick: _cellOnClick,
|
|
530
|
+
classNameCondition: cellClassNameCondition,
|
|
489
531
|
...cellDomProps
|
|
490
532
|
} = props?.cellBodyProps || {};
|
|
491
533
|
|
|
492
|
-
|
|
493
534
|
if (showNoData) {
|
|
494
535
|
return (
|
|
495
536
|
<TableRow
|
|
496
537
|
key="no-data-skeleton"
|
|
497
|
-
className={cn(classNames?.body?.row)}
|
|
538
|
+
className={cn(classNames?.body?.row, )}
|
|
498
539
|
{...rowDomProps}
|
|
499
540
|
>
|
|
500
541
|
<TableCell
|
|
501
542
|
colSpan={columns.length}
|
|
502
|
-
className={cn("h-24 text-center"
|
|
543
|
+
className={cn("h-24 text-center")}
|
|
503
544
|
{...cellDomProps}
|
|
504
545
|
>
|
|
505
546
|
{emptyLabel}
|
|
@@ -1,95 +1,200 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import React from 'react';
|
|
4
4
|
|
|
5
|
-
import { ColumnDef } from
|
|
5
|
+
import { ColumnDef } from '@tanstack/react-table';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { cn } from '../../lib/utils';
|
|
8
|
+
import {
|
|
9
|
+
DataTable,
|
|
10
|
+
UseTableProps,
|
|
11
|
+
} from './data-table'; // chỉnh path cho đúng
|
|
8
12
|
|
|
13
|
+
/* ======================================================
|
|
14
|
+
* 1. Data type
|
|
15
|
+
* ====================================================== */
|
|
9
16
|
type User = {
|
|
10
17
|
id: string;
|
|
11
18
|
name: string;
|
|
12
19
|
email: string;
|
|
20
|
+
role: "admin" | "user";
|
|
21
|
+
status: "active" | "inactive";
|
|
13
22
|
};
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
/* ======================================================
|
|
25
|
+
* 2. Mock data
|
|
26
|
+
* ====================================================== */
|
|
27
|
+
const USERS: User[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "1",
|
|
30
|
+
name: "John Doe",
|
|
31
|
+
email: "john@example.com",
|
|
32
|
+
role: "admin",
|
|
33
|
+
status: "active",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "2",
|
|
37
|
+
name: "Jane Smith",
|
|
38
|
+
email: "jane@example.com",
|
|
39
|
+
role: "user",
|
|
40
|
+
status: "inactive",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "3",
|
|
44
|
+
name: "Alex Johnson",
|
|
45
|
+
email: "alex@example.com",
|
|
46
|
+
role: "user",
|
|
47
|
+
status: "active",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/* ======================================================
|
|
52
|
+
* 3. Columns
|
|
53
|
+
* ====================================================== */
|
|
16
54
|
const columns: ColumnDef<User>[] = [
|
|
17
55
|
{
|
|
18
56
|
accessorKey: "name",
|
|
19
57
|
header: "Name",
|
|
20
|
-
cell: ({ row }) =>
|
|
21
|
-
|
|
58
|
+
cell: ({ row }) => (
|
|
59
|
+
<span className="font-medium">{row.original.name}</span>
|
|
60
|
+
),
|
|
22
61
|
},
|
|
23
62
|
{
|
|
24
63
|
accessorKey: "email",
|
|
25
64
|
header: "Email",
|
|
26
|
-
|
|
65
|
+
cell: ({ getValue }) => (
|
|
66
|
+
<span className="text-blue-600">{getValue<string>()}</span>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
accessorKey: "role",
|
|
71
|
+
header: "Role",
|
|
72
|
+
cell: ({ getValue }) => (
|
|
73
|
+
<span className="capitalize">{getValue<string>()}</span>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
accessorKey: "status",
|
|
78
|
+
header: "Status",
|
|
79
|
+
cell: ({ getValue }) => {
|
|
80
|
+
const value = getValue<string>();
|
|
81
|
+
return (
|
|
82
|
+
<span
|
|
83
|
+
className={cn(
|
|
84
|
+
"px-2 py-0.5 rounded text-xs font-medium",
|
|
85
|
+
value === "active"
|
|
86
|
+
? "bg-green-100 text-green-700"
|
|
87
|
+
: "bg-red-100 text-red-700"
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{value}
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
27
94
|
},
|
|
28
95
|
];
|
|
29
96
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
97
|
+
/* ======================================================
|
|
98
|
+
* 4. Toolbar
|
|
99
|
+
* ====================================================== */
|
|
100
|
+
const Toolbar = ({ fns }: any) => {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<input
|
|
104
|
+
value={fns.globalFilter ?? ""}
|
|
105
|
+
onChange={(e) => fns.setGlobalFilter(e.target.value)}
|
|
106
|
+
placeholder="Search..."
|
|
107
|
+
className="border rounded px-2 py-1 text-sm"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
37
112
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
113
|
+
/* ======================================================
|
|
114
|
+
* 5. Pagination
|
|
115
|
+
* ====================================================== */
|
|
116
|
+
const Pagination = ({ fns }: any) => {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-center justify-end gap-2">
|
|
119
|
+
<button
|
|
120
|
+
onClick={fns.previousPage}
|
|
121
|
+
disabled={!fns.getCanPreviousPage()}
|
|
122
|
+
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
123
|
+
>
|
|
124
|
+
Prev
|
|
125
|
+
</button>
|
|
126
|
+
|
|
127
|
+
<span className="text-sm">
|
|
128
|
+
Page {fns.pageIndex + 1}
|
|
129
|
+
</span>
|
|
130
|
+
|
|
131
|
+
<button
|
|
132
|
+
onClick={fns.nextPage}
|
|
133
|
+
disabled={!fns.getCanNextPage()}
|
|
134
|
+
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
135
|
+
>
|
|
136
|
+
Next
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
42
141
|
|
|
142
|
+
/* ======================================================
|
|
143
|
+
* 6. useTableProps (row / cell behavior)
|
|
144
|
+
* ====================================================== */
|
|
145
|
+
const useTableProps: UseTableProps<User, unknown> = {
|
|
146
|
+
rowBodyProps: {
|
|
147
|
+
classNameCondition: ({ row }) =>
|
|
148
|
+
row?.original.status === "inactive"
|
|
149
|
+
? "opacity-60"
|
|
150
|
+
: "",
|
|
151
|
+
handleClick: ({ row }) => {
|
|
152
|
+
console.log("Row clicked:", row.original);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
cellBodyProps: {
|
|
156
|
+
classNameCondition: ({ cell }) =>
|
|
157
|
+
cell?.column.id === "email"
|
|
158
|
+
? "underline"
|
|
159
|
+
: "",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/* ======================================================
|
|
164
|
+
* 7. Final Component
|
|
165
|
+
* ====================================================== */
|
|
166
|
+
export default function UserTableExample() {
|
|
43
167
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<div className="flex items-center justify-end gap-4">
|
|
76
|
-
<button
|
|
77
|
-
onClick={fns.previousPage}
|
|
78
|
-
disabled={!fns.getCanPreviousPage()}
|
|
79
|
-
className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
|
|
80
|
-
>
|
|
81
|
-
Trang trước
|
|
82
|
-
</button>
|
|
83
|
-
<span>Trang {fns.pageIndex + 1}</span>
|
|
84
|
-
<button
|
|
85
|
-
onClick={fns.nextPage}
|
|
86
|
-
disabled={!fns.getCanNextPage()}
|
|
87
|
-
className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
|
|
88
|
-
>
|
|
89
|
-
Trang sau
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
/>
|
|
168
|
+
<div className="p-4 space-y-4">
|
|
169
|
+
<h1 className="text-lg font-semibold">User Table</h1>
|
|
170
|
+
|
|
171
|
+
<DataTable<User, unknown>
|
|
172
|
+
data={USERS}
|
|
173
|
+
columns={columns}
|
|
174
|
+
enableSort
|
|
175
|
+
alternate="even"
|
|
176
|
+
alternateColor="#f9fafb"
|
|
177
|
+
emptyLabel="No users found"
|
|
178
|
+
initialState={{
|
|
179
|
+
pagination: {
|
|
180
|
+
pageSize: 5,
|
|
181
|
+
pageIndex: 0,
|
|
182
|
+
},
|
|
183
|
+
}}
|
|
184
|
+
toolbarTable={({ fns }) => <Toolbar fns={fns} />}
|
|
185
|
+
paginationTable={({ fns }) => <Pagination fns={fns} />}
|
|
186
|
+
useTableProps={useTableProps}
|
|
187
|
+
classNames={{
|
|
188
|
+
table: "border rounded-md",
|
|
189
|
+
header: {
|
|
190
|
+
head: "bg-gray-50 text-sm font-semibold",
|
|
191
|
+
},
|
|
192
|
+
body: {
|
|
193
|
+
row: "hover:bg-gray-50 cursor-pointer",
|
|
194
|
+
cell: "text-sm",
|
|
195
|
+
},
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
94
199
|
);
|
|
95
200
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
|
|
2
|
+
/* ============================================================
|
|
3
|
+
RBAC FINAL – BACKEND + FRONTEND – SINGLE FILE
|
|
4
|
+
============================================================ */
|
|
5
|
+
|
|
6
|
+
/* =======================
|
|
7
|
+
REACT HELPERS (OPTIONAL)
|
|
8
|
+
======================= */
|
|
9
|
+
import React, {
|
|
10
|
+
createContext,
|
|
11
|
+
useContext,
|
|
12
|
+
} from 'react';
|
|
13
|
+
|
|
14
|
+
/* =======================
|
|
15
|
+
SHARED CONCEPT
|
|
16
|
+
======================= */
|
|
17
|
+
export type ID = string;
|
|
18
|
+
/**
|
|
19
|
+
* @example "read"
|
|
20
|
+
*/
|
|
21
|
+
export type Action = string;
|
|
22
|
+
/**
|
|
23
|
+
* @description Resource is route name
|
|
24
|
+
* @example "user"
|
|
25
|
+
*/
|
|
26
|
+
export type Resource = string;
|
|
27
|
+
/**
|
|
28
|
+
* @example "user:read"
|
|
29
|
+
*/
|
|
30
|
+
export type PermissionKey = `${Resource}:${Action}`;
|
|
31
|
+
|
|
32
|
+
/* =======================
|
|
33
|
+
BACKEND RBAC (AUTHORITATIVE)
|
|
34
|
+
======================= */
|
|
35
|
+
|
|
36
|
+
export interface Permission {
|
|
37
|
+
id: ID;
|
|
38
|
+
action: Action;
|
|
39
|
+
resource: Resource;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Role {
|
|
43
|
+
id: ID;
|
|
44
|
+
name: string;
|
|
45
|
+
permissions: ID[];
|
|
46
|
+
inherits?: ID[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type ConditionOperator = 'eq' | 'ne' | 'in';
|
|
50
|
+
|
|
51
|
+
export interface Condition {
|
|
52
|
+
field: string; // "user.id", "resource.ownerId", "user.tenantId"
|
|
53
|
+
op: ConditionOperator;
|
|
54
|
+
value: any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Policy {
|
|
58
|
+
id: ID;
|
|
59
|
+
permissionId: ID;
|
|
60
|
+
effect: 'allow' | 'deny';
|
|
61
|
+
conditions?: Condition[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UserIdentity {
|
|
65
|
+
id: ID;
|
|
66
|
+
roles: ID[];
|
|
67
|
+
tenantId?: ID;
|
|
68
|
+
[k: string]: any;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* =======================
|
|
72
|
+
RBAC STORE INTERFACE
|
|
73
|
+
======================= */
|
|
74
|
+
export interface RBACStore {
|
|
75
|
+
getRoles(ids: ID[]): Promise<Role[]>;
|
|
76
|
+
getPermissionId(action: Action, resource: Resource): Promise<ID | null>;
|
|
77
|
+
getPolicies(permissionId: ID): Promise<Policy[]>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* =======================
|
|
81
|
+
BACKEND RBAC ENGINE
|
|
82
|
+
======================= */
|
|
83
|
+
|
|
84
|
+
const getByPath = (obj: any, path: string) =>
|
|
85
|
+
path.split('.').reduce((o, k) => o?.[k], obj);
|
|
86
|
+
|
|
87
|
+
const evalCondition = (cond: Condition, ctx: any) => {
|
|
88
|
+
const left = getByPath(ctx, cond.field);
|
|
89
|
+
if (cond.op === 'eq') return left === cond.value;
|
|
90
|
+
if (cond.op === 'ne') return left !== cond.value;
|
|
91
|
+
if (cond.op === 'in') return Array.isArray(cond.value) && cond.value.includes(left);
|
|
92
|
+
return false;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const evalConditions = (conds: Condition[] = [], ctx: any) =>
|
|
96
|
+
conds.every(c => evalCondition(c, ctx));
|
|
97
|
+
|
|
98
|
+
export async function canAccess(
|
|
99
|
+
store: RBACStore,
|
|
100
|
+
user: UserIdentity,
|
|
101
|
+
action: Action,
|
|
102
|
+
resource: Resource,
|
|
103
|
+
context: {
|
|
104
|
+
resource?: any;
|
|
105
|
+
request?: any;
|
|
106
|
+
} = {}
|
|
107
|
+
): Promise<boolean> {
|
|
108
|
+
const permissionId = await store.getPermissionId(action, resource);
|
|
109
|
+
if (!permissionId) return false;
|
|
110
|
+
|
|
111
|
+
/* ---- Resolve role inheritance ---- */
|
|
112
|
+
const visited = new Set<ID>();
|
|
113
|
+
const collect = async (roleId: ID) => {
|
|
114
|
+
if (visited.has(roleId)) return;
|
|
115
|
+
visited.add(roleId);
|
|
116
|
+
const [role] = await store.getRoles([roleId]);
|
|
117
|
+
for (const p of role?.inherits ?? []) await collect(p);
|
|
118
|
+
};
|
|
119
|
+
for (const r of user.roles) await collect(r);
|
|
120
|
+
|
|
121
|
+
const roles = await store.getRoles([...visited]);
|
|
122
|
+
const hasRoleGrant = roles.some(r => r.permissions.includes(permissionId));
|
|
123
|
+
|
|
124
|
+
/* ---- Evaluate policies ---- */
|
|
125
|
+
const policies = await store.getPolicies(permissionId);
|
|
126
|
+
const ctx = {
|
|
127
|
+
user,
|
|
128
|
+
resource: context.resource,
|
|
129
|
+
request: context.request,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
for (const p of policies) {
|
|
133
|
+
if (!evalConditions(p.conditions, ctx)) continue;
|
|
134
|
+
if (p.effect === 'deny') return false;
|
|
135
|
+
if (p.effect === 'allow') return hasRoleGrant;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return hasRoleGrant;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* =======================
|
|
142
|
+
BACKEND MIDDLEWARE
|
|
143
|
+
======================= */
|
|
144
|
+
|
|
145
|
+
export const requirePermission =
|
|
146
|
+
(store: RBACStore, action: Action, resource: Resource) =>
|
|
147
|
+
async (req: any, res: any, next: any) => {
|
|
148
|
+
if (!req.user) return res.status(401).end();
|
|
149
|
+
|
|
150
|
+
const ok = await canAccess(store, req.user, action, resource, {
|
|
151
|
+
resource: req.body ?? req.params,
|
|
152
|
+
request: req,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!ok) return res.status(403).end();
|
|
156
|
+
next();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/* =======================
|
|
160
|
+
MEMORY STORE (DEV / DEMO)
|
|
161
|
+
======================= */
|
|
162
|
+
|
|
163
|
+
export function createMemoryRBACStore(data: {
|
|
164
|
+
roles: Role[];
|
|
165
|
+
permissions: Permission[];
|
|
166
|
+
policies: Policy[];
|
|
167
|
+
}): RBACStore {
|
|
168
|
+
const roleMap = new Map(data.roles.map(r => [r.id, r]));
|
|
169
|
+
const permMap = new Map<PermissionKey, ID>();
|
|
170
|
+
const policyMap = new Map<ID, Policy[]>();
|
|
171
|
+
|
|
172
|
+
data.permissions.forEach(p =>
|
|
173
|
+
permMap.set(`${p.resource}:${p.action}`, p.id)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
data.policies.forEach(p => {
|
|
177
|
+
const arr = policyMap.get(p.permissionId) ?? [];
|
|
178
|
+
arr.push(p);
|
|
179
|
+
policyMap.set(p.permissionId, arr);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
getRoles: async ids => ids.map(id => roleMap.get(id)!).filter(Boolean),
|
|
184
|
+
getPermissionId: async (a, r) => permMap.get(`${r}:${a}`) ?? null,
|
|
185
|
+
getPolicies: async pid => policyMap.get(pid) ?? [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* =======================
|
|
190
|
+
FRONTEND RBAC (UI ONLY)
|
|
191
|
+
======================= */
|
|
192
|
+
|
|
193
|
+
export class FrontendRBAC {
|
|
194
|
+
private permissions: Set<PermissionKey>;
|
|
195
|
+
|
|
196
|
+
constructor(perms: PermissionKey[]) {
|
|
197
|
+
this.permissions = new Set(perms);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
can(action: Action, resource: Resource): boolean {
|
|
201
|
+
return this.permissions.has(`${resource}:${action}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const RBACContext = createContext<FrontendRBAC | null>(null);
|
|
206
|
+
|
|
207
|
+
export const RBACProvider = ({
|
|
208
|
+
rbac,
|
|
209
|
+
children,
|
|
210
|
+
}: {
|
|
211
|
+
rbac: FrontendRBAC;
|
|
212
|
+
children: React.ReactNode;
|
|
213
|
+
}) => (
|
|
214
|
+
<RBACContext.Provider value={rbac}>
|
|
215
|
+
{children}
|
|
216
|
+
</RBACContext.Provider>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
export const useCan = (action: Action, resource: Resource) => {
|
|
220
|
+
const rbac = useContext(RBACContext);
|
|
221
|
+
return rbac?.can(action, resource) ?? false;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const Can = ({
|
|
225
|
+
action,
|
|
226
|
+
resource,
|
|
227
|
+
children,
|
|
228
|
+
fallback = null,
|
|
229
|
+
}: any) => {
|
|
230
|
+
const ok = useCan(action, resource);
|
|
231
|
+
return ok ? children : fallback;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
// fully usage fe
|
|
236
|
+
|
|
237
|
+
// const rbac = new FrontendRBAC(['user:read', 'user:update']);
|
|
238
|
+
// const store = createMemoryRBACStore({
|
|
239
|
+
// roles: [],
|
|
240
|
+
// permissions: [],
|
|
241
|
+
// policies: [],
|
|
242
|
+
// });
|
|
243
|
+
|
|
244
|
+
// <RBACProvider rbac={rbac}>
|
|
245
|
+
// <Can action="user:read" resource="user">
|
|
246
|
+
// <div>Can read user</div>
|
|
247
|
+
// </Can>
|
|
248
|
+
// </RBACProvider>
|
|
249
|
+
|
|
250
|
+
// fully usage be
|
|
251
|
+
|
|
252
|
+
// import { requirePermission } from './rbac';
|
|
253
|
+
// router.use(requirePermission(store, 'read', 'user'));
|
|
254
|
+
|
|
255
|
+
// app.use(requirePermission(store, 'read', 'user'));
|
package/components.json
CHANGED
|
@@ -147,6 +147,11 @@
|
|
|
147
147
|
"dependencies": [],
|
|
148
148
|
"devDependencies": []
|
|
149
149
|
},
|
|
150
|
+
"list-map": {
|
|
151
|
+
"path": "component-templates/components/list-map.tsx",
|
|
152
|
+
"dependencies": [],
|
|
153
|
+
"devDependencies": []
|
|
154
|
+
},
|
|
150
155
|
"loader-slash-gradient": {
|
|
151
156
|
"path": "component-templates/components/loader-slash-gradient.tsx",
|
|
152
157
|
"dependencies": [],
|
|
@@ -328,6 +333,11 @@
|
|
|
328
333
|
"dependencies": [],
|
|
329
334
|
"devDependencies": []
|
|
330
335
|
},
|
|
336
|
+
"rbac": {
|
|
337
|
+
"path": "component-templates/providers/rbac.tsx",
|
|
338
|
+
"dependencies": [],
|
|
339
|
+
"devDependencies": []
|
|
340
|
+
},
|
|
331
341
|
"refine-provider": {
|
|
332
342
|
"path": "component-templates/providers/refine-provider.tsx",
|
|
333
343
|
"dependencies": [
|
|
@@ -358,9 +368,7 @@
|
|
|
358
368
|
"dependencies": [
|
|
359
369
|
"js-cookie"
|
|
360
370
|
],
|
|
361
|
-
"devDependencies": [
|
|
362
|
-
"@types/js-cookie"
|
|
363
|
-
]
|
|
371
|
+
"devDependencies": []
|
|
364
372
|
},
|
|
365
373
|
"hash-aes": {
|
|
366
374
|
"path": "component-templates/utils/hash/hash-aes.ts",
|