jazz-tools 0.18.31 → 0.18.33
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/.svelte-kit/__package__/media/image.svelte +1 -1
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +1 -1
- package/.turbo/turbo-build.log +58 -58
- package/CHANGELOG.md +23 -0
- package/dist/{chunk-6BIYT3KH.js → chunk-OSQ7S47Q.js} +3 -3
- package/dist/chunk-OSQ7S47Q.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/inspector/{custom-element-RQTLPAPJ.js → custom-element-RBBL46TI.js} +636 -193
- package/dist/inspector/custom-element-RBBL46TI.js.map +1 -0
- package/dist/inspector/index.js +626 -183
- package/dist/inspector/index.js.map +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/tests/ui/data-table.test.d.ts +2 -0
- package/dist/inspector/tests/ui/data-table.test.d.ts.map +1 -0
- package/dist/inspector/tests/viewer/history-view.test.d.ts +2 -0
- package/dist/inspector/tests/viewer/history-view.test.d.ts.map +1 -0
- package/dist/inspector/ui/data-table.d.ts +23 -0
- package/dist/inspector/ui/data-table.d.ts.map +1 -0
- package/dist/inspector/ui/index.d.ts +1 -0
- package/dist/inspector/ui/index.d.ts.map +1 -1
- package/dist/inspector/viewer/history-view.d.ts +6 -0
- package/dist/inspector/viewer/history-view.d.ts.map +1 -0
- package/dist/inspector/viewer/page.d.ts.map +1 -1
- package/dist/svelte/media/image.svelte +1 -1
- package/dist/svelte/tests/media/image.svelte.test.js +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/coFeed.d.ts +1 -1
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +2 -2
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/dist/worker/index.d.ts +2 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +2 -1
- package/dist/worker/index.js.map +1 -1
- package/package.json +4 -4
- package/src/inspector/tests/ui/data-table.test.tsx +296 -0
- package/src/inspector/tests/viewer/history-view.test.tsx +246 -0
- package/src/inspector/ui/data-table.tsx +265 -0
- package/src/inspector/ui/index.ts +1 -0
- package/src/inspector/viewer/history-view.tsx +379 -0
- package/src/inspector/viewer/page.tsx +2 -0
- package/src/media/create-image-factory.test.ts +2 -2
- package/src/svelte/media/image.svelte +1 -1
- package/src/svelte/tests/media/image.svelte.test.ts +1 -1
- package/src/tools/coValues/coFeed.ts +2 -2
- package/src/tools/coValues/interfaces.ts +1 -1
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +1 -1
- package/src/tools/tests/CoValueCoreSubscription.test.ts +1 -1
- package/src/tools/tests/coFeed.test.ts +1 -1
- package/src/tools/tests/load.test.ts +1 -1
- package/src/worker/index.ts +9 -1
- package/dist/chunk-6BIYT3KH.js.map +0 -1
- package/dist/inspector/custom-element-RQTLPAPJ.js.map +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
3
|
+
import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
|
|
4
|
+
import { co, z } from "jazz-tools";
|
|
5
|
+
import {
|
|
6
|
+
cleanup,
|
|
7
|
+
fireEvent,
|
|
8
|
+
render,
|
|
9
|
+
screen,
|
|
10
|
+
waitFor,
|
|
11
|
+
} from "@testing-library/react";
|
|
12
|
+
import { HistoryView } from "../../viewer/history-view";
|
|
13
|
+
import { setup } from "goober";
|
|
14
|
+
import React from "react";
|
|
15
|
+
|
|
16
|
+
function extractAction(row: HTMLElement | null | undefined) {
|
|
17
|
+
if (!row) return "";
|
|
18
|
+
// index 0: author, index 1: action, index 2: timestamp
|
|
19
|
+
return row.querySelectorAll("td")?.[1]?.textContent ?? "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractActions(): string[] {
|
|
23
|
+
// slice 2 to skip header and filters
|
|
24
|
+
return screen.getAllByRole("row").slice(2).map(extractAction);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("HistoryView", async () => {
|
|
28
|
+
const account = await setupJazzTestSync();
|
|
29
|
+
const account2 = await createJazzTestAccount();
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
// setup goober
|
|
33
|
+
setup(React.createElement);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
cleanup();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should render a history card", async () => {
|
|
41
|
+
const value = co
|
|
42
|
+
.map({
|
|
43
|
+
foo: z.string(),
|
|
44
|
+
})
|
|
45
|
+
.create({ foo: "bar" }, account);
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(
|
|
52
|
+
screen.getAllByText('Property "foo" has been set to "bar"'),
|
|
53
|
+
).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("co.map", () => {
|
|
57
|
+
it("should render co.map changes", async () => {
|
|
58
|
+
const value = co
|
|
59
|
+
.map({
|
|
60
|
+
pet: z.string(),
|
|
61
|
+
age: z.number(),
|
|
62
|
+
certified: z.boolean().optional(),
|
|
63
|
+
})
|
|
64
|
+
.create({ pet: "dog", age: 10, certified: false }, account);
|
|
65
|
+
|
|
66
|
+
value.$jazz.set("pet", "cat");
|
|
67
|
+
value.$jazz.set("age", 20);
|
|
68
|
+
value.$jazz.set("certified", true);
|
|
69
|
+
value.$jazz.delete("certified");
|
|
70
|
+
|
|
71
|
+
render(
|
|
72
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const history = [
|
|
76
|
+
'Property "pet" has been set to "dog"',
|
|
77
|
+
'Property "age" has been set to "10"',
|
|
78
|
+
'Property "certified" has been set to "false"',
|
|
79
|
+
'Property "pet" has been set to "cat"',
|
|
80
|
+
'Property "age" has been set to "20"',
|
|
81
|
+
'Property "certified" has been set to "true"',
|
|
82
|
+
'Property "certified" has been deleted',
|
|
83
|
+
].toReversed(); // Default sort is descending
|
|
84
|
+
|
|
85
|
+
expect(screen.getAllByRole("row")).toHaveLength(history.length + 2);
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(screen.getAllByRole("row")[2]?.textContent).toContain(
|
|
89
|
+
account.$jazz.id,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(extractActions()).toEqual(history);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("co.list", () => {
|
|
98
|
+
it("should render simple co.list changes", async () => {
|
|
99
|
+
const value = co.list(z.string()).create(["dog", "cat"], account);
|
|
100
|
+
|
|
101
|
+
value.$jazz.push("bird");
|
|
102
|
+
|
|
103
|
+
value.$jazz.splice(1, 0, "fish");
|
|
104
|
+
|
|
105
|
+
value.$jazz.shift();
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const history = [
|
|
112
|
+
'"dog" has been appended',
|
|
113
|
+
'"cat" has been appended',
|
|
114
|
+
'"bird" has been inserted after "cat"',
|
|
115
|
+
'"fish" has been inserted after "dog"',
|
|
116
|
+
'"dog" has been deleted',
|
|
117
|
+
].toReversed(); // Default sort is descending
|
|
118
|
+
|
|
119
|
+
expect(extractActions()).toEqual(history);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should render changes of a co.list of co.maps", async () => {
|
|
123
|
+
const Animal = co.map({
|
|
124
|
+
pet: z.string(),
|
|
125
|
+
age: z.number(),
|
|
126
|
+
certified: z.boolean(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const dog = Animal.create(
|
|
130
|
+
{ pet: "dog", age: 10, certified: false },
|
|
131
|
+
account,
|
|
132
|
+
);
|
|
133
|
+
const cat = Animal.create(
|
|
134
|
+
{ pet: "cat", age: 20, certified: true },
|
|
135
|
+
account,
|
|
136
|
+
);
|
|
137
|
+
const fish = Animal.create(
|
|
138
|
+
{ pet: "fish", age: 30, certified: false },
|
|
139
|
+
account,
|
|
140
|
+
);
|
|
141
|
+
const bird = Animal.create(
|
|
142
|
+
{ pet: "bird", age: 40, certified: true },
|
|
143
|
+
account,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const value = co.list(Animal).create([dog, cat], account);
|
|
147
|
+
|
|
148
|
+
value.$jazz.push(bird);
|
|
149
|
+
|
|
150
|
+
value.$jazz.splice(1, 0, fish);
|
|
151
|
+
|
|
152
|
+
value.$jazz.shift();
|
|
153
|
+
|
|
154
|
+
render(
|
|
155
|
+
<HistoryView coValue={value.$jazz.raw} node={value.$jazz.localNode} />,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const history = [
|
|
159
|
+
`"${dog.$jazz.id}" has been appended`,
|
|
160
|
+
`"${cat.$jazz.id}" has been appended`,
|
|
161
|
+
`"${bird.$jazz.id}" has been inserted after "${cat.$jazz.id}"`,
|
|
162
|
+
`"${fish.$jazz.id}" has been inserted after "${dog.$jazz.id}"`,
|
|
163
|
+
`"${dog.$jazz.id}" has been deleted`,
|
|
164
|
+
].toReversed(); // Default sort is descending
|
|
165
|
+
|
|
166
|
+
expect(extractActions()).toEqual(history);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("co.group", () => {
|
|
171
|
+
it("should render co.group changes", async () => {
|
|
172
|
+
const group = co.group().create(account);
|
|
173
|
+
|
|
174
|
+
const group2 = co.group().create(account);
|
|
175
|
+
|
|
176
|
+
group.addMember(group2, "writer");
|
|
177
|
+
|
|
178
|
+
group.addMember(account2, "reader");
|
|
179
|
+
group.removeMember(account2);
|
|
180
|
+
|
|
181
|
+
const group3 = co.group().create(account);
|
|
182
|
+
group3.addMember(group, "inherit");
|
|
183
|
+
|
|
184
|
+
const { container } = render(
|
|
185
|
+
<HistoryView coValue={group.$jazz.raw} node={group.$jazz.localNode} />,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const history = [
|
|
189
|
+
`${account.$jazz.id} has been promoted to admin`,
|
|
190
|
+
expect.stringContaining(` has been revealed to `), // key revelation
|
|
191
|
+
expect.stringContaining('Property "readKey" has been set to'),
|
|
192
|
+
`Group ${group2.$jazz.id} has been promoted to writer`,
|
|
193
|
+
expect.stringContaining(" has been revealed to"),
|
|
194
|
+
`${account2.$jazz.id} has been promoted to reader`,
|
|
195
|
+
expect.stringContaining(" has been revealed to"),
|
|
196
|
+
// Member revocation: key rotation
|
|
197
|
+
expect.stringContaining(" has been revealed to"),
|
|
198
|
+
expect.stringContaining(" has been revealed to"),
|
|
199
|
+
expect.stringContaining('Property "readKey" has been set to'),
|
|
200
|
+
expect.stringContaining(" has been revealed to"),
|
|
201
|
+
`${account2.$jazz.id} has been revoked`,
|
|
202
|
+
|
|
203
|
+
// Group extension
|
|
204
|
+
// `Group become a member of ${group3.$jazz.id}`,
|
|
205
|
+
].toReversed(); // Default sort is descending
|
|
206
|
+
|
|
207
|
+
const historyPage1 = history.slice(0, 10);
|
|
208
|
+
const historyPage2 = history.slice(10, 20);
|
|
209
|
+
|
|
210
|
+
// Page 1: 10 rows
|
|
211
|
+
expect(extractActions()).toEqual(historyPage1);
|
|
212
|
+
|
|
213
|
+
// Go to page 2
|
|
214
|
+
fireEvent.click(screen.getByText("»"));
|
|
215
|
+
|
|
216
|
+
// Page 2: 3 rows
|
|
217
|
+
expect(extractActions()).toEqual(historyPage2);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("co.account", () => {
|
|
222
|
+
it("should render co.account changes", async () => {
|
|
223
|
+
const account = await createJazzTestAccount({
|
|
224
|
+
creationProps: {
|
|
225
|
+
name: "John Doe",
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const history = [
|
|
230
|
+
expect.stringContaining(' has been set to "admin"'),
|
|
231
|
+
expect.stringContaining(" has been revealed to "),
|
|
232
|
+
expect.stringContaining('Property "readKey" has been set to '),
|
|
233
|
+
`Property "profile" has been set to "${account.profile!.$jazz.id}"`,
|
|
234
|
+
].toReversed(); // Default sort is descending
|
|
235
|
+
|
|
236
|
+
render(
|
|
237
|
+
<HistoryView
|
|
238
|
+
coValue={account.$jazz.raw}
|
|
239
|
+
node={account.$jazz.localNode}
|
|
240
|
+
/>,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(extractActions()).toEqual(history);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableBody,
|
|
5
|
+
TableCell,
|
|
6
|
+
TableHead,
|
|
7
|
+
TableHeader,
|
|
8
|
+
TableRow,
|
|
9
|
+
} from "./table";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
import { Input } from "./input";
|
|
12
|
+
|
|
13
|
+
export type ColumnDef<T> = {
|
|
14
|
+
id: string;
|
|
15
|
+
header: string;
|
|
16
|
+
accessor: (row: T) => React.ReactNode;
|
|
17
|
+
sortable?: boolean;
|
|
18
|
+
filterable?: boolean;
|
|
19
|
+
sortFn?: (a: T, b: T) => number;
|
|
20
|
+
filterFn?: (row: T, filterValue: string) => boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type SortConfig = {
|
|
24
|
+
columnId: string;
|
|
25
|
+
direction: "asc" | "desc";
|
|
26
|
+
} | null;
|
|
27
|
+
|
|
28
|
+
export type DataTableProps<T> = {
|
|
29
|
+
columns: ColumnDef<T>[];
|
|
30
|
+
data: T[];
|
|
31
|
+
pageSize?: number;
|
|
32
|
+
initialSort?: SortConfig;
|
|
33
|
+
getRowKey: (row: T, index: number) => string;
|
|
34
|
+
emptyMessage?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function DataTable<T>({
|
|
38
|
+
columns,
|
|
39
|
+
data,
|
|
40
|
+
pageSize = 10,
|
|
41
|
+
initialSort = null,
|
|
42
|
+
getRowKey,
|
|
43
|
+
emptyMessage = "No data available",
|
|
44
|
+
}: DataTableProps<T>) {
|
|
45
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
46
|
+
const [sortConfig, setSortConfig] = useState<SortConfig>(initialSort);
|
|
47
|
+
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
48
|
+
|
|
49
|
+
// Apply filtering
|
|
50
|
+
const filteredData = useMemo(() => {
|
|
51
|
+
return data.filter((row) => {
|
|
52
|
+
return Object.entries(filters).every(([columnId, filterValue]) => {
|
|
53
|
+
if (!filterValue) return true;
|
|
54
|
+
|
|
55
|
+
const column = columns.find((col) => col.id === columnId);
|
|
56
|
+
if (!column?.filterable) return true;
|
|
57
|
+
|
|
58
|
+
if (column.filterFn) {
|
|
59
|
+
return column.filterFn(row, filterValue);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Default filter: convert to string and check inclusion
|
|
63
|
+
const cellValue = String(column.accessor(row));
|
|
64
|
+
return cellValue.toLowerCase().includes(filterValue.toLowerCase());
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}, [data, filters, columns]);
|
|
68
|
+
|
|
69
|
+
// Apply sorting
|
|
70
|
+
const sortedData = useMemo(() => {
|
|
71
|
+
if (!sortConfig) return filteredData;
|
|
72
|
+
|
|
73
|
+
const column = columns.find((col) => col.id === sortConfig.columnId);
|
|
74
|
+
if (!column?.sortable) return filteredData;
|
|
75
|
+
|
|
76
|
+
const sorted = [...filteredData].sort((a, b) => {
|
|
77
|
+
if (column.sortFn) {
|
|
78
|
+
return column.sortFn(a, b);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Default sort: compare string values
|
|
82
|
+
const aValue = String(column.accessor(a));
|
|
83
|
+
const bValue = String(column.accessor(b));
|
|
84
|
+
return aValue.localeCompare(bValue);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return sortConfig.direction === "desc" ? sorted.reverse() : sorted;
|
|
88
|
+
}, [filteredData, sortConfig, columns]);
|
|
89
|
+
|
|
90
|
+
// Calculate pagination
|
|
91
|
+
const totalPages = Math.ceil(sortedData.length / pageSize);
|
|
92
|
+
const showPagination = sortedData.length > pageSize;
|
|
93
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
94
|
+
const endIndex = startIndex + pageSize;
|
|
95
|
+
const paginatedData = sortedData.slice(startIndex, endIndex);
|
|
96
|
+
|
|
97
|
+
// Reset to page 1 when filters change
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setCurrentPage(1);
|
|
100
|
+
}, [filters]);
|
|
101
|
+
|
|
102
|
+
const handleSort = (columnId: string) => {
|
|
103
|
+
const column = columns.find((col) => col.id === columnId);
|
|
104
|
+
if (!column?.sortable) return;
|
|
105
|
+
|
|
106
|
+
setSortConfig((current) => {
|
|
107
|
+
if (current?.columnId === columnId) {
|
|
108
|
+
if (current.direction === "asc") {
|
|
109
|
+
return { columnId, direction: "desc" };
|
|
110
|
+
}
|
|
111
|
+
return null; // Remove sorting
|
|
112
|
+
}
|
|
113
|
+
return { columnId, direction: "asc" };
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleFilterChange = (columnId: string, value: string) => {
|
|
118
|
+
setFilters((current) => ({
|
|
119
|
+
...current,
|
|
120
|
+
[columnId]: value,
|
|
121
|
+
}));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handlePageChange = (page: number) => {
|
|
125
|
+
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
<Table>
|
|
131
|
+
<TableHead>
|
|
132
|
+
<TableRow>
|
|
133
|
+
{columns.map((column) => (
|
|
134
|
+
<TableHeader key={column.id}>
|
|
135
|
+
<div
|
|
136
|
+
style={{
|
|
137
|
+
display: "flex",
|
|
138
|
+
alignItems: "center",
|
|
139
|
+
gap: "8px",
|
|
140
|
+
cursor: column.sortable ? "pointer" : "default",
|
|
141
|
+
}}
|
|
142
|
+
onClick={() => handleSort(column.id)}
|
|
143
|
+
>
|
|
144
|
+
<span>{column.header}</span>
|
|
145
|
+
{column.sortable && (
|
|
146
|
+
<span
|
|
147
|
+
style={{
|
|
148
|
+
fontSize: "12px",
|
|
149
|
+
opacity: 0.7,
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{sortConfig?.columnId === column.id
|
|
153
|
+
? sortConfig.direction === "asc"
|
|
154
|
+
? "↑"
|
|
155
|
+
: "↓"
|
|
156
|
+
: "↕"}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</TableHeader>
|
|
161
|
+
))}
|
|
162
|
+
</TableRow>
|
|
163
|
+
</TableHead>
|
|
164
|
+
<TableBody>
|
|
165
|
+
{columns.some((column) => column.filterable) && (
|
|
166
|
+
<TableRow>
|
|
167
|
+
{columns.map((column) => (
|
|
168
|
+
<TableCell key={column.id}>
|
|
169
|
+
{column.filterable && (
|
|
170
|
+
<Input
|
|
171
|
+
label="Filter"
|
|
172
|
+
hideLabel
|
|
173
|
+
type="search"
|
|
174
|
+
placeholder={`Filter ${column.header.toLowerCase()}`}
|
|
175
|
+
value={filters[column.id] || ""}
|
|
176
|
+
onChange={(e) =>
|
|
177
|
+
handleFilterChange(column.id, e.target.value)
|
|
178
|
+
}
|
|
179
|
+
onClick={(e) => e.stopPropagation()}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
</TableCell>
|
|
183
|
+
))}
|
|
184
|
+
</TableRow>
|
|
185
|
+
)}
|
|
186
|
+
{paginatedData.length === 0 ? (
|
|
187
|
+
<TableRow>
|
|
188
|
+
<TableCell colSpan={columns.length}>
|
|
189
|
+
<div
|
|
190
|
+
style={{
|
|
191
|
+
textAlign: "center",
|
|
192
|
+
padding: "20px",
|
|
193
|
+
opacity: 0.6,
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
{emptyMessage}
|
|
197
|
+
</div>
|
|
198
|
+
</TableCell>
|
|
199
|
+
</TableRow>
|
|
200
|
+
) : (
|
|
201
|
+
paginatedData.map((row, index) => (
|
|
202
|
+
<TableRow key={getRowKey(row, startIndex + index)}>
|
|
203
|
+
{columns.map((column) => (
|
|
204
|
+
<TableCell key={column.id}>{column.accessor(row)}</TableCell>
|
|
205
|
+
))}
|
|
206
|
+
</TableRow>
|
|
207
|
+
))
|
|
208
|
+
)}
|
|
209
|
+
</TableBody>
|
|
210
|
+
</Table>
|
|
211
|
+
|
|
212
|
+
{showPagination && (
|
|
213
|
+
<div
|
|
214
|
+
style={{
|
|
215
|
+
display: "flex",
|
|
216
|
+
justifyContent: "space-between",
|
|
217
|
+
alignItems: "center",
|
|
218
|
+
marginTop: "16px",
|
|
219
|
+
padding: "8px 0",
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
<div style={{ fontSize: "14px", opacity: 0.7 }}>
|
|
223
|
+
Showing {startIndex + 1} to {Math.min(endIndex, sortedData.length)}{" "}
|
|
224
|
+
of {sortedData.length} entries
|
|
225
|
+
{Object.keys(filters).some((key) => filters[key]) &&
|
|
226
|
+
` (filtered from ${data.length})`}
|
|
227
|
+
</div>
|
|
228
|
+
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
|
229
|
+
<Button
|
|
230
|
+
variant="secondary"
|
|
231
|
+
onClick={() => handlePageChange(1)}
|
|
232
|
+
disabled={currentPage === 1}
|
|
233
|
+
>
|
|
234
|
+
««
|
|
235
|
+
</Button>
|
|
236
|
+
<Button
|
|
237
|
+
variant="secondary"
|
|
238
|
+
onClick={() => handlePageChange(currentPage - 1)}
|
|
239
|
+
disabled={currentPage === 1}
|
|
240
|
+
>
|
|
241
|
+
«
|
|
242
|
+
</Button>
|
|
243
|
+
<span style={{ fontSize: "14px" }}>
|
|
244
|
+
Page {currentPage} of {totalPages}
|
|
245
|
+
</span>
|
|
246
|
+
<Button
|
|
247
|
+
variant="secondary"
|
|
248
|
+
onClick={() => handlePageChange(currentPage + 1)}
|
|
249
|
+
disabled={currentPage === totalPages}
|
|
250
|
+
>
|
|
251
|
+
»
|
|
252
|
+
</Button>
|
|
253
|
+
<Button
|
|
254
|
+
variant="secondary"
|
|
255
|
+
onClick={() => handlePageChange(totalPages)}
|
|
256
|
+
disabled={currentPage === totalPages}
|
|
257
|
+
>
|
|
258
|
+
»»
|
|
259
|
+
</Button>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</>
|
|
264
|
+
);
|
|
265
|
+
}
|