Skip to content

Commit 682a41a

Browse files
authored
Refresh look of cluster pods admin tab (#1583)
Summary: Adds summary info, cleans up presentation. Before... <img width="1224" alt="image" src="https://github.com/pixie-io/pixie/assets/314133/8cd4b437-745b-4676-9dac-5f831c5832a3"> After (with no pod selected)... <img width="1346" alt="image" src="https://github.com/pixie-io/pixie/assets/314133/feffc183-d8ac-4b4c-a4c2-d56e16191707"> And with one selected... <img width="1345" alt="image" src="https://github.com/pixie-io/pixie/assets/314133/7c543af1-06e6-433d-aaff-68b096529613"> Relevant Issues: #1174 Type of change: /kind cleanup Test Plan: Go to `/admin/clusters`. Click a cluster. Go to the Pixie Pods tab. --------- Signed-off-by: Nick Lanam <nlanam@pixielabs.ai>
1 parent 065ed1e commit 682a41a

File tree

3 files changed

+585
-159
lines changed

3 files changed

+585
-159
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright 2018- The Pixie Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
*/
18+
19+
import * as React from 'react';
20+
21+
import { ThemeProvider } from '@mui/material/styles';
22+
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
23+
24+
import { DARK_THEME } from 'app/components';
25+
import { GQLPodStatus } from 'app/types/schema';
26+
27+
import { PixiePodsTab } from './cluster-details-pods';
28+
29+
/* eslint-disable react-memo/require-memo, react-memo/require-usememo */
30+
31+
const healthyCont = 'CONTAINER_STATE_RUNNING';
32+
33+
const mockControlPlanePods: GQLPodStatus[] = [
34+
{
35+
name: 'control-pod-foo',
36+
status: 'RUNNING',
37+
message: '',
38+
reason: '',
39+
createdAtMs: 0,
40+
restartCount: 0,
41+
events: [
42+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Something probably good happened' },
43+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Mundane, boring, typical events.' },
44+
],
45+
containers: [
46+
{ name: 'control-pod-foo-foo', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
47+
{ name: 'control-pod-foo-bar', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
48+
],
49+
},
50+
{
51+
name: 'control-pod-bar',
52+
status: 'FAILED',
53+
message: '',
54+
reason: '',
55+
createdAtMs: 0,
56+
restartCount: 0,
57+
events: [
58+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Something probably good happened' },
59+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Mundane, boring, typical events.' },
60+
],
61+
containers: [],
62+
},
63+
];
64+
65+
const mockDataPlanePods: GQLPodStatus[] = [
66+
{
67+
name: 'data-pod-foo',
68+
status: 'PENDING',
69+
message: 'Disconnected',
70+
reason: 'Because I, a unit test, said so. That is within my power. Fear me.',
71+
createdAtMs: 0,
72+
restartCount: 1_000,
73+
events: [
74+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Bad news' },
75+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Worse news' },
76+
],
77+
containers: [
78+
{ name: 'data-pod-foo-foo', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
79+
{ name: 'data-pod-foo-bar', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
80+
],
81+
},
82+
{
83+
name: 'data-pod-bar',
84+
status: 'UNKNOWN',
85+
message: 'Disconnected',
86+
reason: 'Because I, a unit test, said so. That is within my power. Fear me.',
87+
createdAtMs: 0,
88+
restartCount: 1_000,
89+
events: [
90+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Bad news' },
91+
{ firstTimeMs: 0, lastTimeMs: 1, message: 'Worse news' },
92+
],
93+
containers: [
94+
{ name: 'data-pod-bar-foo', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
95+
{ name: 'data-pod-bar-bar', message: '', reason: '', createdAtMs: 0, restartCount: 0, state: healthyCont },
96+
],
97+
},
98+
];
99+
100+
const Tester = ({ control, data }: { control: GQLPodStatus[], data: GQLPodStatus[] }) => (
101+
<ThemeProvider theme={DARK_THEME}>
102+
<PixiePodsTab controlPlanePods={control} dataPlanePods={data} />
103+
</ThemeProvider>
104+
);
105+
106+
const setup = async ({ control, data }: { control?: GQLPodStatus[], data?: GQLPodStatus[] } = {}) => {
107+
let container: HTMLElement;
108+
await act(async () => {
109+
({ container } = render(
110+
<Tester
111+
control={control === undefined ? mockControlPlanePods : control}
112+
data={data === undefined ? mockDataPlanePods : data}
113+
/>,
114+
));
115+
});
116+
await waitFor(() => {});
117+
return container;
118+
};
119+
120+
describe('<PixiePodsTab />', () => {
121+
it('renders both control plane and data plane tables', async () => {
122+
await setup();
123+
await screen.findByText('control-pod-foo');
124+
await screen.findByText('control-pod-bar');
125+
await screen.findByText('data-pod-foo');
126+
await screen.findByText('data-pod-bar');
127+
});
128+
129+
it('shows placeholder text when either group of pods is empty', async () => {
130+
const container = await setup({ control: [], data: [] });
131+
await screen.findByText('Cluster has no Pixie control plane pods.');
132+
await screen.findByText('Cluster has no unhealthy Pixie data plane pods.');
133+
expect(container.querySelectorAll('tbody tr').length).toBe(0);
134+
});
135+
136+
it('does not explode when props are missing', async () => {
137+
const container = await setup({ control: null, data: null });
138+
await screen.findByText('Cluster has no Pixie control plane pods.');
139+
await screen.findByText('Cluster has no unhealthy Pixie data plane pods.');
140+
expect(container.querySelectorAll('tbody tr').length).toBe(0);
141+
});
142+
143+
it('counts how many healthy/unhealthy pods of each group are present', async () => {
144+
const container = await setup();
145+
const hp = container.querySelectorAll('[aria-label="Healthy pods"]');
146+
const up = container.querySelectorAll('[aria-label="Unhealthy pods"]');
147+
148+
expect(hp.length).toBe(1);
149+
expect(up.length).toBe(2);
150+
await within(hp[0] as HTMLElement).findByText('1'); // Control plane healthy pods
151+
await within(up[0] as HTMLElement).findByText('1'); // Control plane unhealthy pods
152+
await within(up[1] as HTMLElement).findByText('2'); // Data plane unhealthy pods
153+
});
154+
155+
it('does not show details before a pod is clicked', async () => {
156+
await setup();
157+
await screen.findByTestId('cluster-details-pods-sidebar-hidden');
158+
expect(screen.queryByTestId('cluster-details-pods-sidebar')).toBeNull();
159+
});
160+
161+
it('shows events and containers within a pod when details are clicked', async () => {
162+
const container = await setup();
163+
await act(async () => {
164+
fireEvent.click(await within(container).findByText('control-pod-foo'));
165+
});
166+
const detailsContainer = await screen.findByTestId('cluster-details-pods-sidebar');
167+
expect(screen.queryByTestId('cluster-details-pods-sidebar-hidden')).toBeNull();
168+
await within(detailsContainer).findByText('control-pod-foo-foo');
169+
await within(detailsContainer).findByText('Something probably good happened');
170+
});
171+
172+
it('shows placeholder text when there are no events or no containers', async () => {
173+
const container = await setup({
174+
control: mockControlPlanePods.map(p => ({ ...p, containers: [], events: [] })),
175+
data: mockDataPlanePods.map(p => ({ ...p, containers: [], events: [] })),
176+
});
177+
await act(async () => {
178+
fireEvent.click(await within(container).findByText('control-pod-foo'));
179+
});
180+
const detailsContainer = await screen.findByTestId('cluster-details-pods-sidebar');
181+
expect(screen.queryByTestId('cluster-details-pods-sidebar-hidden')).toBeNull();
182+
await within(detailsContainer).findByText('No events to report.');
183+
await within(detailsContainer).findByText('No containers in this pod.');
184+
});
185+
186+
it('shows placeholder text when event and container data is absent', async () => {
187+
const container = await setup({
188+
control: mockControlPlanePods.map(p => ({ ...p, containers: null, events: null })),
189+
data: mockDataPlanePods.map(p => ({ ...p, containers: null, events: null })),
190+
});
191+
await act(async () => {
192+
fireEvent.click(await within(container).findByText('control-pod-foo'));
193+
});
194+
const detailsContainer = await screen.findByTestId('cluster-details-pods-sidebar');
195+
expect(screen.queryByTestId('cluster-details-pods-sidebar-hidden')).toBeNull();
196+
await within(detailsContainer).findByText('No events to report.');
197+
await within(detailsContainer).findByText('No containers in this pod.');
198+
});
199+
200+
it('switches which pod details to show when another is clicked', async () => {
201+
const container = await setup();
202+
await act(async () => {
203+
fireEvent.click(await within(container).findByText('control-pod-foo'));
204+
});
205+
await act(async () => {
206+
fireEvent.click(await within(container).findByText('data-pod-bar'));
207+
});
208+
const detailsContainer = await screen.findByTestId('cluster-details-pods-sidebar');
209+
expect(screen.queryByTestId('cluster-details-pods-sidebar-hidden')).toBeNull();
210+
await within(detailsContainer).findByText('data-pod-bar-foo');
211+
await within(detailsContainer).findByText('Bad news');
212+
});
213+
214+
it('stops showing details when the currently-shown row is clicked again', async () => {
215+
const container = await setup();
216+
217+
// Once for control plane
218+
await act(async () => {
219+
fireEvent.click(await within(container).findByText('control-pod-foo'));
220+
});
221+
await screen.findByTestId('cluster-details-pods-sidebar');
222+
await act(async () => {
223+
// We want the td, not the h1 in the details sidebar
224+
fireEvent.click((await within(container).findAllByText('control-pod-foo'))[0]);
225+
});
226+
await screen.findByTestId('cluster-details-pods-sidebar-hidden');
227+
expect(screen.queryByTestId('cluster-details-pods-sidebar')).toBeNull();
228+
229+
// And once for data plane (different branches for coverage)
230+
await act(async () => {
231+
fireEvent.click(await within(container).findByText('data-pod-foo'));
232+
});
233+
await screen.findByTestId('cluster-details-pods-sidebar');
234+
await act(async () => {
235+
// We want the td, not the h1 in the details sidebar
236+
fireEvent.click((await within(container).findAllByText('data-pod-foo'))[0]);
237+
});
238+
await screen.findByTestId('cluster-details-pods-sidebar-hidden');
239+
expect(screen.queryByTestId('cluster-details-pods-sidebar')).toBeNull();
240+
});
241+
});

0 commit comments

Comments
 (0)