Skip to content

Commit 6c9d5ec

Browse files
committed
PMM-10064 inventory list pagination (#481)
* PMM-10064 Add pagination to inventory list tables Replaces tables in inventory list (Services, Agents, Nodes) with component from core-ui and adds pagination to it, which should help with UI lag if there's a lot of instances added. * PMM-10064 Add inventory pagination tests * PMM-10064 bump core ui version * PMM-10064 Update inventory tests
1 parent 91857d1 commit 6c9d5ec

File tree

7 files changed

+186
-46
lines changed

7 files changed

+186
-46
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@
275275
"@opentelemetry/api": "1.1.0",
276276
"@opentelemetry/exporter-collector": "0.25.0",
277277
"@opentelemetry/semantic-conventions": "1.5.0",
278-
"@percona/platform-core": "1.0.7",
278+
"@percona/platform-core": "1.0.8",
279279
"@popperjs/core": "2.11.5",
280280
"@react-aria/button": "3.5.1",
281281
"@react-aria/dialog": "3.2.1",

public/app/percona/inventory/Inventory.test.tsx

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { render } from '@testing-library/react';
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { Table } from '@percona/platform-core';
3+
import { render, waitFor } from '@testing-library/react';
24
import React from 'react';
35

4-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
5-
66
import { AGENTS_COLUMNS, NODES_COLUMNS, SERVICES_COLUMNS } from './Inventory.constants';
77
import { InventoryDataService } from './Inventory.tools';
88

9-
jest.mock('app/percona/settings/Settings.service');
9+
jest.mock('app/percona/settingsz/Settings.service');
1010

1111
// FIXME: types
1212
describe('Inventory tables', () => {
@@ -41,20 +41,68 @@ describe('Inventory tables', () => {
4141
},
4242
],
4343
};
44+
const data = InventoryDataService.getAgentModel(response as any);
4445

4546
const { container } = render(
4647
<Table
47-
data={InventoryDataService.getAgentModel(response as any)}
48-
rowKey={(rec) => rec.agent_id}
48+
data={data}
49+
totalItems={data.length}
50+
rowSelection
4951
columns={AGENTS_COLUMNS}
50-
loading={false}
52+
pendingRequest={false}
53+
showPagination
54+
pageSize={25}
5155
/>
5256
);
5357

5458
// length is 5 because header is also tr
5559
expect(container.querySelectorAll('tr')).toHaveLength(5);
5660
});
5761

