mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Run prettier on all the files in awx/ui_next
This commit is contained in:
parent
051bbcaeb5
commit
55ce409a12
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -36,5 +36,5 @@ export {
|
||||
UnifiedJobTemplatesAPI,
|
||||
UnifiedJobsAPI,
|
||||
UsersAPI,
|
||||
WorkflowJobTemplatesAPI
|
||||
WorkflowJobTemplatesAPI,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Config extends Base {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/config/';
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class InstanceGroups extends Base {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/instance_groups/';
|
||||
}
|
||||
|
||||
@ -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/`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Jobs extends Base {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/jobs/';
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Me extends Base {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/me/';
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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/`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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/';
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class UnifiedJobs extends Base {
|
||||
constructor (http) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/unified_jobs/';
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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/';
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
`)}
|
||||
`}
|
||||
`;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`))}
|
||||
`}
|
||||
`;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user