diff --git a/app/assets/stylesheets/base.scss b/app/assets/stylesheets/base.scss
index 979676a3b11a..87d2aab45d7f 100644
--- a/app/assets/stylesheets/base.scss
+++ b/app/assets/stylesheets/base.scss
@@ -143,7 +143,12 @@ select {
padding-bottom: 9px;
}
+.title_filter {
+ padding-right: 8px;
+}
+
#title_action {
+ padding-left: 8px;
padding-bottom: 12px;
}
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 6cd1e4729076..f876b96b5d6e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/helpers/pagelets_helper.rb b/app/helpers/pagelets_helper.rb
index d6323d40b589..01bd2324b837 100644
--- a/app/helpers/pagelets_helper.rb
+++ b/app/helpers/pagelets_helper.rb
@@ -67,8 +67,67 @@ 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"
+ react_component("ColumnSelector", data:
+ {
+ url: url,
+ controller: controller_name,
+ categories: columns_view,
+ hasPreference: @selected_columns.present?,
+ }
+ )
+ 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,
+ 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 { |pl| pl.id }
+ 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|
+ category_view = {
+ name: category.label,
+ key: category.id,
+ defaultExpanded: category.id == 'general',
+ checkProps: {},
+ children: [],
+ }
+ columns.each do |column|
+ if column[:profiles].map(&:id).include?(category_view[:key])
+ category_view[:children] << {
+ name: column[:label],
+ key: column[:key],
+ checkProps: { checked: column[:checked] },
+ }
+ end
+ end
+ category_view[:checkProps][:checked] = all_checked?(category_view[:children])
+ categories << category_view
+ end
+ categories
+ end
+
def filter_opts(opts = {})
opts.merge(
{
diff --git a/app/registries/pagelets/manager.rb b/app/registries/pagelets/manager.rb
index d6b18b727321..33d0859c9faa 100644
--- a/app/registries/pagelets/manager.rb
+++ b/app/registries/pagelets/manager.rb
@@ -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)
diff --git a/app/views/layouts/_application_content.html.erb b/app/views/layouts/_application_content.html.erb
index 1df71084b932..247958d7f0ed 100644
--- a/app/views/layouts/_application_content.html.erb
+++ b/app/views/layouts/_application_content.html.erb
@@ -14,6 +14,12 @@
<%= yield(:search_bar) %>
>
+ <% if filter_columns? %>
+
+ <%= mount_column_selector %>
+ <%= yield(:column_selector) %>
+
+ <% end %>
<%=h yield(:title_actions) %>
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnSelector.js b/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnSelector.js
new file mode 100644
index 000000000000..57c5e0f708d1
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnSelector.js
@@ -0,0 +1,192 @@
+import React, { useState } from 'react';
+import { PropTypes } from 'prop-types';
+import { Button, Modal, ModalVariant, TreeView } from '@patternfly/react-core';
+import { ColumnsIcon } from '@patternfly/react-icons';
+import { cloneDeep } from 'lodash';
+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, controller, categories, hasPreference },
+ } = props;
+
+ const initialColumns = cloneDeep(categories);
+ const [isModalOpen, setModalOpen] = useState(false);
+ const [selectedColumns, setSelectedColumns] = useState(categories);
+
+ const getColumnKeys = () => {
+ const keys = selectedColumns
+ .map(category => category.children)
+ .flat()
+ .map(column => {
+ if (column.checkProps.checked) {
+ return column.key;
+ }
+ return null;
+ })
+ .filter(item => item);
+ return keys;
+ };
+
+ async function updateTablePreference() {
+ if (!hasPreference) {
+ await API.post(url, { name: 'hosts', columns: getColumnKeys() });
+ } else {
+ await API.put(`${url}/${controller}`, { columns: getColumnKeys() });
+ }
+ changeQuery({});
+ }
+
+ 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 toggleModal = () => {
+ 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 (
+
+
+ }
+ iconPosition="left"
+ className="columns-selector"
+ onClick={() => toggleModal()}
+ >
+ {__('Manage columns')}
+
+ updateTablePreference()}
+ >
+ {__('Save')}
+ ,
+ ,
+ ]}
+ >
+
+
+
+
+ );
+};
+
+ColumnSelector.propTypes = {
+ data: PropTypes.shape({
+ url: PropTypes.string,
+ controller: PropTypes.string,
+ categories: PropTypes.arrayOf(PropTypes.object),
+ hasPreference: PropTypes.bool,
+ }),
+};
+
+ColumnSelector.defaultProps = {
+ data: {
+ url: '',
+ controller: '',
+ categories: [],
+ hasPreference: false,
+ },
+};
+
+export default ColumnSelector;
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnsSelector.fixtures.js b/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnsSelector.fixtures.js
new file mode 100644
index 000000000000..428cdd0d1309
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/ColumnsSelector.fixtures.js
@@ -0,0 +1,59 @@
+export const user = {
+ impersonated_by: true,
+ id: 4,
+ login: 'admin',
+ firstname: 'Admin',
+ lastname: 'User',
+ name: 'Admin User',
+};
+
+export const ColumnSelectorProps = {
+ data: {
+ url: `api/users/${user.id}/table_preferences`,
+ controller: 'hosts',
+ categories: [{
+ name: 'General',
+ key: 'general',
+ defaultExpanded: true,
+ checkProps: { checked: true },
+ children: [
+ {
+ name: 'Name',
+ key: 'name',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Operating system',
+ key: 'os_title',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Model',
+ key: 'model',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Owner',
+ key: 'owner',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Host group',
+ key: 'hostgroup',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Last report',
+ key: 'last_report',
+ checkProps: { checked: true },
+ },
+ {
+ name: 'Comment',
+ key: 'comment',
+ checkProps: { checked: true },
+ },
+ ],
+ }],
+ hasPreference: true,
+ },
+};
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/ColumnSelector.test.js b/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/ColumnSelector.test.js
new file mode 100644
index 000000000000..c86ffc33ba17
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/ColumnSelector.test.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import ColumnSelector from '../ColumnSelector';
+import { ColumnSelectorProps } from '../ColumnsSelector.fixtures';
+import { testComponentSnapshotsWithFixtures } from '../../../common/testHelpers';
+
+const fixtures = {
+ 'should render': ColumnSelectorProps,
+};
+describe('ColumnSelector', () => {
+ const columnSelector = () => (
+
+ );
+ testComponentSnapshotsWithFixtures(columnSelector, fixtures);
+});
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/__snapshots__/ColumnSelector.test.js.snap b/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/__snapshots__/ColumnSelector.test.js.snap
new file mode 100644
index 000000000000..6ccdf9710e9d
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/__tests__/__snapshots__/ColumnSelector.test.js.snap
@@ -0,0 +1,74 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ColumnSelector should render 1`] = `
+
+`;
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/column-selector.scss b/webpack/assets/javascripts/react_app/components/ColumnSelector/column-selector.scss
new file mode 100644
index 000000000000..e4858917f9c3
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/column-selector.scss
@@ -0,0 +1,13 @@
+.pf-c-input-group {
+ .columns-selector {
+ padding-left: 0px;
+
+ .pf-c-button__icon.pf-m-start {
+ padding-right: 4px;
+ }
+ }
+}
+
+.pf-c-tree-view {
+ padding-top: 0px
+}
diff --git a/webpack/assets/javascripts/react_app/components/ColumnSelector/index.js b/webpack/assets/javascripts/react_app/components/ColumnSelector/index.js
new file mode 100644
index 000000000000..b6979da02d78
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/ColumnSelector/index.js
@@ -0,0 +1,3 @@
+import ColumnSelector from './ColumnSelector';
+
+export default ColumnSelector;
diff --git a/webpack/assets/javascripts/react_app/components/componentRegistry.js b/webpack/assets/javascripts/react_app/components/componentRegistry.js
index 1fd1a11a376e..2af43c01f6da 100644
--- a/webpack/assets/javascripts/react_app/components/componentRegistry.js
+++ b/webpack/assets/javascripts/react_app/components/componentRegistry.js
@@ -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';
@@ -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 },