62+
it('Agents table only first page', async () => {
63+
const response = {
64+
pmm_agent: [{ agent_id: 'pmm-server', runs_on_node_id: 'pmm-server', connected: true }],
65+
node_exporter: new Array(50).fill({
66+
agent_id: '/agent_id/262189d8-e10f-41c2-b0ae-73cc76be6968',
67+
pmm_agent_id: 'pmm-server',
68+
status: 'RUNNING',
69+
listen_port: 42000,
70+
}),
71+
postgres_exporter: new Array(50).fill({
72+
agent_id: '/agent_id/8b74c54e-4307-4a10-9a6f-1646215cbe07',
73+
pmm_agent_id: 'pmm-server',
74+
service_id: '/service_id/ab477624-4ee9-49cd-8bd4-9bf3b91628b2',
75+
username: 'pmm-managed',
76+
status: 'RUNNING',
77+
listen_port: 42001,
78+
}),
79+
80+
qan_postgresql_pgstatements_agent: new Array(50).fill({
81+
agent_id: '/agent_id/ac55153c-5211-4072-a5de-59eb2a136a5c',
82+
pmm_agent_id: 'pmm-server',
83+
service_id: '/service_id/ab477624-4ee9-49cd-8bd4-9bf3b91628b2',
84+
username: 'pmm-managed',
85+
status: 'RUNNING',
86+
}),
87+
};
88+
const data = InventoryDataService.getAgentModel(response as any);
89+
90+
const { container } = render(
91+
<Table
92+
data={data}
93+
totalItems={data.length}
94+
rowSelection
95+
columns={AGENTS_COLUMNS}
96+
pendingRequest={false}
97+
showPagination
98+
pageSize={25}
99+
/>
100+
);
101+
102+
// default page size is 25
103+
await waitFor(() => expect(container.querySelectorAll('tbody tr')).toHaveLength(25));
104+
});
105+
58106
it('Services table renders correct with right data', () => {
59107
const response = {
60108
postgresql: [
@@ -67,36 +115,92 @@ describe('Inventory tables', () => {
67115
},
68116
],
69117
};
118+
const data = InventoryDataService.getServiceModel(response as any);
70119
const { container } = render(
71120
<Table
72-
data={InventoryDataService.getServiceModel(response as any)}
73-
rowKey={(rec) => rec.service_id}
121+
data={data}
122+
totalItems={data.length}
74123
columns={SERVICES_COLUMNS}
75-
loading={false}
124+
pendingRequest={false}
125+
rowSelection
126+
showPagination
127+
pageSize={25}
76128
/>
77129
);
78130

79131
// length is 2 because header is also tr
80132
expect(container.querySelectorAll('tr')).toHaveLength(2);
81133
});
82134

135+
it('Services table renders only first page', async () => {
136+
const response = {
137+
postgresql: new Array(100).fill({
138+
service_id: '/service_id/ab477624-4ee9-49cd-8bd4-9bf3b91628b2',
139+
service_name: 'pmm-server-postgresql',
140+
node_id: 'pmm-server',
141+
address: '127.0.0.1',
142+
port: 5432,
143+
}),
144+
};
145+
const data = InventoryDataService.getServiceModel(response as any);
146+
const { container } = render(
147+
<Table
148+
data={data}
149+
totalItems={data.length}
150+
columns={SERVICES_COLUMNS}
151+
pendingRequest={false}
152+
rowSelection
153+
showPagination
154+
pageSize={25}
155+
/>
156+
);
157+
158+
// default page size is 25
159+
await waitFor(() => expect(container.querySelectorAll('tbody tr')).toHaveLength(25));
160+
});
161+
83162
it('Nodes table renders correct with right data', () => {
84163
const response = {
85164
generic: [
86165
{ node_id: 'pmm-server', node_name: 'pmm-server', address: '127.0.0.1' },
87166
{ node_id: 'pmm-server2', node_name: 'pmm-server2', address: '127.0.0.1' },
88167
],
89168
};
169+
const data = InventoryDataService.getNodeModel(response as any);
90170
const { container } = render(
91171
<Table
92-
data={InventoryDataService.getNodeModel(response as any)}
93-
rowKey={(rec) => rec.node_id}
172+
data={data}
173+
totalItems={data.length}
94174
columns={NODES_COLUMNS}
95-
loading={false}
175+
pendingRequest={false}
176+
rowSelection
177+
showPagination
178+
pageSize={25}
96179
/>
97180
);
98181

99182
// length is 3 because header is also tr
100183
expect(container.querySelectorAll('tr')).toHaveLength(3);
101184
});
185+
186+
it('Nodes table renders first page only', async () => {
187+
const response = {
188+
generic: new Array(100).fill({ node_id: 'pmm-server', node_name: 'pmm-server', address: '127.0.0.1' }),
189+
};
190+
const data = InventoryDataService.getNodeModel(response as any);
191+
const { container } = render(
192+
<Table
193+
data={data}
194+
totalItems={data.length}
195+
columns={NODES_COLUMNS}
196+
pendingRequest={false}
197+
rowSelection
198+
showPagination
199+
pageSize={25}
200+
/>
201+
);
202+
203+
// default page size is 25
204+
await waitFor(() => expect(container.querySelectorAll('tbody tr')).toHaveLength(25));
205+
});
102206
});

public/app/percona/inventory/Tabs/Agents.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { CheckboxField, logger } from '@percona/platform-core';
1+
/* eslint-disable @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-explicit-any */
2+
import { CheckboxField, Table, logger } from '@percona/platform-core';
23
import React, { useCallback, useEffect, useState } from 'react';
34
import { Form } from 'react-final-form';
45

