Run prettier on all the files in awx/ui_next

This commit is contained in:
mabashian 2019-06-28 09:26:11 -04:00
parent 051bbcaeb5
commit 55ce409a12
182 changed files with 3000 additions and 2747 deletions

View File

@ -35,12 +35,13 @@ const PageHeader = styled(PFPageHeader)`
`;
class App extends Component {
constructor (props) {
constructor(props) {
super(props);
// initialize with a closed navbar if window size is small
const isNavOpen = typeof window !== 'undefined'
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
const isNavOpen =
typeof window !== 'undefined' &&
window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
this.state = {
ansible_version: null,
@ -49,7 +50,7 @@ class App extends Component {
version: null,
isAboutModalOpen: false,
isNavOpen,
configError: null
configError: null,
};
this.handleLogout = this.handleLogout.bind(this);
@ -59,39 +60,48 @@ class App extends Component {
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
}
async componentDidMount () {
async componentDidMount() {
await this.loadConfig();
}
// eslint-disable-next-line class-methods-use-this
async handleLogout () {
async handleLogout() {
await RootAPI.logout();
window.location.replace('/#/login');
}
handleAboutOpen () {
handleAboutOpen() {
this.setState({ isAboutModalOpen: true });
}
handleAboutClose () {
handleAboutClose() {
this.setState({ isAboutModalOpen: false });
}
handleNavToggle () {
handleNavToggle() {
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
}
handleConfigErrorClose () {
handleConfigErrorClose() {
this.setState({
configError: null
configError: null,
});
}
async loadConfig () {
async loadConfig() {
try {
const [configRes, meRes] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
const { data: { ansible_version, custom_virtualenvs, version } } = configRes;
const { data: { results: [me] } } = meRes;
const [configRes, meRes] = await Promise.all([
ConfigAPI.read(),
MeAPI.read(),
]);
const {
data: { ansible_version, custom_virtualenvs, version },
} = configRes;
const {
data: {
results: [me],
},
} = meRes;
this.setState({ ansible_version, custom_virtualenvs, version, me });
} catch (err) {
@ -99,7 +109,7 @@ class App extends Component {
}
}
render () {
render() {
const {
ansible_version,
custom_virtualenvs,
@ -107,7 +117,7 @@ class App extends Component {
isNavOpen,
me,
version,
configError
configError,
} = this.state;
const {
i18n,
@ -122,47 +132,43 @@ class App extends Component {
onNavToggle={this.handleNavToggle}
logo={<BrandLogo />}
logoProps={{ href: '/' }}
toolbar={(
toolbar={
<PageHeaderToolbar
loggedInUser={me}
isAboutDisabled={!version}
onAboutClick={this.handleAboutOpen}
onLogoutClick={this.handleLogout}
/>
)}
}
/>
);
const sidebar = (
<PageSidebar
isNavOpen={isNavOpen}
nav={(
nav={
<Nav aria-label={navLabel}>
<NavList>
{routeGroups.map(
({ groupId, groupTitle, routes }) => (
<NavExpandableGroup
key={groupId}
groupId={groupId}
groupTitle={groupTitle}
routes={routes}
/>
)
)}
{routeGroups.map(({ groupId, groupTitle, routes }) => (
<NavExpandableGroup
key={groupId}
groupId={groupId}
groupTitle={groupTitle}
routes={routes}
/>
))}
</NavList>
</Nav>
)}
}
/>
);
return (
<Fragment>
<Page
usecondensed="True"
header={header}
sidebar={sidebar}
>
<ConfigProvider value={{ ansible_version, custom_virtualenvs, me, version }}>
<Page usecondensed="True" header={header} sidebar={sidebar}>
<ConfigProvider
value={{ ansible_version, custom_virtualenvs, me, version }}
>
{render({ routeGroups })}
</ConfigProvider>
</Page>

View File

@ -14,13 +14,14 @@ describe('<App />', () => {
const version = '222';
beforeEach(() => {
ConfigAPI.read = () => Promise.resolve({
data: {
ansible_version,
custom_virtualenvs,
version
}
});
ConfigAPI.read = () =>
Promise.resolve({
data: {
ansible_version,
custom_virtualenvs,
version,
},
});
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
});
@ -43,15 +44,13 @@ describe('<App />', () => {
{
groupTitle: 'Group Two',
groupId: 'group_two',
routes: [
{ title: 'Fiz', path: '/fiz' },
]
}
routes: [{ title: 'Fiz', path: '/fiz' }],
},
]}
render={({ routeGroups }) => (
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
)}
/>,
render={({ routeGroups }) =>
routeGroups.map(({ groupId }) => <div key={groupId} id={groupId} />)
}
/>
);
// page components
@ -70,7 +69,7 @@ describe('<App />', () => {
expect(appWrapper.find('#group_two').length).toBe(1);
});
test('opening the about modal renders prefetched config data', async (done) => {
test('opening the about modal renders prefetched config data', async done => {
const wrapper = mountWithContexts(<App />);
wrapper.update();
@ -83,7 +82,11 @@ describe('<App />', () => {
await waitForElement(wrapper, aboutDropdown);
wrapper.find(aboutDropdown).simulate('click');
const button = await waitForElement(wrapper, aboutButton, el => !el.props().disabled);
const button = await waitForElement(
wrapper,
aboutButton,
el => !el.props().disabled
);
button.simulate('click');
// check about modal content
@ -108,7 +111,7 @@ describe('<App />', () => {
});
});
test('onLogout makes expected call to api client', async (done) => {
test('onLogout makes expected call to api client', async done => {
const appWrapper = mountWithContexts(<App />).find('App');
appWrapper.instance().handleLogout();
await asyncFlush();

View File

@ -1,24 +1,21 @@
import React, { Component } from 'react';
import {
I18nProvider,
} from '@lingui/react';
import { I18nProvider } from '@lingui/react';
import {
HashRouter
} from 'react-router-dom';
import { HashRouter } from 'react-router-dom';
import ja from '../build/locales/ja/messages';
import en from '../build/locales/en/messages';
export function getLanguage (nav) {
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
export function getLanguage(nav) {
const language =
(nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
return languageWithoutRegionCode;
}
class RootProvider extends Component {
render () {
render() {
const { children } = this.props;
const catalogs = { en, ja };
@ -26,10 +23,7 @@ class RootProvider extends Component {
return (
<HashRouter>
<I18nProvider
language={language}
catalogs={catalogs}
>
<I18nProvider language={language} catalogs={catalogs}>
{children}
</I18nProvider>
</HashRouter>

View File

@ -3,8 +3,16 @@ import { getLanguage } from './RootProvider';
describe('RootProvider.jsx', () => {
test('getLanguage returns the expected language code', () => {
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
expect(
getLanguage({
languages: ['es-US'],
language: 'fr-FR',
userLanguage: 'en-US',
})
).toEqual('es');
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual(
'fr'
);
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
});
});

View File

@ -2,40 +2,40 @@ import axios from 'axios';
const defaultHttp = axios.create({
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken'
xsrfHeaderName: 'X-CSRFToken',
});
class Base {
constructor (http = defaultHttp, baseURL) {
constructor(http = defaultHttp, baseURL) {
this.http = http;
this.baseUrl = baseURL;
}
create (data) {
create(data) {
return this.http.post(this.baseUrl, data);
}
destroy (id) {
destroy(id) {
return this.http.delete(`${this.baseUrl}${id}/`);
}
read (params = {}) {
read(params = {}) {
return this.http.get(this.baseUrl, { params });
}
readDetail (id) {
readDetail(id) {
return this.http.get(`${this.baseUrl}${id}/`);
}
readOptions () {
readOptions() {
return this.http.options(this.baseUrl);
}
replace (id, data) {
replace(id, data) {
return this.http.put(`${this.baseUrl}${id}/`, data);
}
update (id, data) {
update(id, data) {
return this.http.patch(`${this.baseUrl}${id}/`, data);
}
}

View File

@ -3,14 +3,14 @@ import Base from './Base';
describe('Base', () => {
const createPromise = () => Promise.resolve();
const mockBaseURL = '/api/v2/organizations/';
const mockHttp = ({
const mockHttp = {
delete: jest.fn(createPromise),
get: jest.fn(createPromise),
options: jest.fn(createPromise),
patch: jest.fn(createPromise),
post: jest.fn(createPromise),
put: jest.fn(createPromise)
});
put: jest.fn(createPromise),
};
const BaseAPI = new Base(mockHttp, mockBaseURL);
@ -18,7 +18,7 @@ describe('Base', () => {
jest.clearAllMocks();
});
test('create calls http method with expected data', async (done) => {
test('create calls http method with expected data', async done => {
const data = { name: 'test ' };
await BaseAPI.create(data);
@ -28,17 +28,19 @@ describe('Base', () => {
done();
});
test('destroy calls http method with expected data', async (done) => {
test('destroy calls http method with expected data', async done => {
const resourceId = 1;
await BaseAPI.destroy(resourceId);
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
expect(mockHttp.delete.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.delete.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
done();
});
test('read calls http method with expected data', async (done) => {
test('read calls http method with expected data', async done => {
const defaultParams = {};
const testParams = { foo: 'bar' };
@ -51,17 +53,19 @@ describe('Base', () => {
done();
});
test('readDetail calls http method with expected data', async (done) => {
test('readDetail calls http method with expected data', async done => {
const resourceId = 1;
await BaseAPI.readDetail(resourceId);
expect(mockHttp.get).toHaveBeenCalledTimes(1);
expect(mockHttp.get.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.get.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
done();
});
test('readOptions calls http method with expected data', async (done) => {
test('readOptions calls http method with expected data', async done => {
await BaseAPI.readOptions();
expect(mockHttp.options).toHaveBeenCalledTimes(1);
@ -69,27 +73,31 @@ describe('Base', () => {
done();
});
test('replace calls http method with expected data', async (done) => {
test('replace calls http method with expected data', async done => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.replace(resourceId, data);
expect(mockHttp.put).toHaveBeenCalledTimes(1);
expect(mockHttp.put.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.put.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
done();
});
test('update calls http method with expected data', async (done) => {
test('update calls http method with expected data', async done => {
const resourceId = 1;
const data = { name: 'test ' };
await BaseAPI.update(resourceId, data);
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
expect(mockHttp.patch.mock.calls[0][0]).toEqual(`${mockBaseURL}${resourceId}/`);
expect(mockHttp.patch.mock.calls[0][0]).toEqual(
`${mockBaseURL}${resourceId}/`
);
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
done();

View File

@ -36,5 +36,5 @@ export {
UnifiedJobTemplatesAPI,
UnifiedJobsAPI,
UsersAPI,
WorkflowJobTemplatesAPI
WorkflowJobTemplatesAPI,
};

View File

@ -1,15 +1,23 @@
const InstanceGroupsMixin = (parent) => class extends parent {
readInstanceGroups (resourceId, params = {}) {
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, { params });
}
const InstanceGroupsMixin = parent =>
class extends parent {
readInstanceGroups(resourceId, params = {}) {
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, {
params,
});
}
associateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId });
}
associateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
id: instanceGroupId,
});
}
disassociateInstanceGroup (resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true });
}
};
disassociateInstanceGroup(resourceId, instanceGroupId) {
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
id: instanceGroupId,
disassociate: true,
});
}
};
export default InstanceGroupsMixin;

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class Config extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/config/';
}

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class InstanceGroups extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/instance_groups/';
}

View File

@ -2,7 +2,7 @@ import Base from '../Base';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class JobTemplates extends InstanceGroupsMixin(Base) {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/job_templates/';
@ -10,11 +10,11 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
this.readLaunch = this.readLaunch.bind(this);
}
launch (id, data) {
launch(id, data) {
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
}
readLaunch (id) {
readLaunch(id) {
return this.http.get(`${this.baseUrl}${id}/launch/`);
}
}

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class Jobs extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/jobs/';
}

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class Me extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/me/';
}

View File

@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin';
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/organizations/';
}
readAccessList (id, params = {}) {
readAccessList(id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/access_list/`, { params });
}
readTeams (id, params = {}) {
readTeams(id, params = {}) {
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
}
}

View File

@ -5,7 +5,7 @@ describe('OrganizationsAPI', () => {
const orgId = 1;
const searchParams = { foo: 'bar' };
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise) });
const mockHttp = { get: jest.fn(createPromise) };
const OrganizationsAPI = new Organizations(mockHttp);
@ -13,24 +13,36 @@ describe('OrganizationsAPI', () => {
jest.clearAllMocks();
});
test('read access list calls get with expected params', async (done) => {
test('read access list calls get with expected params', async done => {
await OrganizationsAPI.readAccessList(orgId);
await OrganizationsAPI.readAccessList(orgId, searchParams);
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: {} });
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: searchParams });
expect(mockHttp.get.mock.calls[0]).toContainEqual(
`/api/v2/organizations/${orgId}/access_list/`,
{ params: {} }
);
expect(mockHttp.get.mock.calls[1]).toContainEqual(
`/api/v2/organizations/${orgId}/access_list/`,
{ params: searchParams }
);
done();
});
test('read teams calls get with expected params', async (done) => {
test('read teams calls get with expected params', async done => {
await OrganizationsAPI.readTeams(orgId);
await OrganizationsAPI.readTeams(orgId, searchParams);
expect(mockHttp.get).toHaveBeenCalledTimes(2);
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: {} });
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: searchParams });
expect(mockHttp.get.mock.calls[0]).toContainEqual(
`/api/v2/organizations/${orgId}/teams/`,
{ params: {} }
);
expect(mockHttp.get.mock.calls[1]).toContainEqual(
`/api/v2/organizations/${orgId}/teams/`,
{ params: searchParams }
);
done();
});

View File

@ -1,13 +1,13 @@
import Base from '../Base';
class Root extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/';
this.redirectURL = '/api/v2/config/';
}
async login (username, password, redirect = this.redirectURL) {
async login(username, password, redirect = this.redirectURL) {
const loginUrl = `${this.baseUrl}login/`;
const un = encodeURIComponent(username);
const pw = encodeURIComponent(password);
@ -22,7 +22,7 @@ class Root extends Base {
return response;
}
logout () {
logout() {
return this.http.get(`${this.baseUrl}logout/`);
}
}

View File

@ -2,7 +2,10 @@ import Root from './Root';
describe('RootAPI', () => {
const createPromise = () => Promise.resolve();
const mockHttp = ({ get: jest.fn(createPromise), post: jest.fn(createPromise) });
const mockHttp = {
get: jest.fn(createPromise),
post: jest.fn(createPromise),
};
const RootAPI = new Root(mockHttp);
@ -10,7 +13,7 @@ describe('RootAPI', () => {
jest.clearAllMocks();
});
test('login calls get and post with expected content headers', async (done) => {
test('login calls get and post with expected content headers', async done => {
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
await RootAPI.login('username', 'password');
@ -24,18 +27,22 @@ describe('RootAPI', () => {
done();
});
test('login sends expected data', async (done) => {
test('login sends expected data', async done => {
await RootAPI.login('foo', 'bar');
await RootAPI.login('foo', 'bar', 'baz');
expect(mockHttp.post).toHaveBeenCalledTimes(2);
expect(mockHttp.post.mock.calls[0]).toContainEqual('username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F');
expect(mockHttp.post.mock.calls[1]).toContainEqual('username=foo&password=bar&next=baz');
expect(mockHttp.post.mock.calls[0]).toContainEqual(
'username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F'
);
expect(mockHttp.post.mock.calls[1]).toContainEqual(
'username=foo&password=bar&next=baz'
);
done();
});
test('logout calls expected http method', async (done) => {
test('logout calls expected http method', async done => {
await RootAPI.logout();
expect(mockHttp.get).toHaveBeenCalledTimes(1);

View File

@ -1,17 +1,20 @@
import Base from '../Base';
class Teams extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/teams/';
}
associateRole (teamId, roleId) {
associateRole(teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
}
disassociateRole (teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId, disassociate: true });
disassociateRole(teamId, roleId) {
return this.http.post(`${this.baseUrl}${teamId}/roles/`, {
id: roleId,
disassociate: true,
});
}
}

View File

@ -4,7 +4,7 @@ describe('TeamsAPI', () => {
const teamId = 1;
const roleId = 7;
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const mockHttp = { post: jest.fn(createPromise) };
const TeamsAPI = new Teams(mockHttp);
@ -12,23 +12,29 @@ describe('TeamsAPI', () => {
jest.clearAllMocks();
});
test('associate role calls post with expected params', async (done) => {
test('associate role calls post with expected params', async done => {
await TeamsAPI.associateRole(teamId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/teams/${teamId}/roles/`, { id: roleId });
expect(mockHttp.post.mock.calls[0]).toContainEqual(
`/api/v2/teams/${teamId}/roles/`,
{ id: roleId }
);
done();
});
test('read teams calls post with expected params', async (done) => {
test('read teams calls post with expected params', async done => {
await TeamsAPI.disassociateRole(teamId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/teams/${teamId}/roles/`, {
id: roleId,
disassociate: true
});
expect(mockHttp.post.mock.calls[0]).toContainEqual(
`/api/v2/teams/${teamId}/roles/`,
{
id: roleId,
disassociate: true,
}
);
done();
});

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class UnifiedJobTemplates extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/unified_job_templates/';
}

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class UnifiedJobs extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/unified_jobs/';
}

View File

@ -1,17 +1,20 @@
import Base from '../Base';
class Users extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/users/';
}
associateRole (userId, roleId) {
associateRole(userId, roleId) {
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId });
}
disassociateRole (userId, roleId) {
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId, disassociate: true });
disassociateRole(userId, roleId) {
return this.http.post(`${this.baseUrl}${userId}/roles/`, {
id: roleId,
disassociate: true,
});
}
}

View File

@ -4,7 +4,7 @@ describe('UsersAPI', () => {
const userId = 1;
const roleId = 7;
const createPromise = () => Promise.resolve();
const mockHttp = ({ post: jest.fn(createPromise) });
const mockHttp = { post: jest.fn(createPromise) };
const UsersAPI = new Users(mockHttp);
@ -12,23 +12,29 @@ describe('UsersAPI', () => {
jest.clearAllMocks();
});
test('associate role calls post with expected params', async (done) => {
test('associate role calls post with expected params', async done => {
await UsersAPI.associateRole(userId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/users/${userId}/roles/`, { id: roleId });
expect(mockHttp.post.mock.calls[0]).toContainEqual(
`/api/v2/users/${userId}/roles/`,
{ id: roleId }
);
done();
});
test('read users calls post with expected params', async (done) => {
test('read users calls post with expected params', async done => {
await UsersAPI.disassociateRole(userId, roleId);
expect(mockHttp.post).toHaveBeenCalledTimes(1);
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/users/${userId}/roles/`, {
id: roleId,
disassociate: true
});
expect(mockHttp.post.mock.calls[0]).toContainEqual(
`/api/v2/users/${userId}/roles/`,
{
id: roleId,
disassociate: true,
}
);
done();
});

View File

@ -1,7 +1,7 @@
import Base from '../Base';
class WorkflowJobTemplates extends Base {
constructor (http) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/workflow_job_templates/';
}

View File

@ -80,7 +80,6 @@
--pf-c-data-list__cell--PaddingTop: 16px;
--pf-c-data-list__cell-cell--PaddingTop: 16px;
&.pf-c-data-list__cell--divider {
--pf-c-data-list__cell-cell--MarginRight: 0;
--pf-c-data-list__cell--PaddingTop: 12px;

View File

@ -6,14 +6,14 @@ import {
AboutModal,
TextContent,
TextList,
TextListItem
TextListItem,
} from '@patternfly/react-core';
import { BrandName } from '../../variables';
import brandLogoImg from '../../../images/brand-logo.svg';
class About extends React.Component {
static createSpeechBubble (version) {
static createSpeechBubble(version) {
let text = `${BrandName} ${version}`;
let top = '';
let bottom = '';
@ -30,20 +30,14 @@ class About extends React.Component {
return top + text + bottom;
}
constructor (props) {
constructor(props) {
super(props);
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
}
render () {
const {
ansible_version,
version,
isOpen,
onClose,
i18n
} = this.props;
render() {
const { ansible_version, version, isOpen, onClose, i18n } = this.props;
const speechBubble = this.createSpeechBubble(version);
@ -57,7 +51,7 @@ class About extends React.Component {
brandImageAlt={i18n._(t`Brand Image`)}
>
<pre>
{ speechBubble }
{speechBubble}
{`
\\
\\ ^__^
@ -72,7 +66,7 @@ class About extends React.Component {
<TextListItem component="dt">
{i18n._(t`Ansible Version`)}
</TextListItem>
<TextListItem component="dd">{ ansible_version }</TextListItem>
<TextListItem component="dd">{ansible_version}</TextListItem>
</TextList>
</TextContent>
</AboutModal>

View File

@ -7,17 +7,13 @@ describe('<About />', () => {
let closeButton;
const onClose = jest.fn();
test('initially renders without crashing', () => {
aboutWrapper = mountWithContexts(
<About isOpen onClose={onClose} />
);
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
expect(aboutWrapper.length).toBe(1);
aboutWrapper.unmount();
});
test('close button calls onClose handler', () => {
aboutWrapper = mountWithContexts(
<About isOpen onClose={onClose} />
);
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
closeButton.simulate('click');
expect(onClose).toBeCalled();

View File

@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep';
import SelectableCard from './SelectableCard';
import { TeamsAPI, UsersAPI } from '../../api';
const readUsers = async (queryParams) => UsersAPI.read(
Object.assign(queryParams, { is_superuser: false })
);
const readUsers = async queryParams =>
UsersAPI.read(Object.assign(queryParams, { is_superuser: false }));
const readTeams = async (queryParams) => TeamsAPI.read(queryParams);
const readTeams = async queryParams => TeamsAPI.read(queryParams);
class AddResourceRole extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -25,67 +24,69 @@ class AddResourceRole extends React.Component {
currentStepId: 1,
};
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this);
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);
}
handleResourceCheckboxClick (user) {
handleResourceCheckboxClick(user) {
const { selectedResourceRows } = this.state;
const selectedIndex = selectedResourceRows
.findIndex(selectedRow => selectedRow.id === user.id);
const selectedIndex = selectedResourceRows.findIndex(
selectedRow => selectedRow.id === user.id
);
if (selectedIndex > -1) {
selectedResourceRows.splice(selectedIndex, 1);
this.setState({ selectedResourceRows });
} else {
this.setState(prevState => ({
selectedResourceRows: [...prevState.selectedResourceRows, user]
selectedResourceRows: [...prevState.selectedResourceRows, user],
}));
}
}
handleRoleCheckboxClick (role) {
handleRoleCheckboxClick(role) {
const { selectedRoleRows } = this.state;
const selectedIndex = selectedRoleRows
.findIndex(selectedRow => selectedRow.id === role.id);
const selectedIndex = selectedRoleRows.findIndex(
selectedRow => selectedRow.id === role.id
);
if (selectedIndex > -1) {
selectedRoleRows.splice(selectedIndex, 1);
this.setState({ selectedRoleRows });
} else {
this.setState(prevState => ({
selectedRoleRows: [...prevState.selectedRoleRows, role]
selectedRoleRows: [...prevState.selectedRoleRows, role],
}));
}
}
handleResourceSelect (resourceType) {
handleResourceSelect(resourceType) {
this.setState({
selectedResource: resourceType,
selectedResourceRows: [],
selectedRoleRows: []
selectedRoleRows: [],
});
}
handleWizardNext (step) {
handleWizardNext(step) {
this.setState({
currentStepId: step.id,
});
}
async handleWizardSave () {
const {
onSave
} = this.props;
async handleWizardSave() {
const { onSave } = this.props;
const {
selectedResourceRows,
selectedRoleRows,
selectedResource
selectedResource,
} = this.state;
try {
@ -95,11 +96,17 @@ class AddResourceRole extends React.Component {
for (let j = 0; j < selectedRoleRows.length; j++) {
if (selectedResource === 'users') {
roleRequests.push(
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
UsersAPI.associateRole(
selectedResourceRows[i].id,
selectedRoleRows[j].id
)
);
} else if (selectedResource === 'teams') {
roleRequests.push(
TeamsAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
TeamsAPI.associateRole(
selectedResourceRows[i].id,
selectedRoleRows[j].id
)
);
}
}
@ -112,25 +119,21 @@ class AddResourceRole extends React.Component {
}
}
render () {
render() {
const {
selectedResource,
selectedResourceRows,
selectedRoleRows,
currentStepId,
} = this.state;
const {
onClose,
roles,
i18n
} = this.props;
const { onClose, roles, i18n } = this.props;
const userColumns = [
{ name: i18n._(t`Username`), key: 'username', isSortable: true }
{ name: i18n._(t`Username`), key: 'username', isSortable: true },
];
const teamColumns = [
{ name: i18n._(t`Name`), key: 'name', isSortable: true }
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
];
let wizardTitle = '';
@ -164,7 +167,7 @@ class AddResourceRole extends React.Component {
/>
</div>
),
enableNext: selectedResource !== null
enableNext: selectedResource !== null,
},
{
id: 2,
@ -195,7 +198,7 @@ class AddResourceRole extends React.Component {
)}
</Fragment>
),
enableNext: selectedResourceRows.length > 0
enableNext: selectedResourceRows.length > 0,
},
{
id: 3,
@ -211,8 +214,8 @@ class AddResourceRole extends React.Component {
/>
),
nextButtonText: i18n._(t`Save`),
enableNext: selectedRoleRows.length > 0
}
enableNext: selectedRoleRows.length > 0,
},
];
const currentStep = steps.find(step => step.id === currentStepId);
@ -236,11 +239,11 @@ class AddResourceRole extends React.Component {
AddResourceRole.propTypes = {
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
roles: PropTypes.shape()
roles: PropTypes.shape(),
};
AddResourceRole.defaultProps = {
roles: {}
roles: {},
};
export { AddResourceRole as _AddResourceRole };

View File

@ -11,23 +11,20 @@ describe('<_AddResourceRole />', () => {
UsersAPI.read.mockResolvedValue({
data: {
count: 2,
results: [
{ id: 1, username: 'foo' },
{ id: 2, username: 'bar' }
]
}
results: [{ id: 1, username: 'foo' }, { id: 2, username: 'bar' }],
},
});
const roles = {
admin_role: {
description: 'Can manage all aspects of the organization',
id: 1,
name: 'Admin'
name: 'Admin',
},
execute_role: {
description: 'May run any executable resources in the organization',
id: 2,
name: 'Execute'
}
name: 'Execute',
},
};
test('initially renders without crashing', () => {
shallow(
@ -53,26 +50,28 @@ describe('<_AddResourceRole />', () => {
{
description: 'Can manage all aspects of the organization',
name: 'Admin',
id: 1
}
]
id: 1,
},
],
});
wrapper.instance().handleRoleCheckboxClick({
description: 'Can manage all aspects of the organization',
name: 'Admin',
id: 1
id: 1,
});
expect(wrapper.state('selectedRoleRows')).toEqual([]);
wrapper.instance().handleRoleCheckboxClick({
description: 'Can manage all aspects of the organization',
name: 'Admin',
id: 1
id: 1,
});
expect(wrapper.state('selectedRoleRows')).toEqual([{
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(
@ -87,32 +86,31 @@ describe('<_AddResourceRole />', () => {
selectedResourceRows: [
{
id: 1,
username: 'foobar'
}
]
username: 'foobar',
},
],
});
wrapper.instance().handleResourceCheckboxClick({
id: 1,
username: 'foobar'
username: 'foobar',
});
expect(wrapper.state('selectedResourceRows')).toEqual([]);
wrapper.instance().handleResourceCheckboxClick({
id: 1,
username: 'foobar'
username: 'foobar',
});
expect(wrapper.state('selectedResourceRows')).toEqual([{
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: () => {} } } }
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
{ context: { network: { handleHttpError: () => {} } } }
).find('AddResourceRole');
const selectableCardWrapper = wrapper.find('SelectableCard');
expect(selectableCardWrapper.length).toBe(2);
@ -137,16 +135,16 @@ describe('<_AddResourceRole />', () => {
selectedResourceRows: [
{
id: 1,
username: 'foobar'
}
username: 'foobar',
},
],
selectedRoleRows: [
{
description: 'Can manage all aspects of the organization',
id: 1,
name: 'Admin'
}
]
name: 'Admin',
},
],
});
wrapper.instance().handleResourceSelect('users');
expect(wrapper.state()).toEqual({
@ -160,38 +158,35 @@ describe('<_AddResourceRole />', () => {
selectedResource: 'teams',
selectedResourceRows: [],
selectedRoleRows: [],
currentStepId: 1
currentStepId: 1,
});
});
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
const handleSave = jest.fn();
const wrapper = mountWithContexts(
<AddResourceRole
onClose={() => {}}
onSave={handleSave}
roles={roles}
/>, { context: { network: { handleHttpError: () => {} } } }
<AddResourceRole onClose={() => {}} onSave={handleSave} roles={roles} />,
{ context: { network: { handleHttpError: () => {} } } }
).find('AddResourceRole');
wrapper.setState({
selectedResource: 'users',
selectedResourceRows: [
{
id: 1,
username: 'foobar'
}
username: 'foobar',
},
],
selectedRoleRows: [
{
description: 'Can manage all aspects of the organization',
id: 1,
name: 'Admin'
name: 'Admin',
},
{
description: 'May run any executable resources in the organization',
id: 2,
name: 'Execute'
}
]
name: 'Execute',
},
],
});
await wrapper.instance().handleWizardSave();
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
@ -201,21 +196,21 @@ describe('<_AddResourceRole />', () => {
selectedResourceRows: [
{
id: 1,
name: 'foobar'
}
name: 'foobar',
},
],
selectedRoleRows: [
{
description: 'Can manage all aspects of the organization',
id: 1,
name: 'Admin'
name: 'Admin',
},
{
description: 'May run any executable resources in the organization',
id: 2,
name: 'Execute'
}
]
name: 'Execute',
},
],
});
await wrapper.instance().handleWizardSave();
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);

View File

@ -1,31 +1,30 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import {
Checkbox
} from '@patternfly/react-core';
import { Checkbox } from '@patternfly/react-core';
class CheckboxCard extends Component {
render () {
render() {
const { name, description, isSelected, onSelect, itemId } = this.props;
return (
<div style={{
display: 'flex',
border: '1px solid var(--pf-global--BorderColor--200)',
borderRadius: 'var(--pf-global--BorderRadius--sm)',
padding: '10px'
}}
<div
style={{
display: 'flex',
border: '1px solid var(--pf-global--BorderColor--200)',
borderRadius: 'var(--pf-global--BorderRadius--sm)',
padding: '10px',
}}
>
<Checkbox
checked={isSelected}
onChange={onSelect}
aria-label={name}
id={`checkbox-card-${itemId}`}
label={(
label={
<Fragment>
<div style={{ fontWeight: 'bold' }}>{name}</div>
<div>{description}</div>
</Fragment>
)}
}
value={itemId}
/>
</div>
@ -38,13 +37,13 @@ CheckboxCard.propTypes = {
description: PropTypes.string,
isSelected: PropTypes.bool,
onSelect: PropTypes.func,
itemId: PropTypes.number.isRequired
itemId: PropTypes.number.isRequired,
};
CheckboxCard.defaultProps = {
description: '',
isSelected: false,
onSelect: null
onSelect: null,
};
export default CheckboxCard;

View File

@ -5,12 +5,7 @@ import CheckboxCard from './CheckboxCard';
describe('<CheckboxCard />', () => {
let wrapper;
test('initially renders without crashing', () => {
wrapper = shallow(
<CheckboxCard
name="Foobar"
itemId={5}
/>
);
wrapper = shallow(<CheckboxCard name="Foobar" itemId={5} />);
expect(wrapper.length).toBe(1);
});
});

View File

@ -10,7 +10,7 @@ import SelectedList from '../SelectedList';
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
class SelectResourceStep extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -27,20 +27,23 @@ class SelectResourceStep extends React.Component {
});
}
componentDidMount () {
componentDidMount() {
this.readResourceList();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.readResourceList();
}
}
async readResourceList () {
async readResourceList() {
const { onSearch, location } = this.props;
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
const queryParams = parseNamespacedQueryString(
this.qsConfig,
location.search
);
this.setState({
isLoading: true,
@ -65,14 +68,8 @@ class SelectResourceStep extends React.Component {
}
}
render () {
const {
isInitialized,
isLoading,
count,
error,
resources,
} = this.state;
render() {
const { isInitialized, isLoading, count, error, resources } = this.state;
const {
columns,
@ -81,12 +78,12 @@ class SelectResourceStep extends React.Component {
selectedLabel,
selectedResourceRows,
itemName,
i18n
i18n,
} = this.props;
return (
<Fragment>
{isLoading && (<div>{i18n._(t`Loading...`)}</div>)}
{isLoading && <div>{i18n._(t`Loading...`)}</div>}
{isInitialized && (
<Fragment>
{selectedResourceRows.length > 0 && (
@ -113,14 +110,14 @@ class SelectResourceStep extends React.Component {
onSelect={() => onRowClick(item)}
/>
)}
renderToolbar={(props) => (
renderToolbar={props => (
<DataListToolbar {...props} alignToolbarLeft />
)}
showPageSizeOptions={false}
/>
</Fragment>
)}
{ error ? <div>error</div> : '' }
{error ? <div>error</div> : ''}
</Fragment>
);
}

View File

@ -6,9 +6,7 @@ import { sleep } from '../../../testUtils/testUtils';
import SelectResourceStep from './SelectResourceStep';
describe('<SelectResourceStep />', () => {
const columns = [
{ name: 'Username', key: 'username', isSortable: true }
];
const columns = [{ name: 'Username', key: 'username', isSortable: true }];
afterEach(() => {
jest.restoreAllMocks();
});
@ -30,9 +28,9 @@ describe('<SelectResourceStep />', () => {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
}
{ id: 2, username: 'bar', url: 'item/2' },
],
},
});
mountWithContexts(
<SelectResourceStep
@ -46,25 +44,25 @@ describe('<SelectResourceStep />', () => {
expect(handleSearch).toHaveBeenCalledWith({
order_by: 'username',
page: 1,
page_size: 5
page_size: 5,
});
});
test('readResourceList properly adds rows to state', async () => {
const selectedResourceRows = [
{ id: 1, username: 'foo', url: 'item/1' }
];
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
const handleSearch = jest.fn().mockResolvedValue({
data: {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
}
{ id: 2, username: 'bar', url: 'item/2' },
],
},
});
const history = createMemoryHistory({
initialEntries: ['/organizations/1/access?resource.page=1&resource.order_by=-username'],
initialEntries: [
'/organizations/1/access?resource.page=1&resource.order_by=-username',
],
});
const wrapper = await mountWithContexts(
<SelectResourceStep
@ -74,7 +72,10 @@ describe('<SelectResourceStep />', () => {
onSearch={handleSearch}
selectedResourceRows={selectedResourceRows}
sortedColumnKey="username"
/>, { context: { router: { history, route: { location: history.location } } } }
/>,
{
context: { router: { history, route: { location: history.location } } },
}
).find('SelectResourceStep');
await wrapper.instance().readResourceList();
expect(handleSearch).toHaveBeenCalledWith({
@ -84,7 +85,7 @@ describe('<SelectResourceStep />', () => {
});
expect(wrapper.state('resources')).toEqual([
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
{ id: 2, username: 'bar', url: 'item/2' },
]);
});
@ -94,8 +95,8 @@ describe('<SelectResourceStep />', () => {
count: 2,
results: [
{ id: 1, username: 'foo', url: 'item/1' },
{ id: 2, username: 'bar', url: 'item/2' }
]
{ id: 2, username: 'bar', url: 'item/2' },
],
};
const wrapper = mountWithContexts(
<SelectResourceStep
@ -111,7 +112,9 @@ describe('<SelectResourceStep />', () => {
wrapper.update();
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
expect(checkboxListItemWrapper.length).toBe(2);
checkboxListItemWrapper.first().find('input[type="checkbox"]')
checkboxListItemWrapper
.first()
.find('input[type="checkbox"]')
.simulate('change', { target: { checked: true } });
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
});

View File

@ -8,7 +8,7 @@ import CheckboxCard from './CheckboxCard';
import SelectedList from '../SelectedList';
class RolesStep extends React.Component {
render () {
render() {
const {
onRolesClick,
roles,
@ -16,7 +16,7 @@ class RolesStep extends React.Component {
selectedListLabel,
selectedResourceRows,
selectedRoleRows,
i18n
i18n,
} = this.props;
return (
@ -32,14 +32,21 @@ class RolesStep extends React.Component {
/>
)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px 20px', marginTop: '20px' }}>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '20px 20px',
marginTop: '20px',
}}
>
{Object.keys(roles).map(role => (
<CheckboxCard
description={roles[role].description}
itemId={roles[role].id}
isSelected={
selectedRoleRows.some(item => item.id === roles[role].id)
}
isSelected={selectedRoleRows.some(
item => item.id === roles[role].id
)}
key={roles[role].id}
name={roles[role].name}
onSelect={() => onRolesClick(roles[role])}
@ -57,7 +64,7 @@ RolesStep.propTypes = {
selectedListKey: PropTypes.string,
selectedListLabel: PropTypes.string,
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
selectedRoleRows: PropTypes.arrayOf(PropTypes.object)
selectedRoleRows: PropTypes.arrayOf(PropTypes.object),
};
RolesStep.defaultProps = {
@ -65,7 +72,7 @@ RolesStep.defaultProps = {
selectedListKey: 'name',
selectedListLabel: null,
selectedResourceRows: [],
selectedRoleRows: []
selectedRoleRows: [],
};
export default withI18n()(RolesStep);

View File

@ -9,26 +9,26 @@ describe('<SelectRoleStep />', () => {
project_admin_role: {
id: 1,
name: 'Project Admin',
description: 'Can manage all projects of the organization'
description: 'Can manage all projects of the organization',
},
execute_role: {
id: 2,
name: 'Execute',
description: 'May run any executable resources in the organization'
}
description: 'May run any executable resources in the organization',
},
};
const selectedRoles = [
{
id: 1,
name: 'Project Admin',
description: 'Can manage all projects of the organization'
}
description: 'Can manage all projects of the organization',
},
];
const selectedResourceRows = [
{
id: 1,
name: 'foo'
}
name: 'foo',
},
];
test('initially renders without crashing', () => {
wrapper = shallow(
@ -57,7 +57,7 @@ describe('<SelectRoleStep />', () => {
expect(onRolesClick).toBeCalledWith({
id: 1,
name: 'Project Admin',
description: 'Can manage all projects of the organization'
description: 'Can manage all projects of the organization',
});
wrapper.unmount();
});

View File

@ -7,7 +7,10 @@ const SelectableItem = styled.div`
border: 1px solid var(--pf-global--BorderColor--200);
border-radius: var(--pf-global--BorderRadius--sm);
border: 1px solid;
border-color: ${props => (props.isSelected ? 'var(--pf-global--active-color--100)' : 'var(--pf-global--BorderColor--200)')};
border-color: ${props =>
props.isSelected
? 'var(--pf-global--active-color--100)'
: 'var(--pf-global--BorderColor--200)'};
margin-right: 20px;
font-weight: bold;
display: flex;
@ -17,7 +20,8 @@ const SelectableItem = styled.div`
const Indicator = styled.div`
display: flex;
flex: 0 0 5px;
background-color: ${props => (props.isSelected ? 'var(--pf-global--active-color--100)' : null)};
background-color: ${props =>
props.isSelected ? 'var(--pf-global--active-color--100)' : null};
`;
const Label = styled.div`
@ -28,12 +32,8 @@ const Label = styled.div`
`;
class SelectableCard extends Component {
render () {
const {
label,
onClick,
isSelected
} = this.props;
render() {
const { label, onClick, isSelected } = this.props;
return (
<SelectableItem
@ -43,10 +43,7 @@ class SelectableCard extends Component {
tabIndex="0"
isSelected={isSelected}
>
<Indicator
isSelected={isSelected}
/>
<Indicator isSelected={isSelected} />
<Label>{label}</Label>
</SelectableItem>
);
@ -56,12 +53,12 @@ class SelectableCard extends Component {
SelectableCard.propTypes = {
label: PropTypes.string,
onClick: PropTypes.func.isRequired,
isSelected: PropTypes.bool
isSelected: PropTypes.bool,
};
SelectableCard.defaultProps = {
label: '',
isSelected: false
isSelected: false,
};
export default SelectableCard;

View File

@ -6,21 +6,12 @@ describe('<SelectableCard />', () => {
let wrapper;
const onClick = jest.fn();
test('initially renders without crashing when not selected', () => {
wrapper = shallow(
<SelectableCard
onClick={onClick}
/>
);
wrapper = shallow(<SelectableCard onClick={onClick} />);
expect(wrapper.length).toBe(1);
wrapper.unmount();
});
test('initially renders without crashing when selected', () => {
wrapper = shallow(
<SelectableCard
isSelected
onClick={onClick}
/>
);
wrapper = shallow(<SelectableCard isSelected onClick={onClick} />);
expect(wrapper.length).toBe(1);
wrapper.unmount();
});

View File

@ -1,21 +1,26 @@
import React from 'react';
import { Modal } from '@patternfly/react-core';
import {
Modal
} from '@patternfly/react-core';
ExclamationTriangleIcon,
ExclamationCircleIcon,
InfoCircleIcon,
CheckCircleIcon,
} from '@patternfly/react-icons';
import { ExclamationTriangleIcon, ExclamationCircleIcon, InfoCircleIcon, CheckCircleIcon } from '@patternfly/react-icons';
const getIcon = (variant) => {
const getIcon = variant => {
let icon;
if (variant === 'warning') {
icon = (<ExclamationTriangleIcon className="at-c-alertModal__icon" />);
icon = <ExclamationTriangleIcon className="at-c-alertModal__icon" />;
} else if (variant === 'danger') {
icon = (<ExclamationCircleIcon className="at-c-alertModal__icon" />);
} if (variant === 'info') {
icon = (<InfoCircleIcon className="at-c-alertModal__icon" />);
} if (variant === 'success') {
icon = (<CheckCircleIcon className="at-c-alertModal__icon" />);
icon = <ExclamationCircleIcon className="at-c-alertModal__icon" />;
}
if (variant === 'info') {
icon = <InfoCircleIcon className="at-c-alertModal__icon" />;
}
if (variant === 'success') {
icon = <CheckCircleIcon className="at-c-alertModal__icon" />;
}
return icon;
};
@ -24,7 +29,11 @@ export default ({ variant, children, ...props }) => {
const { isOpen = null } = props;
props.isOpen = Boolean(isOpen);
return (
<Modal className={`awx-c-modal${variant && ` at-c-alertModal at-c-alertModal--${variant}`}`} {...props}>
<Modal
className={`awx-c-modal${variant &&
` at-c-alertModal at-c-alertModal--${variant}`}`}
{...props}
>
{children}
{getIcon(variant)}
</Modal>

View File

@ -5,9 +5,7 @@ import AlertModal from './AlertModal';
describe('AlertModal', () => {
test('renders the expected content', () => {
const wrapper = mount(
<AlertModal title="Danger!" />
);
const wrapper = mount(<AlertModal title="Danger!" />);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -2,24 +2,21 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
FormSelect,
FormSelectOption,
} from '@patternfly/react-core';
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
class AnsibleSelect extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.onSelectChange = this.onSelectChange.bind(this);
}
onSelectChange (val, event) {
onSelectChange(val, event) {
const { onChange, name } = this.props;
event.target.name = name;
onChange(event, val);
}
render () {
render() {
const { value, data, i18n } = this.props;
return (
@ -28,7 +25,7 @@ class AnsibleSelect extends React.Component {
onChange={this.onSelectChange}
aria-label={i18n._(t`Select Input`)}
>
{data.map((datum) => (
{data.map(datum => (
<FormSelectOption
key={datum.key}
value={datum.value}

View File

@ -6,13 +6,13 @@ const mockData = [
{
key: 'baz',
label: 'Baz',
value: '/venv/baz/'
value: '/venv/baz/',
},
{
key: 'default',
label: 'Default',
value: '/venv/ansible/'
}
value: '/venv/ansible/',
},
];
describe('<AnsibleSelect />', () => {
@ -21,7 +21,7 @@ describe('<AnsibleSelect />', () => {
<AnsibleSelect
value="foo"
name="bar"
onChange={() => { }}
onChange={() => {}}
data={mockData}
/>
);
@ -33,7 +33,7 @@ describe('<AnsibleSelect />', () => {
<AnsibleSelect
value="foo"
name="bar"
onChange={() => { }}
onChange={() => {}}
data={mockData}
/>
);
@ -47,7 +47,7 @@ describe('<AnsibleSelect />', () => {
<AnsibleSelect
value="foo"
name="bar"
onChange={() => { }}
onChange={() => {}}
data={mockData}
/>
);

View File

@ -1,9 +1,6 @@
import React, { Fragment } from 'react';
import {
BackgroundImage,
BackgroundImageSrc,
} from '@patternfly/react-core';
import { BackgroundImage, BackgroundImageSrc } from '@patternfly/react-core';
import bgFilter from '@patternfly/patternfly/assets/images/background-filter.svg';
const backgroundImageConfig = {
@ -18,6 +15,6 @@ const backgroundImageConfig = {
export default ({ children }) => (
<Fragment>
<BackgroundImage src={backgroundImageConfig} />
{ children }
{children}
</Fragment>
);

View File

@ -5,7 +5,11 @@ import Background from './Background';
describe('Background', () => {
test('renders the expected content', () => {
const wrapper = mount(<Background><div id="test" /></Background>);
const wrapper = mount(
<Background>
<div id="test" />
</Background>
);
expect(wrapper).toHaveLength(1);
expect(wrapper.find('BackgroundImage')).toHaveLength(1);
expect(wrapper.find('#test')).toHaveLength(1);

View File

@ -5,57 +5,57 @@ import { t } from '@lingui/macro';
import styled from 'styled-components';
const ST0 = styled.g`
display:none;
display: none;
`;
const ST1 = styled.path`
display:inline;
fill:#ED1C24;
display: inline;
fill: #ed1c24;
`;
const ST2 = styled.path`
fill:#42210B;
fill: #42210b;
`;
const ST3 = styled.path`
fill:#FFFFFF;
fill: #ffffff;
`;
const ST4 = styled.path`
fill:#C69C6D;
stroke:#8C6239;
stroke-width:5;
stroke-miterlimit:10;
fill: #c69c6d;
stroke: #8c6239;
stroke-width: 5;
stroke-miterlimit: 10;
`;
const ST5 = styled.path`
fill:#FFFFFF;
stroke:#42210B;
stroke-width:3;
stroke-miterlimit:10;
fill: #ffffff;
stroke: #42210b;
stroke-width: 3;
stroke-miterlimit: 10;
`;
const ST6 = styled.ellipse`
fill:#ED1C24;
stroke:#8C6239;
stroke-width:5;
stroke-miterlimit:10;
fill: #ed1c24;
stroke: #8c6239;
stroke-width: 5;
stroke-miterlimit: 10;
`;
const ST7 = styled.path`
fill:#A67C52;
fill: #a67c52;
`;
const ST8 = styled.path`
fill: #ED1C24;
fill: #ed1c24;
`;
const ST9 = styled.ellipse`
fill:#42210B;
fill: #42210b;
`;
class BrandLogo extends Component {
render () {
render() {
const { i18n } = this.props;
return (
<svg
@ -195,24 +195,12 @@ class BrandLogo extends Component {
/>
</g>
</g>
<ST5
d="M215.5,166.5l34-73c0,0,35,0,46,21c7.5,14.3,8,39,8,39L215.5,166.5z"
/>
<ST5
d="M208.2,170.5l-79.5-12.7c0,0-19.6,29-8.4,49.9c7.6,14.2,27.8,28.5,27.8,28.5L208.2,170.5z"
/>
<ST2
d="M210.5,164.5l33-74c0,0-2.5-5.5-8-7s-12,0-12,0L210.5,164.5z"
/>
<ST2
d="M207.4,165.3l-73.1-35c0,0-5.6,2.4-7.2,7.8c-1.6,5.5-0.3,12-0.3,12L207.4,165.3z"
/>
<path
d="M215.5,166.5L234,127c0,0,17-6,25.5,7.5c8.6,13.6-3.5,25.5-3.5,25.5L215.5,166.5z"
/>
<path
d="M206.7,170.9l-29.6,32c0,0-18,0.5-22-14.9c-4-15.6,11.1-23.2,11.1-23.2L206.7,170.9z"
/>
<ST5 d="M215.5,166.5l34-73c0,0,35,0,46,21c7.5,14.3,8,39,8,39L215.5,166.5z" />
<ST5 d="M208.2,170.5l-79.5-12.7c0,0-19.6,29-8.4,49.9c7.6,14.2,27.8,28.5,27.8,28.5L208.2,170.5z" />
<ST2 d="M210.5,164.5l33-74c0,0-2.5-5.5-8-7s-12,0-12,0L210.5,164.5z" />
<ST2 d="M207.4,165.3l-73.1-35c0,0-5.6,2.4-7.2,7.8c-1.6,5.5-0.3,12-0.3,12L207.4,165.3z" />
<path d="M215.5,166.5L234,127c0,0,17-6,25.5,7.5c8.6,13.6-3.5,25.5-3.5,25.5L215.5,166.5z" />
<path d="M206.7,170.9l-29.6,32c0,0-18,0.5-22-14.9c-4-15.6,11.1-23.2,11.1-23.2L206.7,170.9z" />
<g>
<g>
<ST3

View File

@ -13,9 +13,7 @@ const findChildren = () => {
describe('<BrandLogo />', () => {
test('initially renders without crashing', () => {
logoWrapper = mountWithContexts(
<BrandLogo />
);
logoWrapper = mountWithContexts(<BrandLogo />);
findChildren();
expect(logoWrapper.length).toBe(1);
expect(brandLogoElem.length).toBe(1);

View File

@ -5,13 +5,9 @@ import {
PageSectionVariants,
Breadcrumb,
BreadcrumbItem,
BreadcrumbHeading as PFBreadcrumbHeading
BreadcrumbHeading as PFBreadcrumbHeading,
} from '@patternfly/react-core';
import {
Link,
Route,
withRouter
} from 'react-router-dom';
import { Link, Route, withRouter } from 'react-router-dom';
import styled from 'styled-components';
const PageSection = styled(PFPageSection)`
@ -20,7 +16,7 @@ const PageSection = styled(PFPageSection)`
`;
const BreadcrumbHeading = styled(PFBreadcrumbHeading)`
--pf-c-breadcrumb__heading--FontSize: 20px;
--pf-c-breadcrumb__heading--FontSize: 20px;
line-height: 24px;
flex: 100%;
`;
@ -29,13 +25,13 @@ const Breadcrumbs = ({ breadcrumbConfig }) => {
const { light } = PageSectionVariants;
return (
<PageSection
variant={light}
>
<PageSection variant={light}>
<Breadcrumb>
<Route
path="/:path"
render={(props) => <Crumb breadcrumbConfig={breadcrumbConfig} {...props} />}
render={props => (
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
)}
/>
</Breadcrumb>
</PageSection>
@ -47,19 +43,13 @@ const Crumb = ({ breadcrumbConfig, match }) => {
let crumbElement = (
<BreadcrumbItem key={match.url}>
<Link to={match.url}>
{crumb}
</Link>
<Link to={match.url}>{crumb}</Link>
</BreadcrumbItem>
);
if (match.isExact) {
crumbElement = (
<BreadcrumbHeading
key="breadcrumb-heading"
>
{crumb}
</BreadcrumbHeading>
<BreadcrumbHeading key="breadcrumb-heading">{crumb}</BreadcrumbHeading>
);
}
@ -72,7 +62,9 @@ const Crumb = ({ breadcrumbConfig, match }) => {
{crumbElement}
<Route
path={`${match.url}/:path`}
render={(props) => <Crumb breadcrumbConfig={breadcrumbConfig} {...props} />}
render={props => (
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
)}
/>
</Fragment>
);

View File

@ -13,7 +13,7 @@ describe('<Breadcrumb />', () => {
'/foo': 'Foo',
'/foo/1': 'One',
'/foo/1/bar': 'Bar',
'/foo/1/bar/fiz': 'Fiz'
'/foo/1/bar/fiz': 'Fiz',
};
const findChildren = () => {
@ -25,9 +25,7 @@ describe('<Breadcrumb />', () => {
test('initially renders succesfully', () => {
breadcrumbWrapper = mount(
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
<Breadcrumbs
breadcrumbConfig={config}
/>
<Breadcrumbs breadcrumbConfig={config} />
</MemoryRouter>
);
@ -55,13 +53,13 @@ describe('<Breadcrumb />', () => {
routes.forEach(([location, crumbLength]) => {
breadcrumbWrapper = mount(
<MemoryRouter initialEntries={[location]}>
<Breadcrumbs
breadcrumbConfig={config}
/>
<Breadcrumbs breadcrumbConfig={config} />
</MemoryRouter>
);
expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(crumbLength);
expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(
crumbLength
);
breadcrumbWrapper.unmount();
});
});

View File

@ -21,12 +21,8 @@ const Group = styled.div`
}
`;
function ButtonGroup ({ children }) {
return (
<Group>
{children}
</Group>
);
function ButtonGroup({ children }) {
return <Group>{children}</Group>;
}
export default ButtonGroup;

View File

@ -8,13 +8,13 @@ import { t } from '@lingui/macro';
import styled from 'styled-components';
const Link = styled(RRLink)`
position: absolute;
top: 5px;
right: 4px;
color: var(--pf-c-button--m-plain--Color);
position: absolute;
top: 5px;
right: 4px;
color: var(--pf-c-button--m-plain--Color);
`;
function CardCloseButton ({ linkTo, i18n, i18nHash, ...props }) {
function CardCloseButton({ linkTo, i18n, i18nHash, ...props }) {
if (linkTo) {
return (
<Link
@ -29,11 +29,7 @@ function CardCloseButton ({ linkTo, i18n, i18nHash, ...props }) {
);
}
return (
<Button
variant="plain"
aria-label={i18n._(t`Close`)}
{...props}
>
<Button variant="plain" aria-label={i18n._(t`Close`)} {...props}>
<TimesIcon />
</Button>
);

View File

@ -10,12 +10,7 @@ import {
import VerticalSeparator from '../VerticalSeparator';
const CheckboxListItem = ({
itemId,
name,
isSelected,
onSelect,
}) => (
const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => (
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
<DataListItemRow>
<DataListCheck
@ -25,20 +20,21 @@ const CheckboxListItem = ({
aria-labelledby={`check-action-item-${itemId}`}
value={itemId}
/>
<DataListItemCells dataListCells={[
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
<VerticalSeparator />
</DataListCell>,
<DataListCell key="name">
<label
id={`check-action-item-${itemId}`}
htmlFor={`selected-${itemId}`}
className="check-action-item"
>
<b>{name}</b>
</label>
</DataListCell>
]}
<DataListItemCells
dataListCells={[
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
<VerticalSeparator />
</DataListCell>,
<DataListCell key="name">
<label
id={`check-action-item-${itemId}`}
htmlFor={`selected-${itemId}`}
className="check-action-item"
>
<b>{name}</b>
</label>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>

View File

@ -1,4 +1,3 @@
import { Chip } from '@patternfly/react-core';
import styled from 'styled-components';
@ -12,7 +11,9 @@ export default styled(Chip)`
padding: 3px 8px;
}
${props => (props.isOverflowChip && `
${props =>
props.isOverflowChip &&
`
padding: 0;
`)}
`}
`;

View File

@ -7,10 +7,11 @@ const ChipGroup = ({ children, className, showOverflowAfter, ...props }) => {
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
const toggleIsOpen = () => setIsExpanded(!isExpanded);
const mappedChildren = React.Children.map(children, c => (
const mappedChildren = React.Children.map(children, c =>
React.cloneElement(c, { component: 'li' })
));
const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter;
);
const showOverflowToggle =
showOverflowAfter && children.length > showOverflowAfter;
const numToShow = isExpanded
? children.length
: Math.min(showOverflowAfter, children.length);

View File

@ -57,7 +57,12 @@ describe('<ChipGroup />', () => {
});
wrapper.update();
expect(wrapper.find(Chip)).toHaveLength(8);
expect(wrapper.find(Chip).at(7).text()).toEqual('Show Less');
expect(
wrapper
.find(Chip)
.at(7)
.text()
).toEqual('Show Less');
act(() => {
const toggle2 = wrapper.find(Chip).at(7);
expect(toggle2.prop('isOverflowChip')).toBe(true);

View File

@ -17,11 +17,13 @@ const CodeMirror = styled(ReactCodeMirror)`
}
& > .CodeMirror {
height: ${props => (props.rows * LINE_HEIGHT + PADDING)}px;
height: ${props => props.rows * LINE_HEIGHT + PADDING}px;
font-family: var(--pf-global--FontFamily--monospace);
}
${props => props.hasErrors && `
${props =>
props.hasErrors &&
`
&& {
--pf-c-form-control--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight);
--pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor);
@ -32,7 +34,7 @@ const CodeMirror = styled(ReactCodeMirror)`
}`}
`;
function CodeMirrorInput ({ value, onChange, mode, readOnly, hasErrors, rows }) {
function CodeMirrorInput({ value, onChange, mode, readOnly, hasErrors, rows }) {
return (
<CodeMirror
className="pf-c-form-control"
@ -43,7 +45,7 @@ function CodeMirrorInput ({ value, onChange, mode, readOnly, hasErrors, rows })
options={{
smartIndent: false,
lineNumbers: true,
readOnly
readOnly,
}}
rows={rows}
/>

View File

@ -10,11 +10,7 @@ describe('CodeMirrorInput', () => {
it('should trigger onChange prop', () => {
const onChange = jest.fn();
const wrapper = mount(
<CodeMirrorInput
value="---\n"
onChange={onChange}
mode="yaml"
/>
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" />
);
const codemirror = wrapper.find('Controlled');
expect(codemirror.prop('mode')).toEqual('yaml');
@ -26,12 +22,7 @@ describe('CodeMirrorInput', () => {
it('should render in read only mode', () => {
const onChange = jest.fn();
const wrapper = mount(
<CodeMirrorInput
value="---\n"
onChange={onChange}
mode="yaml"
readOnly
/>
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" readOnly />
);
const codemirror = wrapper.find('Controlled');
expect(codemirror.prop('options').readOnly).toEqual(true);

View File

@ -15,7 +15,7 @@ const SmallButton = styled(Button)`
font-size: var(--pf-global--FontSize--xs);
`;
function VariablesField ({ id, name, label, readOnly }) {
function VariablesField({ id, name, label, readOnly }) {
const [mode, setMode] = useState(YAML_MODE);
return (
@ -25,13 +25,17 @@ function VariablesField ({ id, name, label, readOnly }) {
<div className="pf-c-form__group">
<Split gutter="sm">
<SplitItem>
<label htmlFor={id} className="pf-c-form__label">{label}</label>
<label htmlFor={id} className="pf-c-form__label">
{label}
</label>
</SplitItem>
<SplitItem>
<ButtonGroup>
<SmallButton
onClick={() => {
if (mode === YAML_MODE) { return; }
if (mode === YAML_MODE) {
return;
}
try {
form.setFieldValue(name, jsonToYaml(field.value));
setMode(YAML_MODE);
@ -45,7 +49,9 @@ function VariablesField ({ id, name, label, readOnly }) {
</SmallButton>
<SmallButton
onClick={() => {
if (mode === JSON_MODE) { return; }
if (mode === JSON_MODE) {
return;
}
try {
form.setFieldValue(name, yamlToJson(field.value));
setMode(JSON_MODE);
@ -64,7 +70,7 @@ function VariablesField ({ id, name, label, readOnly }) {
mode={mode}
readOnly={readOnly}
{...field}
onChange={(value) => {
onChange={value => {
form.setFieldValue(name, value);
}}
hasErrors={!!form.errors[field.name]}
@ -76,7 +82,7 @@ function VariablesField ({ id, name, label, readOnly }) {
>
{form.errors[field.name]}
</div>
) : null }
) : null}
</div>
)}
/>

View File

@ -15,11 +15,7 @@ describe('VariablesField', () => {
<Formik
initialValues={{ variables: value }}
render={() => (
<VariablesField
id="the-field"
name="variables"
label="Variables"
/>
<VariablesField id="the-field" name="variables" label="Variables" />
)}
/>
);
@ -33,11 +29,7 @@ describe('VariablesField', () => {
<Formik
initialValues={{ variables: value }}
render={() => (
<VariablesField
id="the-field"
name="variables"
label="Variables"
/>
<VariablesField id="the-field" name="variables" label="Variables" />
)}
/>
);
@ -63,15 +55,14 @@ describe('VariablesField', () => {
<Formik
initialValues={{ variables: value }}
render={() => (
<VariablesField
id="the-field"
name="variables"
label="Variables"
/>
<VariablesField id="the-field" name="variables" label="Variables" />
)}
/>
);
wrapper.find('Button').at(1).simulate('click');
wrapper
.find('Button')
.at(1)
.simulate('click');
wrapper.update();
const field = wrapper.find('CodeMirrorInput');
@ -86,14 +77,12 @@ describe('VariablesField', () => {
<Formik
initialValues={{ variables: value }}
onSubmit={handleSubmit}
render={(formik) => (
render={formik => (
<form onSubmit={formik.handleSubmit}>
<VariablesField
id="the-field"
name="variables"
label="Variables"
/>
<button type="submit" id="submit">Submit</button>
<VariablesField id="the-field" name="variables" label="Variables" />
<button type="submit" id="submit">
Submit
</button>
</form>
)}
/>
@ -105,7 +94,7 @@ describe('VariablesField', () => {
expect(handleSubmit).toHaveBeenCalled();
expect(handleSubmit.mock.calls[0][0]).toEqual({
variables: '---\nnewval: changed'
variables: '---\nnewval: changed',
});
});
});

View File

@ -5,19 +5,15 @@ import {
Title,
EmptyState,
EmptyStateIcon,
EmptyStateBody
EmptyStateBody,
} from '@patternfly/react-core';
import { CubesIcon } from '@patternfly/react-icons';
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
<EmptyState>
<EmptyStateIcon icon={CubesIcon} />
<Title size="lg">
{title || i18n._(t`No items found.`)}
</Title>
<EmptyStateBody>
{message}
</EmptyStateBody>
<Title size="lg">{title || i18n._(t`No items found.`)}</Title>
<EmptyStateBody>{message}</EmptyStateBody>
</EmptyState>
);

View File

@ -6,7 +6,7 @@ import {
Title,
EmptyState as PFEmptyState,
EmptyStateIcon,
EmptyStateBody
EmptyStateBody,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
@ -17,20 +17,18 @@ const EmptyState = styled(PFEmptyState)`
`;
class ContentError extends React.Component {
render () {
render() {
const { error, i18n } = this.props;
return (
<EmptyState>
<EmptyStateIcon icon={ExclamationTriangleIcon} />
<Title size="lg">
{i18n._(t`Something went wrong...`)}
</Title>
<Title size="lg">{i18n._(t`Something went wrong...`)}</Title>
<EmptyStateBody>
{i18n._(t`There was an error loading this content. Please reload the page.`)}
{i18n._(
t`There was an error loading this content. Please reload the page.`
)}
</EmptyStateBody>
{error && (
<ErrorDetail error={error} />
)}
{error && <ErrorDetail error={error} />}
</EmptyState>
);
}

View File

@ -5,15 +5,20 @@ import ContentError from './ContentError';
describe('ContentError', () => {
test('renders the expected content', () => {
const wrapper = mountWithContexts(<ContentError error={new Error({
response: {
config: {
method: 'post'
},
data: 'An error occurred',
}
})}
/>);
const wrapper = mountWithContexts(
<ContentError
error={
new Error({
response: {
config: {
method: 'post',
},
data: 'An error occurred',
},
})
}
/>
);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -1,17 +1,12 @@
import React from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import {
EmptyState,
EmptyStateBody
} from '@patternfly/react-core';
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
// TODO: Better loading state - skeleton lines / spinner, etc.
const ContentLoading = ({ i18n }) => (
<EmptyState>
<EmptyStateBody>
{i18n._(t`Loading...`)}
</EmptyStateBody>
<EmptyStateBody>{i18n._(t`Loading...`)}</EmptyStateBody>
</EmptyState>
);

View File

@ -24,7 +24,8 @@ const AWXToolbar = styled.div`
--pf-global--target-size--MinWidth: 0;
--pf-global--FontSize--md: 14px;
border-bottom: var(--awx-toolbar--BorderWidth) solid var(--awx-toolbar--BorderColor);
border-bottom: var(--awx-toolbar--BorderWidth) solid
var(--awx-toolbar--BorderColor);
background-color: var(--awx-toolbar--BackgroundColor);
display: flex;
min-height: 70px;
@ -70,13 +71,13 @@ const AdditionalControlsWrapper = styled.div`
justify-content: flex-end;
align-items: center;
& > :not(:first-child) {
margin-left: 20px;
& > :not(:first-child) {
margin-left: 20px;
}
`;
class DataListToolbar extends React.Component {
render () {
render() {
const {
columns,
showSelectAll,
@ -91,15 +92,15 @@ class DataListToolbar extends React.Component {
sortOrder,
sortedColumnKey,
additionalControls,
i18n
i18n,
} = this.props;
const showExpandCollapse = (onCompact && onExpand);
const showExpandCollapse = onCompact && onExpand;
return (
<AWXToolbar>
<Toolbar marginleft={noLeftMargin ? 1 : 0}>
<ColumnLeft>
{ showSelectAll && (
{showSelectAll && (
<Fragment>
<ToolbarItem>
<Checkbox
@ -140,9 +141,7 @@ class DataListToolbar extends React.Component {
onExpand={onExpand}
/>
</ToolbarGroup>
{ additionalControls && (
<VerticalSeparator />
)}
{additionalControls && <VerticalSeparator />}
</Fragment>
)}
<AdditionalControlsWrapper>

View File

@ -65,7 +65,7 @@ describe('<DataListToolbar />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -97,12 +97,16 @@ describe('<DataListToolbar />', () => {
);
toolbar.update();
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector);
const sortDropdownToggleDescending = toolbar.find(
sortDropdownToggleSelector
);
expect(sortDropdownToggleDescending.length).toBe(1);
sortDropdownToggleDescending.simulate('click');
toolbar.update();
const sortDropdownItemsDescending = toolbar.find(dropdownMenuItems).children();
const sortDropdownItemsDescending = toolbar
.find(dropdownMenuItems)
.children();
expect(sortDropdownItemsDescending.length).toBe(2);
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
@ -128,8 +132,12 @@ describe('<DataListToolbar />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
toolbar = mountWithContexts(
<DataListToolbar
@ -188,7 +196,11 @@ describe('<DataListToolbar />', () => {
onSearch={onSearch}
onSort={onSort}
onSelectAll={onSelectAll}
additionalControls={[<button key="1" id="test" type="button">click</button>]}
additionalControls={[
<button key="1" id="test" type="button">
click
</button>,
]}
/>
);

View File

@ -7,7 +7,9 @@ const DetailName = styled(({ fullWidth, ...props }) => (
<TextListItem {...props} />
))`
font-weight: var(--pf-global--FontWeight--bold);
${props => props.fullWidth && `
${props =>
props.fullWidth &&
`
grid-column: 1;
`}
`;
@ -16,7 +18,9 @@ const DetailValue = styled(({ fullWidth, ...props }) => (
<TextListItem {...props} />
))`
word-break: break-all;
${props => props.fullWidth && `
${props =>
props.fullWidth &&
`
grid-column: 2 / -1;
`}
`;
@ -25,16 +29,10 @@ const Detail = ({ label, value, fullWidth }) => {
if (!value) return null;
return (
<Fragment>
<DetailName
component={TextListItemVariants.dt}
fullWidth={fullWidth}
>
<DetailName component={TextListItemVariants.dt} fullWidth={fullWidth}>
{label}
</DetailName>
<DetailValue
component={TextListItemVariants.dd}
fullWidth={fullWidth}
>
<DetailValue component={TextListItemVariants.dd} fullWidth={fullWidth}>
{value}
</DetailValue>
</Fragment>

View File

@ -5,9 +5,7 @@ import Detail from './Detail';
describe('Detail', () => {
test('renders the expected content', () => {
const wrapper = mount(
<Detail label="foo" />
);
const wrapper = mount(<Detail label="foo" />);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -11,9 +11,12 @@ const DetailList = ({ children, stacked, ...props }) => (
export default styled(DetailList)`
display: grid;
grid-gap: 20px;
${props => (props.stacked ? (`
${props =>
props.stacked
? `
grid-template-columns: auto 1fr;
`) : (`
`
: `
--column-count: 1;
grid-template-columns: repeat(var(--column-count), auto minmax(10em, 1fr));
@ -24,5 +27,5 @@ export default styled(DetailList)`
@media (min-width: 1210px) {
--column-count: 3;
}
`))}
`}
`;

View File

@ -7,7 +7,7 @@ import { t } from '@lingui/macro';
import {
Card as PFCard,
CardBody as PFCardBody,
Expandable as PFExpandable
Expandable as PFExpandable,
} from '@patternfly/react-core';
const Card = styled(PFCard)`
@ -25,11 +25,11 @@ const Expandable = styled(PFExpandable)`
`;
class ErrorDetail extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
isExpanded: false
isExpanded: false,
};
this.handleToggle = this.handleToggle.bind(this);
@ -37,51 +37,48 @@ class ErrorDetail extends Component {
this.renderStack = this.renderStack.bind(this);
}
handleToggle () {
handleToggle() {
const { isExpanded } = this.state;
this.setState({ isExpanded: !isExpanded });
}
renderNetworkError () {
renderNetworkError() {
const { error } = this.props;
const { response } = error;
const message = typeof response.data === 'string'
? response.data
: response.data.detail;
const message =
typeof response.data === 'string' ? response.data : response.data.detail;
return (
<Fragment>
<CardBody>
{response.config.method.toUpperCase()}
{' '}
{response.config.url}
{' '}
<strong>
{response.status}
</strong>
{response.config.method.toUpperCase()} {response.config.url}{' '}
<strong>{response.status}</strong>
</CardBody>
<CardBody>{message}</CardBody>
</Fragment>
);
}
renderStack () {
renderStack() {
const { error } = this.props;
return (<CardBody>{error.stack}</CardBody>);
return <CardBody>{error.stack}</CardBody>;
}
render () {
render() {
const { isExpanded } = this.state;
const { error, i18n } = this.props;
return (
<Expandable toggleText={i18n._(t`Details`)} onToggle={this.handleToggle} isExpanded={isExpanded}>
<Expandable
toggleText={i18n._(t`Details`)}
onToggle={this.handleToggle}
isExpanded={isExpanded}
>
<Card>
{Object.prototype.hasOwnProperty.call(error, 'response')
? this.renderNetworkError()
: this.renderStack()
}
: this.renderStack()}
</Card>
</Expandable>
);
@ -89,7 +86,7 @@ class ErrorDetail extends Component {
}
ErrorDetail.propTypes = {
error: PropTypes.instanceOf(Error).isRequired
error: PropTypes.instanceOf(Error).isRequired,
};
export default withI18n()(ErrorDetail);

View File

@ -5,15 +5,20 @@ import ErrorDetail from './ErrorDetail';
describe('ErrorDetail', () => {
test('renders the expected content', () => {
const wrapper = mountWithContexts(<ErrorDetail error={new Error({
response: {
config: {
method: 'post'
},
data: 'An error occurred'
}
})}
/>);
const wrapper = mountWithContexts(
<ErrorDetail
error={
new Error({
response: {
config: {
method: 'post',
},
data: 'An error occurred',
},
})
}
/>
);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -4,40 +4,34 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Button as PFButton,
ToolbarItem as PFToolbarItem
ToolbarItem as PFToolbarItem,
} from '@patternfly/react-core';
import {
BarsIcon,
EqualsIcon,
} from '@patternfly/react-icons';
import { BarsIcon, EqualsIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
const Button = styled(PFButton)`
padding: 0;
margin: 0;
height: 30px;
width: 30px;
${props => (props.isActive ? `
padding: 0;
margin: 0;
height: 30px;
width: 30px;
${props =>
props.isActive
? `
background-color: #007bba;
--pf-c-button--m-plain--active--Color: white;
--pf-c-button--m-plain--focus--Color: white;`
: null)};
: null};
`;
const ToolbarItem = styled(PFToolbarItem)`
& :not(:last-child) {
margin-right: 20px;
margin-right: 20px;
}
`;
class ExpandCollapse extends React.Component {
render () {
const {
isCompact,
onCompact,
onExpand,
i18n
} = this.props;
render() {
const { isCompact, onCompact, onExpand, i18n } = this.props;
return (
<Fragment>
@ -69,11 +63,11 @@ class ExpandCollapse extends React.Component {
ExpandCollapse.propTypes = {
onCompact: PropTypes.func.isRequired,
onExpand: PropTypes.func.isRequired,
isCompact: PropTypes.bool
isCompact: PropTypes.bool,
};
ExpandCollapse.defaultProps = {
isCompact: true
isCompact: true,
};
export default withI18n()(ExpandCollapse);

View File

@ -2,10 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
ActionGroup as PFActionGroup,
Button
} from '@patternfly/react-core';
import { ActionGroup as PFActionGroup, Button } from '@patternfly/react-core';
import styled from 'styled-components';
const ActionGroup = styled(PFActionGroup)`
@ -27,8 +24,23 @@ const ActionGroup = styled(PFActionGroup)`
const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => (
<ActionGroup>
<Button aria-label={i18n._(t`Save`)} variant="primary" type="submit" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" type="button" onClick={onCancel}>{i18n._(t`Cancel`)}</Button>
<Button
aria-label={i18n._(t`Save`)}
variant="primary"
type="submit"
onClick={onSubmit}
isDisabled={submitDisabled}
>
{i18n._(t`Save`)}
</Button>
<Button
aria-label={i18n._(t`Cancel`)}
variant="secondary"
type="button"
onClick={onCancel}
>
{i18n._(t`Cancel`)}
</Button>
</ActionGroup>
);

View File

@ -6,10 +6,7 @@ import FormActionGroup from './FormActionGroup';
describe('FormActionGroup', () => {
test('renders the expected content', () => {
const wrapper = mountWithContexts(
<FormActionGroup
onSubmit={() => {}}
onCancel={() => {}}
/>
<FormActionGroup onSubmit={() => {}} onCancel={() => {}} />
);
expect(wrapper).toHaveLength(1);
});

View File

@ -9,7 +9,7 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
margin-left: 10px;
`;
function FormField (props) {
function FormField(props) {
const { id, name, label, tooltip, validate, isRequired, ...rest } = props;
return (
@ -17,7 +17,8 @@ function FormField (props) {
name={name}
validate={validate}
render={({ field, form }) => {
const isValid = form && (!form.touched[field.name] || !form.errors[field.name]);
const isValid =
form && (!form.touched[field.name] || !form.errors[field.name]);
return (
<FormGroup
@ -28,13 +29,9 @@ function FormField (props) {
label={label}
>
{tooltip && (
<Tooltip
position="right"
content={tooltip}
>
<Tooltip position="right" content={tooltip}>
<QuestionCircleIcon />
</Tooltip>
)}
<TextInput
id={id}
@ -67,7 +64,7 @@ FormField.defaultProps = {
type: 'text',
validate: () => {},
isRequired: false,
tooltip: null
tooltip: null,
};
export default FormField;

View File

@ -5,11 +5,7 @@ const Row = styled.div`
display: grid;
grid-gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
`;
export default function FormRow ({ children }) {
return (
<Row>
{children}
</Row>
);
`;
export default function FormRow({ children }) {
return <Row>{children}</Row>;
}

View File

@ -25,12 +25,12 @@ class LaunchButton extends React.Component {
templateId: number.isRequired,
};
constructor (props) {
constructor(props) {
super(props);
this.state = {
launchError: null,
promptError: false
promptError: false,
};
this.handleLaunch = this.handleLaunch.bind(this);
@ -38,18 +38,20 @@ class LaunchButton extends React.Component {
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
}
handleLaunchErrorClose () {
handleLaunchErrorClose() {
this.setState({ launchError: null });
}
handlePromptErrorClose () {
handlePromptErrorClose() {
this.setState({ promptError: false });
}
async handleLaunch () {
async handleLaunch() {
const { history, templateId } = this.props;
try {
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(templateId);
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
templateId
);
if (launchConfig.can_start_without_user_input) {
const { data: job } = await JobTemplatesAPI.launch(templateId);
history.push(`/jobs/${job.id}/details`);
@ -61,18 +63,12 @@ class LaunchButton extends React.Component {
}
}
render () {
const {
launchError,
promptError
} = this.state;
render() {
const { launchError, promptError } = this.state;
const { i18n } = this.props;
return (
<Fragment>
<Tooltip
content={i18n._(t`Launch Job`)}
position="top"
>
<Tooltip content={i18n._(t`Launch Job`)} position="top">
<div>
<StyledLaunchButton
variant="plain"
@ -98,7 +94,9 @@ class LaunchButton extends React.Component {
title={i18n._(t`Attention!`)}
onClose={this.handlePromptErrorClose}
>
{i18n._(t`Launching jobs with promptable fields is not supported at this time.`)}
{i18n._(
t`Launching jobs with promptable fields is not supported at this time.`
)}
</AlertModal>
</Fragment>
);

View File

@ -10,30 +10,28 @@ jest.mock('@api');
describe('LaunchButton', () => {
JobTemplatesAPI.readLaunch.mockResolvedValue({
data: {
can_start_without_user_input: true
}
can_start_without_user_input: true,
},
});
test('renders the expected content', () => {
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
expect(wrapper).toHaveLength(1);
});
test('redirects to details after successful launch', async (done) => {
test('redirects to details after successful launch', async done => {
const history = {
push: jest.fn(),
};
JobTemplatesAPI.launch.mockResolvedValue({
data: {
id: 9000
}
id: 9000,
},
});
const wrapper = mountWithContexts(<LaunchButton templateId={1} />, {
context: {
router: { history },
},
});
const wrapper = mountWithContexts(
<LaunchButton templateId={1} />, {
context: {
router: { history }
}
}
);
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
launchButton.simulate('click');
await sleep(0);
@ -42,24 +40,34 @@ describe('LaunchButton', () => {
expect(history.push).toHaveBeenCalledWith('/jobs/9000/details');
done();
});
test('displays error modal after unsuccessful launch', async (done) => {
JobTemplatesAPI.launch.mockRejectedValue(new Error({
response: {
config: {
method: 'post',
url: '/api/v2/job_templates/1/launch'
test('displays error modal after unsuccessful launch', async done => {
JobTemplatesAPI.launch.mockRejectedValue(
new Error({
response: {
config: {
method: 'post',
url: '/api/v2/job_templates/1/launch',
},
data: 'An error occurred',
status: 403,
},
data: 'An error occurred',
status: 403
}
}));
})
);
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
launchButton.simulate('click');
await waitForElement(wrapper, 'Modal.at-c-alertModal--danger', (el) => el.props().isOpen === true && el.props().title === 'Error!');
await waitForElement(
wrapper,
'Modal.at-c-alertModal--danger',
el => el.props().isOpen === true && el.props().title === 'Error!'
);
const modalCloseButton = wrapper.find('ModalBoxCloseButton');
modalCloseButton.simulate('click');
await waitForElement(wrapper, 'Modal.at-c-alertModal--danger', (el) => el.props().isOpen === false);
await waitForElement(
wrapper,
'Modal.at-c-alertModal--danger',
el => el.props().isOpen === false
);
done();
});
});

View File

@ -19,7 +19,7 @@ import { ChipGroup, Chip } from '../Chip';
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
class Lookup extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -40,19 +40,22 @@ class Lookup extends React.Component {
this.getData = this.getData.bind(this);
}
componentDidMount () {
componentDidMount() {
this.getData();
}
componentDidUpdate (prevProps) {
componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.getData();
}
}
async getData () {
const { getItems, location: { search } } = this.props;
async getData() {
const {
getItems,
location: { search },
} = this.props;
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
this.setState({ error: false });
@ -62,26 +65,30 @@ class Lookup extends React.Component {
this.setState({
results,
count
count,
});
} catch (err) {
this.setState({ error: true });
}
}
toggleSelected (row) {
toggleSelected(row) {
const { name, onLookupSave } = this.props;
const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
const {
lookupSelectedItems: updatedSelectedItems,
isModalOpen,
} = this.state;
const selectedIndex = updatedSelectedItems
.findIndex(selectedRow => selectedRow.id === row.id);
const selectedIndex = updatedSelectedItems.findIndex(
selectedRow => selectedRow.id === row.id
);
if (selectedIndex > -1) {
updatedSelectedItems.splice(selectedIndex, 1);
this.setState({ lookupSelectedItems: updatedSelectedItems });
} else {
this.setState(prevState => ({
lookupSelectedItems: [...prevState.lookupSelectedItems, row]
lookupSelectedItems: [...prevState.lookupSelectedItems, row],
}));
}
@ -93,7 +100,7 @@ class Lookup extends React.Component {
}
}
handleModalToggle () {
handleModalToggle() {
const { isModalOpen } = this.state;
const { value } = this.props;
// Resets the selected items from parent state whenever modal is opened
@ -102,19 +109,19 @@ class Lookup extends React.Component {
if (!isModalOpen) {
this.setState({ lookupSelectedItems: [...value] });
}
this.setState((prevState) => ({
this.setState(prevState => ({
isModalOpen: !prevState.isModalOpen,
}));
}
saveModal () {
saveModal() {
const { onLookupSave, name } = this.props;
const { lookupSelectedItems } = this.state;
onLookupSave(lookupSelectedItems, name);
this.handleModalToggle();
}
render () {
render() {
const {
isModalOpen,
lookupSelectedItems,
@ -147,9 +154,7 @@ class Lookup extends React.Component {
>
<SearchIcon />
</Button>
<div className="pf-c-form-control">
{chips}
</div>
<div className="pf-c-form-control">{chips}</div>
</InputGroup>
<Modal
className="awx-c-modal"
@ -157,8 +162,21 @@ class Lookup extends React.Component {
isOpen={isModalOpen}
onClose={this.handleModalToggle}
actions={[
<Button key="save" variant="primary" onClick={this.saveModal} style={(results.length === 0) ? { display: 'none' } : {}}>{i18n._(t`Save`)}</Button>,
<Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>{(results.length === 0) ? i18n._(t`Close`) : i18n._(t`Cancel`)}</Button>
<Button
key="save"
variant="primary"
onClick={this.saveModal}
style={results.length === 0 ? { display: 'none' } : {}}
>
{i18n._(t`Save`)}
</Button>,
<Button
key="cancel"
variant="secondary"
onClick={this.handleModalToggle}
>
{results.length === 0 ? i18n._(t`Close`) : i18n._(t`Cancel`)}
</Button>,
]}
>
<PaginatedDataList
@ -176,7 +194,7 @@ class Lookup extends React.Component {
onSelect={() => this.toggleSelected(item)}
/>
)}
renderToolbar={(props) => (
renderToolbar={props => (
<DataListToolbar {...props} alignToolbarLeft />
)}
showPageSizeOptions={false}
@ -189,7 +207,7 @@ class Lookup extends React.Component {
onRemove={this.toggleSelected}
/>
)}
{ error ? <div>error</div> : '' }
{error ? <div>error</div> : ''}
</Modal>
</Fragment>
);

View File

@ -5,9 +5,7 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
import Lookup, { _Lookup } from './Lookup';
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
const mockColumns = [
{ name: 'Name', key: 'name', isSortable: true }
];
const mockColumns = [{ name: 'Name', key: 'name', isSortable: true }];
describe('<Lookup />', () => {
test('initially renders succesfully', () => {
mountWithContexts(
@ -15,29 +13,33 @@ describe('<Lookup />', () => {
lookupHeader="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={() => { }}
getItems={() => { }}
onLookupSave={() => {}}
getItems={() => {}}
columns={mockColumns}
sortedColumnKey="name"
/>
);
});
test('API response is formatted properly', (done) => {
test('API response is formatted properly', done => {
const wrapper = mountWithContexts(
<Lookup
lookupHeader="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={() => { }}
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
onLookupSave={() => {}}
getItems={() => ({
data: { results: [{ name: 'test instance', id: 1 }] },
})}
columns={mockColumns}
sortedColumnKey="name"
/>
).find('Lookup');
setImmediate(() => {
expect(wrapper.state().results).toEqual([{ id: 1, name: 'test instance' }]);
expect(wrapper.state().results).toEqual([
{ id: 1, name: 'test instance' },
]);
done();
});
});
@ -51,8 +53,8 @@ describe('<Lookup />', () => {
lookupHeader="Foo Bar"
name="fooBar"
value={mockSelected}
onLookupSave={() => { }}
getItems={() => { }}
onLookupSave={() => {}}
getItems={() => {}}
columns={mockColumns}
sortedColumnKey="name"
/>
@ -62,20 +64,20 @@ describe('<Lookup />', () => {
const searchItem = wrapper.find('button[aria-label="Search"]');
searchItem.first().simulate('click');
expect(spy).toHaveBeenCalled();
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
expect(wrapper.state('lookupSelectedItems')).toEqual([
{
id: 1,
name: 'foo',
},
]);
expect(wrapper.state('isModalOpen')).toEqual(true);
});
test('calls "toggleSelected" when a user changes a checkbox', (done) => {
test('calls "toggleSelected" when a user changes a checkbox', done => {
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
const mockSelected = [{ name: 'foo', id: 1 }];
const data = {
results: [
{ name: 'test instance', id: 1, url: '/foo' }
],
results: [{ name: 'test instance', id: 1, url: '/foo' }],
count: 1,
};
const wrapper = mountWithContexts(
@ -84,7 +86,7 @@ describe('<Lookup />', () => {
lookupHeader="Foo Bar"
name="fooBar"
value={mockSelected}
onLookupSave={() => { }}
onLookupSave={() => {}}
getItems={() => ({ data })}
columns={mockColumns}
sortedColumnKey="name"
@ -103,9 +105,7 @@ describe('<Lookup />', () => {
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
const data = {
results: [
{ name: 'test instance', id: 1, url: '/foo' }
],
results: [{ name: 'test instance', id: 1, url: '/foo' }],
count: 1,
};
const wrapper = mountWithContexts(
@ -114,7 +114,7 @@ describe('<Lookup />', () => {
lookupHeader="Foo Bar"
name="fooBar"
value={mockData}
onLookupSave={() => { }}
onLookupSave={() => {}}
getItems={() => ({ data })}
columns={mockColumns}
sortedColumnKey="name"
@ -130,10 +130,10 @@ describe('<Lookup />', () => {
const wrapper = mountWithContexts(
<Lookup
lookupHeader="Foo Bar"
onLookupSave={() => { }}
onLookupSave={() => {}}
value={mockData}
selected={[]}
getItems={() => { }}
getItems={() => {}}
columns={mockColumns}
sortedColumnKey="name"
/>
@ -147,24 +147,26 @@ describe('<Lookup />', () => {
const wrapper = mountWithContexts(
<Lookup
lookupHeader="Foo Bar"
onLookupSave={() => { }}
onLookupSave={() => {}}
value={mockData}
getItems={() => { }}
getItems={() => {}}
columns={mockColumns}
sortedColumnKey="name"
/>
).find('Lookup');
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
name: 'foo',
});
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
expect(wrapper.state('lookupSelectedItems')).toEqual([
{
id: 1,
name: 'foo',
},
]);
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
name: 'foo',
});
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
});
@ -178,23 +180,30 @@ describe('<Lookup />', () => {
name="fooBar"
value={mockData}
onLookupSave={onLookupSaveFn}
getItems={() => { }}
getItems={() => {}}
sortedColumnKey="name"
/>
).find('Lookup');
wrapper.instance().toggleSelected({
id: 1,
name: 'foo'
name: 'foo',
});
expect(wrapper.state('lookupSelectedItems')).toEqual([{
id: 1,
name: 'foo'
}]);
expect(wrapper.state('lookupSelectedItems')).toEqual([
{
id: 1,
name: 'foo',
},
]);
wrapper.instance().saveModal();
expect(onLookupSaveFn).toHaveBeenCalledWith([{
id: 1,
name: 'foo'
}], 'fooBar');
expect(onLookupSaveFn).toHaveBeenCalledWith(
[
{
id: 1,
name: 'foo',
},
],
'fooBar'
);
});
test('should re-fetch data when URL params change', async () => {
@ -205,7 +214,7 @@ describe('<Lookup />', () => {
const wrapper = mountWithContexts(
<_Lookup
lookupHeader="Foo Bar"
onLookupSave={() => { }}
onLookupSave={() => {}}
value={mockData}
selected={[]}
columns={mockColumns}

View File

@ -1,15 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
withRouter
} from 'react-router-dom';
import {
NavExpandable,
NavItem,
} from '@patternfly/react-core';
import { withRouter } from 'react-router-dom';
import { NavExpandable, NavItem } from '@patternfly/react-core';
class NavExpandableGroup extends Component {
constructor (props) {
constructor(props) {
super(props);
const { routes } = this.props;
// Extract a list of paths from the route params and store them for later. This creates
@ -20,17 +15,17 @@ class NavExpandableGroup extends Component {
this.isActivePath = this.isActivePath.bind(this);
}
isActiveGroup () {
isActiveGroup() {
return this.navItemPaths.some(this.isActivePath);
}
isActivePath (path) {
isActivePath(path) {
const { history } = this.props;
return history.location.pathname.startsWith(path);
}
render () {
render() {
const { groupId, groupTitle, routes } = this.props;
const isActive = this.isActiveGroup();

View File

@ -21,7 +21,9 @@ describe('NavExpandableGroup', () => {
/>
</Nav>
</MemoryRouter>
).find('NavExpandableGroup').instance();
)
.find('NavExpandableGroup')
.instance();
expect(component.navItemPaths).toEqual(['/foo', '/bar', '/fiz']);
expect(component.isActiveGroup()).toEqual(true);
@ -54,7 +56,9 @@ describe('NavExpandableGroup', () => {
/>
</Nav>
</MemoryRouter>
).find('NavExpandableGroup').instance();
)
.find('NavExpandableGroup')
.instance();
expect(component.isActivePath(path)).toEqual(expected);
});

View File

@ -18,7 +18,7 @@ const DataListCell = styled(PFDataListCell)`
display: flex;
justify-content: ${props => (props.righthalf ? 'flex-start' : 'inherit')};
padding-bottom: ${props => (props.righthalf ? '16px' : '8px')};
@media screen and (min-width: 768px) {
justify-content: ${props => (props.righthalf ? 'flex-end' : 'inherit')};
padding-bottom: 0;
@ -30,7 +30,7 @@ const Switch = styled(PFSwitch)`
flex-wrap: no-wrap;
`;
function NotificationListItem (props) {
function NotificationListItem(props) {
const {
canToggleNotifications,
notification,
@ -38,7 +38,7 @@ function NotificationListItem (props) {
successTurnedOn,
errorTurnedOn,
toggleNotification,
i18n
i18n,
} = props;
return (
@ -47,50 +47,50 @@ function NotificationListItem (props) {
key={notification.id}
>
<DataListItemRow>
<DataListItemCells dataListCells={[
<DataListCell key="name">
<Link
to={{
pathname: detailUrl
}}
css="margin-right: 1.5em;"
>
<b id={`items-list-item-${notification.id}`}>{notification.name}</b>
</Link>
<Badge
css="text-transform: capitalize;"
isRead
>
{notification.notification_type}
</Badge>
</DataListCell>,
<DataListCell righthalf="true" key="toggles">
<Switch
id={`notification-${notification.id}-success-toggle`}
label={i18n._(t`Successful`)}
isChecked={successTurnedOn}
isDisabled={!canToggleNotifications}
onChange={() => toggleNotification(
notification.id,
successTurnedOn,
'success'
)}
aria-label={i18n._(t`Toggle notification success`)}
/>
<Switch
id={`notification-${notification.id}-error-toggle`}
label={i18n._(t`Failure`)}
isChecked={errorTurnedOn}
isDisabled={!canToggleNotifications}
onChange={() => toggleNotification(
notification.id,
errorTurnedOn,
'error'
)}
aria-label={i18n._(t`Toggle notification failure`)}
/>
</DataListCell>
]}
<DataListItemCells
dataListCells={[
<DataListCell key="name">
<Link
to={{
pathname: detailUrl,
}}
css="margin-right: 1.5em;"
>
<b id={`items-list-item-${notification.id}`}>
{notification.name}
</b>
</Link>
<Badge css="text-transform: capitalize;" isRead>
{notification.notification_type}
</Badge>
</DataListCell>,
<DataListCell righthalf="true" key="toggles">
<Switch
id={`notification-${notification.id}-success-toggle`}
label={i18n._(t`Successful`)}
isChecked={successTurnedOn}
isDisabled={!canToggleNotifications}
onChange={() =>
toggleNotification(
notification.id,
successTurnedOn,
'success'
)
}
aria-label={i18n._(t`Toggle notification success`)}
/>
<Switch
id={`notification-${notification.id}-error-toggle`}
label={i18n._(t`Failure`)}
isChecked={errorTurnedOn}
isDisabled={!canToggleNotifications}
onChange={() =>
toggleNotification(notification.id, errorTurnedOn, 'error')
}
aria-label={i18n._(t`Toggle notification failure`)}
/>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>

View File

@ -48,7 +48,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
canToggleNotifications
/>
);
wrapper.find('Switch').first().find('input').simulate('change');
wrapper
.find('Switch')
.first()
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
});
@ -66,7 +70,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
canToggleNotifications
/>
);
wrapper.find('Switch').first().find('input').simulate('change');
wrapper
.find('Switch')
.first()
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
});
@ -84,7 +92,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
canToggleNotifications
/>
);
wrapper.find('Switch').at(1).find('input').simulate('change');
wrapper
.find('Switch')
.at(1)
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'error');
});
@ -102,7 +114,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
canToggleNotifications
/>
);
wrapper.find('Switch').at(1).find('input').simulate('change');
wrapper
.find('Switch')
.at(1)
.find('input')
.simulate('change');
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'error');
});
});

View File

@ -10,18 +10,19 @@ import {
Toolbar,
ToolbarGroup,
ToolbarItem,
Tooltip
Tooltip,
} from '@patternfly/react-core';
import { QuestionCircleIcon, UserIcon } from '@patternfly/react-icons';
const DOCLINK = 'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
const DOCLINK =
'https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html';
class PageHeaderToolbar extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
isHelpOpen: false,
isUserOpen: false
isUserOpen: false,
};
this.handleHelpSelect = this.handleHelpSelect.bind(this);
@ -30,34 +31,34 @@ class PageHeaderToolbar extends Component {
this.handleUserToggle = this.handleUserToggle.bind(this);
}
handleHelpSelect () {
handleHelpSelect() {
const { isHelpOpen } = this.state;
this.setState({ isHelpOpen: !isHelpOpen });
}
handleUserSelect () {
handleUserSelect() {
const { isUserOpen } = this.state;
this.setState({ isUserOpen: !isUserOpen });
}
handleHelpToggle (isOpen) {
handleHelpToggle(isOpen) {
this.setState({ isHelpOpen: isOpen });
}
handleUserToggle (isOpen) {
handleUserToggle(isOpen) {
this.setState({ isUserOpen: isOpen });
}
render () {
render() {
const { isHelpOpen, isUserOpen } = this.state;
const {
isAboutDisabled,
onAboutClick,
onLogoutClick,
loggedInUser,
i18n
i18n,
} = this.props;
return (
@ -70,11 +71,11 @@ class PageHeaderToolbar extends Component {
isOpen={isHelpOpen}
position={DropdownPosition.right}
onSelect={this.handleHelpSelect}
toggle={(
toggle={
<DropdownToggle onToggle={this.handleHelpToggle}>
<QuestionCircleIcon />
</DropdownToggle>
)}
}
dropdownItems={[
<DropdownItem key="help" target="_blank" href={DOCLINK}>
{i18n._(t`Help`)}
@ -86,7 +87,7 @@ class PageHeaderToolbar extends Component {
onClick={onAboutClick}
>
{i18n._(t`About`)}
</DropdownItem>
</DropdownItem>,
]}
/>
</ToolbarItem>
@ -98,7 +99,7 @@ class PageHeaderToolbar extends Component {
isOpen={isUserOpen}
position={DropdownPosition.right}
onSelect={this.handleUserSelect}
toggle={(
toggle={
<DropdownToggle onToggle={this.handleUserToggle}>
<UserIcon />
{loggedInUser && (
@ -107,7 +108,7 @@ class PageHeaderToolbar extends Component {
</span>
)}
</DropdownToggle>
)}
}
dropdownItems={[
<DropdownItem key="user" href="#/home">
{i18n._(t`User Details`)}
@ -118,7 +119,7 @@ class PageHeaderToolbar extends Component {
onClick={onLogoutClick}
>
{i18n._(t`Logout`)}
</DropdownItem>
</DropdownItem>,
]}
/>
</ToolbarItem>
@ -132,11 +133,11 @@ class PageHeaderToolbar extends Component {
PageHeaderToolbar.propTypes = {
isAboutDisabled: PropTypes.bool,
onAboutClick: PropTypes.func.isRequired,
onLogoutClick: PropTypes.func.isRequired
onLogoutClick: PropTypes.func.isRequired,
};
PageHeaderToolbar.defaultProps = {
isAboutDisabled: false
isAboutDisabled: false,
};
export default withI18n()(PageHeaderToolbar);

View File

@ -26,19 +26,19 @@ const EmptyStateControlsWrapper = styled.div`
margin-bottom: 20px;
justify-content: flex-end;
& > :not(:first-child) {
& > :not(:first-child) {
margin-left: 20px;
}
`;
class PaginatedDataList extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.handleSetPage = this.handleSetPage.bind(this);
this.handleSetPageSize = this.handleSetPageSize.bind(this);
this.handleSort = this.handleSort.bind(this);
}
getSortOrder () {
getSortOrder() {
const { qsConfig, location } = this.props;
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
@ -47,29 +47,30 @@ class PaginatedDataList extends React.Component {
return [queryParams.order_by, 'ascending'];
}
handleSetPage (event, pageNumber) {
handleSetPage(event, pageNumber) {
this.pushHistoryState({ page: pageNumber });
}
handleSetPageSize (event, pageSize) {
handleSetPageSize(event, pageSize) {
this.pushHistoryState({ page_size: pageSize });
}
handleSort (sortedColumnKey, sortOrder) {
handleSort(sortedColumnKey, sortOrder) {
this.pushHistoryState({
order_by: sortOrder === 'ascending' ? sortedColumnKey : `-${sortedColumnKey}`,
order_by:
sortOrder === 'ascending' ? sortedColumnKey : `-${sortedColumnKey}`,
page: null,
});
}
pushHistoryState (newParams) {
pushHistoryState(newParams) {
const { history, qsConfig } = this.props;
const { pathname, search } = history.location;
const qs = updateNamespacedQueryString(qsConfig, search, newParams);
history.push(`${pathname}?${qs}`);
}
render () {
render() {
const [orderBy, sortOrder] = this.getSortOrder();
const {
contentError,
@ -87,25 +88,35 @@ class PaginatedDataList extends React.Component {
i18n,
renderToolbar,
} = this.props;
const columns = toolbarColumns.length ? toolbarColumns : [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
const columns = toolbarColumns.length
? toolbarColumns
: [{ name: i18n._(t`Name`), key: 'name', isSortable: true }];
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
const itemDisplayName = ucFirst(pluralize(itemName));
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
const itemDisplayNamePlural = ucFirst(
itemNamePlural || pluralize(itemName)
);
const dataListLabel = i18n._(t`${itemDisplayName} List`);
const emptyContentMessage = i18n._(t`Please add ${itemDisplayNamePlural} to populate this list `);
const emptyContentMessage = i18n._(
t`Please add ${itemDisplayNamePlural} to populate this list `
);
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
let Content;
if (hasContentLoading && items.length <= 0) {
Content = (<ContentLoading />);
Content = <ContentLoading />;
} else if (contentError) {
Content = (<ContentError error={contentError} />);
Content = <ContentError error={contentError} />;
} else if (items.length <= 0) {
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
Content = (
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
);
} else {
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
Content = (
<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>
);
}
if (items.length <= 0) {
@ -116,9 +127,7 @@ class PaginatedDataList extends React.Component {
{emptyStateControls}
</EmptyStateControlsWrapper>
)}
{emptyStateControls && (
<div css="border-bottom: 1px solid #d2d2d2" />
)}
{emptyStateControls && <div css="border-bottom: 1px solid #d2d2d2" />}
{Content}
</Fragment>
);
@ -130,7 +139,7 @@ class PaginatedDataList extends React.Component {
sortedColumnKey: orderBy,
sortOrder,
columns,
onSearch: () => { },
onSearch: () => {},
onSort: this.handleSort,
})}
{Content}
@ -139,12 +148,16 @@ class PaginatedDataList extends React.Component {
itemCount={itemCount}
page={queryParams.page || 1}
perPage={queryParams.page_size}
perPageOptions={showPageSizeOptions ? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 }
] : []}
perPageOptions={
showPageSizeOptions
? [
{ title: '5', value: 5 },
{ title: '10', value: 10 },
{ title: '20', value: 20 },
{ title: '50', value: 50 },
]
: []
}
onSetPage={this.handleSetPage}
onPerPageSelect={this.handleSetPageSize}
/>
@ -166,11 +179,13 @@ PaginatedDataList.propTypes = {
itemNamePlural: PropTypes.string,
qsConfig: QSConfig.isRequired,
renderItem: PropTypes.func,
toolbarColumns: arrayOf(shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
})),
toolbarColumns: arrayOf(
shape({
name: string.isRequired,
key: string.isRequired,
isSortable: bool,
})
),
showPageSizeOptions: PropTypes.bool,
renderToolbar: PropTypes.func,
hasContentLoading: PropTypes.bool,
@ -184,8 +199,8 @@ PaginatedDataList.defaultProps = {
itemName: 'item',
itemNamePlural: '',
showPageSizeOptions: true,
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
renderToolbar: (props) => (<DataListToolbar {...props} />),
renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,
renderToolbar: props => <DataListToolbar {...props} />,
};
export { PaginatedDataList as _PaginatedDataList };

View File

@ -52,7 +52,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name',
}}
qsConfig={qsConfig}
/>, { context: { router: { history } } }
/>,
{ context: { router: { history } } }
);
const toolbar = wrapper.find('DataListToolbar');
@ -85,7 +86,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name',
}}
qsConfig={qsConfig}
/>, { context: { router: { history } } }
/>,
{ context: { router: { history } } }
);
const pagination = wrapper.find('Pagination');
@ -110,7 +112,8 @@ describe('<PaginatedDataList />', () => {
order_by: 'name',
}}
qsConfig={qsConfig}
/>, { context: { router: { history } } }
/>,
{ context: { router: { history } } }
);
const pagination = wrapper.find('Pagination');

View File

@ -17,24 +17,20 @@ const DetailWrapper = styled(TextContent)`
grid-gap: 10px;
`;
export default function PaginatedDataListItem ({ item }) {
export default function PaginatedDataListItem({ item }) {
return (
<DataListItem
aria-labelledby={`items-list-item-${item.id}`}
key={item.id}
>
<DataListItem aria-labelledby={`items-list-item-${item.id}`} key={item.id}>
<DataListItemRow>
<DataListItemCells dataListCells={[
<DataListCell key="team-name">
<DetailWrapper>
<Link to={{ pathname: item.url }}>
<b id={`items-list-item-${item.id}`}>
{item.name}
</b>
</Link>
</DetailWrapper>
</DataListCell>
]}
<DataListItemCells
dataListCells={[
<DataListCell key="team-name">
<DetailWrapper>
<Link to={{ pathname: item.url }}>
<b id={`items-list-item-${item.id}`}>{item.name}</b>
</Link>
</DetailWrapper>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>

View File

@ -15,16 +15,15 @@ const Button = styled(PFButton)`
}
`;
function ToolbarAddButton ({ linkTo, onClick, i18n }) {
function ToolbarAddButton({ linkTo, onClick, i18n }) {
if (!linkTo && !onClick) {
throw new Error('ToolbarAddButton requires either `linkTo` or `onClick` prop');
throw new Error(
'ToolbarAddButton requires either `linkTo` or `onClick` prop'
);
}
if (linkTo) {
return (
<Tooltip
content={i18n._(t`Add`)}
position="top"
>
<Tooltip content={i18n._(t`Add`)} position="top">
<Button
component={Link}
to={linkTo}
@ -37,11 +36,7 @@ function ToolbarAddButton ({ linkTo, onClick, i18n }) {
);
}
return (
<Button
variant="primary"
aria-label={i18n._(t`Add`)}
onClick={onClick}
>
<Button variant="primary" aria-label={i18n._(t`Add`)} onClick={onClick}>
<PlusIcon />
</Button>
);
@ -52,7 +47,7 @@ ToolbarAddButton.propTypes = {
};
ToolbarAddButton.defaultProps = {
linkTo: null,
onClick: null
onClick: null,
};
export default withI18n()(ToolbarAddButton);

View File

@ -5,9 +5,7 @@ import ToolbarAddButton from './ToolbarAddButton';
describe('<ToolbarAddButton />', () => {
test('should render button', () => {
const onClick = jest.fn();
const wrapper = mountWithContexts(
<ToolbarAddButton onClick={onClick} />
);
const wrapper = mountWithContexts(<ToolbarAddButton onClick={onClick} />);
const button = wrapper.find('button');
expect(button).toHaveLength(1);
button.simulate('click');
@ -15,9 +13,7 @@ describe('<ToolbarAddButton />', () => {
});
test('should render link', () => {
const wrapper = mountWithContexts(
<ToolbarAddButton linkTo="/foo" />
);
const wrapper = mountWithContexts(<ToolbarAddButton linkTo="/foo" />);
const link = wrapper.find('Link');
expect(link).toHaveLength(1);
expect(link.prop('to')).toBe('/foo');

View File

@ -12,7 +12,7 @@ const DeleteButton = styled(Button)`
padding: 5px 8px;
&:hover {
background-color:#d9534f;
background-color: #d9534f;
color: white;
}
@ -33,7 +33,7 @@ const ItemToDelete = shape({
}).isRequired,
});
function cannotDelete (item) {
function cannotDelete(item) {
return !item.summary_fields.user_capabilities.delete;
}
@ -48,7 +48,7 @@ class ToolbarDeleteButton extends React.Component {
itemName: 'item',
};
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -60,34 +60,34 @@ class ToolbarDeleteButton extends React.Component {
this.handleDelete = this.handleDelete.bind(this);
}
handleConfirmDelete () {
handleConfirmDelete() {
this.setState({ isModalOpen: true });
}
handleCancelDelete () {
this.setState({ isModalOpen: false, });
handleCancelDelete() {
this.setState({ isModalOpen: false });
}
handleDelete () {
handleDelete() {
const { onDelete } = this.props;
onDelete();
this.setState({ isModalOpen: false });
}
renderTooltip () {
renderTooltip() {
const { itemsToDelete, itemName, i18n } = this.props;
const itemsUnableToDelete = itemsToDelete
.filter(cannotDelete)
.map(item => (
<div key={item.id}>
{item.name}
</div>
));
.map(item => <div key={item.id}>{item.name}</div>);
if (itemsToDelete.some(cannotDelete)) {
return (
<div>
{i18n._(t`You do not have permission to delete the following ${pluralize(itemName)}: ${itemsUnableToDelete}`)}
{i18n._(
t`You do not have permission to delete the following ${pluralize(
itemName
)}: ${itemsUnableToDelete}`
)}
</div>
);
}
@ -97,22 +97,19 @@ class ToolbarDeleteButton extends React.Component {
return i18n._(t`Select a row to delete`);
}
render () {
render() {
const { itemsToDelete, itemName, i18n } = this.props;
const { isModalOpen } = this.state;
const isDisabled = itemsToDelete.length === 0
|| itemsToDelete.some(cannotDelete);
const isDisabled =
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
// NOTE: Once PF supports tooltips on disabled elements,
// we can delete the extra <div> around the <DeleteButton> below.
// See: https://github.com/patternfly/patternfly-react/issues/1894
return (
<Fragment>
<Tooltip
content={this.renderTooltip()}
position="top"
>
<Tooltip content={this.renderTooltip()} position="top">
<div>
<DeleteButton
variant="plain"
@ -124,12 +121,13 @@ class ToolbarDeleteButton extends React.Component {
</DeleteButton>
</div>
</Tooltip>
{ isModalOpen && (
{isModalOpen && (
<AlertModal
variant="danger"
title={itemsToDelete === 1
? i18n._(t`Delete ${itemName}`)
: i18n._(t`Delete ${pluralize(itemName)}`)
title={
itemsToDelete === 1
? i18n._(t`Delete ${itemName}`)
: i18n._(t`Delete ${pluralize(itemName)}`)
}
isOpen={isModalOpen}
onClose={this.handleCancelDelete}
@ -149,16 +147,14 @@ class ToolbarDeleteButton extends React.Component {
onClick={this.handleCancelDelete}
>
{i18n._(t`Cancel`)}
</Button>
</Button>,
]}
>
{i18n._(t`Are you sure you want to delete:`)}
<br />
{itemsToDelete.map((item) => (
{itemsToDelete.map(item => (
<span key={item.id}>
<strong>
{item.name}
</strong>
<strong>{item.name}</strong>
<br />
</span>
))}

View File

@ -16,10 +16,7 @@ const itemB = {
describe('<ToolbarDeleteButton />', () => {
test('should render button', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton
onDelete={() => {}}
itemsToDelete={[]}
/>
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[]} />
);
expect(wrapper.find('button')).toHaveLength(1);
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
@ -27,14 +24,10 @@ describe('<ToolbarDeleteButton />', () => {
test('should open confirmation modal', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton
onDelete={() => {}}
itemsToDelete={[itemA]}
/>
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
);
wrapper.find('button').simulate('click');
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen'))
.toBe(true);
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(true);
wrapper.update();
expect(wrapper.find('Modal')).toHaveLength(1);
});
@ -42,33 +35,26 @@ describe('<ToolbarDeleteButton />', () => {
test('should invoke onDelete prop', () => {
const onDelete = jest.fn();
const wrapper = mountWithContexts(
<ToolbarDeleteButton
onDelete={onDelete}
itemsToDelete={[itemA]}
/>
<ToolbarDeleteButton onDelete={onDelete} itemsToDelete={[itemA]} />
);
wrapper.find('ToolbarDeleteButton').setState({ isModalOpen: true });
wrapper.find('button.pf-m-danger').simulate('click');
expect(onDelete).toHaveBeenCalled();
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(false);
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(
false
);
});
test('should disable button when no delete permissions', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton
onDelete={() => {}}
itemsToDelete={[itemB]}
/>
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemB]} />
);
expect(wrapper.find('button[disabled]')).toHaveLength(1);
});
test('should render tooltip', () => {
const wrapper = mountWithContexts(
<ToolbarDeleteButton
onDelete={() => {}}
itemsToDelete={[itemA]}
/>
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
);
expect(wrapper.find('Tooltip')).toHaveLength(1);
expect(wrapper.find('Tooltip').prop('content')).toEqual('Delete');

View File

@ -1,18 +1,24 @@
import React from 'react';
import styled, { css } from 'styled-components';
import { Pagination as PFPagination, DropdownDirection } from '@patternfly/react-core';
import {
Pagination as PFPagination,
DropdownDirection,
} from '@patternfly/react-core';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
const AWXPagination = styled(PFPagination)`
${props => (props.perPageOptions && !props.perPageOptions.length && css`
.pf-c-options-menu__toggle-button {
${props =>
props.perPageOptions &&
!props.perPageOptions.length &&
css`
.pf-c-options-menu__toggle-button {
display: none;
}
`)}
`}
`;
export default (props) => (
export default props => (
<I18n>
{({ i18n }) => (
<AWXPagination
@ -27,7 +33,7 @@ export default (props) => (
toNextPage: i18n._(t`Go to next page`),
optionsToggle: i18n._(t`Select`),
currPage: i18n._(t`Current page`),
paginationTitle: i18n._(t`Pagination`)
paginationTitle: i18n._(t`Pagination`),
}}
dropDirection={DropdownDirection.up}
{...props}

View File

@ -5,12 +5,7 @@ import Pagination from './Pagination';
describe('Pagination', () => {
test('renders the expected content', () => {
const wrapper = mountWithContexts(
<Pagination
itemCount={0}
max={9000}
/>
);
const wrapper = mountWithContexts(<Pagination itemCount={0} max={9000} />);
expect(wrapper).toHaveLength(1);
});
});

View File

@ -25,13 +25,14 @@ const Tabs = styled(PFTabs)`
right: 0;
bottom: 0;
left: 0;
content: "";
content: '';
border: solid var(--pf-c-tabs__item--BorderColor);
border-width: var(--pf-c-tabs__item--BorderWidth) 0 var(--pf-c-tabs__item--BorderWidth) 0;
border-width: var(--pf-c-tabs__item--BorderWidth) 0
var(--pf-c-tabs__item--BorderWidth) 0;
}
`;
function RoutedTabs (props) {
function RoutedTabs(props) {
const { history, tabsArray } = props;
const getActiveTabId = () => {
@ -42,7 +43,7 @@ function RoutedTabs (props) {
return 0;
};
function handleTabSelect (event, eventKey) {
function handleTabSelect(event, eventKey) {
const match = tabsArray.find(tab => tab.id === eventKey);
if (match) {
history.push(match.link);
@ -50,10 +51,7 @@ function RoutedTabs (props) {
}
return (
<Tabs
activeKey={getActiveTabId()}
onSelect={handleTabSelect}
>
<Tabs activeKey={getActiveTabId()} onSelect={handleTabSelect}>
{tabsArray.map(tab => (
<Tab
className={`${tab.name}`}
@ -70,14 +68,16 @@ function RoutedTabs (props) {
RoutedTabs.propTypes = {
history: shape({
location: shape({
pathname: string.isRequired
pathname: string.isRequired,
}).isRequired,
}).isRequired,
tabsArray: arrayOf(shape({
id: number.isRequired,
link: string.isRequired,
name: string.isRequired,
})).isRequired,
tabsArray: arrayOf(
shape({
id: number.isRequired,
link: string.isRequired,
name: string.isRequired,
})
).isRequired,
};
export { RoutedTabs as _RoutedTabs };

View File

@ -13,7 +13,7 @@ const tabs = [
{ name: 'Details', link: '/organizations/19/details', id: 1 },
{ name: 'Access', link: '/organizations/19/access', id: 2 },
{ name: 'Teams', link: '/organizations/19/teams', id: 3 },
{ name: 'Notification', link: '/organizations/19/notification', id: 4 }
{ name: 'Notification', link: '/organizations/19/notification', id: 4 },
];
describe('<RoutedTabs />', () => {
@ -24,21 +24,14 @@ describe('<RoutedTabs />', () => {
});
test('RoutedTabs renders successfully', () => {
wrapper = shallow(
<_RoutedTabs
tabsArray={tabs}
history={history}
/>
);
wrapper = shallow(<_RoutedTabs tabsArray={tabs} history={history} />);
expect(wrapper.find(Tab)).toHaveLength(4);
});
test('Given a URL the correct tab is active', async () => {
wrapper = mount(
<Router history={history}>
<RoutedTabs
tabsArray={tabs}
/>
<RoutedTabs tabsArray={tabs} />
</Router>
);
@ -49,9 +42,7 @@ describe('<RoutedTabs />', () => {
test('should update history when new tab selected', async () => {
wrapper = mount(
<Router history={history}>
<RoutedTabs
tabsArray={tabs}
/>
<RoutedTabs tabsArray={tabs} />
</Router>
);

View File

@ -8,11 +8,9 @@ import {
DropdownPosition,
DropdownToggle,
DropdownItem,
TextInput as PFTextInput
TextInput as PFTextInput,
} from '@patternfly/react-core';
import {
SearchIcon
} from '@patternfly/react-icons';
import { SearchIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
@ -27,7 +25,8 @@ const Button = styled(PFButton)`
`;
const Dropdown = styled(PFDropdown)`
&&& { /* Higher specificity required because we are selecting unclassed elements */
&&& {
/* Higher specificity required because we are selecting unclassed elements */
> button {
min-height: 30px;
min-width: 70px;
@ -35,20 +34,22 @@ const Dropdown = styled(PFDropdown)`
padding: 0 10px;
margin: 0px;
> span { /* text element */
> span {
/* text element */
width: auto;
}
> svg { /* caret icon */
> svg {
/* caret icon */
margin: 0px;
padding-top: 3px;
padding-left: 3px;
}
}
}
}
`;
class Search extends React.Component {
constructor (props) {
constructor(props) {
super(props);
const { sortedColumnKey } = this.props;
@ -64,11 +65,11 @@ class Search extends React.Component {
this.handleSearch = this.handleSearch.bind(this);
}
handleDropdownToggle (isSearchDropdownOpen) {
handleDropdownToggle(isSearchDropdownOpen) {
this.setState({ isSearchDropdownOpen });
}
handleDropdownSelect ({ target }) {
handleDropdownSelect({ target }) {
const { columns } = this.props;
const { innerText } = target;
@ -76,30 +77,25 @@ class Search extends React.Component {
this.setState({ isSearchDropdownOpen: false, searchKey });
}
handleSearch () {
handleSearch() {
const { searchValue } = this.state;
const { onSearch } = this.props;
onSearch(searchValue);
}
handleSearchInputChange (searchValue) {
handleSearchInputChange(searchValue) {
this.setState({ searchValue });
}
render () {
render() {
const { up } = DropdownPosition;
const {
columns,
i18n
} = this.props;
const {
isSearchDropdownOpen,
searchKey,
searchValue,
} = this.state;
const { columns, i18n } = this.props;
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
const { name: searchColumnName } = columns.find(({ key }) => key === searchKey);
const { name: searchColumnName } = columns.find(
({ key }) => key === searchKey
);
const searchDropdownItems = columns
.filter(({ key }) => key !== searchKey)
@ -116,14 +112,14 @@ class Search extends React.Component {
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSearchDropdownOpen}
toggle={(
toggle={
<DropdownToggle
id="awx-search"
onToggle={this.handleDropdownToggle}
>
{searchColumnName}
</DropdownToggle>
)}
}
dropdownItems={searchDropdownItems}
/>
<TextInput
@ -153,7 +149,7 @@ Search.propTypes = {
Search.defaultProps = {
onSearch: null,
sortedColumnKey: 'name'
sortedColumnKey: 'name',
};
export default withI18n()(Search);

View File

@ -20,11 +20,7 @@ describe('<Search />', () => {
const onSearch = jest.fn();
search = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
);
search.find(searchTextInput).instance().value = 'test-321';
@ -39,11 +35,7 @@ describe('<Search />', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
).find('Search');
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
wrapper.instance().handleDropdownToggle(true);
@ -53,18 +45,16 @@ describe('<Search />', () => {
test('handleDropdownSelect properly updates state', async () => {
const columns = [
{ name: 'Name', key: 'name', isSortable: true },
{ name: 'Description', key: 'description', isSortable: true }
{ name: 'Description', key: 'description', isSortable: true },
];
const onSearch = jest.fn();
const wrapper = mountWithContexts(
<Search
sortedColumnKey="name"
columns={columns}
onSearch={onSearch}
/>
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
).find('Search');
expect(wrapper.state('searchKey')).toEqual('name');
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } });
wrapper
.instance()
.handleDropdownSelect({ target: { innerText: 'Description' } });
expect(wrapper.state('searchKey')).toEqual('description');
});
});

View File

@ -19,20 +19,18 @@ const SplitLabelItem = styled(SplitItem)`
`;
class SelectedList extends Component {
render () {
render() {
const {
label,
selected,
showOverflowAfter,
onRemove,
displayKey,
isReadOnly
isReadOnly,
} = this.props;
return (
<Split>
<SplitLabelItem>
{label}
</SplitLabelItem>
<SplitLabelItem>{label}</SplitLabelItem>
<VerticalSeparator />
<SplitItem>
<ChipGroup showOverflowAfter={showOverflowAfter}>
@ -58,7 +56,7 @@ SelectedList.propTypes = {
onRemove: PropTypes.func,
selected: PropTypes.arrayOf(PropTypes.object).isRequired,
showOverflowAfter: PropTypes.number,
isReadOnly: PropTypes.bool
isReadOnly: PropTypes.bool,
};
SelectedList.defaultProps = {
@ -66,7 +64,7 @@ SelectedList.defaultProps = {
label: 'Selected',
onRemove: () => null,
showOverflowAfter: 5,
isReadOnly: false
isReadOnly: false,
};
export default SelectedList;

View File

@ -8,11 +8,12 @@ describe('<SelectedList />', () => {
const mockSelected = [
{
id: 1,
name: 'foo'
}, {
name: 'foo',
},
{
id: 2,
name: 'bar'
}
name: 'bar',
},
];
mount(
<SelectedList
@ -43,8 +44,8 @@ describe('<SelectedList />', () => {
const mockSelected = [
{
id: 1,
name: 'foo'
}
name: 'foo',
},
];
const wrapper = mount(
<SelectedList
@ -54,10 +55,13 @@ describe('<SelectedList />', () => {
onRemove={onRemove}
/>
);
wrapper.find('.pf-c-chip button').first().simulate('click');
wrapper
.find('.pf-c-chip button')
.first()
.simulate('click');
expect(onRemove).toBeCalledWith({
id: 1,
name: 'foo'
name: 'foo',
});
});
});

View File

@ -7,13 +7,13 @@ import {
Dropdown as PFDropdown,
DropdownPosition,
DropdownToggle,
DropdownItem
DropdownItem,
} from '@patternfly/react-core';
import {
SortAlphaDownIcon,
SortAlphaUpIcon,
SortNumericDownIcon,
SortNumericUpIcon
SortNumericUpIcon,
} from '@patternfly/react-icons';
import styled from 'styled-components';
@ -27,17 +27,19 @@ const Dropdown = styled(PFDropdown)`
padding: 0 10px;
margin: 0px;
> span { /* text element within dropdown */
> span {
/* text element within dropdown */
width: auto;
}
> svg { /* caret icon */
> svg {
/* caret icon */
margin: 0px;
padding-top: 3px;
padding-left: 3px;
}
}
}
}
`;
const IconWrapper = styled.span`
@ -47,7 +49,7 @@ const IconWrapper = styled.span`
`;
class Sort extends React.Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -59,40 +61,36 @@ class Sort extends React.Component {
this.handleSort = this.handleSort.bind(this);
}
handleDropdownToggle (isSortDropdownOpen) {
handleDropdownToggle(isSortDropdownOpen) {
this.setState({ isSortDropdownOpen });
}
handleDropdownSelect ({ target }) {
handleDropdownSelect({ target }) {
const { columns, onSort, sortOrder } = this.props;
const { innerText } = target;
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
const [{ key: searchKey }] = columns.filter(
({ name }) => name === innerText
);
this.setState({ isSortDropdownOpen: false });
onSort(searchKey, sortOrder);
}
handleSort () {
handleSort() {
const { onSort, sortedColumnKey, sortOrder } = this.props;
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
onSort(sortedColumnKey, newSortOrder);
}
render () {
render() {
const { up } = DropdownPosition;
const {
columns,
sortedColumnKey,
sortOrder,
i18n
} = this.props;
const {
isSortDropdownOpen
} = this.state;
const [{ name: sortedColumnName, isNumeric }] = columns
.filter(({ key }) => key === sortedColumnKey);
const { columns, sortedColumnKey, sortOrder, i18n } = this.props;
const { isSortDropdownOpen } = this.state;
const [{ name: sortedColumnName, isNumeric }] = columns.filter(
({ key }) => key === sortedColumnKey
);
const sortDropdownItems = columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
@ -104,28 +102,30 @@ class Sort extends React.Component {
let SortIcon;
if (isNumeric) {
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
SortIcon =
sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
} else {
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
SortIcon =
sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
}
return (
<React.Fragment>
{ sortDropdownItems.length > 1 && (
{sortDropdownItems.length > 1 && (
<Dropdown
style={{ marginRight: '20px' }}
onToggle={this.handleDropdownToggle}
onSelect={this.handleDropdownSelect}
direction={up}
isOpen={isSortDropdownOpen}
toggle={(
toggle={
<DropdownToggle
id="awx-sort"
onToggle={this.handleDropdownToggle}
>
{sortedColumnName}
</DropdownToggle>
)}
}
dropdownItems={sortDropdownItems}
/>
)}
@ -148,13 +148,13 @@ Sort.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSort: PropTypes.func,
sortOrder: PropTypes.string,
sortedColumnKey: PropTypes.string
sortedColumnKey: PropTypes.string,
};
Sort.defaultProps = {
onSort: null,
sortOrder: 'ascending',
sortedColumnKey: 'name'
sortedColumnKey: 'name',
};
export default withI18n()(Sort);

View File

@ -38,7 +38,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -62,7 +62,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -86,7 +86,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -109,7 +109,7 @@ describe('<Sort />', () => {
{ name: 'Foo', key: 'foo', isSortable: true },
{ name: 'Bar', key: 'bar', isSortable: true },
{ name: 'Bakery', key: 'bakery', isSortable: true },
{ name: 'Baz', key: 'baz' }
{ name: 'Baz', key: 'baz' },
];
const onSort = jest.fn();
@ -133,8 +133,12 @@ describe('<Sort />', () => {
const downAlphaIconSelector = 'SortAlphaDownIcon';
const upAlphaIconSelector = 'SortAlphaUpIcon';
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
const numericColumns = [
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
];
const alphaColumns = [
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
];
const onSort = jest.fn();
sort = mountWithContexts(

View File

@ -1,13 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {
Route,
Switch,
Redirect
} from 'react-router-dom';
import {
I18n
} from '@lingui/react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import '@patternfly/react-core/dist/styles/base.css';
@ -44,19 +38,21 @@ import RootProvider from './RootProvider';
import { BrandName } from './variables';
// eslint-disable-next-line import/prefer-default-export
export function main (render) {
export function main(render) {
const el = document.getElementById('app');
document.title = `Ansible ${BrandName}`;
const defaultRedirect = () => (<Redirect to="/home" />);
const defaultRedirect = () => <Redirect to="/home" />;
const removeTrailingSlash = (
<Route
exact
strict
path="/*/"
render={({ history: { location: { pathname, search, hash } } }) => (
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
)}
render={({
history: {
location: { pathname, search, hash },
},
}) => <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />}
/>
);
const loginRoutes = (
@ -64,9 +60,7 @@ export function main (render) {
{removeTrailingSlash}
<Route
path="/login"
render={() => (
<Login isAuthenticated={isAuthenticated} />
)}
render={() => <Login isAuthenticated={isAuthenticated} />}
/>
<Redirect to="/login" />
</Switch>
@ -77,7 +71,9 @@ export function main (render) {
<I18n>
{({ i18n }) => (
<Background>
{!isAuthenticated(document.cookie) ? loginRoutes : (
{!isAuthenticated(document.cookie) ? (
loginRoutes
) : (
<Switch>
{removeTrailingSlash}
<Route path="/login" render={defaultRedirect} />
@ -94,22 +90,22 @@ export function main (render) {
{
title: i18n._(t`Dashboard`),
path: '/home',
component: Dashboard
component: Dashboard,
},
{
title: i18n._(t`Jobs`),
path: '/jobs',
component: Jobs
component: Jobs,
},
{
title: i18n._(t`Schedules`),
path: '/schedules',
component: Schedules
component: Schedules,
},
{
title: i18n._(t`My View`),
path: '/portal',
component: Portal
component: Portal,
},
],
},
@ -120,27 +116,27 @@ export function main (render) {
{
title: i18n._(t`Templates`),
path: '/templates',
component: Templates
component: Templates,
},
{
title: i18n._(t`Credentials`),
path: '/credentials',
component: Credentials
component: Credentials,
},
{
title: i18n._(t`Projects`),
path: '/projects',
component: Projects
component: Projects,
},
{
title: i18n._(t`Inventories`),
path: '/inventories',
component: Inventories
component: Inventories,
},
{
title: i18n._(t`Inventory Scripts`),
path: '/inventory_scripts',
component: InventoryScripts
component: InventoryScripts,
},
],
},
@ -151,17 +147,17 @@ export function main (render) {
{
title: i18n._(t`Organizations`),
path: '/organizations',
component: Organizations
component: Organizations,
},
{
title: i18n._(t`Users`),
path: '/users',
component: Users
component: Users,
},
{
title: i18n._(t`Teams`),
path: '/teams',
component: Teams
component: Teams,
},
],
},
@ -172,27 +168,27 @@ export function main (render) {
{
title: i18n._(t`Credential Types`),
path: '/credential_types',
component: CredentialTypes
component: CredentialTypes,
},
{
title: i18n._(t`Notifications`),
path: '/notification_templates',
component: NotificationTemplates
component: NotificationTemplates,
},
{
title: i18n._(t`Management Jobs`),
path: '/management_jobs',
component: ManagementJobs
component: ManagementJobs,
},
{
title: i18n._(t`Instance Groups`),
path: '/instance_groups',
component: InstanceGroups
component: InstanceGroups,
},
{
title: i18n._(t`Integrations`),
path: '/applications',
component: Applications
component: Applications,
},
],
},
@ -203,34 +199,37 @@ export function main (render) {
{
title: i18n._(t`Authentication`),
path: '/auth_settings',
component: AuthSettings
component: AuthSettings,
},
{
title: i18n._(t`Jobs`),
path: '/jobs_settings',
component: JobsSettings
component: JobsSettings,
},
{
title: i18n._(t`System`),
path: '/system_settings',
component: SystemSettings
component: SystemSettings,
},
{
title: i18n._(t`User Interface`),
path: '/ui_settings',
component: UISettings
component: UISettings,
},
{
title: i18n._(t`License`),
path: '/license',
component: License
component: License,
},
],
},
]}
render={({ routeGroups }) => (
render={({ routeGroups }) =>
routeGroups
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
.reduce(
(allRoutes, { routes }) => allRoutes.concat(routes),
[]
)
.map(({ component: PageComponent, path }) => (
<Route
key={path}
@ -240,7 +239,7 @@ export function main (render) {
)}
/>
))
)}
}
/>
)}
/>
@ -249,7 +248,8 @@ export function main (render) {
</Background>
)}
</I18n>
</RootProvider>, el || document.createElement('div')
</RootProvider>,
el || document.createElement('div')
);
}

Some files were not shown because too many files have changed in this diff Show More