Skip to content

Commit

Permalink
Fixes #37228 - Replace Ansible/Roles page with React component
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorben-D committed Mar 21, 2024
1 parent 38676da commit 21c7da7
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 40 deletions.
52 changes: 12 additions & 40 deletions app/views/ansible_roles/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,47 +1,19 @@
<%= webpacked_plugins_js_for :foreman_ansible %>
<%= webpacked_plugins_css_for :foreman_ansible %>
<% title _("Ansible Roles") %>
<% title_actions ansible_proxy_import(hash_for_import_ansible_roles_path),
documentation_button('#4.1ImportingRoles', :root_url => ansible_doc_url) %>
<table class="<%= table_css_classes 'table-fixed' %>">
<thead>
<tr>
<th class="col-md-6"><%= sort :name, :as => s_("Role|Name") %></th>
<th class="col-md-2"><%= _("Hostgroups") %></th>
<th class="col-md-2"><%= _("Hosts") %></th>
<th class="col-md-2"><%= _("Variables") %></th>
<th class="col-md-2"><%= sort :updated_at, :as => _("Imported at") %></th>
<th class="col-md-2"><%= _("Actions") %></th>
</tr>
</thead>
<tbody>
<% @ansible_roles.each do |role| %>
<tr>
<td class="ellipsis"><%= role.name %></td>
<td class="ellipsis"><%= link_to role.hostgroups.count, hostgroups_path(:search => "ansible_role = #{role.name}") %></td>
<td class="ellipsis"><%= link_to role.hosts.count, hosts_path(:search => "ansible_role = #{role.name}")%></td>
<td class="ellipsis"><%= link_to(role.ansible_variables.count, ansible_variables_path(:search => "ansible_role = #{role}")) %></td>
<td class="ellipsis"><%= import_time role %></td>
<td>
<%
links = [
link_to(
_('Variables'),
ansible_variables_path(:search => "ansible_role = #{role}")
),
display_delete_if_authorized(
hash_for_ansible_role_path(:id => role).
merge(:auth_object => role, :authorizer => authorizer),
:data => { :confirm => _("Delete %s?") % role.name },
:action => :delete
)
]
%>
<%= action_buttons(*links) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<%= react_component('AnsibleRolesTable', {:ansibleRoles => @ansible_roles.map { |role| {
:name => role.name,
:id => role.id,
:hostgroupsCount => role.hostgroups.count,
:hostsCount => role.hosts.count,
:variablesCount => role.ansible_variables.count,
:importTime => import_time(role),
:updatedAt => role.updated_at
} }})%>
<%= will_paginate_with_info @ansible_roles %>
64 changes: 64 additions & 0 deletions webpack/components/AnsibleRoles/AnsibleRolesTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { TableComposable, Thead, Tr, Th, Tbody } from '@patternfly/react-table';
import PropTypes from 'prop-types';
import { translate as __ } from 'foremanReact/common/I18n';
import { AnsibleRolesTableRow } from './components/AnsibleRolesTableRow';

export const AnsibleRolesTable = props => {
const searchParams = new URLSearchParams(window.location.search);
const sortString = searchParams.get('order');

let sortIndex = null;
let sortDirection = null;
if (sortString) {
const sortStrings = sortString.split(' ');
sortIndex = sortStrings[0] === 'name' ? 0 : 6;
// eslint-disable-next-line prefer-destructuring
sortDirection = sortStrings[1];
}

const getSortParams = columnIndex => ({
sortBy: {
index: sortIndex,
direction: sortDirection,
defaultDirection: 'asc',
},
onSort: (_event, index, direction) => {
if (direction !== null && index !== null) {
searchParams.set(
'order',
`${index === 0 ? 'name' : 'updated_at'} ${direction}`
);
window.location.search = searchParams.toString();
}
},
columnIndex,
});
return (
<TableComposable variant="compact" borders="compactBorderless">
<Thead>
<Tr>
<Th sort={getSortParams(0)}>{__('Name')}</Th>
<Th>{__('Hostgroups')}</Th>
<Th>{__('Hosts')}</Th>
<Th>{__('Variables')}</Th>
<Th sort={getSortParams(6)}>{__('Imported at')}</Th>
<Th>{__('Actions')}</Th>
</Tr>
</Thead>
<Tbody>
{props.ansibleRoles.map(role => (
<AnsibleRolesTableRow key={role.name} ansibleRole={role} />
))}
</Tbody>
</TableComposable>
);
};

AnsibleRolesTable.propTypes = {
ansibleRoles: PropTypes.array,
};

AnsibleRolesTable.defaultProps = {
ansibleRoles: [],
};
57 changes: 57 additions & 0 deletions webpack/components/AnsibleRoles/AnsibleRolesTable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { AnsibleRolesTable } from './AnsibleRolesTable';