@@ -8,7 +9,6 @@ import { OldPage } from 'app/core/components/Page/Page';
89
import { InventoryDataService, Model } from 'app/percona/inventory/Inventory.tools';
910
import { AgentsList } from 'app/percona/inventory/Inventory.types';
1011
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
11-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
1212
import { SelectedTableRows } from 'app/percona/shared/components/Elements/Table/Table.types';
1313
import { FormElement } from 'app/percona/shared/components/Form';
1414
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
@@ -31,7 +31,7 @@ export const Agents = () => {
3131
const [loading, setLoading] = useState(false);
3232
const [modalVisible, setModalVisible] = useState(false);
3333
const [data, setData] = useState<Model[]>([]);
34-
const [selected, setSelectedRows] = useState([]);
34+
const [selected, setSelectedRows] = useState<any[]>([]);
3535
const navModel = usePerconaNavModel('inventory-agents');
3636
const [generateToken] = useCancelToken();
3737

@@ -83,6 +83,10 @@ export const Agents = () => {
8383
[loadData]
8484
);
8585

86+
const handleSelectionChange = useCallback((rows: any[]) => {
87+
setSelectedRows(rows);
88+
}, []);
89+
8690
return (
8791
<OldPage navModel={navModel}>
8892
<OldPage.Contents>
@@ -151,13 +155,17 @@ export const Agents = () => {
151155
</Modal>
152156
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
153157
<Table
154-
className={styles.table}
155158
columns={AGENTS_COLUMNS}
156159
data={data}
160+
totalItems={data.length}
157161
rowSelection
158-
onRowSelection={(selected) => setSelectedRows(selected)}
159-
noData={<h1>No agents Available</h1>}
160-
loading={loading}
162+
onRowSelection={handleSelectionChange}
163+
showPagination
164+
pageSize={25}
165+
emptyMessage="No agents Available"
166+
emptyMessageClassName={styles.emptyMessage}
167+
pendingRequest={loading}
168+
overlayClassName={styles.overlay}
161169
/>
162170
</div>
163171
</div>

public/app/percona/inventory/Tabs/Nodes.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
/* eslint-disable @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-explicit-any */
2-
import { CheckboxField, logger } from '@percona/platform-core';
2+
import { CheckboxField, Table, logger } from '@percona/platform-core';
33
import React, { useCallback, useEffect, useState } from 'react';
44
import { Form } from 'react-final-form';
55

66
import { AppEvents } from '@grafana/data';
77
import { Button, HorizontalGroup, Modal } from '@grafana/ui';
88
import { OldPage } from 'app/core/components/Page/Page';
9-
import { InventoryDataService, Model } from 'app/percona/inventory/Inventory.tools';
109
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
11-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
12-
import { SelectedTableRows } from 'app/percona/shared/components/Elements/Table/Table.types';
10+
import { SelectedTableRows } from 'app/percona/shared/components/Elements/Table';
1311
import { FormElement } from 'app/percona/shared/components/Form';
1412
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
1513
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
@@ -19,6 +17,7 @@ import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpe
1917
import { appEvents } from '../../../core/app_events';
2018
import { GET_NODES_CANCEL_TOKEN, NODES_COLUMNS } from '../Inventory.constants';
2119
import { InventoryService } from '../Inventory.service';
20+
import { InventoryDataService, Model } from '../Inventory.tools';
2221
import { NodesList } from '../Inventory.types';
2322

2423
import { styles } from './Tabs.styles';
@@ -34,7 +33,7 @@ export const NodesTab = () => {
3433
const [loading, setLoading] = useState(false);
3534
const [data, setData] = useState<Model[]>([]);
3635
const [modalVisible, setModalVisible] = useState(false);
37-
const [selected, setSelectedRows] = useState([]);
36+
const [selected, setSelectedRows] = useState<any[]>([]);
3837
const navModel = usePerconaNavModel('inventory-nodes');
3938
const [generateToken] = useCancelToken();
4039

@@ -94,6 +93,10 @@ export const NodesTab = () => {
9493
[removeNodes, selected]
9594
);
9695

96+
const handleSelectionChange = useCallback((rows: any[]) => {
97+
setSelectedRows(rows);
98+
}, []);
99+
97100
return (
98101
<OldPage navModel={navModel}>
99102
<OldPage.Contents>
@@ -158,13 +161,17 @@ export const NodesTab = () => {
158161
</Modal>
159162
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
160163
<Table
161-
className={styles.table}
162164
columns={NODES_COLUMNS}
163165
data={data}
166+
totalItems={data.length}
164167
rowSelection
165-
onRowSelection={(selected) => setSelectedRows(selected)}
166-
noData={<h1>No nodes Available</h1>}
167-
loading={loading}
168+
onRowSelection={handleSelectionChange}
169+
showPagination
170+
pageSize={25}
171+
emptyMessage="No nodes Available"
172+
emptyMessageClassName={styles.emptyMessage}
173+
pendingRequest={loading}
174+
overlayClassName={styles.overlay}
168175
/>
169176
</div>
170177
</div>

public/app/percona/inventory/Tabs/Services.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { CheckboxField, logger } from '@percona/platform-core';
2-
import React, { useState, useCallback, useEffect } from 'react';
1+
/* eslint-disable @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-explicit-any */
2+
import { CheckboxField, logger, Table } from '@percona/platform-core';
3+
import React, { useCallback, useEffect, useState } from 'react';
34
import { Form } from 'react-final-form';
5+
import { Row } from 'react-table';
46

57
import { AppEvents } from '@grafana/data';
68
import { Button, HorizontalGroup, Modal } from '@grafana/ui';
79
import { OldPage } from 'app/core/components/Page/Page';
8-
import { InventoryDataService, Model } from 'app/percona/inventory/Inventory.tools';
910
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
10-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
11-
import { SelectedTableRows } from 'app/percona/shared/components/Elements/Table/Table.types';
11+
import { SelectedTableRows } from 'app/percona/shared/components/Elements/Table';
1212
import { FormElement } from 'app/percona/shared/components/Form';
1313
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
1414
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
@@ -18,6 +18,7 @@ import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpe
1818
import { appEvents } from '../../../core/app_events';
1919
import { GET_SERVICES_CANCEL_TOKEN, SERVICES_COLUMNS } from '../Inventory.constants';
2020
import { InventoryService } from '../Inventory.service';
21+
import { InventoryDataService, Model } from '../Inventory.tools';
2122
import { ServicesList } from '../Inventory.types';
2223

2324
import { styles } from './Tabs.styles';
@@ -35,7 +36,7 @@ export const Services = () => {
3536
const [loading, setLoading] = useState(false);
3637
const [modalVisible, setModalVisible] = useState(false);
3738
const [data, setData] = useState<Model[]>([]);
38-
const [selected, setSelectedRows] = useState([]);
39+
const [selected, setSelectedRows] = useState<any[]>([]);
3940
const navModel = usePerconaNavModel('inventory-services');
4041
const [generateToken] = useCancelToken();
4142

@@ -86,6 +87,10 @@ export const Services = () => {
8687
[loadData]
8788
);
8889

90+
const handleSelectionChange = useCallback((rows: Array<Row<{}>>) => {
91+
setSelectedRows(rows);
92+
}, []);
93+
8994
return (
9095
<OldPage navModel={navModel}>
9196
<OldPage.Contents>
@@ -153,13 +158,17 @@ export const Services = () => {
153158
</Modal>
154159
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
155160
<Table
156-
className={styles.table}
157161
columns={SERVICES_COLUMNS}
158162
data={data}
163+
totalItems={data.length}
159164
rowSelection
160-
onRowSelection={(selected) => setSelectedRows(selected)}
161-
noData={<h1>No services Available</h1>}
162-
loading={loading}
165+
onRowSelection={handleSelectionChange}
166+
showPagination
167+
pageSize={25}
168+
emptyMessage="No services Available"
169+
emptyMessageClassName={styles.emptyMessage}
170+
pendingRequest={loading}
171+
overlayClassName={styles.overlay}
163172
/>
164173
</div>
165174
</div>

0 commit comments

Comments
 (0)