@gravitee/graphene-core 2.42.0 → 2.42.1-data-table-migration-work.9921dd4
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/USAGE_GUIDE.md +166 -0
- package/dist/composed/DataTable/DataTable.d.ts +33 -4
- package/dist/composed/DataTable/DataTable.d.ts.map +1 -1
- package/dist/composed/DataTable/cells.d.ts +38 -0
- package/dist/composed/DataTable/cells.d.ts.map +1 -0
- package/dist/composed/DataTable/index.d.ts +3 -1
- package/dist/composed/DataTable/index.d.ts.map +1 -1
- package/dist/composed/DataTableEmptyState/DataTableEmptyState.d.ts +33 -0
- package/dist/composed/DataTableEmptyState/DataTableEmptyState.d.ts.map +1 -0
- package/dist/composed/DataTableEmptyState/index.d.ts +3 -0
- package/dist/composed/DataTableEmptyState/index.d.ts.map +1 -0
- package/dist/icons/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1340 -1092
- package/dist/styles/globals.css +1 -1
- package/dist/testing/index.js +1 -1
- package/package.json +6 -5
- package/snippets/data-table-list-page.tsx +321 -0
- /package/dist/{ThemeProvider-C_Ea8dI-.js → ThemeProvider-D0l3ICGl.js} +0 -0
- /package/dist/{icons-D_4R9XrW.js → icons-DdCWtjqd.js} +0 -0
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as e } from "../ThemeProvider-
|
|
1
|
+
import { t as e } from "../ThemeProvider-D0l3ICGl.js";
|
|
2
2
|
import { jsx as t } from "react/jsx-runtime";
|
|
3
3
|
import { render as n, screen as r, within as i } from "@testing-library/react";
|
|
4
4
|
import a from "@testing-library/user-event";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravitee/graphene-core",
|
|
3
|
-
"version": "2.42.
|
|
3
|
+
"version": "2.42.1-data-table-migration-work.9921dd4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Gravitee's Graphene design system: accessible React components, design tokens, icons, and shared ESLint/TypeScript configs. Built on shadcn/ui and Tailwind.",
|
|
6
6
|
"keywords": [
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"@fontsource/dm-sans": ">=5.0.0",
|
|
94
|
+
"@gravitee/graphene-core": "2.42.1-data-table-migration-work.9921dd4",
|
|
94
95
|
"@tanstack/react-table": "^8.0.0",
|
|
95
96
|
"@testing-library/dom": ">=10.0.0",
|
|
96
97
|
"@testing-library/react": ">=16.0.0",
|
|
@@ -142,10 +143,10 @@
|
|
|
142
143
|
}
|
|
143
144
|
},
|
|
144
145
|
"devDependencies": {
|
|
145
|
-
"@hookform/resolvers": "5.
|
|
146
|
-
"react-hook-form": "7.
|
|
147
|
-
"shadcn": "4.
|
|
148
|
-
"vitest": "4.1.
|
|
146
|
+
"@hookform/resolvers": "5.4.0",
|
|
147
|
+
"react-hook-form": "7.78.0",
|
|
148
|
+
"shadcn": "4.11.0",
|
|
149
|
+
"vitest": "4.1.8",
|
|
149
150
|
"zod": "4.4.3"
|
|
150
151
|
},
|
|
151
152
|
"publishConfig": {
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// Snippet: Entity List page with DataTable
|
|
2
|
+
//
|
|
3
|
+
// Use when building a list page for server-paginated entity data (APIs, Applications,
|
|
4
|
+
// Policies, Agents, Plans, etc.) — any CRUD list where users manage entities.
|
|
5
|
+
//
|
|
6
|
+
// Do NOT use for:
|
|
7
|
+
// - Static key-value detail panels (use raw Table)
|
|
8
|
+
// - Time-series/triage explorers (logs, traces) — see Record Explorer pattern (deferred)
|
|
9
|
+
// - Tables with fewer than 5 fixed rows
|
|
10
|
+
//
|
|
11
|
+
// Empty state pattern (page-level decision):
|
|
12
|
+
// - totalCount === 0 && !hasFilters → DataTableEmptyState variant="first-use" (INSTEAD of DataTable)
|
|
13
|
+
// - filteredCount === 0 && hasFilters → DataTableEmptyState variant="no-results" (INSIDE DataTable)
|
|
14
|
+
//
|
|
15
|
+
// Header button coordination:
|
|
16
|
+
// - First-use → header hides "Add" button; empty state owns the primary CTA
|
|
17
|
+
// - Has data → header shows "Add" button (primary); table is main content
|
|
18
|
+
// - Both buttons trigger the same creation flow
|
|
19
|
+
//
|
|
20
|
+
// See Storybook "Composed/DataTableEmptyState → Integration" for interactive example.
|
|
21
|
+
//
|
|
22
|
+
// Column ordering (left to right):
|
|
23
|
+
// 1. Name (link, always first, font-medium hover:underline)
|
|
24
|
+
// 2. Status badge (lifecycle state)
|
|
25
|
+
// 3. Category badges (type, version, security)
|
|
26
|
+
// 4. Dates (relative format + full tooltip)
|
|
27
|
+
// 5. Owner/actor (truncated text)
|
|
28
|
+
// 6. Actions (always last, sr-only header)
|
|
29
|
+
//
|
|
30
|
+
// Minimum 3 data columns (excluding actions). Fewer → use Item list or card grid.
|
|
31
|
+
// Target 4-7 visible columns. More than 7 → enable column visibility.
|
|
32
|
+
// Do NOT show raw entity IDs as visible columns.
|
|
33
|
+
//
|
|
34
|
+
// Actions column: ALWAYS use MoreVerticalIcon (⋮) + DropdownMenu.
|
|
35
|
+
// Never render a row of inline icon buttons. Single-action exception: exactly 1
|
|
36
|
+
// action may be a lone icon button without a dropdown.
|
|
37
|
+
//
|
|
38
|
+
// See Storybook "Patterns/Data Table → ApiList" for interactive example.
|
|
39
|
+
|
|
40
|
+
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
|
41
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
42
|
+
import {
|
|
43
|
+
Badge,
|
|
44
|
+
Button,
|
|
45
|
+
DataTable,
|
|
46
|
+
DataTableColumnHeader,
|
|
47
|
+
DataTableEmptyState,
|
|
48
|
+
DropdownMenu,
|
|
49
|
+
DropdownMenuContent,
|
|
50
|
+
DropdownMenuItem,
|
|
51
|
+
DropdownMenuSeparator,
|
|
52
|
+
DropdownMenuTrigger,
|
|
53
|
+
FacetedFilter,
|
|
54
|
+
Input,
|
|
55
|
+
} from '@gravitee/graphene-core';
|
|
56
|
+
import { DateCell } from '@gravitee/graphene-core/composed/DataTable';
|
|
57
|
+
import {
|
|
58
|
+
GlobeIcon,
|
|
59
|
+
MoreVerticalIcon,
|
|
60
|
+
PencilIcon,
|
|
61
|
+
PlusIcon,
|
|
62
|
+
SearchIcon,
|
|
63
|
+
Trash2Icon,
|
|
64
|
+
} from '@gravitee/graphene-core/icons';
|
|
65
|
+
|
|
66
|
+
// Replace with your entity type
|
|
67
|
+
interface Entity {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
status: 'active' | 'inactive' | 'draft';
|
|
71
|
+
createdAt: string;
|
|
72
|
+
owner: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Replace with your API fetch function
|
|
76
|
+
declare function fetchEntities(params: {
|
|
77
|
+
page: number;
|
|
78
|
+
perPage: number;
|
|
79
|
+
sortBy?: string;
|
|
80
|
+
sortOrder?: 'asc' | 'desc';
|
|
81
|
+
search?: string;
|
|
82
|
+
status?: string[];
|
|
83
|
+
}): Promise<{ data: Entity[]; totalCount: number }>;
|
|
84
|
+
|
|
85
|
+
const STATUS_VARIANTS = {
|
|
86
|
+
active: 'success',
|
|
87
|
+
inactive: 'secondary',
|
|
88
|
+
draft: 'outline',
|
|
89
|
+
} as const;
|
|
90
|
+
|
|
91
|
+
const statusOptions = [
|
|
92
|
+
{ value: 'active', label: 'Active' },
|
|
93
|
+
{ value: 'inactive', label: 'Inactive' },
|
|
94
|
+
{ value: 'draft', label: 'Draft' },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const columns: ColumnDef<Entity, unknown>[] = [
|
|
98
|
+
{
|
|
99
|
+
accessorKey: 'name',
|
|
100
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
|
101
|
+
cell: ({ row }) => (
|
|
102
|
+
<a href={`#/entities/${row.original.id}`} className="font-medium hover:underline">
|
|
103
|
+
{row.original.name}
|
|
104
|
+
</a>
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
accessorKey: 'status',
|
|
109
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
|
|
110
|
+
cell: ({ row }) => <Badge variant={STATUS_VARIANTS[row.original.status]}>{row.original.status}</Badge>,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
accessorKey: 'createdAt',
|
|
114
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title="Created" />,
|
|
115
|
+
cell: ({ row }) => <DateCell value={row.original.createdAt} />,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
accessorKey: 'owner',
|
|
119
|
+
header: 'Owner',
|
|
120
|
+
enableSorting: false,
|
|
121
|
+
cell: ({ row }) => <span className="max-w-[150px] truncate text-muted-foreground">{row.original.owner}</span>,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'actions',
|
|
125
|
+
header: () => <span className="sr-only">Actions</span>,
|
|
126
|
+
enableSorting: false,
|
|
127
|
+
enableHiding: false,
|
|
128
|
+
cell: () => (
|
|
129
|
+
<div className="flex justify-end">
|
|
130
|
+
<DropdownMenu>
|
|
131
|
+
<DropdownMenuTrigger asChild>
|
|
132
|
+
<Button variant="ghost" size="icon-xs">
|
|
133
|
+
<MoreVerticalIcon className="size-4" />
|
|
134
|
+
</Button>
|
|
135
|
+
</DropdownMenuTrigger>
|
|
136
|
+
<DropdownMenuContent align="end">
|
|
137
|
+
<DropdownMenuItem>
|
|
138
|
+
<PencilIcon className="size-3.5" />
|
|
139
|
+
Edit
|
|
140
|
+
</DropdownMenuItem>
|
|
141
|
+
<DropdownMenuSeparator />
|
|
142
|
+
<DropdownMenuItem className="text-destructive">
|
|
143
|
+
<Trash2Icon className="size-3.5" />
|
|
144
|
+
Delete
|
|
145
|
+
</DropdownMenuItem>
|
|
146
|
+
</DropdownMenuContent>
|
|
147
|
+
</DropdownMenu>
|
|
148
|
+
</div>
|
|
149
|
+
),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
interface FetchState {
|
|
154
|
+
data: Entity[];
|
|
155
|
+
totalCount: number;
|
|
156
|
+
loading: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type FetchAction = { type: 'loading' } | { type: 'success'; data: Entity[]; totalCount: number };
|
|
160
|
+
|
|
161
|
+
function fetchReducer(state: FetchState, action: FetchAction): FetchState {
|
|
162
|
+
if (action.type === 'loading') return { ...state, loading: true };
|
|
163
|
+
return { data: action.data, totalCount: action.totalCount, loading: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function EntityListPage() {
|
|
167
|
+
const [page, setPage] = useState(1);
|
|
168
|
+
const [pageSize, setPageSize] = useState(10);
|
|
169
|
+
const [search, setSearch] = useState('');
|
|
170
|
+
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
171
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
172
|
+
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
|
173
|
+
const [state, dispatch] = useReducer(fetchReducer, { data: [], totalCount: 0, loading: true });
|
|
174
|
+
|
|
175
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
debounceRef.current = setTimeout(() => {
|
|
178
|
+
setDebouncedSearch(search);
|
|
179
|
+
setPage(1);
|
|
180
|
+
}, 300);
|
|
181
|
+
return () => clearTimeout(debounceRef.current);
|
|
182
|
+
}, [search]);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
let active = true;
|
|
186
|
+
dispatch({ type: 'loading' });
|
|
187
|
+
fetchEntities({
|
|
188
|
+
page,
|
|
189
|
+
perPage: pageSize,
|
|
190
|
+
sortBy: sorting[0]?.id,
|
|
191
|
+
sortOrder: sorting[0]?.desc ? 'desc' : 'asc',
|
|
192
|
+
search: debouncedSearch || undefined,
|
|
193
|
+
status: statusFilter.length ? statusFilter : undefined,
|
|
194
|
+
}).then((res) => {
|
|
195
|
+
if (active) dispatch({ type: 'success', data: res.data, totalCount: res.totalCount });
|
|
196
|
+
});
|
|
197
|
+
return () => {
|
|
198
|
+
active = false;
|
|
199
|
+
};
|
|
200
|
+
}, [page, pageSize, sorting, debouncedSearch, statusFilter]);
|
|
201
|
+
|
|
202
|
+
const handleSortingChange = (updater: SortingState | ((prev: SortingState) => SortingState)) => {
|
|
203
|
+
setSorting(typeof updater === 'function' ? updater(sorting) : updater);
|
|
204
|
+
setPage(1);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const handleStatusChange = (values: string[]) => {
|
|
208
|
+
setStatusFilter(values);
|
|
209
|
+
setPage(1);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const { data, totalCount, loading } = state;
|
|
213
|
+
const hasFilters = search || statusFilter.length > 0;
|
|
214
|
+
const isFirstUse = totalCount === 0 && !hasFilters && !loading;
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="space-y-4">
|
|
218
|
+
<div className="flex items-center justify-between">
|
|
219
|
+
<div>
|
|
220
|
+
<h2 className="text-lg font-semibold">Entities</h2>
|
|
221
|
+
<p className="text-sm text-muted-foreground">Manage your entity catalog.</p>
|
|
222
|
+
</div>
|
|
223
|
+
{!isFirstUse && (
|
|
224
|
+
<Button size="sm">
|
|
225
|
+
<PlusIcon />
|
|
226
|
+
Add entity
|
|
227
|
+
</Button>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{isFirstUse ? (
|
|
232
|
+
<div className="rounded-lg border">
|
|
233
|
+
<DataTableEmptyState
|
|
234
|
+
variant="first-use"
|
|
235
|
+
icon={<GlobeIcon />}
|
|
236
|
+
title="No entities yet"
|
|
237
|
+
description="Get started by creating your first entity."
|
|
238
|
+
primaryAction={
|
|
239
|
+
<Button size="sm">
|
|
240
|
+
<PlusIcon />
|
|
241
|
+
Create entity
|
|
242
|
+
</Button>
|
|
243
|
+
}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
) : (
|
|
247
|
+
<DataTable
|
|
248
|
+
columns={columns}
|
|
249
|
+
data={data}
|
|
250
|
+
loading={loading}
|
|
251
|
+
skeletonCount={pageSize}
|
|
252
|
+
sorting={sorting}
|
|
253
|
+
onSortingChange={handleSortingChange}
|
|
254
|
+
enableColumnVisibility
|
|
255
|
+
serverSide
|
|
256
|
+
pagination={{
|
|
257
|
+
page,
|
|
258
|
+
pageSize,
|
|
259
|
+
totalCount,
|
|
260
|
+
pageSizeOptions: [10, 25, 50, 100],
|
|
261
|
+
onPageChange: setPage,
|
|
262
|
+
onPageSizeChange: (size) => {
|
|
263
|
+
setPageSize(size);
|
|
264
|
+
setPage(1);
|
|
265
|
+
},
|
|
266
|
+
}}
|
|
267
|
+
emptyMessage={
|
|
268
|
+
<DataTableEmptyState
|
|
269
|
+
variant="no-results"
|
|
270
|
+
icon={<SearchIcon />}
|
|
271
|
+
title="No entities match your search"
|
|
272
|
+
description="Try adjusting your search terms or clearing filters."
|
|
273
|
+
action={
|
|
274
|
+
<Button
|
|
275
|
+
size="sm"
|
|
276
|
+
variant="outline"
|
|
277
|
+
onClick={() => {
|
|
278
|
+
setSearch('');
|
|
279
|
+
setStatusFilter([]);
|
|
280
|
+
setPage(1);
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
Clear filters
|
|
284
|
+
</Button>
|
|
285
|
+
}
|
|
286
|
+
/>
|
|
287
|
+
}
|
|
288
|
+
toolbar={
|
|
289
|
+
<>
|
|
290
|
+
<Input
|
|
291
|
+
placeholder="Search..."
|
|
292
|
+
value={search}
|
|
293
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
294
|
+
className="h-8 w-64"
|
|
295
|
+
/>
|
|
296
|
+
<FacetedFilter
|
|
297
|
+
title="Status"
|
|
298
|
+
options={statusOptions}
|
|
299
|
+
selected={statusFilter}
|
|
300
|
+
onChange={handleStatusChange}
|
|
301
|
+
/>
|
|
302
|
+
{hasFilters && (
|
|
303
|
+
<Button
|
|
304
|
+
variant="ghost"
|
|
305
|
+
size="sm"
|
|
306
|
+
onClick={() => {
|
|
307
|
+
setSearch('');
|
|
308
|
+
setStatusFilter([]);
|
|
309
|
+
setPage(1);
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
Reset
|
|
313
|
+
</Button>
|
|
314
|
+
)}
|
|
315
|
+
</>
|
|
316
|
+
}
|
|
317
|
+
/>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
File without changes
|
|
File without changes
|