Skip to content

Commit

Permalink
Fixes #35287 - Create column selector on host index page
Browse files Browse the repository at this point in the history
  • Loading branch information
pkoprda authored and pkoprda committed Sep 14, 2022
1 parent 68eb570 commit 1f0d4d3
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 0 deletions.
4 changes: 4 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def searchable?
end
end

def filter_columns?
controller_name == 'hosts' && controller.action_name == 'index'
end

def auto_complete_controller_name
controller.respond_to?(:auto_complete_controller_name) ? controller.auto_complete_controller_name : controller_name
end
Expand Down
58 changes: 58 additions & 0 deletions app/helpers/pagelets_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,64 @@ def th_content(col)
sort col[:key], as: col[:label], default: col[:default_sort] || 'ASC'
end

def defined_columns
columns = Pagelets::Manager.pagelets_at('hosts/_list', :hosts_table_column_header).map { |pt| { key: pt.opts[:key], label: pt.opts[:label], profiles: pt.profiles.map { |pr| pr.label } } if pt.opts[:key] }.compact
columns.each do |column|
column[:checked] = @selected_columns&.include?(column[:key].to_s)
end
end

def defined_categories
defined_columns.map { |col| col[:profiles] }.flatten.uniq
end

def checked_columns(category)
all_checked = false
category.each do |column|
if column[:checkProps][:checked]
all_checked = true
elsif all_checked
all_checked = nil
break
end
end
all_checked
end

def columns_view
columns = []
defined_categories.each do |category|
category_key = category.downcase
columns << {
name: category,
key: category_key,
defaultExpanded: category_key == 'general',
checkProps: {},
children: [],
}
category = columns.find { |c| c[:key] == category_key }
defined_columns.each do |column|
category[:children] << {
name: column[:label],
key: column[:key],
checkProps: { checked: column[:checked] },
}
end
category[:checkProps][:checked] = checked_columns(category[:children])
end
columns
end

def mount_column_selector
url = "#{api_users_path}/#{User.current[:id]}/table_preferences/#{auto_complete_controller_name}"
react_component("ColumnSelector", data:
{
url: url,
columns: columns_view,
}
)
end

private

def filter_opts(opts = {})
Expand Down
6 changes: 6 additions & 0 deletions app/views/layouts/_application_content.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
<%= yield(:search_bar) %>&nbsp;
</div>
<div id="title_action" class= <%= searchable? ? "col-md-6" : "col-md-8" %>>
<% if filter_columns? %>
<div class="col-md-1">
<%= mount_column_selector %>
<%= yield(:column_selector) %>&nbsp;
</div>
<% end %>
<div class="btn-toolbar pull-right">
<%=h yield(:title_actions) %>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import React, { useState } from 'react';
import { PropTypes } from 'prop-types';
import { Button, Modal, ModalVariant, TreeView } from '@patternfly/react-core';
import { translate as __ } from '../../common/I18n';
import API from '../../API';
import { changeQuery } from '../../common/urlHelpers';
import './column-selector.scss';

