Skip to content

Commit

Permalink
Fixes #37293 - Add user columns to hosts index
Browse files Browse the repository at this point in the history
- add column registry
- add plugin documentation
- Make breadcrumb a React link
- Respect display_fqdn_for_hosts setting
  • Loading branch information
jeremylenz committed Apr 11, 2024
1 parent 6cafc68 commit c21ac65
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 19 deletions.
46 changes: 46 additions & 0 deletions developer_docs/how_to_create_a_plugin.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,52 @@ You can find usage of those pagelets in the https://github.com/theforeman/forema
_Requires Foreman 1.18 or higher, set `requires_foreman '>= 1.18'` in
engine.rb_

[[adding-columns-to-the-react-hosts-index-page]]
==== Adding columns to the React hosts index page

Similar to the way the legacy hosts index page can be extended via pagelets, columns can also be added to the React hosts index page, or any other page that uses the ColumnSelector component and user TablePreferences.
These columns will then be available in the ColumnSelector so that users can customize which columns are displayed in the table.
Instead of pagelets, column data is defined in the plugin's `webpack/global_index.js` file.
The following example demonstrates how to add a new column to the React hosts index page:

[source, javascript]
----
import React from 'react';
import { RelativeDateTime } from 'foremanReact/components/RelativeDateTime';
import { registerColumns } from 'foremanReact/components/HostsIndex/Columns/core';
import { __ as translate } from 'foremanReact/common/i18n';
const hostsIndexColumnExtensions = [
{
columnName: 'last_checkin',
title: __('Last seen'),
wrapper: (hostDetails) => {
const lastCheckin =
hostDetails?.subscription_facet_attributes?.last_checkin;
return <RelativeDateTime defaultValue={__('Never')} date={lastCheckin} />;
},
weight: 400,
tableName: 'hosts',
categoryName: __('Content'),
categoryKey: 'content',
isSorted: false,
},
];
registerColumns(hostsIndexColumnExtensions);
----

Each column extension object must contain the following properties:

* `columnName` - the name of the column, which must match the column name in the API response.
* `title` - the title of the column to be displayed on screen in the <th> element. Should be translated.
* `wrapper` - a function that returns the content (as JSX) to be displayed in the table cell. The function receives the host details as an argument.
* `weight` - the weight of the column, which determines the order in which columns are displayed. Lower weights are displayed first.
* `tableName` - the name of the table. Should match the `name` of the user's TablePreference.
* `categoryName` - the name of the category to which the column belongs. Displayed on screen in the ColumnSelector. Should be translated.
* `categoryKey` - the key of the category to which the column belongs. Used to group columns in the ColumnSelector. Should not be translated.
* `isSorted` - whether the column is sortable. Sortable columns must have a `columnName` that matches a sortable column in the API response.

