Skip to content

Commit 41b8dee

Browse files
kmalyjuradamruzicka
authored andcommitted
Fixes #38806 - Stabilize job detail table and fix unresponsive actions
1 parent 4aefe7b commit 41b8dee

File tree

6 files changed

+319
-175
lines changed

6 files changed

+319
-175
lines changed

webpack/JobInvocationDetail/JobInvocationDetail.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ section.job-additional-info {
7474
margin-left: 10px;
7575
margin-right: 15px;
7676
}
77+
78+
.pf-v5-c-table__tbody > tr.row-hidden {
79+
display: none;
80+
}
7781
}
7882

7983
.template-invocation {

webpack/JobInvocationDetail/JobInvocationHostTable.js

Lines changed: 123 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,47 @@ const JobInvocationHostTable = ({
6161
const [allHostsIds, setAllHostsIds] = useState([]);
6262

6363
// Expansive items
64-
const [expandedHost, setExpandedHost] = useState([]);
64+
const [expandedHost, setExpandedHost] = useState(new Set());
6565
const prevStatusLabel = useRef(statusLabel);
6666

67+
const [hostInvocationStates, setHostInvocationStates] = useState({});
68+
69+
const getInvocationState = hostId =>
70+
hostInvocationStates[hostId] || {
71+
showOutputType: { stderr: true, stdout: true, debug: true },
72+
showTemplatePreview: false,
73+
showCommand: false,
74+
};
75+
76+
const updateInvocationState = (hostId, stateKey, value) => {
77+
setHostInvocationStates(prevStates => {
78+
const currentHostState = getInvocationState(hostId);
79+
80+
const newValue =
81+
typeof value === 'function' ? value(currentHostState[stateKey]) : value;
82+
83+
return {
84+
...prevStates,
85+
[hostId]: {
86+
...currentHostState,
87+
[stateKey]: newValue,
88+
},
89+
};
90+
});
91+
};
92+
93+
const isHostExpanded = hostId => expandedHost.has(hostId);
94+
const setHostExpanded = (hostId, isExpanding = true) =>
95+
setExpandedHost(prevExpandedSet => {
96+
const newSet = new Set(prevExpandedSet);
97+
if (isExpanding) {
98+
newSet.add(hostId);
99+
} else {
100+
newSet.delete(hostId);
101+
}
102+
return newSet;
103+
});
104+
67105
// Page table params
68106
// Parse URL
69107
const {
@@ -307,29 +345,18 @@ const JobInvocationHostTable = ({
307345
</Tr>
308346
);
309347

310-
const isHostExpanded = host => expandedHost.includes(host.id);
311-
312-
const setHostExpanded = (host, isExpanding = true) =>
313-
setExpandedHost(prevExpanded => {
314-
const otherExpandedHosts = prevExpanded.filter(h => h !== host.id);
315-
return isExpanding
316-
? [...otherExpandedHosts, host.id]
317-
: otherExpandedHosts;
318-
});
319-
320348
const pageHostIds = results.map(h => h.id);
321349

322350
const areAllPageRowsExpanded =
323351
pageHostIds.length > 0 &&
324-
pageHostIds.every(hostId => expandedHost.includes(hostId));
352+
pageHostIds.every(hostId => expandedHost.has(hostId));
325353

326354
const onExpandAll = () => {
327355
setExpandedHost(() => {
328356
if (areAllPageRowsExpanded) {
329-
return [];
357+
return new Set();
330358
}
331-
332-
return pageHostIds;
359+
return new Set(pageHostIds);
333360
});
334361
};
335362

@@ -393,54 +420,88 @@ const JobInvocationHostTable = ({
393420
isDeleteable={false}
394421
childrenOutsideTbody
395422
>
396-
{results.map((result, rowIndex) => (
397-
<Tbody key={result.id} isExpanded={isHostExpanded(result)}>
398-
<Tr ouiaId={`table-row-${result.id}`}>
399-
<Td
400-
expand={{
401-
rowIndex,
402-
isExpanded: isHostExpanded(result),
403-
onToggle: () =>
404-
setHostExpanded(result, !isHostExpanded(result)),
405-
expandId: 'host-expandable',
406-
}}
407-
/>
408-
<RowSelectTd rowData={result} {...{ selectOne, isSelected }} />
409-
{columnNamesKeys.map(k => (
410-
<Td key={k}>{columns[k].wrapper(result)}</Td>
411-
))}
412-
<Td isActionCell>
413-
<RowActions hostID={result.id} jobID={id} />
414-
</Td>
415-
</Tr>
416-
<Tr
417-
isExpanded={isHostExpanded(result)}
418-
ouiaId="table-row-expanded-sections"
419-
>
420-
<Td
421-
dataLabel={`${result.id}-expandable-content`}
422-
colSpan={columnNamesKeys.length + 3}
423+
{results.map((result, rowIndex) => {
424+
const currentInvocationState = getInvocationState(result.id);
425+
return (
426+
<Tbody key={result.id}>
427+
<Tr ouiaId={`table-row-${result.id}`}>
428+
<Td
429+
expand={{
430+
rowIndex,
431+
isExpanded: isHostExpanded(result.id),
432+
onToggle: () =>
433+
setHostExpanded(result.id, !isHostExpanded(result.id)),
434+
expandId: 'host-expandable',
435+
}}
436+
/>
437+
<RowSelectTd
438+
rowData={result}
439+
selectOne={selectOne}
440+
isSelected={isSelected}
441+
/>
442+
{columnNamesKeys.map(k => (
443+
<Td key={k}>{columns[k].wrapper(result)}</Td>
444+
))}
445+
<Td isActionCell>
446+
<RowActions hostID={result.id} jobID={id} />
447+
</Td>
448+
</Tr>
449+
<Tr
450+
isExpanded={isHostExpanded(result.id)}
451+
ouiaId="table-row-expanded-sections"
452+
className={!isHostExpanded(result.id) ? 'row-hidden' : ''}
423453
>
424-
<ExpandableRowContent>
425-
{result.job_status === 'cancelled' ||
426-
result.job_status === 'N/A' ? (
427-
<div>
428-
{__('A task for this host has not been started')}
429-
</div>
430-
) : (
431-
<TemplateInvocation
432-
key={`${result.id}-${result.job_status}`}
433-
hostID={result.id}
434-
jobID={id}
435-
isInTableView
436-
isExpanded={isHostExpanded(result)}
437-
/>
438-
)}
439-
</ExpandableRowContent>
440-
</Td>
441-
</Tr>
442-
</Tbody>
443-
))}
454+
<Td
455+
dataLabel={`${result.id}-expandable-content`}
456+
colSpan={columnNamesKeys.length + 3}
457+
>
458+
<ExpandableRowContent>
459+
{result.job_status === 'cancelled' ||
460+
result.job_status === 'N/A' ? (
461+
<div>
462+
{__('A task for this host has not been started')}
463+
</div>
464+
) : (
465+
<TemplateInvocation
466+
key={result.id}
467+
hostID={result.id}
468+
jobID={id}
469+
isInTableView
470+
isExpanded={isHostExpanded(result.id)}
471+
showOutputType={currentInvocationState.showOutputType}
472+
showTemplatePreview={
473+
currentInvocationState.showTemplatePreview
474+
}
475+
showCommand={currentInvocationState.showCommand}
476+
setShowOutputType={value =>
477+
updateInvocationState(
478+
result.id,
479+
'showOutputType',
480+
value
481+
)
482+
}
483+
setShowTemplatePreview={value =>
484+
updateInvocationState(
485+
result.id,
486+
'showTemplatePreview',
487+
value
488+
)
489+
}
490+
setShowCommand={value =>
491+
updateInvocationState(
492+
result.id,
493+
'showCommand',
494+
value
495+
)
496+
}
497+
/>
498+
)}
499+
</ExpandableRowContent>
500+
</Td>
501+
</Tr>
502+
</Tbody>
503+
);
504+
})}
444505
</Table>
445506
</TableIndexPage>
446507
</>

webpack/JobInvocationDetail/TemplateInvocation.js

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useEffect, useRef } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import { isEmpty } from 'lodash';
33
import PropTypes from 'prop-types';
44
import { ClipboardCopyButton, Alert, Skeleton } from '@patternfly/react-core';
@@ -60,6 +60,12 @@ export const TemplateInvocation = ({
6060
isExpanded,
6161
hostName,
6262
hostProxy,
63+
showOutputType,
64+
setShowOutputType,
65+
showTemplatePreview,
66+
setShowTemplatePreview,
67+
showCommand,
68+
setShowCommand,
6369
}) => {
6470
const intervalRef = useRef(null);
6571
const templateURL = showTemplateInvocationUrl(hostID, jobID);
@@ -74,14 +80,6 @@ export const TemplateInvocation = ({
7480
responseRef.current = response;
7581
}, [response]);
7682

77-
const [showOutputType, setShowOutputType] = useState({
78-
stderr: true,
79-
stdout: true,
80-
debug: true,
81-
});
82-
const [showTemplatePreview, setShowTemplatePreview] = useState(false);
83-
const [showCommand, setShowCommand] = useState(false);
84-
8583
useEffect(() => {
8684
const dispatchFetch = () => {
8785
dispatch(
@@ -123,12 +121,8 @@ export const TemplateInvocation = ({
123121
};
124122
}, [isExpanded, dispatch, templateURL, hostID]);
125123

126-
if (!isExpanded) {
127-
return null;
128-
}
129-
130-
if ((status === STATUS.PENDING && isEmpty(response)) || !response) {
131-
return <Skeleton />;
124+
if (!response || (status === STATUS.PENDING && isEmpty(response))) {
125+
return <Skeleton data-testid="template-invocation-skeleton" />;
132126
}
133127

134128
const errorMessage =
@@ -239,6 +233,16 @@ TemplateInvocation.propTypes = {
239233
jobID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
240234
isInTableView: PropTypes.bool,
241235
isExpanded: PropTypes.bool,
236+
showOutputType: PropTypes.shape({
237+
stderr: PropTypes.bool,
238+
stdout: PropTypes.bool,
239+
debug: PropTypes.bool,
240+
}).isRequired,
241+
setShowOutputType: PropTypes.func.isRequired,
242+
showTemplatePreview: PropTypes.bool.isRequired,
243+
setShowTemplatePreview: PropTypes.func.isRequired,
244+
showCommand: PropTypes.bool.isRequired,
245+
setShowCommand: PropTypes.func.isRequired,
242246
};
243247

244248
TemplateInvocation.defaultProps = {

webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { useCallback } from 'react';
22
import PropTypes from 'prop-types';
33
import {
44
ToggleGroup,
@@ -27,31 +27,49 @@ export const OutputToggleGroup = ({
2727
taskCancellable,
2828
permissions,
2929
}) => {
30-
const handleSTDERRClick = _isSelected => {
31-
setShowOutputType(prevShowOutputType => ({
32-
...prevShowOutputType,
33-
stderr: _isSelected,
34-
}));
35-
};
30+
const handleSTDERRClick = useCallback(
31+
_isSelected => {
32+
setShowOutputType(prevShowOutputType => ({
33+
...prevShowOutputType,
34+
stderr: _isSelected,
35+
}));
36+
},
37+
[setShowOutputType]
38+
);
3639

37-
const handleSTDOUTClick = _isSelected => {
38-
setShowOutputType(prevShowOutputType => ({
39-
...prevShowOutputType,
40-
stdout: _isSelected,
41-
}));
42-
};
43-
const handleDEBUGClick = _isSelected => {
44-
setShowOutputType(prevShowOutputType => ({
45-
...prevShowOutputType,
46-
debug: _isSelected,
47-
}));
48-
};
49-
const handlePreviewTemplateClick = _isSelected => {
50-
setShowTemplatePreview(_isSelected);
51-
};
52-
const handleCommandClick = _isSelected => {
53-
setShowCommand(_isSelected);
54-
};
40+
const handleSTDOUTClick = useCallback(
41+
_isSelected => {
42+
setShowOutputType(prevShowOutputType => ({
43+
...prevShowOutputType,
44+
stdout: _isSelected,
45+
}));
46+
},
47+
[setShowOutputType]
48+
);
49+
50+
const handleDEBUGClick = useCallback(
51+
_isSelected => {
52+
setShowOutputType(prevShowOutputType => ({
53+
...prevShowOutputType,
54+
debug: _isSelected,
55+
}));
56+
},
57+
[setShowOutputType]
58+
);
59+
60+
const handlePreviewTemplateClick = useCallback(
61+
_isSelected => {
62+
setShowTemplatePreview(_isSelected);
63+
},
64+
[setShowTemplatePreview]
65+
);
66+
67+
const handleCommandClick = useCallback(
68+
_isSelected => {
69+
setShowCommand(_isSelected);
70+
},
71+
[setShowCommand]
72+
);
5573

5674
const toggleGroupItems = {
5775
stderr: {

0 commit comments

Comments
 (0)