const ColumnSelector = props => {
const {
data: { url, columns },
} = props;

const [isModalOpen, setModalOpen] = useState(false);
const [selectedColumns, setSelectedColumns] = useState(columns);

const getColumnKeys = event => {
return columns
.find(category => category.key === 'general')
.children.map(col => {
if (col.checkProps.checked) {
return col.key;
}
return null;
})
.filter(item => item);
};

const updateTablePreference = event => {
API.put(url, { columns: getColumnKeys() });
changeQuery({});
};

const onClose = event => {
setModalOpen(!isModalOpen);
};

const filterItems = (item, checkedItem) => {
if (item.key === checkedItem.key) {
return true;
}

if (item.children) {
item.children = item.children
.map(opt => Object.assign({}, opt))
.filter(column => filterItems(column, checkedItem));
return item.children;
}

return null;
};

const flattenTree = tree => {
let result = [];
tree.forEach(item => {
result.push(item);
if (item.children) {
result = result.concat(flattenTree(item.children));
}
});
return result;
};

const unCheck = treeViewItem => {
treeViewItem.checkProps.checked = false;
if (treeViewItem.children) {
treeViewItem.children.forEach(item => {
item.checkProps.checked = false;
});
}
};

const onCheck = (evt, treeViewItem) => {
const { checked } = evt.target;
const checkedItemTree = columns
.map(column => Object.assign({}, column))
.filter(item => filterItems(item, treeViewItem));
const flatCheckedItems = flattenTree(checkedItemTree);

if (checked) {
setSelectedColumns(
selectedColumns.concat(
flatCheckedItems.filter(
item => !selectedColumns.some(i => i.key === item.key)
)
)
);
} else {
unCheck(treeViewItem);
setSelectedColumns(
selectedColumns.filter(
item => !flatCheckedItems.some(i => i.key === item.key)
)
);
}
};

const isChecked = dataItem =>
selectedColumns.some(item => item.key === dataItem.key);
const areAllDescendantsChecked = dataItem =>
dataItem.children
? dataItem.children.every(child => areAllDescendantsChecked(child))
: isChecked(dataItem);
const areSomeDescendantsChecked = dataItem =>
dataItem.children
? dataItem.children.some(child => areSomeDescendantsChecked(child))
: isChecked(dataItem);

const mapTree = item => {
const hasCheck = areAllDescendantsChecked(item);

if (hasCheck) {
item.checkProps.checked = true;
} else {
const hasPartialCheck = areSomeDescendantsChecked(item);
if (hasPartialCheck) {
item.checkProps.checked = null;
}
}

if (item.children) {
return {
...item,
children: item.children.map(child => mapTree(child)),
};
}

return item;
};

return (
<div className="pf-c-select-input">
<div className="column-selector pf-c-input-group" id="column-selector">
<Button
id="btn-filter"
variant="tertiary"
className="pull-left"
onClick={() => onClose()}
>
{__('Filter columns')}
</Button>
<Modal
variant={ModalVariant.small}
title={__('Filter columns')}
isOpen={isModalOpen}
onClose={onClose}
actions={[
<Button
key="save"
variant="primary"
onClick={() => updateTablePreference()}
>
{__('Save')}
</Button>,
<Button key="cancel" variant="secondary" onClick={() => onClose()}>
{__('Cancel')}
</Button>,
]}
>
<TreeView
data={columns.map(category => mapTree(category))}
onCheck={onCheck}
hasChecks
/>
</Modal>
</div>
</div>
);
};

ColumnSelector.propTypes = {
data: PropTypes.shape({
url: PropTypes.string,
columns: PropTypes.arrayOf(PropTypes.object),
}),
};

ColumnSelector.defaultProps = {
data: {
url: '',
columns: [],
},
};

export default ColumnSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.column-selector {
width: 100%;

label {
font-size: 16px;
height: 36px;
border-bottom-color: var(--pf-global--BorderColor--200);
border-top-color: var(--pf-global--BorderColor--300);
border-left-color: var(--pf-global--BorderColor--300);
border-left-width: 0.7px;
border-top-width: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ColumnSelector from './ColumnSelector';

export default ColumnSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import FactChart from './FactCharts';
import Pagination from './Pagination';
import AutoComplete from './AutoComplete';
import SearchBar from './SearchBar';
import ColumnSelector from './ColumnSelector';
import Layout from './Layout';
import EmptyState from './common/EmptyState';
import ComponentWrapper from './common/ComponentWrapper/ComponentWrapper';
Expand Down Expand Up @@ -118,6 +119,7 @@ const componentRegistry = {
const coreComponets = [
{ name: 'ReactApp', type: ReactApp },
{ name: 'SearchBar', type: SearchBar },
{ name: 'ColumnSelector', type: ColumnSelector },
{ name: 'AutoComplete', type: AutoComplete },
{ name: 'AreaChart', type: AreaChart },
{ name: 'DonutChart', type: DonutChart },
Expand Down

0 comments on commit 1f0d4d3

Please sign in to comment.