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 Oct 17, 2022
1 parent 68eb570 commit 708b061
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 1 deletion.
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
62 changes: 62 additions & 0 deletions app/helpers/pagelets_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,70 @@ def th_content(col)
sort col[:key], as: col[:label], default: col[:default_sort] || 'ASC'
end

def mount_column_selector
url = "#{api_users_path}/#{User.current[:id]}/table_preferences"
has_preference = !@selected_columns.nil?
columns = columns_view
react_component("ColumnSelector", data:
{
url: url,
controller: controller_name,
columns: columns,
initialColumns: columns,
hasPreference: has_preference,
}
)
end

private

def defined_columns
Pagelets::Manager.pagelets_at('hosts/_list', :hosts_table_column_header).map do |pt|
next unless pt.opts[:key]
{
key: pt.opts[:key],
label: pt.opts[:label],
profiles: pt.profiles.map { |pr| pr.id },
checked: @selected_columns ? @selected_columns.include?(pt.opts[:key].to_s) : pt.profiles.any? { |profile| profile.default? },
}
end.compact
end

def defined_categories(columns)
columns.map { |col| col[:profiles] }.flatten.uniq
end

def all_checked?(category)
return true if category.all? { |column| column[:checkProps][:checked] }
category.any? { |column| column[:checkProps][:checked] } ? nil : false
end

def columns_view
categories = []
columns = defined_columns
defined_categories(columns).each do |category|
categories << {
name: category.capitalize,
key: category,
defaultExpanded: category == 'general',
checkProps: {},
children: [],
}
category = categories.find { |c| c[:key] == category }
columns.each do |column|
if column[:profiles].first == category[:key]
category[:children] << {
name: column[:label],
key: column[:key],
checkProps: { checked: column[:checked] },
}
end
end
category[:checkProps][:checked] = all_checked?(category[:children])
end
categories
end

def filter_opts(opts = {})
opts.merge(
{
Expand Down
2 changes: 1 addition & 1 deletion app/registries/pagelets/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def pagelets_at(*args)
end

def with_profile(id, label, opts, &block)
profile = Profile.new(id, label, self, opts)
profile = Profile.new(id.to_s, label, self, opts)
profile.instance_exec(&block)
profile.pagelets.each do |pagelet|
add_pagelet(*pagelet)
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="pull-left">
<%= 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,201 @@
import React, { useState, useEffect, useCallback } 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';

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

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

useEffect(() => {
if (!tablePreference) {
createTablePreference();
setTablePreference(true);
}

function createTablePreference() {
API.post(url, { name: 'hosts', columns: getColumnKeys() });
}
}, [url, tablePreference, selectedColumns, getColumnKeys, initialColumns]);

const getColumnKeys = useCallback(() => {
const keys = selectedColumns
.map(category => category.children)
.flat()
.map(column => {
if (column.checkProps.checked) {
return column.key;
}
return null;
})
.filter(item => item);
return keys;
}, [selectedColumns]);

const updateTablePreference = useCallback(() => {
API.put([url, controller].join('/'), { columns: getColumnKeys() });
}, [url, controller, getColumnKeys]);

const filterItems = useCallback((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 = useCallback(tree => {
let result = [];
tree.forEach(item => {
result.push(item);
if (item.children) {
result = result.concat(flattenTree(item.children));
}
});
return result;
}, []);

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

const onCancel = () => {
setSelectedColumns(initialColumns);
setModalOpen(!isModalOpen);
};

const isDisabled = () => {
const hasPartialCheck = selectedColumns.map(
item => item.checkProps.checked
);
return hasPartialCheck.every(el => el === false);
};

const updateCheckBox = (treeViewItem, checked = true) => {
treeViewItem.checkProps.checked = checked;
if (treeViewItem.children) {
treeViewItem.children.forEach(item => {
item.checkProps.checked = checked;
});
}
};

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

if (checked) {
updateCheckBox(treeViewItem);
setSelectedColumns(
selectedColumns
.concat(
flatCheckedItems.filter(
item => !selectedColumns.some(i => i.key === item.key)
)
)
.filter(item => item.children)
);
} else {
updateCheckBox(treeViewItem, false);
setSelectedColumns(
selectedColumns.filter(item =>
flatCheckedItems.some(i => i.key === item.key)
)
);
}
selectedColumns.map(category => areDescendantsChecked(category));
};

const isChecked = dataItem => dataItem.checkProps.checked;
const areDescendantsChecked = dataItem => {
if (dataItem.children) {
if (dataItem.children.every(child => isChecked(child))) {
dataItem.checkProps.checked = true;
} else if (dataItem.children.some(child => isChecked(child))) {
dataItem.checkProps.checked = null;
} else {
dataItem.checkProps.checked = false;
}
}
};

return (
<div className="pf-c-select-input">
<div className="pf-c-input-group" id="column-selector">
<Button
id="btn-filter"
variant="secondary"
className="pull-left"
onClick={() => onClose()}
>
{__('Manage columns')}
</Button>
<Modal
variant={ModalVariant.small}
title={__('Manage columns')}
isOpen={isModalOpen}
onClose={onClose}
description={__('Select columns to display in the table')}
position="top"
actions={[
<Button
key="save"
variant="primary"
isDisabled={isDisabled()}
onClick={() => {
updateTablePreference();
changeQuery({});
}}
>
{__('Save')}
</Button>,
<Button key="cancel" variant="secondary" onClick={onCancel}>
{__('Cancel')}
</Button>,
]}
>
<TreeView data={selectedColumns} onCheck={onCheck} hasChecks />
</Modal>
</div>
</div>
);
};

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

ColumnSelector.defaultProps = {
data: {
url: '',
controller: '',
columns: [],
initialColumns: [],
hasPreference: false,
},
};

export default ColumnSelector;
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 708b061

Please sign in to comment.