[[new-structure-for-assets]]
===== New structure for assets.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const Breadcrumb = ({
active,
'breadcrumb-item-with-icon': icon && active,
})}
{...{ item }}
>
{icon && <img src={icon.url} alt={icon.alt} title={icon.alt} />}{' '}
{inner}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ exports[`Breadcrumbs renders breadcrumbs menu 1`] = `
<BreadcrumbItem
className="breadcrumb-item"
isActive={false}
item={
Object {
"caption": "root",
"url": "/some-url",
}
}
key="0"
to="/some-url"
>
Expand All @@ -14,6 +20,12 @@ exports[`Breadcrumbs renders breadcrumbs menu 1`] = `
<BreadcrumbItem
className="breadcrumb-item"
isActive={false}
item={
Object {
"caption": "child with onClick",
"onClick": [MockFunction],
}
}
key="1"
onClick={[MockFunction]}
>
Expand All @@ -23,6 +35,11 @@ exports[`Breadcrumbs renders breadcrumbs menu 1`] = `
<BreadcrumbItem
className="breadcrumb-item active"
isActive={true}
item={
Object {
"caption": "active child",
}
}
key="2"
>
Expand All @@ -40,6 +57,11 @@ exports[`Breadcrumbs renders h1 title 1`] = `
<BreadcrumbItem
className="breadcrumb-item active"
isActive={true}
item={
Object {
"caption": "title",
}
}
key="0"
>
Expand All @@ -57,6 +79,12 @@ exports[`Breadcrumbs renders title override 1`] = `
<BreadcrumbItem
className="breadcrumb-item"
isActive={false}
item={
Object {
"caption": "root",
"url": "/some-url",
}
}
key="0"
to="/some-url"
>
Expand All @@ -66,6 +94,11 @@ exports[`Breadcrumbs renders title override 1`] = `
<BreadcrumbItem
className="breadcrumb-item active"
isActive={true}
item={
Object {
"caption": "active child",
}
}
key="1"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const ColumnSelector = props => {
const initialColumns = cloneDeep(categories);
const [isModalOpen, setModalOpen] = useState(false);
const [selectedColumns, setSelectedColumns] = useState(categories);
const [saving, setSaving] = useState(false);

const getColumnKeys = () => {
const keys = selectedColumns
Expand All @@ -32,8 +33,10 @@ const ColumnSelector = props => {
};

async function updateTablePreference() {
if (!url || !controller) return;
setSaving(true);
if (!hasPreference) {
await API.post(url, { name: 'hosts', columns: getColumnKeys() });
await API.post(url, { name: controller, columns: getColumnKeys() });
} else {
await API.put(`${url}/${controller}`, { columns: getColumnKeys() });
}
Expand Down Expand Up @@ -69,6 +72,7 @@ const ColumnSelector = props => {
const toggleModal = () => {
setSelectedColumns(initialColumns);
setModalOpen(!isModalOpen);
setSaving(false);
};

const updateCheckBox = (treeViewItem, checked = true) => {
Expand Down Expand Up @@ -167,6 +171,8 @@ const ColumnSelector = props => {
ouiaId="save-columns-button"
key="save"
variant="primary"
isLoading={saving}
isDisabled={saving}
onClick={() => updateTablePreference()}
>
{__('Save')}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const getCheckedStateForCategory = (category = { children: [] }) => {
// return true if all children are checked
// return null if some children are checked
// return false if no children are checked
const checked = category.children.map(child => child.checkProps?.checked);
if (checked.every(Boolean)) return true;
if (checked.some(Boolean)) return null;
return false;
};

export const categoriesFromFrontendColumnData = ({
registeredColumns,
userId,
controller = 'hosts',
userColumns = ['name'],
hasPreference = false,
}) => {
// need to build an object like
// {
// "url": "/api/users/4/table_preferences",
// "controller": "hosts",
// "categories": [
// {
// "name": "General",
// "key": "general",
// "defaultExpanded": true,
// "checkProps": {
// "checked": true
// },
// "children": [
// {
// "name": "Power",
// "key": "power_status",
// "checkProps": {
// "disabled": null,
// "checked": true
// }
// },
// ]
// },
// ],
// "hasPreference": true
// }

const result = {
url: userId ? `/api/users/${userId}/table_preferences` : null,
controller,
hasPreference,
};

const categories = [];
Object.keys(registeredColumns).forEach(column => {
const {
categoryName,
categoryKey,
tableName,
columnName,
title,
isRequired,
} = registeredColumns[column];
if (tableName !== controller) return;
const category = categories.find(cat => cat.key === categoryKey);
if (!category) {
categories.push({
name: categoryName,
key: categoryKey,
defaultExpanded: true,
checkProps: {
checked: false,
},
children: [],
});
}
const categoryIndex = categories.findIndex(cat => cat.key === categoryKey);
categories[categoryIndex].children.push({
name: title,
key: columnName,
checkProps: {
checked: isRequired || userColumns.includes(columnName),
disabled: isRequired ?? null,
},
});
});
categories.forEach(category => {
category.checkProps.checked = getCheckedStateForCategory(category);
});
result.categories = categories;
return result;
};
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const HostDetails = ({
location: { hash },
history,
}) => {
const { displayFqdnForHosts } = useForemanSettings();
const { displayFqdnForHosts, displayNewHostsPage } = useForemanSettings();
const { response, status } = useAPI(
'get',
`/api/hosts/${id}?show_hidden_parameters=true`,
Expand Down Expand Up @@ -116,7 +116,13 @@ const HostDetails = ({
switcherItemUrl: '/new/hosts/:name',
}}
breadcrumbItems={[
{ caption: __('Hosts'), url: hostsIndexUrl },
{
caption: __('Hosts'),
url: hostsIndexUrl,
render: displayNewHostsPage
? ({ caption }) => <Link to={hostsIndexUrl}>{caption}</Link>
: ({ caption }) => <a href={hostsIndexUrl}>{caption}</a>,
},
{
caption: displayFqdnForHosts
? response.name
Expand Down
Loading

0 comments on commit c21ac65

Please sign in to comment.