Skip to content

Commit 119bcdf

Browse files
authored
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 183d9e7 commit 119bcdf

File tree

7 files changed

+177
-39
lines changed

7 files changed

+177
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@
255255
"@opentelemetry/api": "1.0.2",
256256
"@opentelemetry/exporter-collector": "0.23.0",
257257
"@opentelemetry/semantic-conventions": "1.0.0",
258-
"@percona/platform-core": "1.0.7",
258+
"@percona/platform-core": "1.0.8",
259259
"@popperjs/core": "2.11.0",
260260
"@react-aria/button": "3.3.4",
261261
"@react-aria/focus": "3.5.0",

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

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
2-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
2+
import { Table } from '@percona/platform-core';
33
import { AGENTS_COLUMNS, NODES_COLUMNS, SERVICES_COLUMNS } from './Inventory.constants';
44
import { InventoryDataService } from './Inventory.tools';
5-
import { render } from '@testing-library/react';
5+
import { render, waitFor } from '@testing-library/react';
66

77
jest.mock('app/percona/settings/Settings.service');
88

@@ -39,20 +39,68 @@ describe('Inventory tables', () => {
3939
},
4040
],
4141
};
42+
const data = InventoryDataService.getAgentModel(response as any);
4243

4344
const { container } = render(
4445
<Table
45-
data={InventoryDataService.getAgentModel(response as any)}
46-
rowKey={(rec) => rec.agent_id}
46+
data={data}
47+
totalItems={data.length}
48+
rowSelection
4749
columns={AGENTS_COLUMNS}
48-
loading={false}
50+
pendingRequest={false}
51+
showPagination
52+
pageSize={25}
4953
/>
5054
);
5155

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

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

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

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

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

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { useCallback, useEffect, useState } from 'react';
22
import { Button, HorizontalGroup, Modal } from '@grafana/ui';
3-
import { CheckboxField, logger } from '@percona/platform-core';
3+
import { CheckboxField, Table, logger } from '@percona/platform-core';
44
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
55
import { isApiCancelError } from 'app/percona/shared/helpers/api';
66
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
77
import Page from 'app/core/components/Page/Page';
88
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
99
import { Form } from 'react-final-form';
10-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
1110
import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpers/promises';
1211
import { InventoryDataService } from 'app/percona/inventory/Inventory.tools';
1312
import { FormElement } from 'app/percona/shared/components/Form';
@@ -28,7 +27,7 @@ export const Agents = () => {
2827
const [loading, setLoading] = useState(false);
2928
const [modalVisible, setModalVisible] = useState(false);
3029
const [data, setData] = useState<any[]>([]);
31-
const [selected, setSelectedRows] = useState([]);
30+
const [selected, setSelectedRows] = useState<any[]>([]);
3231
const navModel = usePerconaNavModel('inventory-agents');
3332
const [generateToken] = useCancelToken();
3433

@@ -80,6 +79,10 @@ export const Agents = () => {
8079
[loadData]
8180
);
8281

82+
const handleSelectionChange = useCallback((rows: any[]) => {
83+
setSelectedRows(rows);
84+
}, []);
85+
8386
return (
8487
<Page navModel={navModel}>
8588
<Page.Contents>
@@ -148,13 +151,17 @@ export const Agents = () => {
148151
</Modal>
149152
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
150153
<Table
151-
className={styles.table}
152154
columns={AGENTS_COLUMNS}
153155
data={data}
156+
totalItems={data.length}
154157
rowSelection
155-
onRowSelection={(selected) => setSelectedRows(selected)}
156-
noData={<h1>No agents Available</h1>}
157-
loading={loading}
158+
onRowSelection={handleSelectionChange}
159+
showPagination
160+
pageSize={25}
161+
emptyMessage="No agents Available"
162+
emptyMessageClassName={styles.emptyMessage}
163+
pendingRequest={loading}
164+
overlayClassName={styles.overlay}
158165
/>
159166
</div>
160167
</div>

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { useCallback, useEffect, useState } from 'react';
22
import { Button, HorizontalGroup, Modal } from '@grafana/ui';
3-
import { CheckboxField, logger } from '@percona/platform-core';
3+
import { CheckboxField, Table, logger } from '@percona/platform-core';
44
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
55
import { isApiCancelError } from 'app/percona/shared/helpers/api';
66
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
77
import Page from 'app/core/components/Page/Page';
88
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
99
import { Form } from 'react-final-form';
10-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
1110
import { FormElement } from 'app/percona/shared/components/Form';
1211
import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpers/promises';
1312
import { InventoryDataService } from 'app/percona/inventory/Inventory.tools';
@@ -30,7 +29,7 @@ export const NodesTab = () => {
3029
const [loading, setLoading] = useState(false);
3130
const [data, setData] = useState<any[]>([]);
3231
const [modalVisible, setModalVisible] = useState(false);
33-
const [selected, setSelectedRows] = useState([]);
32+
const [selected, setSelectedRows] = useState<any[]>([]);
3433
const navModel = usePerconaNavModel('inventory-nodes');
3534
const [generateToken] = useCancelToken();
3635

@@ -90,6 +89,10 @@ export const NodesTab = () => {
9089
[removeNodes, selected]
9190
);
9291

92+
const handleSelectionChange = useCallback((rows: any[]) => {
93+
setSelectedRows(rows);
94+
}, []);
95+
9396
return (
9497
<Page navModel={navModel}>
9598
<Page.Contents>
@@ -154,13 +157,17 @@ export const NodesTab = () => {
154157
</Modal>
155158
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
156159
<Table
157-
className={styles.table}
158160
columns={NODES_COLUMNS}
159161
data={data}
162+
totalItems={data.length}
160163
rowSelection
161-
onRowSelection={(selected) => setSelectedRows(selected)}
162-
noData={<h1>No nodes Available</h1>}
163-
loading={loading}
164+
onRowSelection={handleSelectionChange}
165+
showPagination
166+
pageSize={25}
167+
emptyMessage="No nodes Available"
168+
emptyMessageClassName={styles.emptyMessage}
169+
pendingRequest={loading}
170+
overlayClassName={styles.overlay}
164171
/>
165172
</div>
166173
</div>

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { useCallback, useEffect, useState } from 'react';
22
import { Button, HorizontalGroup, Modal } from '@grafana/ui';
3-
import { CheckboxField, logger } from '@percona/platform-core';
3+
import { CheckboxField, logger, Table } from '@percona/platform-core';
44
import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel';
55
import Page from 'app/core/components/Page/Page';
66
import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader';
77
import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook';
88
import { isApiCancelError } from 'app/percona/shared/helpers/api';
99
import { Form } from 'react-final-form';
10-
import { Table } from 'app/percona/shared/components/Elements/Table/Table';
1110
import { FormElement } from 'app/percona/shared/components/Form';
1211
import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpers/promises';
1312
import { InventoryDataService } from 'app/percona/inventory/Inventory.tools';
@@ -18,6 +17,7 @@ import { GET_SERVICES_CANCEL_TOKEN, SERVICES_COLUMNS } from '../Inventory.consta
1817
import { styles } from './Tabs.styles';
1918
import { appEvents } from '../../../core/app_events';
2019
import { AppEvents } from '@grafana/data';
20+
import { Row } from 'react-table';
2121

2222
interface Service {
2323
service_id: string;
@@ -32,7 +32,7 @@ export const Services = () => {
3232
const [loading, setLoading] = useState(false);
3333
const [modalVisible, setModalVisible] = useState(false);
3434
const [data, setData] = useState<any[]>([]);
35-
const [selected, setSelectedRows] = useState([]);
35+
const [selected, setSelectedRows] = useState<any[]>([]);
3636
const navModel = usePerconaNavModel('inventory-services');
3737
const [generateToken] = useCancelToken();
3838

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

86+
const handleSelectionChange = useCallback((rows: Array<Row<{}>>) => {
87+
setSelectedRows(rows);
88+
}, []);
89+
8690
return (
8791
<Page navModel={navModel}>
8892
<Page.Contents>
@@ -150,13 +154,17 @@ export const Services = () => {
150154
</Modal>
151155
<div className={styles.tableInnerWrapper} data-testid="table-inner-wrapper">
152156
<Table
153-
className={styles.table}
154157
columns={SERVICES_COLUMNS}
155158
data={data}
159+
totalItems={data.length}
156160
rowSelection
157-
onRowSelection={(selected) => setSelectedRows(selected)}
158-
noData={<h1>No services Available</h1>}
159-
loading={loading}
161+
onRowSelection={handleSelectionChange}
162+
showPagination
163+
pageSize={25}
164+
emptyMessage="No services Available"
165+
emptyMessageClassName={styles.emptyMessage}
166+
pendingRequest={loading}
167+
overlayClassName={styles.overlay}
160168
/>
161169
</div>
162170
</div>

public/app/percona/inventory/Tabs/Tabs.styles.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export const styles = {
2929
`,
3030
tableInnerWrapper: css`
3131
flex: 1;
32-
overflow: hidden;
3332
`,
3433
destructiveButton: css`
3534
background: rgba(0, 0, 0, 0) linear-gradient(rgb(224, 47, 68) 0%, rgb(196, 22, 42) 100%) repeat scroll 0% 0%;
@@ -38,4 +37,17 @@ export const styles = {
3837
confirmationText: css`
3938
margin-bottom: 2em;
4039
`,
40+
emptyMessage: css`
41+
height: 160px;
42+
display: flex;
43+
justify-content: center;
44+
align-items: center;
45+
`,
46+
overlay: css`
47+
height: 160px;
48+
display: flex;
49+
justify-content: center;
50+
align-items: center;
51+
background-color: transparent;
52+
`,
4153
};

0 commit comments

Comments
 (0)