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 27, 2025
1 parent dbc983d commit 0b9fd6a
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 56 deletions.
16 changes: 14 additions & 2 deletions webpack/JobInvocationDetail/JobInvocationConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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 @@ -52,7 +60,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 @@ -70,13 +78,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 @@ -86,13 +96,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
118 changes: 78 additions & 40 deletions webpack/JobInvocationDetail/JobInvocationHostTable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types';
import React, { useMemo, useEffect } from 'react';
import React, { useMemo, useEffect, useState } from 'react';
import { Icon } from 'patternfly-react';
import { translate as __ } from 'foremanReact/common/I18n';
import { FormattedMessage } from 'react-intl';
Expand All @@ -20,23 +20,43 @@ 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,
STATUS_UPPERCASE,
} from './JobInvocationConstants';
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 { response, status, setAPIOptions } = useAPI(
Expand All @@ -47,30 +67,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 @@ -81,6 +89,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 @@ -98,24 +123,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 @@ -158,6 +190,12 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
apiUrl=""
apiOptions={apiOptions}
customSearchProps={memoDefaultSearchProps}
customToolbarItems={
<JobInvocationHostTableToolbar
dropdownFilter={selectedFilter}
setDropdownFilter={wrapSetSelectedFilter}
/>
}
controller="hosts"
creatable={false}
replacementResponse={combinedResponse}
Expand All @@ -172,7 +210,7 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
: null
}
params={params}
setParams={setParamsAndAPI}
setParams={wrapSetAPIOptions}
itemCount={response?.subtotal}
results={response?.results}
url=""
Expand All @@ -184,7 +222,6 @@ const JobInvocationHostTable = ({ id, targeting, finished, autoRefresh }) => {
}
isPending={status === STATUS_UPPERCASE.PENDING}
isDeleteable={false}
bottomPagination={bottomPagination}
>
{response?.results?.map((result, rowIndex) => (
<Tr key={rowIndex} ouiaId={`table-row-${rowIndex}`}>
Expand All @@ -203,6 +240,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 0b9fd6a

Please sign in to comment.