Skip to content

Commit

Permalink
Fixes #38090 - Add filtering to job invocation hosts table
Browse files Browse the repository at this point in the history
  • Loading branch information
kmalyjur committed Jan 28, 2025
1 parent 45bd8b8 commit f4aefaa
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 60 deletions.
16 changes: 14 additions & 2 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const STATUS_UPPERCASE = {
PENDING: 'PENDING',
};

export const STATUS_TITLES = {
ALL_STATUSES: { id: 'all_statuses', title: __('All statuses') },
SUCCESS: { id: 'success', title: __('Succeeded') },
FAILED: { id: 'failed', title: __('Failed') },
PENDING: { id: 'pending', title: __('In Progress') },
CANCELLED: { id: 'cancelled', title: __('Cancelled') },
};

export const DATE_OPTIONS = {
day: 'numeric',
month: 'short',
Expand All @@ -61,7 +69,7 @@ const Columns = () => {
return { title: __('Failed'), status: 1 };
case 'planned':
return { title: __('Scheduled'), status: 2 };
case 'running':
case 'running' || 'pending':
return { title: __('Pending'), status: 3 };
case 'cancelled':
return { title: __('Cancelled'), status: 4 };
Expand All @@ -84,13 +92,15 @@ const Columns = () => {
wrapper: ({ name }) => (
<a href={`${hostDetailsPageUrl}${name}`}>{name}</a>
),
isSorted: true,
weight: 1,
},
groups: {
hostgroup: {
title: __('Host group'),
wrapper: ({ hostgroup_id, hostgroup_name }) => (
<a href={`/hostgroups/${hostgroup_id}/edit`}>{hostgroup_name}</a>
),
isSorted: true,
weight: 2,
},
os: {
Expand All @@ -100,13 +110,15 @@ const Columns = () => {
{operatingsystem_name}
</a>
),
isSorted: true,
weight: 3,
},
smart_proxy: {
title: __('Smart proxy'),
wrapper: ({ smart_proxy_name, smart_proxy_id }) => (
<a href={`/smart_proxies/${smart_proxy_id}`}>{smart_proxy_name}</a>
),
isSorted: true,
weight: 4,
},
status: {
Expand Down
124 changes: 80 additions & 44 deletions webpack/JobInvocationDetail/JobInvocationHostTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
useBulkSelect,
useUrlParams,
} from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
import Pagination from 'foremanReact/components/Pagination';
import { getControllerSearchProps } from 'foremanReact/constants';
import Columns, {
JOB_INVOCATION_HOSTS,
Expand All @@ -29,17 +28,38 @@ import Columns, {
import { TemplateInvocation } from './TemplateInvocation';
import { OpenAlInvocations, PopupAlert } from './OpenAlInvocations';
import { RowActions } from './TemplateInvocationComponents/TemplateActionButtons';
import JobInvocationHostTableToolbar from './JobInvocationHostTableToolbar';

const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
const JobInvocationHostTable = ({
id,
targeting,
finished,
autoRefresh,
initialFilter,
}) => {
const columns = Columns();
const columnNamesKeys = Object.keys(columns);
const apiOptions = { key: JOB_INVOCATION_HOSTS };
const [selectedFilter, setSelectedFilter] = useState(initialFilter || '');
const {
searchParam: urlSearchQuery = '',
page: urlPage,
per_page: urlPerPage,
} = useUrlParams();
const defaultParams = { search: urlSearchQuery };
const constructFilter = (
filter = selectedFilter,
search = urlSearchQuery
) => {
const baseFilter = `job_invocation.id = ${id}`;
const dropdownFilterClause =
filter && filter !== 'all_statuses'
? `and job_invocation.result = ${filter}`
: '';
const searchQueryClause = search ? `and (${search})` : '';
return `${baseFilter} ${dropdownFilterClause} ${searchQueryClause}`;
};

const defaultParams = { search: constructFilter() };
if (urlPage) defaultParams.page = Number(urlPage);
if (urlPerPage) defaultParams.per_page = Number(urlPerPage);
const [expandedHost, setExpandedHost] = useState([]);
Expand All @@ -57,30 +77,18 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
}
);

const combinedResponse = {
response: {
search: urlSearchQuery,
can_create: false,
results: response?.results || [],
total: response?.total || 0,
per_page: response?.perPage,
page: response?.page,
subtotal: response?.subtotal || 0,
message: response?.message || 'error',
},
status,
setAPIOptions,
};

const { setParamsAndAPI, params } = useSetParamsAndApiAndSearch({
const { params } = useSetParamsAndApiAndSearch({
defaultParams,
apiOptions,
setAPIOptions: combinedResponse.setAPIOptions,
setAPIOptions,
});

const { updateSearchQuery } = useBulkSelect({
const { updateSearchQuery: updateSearchQueryBulk } = useBulkSelect({
initialSearchQuery: urlSearchQuery,
});
const updateSearchQuery = searchQuery => {
updateSearchQueryBulk(searchQuery);
};

const controller = 'hosts';
const memoDefaultSearchProps = useMemo(
Expand All @@ -91,6 +99,23 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
`/${controller}/auto_complete_search`
);

const wrapSetSelectedFilter = filter => {
const filterSearch = constructFilter(filter);
setAPIOptions(prevOptions => {
if (prevOptions.params.search !== filterSearch) {
return {
...prevOptions,
params: {
...prevOptions.params,
search: filterSearch,
},
};
}
return prevOptions;
});
setSelectedFilter(filter);
};

useEffect(() => {
const intervalId = setInterval(() => {
if (!finished || autoRefresh) {
Expand All @@ -108,24 +133,31 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
};
}, [finished, autoRefresh, setAPIOptions]);

const onPagination = newPagination => {
setParamsAndAPI({
...params,
...newPagination,
search: urlSearchQuery,
});
const wrapSetAPIOptions = newAPIOptions => {
setAPIOptions(prevOptions => ({
...prevOptions,
params: {
...prevOptions.params,
...newAPIOptions.params,
search: constructFilter(undefined, newAPIOptions?.params?.search),
},
}));
};

const bottomPagination = (
<Pagination
ouiaId="table-hosts-bottom-pagination"
key="table-bottom-pagination"
page={params.page}
perPage={params.perPage}
itemCount={response?.subtotal}
onChange={onPagination}
/>
);
const combinedResponse = {
response: {
search: urlSearchQuery,
can_create: false,
results: response?.results || [],
total: response?.total || 0,
per_page: response?.perPage,
page: response?.page,
subtotal: response?.subtotal || 0,
message: response?.message || 'error',
},
status,
setAPIOptions: wrapSetAPIOptions,
};

const customEmptyState = (
<Tr ouiaId="table-empty">
Expand Down Expand Up @@ -183,26 +215,30 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
creatable={false}
replacementResponse={combinedResponse}
updateSearchQuery={updateSearchQuery}
customToolbarItems={
customToolbarItems={[
<OpenAlInvocations
setShowAlert={setShowAlert}
results={results}
id={id}
/>
}
/>,
<JobInvocationHostTableToolbar
dropdownFilter={selectedFilter}
setDropdownFilter={wrapSetSelectedFilter}
/>,
]}
>
<Table
ouiaId="job-invocation-hosts-table"
columns={columns}
customEmptyState={
status === STATUS_UPPERCASE.RESOLVED && !results?.length
status === STATUS_UPPERCASE.RESOLVED && !response?.results?.length
? customEmptyState
: null
}
params={params}
setParams={setParamsAndAPI}
setParams={wrapSetAPIOptions}
itemCount={response?.subtotal}
results={results}
results={response?.results}
url=""
refreshData={() => {}}
errorMessage={
Expand All @@ -212,7 +248,6 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
}
isPending={status === STATUS_UPPERCASE.PENDING}
isDeleteable={false}
bottomPagination={bottomPagination}
childrenOutsideTbody
>
{results?.map((result, rowIndex) => (
Expand Down Expand Up @@ -267,6 +302,7 @@ JobInvocationHostTable.propTypes = {
targeting: PropTypes.object.isRequired,
finished: PropTypes.bool.isRequired,
autoRefresh: PropTypes.bool.isRequired,
initialFilter: PropTypes.string.isRequired,
};

JobInvocationHostTable.defaultProps = {};
Expand Down
63 changes: 63 additions & 0 deletions webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { Select, SelectOption, SelectList } from '@patternfly/react-core/next'; // remove "/next" after switching to PF5
import { MenuToggle, ToolbarItem } from '@patternfly/react-core';
import { STATUS_TITLES } from './JobInvocationConstants';

const JobInvocationHostTableToolbar = ({
dropdownFilter,
setDropdownFilter,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
const onSelect = (_event, itemId) => {
setDropdownFilter(itemId);
setIsOpen(false);
};

const toggle = toggleRef => (
<MenuToggle
ref={toggleRef}
onClick={() => setIsOpen(!isOpen)}
isExpanded={isOpen}
style={{
width: '200px',
}}
>
{Object.values(STATUS_TITLES).find(status => status.id === dropdownFilter)
?.title || __('All statuses')}
</MenuToggle>
);

return (
<ToolbarItem>
<Select
isOpen={isOpen}
selected={dropdownFilter}
onSelect={onSelect}
onOpenChange={newIsOpen => setIsOpen(newIsOpen)}
ouiaId="host-status-select"
toggle={toggle}
>
<SelectList>
{Object.values(STATUS_TITLES).map(result => (
<SelectOption
key={result.id}
itemId={result.id}
isSelected={result.id === dropdownFilter}
>
{result.title}
</SelectOption>
))}
</SelectList>
</Select>
</ToolbarItem>
);
};

JobInvocationHostTableToolbar.propTypes = {
dropdownFilter: PropTypes.string.isRequired,
setDropdownFilter: PropTypes.func.isRequired,
};

export default JobInvocationHostTableToolbar;
Loading

0 comments on commit f4aefaa

Please sign in to comment.