mirror of
https://github.com/ansible/awx.git
synced 2026-03-08 21:19:26 -02:30
Merge pull request #9011 from AlexSCorey/PreLingUI2
Updates files to pre lingUI upgrade work Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -17,95 +17,57 @@ const readTeams = async queryParams => TeamsAPI.read(queryParams);
|
|||||||
|
|
||||||
const readTeamsOptions = async () => TeamsAPI.readOptions();
|
const readTeamsOptions = async () => TeamsAPI.readOptions();
|
||||||
|
|
||||||
class AddResourceRole extends React.Component {
|
function AddResourceRole({ onSave, onClose, roles, i18n, resource }) {
|
||||||
constructor(props) {
|
const [selectedResource, setSelectedResource] = useState(null);
|
||||||
super(props);
|
const [selectedResourceRows, setSelectedResourceRows] = useState([]);
|
||||||
|
const [selectedRoleRows, setSelectedRoleRows] = useState([]);
|
||||||
this.state = {
|
const [currentStepId, setCurrentStepId] = useState(1);
|
||||||
selectedResource: null,
|
const [maxEnabledStep, setMaxEnabledStep] = useState(1);
|
||||||
selectedResourceRows: [],
|
|
||||||
selectedRoleRows: [],
|
|
||||||
currentStepId: 1,
|
|
||||||
maxEnabledStep: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
|
|
||||||
this
|
|
||||||
);
|
|
||||||
this.handleResourceSelect = this.handleResourceSelect.bind(this);
|
|
||||||
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
|
|
||||||
this.handleWizardNext = this.handleWizardNext.bind(this);
|
|
||||||
this.handleWizardSave = this.handleWizardSave.bind(this);
|
|
||||||
this.handleWizardGoToStep = this.handleWizardGoToStep.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResourceCheckboxClick(user) {
|
|
||||||
const { selectedResourceRows, currentStepId } = this.state;
|
|
||||||
|
|
||||||
|
const handleResourceCheckboxClick = user => {
|
||||||
const selectedIndex = selectedResourceRows.findIndex(
|
const selectedIndex = selectedResourceRows.findIndex(
|
||||||
selectedRow => selectedRow.id === user.id
|
selectedRow => selectedRow.id === user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedIndex > -1) {
|
if (selectedIndex > -1) {
|
||||||
selectedResourceRows.splice(selectedIndex, 1);
|
selectedResourceRows.splice(selectedIndex, 1);
|
||||||
const stateToUpdate = { selectedResourceRows };
|
|
||||||
if (selectedResourceRows.length === 0) {
|
if (selectedResourceRows.length === 0) {
|
||||||
stateToUpdate.maxEnabledStep = currentStepId;
|
setMaxEnabledStep(currentStepId);
|
||||||
}
|
}
|
||||||
this.setState(stateToUpdate);
|
setSelectedRoleRows(selectedResourceRows);
|
||||||
} else {
|
} else {
|
||||||
this.setState(prevState => ({
|
setSelectedResourceRows([...selectedResourceRows, user]);
|
||||||
selectedResourceRows: [...prevState.selectedResourceRows, user],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleRoleCheckboxClick(role) {
|
|
||||||
const { selectedRoleRows } = this.state;
|
|
||||||
|
|
||||||
|
const handleRoleCheckboxClick = role => {
|
||||||
const selectedIndex = selectedRoleRows.findIndex(
|
const selectedIndex = selectedRoleRows.findIndex(
|
||||||
selectedRow => selectedRow.id === role.id
|
selectedRow => selectedRow.id === role.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedIndex > -1) {
|
if (selectedIndex > -1) {
|
||||||
selectedRoleRows.splice(selectedIndex, 1);
|
selectedRoleRows.splice(selectedIndex, 1);
|
||||||
this.setState({ selectedRoleRows });
|
setSelectedRoleRows(selectedRoleRows);
|
||||||
} else {
|
} else {
|
||||||
this.setState(prevState => ({
|
setSelectedRoleRows([...selectedRoleRows, role]);
|
||||||
selectedRoleRows: [...prevState.selectedRoleRows, role],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
handleResourceSelect(resourceType) {
|
const handleResourceSelect = resourceType => {
|
||||||
this.setState({
|
setSelectedResource(resourceType);
|
||||||
selectedResource: resourceType,
|
setSelectedResourceRows([]);
|
||||||
selectedResourceRows: [],
|
setSelectedRoleRows([]);
|
||||||
selectedRoleRows: [],
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWizardNext(step) {
|
const handleWizardNext = step => {
|
||||||
this.setState({
|
setCurrentStepId(step.id);
|
||||||
currentStepId: step.id,
|
setMaxEnabledStep(step.id);
|
||||||
maxEnabledStep: step.id,
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWizardGoToStep(step) {
|
const handleWizardGoToStep = step => {
|
||||||
this.setState({
|
setCurrentStepId(step.id);
|
||||||
currentStepId: step.id,
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleWizardSave() {
|
|
||||||
const { onSave } = this.props;
|
|
||||||
const {
|
|
||||||
selectedResourceRows,
|
|
||||||
selectedRoleRows,
|
|
||||||
selectedResource,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
|
const handleWizardSave = async () => {
|
||||||
try {
|
try {
|
||||||
const roleRequests = [];
|
const roleRequests = [];
|
||||||
|
|
||||||
@@ -134,201 +96,186 @@ class AddResourceRole extends React.Component {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// TODO: handle this error
|
// TODO: handle this error
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Object roles can be user only, so we remove them when
|
||||||
|
// showing role choices for team access
|
||||||
|
const selectableRoles = { ...roles };
|
||||||
|
if (selectedResource === 'teams') {
|
||||||
|
Object.keys(roles).forEach(key => {
|
||||||
|
if (selectableRoles[key].user_only) {
|
||||||
|
delete selectableRoles[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const userSearchColumns = [
|
||||||
const {
|
{
|
||||||
selectedResource,
|
name: i18n._(t`Username`),
|
||||||
selectedResourceRows,
|
key: 'username__icontains',
|
||||||
selectedRoleRows,
|
isDefault: true,
|
||||||
currentStepId,
|
},
|
||||||
maxEnabledStep,
|
{
|
||||||
} = this.state;
|
name: i18n._(t`First Name`),
|
||||||
const { onClose, roles, i18n, resource } = this.props;
|
key: 'first_name__icontains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Last Name`),
|
||||||
|
key: 'last_name__icontains',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const userSortColumns = [
|
||||||
|
{
|
||||||
|
name: i18n._(t`Username`),
|
||||||
|
key: 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`First Name`),
|
||||||
|
key: 'first_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Last Name`),
|
||||||
|
key: 'last_name',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const teamSearchColumns = [
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created By (Username)`),
|
||||||
|
key: 'created_by__username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified By (Username)`),
|
||||||
|
key: 'modified_by__username',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Object roles can be user only, so we remove them when
|
const teamSortColumns = [
|
||||||
// showing role choices for team access
|
{
|
||||||
const selectableRoles = { ...roles };
|
name: i18n._(t`Name`),
|
||||||
if (selectedResource === 'teams') {
|
key: 'name',
|
||||||
Object.keys(roles).forEach(key => {
|
},
|
||||||
if (selectableRoles[key].user_only) {
|
];
|
||||||
delete selectableRoles[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const userSearchColumns = [
|
let wizardTitle = '';
|
||||||
{
|
|
||||||
name: i18n._(t`Username`),
|
|
||||||
key: 'username__icontains',
|
|
||||||
isDefault: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`First Name`),
|
|
||||||
key: 'first_name__icontains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Last Name`),
|
|
||||||
key: 'last_name__icontains',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const userSortColumns = [
|
switch (selectedResource) {
|
||||||
{
|
case 'users':
|
||||||
name: i18n._(t`Username`),
|
wizardTitle = i18n._(t`Add User Roles`);
|
||||||
key: 'username',
|
break;
|
||||||
},
|
case 'teams':
|
||||||
{
|
wizardTitle = i18n._(t`Add Team Roles`);
|
||||||
name: i18n._(t`First Name`),
|
break;
|
||||||
key: 'first_name',
|
default:
|
||||||
},
|
wizardTitle = i18n._(t`Add Roles`);
|
||||||
{
|
}
|
||||||
name: i18n._(t`Last Name`),
|
|
||||||
key: 'last_name',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamSearchColumns = [
|
const steps = [
|
||||||
{
|
{
|
||||||
name: i18n._(t`Name`),
|
id: 1,
|
||||||
key: 'name',
|
name: i18n._(t`Select a Resource Type`),
|
||||||
isDefault: true,
|
component: (
|
||||||
},
|
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||||
{
|
<div style={{ width: '100%', marginBottom: '10px' }}>
|
||||||
name: i18n._(t`Created By (Username)`),
|
{i18n._(
|
||||||
key: 'created_by__username',
|
t`Choose the type of resource that will be receiving new roles. For example, if you'd like to add new roles to a set of users please choose Users and click Next. You'll be able to select the specific resources in the next step.`
|
||||||
},
|
|
||||||
{
|
|
||||||
name: i18n._(t`Modified By (Username)`),
|
|
||||||
key: 'modified_by__username',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamSortColumns = [
|
|
||||||
{
|
|
||||||
name: i18n._(t`Name`),
|
|
||||||
key: 'name',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let wizardTitle = '';
|
|
||||||
|
|
||||||
switch (selectedResource) {
|
|
||||||
case 'users':
|
|
||||||
wizardTitle = i18n._(t`Add User Roles`);
|
|
||||||
break;
|
|
||||||
case 'teams':
|
|
||||||
wizardTitle = i18n._(t`Add Team Roles`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
wizardTitle = i18n._(t`Add Roles`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: i18n._(t`Select a Resource Type`),
|
|
||||||
component: (
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
||||||
<div style={{ width: '100%', marginBottom: '10px' }}>
|
|
||||||
{i18n._(
|
|
||||||
t`Choose the type of resource that will be receiving new roles. For example, if you'd like to add new roles to a set of users please choose Users and click Next. You'll be able to select the specific resources in the next step.`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={selectedResource === 'users'}
|
|
||||||
label={i18n._(t`Users`)}
|
|
||||||
dataCy="add-role-users"
|
|
||||||
ariaLabel={i18n._(t`Users`)}
|
|
||||||
onClick={() => this.handleResourceSelect('users')}
|
|
||||||
/>
|
|
||||||
{resource?.type === 'credential' &&
|
|
||||||
!resource?.organization ? null : (
|
|
||||||
<SelectableCard
|
|
||||||
isSelected={selectedResource === 'teams'}
|
|
||||||
label={i18n._(t`Teams`)}
|
|
||||||
dataCy="add-role-teams"
|
|
||||||
ariaLabel={i18n._(t`Teams`)}
|
|
||||||
onClick={() => this.handleResourceSelect('teams')}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
<SelectableCard
|
||||||
enableNext: selectedResource !== null,
|
isSelected={selectedResource === 'users'}
|
||||||
},
|
label={i18n._(t`Users`)}
|
||||||
{
|
ariaLabel={i18n._(t`Users`)}
|
||||||
id: 2,
|
dataCy="add-role-users"
|
||||||
name: i18n._(t`Select Items from List`),
|
onClick={() => handleResourceSelect('users')}
|
||||||
component: (
|
|
||||||
<Fragment>
|
|
||||||
{selectedResource === 'users' && (
|
|
||||||
<SelectResourceStep
|
|
||||||
searchColumns={userSearchColumns}
|
|
||||||
sortColumns={userSortColumns}
|
|
||||||
displayKey="username"
|
|
||||||
onRowClick={this.handleResourceCheckboxClick}
|
|
||||||
fetchItems={readUsers}
|
|
||||||
fetchOptions={readUsersOptions}
|
|
||||||
selectedLabel={i18n._(t`Selected`)}
|
|
||||||
selectedResourceRows={selectedResourceRows}
|
|
||||||
sortedColumnKey="username"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{selectedResource === 'teams' && (
|
|
||||||
<SelectResourceStep
|
|
||||||
searchColumns={teamSearchColumns}
|
|
||||||
sortColumns={teamSortColumns}
|
|
||||||
onRowClick={this.handleResourceCheckboxClick}
|
|
||||||
fetchItems={readTeams}
|
|
||||||
fetchOptions={readTeamsOptions}
|
|
||||||
selectedLabel={i18n._(t`Selected`)}
|
|
||||||
selectedResourceRows={selectedResourceRows}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
),
|
|
||||||
enableNext: selectedResourceRows.length > 0,
|
|
||||||
canJumpTo: maxEnabledStep >= 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: i18n._(t`Select Roles to Apply`),
|
|
||||||
component: (
|
|
||||||
<SelectRoleStep
|
|
||||||
onRolesClick={this.handleRoleCheckboxClick}
|
|
||||||
roles={selectableRoles}
|
|
||||||
selectedListKey={selectedResource === 'users' ? 'username' : 'name'}
|
|
||||||
selectedListLabel={i18n._(t`Selected`)}
|
|
||||||
selectedResourceRows={selectedResourceRows}
|
|
||||||
selectedRoleRows={selectedRoleRows}
|
|
||||||
/>
|
/>
|
||||||
),
|
{resource?.type === 'credential' && !resource?.organization ? null : (
|
||||||
nextButtonText: i18n._(t`Save`),
|
<SelectableCard
|
||||||
enableNext: selectedRoleRows.length > 0,
|
isSelected={selectedResource === 'teams'}
|
||||||
canJumpTo: maxEnabledStep >= 3,
|
label={i18n._(t`Teams`)}
|
||||||
},
|
ariaLabel={i18n._(t`Teams`)}
|
||||||
];
|
dataCy="add-role-teams"
|
||||||
|
onClick={() => handleResourceSelect('teams')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableNext: selectedResource !== null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: i18n._(t`Select Items from List`),
|
||||||
|
component: (
|
||||||
|
<Fragment>
|
||||||
|
{selectedResource === 'users' && (
|
||||||
|
<SelectResourceStep
|
||||||
|
searchColumns={userSearchColumns}
|
||||||
|
sortColumns={userSortColumns}
|
||||||
|
displayKey="username"
|
||||||
|
onRowClick={handleResourceCheckboxClick}
|
||||||
|
fetchItems={readUsers}
|
||||||
|
fetchOptions={readUsersOptions}
|
||||||
|
selectedLabel={i18n._(t`Selected`)}
|
||||||
|
selectedResourceRows={selectedResourceRows}
|
||||||
|
sortedColumnKey="username"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedResource === 'teams' && (
|
||||||
|
<SelectResourceStep
|
||||||
|
searchColumns={teamSearchColumns}
|
||||||
|
sortColumns={teamSortColumns}
|
||||||
|
onRowClick={handleResourceCheckboxClick}
|
||||||
|
fetchItems={readTeams}
|
||||||
|
fetchOptions={readTeamsOptions}
|
||||||
|
selectedLabel={i18n._(t`Selected`)}
|
||||||
|
selectedResourceRows={selectedResourceRows}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
),
|
||||||
|
enableNext: selectedResourceRows.length > 0,
|
||||||
|
canJumpTo: maxEnabledStep >= 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: i18n._(t`Select Roles to Apply`),
|
||||||
|
component: (
|
||||||
|
<SelectRoleStep
|
||||||
|
onRolesClick={handleRoleCheckboxClick}
|
||||||
|
roles={selectableRoles}
|
||||||
|
selectedListKey={selectedResource === 'users' ? 'username' : 'name'}
|
||||||
|
selectedListLabel={i18n._(t`Selected`)}
|
||||||
|
selectedResourceRows={selectedResourceRows}
|
||||||
|
selectedRoleRows={selectedRoleRows}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
nextButtonText: i18n._(t`Save`),
|
||||||
|
enableNext: selectedRoleRows.length > 0,
|
||||||
|
canJumpTo: maxEnabledStep >= 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const currentStep = steps.find(step => step.id === currentStepId);
|
const currentStep = steps.find(step => step.id === currentStepId);
|
||||||
|
|
||||||
// TODO: somehow internationalize steps and currentStep.nextButtonText
|
// TODO: somehow internationalize steps and currentStep.nextButtonText
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<Wizard
|
||||||
style={{ overflow: 'scroll' }}
|
style={{ overflow: 'scroll' }}
|
||||||
isOpen
|
isOpen
|
||||||
onNext={this.handleWizardNext}
|
onNext={handleWizardNext}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSave={this.handleWizardSave}
|
onSave={handleWizardSave}
|
||||||
onGoToStep={this.handleWizardGoToStep}
|
onGoToStep={step => handleWizardGoToStep(step)}
|
||||||
steps={steps}
|
steps={steps}
|
||||||
title={wizardTitle}
|
title={wizardTitle}
|
||||||
nextButtonText={currentStep.nextButtonText || undefined}
|
nextButtonText={currentStep.nextButtonText || undefined}
|
||||||
backButtonText={i18n._(t`Back`)}
|
backButtonText={i18n._(t`Back`)}
|
||||||
cancelButtonText={i18n._(t`Cancel`)}
|
cancelButtonText={i18n._(t`Cancel`)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddResourceRole.propTypes = {
|
AddResourceRole.propTypes = {
|
||||||
|
|||||||
@@ -1,22 +1,46 @@
|
|||||||
/* eslint-disable react/jsx-pascal-case */
|
/* eslint-disable react/jsx-pascal-case */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import AddResourceRole, { _AddResourceRole } from './AddResourceRole';
|
import AddResourceRole, { _AddResourceRole } from './AddResourceRole';
|
||||||
import { TeamsAPI, UsersAPI } from '../../api';
|
import { TeamsAPI, UsersAPI } from '../../api';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api/models/Teams');
|
||||||
|
jest.mock('../../api/models/Users');
|
||||||
|
|
||||||
|
// TODO: Once error handling is functional in
|
||||||
|
// this component write tests for it
|
||||||
|
|
||||||
describe('<_AddResourceRole />', () => {
|
describe('<_AddResourceRole />', () => {
|
||||||
UsersAPI.read.mockResolvedValue({
|
UsersAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: 2,
|
count: 2,
|
||||||
results: [
|
results: [
|
||||||
{ id: 1, username: 'foo' },
|
{ id: 1, username: 'foo', url: '' },
|
||||||
{ id: 2, username: 'bar' },
|
{ id: 2, username: 'bar', url: '' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
UsersAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { related: {}, actions: { GET: {} } },
|
||||||
|
});
|
||||||
|
TeamsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: 2,
|
||||||
|
results: [
|
||||||
|
{ id: 1, name: 'Team foo', url: '' },
|
||||||
|
{ id: 2, name: 'Team bar', url: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
TeamsAPI.readOptions.mockResolvedValue({
|
||||||
|
data: { related: {}, actions: { GET: {} } },
|
||||||
|
});
|
||||||
const roles = {
|
const roles = {
|
||||||
admin_role: {
|
admin_role: {
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
@@ -39,191 +63,165 @@ describe('<_AddResourceRole />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('handleRoleCheckboxClick properly updates state', () => {
|
test('should save properly', async () => {
|
||||||
const wrapper = shallow(
|
let wrapper;
|
||||||
<_AddResourceRole
|
act(() => {
|
||||||
onClose={() => {}}
|
wrapper = mountWithContexts(
|
||||||
onSave={() => {}}
|
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
||||||
roles={roles}
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
i18n={{ _: val => val.toString() }}
|
);
|
||||||
/>
|
|
||||||
);
|
|
||||||
wrapper.setState({
|
|
||||||
selectedRoleRows: [
|
|
||||||
{
|
|
||||||
description: 'Can manage all aspects of the organization',
|
|
||||||
name: 'Admin',
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
wrapper.instance().handleRoleCheckboxClick({
|
wrapper.update();
|
||||||
description: 'Can manage all aspects of the organization',
|
|
||||||
name: 'Admin',
|
// Step 1
|
||||||
id: 1,
|
|
||||||
});
|
|
||||||
expect(wrapper.state('selectedRoleRows')).toEqual([]);
|
|
||||||
wrapper.instance().handleRoleCheckboxClick({
|
|
||||||
description: 'Can manage all aspects of the organization',
|
|
||||||
name: 'Admin',
|
|
||||||
id: 1,
|
|
||||||
});
|
|
||||||
expect(wrapper.state('selectedRoleRows')).toEqual([
|
|
||||||
{
|
|
||||||
description: 'Can manage all aspects of the organization',
|
|
||||||
name: 'Admin',
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
test('handleResourceCheckboxClick properly updates state', () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<_AddResourceRole
|
|
||||||
onClose={() => {}}
|
|
||||||
onSave={() => {}}
|
|
||||||
roles={roles}
|
|
||||||
i18n={{ _: val => val.toString() }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
wrapper.setState({
|
|
||||||
selectedResourceRows: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: 'foobar',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
wrapper.instance().handleResourceCheckboxClick({
|
|
||||||
id: 1,
|
|
||||||
username: 'foobar',
|
|
||||||
});
|
|
||||||
expect(wrapper.state('selectedResourceRows')).toEqual([]);
|
|
||||||
wrapper.instance().handleResourceCheckboxClick({
|
|
||||||
id: 1,
|
|
||||||
username: 'foobar',
|
|
||||||
});
|
|
||||||
expect(wrapper.state('selectedResourceRows')).toEqual([
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: 'foobar',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
test('clicking user/team cards updates state', () => {
|
|
||||||
const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect');
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
|
||||||
{ context: { network: { handleHttpError: () => {} } } }
|
|
||||||
).find('AddResourceRole');
|
|
||||||
const selectableCardWrapper = wrapper.find('SelectableCard');
|
const selectableCardWrapper = wrapper.find('SelectableCard');
|
||||||
expect(selectableCardWrapper.length).toBe(2);
|
expect(selectableCardWrapper.length).toBe(2);
|
||||||
selectableCardWrapper.first().simulate('click');
|
act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')());
|
||||||
expect(spy).toHaveBeenCalledWith('users');
|
wrapper.update();
|
||||||
expect(wrapper.state('selectedResource')).toBe('users');
|
await act(async () =>
|
||||||
selectableCardWrapper.at(1).simulate('click');
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
expect(spy).toHaveBeenCalledWith('teams');
|
|
||||||
expect(wrapper.state('selectedResource')).toBe('teams');
|
|
||||||
});
|
|
||||||
test('handleResourceSelect clears out selected lists and sets selectedResource', () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<_AddResourceRole
|
|
||||||
onClose={() => {}}
|
|
||||||
onSave={() => {}}
|
|
||||||
roles={roles}
|
|
||||||
i18n={{ _: val => val.toString() }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
wrapper.setState({
|
wrapper.update();
|
||||||
selectedResource: 'teams',
|
|
||||||
selectedResourceRows: [
|
// Step 2
|
||||||
{
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
id: 1,
|
act(() =>
|
||||||
username: 'foobar',
|
wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true)
|
||||||
},
|
);
|
||||||
],
|
wrapper.update();
|
||||||
selectedRoleRows: [
|
expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe(
|
||||||
{
|
true
|
||||||
description: 'Can manage all aspects of the organization',
|
);
|
||||||
id: 1,
|
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||||
name: 'Admin',
|
wrapper.update();
|
||||||
},
|
|
||||||
],
|
// Step 3
|
||||||
});
|
act(() =>
|
||||||
wrapper.instance().handleResourceSelect('users');
|
wrapper.find('Checkbox[aria-label="Admin"]').invoke('onChange')(true)
|
||||||
expect(wrapper.state()).toEqual({
|
);
|
||||||
selectedResource: 'users',
|
wrapper.update();
|
||||||
selectedResourceRows: [],
|
expect(wrapper.find('Checkbox[aria-label="Admin"]').prop('isChecked')).toBe(
|
||||||
selectedRoleRows: [],
|
true
|
||||||
currentStepId: 1,
|
);
|
||||||
maxEnabledStep: 1,
|
|
||||||
});
|
// Save
|
||||||
wrapper.instance().handleResourceSelect('teams');
|
await act(async () =>
|
||||||
expect(wrapper.state()).toEqual({
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
selectedResource: 'teams',
|
);
|
||||||
selectedResourceRows: [],
|
expect(UsersAPI.associateRole).toBeCalledWith(1, 1);
|
||||||
selectedRoleRows: [],
|
|
||||||
currentStepId: 1,
|
|
||||||
maxEnabledStep: 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
|
|
||||||
const handleSave = jest.fn();
|
test('should successfuly click user/team cards', async () => {
|
||||||
const wrapper = mountWithContexts(
|
let wrapper;
|
||||||
<AddResourceRole onClose={() => {}} onSave={handleSave} roles={roles} />,
|
act(() => {
|
||||||
{ context: { network: { handleHttpError: () => {} } } }
|
wrapper = mountWithContexts(
|
||||||
).find('AddResourceRole');
|
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
||||||
wrapper.setState({
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
selectedResource: 'users',
|
);
|
||||||
selectedResourceRows: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
username: 'foobar',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectedRoleRows: [
|
|
||||||
{
|
|
||||||
description: 'Can manage all aspects of the organization',
|
|
||||||
id: 1,
|
|
||||||
name: 'Admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: 'May run any executable resources in the organization',
|
|
||||||
id: 2,
|
|
||||||
name: 'Execute',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await wrapper.instance().handleWizardSave();
|
wrapper.update();
|
||||||
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
|
|
||||||
expect(handleSave).toHaveBeenCalled();
|
const selectableCardWrapper = wrapper.find('SelectableCard');
|
||||||
wrapper.setState({
|
expect(selectableCardWrapper.length).toBe(2);
|
||||||
selectedResource: 'teams',
|
act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')());
|
||||||
selectedResourceRows: [
|
wrapper.update();
|
||||||
{
|
|
||||||
id: 1,
|
await waitForElement(
|
||||||
name: 'foobar',
|
wrapper,
|
||||||
},
|
'SelectableCard[label="Users"]',
|
||||||
],
|
el => el.prop('isSelected') === true
|
||||||
selectedRoleRows: [
|
);
|
||||||
{
|
act(() => wrapper.find('SelectableCard[label="Teams"]').prop('onClick')());
|
||||||
description: 'Can manage all aspects of the organization',
|
wrapper.update();
|
||||||
id: 1,
|
|
||||||
name: 'Admin',
|
await waitForElement(
|
||||||
},
|
wrapper,
|
||||||
{
|
'SelectableCard[label="Teams"]',
|
||||||
description: 'May run any executable resources in the organization',
|
el => el.prop('isSelected') === true
|
||||||
id: 2,
|
);
|
||||||
name: 'Execute',
|
});
|
||||||
},
|
|
||||||
],
|
test('should reset values with resource type changes', async () => {
|
||||||
|
let wrapper;
|
||||||
|
act(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
||||||
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await wrapper.instance().handleWizardSave();
|
wrapper.update();
|
||||||
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);
|
|
||||||
expect(handleSave).toHaveBeenCalled();
|
// Step 1
|
||||||
|
const selectableCardWrapper = wrapper.find('SelectableCard');
|
||||||
|
expect(selectableCardWrapper.length).toBe(2);
|
||||||
|
act(() => wrapper.find('SelectableCard[label="Users"]').prop('onClick')());
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
// Step 2
|
||||||
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
|
act(() =>
|
||||||
|
wrapper.find('DataListCheck[name="foo"]').invoke('onChange')(true)
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('DataListCheck[name="foo"]').prop('checked')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
act(() =>
|
||||||
|
wrapper.find('Checkbox[aria-label="Admin"]').invoke('onChange')(true)
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Checkbox[aria-label="Admin"]').prop('isChecked')).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Go back to step 1
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('WizardNavItem[content="Select a Resource Type"]')
|
||||||
|
.find('button')
|
||||||
|
.prop('onClick')({ id: 1 });
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('WizardNavItem[content="Select a Resource Type"]')
|
||||||
|
.prop('isCurrent')
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
// Go back to step 1 and this time select teams. Doing so should clear following steps
|
||||||
|
act(() => wrapper.find('SelectableCard[label="Teams"]').prop('onClick')());
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
// Make sure no teams have been selected
|
||||||
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
|
wrapper
|
||||||
|
.find('DataListCheck')
|
||||||
|
.map(item => expect(item.prop('checked')).toBe(false));
|
||||||
|
act(() => wrapper.find('Button[type="submit"]').prop('onClick')());
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
// Make sure that no roles have been selected
|
||||||
|
wrapper
|
||||||
|
.find('Checkbox')
|
||||||
|
.map(card => expect(card.prop('isChecked')).toBe(false));
|
||||||
|
|
||||||
|
// Make sure the save button is disabled
|
||||||
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not display team as a choice in case credential does not have organization', () => {
|
test('should not display team as a choice in case credential does not have organization', () => {
|
||||||
const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect');
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AddResourceRole
|
<AddResourceRole
|
||||||
onClose={() => {}}
|
onClose={() => {}}
|
||||||
@@ -232,11 +230,13 @@ describe('<_AddResourceRole />', () => {
|
|||||||
resource={{ type: 'credential', organization: null }}
|
resource={{ type: 'credential', organization: null }}
|
||||||
/>,
|
/>,
|
||||||
{ context: { network: { handleHttpError: () => {} } } }
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
).find('AddResourceRole');
|
);
|
||||||
const selectableCardWrapper = wrapper.find('SelectableCard');
|
|
||||||
expect(selectableCardWrapper.length).toBe(1);
|
expect(wrapper.find('SelectableCard').length).toBe(1);
|
||||||
selectableCardWrapper.first().simulate('click');
|
wrapper.find('SelectableCard[label="Users"]').simulate('click');
|
||||||
expect(spy).toHaveBeenCalledWith('users');
|
wrapper.update();
|
||||||
expect(wrapper.state('selectedResource')).toBe('users');
|
expect(
|
||||||
|
wrapper.find('SelectableCard[label="Users"]').prop('isSelected')
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,59 +7,55 @@ import { t } from '@lingui/macro';
|
|||||||
import CheckboxCard from './CheckboxCard';
|
import CheckboxCard from './CheckboxCard';
|
||||||
import SelectedList from '../SelectedList';
|
import SelectedList from '../SelectedList';
|
||||||
|
|
||||||
class RolesStep extends React.Component {
|
function RolesStep({
|
||||||
render() {
|
onRolesClick,
|
||||||
const {
|
roles,
|
||||||
onRolesClick,
|
selectedListKey,
|
||||||
roles,
|
selectedListLabel,
|
||||||
selectedListKey,
|
selectedResourceRows,
|
||||||
selectedListLabel,
|
selectedRoleRows,
|
||||||
selectedResourceRows,
|
i18n,
|
||||||
selectedRoleRows,
|
}) {
|
||||||
i18n,
|
return (
|
||||||
} = this.props;
|
<Fragment>
|
||||||
|
<div>
|
||||||
return (
|
{i18n._(
|
||||||
<Fragment>
|
t`Choose roles to apply to the selected resources. Note that all selected roles will be applied to all selected resources.`
|
||||||
<div>
|
)}
|
||||||
{i18n._(
|
</div>
|
||||||
t`Choose roles to apply to the selected resources. Note that all selected roles will be applied to all selected resources.`
|
<div>
|
||||||
)}
|
{selectedResourceRows.length > 0 && (
|
||||||
</div>
|
<SelectedList
|
||||||
<div>
|
displayKey={selectedListKey}
|
||||||
{selectedResourceRows.length > 0 && (
|
isReadOnly
|
||||||
<SelectedList
|
label={selectedListLabel || i18n._(t`Selected`)}
|
||||||
displayKey={selectedListKey}
|
selected={selectedResourceRows}
|
||||||
isReadOnly
|
/>
|
||||||
label={selectedListLabel || i18n._(t`Selected`)}
|
)}
|
||||||
selected={selectedResourceRows}
|
</div>
|
||||||
/>
|
<div
|
||||||
)}
|
style={{
|
||||||
</div>
|
display: 'grid',
|
||||||
<div
|
gridTemplateColumns: '1fr 1fr',
|
||||||
style={{
|
gap: '20px 20px',
|
||||||
display: 'grid',
|
marginTop: '20px',
|
||||||
gridTemplateColumns: '1fr 1fr',
|
}}
|
||||||
gap: '20px 20px',
|
>
|
||||||
marginTop: '20px',
|
{Object.keys(roles).map(role => (
|
||||||
}}
|
<CheckboxCard
|
||||||
>
|
description={roles[role].description}
|
||||||
{Object.keys(roles).map(role => (
|
itemId={roles[role].id}
|
||||||
<CheckboxCard
|
isSelected={selectedRoleRows.some(
|
||||||
description={roles[role].description}
|
item => item.id === roles[role].id
|
||||||
itemId={roles[role].id}
|
)}
|
||||||
isSelected={selectedRoleRows.some(
|
key={roles[role].id}
|
||||||
item => item.id === roles[role].id
|
name={roles[role].name}
|
||||||
)}
|
onSelect={() => onRolesClick(roles[role])}
|
||||||
key={roles[role].id}
|
/>
|
||||||
name={roles[role].name}
|
))}
|
||||||
onSelect={() => onRolesClick(roles[role])}
|
</div>
|
||||||
/>
|
</Fragment>
|
||||||
))}
|
);
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RolesStep.propTypes = {
|
RolesStep.propTypes = {
|
||||||
|
|||||||
@@ -12,52 +12,44 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
||||||
|
|
||||||
class AnsibleSelect extends React.Component {
|
function AnsibleSelect({
|
||||||
constructor(props) {
|
id,
|
||||||
super(props);
|
data,
|
||||||
this.onSelectChange = this.onSelectChange.bind(this);
|
i18n,
|
||||||
}
|
isValid,
|
||||||
|
onBlur,
|
||||||
onSelectChange(val, event) {
|
value,
|
||||||
const { onChange, name } = this.props;
|
className,
|
||||||
|
isDisabled,
|
||||||
|
onChange,
|
||||||
|
name,
|
||||||
|
}) {
|
||||||
|
const onSelectChange = (val, event) => {
|
||||||
event.target.name = name;
|
event.target.name = name;
|
||||||
onChange(event, val);
|
onChange(event, val);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const {
|
<FormSelect
|
||||||
id,
|
id={id}
|
||||||
data,
|
value={value}
|
||||||
i18n,
|
onChange={onSelectChange}
|
||||||
isValid,
|
onBlur={onBlur}
|
||||||
onBlur,
|
aria-label={i18n._(t`Select Input`)}
|
||||||
value,
|
validated={isValid ? 'default' : 'error'}
|
||||||
className,
|
className={className}
|
||||||
isDisabled,
|
isDisabled={isDisabled}
|
||||||
} = this.props;
|
>
|
||||||
|
{data.map(option => (
|
||||||
return (
|
<FormSelectOption
|
||||||
<FormSelect
|
key={option.key}
|
||||||
id={id}
|
value={option.value}
|
||||||
value={value}
|
label={option.label}
|
||||||
onChange={this.onSelectChange}
|
isDisabled={option.isDisabled}
|
||||||
onBlur={onBlur}
|
/>
|
||||||
aria-label={i18n._(t`Select Input`)}
|
))}
|
||||||
validated={isValid ? 'default' : 'error'}
|
</FormSelect>
|
||||||
className={className}
|
);
|
||||||
isDisabled={isDisabled}
|
|
||||||
>
|
|
||||||
{data.map(option => (
|
|
||||||
<FormSelectOption
|
|
||||||
key={option.key}
|
|
||||||
value={option.value}
|
|
||||||
label={option.label}
|
|
||||||
isDisabled={option.isDisabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</FormSelect>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Option = shape({
|
const Option = shape({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import AnsibleSelect, { _AnsibleSelect } from './AnsibleSelect';
|
import AnsibleSelect from './AnsibleSelect';
|
||||||
|
|
||||||
const mockData = [
|
const mockData = [
|
||||||
{
|
{
|
||||||
@@ -16,6 +16,7 @@ const mockData = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe('<AnsibleSelect />', () => {
|
describe('<AnsibleSelect />', () => {
|
||||||
|
const onChange = jest.fn();
|
||||||
test('initially renders succesfully', async () => {
|
test('initially renders succesfully', async () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
@@ -29,19 +30,18 @@ describe('<AnsibleSelect />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('calls "onSelectChange" on dropdown select change', () => {
|
test('calls "onSelectChange" on dropdown select change', () => {
|
||||||
const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange');
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="bar"
|
id="bar"
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => {}}
|
onChange={onChange}
|
||||||
data={mockData}
|
data={mockData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
wrapper.find('select').simulate('change');
|
wrapper.find('select').simulate('change');
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(onChange).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Returns correct select options', () => {
|
test('Returns correct select options', () => {
|
||||||
|
|||||||
@@ -31,35 +31,31 @@ const ToolbarItem = styled(PFToolbarItem)`
|
|||||||
|
|
||||||
// TODO: Recommend renaming this component to avoid confusion
|
// TODO: Recommend renaming this component to avoid confusion
|
||||||
// with ExpandingContainer
|
// with ExpandingContainer
|
||||||
class ExpandCollapse extends React.Component {
|
function ExpandCollapse({ isCompact, onCompact, onExpand, i18n }) {
|
||||||
render() {
|
return (
|
||||||
const { isCompact, onCompact, onExpand, i18n } = this.props;
|
<Fragment>
|
||||||
|
<ToolbarItem>
|
||||||
return (
|
<Button
|
||||||
<Fragment>
|
variant="plain"
|
||||||
<ToolbarItem>
|
aria-label={i18n._(t`Collapse`)}
|
||||||
<Button
|
onClick={onCompact}
|
||||||
variant="plain"
|
isActive={isCompact}
|
||||||
aria-label={i18n._(t`Collapse`)}
|
>
|
||||||
onClick={onCompact}
|
<BarsIcon />
|
||||||
isActive={isCompact}
|
</Button>
|
||||||
>
|
</ToolbarItem>
|
||||||
<BarsIcon />
|
<ToolbarItem>
|
||||||
</Button>
|
<Button
|
||||||
</ToolbarItem>
|
variant="plain"
|
||||||
<ToolbarItem>
|
aria-label={i18n._(t`Expand`)}
|
||||||
<Button
|
onClick={onExpand}
|
||||||
variant="plain"
|
isActive={!isCompact}
|
||||||
aria-label={i18n._(t`Expand`)}
|
>
|
||||||
onClick={onExpand}
|
<EqualsIcon />
|
||||||
isActive={!isCompact}
|
</Button>
|
||||||
>
|
</ToolbarItem>
|
||||||
<EqualsIcon />
|
</Fragment>
|
||||||
</Button>
|
);
|
||||||
</ToolbarItem>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ExpandCollapse.propTypes = {
|
ExpandCollapse.propTypes = {
|
||||||
|
|||||||
@@ -7,69 +7,71 @@ import { t } from '@lingui/macro';
|
|||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import { Role } from '../../types';
|
import { Role } from '../../types';
|
||||||
|
|
||||||
class DeleteRoleConfirmationModal extends React.Component {
|
function DeleteRoleConfirmationModal({
|
||||||
static propTypes = {
|
role,
|
||||||
role: Role.isRequired,
|
username,
|
||||||
username: string,
|
onCancel,
|
||||||
onCancel: func.isRequired,
|
onConfirm,
|
||||||
onConfirm: func.isRequired,
|
i18n,
|
||||||
};
|
}) {
|
||||||
|
const isTeamRole = () => {
|
||||||
static defaultProps = {
|
|
||||||
username: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
isTeamRole() {
|
|
||||||
const { role } = this.props;
|
|
||||||
return typeof role.team_id !== 'undefined';
|
return typeof role.team_id !== 'undefined';
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const title = i18n._(
|
||||||
const { role, username, onCancel, onConfirm, i18n } = this.props;
|
t`Remove ${isTeamRole() ? i18n._(t`Team`) : i18n._(t`User`)} Access`
|
||||||
const title = i18n._(
|
);
|
||||||
t`Remove ${this.isTeamRole() ? i18n._(t`Team`) : i18n._(t`User`)} Access`
|
return (
|
||||||
);
|
<AlertModal
|
||||||
return (
|
variant="danger"
|
||||||
<AlertModal
|
title={title}
|
||||||
variant="danger"
|
isOpen
|
||||||
title={title}
|
onClose={onCancel}
|
||||||
isOpen
|
actions={[
|
||||||
onClose={onCancel}
|
<Button
|
||||||
actions={[
|
key="delete"
|
||||||
<Button
|
variant="danger"
|
||||||
key="delete"
|
aria-label={i18n._(t`Confirm delete`)}
|
||||||
variant="danger"
|
onClick={onConfirm}
|
||||||
aria-label={i18n._(t`Confirm delete`)}
|
>
|
||||||
onClick={onConfirm}
|
{i18n._(t`Delete`)}
|
||||||
>
|
</Button>,
|
||||||
{i18n._(t`Delete`)}
|
<Button key="cancel" variant="secondary" onClick={onCancel}>
|
||||||
</Button>,
|
{i18n._(t`Cancel`)}
|
||||||
<Button key="cancel" variant="secondary" onClick={onCancel}>
|
</Button>,
|
||||||
{i18n._(t`Cancel`)}
|
]}
|
||||||
</Button>,
|
>
|
||||||
]}
|
{isTeamRole() ? (
|
||||||
>
|
<Fragment>
|
||||||
{this.isTeamRole() ? (
|
{i18n._(
|
||||||
<Fragment>
|
t`Are you sure you want to remove ${role.name} access from ${role.team_name}? Doing so affects all members of the team.`
|
||||||
{i18n._(
|
)}
|
||||||
t`Are you sure you want to remove ${role.name} access from ${role.team_name}? Doing so affects all members of the team.`
|
<br />
|
||||||
)}
|
<br />
|
||||||
<br />
|
{i18n._(
|
||||||
<br />
|
t`If you only want to remove access for this particular user, please remove them from the team.`
|
||||||
{i18n._(
|
)}
|
||||||
t`If you only want to remove access for this particular user, please remove them from the team.`
|
</Fragment>
|
||||||
)}
|
) : (
|
||||||
</Fragment>
|
<Fragment>
|
||||||
) : (
|
{i18n._(
|
||||||
<Fragment>
|
t`Are you sure you want to remove ${role.name} access from ${username}?`
|
||||||
{i18n._(
|
)}
|
||||||
t`Are you sure you want to remove ${role.name} access from ${username}?`
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</AlertModal>
|
||||||
)}
|
);
|
||||||
</AlertModal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeleteRoleConfirmationModal.propTypes = {
|
||||||
|
role: Role.isRequired,
|
||||||
|
username: string,
|
||||||
|
onCancel: func.isRequired,
|
||||||
|
onConfirm: func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeleteRoleConfirmationModal.defaultProps = {
|
||||||
|
username: '',
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(DeleteRoleConfirmationModal);
|
export default withI18n()(DeleteRoleConfirmationModal);
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
setDeletionRole(role);
|
setDeletionRole(role);
|
||||||
setShowDeleteModal(true);
|
setShowDeleteModal(true);
|
||||||
}}
|
}}
|
||||||
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,19 +24,13 @@ const DataListItemCells = styled(PFDataListItemCells)`
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class ResourceAccessListItem extends React.Component {
|
function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) {
|
||||||
static propTypes = {
|
ResourceAccessListItem.propTypes = {
|
||||||
accessRecord: AccessRecord.isRequired,
|
accessRecord: AccessRecord.isRequired,
|
||||||
onRoleDelete: func.isRequired,
|
onRoleDelete: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
const getRoleLists = () => {
|
||||||
super(props);
|
|
||||||
this.renderChip = this.renderChip.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoleLists() {
|
|
||||||
const { accessRecord } = this.props;
|
|
||||||
const teamRoles = [];
|
const teamRoles = [];
|
||||||
const userRoles = [];
|
const userRoles = [];
|
||||||
|
|
||||||
@@ -52,10 +46,9 @@ class ResourceAccessListItem extends React.Component {
|
|||||||
accessRecord.summary_fields.direct_access.map(sort);
|
accessRecord.summary_fields.direct_access.map(sort);
|
||||||
accessRecord.summary_fields.indirect_access.map(sort);
|
accessRecord.summary_fields.indirect_access.map(sort);
|
||||||
return [teamRoles, userRoles];
|
return [teamRoles, userRoles];
|
||||||
}
|
};
|
||||||
|
|
||||||
renderChip(role) {
|
const renderChip = role => {
|
||||||
const { accessRecord, onRoleDelete } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
key={role.id}
|
key={role.id}
|
||||||
@@ -67,79 +60,76 @@ class ResourceAccessListItem extends React.Component {
|
|||||||
{role.name}
|
{role.name}
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const [teamRoles, userRoles] = getRoleLists();
|
||||||
const { accessRecord, i18n } = this.props;
|
|
||||||
const [teamRoles, userRoles] = this.getRoleLists();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<DataListItem
|
||||||
aria-labelledby="access-list-item"
|
aria-labelledby="access-list-item"
|
||||||
key={accessRecord.id}
|
key={accessRecord.id}
|
||||||
id={`${accessRecord.id}`}
|
id={`${accessRecord.id}`}
|
||||||
>
|
>
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key="name">
|
<DataListCell key="name">
|
||||||
{accessRecord.username && (
|
{accessRecord.username && (
|
||||||
<TextContent>
|
<TextContent>
|
||||||
{accessRecord.id ? (
|
{accessRecord.id ? (
|
||||||
<Text component={TextVariants.h6}>
|
<Text component={TextVariants.h6}>
|
||||||
<Link
|
<Link
|
||||||
to={{ pathname: `/users/${accessRecord.id}/details` }}
|
to={{ pathname: `/users/${accessRecord.id}/details` }}
|
||||||
css="font-weight: bold"
|
css="font-weight: bold"
|
||||||
>
|
>
|
||||||
{accessRecord.username}
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text component={TextVariants.h6} css="font-weight: bold">
|
|
||||||
{accessRecord.username}
|
{accessRecord.username}
|
||||||
</Text>
|
</Link>
|
||||||
)}
|
</Text>
|
||||||
</TextContent>
|
) : (
|
||||||
)}
|
<Text component={TextVariants.h6} css="font-weight: bold">
|
||||||
{accessRecord.first_name || accessRecord.last_name ? (
|
{accessRecord.username}
|
||||||
<DetailList stacked>
|
</Text>
|
||||||
<Detail
|
)}
|
||||||
label={i18n._(t`Name`)}
|
</TextContent>
|
||||||
value={`${accessRecord.first_name} ${accessRecord.last_name}`}
|
)}
|
||||||
/>
|
{accessRecord.first_name || accessRecord.last_name ? (
|
||||||
</DetailList>
|
|
||||||
) : null}
|
|
||||||
</DataListCell>,
|
|
||||||
<DataListCell key="roles">
|
|
||||||
<DetailList stacked>
|
<DetailList stacked>
|
||||||
{userRoles.length > 0 && (
|
<Detail
|
||||||
<Detail
|
label={i18n._(t`Name`)}
|
||||||
label={i18n._(t`User Roles`)}
|
value={`${accessRecord.first_name} ${accessRecord.last_name}`}
|
||||||
value={
|
/>
|
||||||
<ChipGroup numChips={5} totalChips={userRoles.length}>
|
|
||||||
{userRoles.map(this.renderChip)}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{teamRoles.length > 0 && (
|
|
||||||
<Detail
|
|
||||||
label={i18n._(t`Team Roles`)}
|
|
||||||
value={
|
|
||||||
<ChipGroup numChips={5} totalChips={teamRoles.length}>
|
|
||||||
{teamRoles.map(this.renderChip)}
|
|
||||||
</ChipGroup>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DetailList>
|
</DetailList>
|
||||||
</DataListCell>,
|
) : null}
|
||||||
]}
|
</DataListCell>,
|
||||||
/>
|
<DataListCell key="roles">
|
||||||
</DataListItemRow>
|
<DetailList stacked>
|
||||||
</DataListItem>
|
{userRoles.length > 0 && (
|
||||||
);
|
<Detail
|
||||||
}
|
label={i18n._(t`User Roles`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5} totalChips={userRoles.length}>
|
||||||
|
{userRoles.map(renderChip)}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{teamRoles.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Team Roles`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5} totalChips={teamRoles.length}>
|
||||||
|
{teamRoles.map(renderChip)}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DetailList>
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(ResourceAccessListItem);
|
export default withI18n()(ResourceAccessListItem);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { useLocation, withRouter } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -31,140 +31,110 @@ const NoOptionDropdown = styled.div`
|
|||||||
border-bottom-color: var(--pf-global--BorderColor--200);
|
border-bottom-color: var(--pf-global--BorderColor--200);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class Sort extends React.Component {
|
function Sort({ columns, qsConfig, onSort, i18n }) {
|
||||||
constructor(props) {
|
const location = useLocation();
|
||||||
super(props);
|
const [isSortDropdownOpen, setIsSortDropdownOpen] = useState(false);
|
||||||
|
|
||||||
let sortKey;
|
let sortKey;
|
||||||
let sortOrder;
|
let sortOrder;
|
||||||
let isNumeric;
|
let isNumeric;
|
||||||
|
|
||||||
const { qsConfig, location } = this.props;
|
const queryParams = parseQueryString(qsConfig, location.search);
|
||||||
const queryParams = parseQueryString(qsConfig, location.search);
|
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
||||||
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
sortKey = queryParams.order_by.substr(1);
|
||||||
sortKey = queryParams.order_by.substr(1);
|
sortOrder = 'descending';
|
||||||
sortOrder = 'descending';
|
} else if (queryParams.order_by) {
|
||||||
} else if (queryParams.order_by) {
|
sortKey = queryParams.order_by;
|
||||||
sortKey = queryParams.order_by;
|
sortOrder = 'ascending';
|
||||||
sortOrder = 'ascending';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qsConfig.integerFields.find(field => field === sortKey)) {
|
|
||||||
isNumeric = true;
|
|
||||||
} else {
|
|
||||||
isNumeric = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isSortDropdownOpen: false,
|
|
||||||
sortKey,
|
|
||||||
sortOrder,
|
|
||||||
isNumeric,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.handleDropdownToggle = this.handleDropdownToggle.bind(this);
|
|
||||||
this.handleDropdownSelect = this.handleDropdownSelect.bind(this);
|
|
||||||
this.handleSort = this.handleSort.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownToggle(isSortDropdownOpen) {
|
if (qsConfig.integerFields.find(field => field === sortKey)) {
|
||||||
this.setState({ isSortDropdownOpen });
|
isNumeric = true;
|
||||||
|
} else {
|
||||||
|
isNumeric = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownSelect({ target }) {
|
const handleDropdownToggle = isOpen => {
|
||||||
const { columns, onSort, qsConfig } = this.props;
|
setIsSortDropdownOpen(isOpen);
|
||||||
const { sortOrder } = this.state;
|
};
|
||||||
|
|
||||||
|
const handleDropdownSelect = ({ target }) => {
|
||||||
const { innerText } = target;
|
const { innerText } = target;
|
||||||
|
|
||||||
const [{ key: sortKey }] = columns.filter(({ name }) => name === innerText);
|
const [{ key }] = columns.filter(({ name }) => name === innerText);
|
||||||
|
sortKey = key;
|
||||||
let isNumeric;
|
if (qsConfig.integerFields.find(field => field === key)) {
|
||||||
|
|
||||||
if (qsConfig.integerFields.find(field => field === sortKey)) {
|
|
||||||
isNumeric = true;
|
isNumeric = true;
|
||||||
} else {
|
} else {
|
||||||
isNumeric = false;
|
isNumeric = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ isSortDropdownOpen: false, sortKey, isNumeric });
|
setIsSortDropdownOpen(false);
|
||||||
onSort(sortKey, sortOrder);
|
onSort(sortKey, sortOrder);
|
||||||
}
|
};
|
||||||
|
|
||||||
handleSort() {
|
const handleSort = () => {
|
||||||
const { onSort } = this.props;
|
onSort(sortKey, sortOrder === 'ascending' ? 'descending' : 'ascending');
|
||||||
const { sortKey, sortOrder } = this.state;
|
};
|
||||||
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
|
|
||||||
this.setState({ sortOrder: newSortOrder });
|
|
||||||
onSort(sortKey, newSortOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
const { up } = DropdownPosition;
|
||||||
const { up } = DropdownPosition;
|
|
||||||
const { columns, i18n } = this.props;
|
|
||||||
const { isSortDropdownOpen, sortKey, sortOrder, isNumeric } = this.state;
|
|
||||||
|
|
||||||
const defaultSortedColumn = columns.find(({ key }) => key === sortKey);
|
const defaultSortedColumn = columns.find(({ key }) => key === sortKey);
|
||||||
|
|
||||||
if (!defaultSortedColumn) {
|
if (!defaultSortedColumn) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'sortKey must match one of the column keys, check the sortColumns prop passed to <Sort />'
|
'sortKey must match one of the column keys, check the sortColumns prop passed to <Sort />'
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedColumnName = defaultSortedColumn?.name;
|
|
||||||
|
|
||||||
const sortDropdownItems = columns
|
|
||||||
.filter(({ key }) => key !== sortKey)
|
|
||||||
.map(({ key, name }) => (
|
|
||||||
<DropdownItem key={key} component="button">
|
|
||||||
{name}
|
|
||||||
</DropdownItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
let SortIcon;
|
|
||||||
if (isNumeric) {
|
|
||||||
SortIcon =
|
|
||||||
sortOrder === 'ascending'
|
|
||||||
? SortNumericDownIcon
|
|
||||||
: SortNumericDownAltIcon;
|
|
||||||
} else {
|
|
||||||
SortIcon =
|
|
||||||
sortOrder === 'ascending' ? SortAlphaDownIcon : SortAlphaDownAltIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
{sortedColumnName && (
|
|
||||||
<InputGroup>
|
|
||||||
{(sortDropdownItems.length > 0 && (
|
|
||||||
<Dropdown
|
|
||||||
onToggle={this.handleDropdownToggle}
|
|
||||||
onSelect={this.handleDropdownSelect}
|
|
||||||
direction={up}
|
|
||||||
isOpen={isSortDropdownOpen}
|
|
||||||
toggle={
|
|
||||||
<DropdownToggle
|
|
||||||
id="awx-sort"
|
|
||||||
onToggle={this.handleDropdownToggle}
|
|
||||||
>
|
|
||||||
{sortedColumnName}
|
|
||||||
</DropdownToggle>
|
|
||||||
}
|
|
||||||
dropdownItems={sortDropdownItems}
|
|
||||||
/>
|
|
||||||
)) || <NoOptionDropdown>{sortedColumnName}</NoOptionDropdown>}
|
|
||||||
<Button
|
|
||||||
variant={ButtonVariant.control}
|
|
||||||
aria-label={i18n._(t`Sort`)}
|
|
||||||
onClick={this.handleSort}
|
|
||||||
>
|
|
||||||
<SortIcon />
|
|
||||||
</Button>
|
|
||||||
</InputGroup>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortedColumnName = defaultSortedColumn?.name;
|
||||||
|
|
||||||
|
const sortDropdownItems = columns
|
||||||
|
.filter(({ key }) => key !== sortKey)
|
||||||
|
.map(({ key, name }) => (
|
||||||
|
<DropdownItem key={key} component="button">
|
||||||
|
{name}
|
||||||
|
</DropdownItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
let SortIcon;
|
||||||
|
if (isNumeric) {
|
||||||
|
SortIcon =
|
||||||
|
sortOrder === 'ascending' ? SortNumericDownIcon : SortNumericDownAltIcon;
|
||||||
|
} else {
|
||||||
|
SortIcon =
|
||||||
|
sortOrder === 'ascending' ? SortAlphaDownIcon : SortAlphaDownAltIcon;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{sortedColumnName && (
|
||||||
|
<InputGroup>
|
||||||
|
{(sortDropdownItems.length > 0 && (
|
||||||
|
<Dropdown
|
||||||
|
onToggle={handleDropdownToggle}
|
||||||
|
onSelect={handleDropdownSelect}
|
||||||
|
direction={up}
|
||||||
|
isOpen={isSortDropdownOpen}
|
||||||
|
toggle={
|
||||||
|
<DropdownToggle id="awx-sort" onToggle={handleDropdownToggle}>
|
||||||
|
{sortedColumnName}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
dropdownItems={sortDropdownItems}
|
||||||
|
/>
|
||||||
|
)) || <NoOptionDropdown>{sortedColumnName}</NoOptionDropdown>}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.control}
|
||||||
|
aria-label={i18n._(t`Sort`)}
|
||||||
|
onClick={handleSort}
|
||||||
|
>
|
||||||
|
<SortIcon />
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Sort.propTypes = {
|
Sort.propTypes = {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import Sort from './Sort';
|
import Sort from './Sort';
|
||||||
|
|
||||||
describe('<Sort />', () => {
|
describe('<Sort />', () => {
|
||||||
@@ -105,7 +110,7 @@ describe('<Sort />', () => {
|
|||||||
expect(onSort).toHaveBeenCalledWith('foo', 'ascending');
|
expect(onSort).toHaveBeenCalledWith('foo', 'ascending');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Changing dropdown correctly passes back new sort key', () => {
|
test('Changing dropdown correctly passes back new sort key', async () => {
|
||||||
const qsConfig = {
|
const qsConfig = {
|
||||||
namespace: 'item',
|
namespace: 'item',
|
||||||
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
|
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
|
||||||
@@ -131,44 +136,18 @@ describe('<Sort />', () => {
|
|||||||
|
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Sort qsConfig={qsConfig} columns={columns} onSort={onSort} />
|
<Sort qsConfig={qsConfig} columns={columns} onSort={onSort} />
|
||||||
).find('Sort');
|
);
|
||||||
|
act(() => wrapper.find('Dropdown').invoke('onToggle')(true));
|
||||||
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Bar' } });
|
wrapper.update();
|
||||||
|
await waitForElement(wrapper, 'Dropdown', el => el.prop('isOpen') === true);
|
||||||
|
wrapper
|
||||||
|
.find('li')
|
||||||
|
.at(0)
|
||||||
|
.prop('onClick')({ target: { innerText: 'Bar' } });
|
||||||
|
wrapper.update();
|
||||||
expect(onSort).toBeCalledWith('bar', 'ascending');
|
expect(onSort).toBeCalledWith('bar', 'ascending');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Opening dropdown correctly updates state', () => {
|
|
||||||
const qsConfig = {
|
|
||||||
namespace: 'item',
|
|
||||||
defaultParams: { page: 1, page_size: 5, order_by: 'foo' },
|
|
||||||
integerFields: ['page', 'page_size'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'Foo',
|
|
||||||
key: 'foo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Bar',
|
|
||||||
key: 'bar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Bakery',
|
|
||||||
key: 'bakery',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const onSort = jest.fn();
|
|
||||||
|
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<Sort qsConfig={qsConfig} columns={columns} onSort={onSort} />
|
|
||||||
).find('Sort');
|
|
||||||
expect(wrapper.state('isSortDropdownOpen')).toEqual(false);
|
|
||||||
wrapper.instance().handleDropdownToggle(true);
|
|
||||||
expect(wrapper.state('isSortDropdownOpen')).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('It displays correct sort icon', () => {
|
test('It displays correct sort icon', () => {
|
||||||
const forwardNumericIconSelector = 'SortNumericDownIcon';
|
const forwardNumericIconSelector = 'SortNumericDownIcon';
|
||||||
const reverseNumericIconSelector = 'SortNumericDownAltIcon';
|
const reverseNumericIconSelector = 'SortNumericDownAltIcon';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { I18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
@@ -518,7 +518,7 @@ class JobOutput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { job, i18n } = this.props;
|
const { job } = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
contentError,
|
contentError,
|
||||||
@@ -596,15 +596,21 @@ class JobOutput extends Component {
|
|||||||
</OutputWrapper>
|
</OutputWrapper>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{deletionError && (
|
{deletionError && (
|
||||||
<AlertModal
|
<>
|
||||||
isOpen={deletionError}
|
<I18n>
|
||||||
variant="danger"
|
{({ i18n }) => (
|
||||||
onClose={() => this.setState({ deletionError: null })}
|
<AlertModal
|
||||||
title={i18n._(t`Job Delete Error`)}
|
isOpen={deletionError}
|
||||||
label={i18n._(t`Job Delete Error`)}
|
variant="danger"
|
||||||
>
|
onClose={() => this.setState({ deletionError: null })}
|
||||||
<ErrorDetail error={deletionError} />
|
title={i18n._(t`Job Delete Error`)}
|
||||||
</AlertModal>
|
label={i18n._(t`Job Delete Error`)}
|
||||||
|
>
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</I18n>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -612,4 +618,4 @@ class JobOutput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { JobOutput as _JobOutput };
|
export { JobOutput as _JobOutput };
|
||||||
export default withI18n()(withRouter(JobOutput));
|
export default withRouter(JobOutput);
|
||||||
|
|||||||
@@ -310,7 +310,17 @@ function ProjectForm({ i18n, project, submitError, ...props }) {
|
|||||||
const { summary_fields = {} } = project;
|
const { summary_fields = {} } = project;
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [scmSubFormState, setScmSubFormState] = useState(null);
|
const [scmSubFormState, setScmSubFormState] = useState({
|
||||||
|
scm_url: '',
|
||||||
|
scm_branch: '',
|
||||||
|
scm_refspec: '',
|
||||||
|
credential: '',
|
||||||
|
scm_clean: false,
|
||||||
|
scm_delete_on_update: false,
|
||||||
|
scm_update_on_launch: false,
|
||||||
|
allow_override: false,
|
||||||
|
scm_update_cache_timeout: 0,
|
||||||
|
});
|
||||||
const [scmTypeOptions, setScmTypeOptions] = useState(null);
|
const [scmTypeOptions, setScmTypeOptions] = useState(null);
|
||||||
const [credentials, setCredentials] = useState({
|
const [credentials, setCredentials] = useState({
|
||||||
scm: { typeId: null, value: null },
|
scm: { typeId: null, value: null },
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { SyncIcon } from '@patternfly/react-icons';
|
|||||||
import { number } from 'prop-types';
|
import { number } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
|
||||||
import AlertModal from '../../../components/AlertModal';
|
import AlertModal from '../../../components/AlertModal';
|
||||||
import ErrorDetail from '../../../components/ErrorDetail';
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
import { ProjectsAPI } from '../../../api';
|
import { ProjectsAPI } from '../../../api';
|
||||||
|
|||||||
@@ -27,71 +27,68 @@ const DataListAction = styled(_DataListAction)`
|
|||||||
grid-template-columns: 40px;
|
grid-template-columns: 40px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class TeamListItem extends React.Component {
|
function TeamListItem({ team, isSelected, onSelect, detailUrl, i18n }) {
|
||||||
static propTypes = {
|
TeamListItem.propTypes = {
|
||||||
team: Team.isRequired,
|
team: Team.isRequired,
|
||||||
detailUrl: string.isRequired,
|
detailUrl: string.isRequired,
|
||||||
isSelected: bool.isRequired,
|
isSelected: bool.isRequired,
|
||||||
onSelect: func.isRequired,
|
onSelect: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const labelId = `check-action-${team.id}`;
|
||||||
const { team, isSelected, onSelect, detailUrl, i18n } = this.props;
|
|
||||||
const labelId = `check-action-${team.id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem key={team.id} aria-labelledby={labelId} id={`${team.id}`}>
|
<DataListItem key={team.id} aria-labelledby={labelId} id={`${team.id}`}>
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
<DataListCheck
|
<DataListCheck
|
||||||
id={`select-team-${team.id}`}
|
id={`select-team-${team.id}`}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={labelId}
|
||||||
/>
|
/>
|
||||||
<DataListItemCells
|
<DataListItemCells
|
||||||
dataListCells={[
|
dataListCells={[
|
||||||
<DataListCell key="name">
|
<DataListCell key="name">
|
||||||
<Link id={labelId} to={`${detailUrl}`}>
|
<Link id={labelId} to={`${detailUrl}`}>
|
||||||
<b>{team.name}</b>
|
<b>{team.name}</b>
|
||||||
</Link>
|
</Link>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell key="organization">
|
<DataListCell key="organization">
|
||||||
{team.summary_fields.organization && (
|
{team.summary_fields.organization && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<b>{i18n._(t`Organization`)}</b>{' '}
|
<b>{i18n._(t`Organization`)}</b>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={`/organizations/${team.summary_fields.organization.id}/details`}
|
to={`/organizations/${team.summary_fields.organization.id}/details`}
|
||||||
>
|
>
|
||||||
<b>{team.summary_fields.organization.name}</b>
|
<b>{team.summary_fields.organization.name}</b>
|
||||||
</Link>
|
</Link>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<DataListAction
|
<DataListAction
|
||||||
aria-label="actions"
|
aria-label="actions"
|
||||||
aria-labelledby={labelId}
|
aria-labelledby={labelId}
|
||||||
id={labelId}
|
id={labelId}
|
||||||
>
|
>
|
||||||
{team.summary_fields.user_capabilities.edit ? (
|
{team.summary_fields.user_capabilities.edit ? (
|
||||||
<Tooltip content={i18n._(t`Edit Team`)} position="top">
|
<Tooltip content={i18n._(t`Edit Team`)} position="top">
|
||||||
<Button
|
<Button
|
||||||
aria-label={i18n._(t`Edit Team`)}
|
aria-label={i18n._(t`Edit Team`)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/teams/${team.id}/edit`}
|
to={`/teams/${team.id}/edit`}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)}
|
)}
|
||||||
</DataListAction>
|
</DataListAction>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export default withI18n()(TeamListItem);
|
export default withI18n()(TeamListItem);
|
||||||
|
|||||||
Reference in New Issue
Block a user