describe('AnsibleRolesTable', () => {
const demoRoles = [
{
name: 'demo_role_0',
hostgroupsCount: 0,
hostsCount: 0,
id: 1,
updatedAt: '2024-01-28',
importTime: '3 days ago',
variablesCount: 1,
},
{
name: 'demo_role_1',
hostgroupsCount: 0,
hostsCount: 0,
id: 2,
updatedAt: '2024-01-29',
importTime: '2 days ago',
variablesCount: 2,
},
{
name: 'demo_role_2',
hostgroupsCount: 0,
hostsCount: 0,
id: 3,
updatedAt: '2024-01-30',
importTime: '1 day ago',
variablesCount: 3,
},
];

it('should render the table', () => {
const { container } = render(
<AnsibleRolesTable ansibleRoles={demoRoles} />
);
expect(container.getElementsByTagName('tr')).toHaveLength(
demoRoles.length + 1
);
});
it('should sort correctly', () => {
Object.defineProperty(window, 'location', {
value: new URL('https://test.url/ansible/ansible_roles'),
writable: true,
});
render(<AnsibleRolesTable ansibleRoles={demoRoles} />);

const importedButton = screen.getByText('Imported at');
importedButton.click(); // asc

expect(global.window.location.search).toContain('order=updated_at+asc');
});
});
42 changes: 42 additions & 0 deletions webpack/components/AnsibleRoles/components/AnsibleRolesTableRow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Tr, Td } from '@patternfly/react-table';
import PropTypes from 'prop-types';
import { AnsibleRolesTableRowActionButton } from './AnsibleRolesTableRowActionButton';

export const AnsibleRolesTableRow = props => (
<Tr>
<Td dataLabel="name">{props.ansibleRole.name}</Td>
<Td dataLabel="hostgroups">
<a href={`/hostgroups?search=ansible_role+%3D+${props.ansibleRole.name}`}>
{props.ansibleRole.hostgroupsCount}
</a>
</Td>
<Td dataLabel="hosts">
<a href={`/hosts?search=ansible_role+%3D+${props.ansibleRole.name}`}>
{props.ansibleRole.hostsCount}
</a>
</Td>
<Td dataLabel="Variablen">
<a
href={`/ansible/ansible_variables?search=ansible_role+%3D+${props.ansibleRole.name}`}
>
{props.ansibleRole.variablesCount}
</a>
</Td>
<Td dataLabel="imported">{props.ansibleRole.importTime}</Td>
<Td dataLabel="actions">
<AnsibleRolesTableRowActionButton
ansibleRoleName={props.ansibleRole.name}
ansibleRoleId={props.ansibleRole.id}
/>
</Td>
</Tr>
);

AnsibleRolesTableRow.propTypes = {
ansibleRole: PropTypes.object,
};

AnsibleRolesTableRow.defaultProps = {
ansibleRole: {},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import {
Dropdown,
DropdownToggle,
DropdownToggleAction,
DropdownItem,
} from '@patternfly/react-core';
import PropTypes from 'prop-types';
import { translate as __, sprintf } from 'foremanReact/common/I18n';

export const AnsibleRolesTableRowActionButton = props => {
const [isActionOpen, setIsActionOpen] = React.useState(false);
const onActionToggle = actionOpen => {
setIsActionOpen(actionOpen);
};

const dropdownItems = [
<DropdownItem
key="delete_action"
component="a"
data-method="delete"
href={`/ansible/ansible_roles/${props.ansibleRoleId}`}
data-confirm={sprintf(__('Delete %s?'), props.ansibleRoleName)}
>
{__('Delete')}
</DropdownItem>,
];

return (
<React.Fragment>
<Dropdown
toggle={
<DropdownToggle
id="toggle-split-button-action-primary"
splitButtonItems={[
<DropdownToggleAction
key="action"
onClick={() => {
window.location = `/ansible/ansible_variables?search=ansible_role+%3D+${props.ansibleRoleName}`;
}}
>
{__('Variables')}
</DropdownToggleAction>,
]}
toggleVariant="default"
splitButtonVariant="action"
onToggle={onActionToggle}
/>
}
isOpen={isActionOpen}
dropdownItems={dropdownItems}
/>{' '}
</React.Fragment>
);
};

AnsibleRolesTableRowActionButton.propTypes = {
ansibleRoleId: PropTypes.number,
ansibleRoleName: PropTypes.string,
};

AnsibleRolesTableRowActionButton.defaultProps = {
ansibleRoleId: 0,
ansibleRoleName: '',
};
6 changes: 6 additions & 0 deletions webpack/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ReportJsonViewer from './components/ReportJsonViewer';
import AnsibleRolesSwitcher from './components/AnsibleRolesSwitcher';
import WrappedImportRolesAndVariables from './components/AnsibleRolesAndVariables';
import reducer from './reducer';
import { AnsibleRolesTable } from './components/AnsibleRoles/AnsibleRolesTable';

componentRegistry.register({
name: 'ReportJsonViewer',
Expand All @@ -19,4 +20,9 @@ componentRegistry.register({
type: WrappedImportRolesAndVariables,
});

componentRegistry.register({
name: 'AnsibleRolesTable',
type: AnsibleRolesTable,
});

injectReducer('foremanAnsible', reducer);

0 comments on commit 21c7da7

Please sign in to comment.