argent-grid 0.1.0 → 0.2.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/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +2 -2
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- package/e2e/screenshots.spec.ts +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +91 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/package.json +20 -6
- package/plan.md +50 -18
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +327 -76
- package/src/lib/components/argent-grid.component.html +186 -64
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +642 -189
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.ts +302 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +366 -0
- package/src/lib/rendering/canvas-renderer.ts +418 -305
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +46 -24
- package/src/lib/rendering/render/column-utils.ts +73 -0
- package/src/lib/rendering/render/hit-test.ts +55 -0
- package/src/lib/rendering/render/index.ts +79 -76
- package/src/lib/rendering/render/lines.ts +43 -43
- package/src/lib/rendering/render/primitives.ts +161 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +2 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +60 -50
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1165 -201
- package/src/lib/services/grid.service.ts +819 -187
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +73 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +188 -0
- package/src/stories/ArgentGrid.stories.ts +277 -0
- package/src/stories/Benchmark.stories.ts +74 -0
- package/src/stories/CellRenderers.stories.ts +221 -0
- package/src/stories/Filtering.stories.ts +252 -0
- package/src/stories/Grouping.stories.ts +217 -0
- package/src/stories/Theming.stories.ts +124 -0
- package/src/stories/benchmark-wrapper.component.ts +315 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { BrowserModule } from '@angular/platform-browser';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/angular';
|
|
3
|
+
import { moduleMetadata } from '@storybook/angular';
|
|
4
|
+
import { ArgentGridComponent, ArgentGridModule, themeQuartz } from '../public-api';
|
|
5
|
+
|
|
6
|
+
interface Employee {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
department: string;
|
|
10
|
+
role: string;
|
|
11
|
+
salary: number;
|
|
12
|
+
location: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meta: Meta<ArgentGridComponent<Employee>> = {
|
|
16
|
+
title: 'Features/Grouping',
|
|
17
|
+
component: ArgentGridComponent,
|
|
18
|
+
decorators: [
|
|
19
|
+
moduleMetadata({
|
|
20
|
+
imports: [ArgentGridModule, BrowserModule],
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
parameters: {
|
|
24
|
+
layout: 'fullscreen',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<ArgentGridComponent<Employee>>;
|
|
30
|
+
|
|
31
|
+
function generateStaticData(count: number): Employee[] {
|
|
32
|
+
const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'];
|
|
33
|
+
const roles = ['Engineer', 'Manager', 'Director', 'VP', 'Intern'];
|
|
34
|
+
const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Remote'];
|
|
35
|
+
|
|
36
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
37
|
+
id: i + 1,
|
|
38
|
+
name: `Employee ${i + 1}`,
|
|
39
|
+
department: departments[i % departments.length],
|
|
40
|
+
role: roles[i % roles.length],
|
|
41
|
+
salary: 50000 + i * 1000,
|
|
42
|
+
location: locations[i % locations.length],
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const RowGrouping: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
columnDefs: [
|
|
49
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
50
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
51
|
+
{
|
|
52
|
+
field: 'department',
|
|
53
|
+
headerName: 'Department 📁',
|
|
54
|
+
width: 180,
|
|
55
|
+
rowGroup: true,
|
|
56
|
+
headerComponentParams: { groupIcon: '📁' },
|
|
57
|
+
},
|
|
58
|
+
{ field: 'role', headerName: 'Role', width: 250 },
|
|
59
|
+
{ field: 'salary', headerName: 'Salary', width: 120 },
|
|
60
|
+
{ field: 'location', headerName: 'Location', width: 150 },
|
|
61
|
+
],
|
|
62
|
+
rowData: generateStaticData(100),
|
|
63
|
+
height: '500px',
|
|
64
|
+
width: '100%',
|
|
65
|
+
theme: themeQuartz,
|
|
66
|
+
gridOptions: {
|
|
67
|
+
autoGroupColumnDef: {
|
|
68
|
+
headerName: 'Organization 📁',
|
|
69
|
+
width: 250,
|
|
70
|
+
pinned: 'left',
|
|
71
|
+
},
|
|
72
|
+
groupDefaultExpanded: 1, // Expand first level by default
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
parameters: {
|
|
76
|
+
docs: {
|
|
77
|
+
description: {
|
|
78
|
+
story:
|
|
79
|
+
'**Row grouping by Department** column. Grouped columns show a 📁 icon. Click the **▶ expand/collapse arrows** in the group rows to show/hide items. **Drag column headers** to the left panel to group by multiple columns.',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const MultiLevelGrouping: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
columnDefs: [
|
|
88
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
89
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
90
|
+
{
|
|
91
|
+
field: 'department',
|
|
92
|
+
headerName: 'Department 📁',
|
|
93
|
+
width: 150,
|
|
94
|
+
rowGroup: true,
|
|
95
|
+
headerComponentParams: { groupIcon: '📁' },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
field: 'location',
|
|
99
|
+
headerName: 'Location 📁',
|
|
100
|
+
width: 150,
|
|
101
|
+
rowGroup: true,
|
|
102
|
+
headerComponentParams: { groupIcon: '📁' },
|
|
103
|
+
},
|
|
104
|
+
{ field: 'role', headerName: 'Role', width: 250 },
|
|
105
|
+
{ field: 'salary', headerName: 'Salary', width: 120 },
|
|
106
|
+
],
|
|
107
|
+
rowData: generateStaticData(100),
|
|
108
|
+
height: '500px',
|
|
109
|
+
width: '100%',
|
|
110
|
+
theme: themeQuartz,
|
|
111
|
+
gridOptions: {
|
|
112
|
+
autoGroupColumnDef: {
|
|
113
|
+
headerName: 'Organization 📁📁',
|
|
114
|
+
width: 300,
|
|
115
|
+
pinned: 'left',
|
|
116
|
+
},
|
|
117
|
+
groupDefaultExpanded: 2, // Expand first 2 levels by default
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
parameters: {
|
|
121
|
+
docs: {
|
|
122
|
+
description: {
|
|
123
|
+
story:
|
|
124
|
+
'**Multi-level grouping** by Department AND Location. Each grouped column shows a 📁 icon. Groups are **expanded by default** to show the hierarchy. Click **▶ arrows** to collapse/expand. Notice the nested structure: Department → Location → Employees.',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const GroupingWithAggregation: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
columnDefs: [
|
|
133
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
134
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
135
|
+
{
|
|
136
|
+
field: 'department',
|
|
137
|
+
headerName: 'Department 📁',
|
|
138
|
+
width: 180,
|
|
139
|
+
rowGroup: true,
|
|
140
|
+
headerComponentParams: { groupIcon: '📁' },
|
|
141
|
+
},
|
|
142
|
+
{ field: 'role', headerName: 'Role', width: 250 },
|
|
143
|
+
{
|
|
144
|
+
field: 'salary',
|
|
145
|
+
headerName: 'Salary 💰',
|
|
146
|
+
width: 120,
|
|
147
|
+
aggFunc: 'sum', // Show sum in group rows
|
|
148
|
+
},
|
|
149
|
+
{ field: 'location', headerName: 'Location', width: 150 },
|
|
150
|
+
],
|
|
151
|
+
rowData: generateStaticData(100),
|
|
152
|
+
height: '500px',
|
|
153
|
+
width: '100%',
|
|
154
|
+
theme: themeQuartz,
|
|
155
|
+
gridOptions: {
|
|
156
|
+
autoGroupColumnDef: {
|
|
157
|
+
headerName: 'Organization 📁',
|
|
158
|
+
width: 250,
|
|
159
|
+
pinned: 'left',
|
|
160
|
+
},
|
|
161
|
+
groupDefaultExpanded: 1,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
parameters: {
|
|
165
|
+
docs: {
|
|
166
|
+
description: {
|
|
167
|
+
story:
|
|
168
|
+
'**Grouping with aggregation**. Salary column shows **sum (💰)** for each department group. Look at the group rows to see aggregated values. Supported aggregations: sum, avg, min, max, count.',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const DragAndDropGrouping: Story = {
|
|
175
|
+
args: {
|
|
176
|
+
columnDefs: [
|
|
177
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
178
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
179
|
+
{
|
|
180
|
+
field: 'department',
|
|
181
|
+
headerName: 'Department 📁 (drag me!)',
|
|
182
|
+
width: 180,
|
|
183
|
+
rowGroup: false, // Not pre-grouped - user can drag
|
|
184
|
+
headerComponentParams: { draggable: true },
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
field: 'location',
|
|
188
|
+
headerName: 'Location 📁 (drag me!)',
|
|
189
|
+
width: 150,
|
|
190
|
+
rowGroup: false,
|
|
191
|
+
headerComponentParams: { draggable: true },
|
|
192
|
+
},
|
|
193
|
+
{ field: 'role', headerName: 'Role', width: 250 },
|
|
194
|
+
{ field: 'salary', headerName: 'Salary', width: 120 },
|
|
195
|
+
],
|
|
196
|
+
rowData: generateStaticData(100),
|
|
197
|
+
height: '500px',
|
|
198
|
+
width: '100%',
|
|
199
|
+
theme: themeQuartz,
|
|
200
|
+
gridOptions: {
|
|
201
|
+
rowGroupPanelShow: 'always', // Always show group panel
|
|
202
|
+
autoGroupColumnDef: {
|
|
203
|
+
headerName: 'Groups 📁',
|
|
204
|
+
width: 250,
|
|
205
|
+
pinned: 'left',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
parameters: {
|
|
210
|
+
docs: {
|
|
211
|
+
description: {
|
|
212
|
+
story:
|
|
213
|
+
'**Drag-and-drop grouping**. See the **"Drag me!"** labels? **Drag column headers** (Department, Location) to the **"Groups" panel on the left** to create groups dynamically. Drop columns back to ungroup. Multiple columns can be grouped at once.',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { BrowserModule } from '@angular/platform-browser';
|
|
2
|
+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/angular';
|
|
4
|
+
import { moduleMetadata } from '@storybook/angular';
|
|
5
|
+
import { ArgentGridComponent, ArgentGridModule, colorSchemeDark, themeQuartz } from '../public-api';
|
|
6
|
+
|
|
7
|
+
interface Employee {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
department: string;
|
|
11
|
+
role: string;
|
|
12
|
+
salary: number;
|
|
13
|
+
location: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const meta: Meta<ArgentGridComponent<Employee>> = {
|
|
17
|
+
title: 'Features/Theming',
|
|
18
|
+
component: ArgentGridComponent,
|
|
19
|
+
decorators: [
|
|
20
|
+
moduleMetadata({
|
|
21
|
+
imports: [ArgentGridModule, BrowserModule, BrowserAnimationsModule],
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
parameters: {
|
|
25
|
+
layout: 'fullscreen',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<ArgentGridComponent<Employee>>;
|
|
31
|
+
|
|
32
|
+
function generateStaticData(count: number): Employee[] {
|
|
33
|
+
const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance'];
|
|
34
|
+
const roles = ['Engineer', 'Manager', 'Director', 'VP', 'Intern'];
|
|
35
|
+
const locations = ['New York', 'San Francisco', 'London', 'Singapore', 'Remote'];
|
|
36
|
+
|
|
37
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
38
|
+
id: i + 1,
|
|
39
|
+
name: `Employee ${i + 1}`,
|
|
40
|
+
department: departments[i % departments.length],
|
|
41
|
+
role: roles[i % roles.length],
|
|
42
|
+
salary: 50000 + i * 1000,
|
|
43
|
+
location: locations[i % locations.length],
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const columnDefs = [
|
|
48
|
+
{ field: 'id', headerName: 'ID', width: 80 },
|
|
49
|
+
{ field: 'name', headerName: 'Name', width: 200 },
|
|
50
|
+
{ field: 'department', headerName: 'Department', width: 180 },
|
|
51
|
+
{ field: 'role', headerName: 'Role', width: 250 },
|
|
52
|
+
{ field: 'salary', headerName: 'Salary', width: 120 },
|
|
53
|
+
{ field: 'location', headerName: 'Location', width: 150 },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export const LightMode: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
columnDefs,
|
|
59
|
+
rowData: generateStaticData(50),
|
|
60
|
+
height: '400px',
|
|
61
|
+
width: '100%',
|
|
62
|
+
theme: themeQuartz,
|
|
63
|
+
},
|
|
64
|
+
parameters: {
|
|
65
|
+
docs: {
|
|
66
|
+
description: {
|
|
67
|
+
story: 'Default light theme using Quartz theme.',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const DarkMode: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
columnDefs,
|
|
76
|
+
rowData: generateStaticData(50),
|
|
77
|
+
height: '400px',
|
|
78
|
+
width: '100%',
|
|
79
|
+
theme: themeQuartz.withPart(colorSchemeDark),
|
|
80
|
+
},
|
|
81
|
+
parameters: {
|
|
82
|
+
docs: {
|
|
83
|
+
description: {
|
|
84
|
+
story: 'Dark mode using Quartz theme with dark color scheme.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const CompactMode: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
columnDefs,
|
|
93
|
+
rowData: generateStaticData(50),
|
|
94
|
+
height: '400px',
|
|
95
|
+
width: '100%',
|
|
96
|
+
theme: themeQuartz.withParams({ rowHeight: 32, fontSize: 12, spacing: 4 }),
|
|
97
|
+
},
|
|
98
|
+
parameters: {
|
|
99
|
+
docs: {
|
|
100
|
+
description: {
|
|
101
|
+
story: 'Compact mode with smaller row height and font size.',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const CompactDarkMode: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
columnDefs,
|
|
110
|
+
rowData: generateStaticData(50),
|
|
111
|
+
height: '400px',
|
|
112
|
+
width: '100%',
|
|
113
|
+
theme: themeQuartz
|
|
114
|
+
.withParams({ rowHeight: 32, fontSize: 12, spacing: 4 })
|
|
115
|
+
.withPart(colorSchemeDark),
|
|
116
|
+
},
|
|
117
|
+
parameters: {
|
|
118
|
+
docs: {
|
|
119
|
+
description: {
|
|
120
|
+
story: 'Combined compact and dark mode for dense data display.',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { AfterViewInit, Component, Input, OnDestroy, ViewChild } from '@angular/core';
|
|
3
|
+
import { ArgentGridComponent, ArgentGridModule, ColDef, GridApi, themeQuartz } from '../public-api';
|
|
4
|
+
|
|
5
|
+
interface Employee {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
department: string;
|
|
9
|
+
role: string;
|
|
10
|
+
salary: number;
|
|
11
|
+
location: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'app-benchmark-wrapper',
|
|
16
|
+
standalone: true,
|
|
17
|
+
imports: [CommonModule, ArgentGridModule],
|
|
18
|
+
template: `
|
|
19
|
+
<div class="benchmark-container">
|
|
20
|
+
<div class="controls">
|
|
21
|
+
<button (click)="runBenchmark()" [disabled]="isRunning">
|
|
22
|
+
{{ isRunning ? 'Running...' : 'Run Benchmark' }}
|
|
23
|
+
</button>
|
|
24
|
+
<button (click)="reloadData()">Reload Data</button>
|
|
25
|
+
<span class="row-count">{{ rowCount | number }} rows</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="results" *ngIf="results">
|
|
29
|
+
<div class="result-item">
|
|
30
|
+
<span class="label">Initial Render:</span>
|
|
31
|
+
<span class="value">{{ results.initialRender }}ms</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="result-item">
|
|
34
|
+
<span class="label">Selection Update:</span>
|
|
35
|
+
<span class="value">{{ results.selectionUpdateTime }}ms</span>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="result-item">
|
|
38
|
+
<span class="label">Grouping Toggle:</span>
|
|
39
|
+
<span class="value">{{ results.groupingUpdateTime }}ms</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="result-item">
|
|
42
|
+
<span class="label">Avg Scroll Frame:</span>
|
|
43
|
+
<span class="value">{{ results.scrollFrameAverage }}ms</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="result-item total">
|
|
46
|
+
<span class="label">Total:</span>
|
|
47
|
+
<span class="value">{{ results.totalTime }}ms</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<argent-grid
|
|
52
|
+
#grid
|
|
53
|
+
[columnDefs]="columnDefs"
|
|
54
|
+
[rowData]="rowData"
|
|
55
|
+
[height]="height"
|
|
56
|
+
[width]="width"
|
|
57
|
+
[theme]="theme"
|
|
58
|
+
[gridOptions]="gridOptions"
|
|
59
|
+
(gridReady)="onGridReady($event)"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
`,
|
|
63
|
+
styles: [
|
|
64
|
+
`
|
|
65
|
+
.benchmark-container {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 12px;
|
|
69
|
+
}
|
|
70
|
+
.controls {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
align-items: center;
|
|
74
|
+
}
|
|
75
|
+
.controls button {
|
|
76
|
+
padding: 8px 16px;
|
|
77
|
+
background: #3b82f6;
|
|
78
|
+
color: white;
|
|
79
|
+
border: none;
|
|
80
|
+
border-radius: 4px;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
font-size: 14px;
|
|
83
|
+
}
|
|
84
|
+
.controls button:disabled {
|
|
85
|
+
background: #9ca3af;
|
|
86
|
+
cursor: not-allowed;
|
|
87
|
+
}
|
|
88
|
+
.controls button:hover:not(:disabled) {
|
|
89
|
+
background: #2563eb;
|
|
90
|
+
}
|
|
91
|
+
.row-count {
|
|
92
|
+
margin-left: auto;
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
color: #6b7280;
|
|
95
|
+
}
|
|
96
|
+
.results {
|
|
97
|
+
display: flex;
|
|
98
|
+
gap: 16px;
|
|
99
|
+
padding: 12px;
|
|
100
|
+
background: #f9fafb;
|
|
101
|
+
border-radius: 4px;
|
|
102
|
+
flex-wrap: wrap;
|
|
103
|
+
}
|
|
104
|
+
.result-item {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 4px;
|
|
108
|
+
}
|
|
109
|
+
.result-item .label {
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: #6b7280;
|
|
112
|
+
}
|
|
113
|
+
.result-item .value {
|
|
114
|
+
font-size: 18px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
color: #111827;
|
|
117
|
+
}
|
|
118
|
+
.result-item.total .value {
|
|
119
|
+
color: #059669;
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
],
|
|
123
|
+
})
|
|
124
|
+
export class BenchmarkWrapperComponent implements AfterViewInit, OnDestroy {
|
|
125
|
+
@ViewChild('grid') gridComponent!: ArgentGridComponent;
|
|
126
|
+
|
|
127
|
+
@Input() rowCount = 10000;
|
|
128
|
+
|
|
129
|
+
columnDefs: ColDef<Employee>[] = [
|
|
130
|
+
{ field: 'id', headerName: 'ID', width: 80, sortable: true },
|
|
131
|
+
{ field: 'name', headerName: 'Name', width: 200, sortable: true },
|
|
132
|
+
{ field: 'department', headerName: 'Department', width: 180, sortable: true },
|
|
133
|
+
{ field: 'role', headerName: 'Role', width: 250, filter: true },
|
|
134
|
+
{ field: 'salary', headerName: 'Salary', width: 120, sortable: true, filter: 'number' },
|
|
135
|
+
{ field: 'location', headerName: 'Location', width: 150, filter: true },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
rowData: Employee[] = [];
|
|
139
|
+
height = '500px';
|
|
140
|
+
width = '100%';
|
|
141
|
+
theme = themeQuartz;
|
|
142
|
+
|
|
143
|
+
gridOptions = {
|
|
144
|
+
floatingFilter: true,
|
|
145
|
+
enableRangeSelection: true,
|
|
146
|
+
defaultColDef: {
|
|
147
|
+
filter: true,
|
|
148
|
+
sortable: true,
|
|
149
|
+
resizable: true,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
private gridApi?: GridApi<Employee>;
|
|
154
|
+
private fpsInterval?: number;
|
|
155
|
+
|
|
156
|
+
isRunning = false;
|
|
157
|
+
results: {
|
|
158
|
+
initialRender: number;
|
|
159
|
+
selectionUpdateTime: number;
|
|
160
|
+
groupingUpdateTime: number;
|
|
161
|
+
scrollFrameAverage: number;
|
|
162
|
+
totalTime: number;
|
|
163
|
+
} | null = null;
|
|
164
|
+
|
|
165
|
+
ngAfterViewInit(): void {
|
|
166
|
+
this.reloadData();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
generateData(count: number): Employee[] {
|
|
170
|
+
const departments = [
|
|
171
|
+
'Engineering',
|
|
172
|
+
'Sales',
|
|
173
|
+
'Marketing',
|
|
174
|
+
'HR',
|
|
175
|
+
'Finance',
|
|
176
|
+
'Operations',
|
|
177
|
+
'Support',
|
|
178
|
+
];
|
|
179
|
+
const roles = ['Software Engineer', 'Manager', 'Director', 'VP', 'Intern', 'Analyst', 'Lead'];
|
|
180
|
+
const locations = [
|
|
181
|
+
'New York',
|
|
182
|
+
'San Francisco',
|
|
183
|
+
'London',
|
|
184
|
+
'Singapore',
|
|
185
|
+
'Remote',
|
|
186
|
+
'Berlin',
|
|
187
|
+
'Tokyo',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
191
|
+
id: i + 1,
|
|
192
|
+
name: `Employee ${i + 1}`,
|
|
193
|
+
department: departments[Math.floor(Math.random() * departments.length)],
|
|
194
|
+
role: roles[Math.floor(Math.random() * roles.length)],
|
|
195
|
+
salary: Math.floor(Math.random() * 150000) + 50000,
|
|
196
|
+
location: locations[Math.floor(Math.random() * locations.length)],
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
reloadData(): void {
|
|
201
|
+
this.rowData = this.generateData(this.rowCount);
|
|
202
|
+
this.results = null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onGridReady(api: GridApi<Employee>): void {
|
|
206
|
+
this.gridApi = api;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
runBenchmark(): void {
|
|
210
|
+
if (!this.gridApi || !this.gridComponent || this.isRunning) return;
|
|
211
|
+
|
|
212
|
+
this.isRunning = true;
|
|
213
|
+
this.results = null;
|
|
214
|
+
|
|
215
|
+
const results = {
|
|
216
|
+
initialRender: 0,
|
|
217
|
+
selectionUpdateTime: 0,
|
|
218
|
+
groupingUpdateTime: 0,
|
|
219
|
+
scrollFrameAverage: 0,
|
|
220
|
+
totalTime: 0,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const startTime = performance.now();
|
|
224
|
+
|
|
225
|
+
// 1. Initial render time
|
|
226
|
+
results.initialRender = this.gridComponent.getLastFrameTime();
|
|
227
|
+
|
|
228
|
+
// 2. Selection Update Time
|
|
229
|
+
const selStart = performance.now();
|
|
230
|
+
this.gridApi.selectAll();
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
results.selectionUpdateTime = Number((performance.now() - selStart).toFixed(2));
|
|
233
|
+
this.gridApi?.deselectAll();
|
|
234
|
+
|
|
235
|
+
// 3. Grouping Toggle Time
|
|
236
|
+
const groupStart = performance.now();
|
|
237
|
+
const colDefs = this.gridApi.getColumnDefs() as ColDef<Employee>[];
|
|
238
|
+
const deptCol = colDefs.find((c) => c.field === 'department');
|
|
239
|
+
const wasGrouped = (deptCol as ColDef<Employee>)?.rowGroup;
|
|
240
|
+
|
|
241
|
+
const newColDefs = colDefs.map((col: ColDef<Employee>) => {
|
|
242
|
+
if (col.field === 'department') {
|
|
243
|
+
return { ...col, rowGroup: !wasGrouped };
|
|
244
|
+
}
|
|
245
|
+
return col;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
this.gridApi.setColumnDefs(newColDefs);
|
|
249
|
+
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
// Toggle back
|
|
252
|
+
const revertColDefs = newColDefs.map((col: ColDef<Employee>) => {
|
|
253
|
+
if (col.field === 'department') {
|
|
254
|
+
return { ...col, rowGroup: wasGrouped ?? false };
|
|
255
|
+
}
|
|
256
|
+
return col;
|
|
257
|
+
});
|
|
258
|
+
this.gridApi.setColumnDefs(revertColDefs);
|
|
259
|
+
|
|
260
|
+
results.groupingUpdateTime = Number((performance.now() - groupStart).toFixed(2));
|
|
261
|
+
|
|
262
|
+
// 4. Scroll Test
|
|
263
|
+
this.runScrollTest(results, startTime);
|
|
264
|
+
}, 100);
|
|
265
|
+
}, 100);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private runScrollTest(results: typeof this.results, startTime: number): void {
|
|
269
|
+
const frameTimes: number[] = [];
|
|
270
|
+
let scrollCount = 0;
|
|
271
|
+
const totalScrollFrames = 30;
|
|
272
|
+
const viewport = this.gridComponent.viewportRef?.nativeElement;
|
|
273
|
+
|
|
274
|
+
if (!viewport) {
|
|
275
|
+
this.finishBenchmark(results, startTime);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const runScroll = () => {
|
|
280
|
+
if (scrollCount < totalScrollFrames) {
|
|
281
|
+
viewport.scrollTop += 100;
|
|
282
|
+
frameTimes.push(this.gridComponent.getLastFrameTime());
|
|
283
|
+
scrollCount++;
|
|
284
|
+
requestAnimationFrame(runScroll);
|
|
285
|
+
} else {
|
|
286
|
+
results.scrollFrameAverage = Number(
|
|
287
|
+
(frameTimes.reduce((a, b) => a + b, 0) / frameTimes.length).toFixed(2)
|
|
288
|
+
);
|
|
289
|
+
this.finishBenchmark(results, startTime);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Reset scroll position first
|
|
294
|
+
viewport.scrollTop = 0;
|
|
295
|
+
setTimeout(() => requestAnimationFrame(runScroll), 100);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private finishBenchmark(results: typeof this.results, startTime: number): void {
|
|
299
|
+
results.totalTime = Number((performance.now() - startTime).toFixed(2));
|
|
300
|
+
this.results = results;
|
|
301
|
+
this.isRunning = false;
|
|
302
|
+
|
|
303
|
+
// Reset scroll
|
|
304
|
+
const viewport = this.gridComponent.viewportRef?.nativeElement;
|
|
305
|
+
if (viewport) {
|
|
306
|
+
viewport.scrollTop = 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
ngOnDestroy(): void {
|
|
311
|
+
if (this.fpsInterval) {
|
|
312
|
+
cancelAnimationFrame(this.fpsInterval);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
package/vitest.config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
4
|
|
|
5
5
|
export default defineConfig({
|
|
6
6
|
plugins: [
|
|
@@ -20,7 +20,7 @@ export default defineConfig({
|
|
|
20
20
|
}
|
|
21
21
|
return match;
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
newCode = newCode.replace(/styleUrls:\s*\[\s*['"]([^'"]+)['"]\s*\]/g, (match, p1) => {
|
|
25
25
|
const stylePath = path.resolve(path.dirname(id), p1);
|
|
26
26
|
if (fs.existsSync(stylePath)) {
|
|
@@ -33,11 +33,11 @@ export default defineConfig({
|
|
|
33
33
|
}
|
|
34
34
|
return match;
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
return { code: newCode, map: null };
|
|
38
38
|
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
41
|
],
|
|
42
42
|
test: {
|
|
43
43
|
globals: true,
|
|
@@ -48,8 +48,8 @@ export default defineConfig({
|
|
|
48
48
|
provider: 'v8',
|
|
49
49
|
reporter: ['text', 'json', 'html'],
|
|
50
50
|
include: ['src/**/*.ts'],
|
|
51
|
-
exclude: ['src/**/*.spec.ts', 'src/**/types/**']
|
|
51
|
+
exclude: ['src/**/*.spec.ts', 'src/**/types/**'],
|
|
52
52
|
},
|
|
53
|
-
testTimeout: 10000
|
|
54
|
-
}
|
|
55
|
-
});
|
|
53
|
+
testTimeout: 10000,
|
|
54
|
+
},
|
|
55
|
+
});
|