mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
Run prettier on all the files in awx/ui_next
This commit is contained in:
@@ -35,12 +35,13 @@ const PageHeader = styled(PFPageHeader)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class App extends Component {
|
class App extends Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// initialize with a closed navbar if window size is small
|
// initialize with a closed navbar if window size is small
|
||||||
const isNavOpen = typeof window !== 'undefined'
|
const isNavOpen =
|
||||||
&& window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
typeof window !== 'undefined' &&
|
||||||
|
window.innerWidth >= parseInt(global_breakpoint_md.value, 10);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
ansible_version: null,
|
ansible_version: null,
|
||||||
@@ -49,7 +50,7 @@ class App extends Component {
|
|||||||
version: null,
|
version: null,
|
||||||
isAboutModalOpen: false,
|
isAboutModalOpen: false,
|
||||||
isNavOpen,
|
isNavOpen,
|
||||||
configError: null
|
configError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleLogout = this.handleLogout.bind(this);
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
@@ -59,39 +60,48 @@ class App extends Component {
|
|||||||
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
this.handleConfigErrorClose = this.handleConfigErrorClose.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount () {
|
async componentDidMount() {
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
async handleLogout () {
|
async handleLogout() {
|
||||||
await RootAPI.logout();
|
await RootAPI.logout();
|
||||||
window.location.replace('/#/login');
|
window.location.replace('/#/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAboutOpen () {
|
handleAboutOpen() {
|
||||||
this.setState({ isAboutModalOpen: true });
|
this.setState({ isAboutModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAboutClose () {
|
handleAboutClose() {
|
||||||
this.setState({ isAboutModalOpen: false });
|
this.setState({ isAboutModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNavToggle () {
|
handleNavToggle() {
|
||||||
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
this.setState(({ isNavOpen }) => ({ isNavOpen: !isNavOpen }));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfigErrorClose () {
|
handleConfigErrorClose() {
|
||||||
this.setState({
|
this.setState({
|
||||||
configError: null
|
configError: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadConfig () {
|
async loadConfig() {
|
||||||
try {
|
try {
|
||||||
const [configRes, meRes] = await Promise.all([ConfigAPI.read(), MeAPI.read()]);
|
const [configRes, meRes] = await Promise.all([
|
||||||
const { data: { ansible_version, custom_virtualenvs, version } } = configRes;
|
ConfigAPI.read(),
|
||||||
const { data: { results: [me] } } = meRes;
|
MeAPI.read(),
|
||||||
|
]);
|
||||||
|
const {
|
||||||
|
data: { ansible_version, custom_virtualenvs, version },
|
||||||
|
} = configRes;
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
results: [me],
|
||||||
|
},
|
||||||
|
} = meRes;
|
||||||
|
|
||||||
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
this.setState({ ansible_version, custom_virtualenvs, version, me });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -99,7 +109,7 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
ansible_version,
|
ansible_version,
|
||||||
custom_virtualenvs,
|
custom_virtualenvs,
|
||||||
@@ -107,7 +117,7 @@ class App extends Component {
|
|||||||
isNavOpen,
|
isNavOpen,
|
||||||
me,
|
me,
|
||||||
version,
|
version,
|
||||||
configError
|
configError,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
i18n,
|
i18n,
|
||||||
@@ -122,47 +132,43 @@ class App extends Component {
|
|||||||
onNavToggle={this.handleNavToggle}
|
onNavToggle={this.handleNavToggle}
|
||||||
logo={<BrandLogo />}
|
logo={<BrandLogo />}
|
||||||
logoProps={{ href: '/' }}
|
logoProps={{ href: '/' }}
|
||||||
toolbar={(
|
toolbar={
|
||||||
<PageHeaderToolbar
|
<PageHeaderToolbar
|
||||||
loggedInUser={me}
|
loggedInUser={me}
|
||||||
isAboutDisabled={!version}
|
isAboutDisabled={!version}
|
||||||
onAboutClick={this.handleAboutOpen}
|
onAboutClick={this.handleAboutOpen}
|
||||||
onLogoutClick={this.handleLogout}
|
onLogoutClick={this.handleLogout}
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sidebar = (
|
const sidebar = (
|
||||||
<PageSidebar
|
<PageSidebar
|
||||||
isNavOpen={isNavOpen}
|
isNavOpen={isNavOpen}
|
||||||
nav={(
|
nav={
|
||||||
<Nav aria-label={navLabel}>
|
<Nav aria-label={navLabel}>
|
||||||
<NavList>
|
<NavList>
|
||||||
{routeGroups.map(
|
{routeGroups.map(({ groupId, groupTitle, routes }) => (
|
||||||
({ groupId, groupTitle, routes }) => (
|
<NavExpandableGroup
|
||||||
<NavExpandableGroup
|
key={groupId}
|
||||||
key={groupId}
|
groupId={groupId}
|
||||||
groupId={groupId}
|
groupTitle={groupTitle}
|
||||||
groupTitle={groupTitle}
|
routes={routes}
|
||||||
routes={routes}
|
/>
|
||||||
/>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</NavList>
|
</NavList>
|
||||||
</Nav>
|
</Nav>
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Page
|
<Page usecondensed="True" header={header} sidebar={sidebar}>
|
||||||
usecondensed="True"
|
<ConfigProvider
|
||||||
header={header}
|
value={{ ansible_version, custom_virtualenvs, me, version }}
|
||||||
sidebar={sidebar}
|
>
|
||||||
>
|
|
||||||
<ConfigProvider value={{ ansible_version, custom_virtualenvs, me, version }}>
|
|
||||||
{render({ routeGroups })}
|
{render({ routeGroups })}
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -14,13 +14,14 @@ describe('<App />', () => {
|
|||||||
const version = '222';
|
const version = '222';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ConfigAPI.read = () => Promise.resolve({
|
ConfigAPI.read = () =>
|
||||||
data: {
|
Promise.resolve({
|
||||||
ansible_version,
|
data: {
|
||||||
custom_virtualenvs,
|
ansible_version,
|
||||||
version
|
custom_virtualenvs,
|
||||||
}
|
version,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
|
MeAPI.read = () => Promise.resolve({ data: { results: [{}] } });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,15 +44,13 @@ describe('<App />', () => {
|
|||||||
{
|
{
|
||||||
groupTitle: 'Group Two',
|
groupTitle: 'Group Two',
|
||||||
groupId: 'group_two',
|
groupId: 'group_two',
|
||||||
routes: [
|
routes: [{ title: 'Fiz', path: '/fiz' }],
|
||||||
{ title: 'Fiz', path: '/fiz' },
|
},
|
||||||
]
|
|
||||||
}
|
|
||||||
]}
|
]}
|
||||||
render={({ routeGroups }) => (
|
render={({ routeGroups }) =>
|
||||||
routeGroups.map(({ groupId }) => (<div key={groupId} id={groupId} />))
|
routeGroups.map(({ groupId }) => <div key={groupId} id={groupId} />)
|
||||||
)}
|
}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// page components
|
// page components
|
||||||
@@ -70,7 +69,7 @@ describe('<App />', () => {
|
|||||||
expect(appWrapper.find('#group_two').length).toBe(1);
|
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 />);
|
const wrapper = mountWithContexts(<App />);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@@ -83,7 +82,11 @@ describe('<App />', () => {
|
|||||||
await waitForElement(wrapper, aboutDropdown);
|
await waitForElement(wrapper, aboutDropdown);
|
||||||
wrapper.find(aboutDropdown).simulate('click');
|
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');
|
button.simulate('click');
|
||||||
|
|
||||||
// check about modal content
|
// 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');
|
const appWrapper = mountWithContexts(<App />).find('App');
|
||||||
appWrapper.instance().handleLogout();
|
appWrapper.instance().handleLogout();
|
||||||
await asyncFlush();
|
await asyncFlush();
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import {
|
import { I18nProvider } from '@lingui/react';
|
||||||
I18nProvider,
|
|
||||||
} from '@lingui/react';
|
|
||||||
|
|
||||||
import {
|
import { HashRouter } from 'react-router-dom';
|
||||||
HashRouter
|
|
||||||
} from 'react-router-dom';
|
|
||||||
|
|
||||||
import ja from '../build/locales/ja/messages';
|
import ja from '../build/locales/ja/messages';
|
||||||
import en from '../build/locales/en/messages';
|
import en from '../build/locales/en/messages';
|
||||||
|
|
||||||
export function getLanguage (nav) {
|
export function getLanguage(nav) {
|
||||||
const language = (nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
const language =
|
||||||
|
(nav.languages && nav.languages[0]) || nav.language || nav.userLanguage;
|
||||||
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0];
|
||||||
|
|
||||||
return languageWithoutRegionCode;
|
return languageWithoutRegionCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RootProvider extends Component {
|
class RootProvider extends Component {
|
||||||
render () {
|
render() {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
const catalogs = { en, ja };
|
const catalogs = { en, ja };
|
||||||
@@ -26,10 +23,7 @@ class RootProvider extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<I18nProvider
|
<I18nProvider language={language} catalogs={catalogs}>
|
||||||
language={language}
|
|
||||||
catalogs={catalogs}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -3,8 +3,16 @@ import { getLanguage } from './RootProvider';
|
|||||||
describe('RootProvider.jsx', () => {
|
describe('RootProvider.jsx', () => {
|
||||||
test('getLanguage returns the expected language code', () => {
|
test('getLanguage returns the expected language code', () => {
|
||||||
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
expect(getLanguage({ languages: ['es-US'] })).toEqual('es');
|
||||||
expect(getLanguage({ languages: ['es-US'], language: 'fr-FR', userLanguage: 'en-US' })).toEqual('es');
|
expect(
|
||||||
expect(getLanguage({ language: 'fr-FR', userLanguage: 'en-US' })).toEqual('fr');
|
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');
|
expect(getLanguage({ userLanguage: 'en-US' })).toEqual('en');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,40 +2,40 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const defaultHttp = axios.create({
|
const defaultHttp = axios.create({
|
||||||
xsrfCookieName: 'csrftoken',
|
xsrfCookieName: 'csrftoken',
|
||||||
xsrfHeaderName: 'X-CSRFToken'
|
xsrfHeaderName: 'X-CSRFToken',
|
||||||
});
|
});
|
||||||
|
|
||||||
class Base {
|
class Base {
|
||||||
constructor (http = defaultHttp, baseURL) {
|
constructor(http = defaultHttp, baseURL) {
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.baseUrl = baseURL;
|
this.baseUrl = baseURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
create (data) {
|
create(data) {
|
||||||
return this.http.post(this.baseUrl, data);
|
return this.http.post(this.baseUrl, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy (id) {
|
destroy(id) {
|
||||||
return this.http.delete(`${this.baseUrl}${id}/`);
|
return this.http.delete(`${this.baseUrl}${id}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
read (params = {}) {
|
read(params = {}) {
|
||||||
return this.http.get(this.baseUrl, { params });
|
return this.http.get(this.baseUrl, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
readDetail (id) {
|
readDetail(id) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/`);
|
return this.http.get(`${this.baseUrl}${id}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
readOptions () {
|
readOptions() {
|
||||||
return this.http.options(this.baseUrl);
|
return this.http.options(this.baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
replace (id, data) {
|
replace(id, data) {
|
||||||
return this.http.put(`${this.baseUrl}${id}/`, data);
|
return this.http.put(`${this.baseUrl}${id}/`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
update (id, data) {
|
update(id, data) {
|
||||||
return this.http.patch(`${this.baseUrl}${id}/`, data);
|
return this.http.patch(`${this.baseUrl}${id}/`, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import Base from './Base';
|
|||||||
describe('Base', () => {
|
describe('Base', () => {
|
||||||
const createPromise = () => Promise.resolve();
|
const createPromise = () => Promise.resolve();
|
||||||
const mockBaseURL = '/api/v2/organizations/';
|
const mockBaseURL = '/api/v2/organizations/';
|
||||||
const mockHttp = ({
|
const mockHttp = {
|
||||||
delete: jest.fn(createPromise),
|
delete: jest.fn(createPromise),
|
||||||
get: jest.fn(createPromise),
|
get: jest.fn(createPromise),
|
||||||
options: jest.fn(createPromise),
|
options: jest.fn(createPromise),
|
||||||
patch: jest.fn(createPromise),
|
patch: jest.fn(createPromise),
|
||||||
post: jest.fn(createPromise),
|
post: jest.fn(createPromise),
|
||||||
put: jest.fn(createPromise)
|
put: jest.fn(createPromise),
|
||||||
});
|
};
|
||||||
|
|
||||||
const BaseAPI = new Base(mockHttp, mockBaseURL);
|
const BaseAPI = new Base(mockHttp, mockBaseURL);
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ describe('Base', () => {
|
|||||||
jest.clearAllMocks();
|
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 ' };
|
const data = { name: 'test ' };
|
||||||
await BaseAPI.create(data);
|
await BaseAPI.create(data);
|
||||||
|
|
||||||
@@ -28,17 +28,19 @@ describe('Base', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destroy calls http method with expected data', async (done) => {
|
test('destroy calls http method with expected data', async done => {
|
||||||
const resourceId = 1;
|
const resourceId = 1;
|
||||||
await BaseAPI.destroy(resourceId);
|
await BaseAPI.destroy(resourceId);
|
||||||
|
|
||||||
expect(mockHttp.delete).toHaveBeenCalledTimes(1);
|
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();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('read calls http method with expected data', async (done) => {
|
test('read calls http method with expected data', async done => {
|
||||||
const defaultParams = {};
|
const defaultParams = {};
|
||||||
const testParams = { foo: 'bar' };
|
const testParams = { foo: 'bar' };
|
||||||
|
|
||||||
@@ -51,17 +53,19 @@ describe('Base', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('readDetail calls http method with expected data', async (done) => {
|
test('readDetail calls http method with expected data', async done => {
|
||||||
const resourceId = 1;
|
const resourceId = 1;
|
||||||
|
|
||||||
await BaseAPI.readDetail(resourceId);
|
await BaseAPI.readDetail(resourceId);
|
||||||
|
|
||||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
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();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('readOptions calls http method with expected data', async (done) => {
|
test('readOptions calls http method with expected data', async done => {
|
||||||
await BaseAPI.readOptions();
|
await BaseAPI.readOptions();
|
||||||
|
|
||||||
expect(mockHttp.options).toHaveBeenCalledTimes(1);
|
expect(mockHttp.options).toHaveBeenCalledTimes(1);
|
||||||
@@ -69,27 +73,31 @@ describe('Base', () => {
|
|||||||
done();
|
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 resourceId = 1;
|
||||||
const data = { name: 'test ' };
|
const data = { name: 'test ' };
|
||||||
|
|
||||||
await BaseAPI.replace(resourceId, data);
|
await BaseAPI.replace(resourceId, data);
|
||||||
|
|
||||||
expect(mockHttp.put).toHaveBeenCalledTimes(1);
|
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);
|
expect(mockHttp.put.mock.calls[0][1]).toEqual(data);
|
||||||
|
|
||||||
done();
|
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 resourceId = 1;
|
||||||
const data = { name: 'test ' };
|
const data = { name: 'test ' };
|
||||||
|
|
||||||
await BaseAPI.update(resourceId, data);
|
await BaseAPI.update(resourceId, data);
|
||||||
|
|
||||||
expect(mockHttp.patch).toHaveBeenCalledTimes(1);
|
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);
|
expect(mockHttp.patch.mock.calls[0][1]).toEqual(data);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -36,5 +36,5 @@ export {
|
|||||||
UnifiedJobTemplatesAPI,
|
UnifiedJobTemplatesAPI,
|
||||||
UnifiedJobsAPI,
|
UnifiedJobsAPI,
|
||||||
UsersAPI,
|
UsersAPI,
|
||||||
WorkflowJobTemplatesAPI
|
WorkflowJobTemplatesAPI,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
const InstanceGroupsMixin = (parent) => class extends parent {
|
const InstanceGroupsMixin = parent =>
|
||||||
readInstanceGroups (resourceId, params = {}) {
|
class extends parent {
|
||||||
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, { params });
|
readInstanceGroups(resourceId, params = {}) {
|
||||||
}
|
return this.http.get(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
associateInstanceGroup (resourceId, instanceGroupId) {
|
associateInstanceGroup(resourceId, instanceGroupId) {
|
||||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId });
|
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||||
}
|
id: instanceGroupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
disassociateInstanceGroup (resourceId, instanceGroupId) {
|
disassociateInstanceGroup(resourceId, instanceGroupId) {
|
||||||
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, { id: instanceGroupId, disassociate: true });
|
return this.http.post(`${this.baseUrl}${resourceId}/instance_groups/`, {
|
||||||
}
|
id: instanceGroupId,
|
||||||
};
|
disassociate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default InstanceGroupsMixin;
|
export default InstanceGroupsMixin;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Config extends Base {
|
class Config extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/config/';
|
this.baseUrl = '/api/v2/config/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class InstanceGroups extends Base {
|
class InstanceGroups extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/instance_groups/';
|
this.baseUrl = '/api/v2/instance_groups/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Base from '../Base';
|
|||||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||||
|
|
||||||
class JobTemplates extends InstanceGroupsMixin(Base) {
|
class JobTemplates extends InstanceGroupsMixin(Base) {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/job_templates/';
|
this.baseUrl = '/api/v2/job_templates/';
|
||||||
|
|
||||||
@@ -10,11 +10,11 @@ class JobTemplates extends InstanceGroupsMixin(Base) {
|
|||||||
this.readLaunch = this.readLaunch.bind(this);
|
this.readLaunch = this.readLaunch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
launch (id, data) {
|
launch(id, data) {
|
||||||
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
return this.http.post(`${this.baseUrl}${id}/launch/`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
readLaunch (id) {
|
readLaunch(id) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
return this.http.get(`${this.baseUrl}${id}/launch/`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Jobs extends Base {
|
class Jobs extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/jobs/';
|
this.baseUrl = '/api/v2/jobs/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Me extends Base {
|
class Me extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/me/';
|
this.baseUrl = '/api/v2/me/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import NotificationsMixin from '../mixins/Notifications.mixin';
|
|||||||
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||||
|
|
||||||
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
class Organizations extends InstanceGroupsMixin(NotificationsMixin(Base)) {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/organizations/';
|
this.baseUrl = '/api/v2/organizations/';
|
||||||
}
|
}
|
||||||
|
|
||||||
readAccessList (id, params = {}) {
|
readAccessList(id, params = {}) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/access_list/`, { 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 });
|
return this.http.get(`${this.baseUrl}${id}/teams/`, { params });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe('OrganizationsAPI', () => {
|
|||||||
const orgId = 1;
|
const orgId = 1;
|
||||||
const searchParams = { foo: 'bar' };
|
const searchParams = { foo: 'bar' };
|
||||||
const createPromise = () => Promise.resolve();
|
const createPromise = () => Promise.resolve();
|
||||||
const mockHttp = ({ get: jest.fn(createPromise) });
|
const mockHttp = { get: jest.fn(createPromise) };
|
||||||
|
|
||||||
const OrganizationsAPI = new Organizations(mockHttp);
|
const OrganizationsAPI = new Organizations(mockHttp);
|
||||||
|
|
||||||
@@ -13,24 +13,36 @@ describe('OrganizationsAPI', () => {
|
|||||||
jest.clearAllMocks();
|
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);
|
||||||
await OrganizationsAPI.readAccessList(orgId, searchParams);
|
await OrganizationsAPI.readAccessList(orgId, searchParams);
|
||||||
|
|
||||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||||
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: {} });
|
expect(mockHttp.get.mock.calls[0]).toContainEqual(
|
||||||
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/access_list/`, { params: searchParams });
|
`/api/v2/organizations/${orgId}/access_list/`,
|
||||||
|
{ params: {} }
|
||||||
|
);
|
||||||
|
expect(mockHttp.get.mock.calls[1]).toContainEqual(
|
||||||
|
`/api/v2/organizations/${orgId}/access_list/`,
|
||||||
|
{ params: searchParams }
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
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);
|
||||||
await OrganizationsAPI.readTeams(orgId, searchParams);
|
await OrganizationsAPI.readTeams(orgId, searchParams);
|
||||||
|
|
||||||
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
expect(mockHttp.get).toHaveBeenCalledTimes(2);
|
||||||
expect(mockHttp.get.mock.calls[0]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: {} });
|
expect(mockHttp.get.mock.calls[0]).toContainEqual(
|
||||||
expect(mockHttp.get.mock.calls[1]).toContainEqual(`/api/v2/organizations/${orgId}/teams/`, { params: searchParams });
|
`/api/v2/organizations/${orgId}/teams/`,
|
||||||
|
{ params: {} }
|
||||||
|
);
|
||||||
|
expect(mockHttp.get.mock.calls[1]).toContainEqual(
|
||||||
|
`/api/v2/organizations/${orgId}/teams/`,
|
||||||
|
{ params: searchParams }
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Root extends Base {
|
class Root extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/';
|
this.baseUrl = '/api/';
|
||||||
this.redirectURL = '/api/v2/config/';
|
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 loginUrl = `${this.baseUrl}login/`;
|
||||||
const un = encodeURIComponent(username);
|
const un = encodeURIComponent(username);
|
||||||
const pw = encodeURIComponent(password);
|
const pw = encodeURIComponent(password);
|
||||||
@@ -22,7 +22,7 @@ class Root extends Base {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
logout () {
|
logout() {
|
||||||
return this.http.get(`${this.baseUrl}logout/`);
|
return this.http.get(`${this.baseUrl}logout/`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import Root from './Root';
|
|||||||
|
|
||||||
describe('RootAPI', () => {
|
describe('RootAPI', () => {
|
||||||
const createPromise = () => Promise.resolve();
|
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);
|
const RootAPI = new Root(mockHttp);
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ describe('RootAPI', () => {
|
|||||||
jest.clearAllMocks();
|
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' };
|
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||||
|
|
||||||
await RootAPI.login('username', 'password');
|
await RootAPI.login('username', 'password');
|
||||||
@@ -24,18 +27,22 @@ describe('RootAPI', () => {
|
|||||||
done();
|
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');
|
||||||
await RootAPI.login('foo', 'bar', 'baz');
|
await RootAPI.login('foo', 'bar', 'baz');
|
||||||
|
|
||||||
expect(mockHttp.post).toHaveBeenCalledTimes(2);
|
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[0]).toContainEqual(
|
||||||
expect(mockHttp.post.mock.calls[1]).toContainEqual('username=foo&password=bar&next=baz');
|
'username=foo&password=bar&next=%2Fapi%2Fv2%2Fconfig%2F'
|
||||||
|
);
|
||||||
|
expect(mockHttp.post.mock.calls[1]).toContainEqual(
|
||||||
|
'username=foo&password=bar&next=baz'
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('logout calls expected http method', async (done) => {
|
test('logout calls expected http method', async done => {
|
||||||
await RootAPI.logout();
|
await RootAPI.logout();
|
||||||
|
|
||||||
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
expect(mockHttp.get).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Teams extends Base {
|
class Teams extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/teams/';
|
this.baseUrl = '/api/v2/teams/';
|
||||||
}
|
}
|
||||||
|
|
||||||
associateRole (teamId, roleId) {
|
associateRole(teamId, roleId) {
|
||||||
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
|
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
disassociateRole (teamId, roleId) {
|
disassociateRole(teamId, roleId) {
|
||||||
return this.http.post(`${this.baseUrl}${teamId}/roles/`, { id: roleId, disassociate: true });
|
return this.http.post(`${this.baseUrl}${teamId}/roles/`, {
|
||||||
|
id: roleId,
|
||||||
|
disassociate: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ describe('TeamsAPI', () => {
|
|||||||
const teamId = 1;
|
const teamId = 1;
|
||||||
const roleId = 7;
|
const roleId = 7;
|
||||||
const createPromise = () => Promise.resolve();
|
const createPromise = () => Promise.resolve();
|
||||||
const mockHttp = ({ post: jest.fn(createPromise) });
|
const mockHttp = { post: jest.fn(createPromise) };
|
||||||
|
|
||||||
const TeamsAPI = new Teams(mockHttp);
|
const TeamsAPI = new Teams(mockHttp);
|
||||||
|
|
||||||
@@ -12,23 +12,29 @@ describe('TeamsAPI', () => {
|
|||||||
jest.clearAllMocks();
|
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);
|
await TeamsAPI.associateRole(teamId, roleId);
|
||||||
|
|
||||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
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();
|
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);
|
await TeamsAPI.disassociateRole(teamId, roleId);
|
||||||
|
|
||||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/teams/${teamId}/roles/`, {
|
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||||
id: roleId,
|
`/api/v2/teams/${teamId}/roles/`,
|
||||||
disassociate: true
|
{
|
||||||
});
|
id: roleId,
|
||||||
|
disassociate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class UnifiedJobTemplates extends Base {
|
class UnifiedJobTemplates extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/unified_job_templates/';
|
this.baseUrl = '/api/v2/unified_job_templates/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class UnifiedJobs extends Base {
|
class UnifiedJobs extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/unified_jobs/';
|
this.baseUrl = '/api/v2/unified_jobs/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class Users extends Base {
|
class Users extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/users/';
|
this.baseUrl = '/api/v2/users/';
|
||||||
}
|
}
|
||||||
|
|
||||||
associateRole (userId, roleId) {
|
associateRole(userId, roleId) {
|
||||||
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId });
|
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId });
|
||||||
}
|
}
|
||||||
|
|
||||||
disassociateRole (userId, roleId) {
|
disassociateRole(userId, roleId) {
|
||||||
return this.http.post(`${this.baseUrl}${userId}/roles/`, { id: roleId, disassociate: true });
|
return this.http.post(`${this.baseUrl}${userId}/roles/`, {
|
||||||
|
id: roleId,
|
||||||
|
disassociate: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ describe('UsersAPI', () => {
|
|||||||
const userId = 1;
|
const userId = 1;
|
||||||
const roleId = 7;
|
const roleId = 7;
|
||||||
const createPromise = () => Promise.resolve();
|
const createPromise = () => Promise.resolve();
|
||||||
const mockHttp = ({ post: jest.fn(createPromise) });
|
const mockHttp = { post: jest.fn(createPromise) };
|
||||||
|
|
||||||
const UsersAPI = new Users(mockHttp);
|
const UsersAPI = new Users(mockHttp);
|
||||||
|
|
||||||
@@ -12,23 +12,29 @@ describe('UsersAPI', () => {
|
|||||||
jest.clearAllMocks();
|
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);
|
await UsersAPI.associateRole(userId, roleId);
|
||||||
|
|
||||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
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();
|
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);
|
await UsersAPI.disassociateRole(userId, roleId);
|
||||||
|
|
||||||
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
expect(mockHttp.post).toHaveBeenCalledTimes(1);
|
||||||
expect(mockHttp.post.mock.calls[0]).toContainEqual(`/api/v2/users/${userId}/roles/`, {
|
expect(mockHttp.post.mock.calls[0]).toContainEqual(
|
||||||
id: roleId,
|
`/api/v2/users/${userId}/roles/`,
|
||||||
disassociate: true
|
{
|
||||||
});
|
id: roleId,
|
||||||
|
disassociate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class WorkflowJobTemplates extends Base {
|
class WorkflowJobTemplates extends Base {
|
||||||
constructor (http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/workflow_job_templates/';
|
this.baseUrl = '/api/v2/workflow_job_templates/';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,6 @@
|
|||||||
--pf-c-data-list__cell--PaddingTop: 16px;
|
--pf-c-data-list__cell--PaddingTop: 16px;
|
||||||
--pf-c-data-list__cell-cell--PaddingTop: 16px;
|
--pf-c-data-list__cell-cell--PaddingTop: 16px;
|
||||||
|
|
||||||
|
|
||||||
&.pf-c-data-list__cell--divider {
|
&.pf-c-data-list__cell--divider {
|
||||||
--pf-c-data-list__cell-cell--MarginRight: 0;
|
--pf-c-data-list__cell-cell--MarginRight: 0;
|
||||||
--pf-c-data-list__cell--PaddingTop: 12px;
|
--pf-c-data-list__cell--PaddingTop: 12px;
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import {
|
|||||||
AboutModal,
|
AboutModal,
|
||||||
TextContent,
|
TextContent,
|
||||||
TextList,
|
TextList,
|
||||||
TextListItem
|
TextListItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
import { BrandName } from '../../variables';
|
import { BrandName } from '../../variables';
|
||||||
import brandLogoImg from '../../../images/brand-logo.svg';
|
import brandLogoImg from '../../../images/brand-logo.svg';
|
||||||
|
|
||||||
class About extends React.Component {
|
class About extends React.Component {
|
||||||
static createSpeechBubble (version) {
|
static createSpeechBubble(version) {
|
||||||
let text = `${BrandName} ${version}`;
|
let text = `${BrandName} ${version}`;
|
||||||
let top = '';
|
let top = '';
|
||||||
let bottom = '';
|
let bottom = '';
|
||||||
@@ -30,20 +30,14 @@ class About extends React.Component {
|
|||||||
return top + text + bottom;
|
return top + text + bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
this.createSpeechBubble = this.constructor.createSpeechBubble.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { ansible_version, version, isOpen, onClose, i18n } = this.props;
|
||||||
ansible_version,
|
|
||||||
version,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
i18n
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const speechBubble = this.createSpeechBubble(version);
|
const speechBubble = this.createSpeechBubble(version);
|
||||||
|
|
||||||
@@ -57,7 +51,7 @@ class About extends React.Component {
|
|||||||
brandImageAlt={i18n._(t`Brand Image`)}
|
brandImageAlt={i18n._(t`Brand Image`)}
|
||||||
>
|
>
|
||||||
<pre>
|
<pre>
|
||||||
{ speechBubble }
|
{speechBubble}
|
||||||
{`
|
{`
|
||||||
\\
|
\\
|
||||||
\\ ^__^
|
\\ ^__^
|
||||||
@@ -72,7 +66,7 @@ class About extends React.Component {
|
|||||||
<TextListItem component="dt">
|
<TextListItem component="dt">
|
||||||
{i18n._(t`Ansible Version`)}
|
{i18n._(t`Ansible Version`)}
|
||||||
</TextListItem>
|
</TextListItem>
|
||||||
<TextListItem component="dd">{ ansible_version }</TextListItem>
|
<TextListItem component="dd">{ansible_version}</TextListItem>
|
||||||
</TextList>
|
</TextList>
|
||||||
</TextContent>
|
</TextContent>
|
||||||
</AboutModal>
|
</AboutModal>
|
||||||
|
|||||||
@@ -7,17 +7,13 @@ describe('<About />', () => {
|
|||||||
let closeButton;
|
let closeButton;
|
||||||
const onClose = jest.fn();
|
const onClose = jest.fn();
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
aboutWrapper = mountWithContexts(
|
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
|
||||||
<About isOpen onClose={onClose} />
|
|
||||||
);
|
|
||||||
expect(aboutWrapper.length).toBe(1);
|
expect(aboutWrapper.length).toBe(1);
|
||||||
aboutWrapper.unmount();
|
aboutWrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('close button calls onClose handler', () => {
|
test('close button calls onClose handler', () => {
|
||||||
aboutWrapper = mountWithContexts(
|
aboutWrapper = mountWithContexts(<About isOpen onClose={onClose} />);
|
||||||
<About isOpen onClose={onClose} />
|
|
||||||
);
|
|
||||||
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button');
|
||||||
closeButton.simulate('click');
|
closeButton.simulate('click');
|
||||||
expect(onClose).toBeCalled();
|
expect(onClose).toBeCalled();
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import SelectRoleStep from './SelectRoleStep';
|
|||||||
import SelectableCard from './SelectableCard';
|
import SelectableCard from './SelectableCard';
|
||||||
import { TeamsAPI, UsersAPI } from '../../api';
|
import { TeamsAPI, UsersAPI } from '../../api';
|
||||||
|
|
||||||
const readUsers = async (queryParams) => UsersAPI.read(
|
const readUsers = async queryParams =>
|
||||||
Object.assign(queryParams, { is_superuser: false })
|
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 {
|
class AddResourceRole extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -25,67 +24,69 @@ class AddResourceRole extends React.Component {
|
|||||||
currentStepId: 1,
|
currentStepId: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(this);
|
this.handleResourceCheckboxClick = this.handleResourceCheckboxClick.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this.handleResourceSelect = this.handleResourceSelect.bind(this);
|
this.handleResourceSelect = this.handleResourceSelect.bind(this);
|
||||||
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
|
this.handleRoleCheckboxClick = this.handleRoleCheckboxClick.bind(this);
|
||||||
this.handleWizardNext = this.handleWizardNext.bind(this);
|
this.handleWizardNext = this.handleWizardNext.bind(this);
|
||||||
this.handleWizardSave = this.handleWizardSave.bind(this);
|
this.handleWizardSave = this.handleWizardSave.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResourceCheckboxClick (user) {
|
handleResourceCheckboxClick(user) {
|
||||||
const { selectedResourceRows } = this.state;
|
const { selectedResourceRows } = this.state;
|
||||||
|
|
||||||
const selectedIndex = selectedResourceRows
|
const selectedIndex = selectedResourceRows.findIndex(
|
||||||
.findIndex(selectedRow => selectedRow.id === user.id);
|
selectedRow => selectedRow.id === user.id
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedIndex > -1) {
|
if (selectedIndex > -1) {
|
||||||
selectedResourceRows.splice(selectedIndex, 1);
|
selectedResourceRows.splice(selectedIndex, 1);
|
||||||
this.setState({ selectedResourceRows });
|
this.setState({ selectedResourceRows });
|
||||||
} else {
|
} else {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
selectedResourceRows: [...prevState.selectedResourceRows, user]
|
selectedResourceRows: [...prevState.selectedResourceRows, user],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRoleCheckboxClick (role) {
|
handleRoleCheckboxClick(role) {
|
||||||
const { selectedRoleRows } = this.state;
|
const { selectedRoleRows } = this.state;
|
||||||
|
|
||||||
const selectedIndex = selectedRoleRows
|
const selectedIndex = selectedRoleRows.findIndex(
|
||||||
.findIndex(selectedRow => selectedRow.id === role.id);
|
selectedRow => selectedRow.id === role.id
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedIndex > -1) {
|
if (selectedIndex > -1) {
|
||||||
selectedRoleRows.splice(selectedIndex, 1);
|
selectedRoleRows.splice(selectedIndex, 1);
|
||||||
this.setState({ selectedRoleRows });
|
this.setState({ selectedRoleRows });
|
||||||
} else {
|
} else {
|
||||||
this.setState(prevState => ({
|
this.setState(prevState => ({
|
||||||
selectedRoleRows: [...prevState.selectedRoleRows, role]
|
selectedRoleRows: [...prevState.selectedRoleRows, role],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResourceSelect (resourceType) {
|
handleResourceSelect(resourceType) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedResource: resourceType,
|
selectedResource: resourceType,
|
||||||
selectedResourceRows: [],
|
selectedResourceRows: [],
|
||||||
selectedRoleRows: []
|
selectedRoleRows: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWizardNext (step) {
|
handleWizardNext(step) {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentStepId: step.id,
|
currentStepId: step.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleWizardSave () {
|
async handleWizardSave() {
|
||||||
const {
|
const { onSave } = this.props;
|
||||||
onSave
|
|
||||||
} = this.props;
|
|
||||||
const {
|
const {
|
||||||
selectedResourceRows,
|
selectedResourceRows,
|
||||||
selectedRoleRows,
|
selectedRoleRows,
|
||||||
selectedResource
|
selectedResource,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -95,11 +96,17 @@ class AddResourceRole extends React.Component {
|
|||||||
for (let j = 0; j < selectedRoleRows.length; j++) {
|
for (let j = 0; j < selectedRoleRows.length; j++) {
|
||||||
if (selectedResource === 'users') {
|
if (selectedResource === 'users') {
|
||||||
roleRequests.push(
|
roleRequests.push(
|
||||||
UsersAPI.associateRole(selectedResourceRows[i].id, selectedRoleRows[j].id)
|
UsersAPI.associateRole(
|
||||||
|
selectedResourceRows[i].id,
|
||||||
|
selectedRoleRows[j].id
|
||||||
|
)
|
||||||
);
|
);
|
||||||
} else if (selectedResource === 'teams') {
|
} else if (selectedResource === 'teams') {
|
||||||
roleRequests.push(
|
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 {
|
const {
|
||||||
selectedResource,
|
selectedResource,
|
||||||
selectedResourceRows,
|
selectedResourceRows,
|
||||||
selectedRoleRows,
|
selectedRoleRows,
|
||||||
currentStepId,
|
currentStepId,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const { onClose, roles, i18n } = this.props;
|
||||||
onClose,
|
|
||||||
roles,
|
|
||||||
i18n
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const userColumns = [
|
const userColumns = [
|
||||||
{ name: i18n._(t`Username`), key: 'username', isSortable: true }
|
{ name: i18n._(t`Username`), key: 'username', isSortable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
const teamColumns = [
|
const teamColumns = [
|
||||||
{ name: i18n._(t`Name`), key: 'name', isSortable: true }
|
{ name: i18n._(t`Name`), key: 'name', isSortable: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
let wizardTitle = '';
|
let wizardTitle = '';
|
||||||
@@ -164,7 +167,7 @@ class AddResourceRole extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
enableNext: selectedResource !== null
|
enableNext: selectedResource !== null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -195,7 +198,7 @@ class AddResourceRole extends React.Component {
|
|||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
),
|
),
|
||||||
enableNext: selectedResourceRows.length > 0
|
enableNext: selectedResourceRows.length > 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -211,8 +214,8 @@ class AddResourceRole extends React.Component {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
nextButtonText: i18n._(t`Save`),
|
nextButtonText: i18n._(t`Save`),
|
||||||
enableNext: selectedRoleRows.length > 0
|
enableNext: selectedRoleRows.length > 0,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentStep = steps.find(step => step.id === currentStepId);
|
const currentStep = steps.find(step => step.id === currentStepId);
|
||||||
@@ -236,11 +239,11 @@ class AddResourceRole extends React.Component {
|
|||||||
AddResourceRole.propTypes = {
|
AddResourceRole.propTypes = {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
onSave: PropTypes.func.isRequired,
|
onSave: PropTypes.func.isRequired,
|
||||||
roles: PropTypes.shape()
|
roles: PropTypes.shape(),
|
||||||
};
|
};
|
||||||
|
|
||||||
AddResourceRole.defaultProps = {
|
AddResourceRole.defaultProps = {
|
||||||
roles: {}
|
roles: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { AddResourceRole as _AddResourceRole };
|
export { AddResourceRole as _AddResourceRole };
|
||||||
|
|||||||
@@ -11,23 +11,20 @@ describe('<_AddResourceRole />', () => {
|
|||||||
UsersAPI.read.mockResolvedValue({
|
UsersAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: 2,
|
count: 2,
|
||||||
results: [
|
results: [{ id: 1, username: 'foo' }, { id: 2, username: 'bar' }],
|
||||||
{ id: 1, username: 'foo' },
|
},
|
||||||
{ id: 2, username: 'bar' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
const roles = {
|
const roles = {
|
||||||
admin_role: {
|
admin_role: {
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Admin'
|
name: 'Admin',
|
||||||
},
|
},
|
||||||
execute_role: {
|
execute_role: {
|
||||||
description: 'May run any executable resources in the organization',
|
description: 'May run any executable resources in the organization',
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Execute'
|
name: 'Execute',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
shallow(
|
shallow(
|
||||||
@@ -53,26 +50,28 @@ describe('<_AddResourceRole />', () => {
|
|||||||
{
|
{
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
id: 1
|
id: 1,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
wrapper.instance().handleRoleCheckboxClick({
|
wrapper.instance().handleRoleCheckboxClick({
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
expect(wrapper.state('selectedRoleRows')).toEqual([]);
|
expect(wrapper.state('selectedRoleRows')).toEqual([]);
|
||||||
wrapper.instance().handleRoleCheckboxClick({
|
wrapper.instance().handleRoleCheckboxClick({
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
id: 1
|
id: 1,
|
||||||
});
|
});
|
||||||
expect(wrapper.state('selectedRoleRows')).toEqual([{
|
expect(wrapper.state('selectedRoleRows')).toEqual([
|
||||||
description: 'Can manage all aspects of the organization',
|
{
|
||||||
name: 'Admin',
|
description: 'Can manage all aspects of the organization',
|
||||||
id: 1
|
name: 'Admin',
|
||||||
}]);
|
id: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
test('handleResourceCheckboxClick properly updates state', () => {
|
test('handleResourceCheckboxClick properly updates state', () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
@@ -87,32 +86,31 @@ describe('<_AddResourceRole />', () => {
|
|||||||
selectedResourceRows: [
|
selectedResourceRows: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'foobar'
|
username: 'foobar',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
wrapper.instance().handleResourceCheckboxClick({
|
wrapper.instance().handleResourceCheckboxClick({
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'foobar'
|
username: 'foobar',
|
||||||
});
|
});
|
||||||
expect(wrapper.state('selectedResourceRows')).toEqual([]);
|
expect(wrapper.state('selectedResourceRows')).toEqual([]);
|
||||||
wrapper.instance().handleResourceCheckboxClick({
|
wrapper.instance().handleResourceCheckboxClick({
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'foobar'
|
username: 'foobar',
|
||||||
});
|
});
|
||||||
expect(wrapper.state('selectedResourceRows')).toEqual([{
|
expect(wrapper.state('selectedResourceRows')).toEqual([
|
||||||
id: 1,
|
{
|
||||||
username: 'foobar'
|
id: 1,
|
||||||
}]);
|
username: 'foobar',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
test('clicking user/team cards updates state', () => {
|
test('clicking user/team cards updates state', () => {
|
||||||
const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect');
|
const spy = jest.spyOn(_AddResourceRole.prototype, 'handleResourceSelect');
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AddResourceRole
|
<AddResourceRole onClose={() => {}} onSave={() => {}} roles={roles} />,
|
||||||
onClose={() => {}}
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
onSave={() => {}}
|
|
||||||
roles={roles}
|
|
||||||
/>, { context: { network: { handleHttpError: () => {} } } }
|
|
||||||
).find('AddResourceRole');
|
).find('AddResourceRole');
|
||||||
const selectableCardWrapper = wrapper.find('SelectableCard');
|
const selectableCardWrapper = wrapper.find('SelectableCard');
|
||||||
expect(selectableCardWrapper.length).toBe(2);
|
expect(selectableCardWrapper.length).toBe(2);
|
||||||
@@ -137,16 +135,16 @@ describe('<_AddResourceRole />', () => {
|
|||||||
selectedResourceRows: [
|
selectedResourceRows: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'foobar'
|
username: 'foobar',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
selectedRoleRows: [
|
selectedRoleRows: [
|
||||||
{
|
{
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Admin'
|
name: 'Admin',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
wrapper.instance().handleResourceSelect('users');
|
wrapper.instance().handleResourceSelect('users');
|
||||||
expect(wrapper.state()).toEqual({
|
expect(wrapper.state()).toEqual({
|
||||||
@@ -160,38 +158,35 @@ describe('<_AddResourceRole />', () => {
|
|||||||
selectedResource: 'teams',
|
selectedResource: 'teams',
|
||||||
selectedResourceRows: [],
|
selectedResourceRows: [],
|
||||||
selectedRoleRows: [],
|
selectedRoleRows: [],
|
||||||
currentStepId: 1
|
currentStepId: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
|
test('handleWizardSave makes correct api calls, calls onSave when done', async () => {
|
||||||
const handleSave = jest.fn();
|
const handleSave = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AddResourceRole
|
<AddResourceRole onClose={() => {}} onSave={handleSave} roles={roles} />,
|
||||||
onClose={() => {}}
|
{ context: { network: { handleHttpError: () => {} } } }
|
||||||
onSave={handleSave}
|
|
||||||
roles={roles}
|
|
||||||
/>, { context: { network: { handleHttpError: () => {} } } }
|
|
||||||
).find('AddResourceRole');
|
).find('AddResourceRole');
|
||||||
wrapper.setState({
|
wrapper.setState({
|
||||||
selectedResource: 'users',
|
selectedResource: 'users',
|
||||||
selectedResourceRows: [
|
selectedResourceRows: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
username: 'foobar'
|
username: 'foobar',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
selectedRoleRows: [
|
selectedRoleRows: [
|
||||||
{
|
{
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Admin'
|
name: 'Admin',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'May run any executable resources in the organization',
|
description: 'May run any executable resources in the organization',
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Execute'
|
name: 'Execute',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
await wrapper.instance().handleWizardSave();
|
await wrapper.instance().handleWizardSave();
|
||||||
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
|
expect(UsersAPI.associateRole).toHaveBeenCalledTimes(2);
|
||||||
@@ -201,21 +196,21 @@ describe('<_AddResourceRole />', () => {
|
|||||||
selectedResourceRows: [
|
selectedResourceRows: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foobar'
|
name: 'foobar',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
selectedRoleRows: [
|
selectedRoleRows: [
|
||||||
{
|
{
|
||||||
description: 'Can manage all aspects of the organization',
|
description: 'Can manage all aspects of the organization',
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Admin'
|
name: 'Admin',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'May run any executable resources in the organization',
|
description: 'May run any executable resources in the organization',
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Execute'
|
name: 'Execute',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
await wrapper.instance().handleWizardSave();
|
await wrapper.instance().handleWizardSave();
|
||||||
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);
|
expect(TeamsAPI.associateRole).toHaveBeenCalledTimes(2);
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { Checkbox } from '@patternfly/react-core';
|
||||||
Checkbox
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
class CheckboxCard extends Component {
|
class CheckboxCard extends Component {
|
||||||
render () {
|
render() {
|
||||||
const { name, description, isSelected, onSelect, itemId } = this.props;
|
const { name, description, isSelected, onSelect, itemId } = this.props;
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
display: 'flex',
|
style={{
|
||||||
border: '1px solid var(--pf-global--BorderColor--200)',
|
display: 'flex',
|
||||||
borderRadius: 'var(--pf-global--BorderRadius--sm)',
|
border: '1px solid var(--pf-global--BorderColor--200)',
|
||||||
padding: '10px'
|
borderRadius: 'var(--pf-global--BorderRadius--sm)',
|
||||||
}}
|
padding: '10px',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={onSelect}
|
onChange={onSelect}
|
||||||
aria-label={name}
|
aria-label={name}
|
||||||
id={`checkbox-card-${itemId}`}
|
id={`checkbox-card-${itemId}`}
|
||||||
label={(
|
label={
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ fontWeight: 'bold' }}>{name}</div>
|
<div style={{ fontWeight: 'bold' }}>{name}</div>
|
||||||
<div>{description}</div>
|
<div>{description}</div>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
}
|
||||||
value={itemId}
|
value={itemId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,13 +37,13 @@ CheckboxCard.propTypes = {
|
|||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
onSelect: PropTypes.func,
|
onSelect: PropTypes.func,
|
||||||
itemId: PropTypes.number.isRequired
|
itemId: PropTypes.number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
CheckboxCard.defaultProps = {
|
CheckboxCard.defaultProps = {
|
||||||
description: '',
|
description: '',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
onSelect: null
|
onSelect: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CheckboxCard;
|
export default CheckboxCard;
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import CheckboxCard from './CheckboxCard';
|
|||||||
describe('<CheckboxCard />', () => {
|
describe('<CheckboxCard />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(<CheckboxCard name="Foobar" itemId={5} />);
|
||||||
<CheckboxCard
|
|
||||||
name="Foobar"
|
|
||||||
itemId={5}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.length).toBe(1);
|
expect(wrapper.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SelectedList from '../SelectedList';
|
|||||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||||
|
|
||||||
class SelectResourceStep extends React.Component {
|
class SelectResourceStep extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -27,20 +27,23 @@ class SelectResourceStep extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
this.readResourceList();
|
this.readResourceList();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.readResourceList();
|
this.readResourceList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readResourceList () {
|
async readResourceList() {
|
||||||
const { onSearch, location } = this.props;
|
const { onSearch, location } = this.props;
|
||||||
const queryParams = parseNamespacedQueryString(this.qsConfig, location.search);
|
const queryParams = parseNamespacedQueryString(
|
||||||
|
this.qsConfig,
|
||||||
|
location.search
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
@@ -65,14 +68,8 @@ class SelectResourceStep extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { isInitialized, isLoading, count, error, resources } = this.state;
|
||||||
isInitialized,
|
|
||||||
isLoading,
|
|
||||||
count,
|
|
||||||
error,
|
|
||||||
resources,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
@@ -81,12 +78,12 @@ class SelectResourceStep extends React.Component {
|
|||||||
selectedLabel,
|
selectedLabel,
|
||||||
selectedResourceRows,
|
selectedResourceRows,
|
||||||
itemName,
|
itemName,
|
||||||
i18n
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isLoading && (<div>{i18n._(t`Loading...`)}</div>)}
|
{isLoading && <div>{i18n._(t`Loading...`)}</div>}
|
||||||
{isInitialized && (
|
{isInitialized && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{selectedResourceRows.length > 0 && (
|
{selectedResourceRows.length > 0 && (
|
||||||
@@ -113,14 +110,14 @@ class SelectResourceStep extends React.Component {
|
|||||||
onSelect={() => onRowClick(item)}
|
onSelect={() => onRowClick(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderToolbar={(props) => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar {...props} alignToolbarLeft />
|
<DataListToolbar {...props} alignToolbarLeft />
|
||||||
)}
|
)}
|
||||||
showPageSizeOptions={false}
|
showPageSizeOptions={false}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
{ error ? <div>error</div> : '' }
|
{error ? <div>error</div> : ''}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { sleep } from '../../../testUtils/testUtils';
|
|||||||
import SelectResourceStep from './SelectResourceStep';
|
import SelectResourceStep from './SelectResourceStep';
|
||||||
|
|
||||||
describe('<SelectResourceStep />', () => {
|
describe('<SelectResourceStep />', () => {
|
||||||
const columns = [
|
const columns = [{ name: 'Username', key: 'username', isSortable: true }];
|
||||||
{ name: 'Username', key: 'username', isSortable: true }
|
|
||||||
];
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
@@ -30,9 +28,9 @@ describe('<SelectResourceStep />', () => {
|
|||||||
count: 2,
|
count: 2,
|
||||||
results: [
|
results: [
|
||||||
{ id: 1, username: 'foo', url: 'item/1' },
|
{ id: 1, username: 'foo', url: 'item/1' },
|
||||||
{ id: 2, username: 'bar', url: 'item/2' }
|
{ id: 2, username: 'bar', url: 'item/2' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
<SelectResourceStep
|
<SelectResourceStep
|
||||||
@@ -46,25 +44,25 @@ describe('<SelectResourceStep />', () => {
|
|||||||
expect(handleSearch).toHaveBeenCalledWith({
|
expect(handleSearch).toHaveBeenCalledWith({
|
||||||
order_by: 'username',
|
order_by: 'username',
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 5
|
page_size: 5,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('readResourceList properly adds rows to state', async () => {
|
test('readResourceList properly adds rows to state', async () => {
|
||||||
const selectedResourceRows = [
|
const selectedResourceRows = [{ id: 1, username: 'foo', url: 'item/1' }];
|
||||||
{ id: 1, username: 'foo', url: 'item/1' }
|
|
||||||
];
|
|
||||||
const handleSearch = jest.fn().mockResolvedValue({
|
const handleSearch = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
count: 2,
|
count: 2,
|
||||||
results: [
|
results: [
|
||||||
{ id: 1, username: 'foo', url: 'item/1' },
|
{ id: 1, username: 'foo', url: 'item/1' },
|
||||||
{ id: 2, username: 'bar', url: 'item/2' }
|
{ id: 2, username: 'bar', url: 'item/2' },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
const history = createMemoryHistory({
|
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(
|
const wrapper = await mountWithContexts(
|
||||||
<SelectResourceStep
|
<SelectResourceStep
|
||||||
@@ -74,7 +72,10 @@ describe('<SelectResourceStep />', () => {
|
|||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
selectedResourceRows={selectedResourceRows}
|
selectedResourceRows={selectedResourceRows}
|
||||||
sortedColumnKey="username"
|
sortedColumnKey="username"
|
||||||
/>, { context: { router: { history, route: { location: history.location } } } }
|
/>,
|
||||||
|
{
|
||||||
|
context: { router: { history, route: { location: history.location } } },
|
||||||
|
}
|
||||||
).find('SelectResourceStep');
|
).find('SelectResourceStep');
|
||||||
await wrapper.instance().readResourceList();
|
await wrapper.instance().readResourceList();
|
||||||
expect(handleSearch).toHaveBeenCalledWith({
|
expect(handleSearch).toHaveBeenCalledWith({
|
||||||
@@ -84,7 +85,7 @@ describe('<SelectResourceStep />', () => {
|
|||||||
});
|
});
|
||||||
expect(wrapper.state('resources')).toEqual([
|
expect(wrapper.state('resources')).toEqual([
|
||||||
{ id: 1, username: 'foo', url: 'item/1' },
|
{ 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,
|
count: 2,
|
||||||
results: [
|
results: [
|
||||||
{ id: 1, username: 'foo', url: 'item/1' },
|
{ id: 1, username: 'foo', url: 'item/1' },
|
||||||
{ id: 2, username: 'bar', url: 'item/2' }
|
{ id: 2, username: 'bar', url: 'item/2' },
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<SelectResourceStep
|
<SelectResourceStep
|
||||||
@@ -111,7 +112,9 @@ describe('<SelectResourceStep />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
|
const checkboxListItemWrapper = wrapper.find('CheckboxListItem');
|
||||||
expect(checkboxListItemWrapper.length).toBe(2);
|
expect(checkboxListItemWrapper.length).toBe(2);
|
||||||
checkboxListItemWrapper.first().find('input[type="checkbox"]')
|
checkboxListItemWrapper
|
||||||
|
.first()
|
||||||
|
.find('input[type="checkbox"]')
|
||||||
.simulate('change', { target: { checked: true } });
|
.simulate('change', { target: { checked: true } });
|
||||||
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
|
expect(handleRowClick).toHaveBeenCalledWith(data.results[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import CheckboxCard from './CheckboxCard';
|
|||||||
import SelectedList from '../SelectedList';
|
import SelectedList from '../SelectedList';
|
||||||
|
|
||||||
class RolesStep extends React.Component {
|
class RolesStep extends React.Component {
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
onRolesClick,
|
onRolesClick,
|
||||||
roles,
|
roles,
|
||||||
@@ -16,7 +16,7 @@ class RolesStep extends React.Component {
|
|||||||
selectedListLabel,
|
selectedListLabel,
|
||||||
selectedResourceRows,
|
selectedResourceRows,
|
||||||
selectedRoleRows,
|
selectedRoleRows,
|
||||||
i18n
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,14 +32,21 @@ class RolesStep extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 => (
|
{Object.keys(roles).map(role => (
|
||||||
<CheckboxCard
|
<CheckboxCard
|
||||||
description={roles[role].description}
|
description={roles[role].description}
|
||||||
itemId={roles[role].id}
|
itemId={roles[role].id}
|
||||||
isSelected={
|
isSelected={selectedRoleRows.some(
|
||||||
selectedRoleRows.some(item => item.id === roles[role].id)
|
item => item.id === roles[role].id
|
||||||
}
|
)}
|
||||||
key={roles[role].id}
|
key={roles[role].id}
|
||||||
name={roles[role].name}
|
name={roles[role].name}
|
||||||
onSelect={() => onRolesClick(roles[role])}
|
onSelect={() => onRolesClick(roles[role])}
|
||||||
@@ -57,7 +64,7 @@ RolesStep.propTypes = {
|
|||||||
selectedListKey: PropTypes.string,
|
selectedListKey: PropTypes.string,
|
||||||
selectedListLabel: PropTypes.string,
|
selectedListLabel: PropTypes.string,
|
||||||
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
selectedResourceRows: PropTypes.arrayOf(PropTypes.object),
|
||||||
selectedRoleRows: PropTypes.arrayOf(PropTypes.object)
|
selectedRoleRows: PropTypes.arrayOf(PropTypes.object),
|
||||||
};
|
};
|
||||||
|
|
||||||
RolesStep.defaultProps = {
|
RolesStep.defaultProps = {
|
||||||
@@ -65,7 +72,7 @@ RolesStep.defaultProps = {
|
|||||||
selectedListKey: 'name',
|
selectedListKey: 'name',
|
||||||
selectedListLabel: null,
|
selectedListLabel: null,
|
||||||
selectedResourceRows: [],
|
selectedResourceRows: [],
|
||||||
selectedRoleRows: []
|
selectedRoleRows: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(RolesStep);
|
export default withI18n()(RolesStep);
|
||||||
|
|||||||
@@ -9,26 +9,26 @@ describe('<SelectRoleStep />', () => {
|
|||||||
project_admin_role: {
|
project_admin_role: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Project Admin',
|
name: 'Project Admin',
|
||||||
description: 'Can manage all projects of the organization'
|
description: 'Can manage all projects of the organization',
|
||||||
},
|
},
|
||||||
execute_role: {
|
execute_role: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Execute',
|
name: 'Execute',
|
||||||
description: 'May run any executable resources in the organization'
|
description: 'May run any executable resources in the organization',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
const selectedRoles = [
|
const selectedRoles = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Project Admin',
|
name: 'Project Admin',
|
||||||
description: 'Can manage all projects of the organization'
|
description: 'Can manage all projects of the organization',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
const selectedResourceRows = [
|
const selectedResourceRows = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(
|
||||||
@@ -57,7 +57,7 @@ describe('<SelectRoleStep />', () => {
|
|||||||
expect(onRolesClick).toBeCalledWith({
|
expect(onRolesClick).toBeCalledWith({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Project Admin',
|
name: 'Project Admin',
|
||||||
description: 'Can manage all projects of the organization'
|
description: 'Can manage all projects of the organization',
|
||||||
});
|
});
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ const SelectableItem = styled.div`
|
|||||||
border: 1px solid var(--pf-global--BorderColor--200);
|
border: 1px solid var(--pf-global--BorderColor--200);
|
||||||
border-radius: var(--pf-global--BorderRadius--sm);
|
border-radius: var(--pf-global--BorderRadius--sm);
|
||||||
border: 1px solid;
|
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;
|
margin-right: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -17,7 +20,8 @@ const SelectableItem = styled.div`
|
|||||||
const Indicator = styled.div`
|
const Indicator = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 5px;
|
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`
|
const Label = styled.div`
|
||||||
@@ -28,12 +32,8 @@ const Label = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class SelectableCard extends Component {
|
class SelectableCard extends Component {
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { label, onClick, isSelected } = this.props;
|
||||||
label,
|
|
||||||
onClick,
|
|
||||||
isSelected
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableItem
|
<SelectableItem
|
||||||
@@ -43,10 +43,7 @@ class SelectableCard extends Component {
|
|||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
>
|
>
|
||||||
|
<Indicator isSelected={isSelected} />
|
||||||
<Indicator
|
|
||||||
isSelected={isSelected}
|
|
||||||
/>
|
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
</SelectableItem>
|
</SelectableItem>
|
||||||
);
|
);
|
||||||
@@ -56,12 +53,12 @@ class SelectableCard extends Component {
|
|||||||
SelectableCard.propTypes = {
|
SelectableCard.propTypes = {
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
isSelected: PropTypes.bool
|
isSelected: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectableCard.defaultProps = {
|
SelectableCard.defaultProps = {
|
||||||
label: '',
|
label: '',
|
||||||
isSelected: false
|
isSelected: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SelectableCard;
|
export default SelectableCard;
|
||||||
|
|||||||
@@ -6,21 +6,12 @@ describe('<SelectableCard />', () => {
|
|||||||
let wrapper;
|
let wrapper;
|
||||||
const onClick = jest.fn();
|
const onClick = jest.fn();
|
||||||
test('initially renders without crashing when not selected', () => {
|
test('initially renders without crashing when not selected', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(<SelectableCard onClick={onClick} />);
|
||||||
<SelectableCard
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.length).toBe(1);
|
expect(wrapper.length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
test('initially renders without crashing when selected', () => {
|
test('initially renders without crashing when selected', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(<SelectableCard isSelected onClick={onClick} />);
|
||||||
<SelectableCard
|
|
||||||
isSelected
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.length).toBe(1);
|
expect(wrapper.length).toBe(1);
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Modal } from '@patternfly/react-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Modal
|
ExclamationTriangleIcon,
|
||||||
} from '@patternfly/react-core';
|
ExclamationCircleIcon,
|
||||||
|
InfoCircleIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
import { ExclamationTriangleIcon, ExclamationCircleIcon, InfoCircleIcon, CheckCircleIcon } from '@patternfly/react-icons';
|
const getIcon = variant => {
|
||||||
|
|
||||||
const getIcon = (variant) => {
|
|
||||||
let icon;
|
let icon;
|
||||||
if (variant === 'warning') {
|
if (variant === 'warning') {
|
||||||
icon = (<ExclamationTriangleIcon className="at-c-alertModal__icon" />);
|
icon = <ExclamationTriangleIcon className="at-c-alertModal__icon" />;
|
||||||
} else if (variant === 'danger') {
|
} else if (variant === 'danger') {
|
||||||
icon = (<ExclamationCircleIcon className="at-c-alertModal__icon" />);
|
icon = <ExclamationCircleIcon className="at-c-alertModal__icon" />;
|
||||||
} if (variant === 'info') {
|
}
|
||||||
icon = (<InfoCircleIcon className="at-c-alertModal__icon" />);
|
if (variant === 'info') {
|
||||||
} if (variant === 'success') {
|
icon = <InfoCircleIcon className="at-c-alertModal__icon" />;
|
||||||
icon = (<CheckCircleIcon className="at-c-alertModal__icon" />);
|
}
|
||||||
|
if (variant === 'success') {
|
||||||
|
icon = <CheckCircleIcon className="at-c-alertModal__icon" />;
|
||||||
}
|
}
|
||||||
return icon;
|
return icon;
|
||||||
};
|
};
|
||||||
@@ -24,7 +29,11 @@ export default ({ variant, children, ...props }) => {
|
|||||||
const { isOpen = null } = props;
|
const { isOpen = null } = props;
|
||||||
props.isOpen = Boolean(isOpen);
|
props.isOpen = Boolean(isOpen);
|
||||||
return (
|
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}
|
{children}
|
||||||
{getIcon(variant)}
|
{getIcon(variant)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import AlertModal from './AlertModal';
|
|||||||
|
|
||||||
describe('AlertModal', () => {
|
describe('AlertModal', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(<AlertModal title="Danger!" />);
|
||||||
<AlertModal title="Danger!" />
|
|
||||||
);
|
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,24 +2,21 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { FormSelect, FormSelectOption } from '@patternfly/react-core';
|
||||||
FormSelect,
|
|
||||||
FormSelectOption,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
class AnsibleSelect extends React.Component {
|
class AnsibleSelect extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onSelectChange = this.onSelectChange.bind(this);
|
this.onSelectChange = this.onSelectChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectChange (val, event) {
|
onSelectChange(val, event) {
|
||||||
const { onChange, name } = this.props;
|
const { onChange, name } = this.props;
|
||||||
event.target.name = name;
|
event.target.name = name;
|
||||||
onChange(event, val);
|
onChange(event, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { value, data, i18n } = this.props;
|
const { value, data, i18n } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +25,7 @@ class AnsibleSelect extends React.Component {
|
|||||||
onChange={this.onSelectChange}
|
onChange={this.onSelectChange}
|
||||||
aria-label={i18n._(t`Select Input`)}
|
aria-label={i18n._(t`Select Input`)}
|
||||||
>
|
>
|
||||||
{data.map((datum) => (
|
{data.map(datum => (
|
||||||
<FormSelectOption
|
<FormSelectOption
|
||||||
key={datum.key}
|
key={datum.key}
|
||||||
value={datum.value}
|
value={datum.value}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ const mockData = [
|
|||||||
{
|
{
|
||||||
key: 'baz',
|
key: 'baz',
|
||||||
label: 'Baz',
|
label: 'Baz',
|
||||||
value: '/venv/baz/'
|
value: '/venv/baz/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'default',
|
key: 'default',
|
||||||
label: 'Default',
|
label: 'Default',
|
||||||
value: '/venv/ansible/'
|
value: '/venv/ansible/',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('<AnsibleSelect />', () => {
|
describe('<AnsibleSelect />', () => {
|
||||||
@@ -21,7 +21,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => { }}
|
onChange={() => {}}
|
||||||
data={mockData}
|
data={mockData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => { }}
|
onChange={() => {}}
|
||||||
data={mockData}
|
data={mockData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -47,7 +47,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => { }}
|
onChange={() => {}}
|
||||||
data={mockData}
|
data={mockData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
|
||||||
import {
|
import { BackgroundImage, BackgroundImageSrc } from '@patternfly/react-core';
|
||||||
BackgroundImage,
|
|
||||||
BackgroundImageSrc,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import bgFilter from '@patternfly/patternfly/assets/images/background-filter.svg';
|
import bgFilter from '@patternfly/patternfly/assets/images/background-filter.svg';
|
||||||
|
|
||||||
const backgroundImageConfig = {
|
const backgroundImageConfig = {
|
||||||
@@ -18,6 +15,6 @@ const backgroundImageConfig = {
|
|||||||
export default ({ children }) => (
|
export default ({ children }) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<BackgroundImage src={backgroundImageConfig} />
|
<BackgroundImage src={backgroundImageConfig} />
|
||||||
{ children }
|
{children}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import Background from './Background';
|
|||||||
|
|
||||||
describe('Background', () => {
|
describe('Background', () => {
|
||||||
test('renders the expected content', () => {
|
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).toHaveLength(1);
|
||||||
expect(wrapper.find('BackgroundImage')).toHaveLength(1);
|
expect(wrapper.find('BackgroundImage')).toHaveLength(1);
|
||||||
expect(wrapper.find('#test')).toHaveLength(1);
|
expect(wrapper.find('#test')).toHaveLength(1);
|
||||||
|
|||||||
@@ -5,57 +5,57 @@ import { t } from '@lingui/macro';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const ST0 = styled.g`
|
const ST0 = styled.g`
|
||||||
display:none;
|
display: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST1 = styled.path`
|
const ST1 = styled.path`
|
||||||
display:inline;
|
display: inline;
|
||||||
fill:#ED1C24;
|
fill: #ed1c24;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST2 = styled.path`
|
const ST2 = styled.path`
|
||||||
fill:#42210B;
|
fill: #42210b;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST3 = styled.path`
|
const ST3 = styled.path`
|
||||||
fill:#FFFFFF;
|
fill: #ffffff;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST4 = styled.path`
|
const ST4 = styled.path`
|
||||||
fill:#C69C6D;
|
fill: #c69c6d;
|
||||||
stroke:#8C6239;
|
stroke: #8c6239;
|
||||||
stroke-width:5;
|
stroke-width: 5;
|
||||||
stroke-miterlimit:10;
|
stroke-miterlimit: 10;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST5 = styled.path`
|
const ST5 = styled.path`
|
||||||
fill:#FFFFFF;
|
fill: #ffffff;
|
||||||
stroke:#42210B;
|
stroke: #42210b;
|
||||||
stroke-width:3;
|
stroke-width: 3;
|
||||||
stroke-miterlimit:10;
|
stroke-miterlimit: 10;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST6 = styled.ellipse`
|
const ST6 = styled.ellipse`
|
||||||
fill:#ED1C24;
|
fill: #ed1c24;
|
||||||
stroke:#8C6239;
|
stroke: #8c6239;
|
||||||
stroke-width:5;
|
stroke-width: 5;
|
||||||
stroke-miterlimit:10;
|
stroke-miterlimit: 10;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST7 = styled.path`
|
const ST7 = styled.path`
|
||||||
fill:#A67C52;
|
fill: #a67c52;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST8 = styled.path`
|
const ST8 = styled.path`
|
||||||
fill: #ED1C24;
|
fill: #ed1c24;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ST9 = styled.ellipse`
|
const ST9 = styled.ellipse`
|
||||||
fill:#42210B;
|
fill: #42210b;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class BrandLogo extends Component {
|
class BrandLogo extends Component {
|
||||||
render () {
|
render() {
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -195,24 +195,12 @@ class BrandLogo extends Component {
|
|||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<ST5
|
<ST5 d="M215.5,166.5l34-73c0,0,35,0,46,21c7.5,14.3,8,39,8,39L215.5,166.5z" />
|
||||||
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" />
|
||||||
<ST5
|
<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" />
|
||||||
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"
|
<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" />
|
||||||
<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>
|
||||||
<g>
|
<g>
|
||||||
<ST3
|
<ST3
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ const findChildren = () => {
|
|||||||
|
|
||||||
describe('<BrandLogo />', () => {
|
describe('<BrandLogo />', () => {
|
||||||
test('initially renders without crashing', () => {
|
test('initially renders without crashing', () => {
|
||||||
logoWrapper = mountWithContexts(
|
logoWrapper = mountWithContexts(<BrandLogo />);
|
||||||
<BrandLogo />
|
|
||||||
);
|
|
||||||
findChildren();
|
findChildren();
|
||||||
expect(logoWrapper.length).toBe(1);
|
expect(logoWrapper.length).toBe(1);
|
||||||
expect(brandLogoElem.length).toBe(1);
|
expect(brandLogoElem.length).toBe(1);
|
||||||
|
|||||||
@@ -5,13 +5,9 @@ import {
|
|||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbHeading as PFBreadcrumbHeading
|
BreadcrumbHeading as PFBreadcrumbHeading,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import { Link, Route, withRouter } from 'react-router-dom';
|
||||||
Link,
|
|
||||||
Route,
|
|
||||||
withRouter
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const PageSection = styled(PFPageSection)`
|
const PageSection = styled(PFPageSection)`
|
||||||
@@ -20,7 +16,7 @@ const PageSection = styled(PFPageSection)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const BreadcrumbHeading = styled(PFBreadcrumbHeading)`
|
const BreadcrumbHeading = styled(PFBreadcrumbHeading)`
|
||||||
--pf-c-breadcrumb__heading--FontSize: 20px;
|
--pf-c-breadcrumb__heading--FontSize: 20px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
`;
|
`;
|
||||||
@@ -29,13 +25,13 @@ const Breadcrumbs = ({ breadcrumbConfig }) => {
|
|||||||
const { light } = PageSectionVariants;
|
const { light } = PageSectionVariants;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection
|
<PageSection variant={light}>
|
||||||
variant={light}
|
|
||||||
>
|
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<Route
|
<Route
|
||||||
path="/:path"
|
path="/:path"
|
||||||
render={(props) => <Crumb breadcrumbConfig={breadcrumbConfig} {...props} />}
|
render={props => (
|
||||||
|
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
@@ -47,19 +43,13 @@ const Crumb = ({ breadcrumbConfig, match }) => {
|
|||||||
|
|
||||||
let crumbElement = (
|
let crumbElement = (
|
||||||
<BreadcrumbItem key={match.url}>
|
<BreadcrumbItem key={match.url}>
|
||||||
<Link to={match.url}>
|
<Link to={match.url}>{crumb}</Link>
|
||||||
{crumb}
|
|
||||||
</Link>
|
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (match.isExact) {
|
if (match.isExact) {
|
||||||
crumbElement = (
|
crumbElement = (
|
||||||
<BreadcrumbHeading
|
<BreadcrumbHeading key="breadcrumb-heading">{crumb}</BreadcrumbHeading>
|
||||||
key="breadcrumb-heading"
|
|
||||||
>
|
|
||||||
{crumb}
|
|
||||||
</BreadcrumbHeading>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +62,9 @@ const Crumb = ({ breadcrumbConfig, match }) => {
|
|||||||
{crumbElement}
|
{crumbElement}
|
||||||
<Route
|
<Route
|
||||||
path={`${match.url}/:path`}
|
path={`${match.url}/:path`}
|
||||||
render={(props) => <Crumb breadcrumbConfig={breadcrumbConfig} {...props} />}
|
render={props => (
|
||||||
|
<Crumb breadcrumbConfig={breadcrumbConfig} {...props} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('<Breadcrumb />', () => {
|
|||||||
'/foo': 'Foo',
|
'/foo': 'Foo',
|
||||||
'/foo/1': 'One',
|
'/foo/1': 'One',
|
||||||
'/foo/1/bar': 'Bar',
|
'/foo/1/bar': 'Bar',
|
||||||
'/foo/1/bar/fiz': 'Fiz'
|
'/foo/1/bar/fiz': 'Fiz',
|
||||||
};
|
};
|
||||||
|
|
||||||
const findChildren = () => {
|
const findChildren = () => {
|
||||||
@@ -25,9 +25,7 @@ describe('<Breadcrumb />', () => {
|
|||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
breadcrumbWrapper = mount(
|
breadcrumbWrapper = mount(
|
||||||
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
|
<MemoryRouter initialEntries={['/foo/1/bar']} initialIndex={0}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs breadcrumbConfig={config} />
|
||||||
breadcrumbConfig={config}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,13 +53,13 @@ describe('<Breadcrumb />', () => {
|
|||||||
routes.forEach(([location, crumbLength]) => {
|
routes.forEach(([location, crumbLength]) => {
|
||||||
breadcrumbWrapper = mount(
|
breadcrumbWrapper = mount(
|
||||||
<MemoryRouter initialEntries={[location]}>
|
<MemoryRouter initialEntries={[location]}>
|
||||||
<Breadcrumbs
|
<Breadcrumbs breadcrumbConfig={config} />
|
||||||
breadcrumbConfig={config}
|
|
||||||
/>
|
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(crumbLength);
|
expect(breadcrumbWrapper.find('BreadcrumbItem')).toHaveLength(
|
||||||
|
crumbLength
|
||||||
|
);
|
||||||
breadcrumbWrapper.unmount();
|
breadcrumbWrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,12 +21,8 @@ const Group = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function ButtonGroup ({ children }) {
|
function ButtonGroup({ children }) {
|
||||||
return (
|
return <Group>{children}</Group>;
|
||||||
<Group>
|
|
||||||
{children}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ButtonGroup;
|
export default ButtonGroup;
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import { t } from '@lingui/macro';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Link = styled(RRLink)`
|
const Link = styled(RRLink)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
color: var(--pf-c-button--m-plain--Color);
|
color: var(--pf-c-button--m-plain--Color);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function CardCloseButton ({ linkTo, i18n, i18nHash, ...props }) {
|
function CardCloseButton({ linkTo, i18n, i18nHash, ...props }) {
|
||||||
if (linkTo) {
|
if (linkTo) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -29,11 +29,7 @@ function CardCloseButton ({ linkTo, i18n, i18nHash, ...props }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button variant="plain" aria-label={i18n._(t`Close`)} {...props}>
|
||||||
variant="plain"
|
|
||||||
aria-label={i18n._(t`Close`)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<TimesIcon />
|
<TimesIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ import {
|
|||||||
|
|
||||||
import VerticalSeparator from '../VerticalSeparator';
|
import VerticalSeparator from '../VerticalSeparator';
|
||||||
|
|
||||||
const CheckboxListItem = ({
|
const CheckboxListItem = ({ itemId, name, isSelected, onSelect }) => (
|
||||||
itemId,
|
|
||||||
name,
|
|
||||||
isSelected,
|
|
||||||
onSelect,
|
|
||||||
}) => (
|
|
||||||
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
|
<DataListItem key={itemId} aria-labelledby={`check-action-item-${itemId}`}>
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
<DataListCheck
|
<DataListCheck
|
||||||
@@ -25,20 +20,21 @@ const CheckboxListItem = ({
|
|||||||
aria-labelledby={`check-action-item-${itemId}`}
|
aria-labelledby={`check-action-item-${itemId}`}
|
||||||
value={itemId}
|
value={itemId}
|
||||||
/>
|
/>
|
||||||
<DataListItemCells dataListCells={[
|
<DataListItemCells
|
||||||
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
|
dataListCells={[
|
||||||
<VerticalSeparator />
|
<DataListCell key="divider" className="pf-c-data-list__cell--divider">
|
||||||
</DataListCell>,
|
<VerticalSeparator />
|
||||||
<DataListCell key="name">
|
</DataListCell>,
|
||||||
<label
|
<DataListCell key="name">
|
||||||
id={`check-action-item-${itemId}`}
|
<label
|
||||||
htmlFor={`selected-${itemId}`}
|
id={`check-action-item-${itemId}`}
|
||||||
className="check-action-item"
|
htmlFor={`selected-${itemId}`}
|
||||||
>
|
className="check-action-item"
|
||||||
<b>{name}</b>
|
>
|
||||||
</label>
|
<b>{name}</b>
|
||||||
</DataListCell>
|
</label>
|
||||||
]}
|
</DataListCell>,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Chip } from '@patternfly/react-core';
|
import { Chip } from '@patternfly/react-core';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -12,7 +11,9 @@ export default styled(Chip)`
|
|||||||
padding: 3px 8px;
|
padding: 3px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => (props.isOverflowChip && `
|
${props =>
|
||||||
|
props.isOverflowChip &&
|
||||||
|
`
|
||||||
padding: 0;
|
padding: 0;
|
||||||
`)}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ const ChipGroup = ({ children, className, showOverflowAfter, ...props }) => {
|
|||||||
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
|
const [isExpanded, setIsExpanded] = useState(!showOverflowAfter);
|
||||||
const toggleIsOpen = () => setIsExpanded(!isExpanded);
|
const toggleIsOpen = () => setIsExpanded(!isExpanded);
|
||||||
|
|
||||||
const mappedChildren = React.Children.map(children, c => (
|
const mappedChildren = React.Children.map(children, c =>
|
||||||
React.cloneElement(c, { component: 'li' })
|
React.cloneElement(c, { component: 'li' })
|
||||||
));
|
);
|
||||||
const showOverflowToggle = showOverflowAfter && children.length > showOverflowAfter;
|
const showOverflowToggle =
|
||||||
|
showOverflowAfter && children.length > showOverflowAfter;
|
||||||
const numToShow = isExpanded
|
const numToShow = isExpanded
|
||||||
? children.length
|
? children.length
|
||||||
: Math.min(showOverflowAfter, children.length);
|
: Math.min(showOverflowAfter, children.length);
|
||||||
|
|||||||
@@ -57,7 +57,12 @@ describe('<ChipGroup />', () => {
|
|||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find(Chip)).toHaveLength(8);
|
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(() => {
|
act(() => {
|
||||||
const toggle2 = wrapper.find(Chip).at(7);
|
const toggle2 = wrapper.find(Chip).at(7);
|
||||||
expect(toggle2.prop('isOverflowChip')).toBe(true);
|
expect(toggle2.prop('isOverflowChip')).toBe(true);
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const CodeMirror = styled(ReactCodeMirror)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > .CodeMirror {
|
& > .CodeMirror {
|
||||||
height: ${props => (props.rows * LINE_HEIGHT + PADDING)}px;
|
height: ${props => props.rows * LINE_HEIGHT + PADDING}px;
|
||||||
font-family: var(--pf-global--FontFamily--monospace);
|
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--PaddingRight: var(--pf-c-form-control--invalid--PaddingRight);
|
||||||
--pf-c-form-control--BorderBottomColor: var(--pf-c-form-control--invalid--BorderBottomColor);
|
--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 (
|
return (
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
className="pf-c-form-control"
|
className="pf-c-form-control"
|
||||||
@@ -43,7 +45,7 @@ function CodeMirrorInput ({ value, onChange, mode, readOnly, hasErrors, rows })
|
|||||||
options={{
|
options={{
|
||||||
smartIndent: false,
|
smartIndent: false,
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
readOnly
|
readOnly,
|
||||||
}}
|
}}
|
||||||
rows={rows}
|
rows={rows}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ describe('CodeMirrorInput', () => {
|
|||||||
it('should trigger onChange prop', () => {
|
it('should trigger onChange prop', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<CodeMirrorInput
|
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" />
|
||||||
value="---\n"
|
|
||||||
onChange={onChange}
|
|
||||||
mode="yaml"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
const codemirror = wrapper.find('Controlled');
|
const codemirror = wrapper.find('Controlled');
|
||||||
expect(codemirror.prop('mode')).toEqual('yaml');
|
expect(codemirror.prop('mode')).toEqual('yaml');
|
||||||
@@ -26,12 +22,7 @@ describe('CodeMirrorInput', () => {
|
|||||||
it('should render in read only mode', () => {
|
it('should render in read only mode', () => {
|
||||||
const onChange = jest.fn();
|
const onChange = jest.fn();
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<CodeMirrorInput
|
<CodeMirrorInput value="---\n" onChange={onChange} mode="yaml" readOnly />
|
||||||
value="---\n"
|
|
||||||
onChange={onChange}
|
|
||||||
mode="yaml"
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
const codemirror = wrapper.find('Controlled');
|
const codemirror = wrapper.find('Controlled');
|
||||||
expect(codemirror.prop('options').readOnly).toEqual(true);
|
expect(codemirror.prop('options').readOnly).toEqual(true);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const SmallButton = styled(Button)`
|
|||||||
font-size: var(--pf-global--FontSize--xs);
|
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);
|
const [mode, setMode] = useState(YAML_MODE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,13 +25,17 @@ function VariablesField ({ id, name, label, readOnly }) {
|
|||||||
<div className="pf-c-form__group">
|
<div className="pf-c-form__group">
|
||||||
<Split gutter="sm">
|
<Split gutter="sm">
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<label htmlFor={id} className="pf-c-form__label">{label}</label>
|
<label htmlFor={id} className="pf-c-form__label">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
</SplitItem>
|
</SplitItem>
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<SmallButton
|
<SmallButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mode === YAML_MODE) { return; }
|
if (mode === YAML_MODE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
form.setFieldValue(name, jsonToYaml(field.value));
|
form.setFieldValue(name, jsonToYaml(field.value));
|
||||||
setMode(YAML_MODE);
|
setMode(YAML_MODE);
|
||||||
@@ -45,7 +49,9 @@ function VariablesField ({ id, name, label, readOnly }) {
|
|||||||
</SmallButton>
|
</SmallButton>
|
||||||
<SmallButton
|
<SmallButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (mode === JSON_MODE) { return; }
|
if (mode === JSON_MODE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
form.setFieldValue(name, yamlToJson(field.value));
|
form.setFieldValue(name, yamlToJson(field.value));
|
||||||
setMode(JSON_MODE);
|
setMode(JSON_MODE);
|
||||||
@@ -64,7 +70,7 @@ function VariablesField ({ id, name, label, readOnly }) {
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(value) => {
|
onChange={value => {
|
||||||
form.setFieldValue(name, value);
|
form.setFieldValue(name, value);
|
||||||
}}
|
}}
|
||||||
hasErrors={!!form.errors[field.name]}
|
hasErrors={!!form.errors[field.name]}
|
||||||
@@ -76,7 +82,7 @@ function VariablesField ({ id, name, label, readOnly }) {
|
|||||||
>
|
>
|
||||||
{form.errors[field.name]}
|
{form.errors[field.name]}
|
||||||
</div>
|
</div>
|
||||||
) : null }
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ describe('VariablesField', () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{ variables: value }}
|
initialValues={{ variables: value }}
|
||||||
render={() => (
|
render={() => (
|
||||||
<VariablesField
|
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||||
id="the-field"
|
|
||||||
name="variables"
|
|
||||||
label="Variables"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -33,11 +29,7 @@ describe('VariablesField', () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{ variables: value }}
|
initialValues={{ variables: value }}
|
||||||
render={() => (
|
render={() => (
|
||||||
<VariablesField
|
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||||
id="the-field"
|
|
||||||
name="variables"
|
|
||||||
label="Variables"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -63,15 +55,14 @@ describe('VariablesField', () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{ variables: value }}
|
initialValues={{ variables: value }}
|
||||||
render={() => (
|
render={() => (
|
||||||
<VariablesField
|
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||||
id="the-field"
|
|
||||||
name="variables"
|
|
||||||
label="Variables"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Button').at(1).simulate('click');
|
wrapper
|
||||||
|
.find('Button')
|
||||||
|
.at(1)
|
||||||
|
.simulate('click');
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
const field = wrapper.find('CodeMirrorInput');
|
const field = wrapper.find('CodeMirrorInput');
|
||||||
@@ -86,14 +77,12 @@ describe('VariablesField', () => {
|
|||||||
<Formik
|
<Formik
|
||||||
initialValues={{ variables: value }}
|
initialValues={{ variables: value }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
render={(formik) => (
|
render={formik => (
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
<VariablesField
|
<VariablesField id="the-field" name="variables" label="Variables" />
|
||||||
id="the-field"
|
<button type="submit" id="submit">
|
||||||
name="variables"
|
Submit
|
||||||
label="Variables"
|
</button>
|
||||||
/>
|
|
||||||
<button type="submit" id="submit">Submit</button>
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -105,7 +94,7 @@ describe('VariablesField', () => {
|
|||||||
|
|
||||||
expect(handleSubmit).toHaveBeenCalled();
|
expect(handleSubmit).toHaveBeenCalled();
|
||||||
expect(handleSubmit.mock.calls[0][0]).toEqual({
|
expect(handleSubmit.mock.calls[0][0]).toEqual({
|
||||||
variables: '---\nnewval: changed'
|
variables: '---\nnewval: changed',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,19 +5,15 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateBody
|
EmptyStateBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { CubesIcon } from '@patternfly/react-icons';
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
const ContentEmpty = ({ i18n, title = '', message = '' }) => (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<EmptyStateIcon icon={CubesIcon} />
|
<EmptyStateIcon icon={CubesIcon} />
|
||||||
<Title size="lg">
|
<Title size="lg">{title || i18n._(t`No items found.`)}</Title>
|
||||||
{title || i18n._(t`No items found.`)}
|
<EmptyStateBody>{message}</EmptyStateBody>
|
||||||
</Title>
|
|
||||||
<EmptyStateBody>
|
|
||||||
{message}
|
|
||||||
</EmptyStateBody>
|
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
EmptyState as PFEmptyState,
|
EmptyState as PFEmptyState,
|
||||||
EmptyStateIcon,
|
EmptyStateIcon,
|
||||||
EmptyStateBody
|
EmptyStateBody,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
@@ -17,20 +17,18 @@ const EmptyState = styled(PFEmptyState)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class ContentError extends React.Component {
|
class ContentError extends React.Component {
|
||||||
render () {
|
render() {
|
||||||
const { error, i18n } = this.props;
|
const { error, i18n } = this.props;
|
||||||
return (
|
return (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
<EmptyStateIcon icon={ExclamationTriangleIcon} />
|
||||||
<Title size="lg">
|
<Title size="lg">{i18n._(t`Something went wrong...`)}</Title>
|
||||||
{i18n._(t`Something went wrong...`)}
|
|
||||||
</Title>
|
|
||||||
<EmptyStateBody>
|
<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>
|
</EmptyStateBody>
|
||||||
{error && (
|
{error && <ErrorDetail error={error} />}
|
||||||
<ErrorDetail error={error} />
|
|
||||||
)}
|
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,20 @@ import ContentError from './ContentError';
|
|||||||
|
|
||||||
describe('ContentError', () => {
|
describe('ContentError', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(<ContentError error={new Error({
|
const wrapper = mountWithContexts(
|
||||||
response: {
|
<ContentError
|
||||||
config: {
|
error={
|
||||||
method: 'post'
|
new Error({
|
||||||
},
|
response: {
|
||||||
data: 'An error occurred',
|
config: {
|
||||||
}
|
method: 'post',
|
||||||
})}
|
},
|
||||||
/>);
|
data: 'An error occurred',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import {
|
import { EmptyState, EmptyStateBody } from '@patternfly/react-core';
|
||||||
EmptyState,
|
|
||||||
EmptyStateBody
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
// TODO: Better loading state - skeleton lines / spinner, etc.
|
// TODO: Better loading state - skeleton lines / spinner, etc.
|
||||||
const ContentLoading = ({ i18n }) => (
|
const ContentLoading = ({ i18n }) => (
|
||||||
<EmptyState>
|
<EmptyState>
|
||||||
<EmptyStateBody>
|
<EmptyStateBody>{i18n._(t`Loading...`)}</EmptyStateBody>
|
||||||
{i18n._(t`Loading...`)}
|
|
||||||
</EmptyStateBody>
|
|
||||||
</EmptyState>
|
</EmptyState>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ const AWXToolbar = styled.div`
|
|||||||
--pf-global--target-size--MinWidth: 0;
|
--pf-global--target-size--MinWidth: 0;
|
||||||
--pf-global--FontSize--md: 14px;
|
--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);
|
background-color: var(--awx-toolbar--BackgroundColor);
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
@@ -70,13 +71,13 @@ const AdditionalControlsWrapper = styled.div`
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& > :not(:first-child) {
|
& > :not(:first-child) {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class DataListToolbar extends React.Component {
|
class DataListToolbar extends React.Component {
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
showSelectAll,
|
showSelectAll,
|
||||||
@@ -91,15 +92,15 @@ class DataListToolbar extends React.Component {
|
|||||||
sortOrder,
|
sortOrder,
|
||||||
sortedColumnKey,
|
sortedColumnKey,
|
||||||
additionalControls,
|
additionalControls,
|
||||||
i18n
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showExpandCollapse = (onCompact && onExpand);
|
const showExpandCollapse = onCompact && onExpand;
|
||||||
return (
|
return (
|
||||||
<AWXToolbar>
|
<AWXToolbar>
|
||||||
<Toolbar marginleft={noLeftMargin ? 1 : 0}>
|
<Toolbar marginleft={noLeftMargin ? 1 : 0}>
|
||||||
<ColumnLeft>
|
<ColumnLeft>
|
||||||
{ showSelectAll && (
|
{showSelectAll && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -140,9 +141,7 @@ class DataListToolbar extends React.Component {
|
|||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
/>
|
/>
|
||||||
</ToolbarGroup>
|
</ToolbarGroup>
|
||||||
{ additionalControls && (
|
{additionalControls && <VerticalSeparator />}
|
||||||
<VerticalSeparator />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
<AdditionalControlsWrapper>
|
<AdditionalControlsWrapper>
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe('<DataListToolbar />', () => {
|
|||||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||||
{ name: 'Baz', key: 'baz' }
|
{ name: 'Baz', key: 'baz' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
@@ -97,12 +97,16 @@ describe('<DataListToolbar />', () => {
|
|||||||
);
|
);
|
||||||
toolbar.update();
|
toolbar.update();
|
||||||
|
|
||||||
const sortDropdownToggleDescending = toolbar.find(sortDropdownToggleSelector);
|
const sortDropdownToggleDescending = toolbar.find(
|
||||||
|
sortDropdownToggleSelector
|
||||||
|
);
|
||||||
expect(sortDropdownToggleDescending.length).toBe(1);
|
expect(sortDropdownToggleDescending.length).toBe(1);
|
||||||
sortDropdownToggleDescending.simulate('click');
|
sortDropdownToggleDescending.simulate('click');
|
||||||
toolbar.update();
|
toolbar.update();
|
||||||
|
|
||||||
const sortDropdownItemsDescending = toolbar.find(dropdownMenuItems).children();
|
const sortDropdownItemsDescending = toolbar
|
||||||
|
.find(dropdownMenuItems)
|
||||||
|
.children();
|
||||||
expect(sortDropdownItemsDescending.length).toBe(2);
|
expect(sortDropdownItemsDescending.length).toBe(2);
|
||||||
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
|
sortDropdownToggleDescending.simulate('click'); // toggle close the sort dropdown
|
||||||
|
|
||||||
@@ -128,8 +132,12 @@ describe('<DataListToolbar />', () => {
|
|||||||
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
||||||
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
||||||
|
|
||||||
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
|
const numericColumns = [
|
||||||
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
|
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
|
||||||
|
];
|
||||||
|
const alphaColumns = [
|
||||||
|
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
|
||||||
|
];
|
||||||
|
|
||||||
toolbar = mountWithContexts(
|
toolbar = mountWithContexts(
|
||||||
<DataListToolbar
|
<DataListToolbar
|
||||||
@@ -188,7 +196,11 @@ describe('<DataListToolbar />', () => {
|
|||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
onSort={onSort}
|
onSort={onSort}
|
||||||
onSelectAll={onSelectAll}
|
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} />
|
<TextListItem {...props} />
|
||||||
))`
|
))`
|
||||||
font-weight: var(--pf-global--FontWeight--bold);
|
font-weight: var(--pf-global--FontWeight--bold);
|
||||||
${props => props.fullWidth && `
|
${props =>
|
||||||
|
props.fullWidth &&
|
||||||
|
`
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
@@ -16,7 +18,9 @@ const DetailValue = styled(({ fullWidth, ...props }) => (
|
|||||||
<TextListItem {...props} />
|
<TextListItem {...props} />
|
||||||
))`
|
))`
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
${props => props.fullWidth && `
|
${props =>
|
||||||
|
props.fullWidth &&
|
||||||
|
`
|
||||||
grid-column: 2 / -1;
|
grid-column: 2 / -1;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
@@ -25,16 +29,10 @@ const Detail = ({ label, value, fullWidth }) => {
|
|||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<DetailName
|
<DetailName component={TextListItemVariants.dt} fullWidth={fullWidth}>
|
||||||
component={TextListItemVariants.dt}
|
|
||||||
fullWidth={fullWidth}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</DetailName>
|
</DetailName>
|
||||||
<DetailValue
|
<DetailValue component={TextListItemVariants.dd} fullWidth={fullWidth}>
|
||||||
component={TextListItemVariants.dd}
|
|
||||||
fullWidth={fullWidth}
|
|
||||||
>
|
|
||||||
{value}
|
{value}
|
||||||
</DetailValue>
|
</DetailValue>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import Detail from './Detail';
|
|||||||
|
|
||||||
describe('Detail', () => {
|
describe('Detail', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(<Detail label="foo" />);
|
||||||
<Detail label="foo" />
|
|
||||||
);
|
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ const DetailList = ({ children, stacked, ...props }) => (
|
|||||||
export default styled(DetailList)`
|
export default styled(DetailList)`
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
${props => (props.stacked ? (`
|
${props =>
|
||||||
|
props.stacked
|
||||||
|
? `
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
`) : (`
|
`
|
||||||
|
: `
|
||||||
--column-count: 1;
|
--column-count: 1;
|
||||||
grid-template-columns: repeat(var(--column-count), auto minmax(10em, 1fr));
|
grid-template-columns: repeat(var(--column-count), auto minmax(10em, 1fr));
|
||||||
|
|
||||||
@@ -24,5 +27,5 @@ export default styled(DetailList)`
|
|||||||
@media (min-width: 1210px) {
|
@media (min-width: 1210px) {
|
||||||
--column-count: 3;
|
--column-count: 3;
|
||||||
}
|
}
|
||||||
`))}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { t } from '@lingui/macro';
|
|||||||
import {
|
import {
|
||||||
Card as PFCard,
|
Card as PFCard,
|
||||||
CardBody as PFCardBody,
|
CardBody as PFCardBody,
|
||||||
Expandable as PFExpandable
|
Expandable as PFExpandable,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
const Card = styled(PFCard)`
|
const Card = styled(PFCard)`
|
||||||
@@ -25,11 +25,11 @@ const Expandable = styled(PFExpandable)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class ErrorDetail extends Component {
|
class ErrorDetail extends Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isExpanded: false
|
isExpanded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleToggle = this.handleToggle.bind(this);
|
this.handleToggle = this.handleToggle.bind(this);
|
||||||
@@ -37,51 +37,48 @@ class ErrorDetail extends Component {
|
|||||||
this.renderStack = this.renderStack.bind(this);
|
this.renderStack = this.renderStack.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleToggle () {
|
handleToggle() {
|
||||||
const { isExpanded } = this.state;
|
const { isExpanded } = this.state;
|
||||||
this.setState({ isExpanded: !isExpanded });
|
this.setState({ isExpanded: !isExpanded });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNetworkError () {
|
renderNetworkError() {
|
||||||
const { error } = this.props;
|
const { error } = this.props;
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
|
|
||||||
const message = typeof response.data === 'string'
|
const message =
|
||||||
? response.data
|
typeof response.data === 'string' ? response.data : response.data.detail;
|
||||||
: response.data.detail;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
{response.config.method.toUpperCase()}
|
{response.config.method.toUpperCase()} {response.config.url}{' '}
|
||||||
{' '}
|
<strong>{response.status}</strong>
|
||||||
{response.config.url}
|
|
||||||
{' '}
|
|
||||||
<strong>
|
|
||||||
{response.status}
|
|
||||||
</strong>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<CardBody>{message}</CardBody>
|
<CardBody>{message}</CardBody>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStack () {
|
renderStack() {
|
||||||
const { error } = this.props;
|
const { error } = this.props;
|
||||||
return (<CardBody>{error.stack}</CardBody>);
|
return <CardBody>{error.stack}</CardBody>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { isExpanded } = this.state;
|
const { isExpanded } = this.state;
|
||||||
const { error, i18n } = this.props;
|
const { error, i18n } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Expandable toggleText={i18n._(t`Details`)} onToggle={this.handleToggle} isExpanded={isExpanded}>
|
<Expandable
|
||||||
|
toggleText={i18n._(t`Details`)}
|
||||||
|
onToggle={this.handleToggle}
|
||||||
|
isExpanded={isExpanded}
|
||||||
|
>
|
||||||
<Card>
|
<Card>
|
||||||
{Object.prototype.hasOwnProperty.call(error, 'response')
|
{Object.prototype.hasOwnProperty.call(error, 'response')
|
||||||
? this.renderNetworkError()
|
? this.renderNetworkError()
|
||||||
: this.renderStack()
|
: this.renderStack()}
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Expandable>
|
</Expandable>
|
||||||
);
|
);
|
||||||
@@ -89,7 +86,7 @@ class ErrorDetail extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ErrorDetail.propTypes = {
|
ErrorDetail.propTypes = {
|
||||||
error: PropTypes.instanceOf(Error).isRequired
|
error: PropTypes.instanceOf(Error).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(ErrorDetail);
|
export default withI18n()(ErrorDetail);
|
||||||
|
|||||||
@@ -5,15 +5,20 @@ import ErrorDetail from './ErrorDetail';
|
|||||||
|
|
||||||
describe('ErrorDetail', () => {
|
describe('ErrorDetail', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(<ErrorDetail error={new Error({
|
const wrapper = mountWithContexts(
|
||||||
response: {
|
<ErrorDetail
|
||||||
config: {
|
error={
|
||||||
method: 'post'
|
new Error({
|
||||||
},
|
response: {
|
||||||
data: 'An error occurred'
|
config: {
|
||||||
}
|
method: 'post',
|
||||||
})}
|
},
|
||||||
/>);
|
data: 'An error occurred',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,40 +4,34 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
Button as PFButton,
|
Button as PFButton,
|
||||||
ToolbarItem as PFToolbarItem
|
ToolbarItem as PFToolbarItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import { BarsIcon, EqualsIcon } from '@patternfly/react-icons';
|
||||||
BarsIcon,
|
|
||||||
EqualsIcon,
|
|
||||||
} from '@patternfly/react-icons';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const Button = styled(PFButton)`
|
const Button = styled(PFButton)`
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
${props => (props.isActive ? `
|
${props =>
|
||||||
|
props.isActive
|
||||||
|
? `
|
||||||
background-color: #007bba;
|
background-color: #007bba;
|
||||||
--pf-c-button--m-plain--active--Color: white;
|
--pf-c-button--m-plain--active--Color: white;
|
||||||
--pf-c-button--m-plain--focus--Color: white;`
|
--pf-c-button--m-plain--focus--Color: white;`
|
||||||
: null)};
|
: null};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ToolbarItem = styled(PFToolbarItem)`
|
const ToolbarItem = styled(PFToolbarItem)`
|
||||||
& :not(:last-child) {
|
& :not(:last-child) {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
class ExpandCollapse extends React.Component {
|
class ExpandCollapse extends React.Component {
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { isCompact, onCompact, onExpand, i18n } = this.props;
|
||||||
isCompact,
|
|
||||||
onCompact,
|
|
||||||
onExpand,
|
|
||||||
i18n
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -69,11 +63,11 @@ class ExpandCollapse extends React.Component {
|
|||||||
ExpandCollapse.propTypes = {
|
ExpandCollapse.propTypes = {
|
||||||
onCompact: PropTypes.func.isRequired,
|
onCompact: PropTypes.func.isRequired,
|
||||||
onExpand: PropTypes.func.isRequired,
|
onExpand: PropTypes.func.isRequired,
|
||||||
isCompact: PropTypes.bool
|
isCompact: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
ExpandCollapse.defaultProps = {
|
ExpandCollapse.defaultProps = {
|
||||||
isCompact: true
|
isCompact: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(ExpandCollapse);
|
export default withI18n()(ExpandCollapse);
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import { ActionGroup as PFActionGroup, Button } from '@patternfly/react-core';
|
||||||
ActionGroup as PFActionGroup,
|
|
||||||
Button
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const ActionGroup = styled(PFActionGroup)`
|
const ActionGroup = styled(PFActionGroup)`
|
||||||
@@ -27,8 +24,23 @@ const ActionGroup = styled(PFActionGroup)`
|
|||||||
|
|
||||||
const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => (
|
const FormActionGroup = ({ onSubmit, submitDisabled, onCancel, i18n }) => (
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<Button aria-label={i18n._(t`Save`)} variant="primary" type="submit" onClick={onSubmit} isDisabled={submitDisabled}>{i18n._(t`Save`)}</Button>
|
<Button
|
||||||
<Button aria-label={i18n._(t`Cancel`)} variant="secondary" type="button" onClick={onCancel}>{i18n._(t`Cancel`)}</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>
|
</ActionGroup>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import FormActionGroup from './FormActionGroup';
|
|||||||
describe('FormActionGroup', () => {
|
describe('FormActionGroup', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<FormActionGroup
|
<FormActionGroup onSubmit={() => {}} onCancel={() => {}} />
|
||||||
onSubmit={() => {}}
|
|
||||||
onCancel={() => {}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function FormField (props) {
|
function FormField(props) {
|
||||||
const { id, name, label, tooltip, validate, isRequired, ...rest } = props;
|
const { id, name, label, tooltip, validate, isRequired, ...rest } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +17,8 @@ function FormField (props) {
|
|||||||
name={name}
|
name={name}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
render={({ field, form }) => {
|
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 (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
@@ -28,13 +29,9 @@ function FormField (props) {
|
|||||||
label={label}
|
label={label}
|
||||||
>
|
>
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
<Tooltip
|
<Tooltip position="right" content={tooltip}>
|
||||||
position="right"
|
|
||||||
content={tooltip}
|
|
||||||
>
|
|
||||||
<QuestionCircleIcon />
|
<QuestionCircleIcon />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
<TextInput
|
<TextInput
|
||||||
id={id}
|
id={id}
|
||||||
@@ -67,7 +64,7 @@ FormField.defaultProps = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
validate: () => {},
|
validate: () => {},
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
tooltip: null
|
tooltip: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormField;
|
export default FormField;
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ const Row = styled.div`
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
`;
|
`;
|
||||||
export default function FormRow ({ children }) {
|
export default function FormRow({ children }) {
|
||||||
return (
|
return <Row>{children}</Row>;
|
||||||
<Row>
|
|
||||||
{children}
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ class LaunchButton extends React.Component {
|
|||||||
templateId: number.isRequired,
|
templateId: number.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
launchError: null,
|
launchError: null,
|
||||||
promptError: false
|
promptError: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleLaunch = this.handleLaunch.bind(this);
|
this.handleLaunch = this.handleLaunch.bind(this);
|
||||||
@@ -38,18 +38,20 @@ class LaunchButton extends React.Component {
|
|||||||
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLaunchErrorClose () {
|
handleLaunchErrorClose() {
|
||||||
this.setState({ launchError: null });
|
this.setState({ launchError: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePromptErrorClose () {
|
handlePromptErrorClose() {
|
||||||
this.setState({ promptError: false });
|
this.setState({ promptError: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLaunch () {
|
async handleLaunch() {
|
||||||
const { history, templateId } = this.props;
|
const { history, templateId } = this.props;
|
||||||
try {
|
try {
|
||||||
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(templateId);
|
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
||||||
|
templateId
|
||||||
|
);
|
||||||
if (launchConfig.can_start_without_user_input) {
|
if (launchConfig.can_start_without_user_input) {
|
||||||
const { data: job } = await JobTemplatesAPI.launch(templateId);
|
const { data: job } = await JobTemplatesAPI.launch(templateId);
|
||||||
history.push(`/jobs/${job.id}/details`);
|
history.push(`/jobs/${job.id}/details`);
|
||||||
@@ -61,18 +63,12 @@ class LaunchButton extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { launchError, promptError } = this.state;
|
||||||
launchError,
|
|
||||||
promptError
|
|
||||||
} = this.state;
|
|
||||||
const { i18n } = this.props;
|
const { i18n } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Tooltip
|
<Tooltip content={i18n._(t`Launch Job`)} position="top">
|
||||||
content={i18n._(t`Launch Job`)}
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<StyledLaunchButton
|
<StyledLaunchButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -98,7 +94,9 @@ class LaunchButton extends React.Component {
|
|||||||
title={i18n._(t`Attention!`)}
|
title={i18n._(t`Attention!`)}
|
||||||
onClose={this.handlePromptErrorClose}
|
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>
|
</AlertModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,30 +10,28 @@ jest.mock('@api');
|
|||||||
describe('LaunchButton', () => {
|
describe('LaunchButton', () => {
|
||||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
can_start_without_user_input: true
|
can_start_without_user_input: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
|
const wrapper = mountWithContexts(<LaunchButton templateId={1} />);
|
||||||
expect(wrapper).toHaveLength(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 = {
|
const history = {
|
||||||
push: jest.fn(),
|
push: jest.fn(),
|
||||||
};
|
};
|
||||||
JobTemplatesAPI.launch.mockResolvedValue({
|
JobTemplatesAPI.launch.mockResolvedValue({
|
||||||
data: {
|
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');
|
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
|
||||||
launchButton.simulate('click');
|
launchButton.simulate('click');
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
@@ -42,24 +40,34 @@ describe('LaunchButton', () => {
|
|||||||
expect(history.push).toHaveBeenCalledWith('/jobs/9000/details');
|
expect(history.push).toHaveBeenCalledWith('/jobs/9000/details');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
test('displays error modal after unsuccessful launch', async (done) => {
|
test('displays error modal after unsuccessful launch', async done => {
|
||||||
JobTemplatesAPI.launch.mockRejectedValue(new Error({
|
JobTemplatesAPI.launch.mockRejectedValue(
|
||||||
response: {
|
new Error({
|
||||||
config: {
|
response: {
|
||||||
method: 'post',
|
config: {
|
||||||
url: '/api/v2/job_templates/1/launch'
|
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 wrapper = mountWithContexts(<LaunchButton templateId={1} />);
|
||||||
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
|
const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
|
||||||
launchButton.simulate('click');
|
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');
|
const modalCloseButton = wrapper.find('ModalBoxCloseButton');
|
||||||
modalCloseButton.simulate('click');
|
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();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { ChipGroup, Chip } from '../Chip';
|
|||||||
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
|
||||||
|
|
||||||
class Lookup extends React.Component {
|
class Lookup extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -40,19 +40,22 @@ class Lookup extends React.Component {
|
|||||||
this.getData = this.getData.bind(this);
|
this.getData = this.getData.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { location } = this.props;
|
const { location } = this.props;
|
||||||
if (location !== prevProps.location) {
|
if (location !== prevProps.location) {
|
||||||
this.getData();
|
this.getData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData () {
|
async getData() {
|
||||||
const { getItems, location: { search } } = this.props;
|
const {
|
||||||
|
getItems,
|
||||||
|
location: { search },
|
||||||
|
} = this.props;
|
||||||
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
|
const queryParams = parseNamespacedQueryString(this.qsConfig, search);
|
||||||
|
|
||||||
this.setState({ error: false });
|
this.setState({ error: false });
|
||||||
@@ -62,26 +65,30 @@ class Lookup extends React.Component {
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
results,
|
results,
|
||||||
count
|
count,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ error: true });
|
this.setState({ error: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSelected (row) {
|
toggleSelected(row) {
|
||||||
const { name, onLookupSave } = this.props;
|
const { name, onLookupSave } = this.props;
|
||||||
const { lookupSelectedItems: updatedSelectedItems, isModalOpen } = this.state;
|
const {
|
||||||
|
lookupSelectedItems: updatedSelectedItems,
|
||||||
|
isModalOpen,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const selectedIndex = updatedSelectedItems
|
const selectedIndex = updatedSelectedItems.findIndex(
|
||||||
.findIndex(selectedRow => selectedRow.id === row.id);
|
selectedRow => selectedRow.id === row.id
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedIndex > -1) {
|
if (selectedIndex > -1) {
|
||||||
updatedSelectedItems.splice(selectedIndex, 1);
|
updatedSelectedItems.splice(selectedIndex, 1);
|
||||||
this.setState({ lookupSelectedItems: updatedSelectedItems });
|
this.setState({ lookupSelectedItems: updatedSelectedItems });
|
||||||
} else {
|
} else {
|
||||||
this.setState(prevState => ({
|
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 { isModalOpen } = this.state;
|
||||||
const { value } = this.props;
|
const { value } = this.props;
|
||||||
// Resets the selected items from parent state whenever modal is opened
|
// Resets the selected items from parent state whenever modal is opened
|
||||||
@@ -102,19 +109,19 @@ class Lookup extends React.Component {
|
|||||||
if (!isModalOpen) {
|
if (!isModalOpen) {
|
||||||
this.setState({ lookupSelectedItems: [...value] });
|
this.setState({ lookupSelectedItems: [...value] });
|
||||||
}
|
}
|
||||||
this.setState((prevState) => ({
|
this.setState(prevState => ({
|
||||||
isModalOpen: !prevState.isModalOpen,
|
isModalOpen: !prevState.isModalOpen,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
saveModal () {
|
saveModal() {
|
||||||
const { onLookupSave, name } = this.props;
|
const { onLookupSave, name } = this.props;
|
||||||
const { lookupSelectedItems } = this.state;
|
const { lookupSelectedItems } = this.state;
|
||||||
onLookupSave(lookupSelectedItems, name);
|
onLookupSave(lookupSelectedItems, name);
|
||||||
this.handleModalToggle();
|
this.handleModalToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
isModalOpen,
|
isModalOpen,
|
||||||
lookupSelectedItems,
|
lookupSelectedItems,
|
||||||
@@ -147,9 +154,7 @@ class Lookup extends React.Component {
|
|||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="pf-c-form-control">
|
<div className="pf-c-form-control">{chips}</div>
|
||||||
{chips}
|
|
||||||
</div>
|
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Modal
|
<Modal
|
||||||
className="awx-c-modal"
|
className="awx-c-modal"
|
||||||
@@ -157,8 +162,21 @@ class Lookup extends React.Component {
|
|||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={this.handleModalToggle}
|
onClose={this.handleModalToggle}
|
||||||
actions={[
|
actions={[
|
||||||
<Button key="save" variant="primary" onClick={this.saveModal} style={(results.length === 0) ? { display: 'none' } : {}}>{i18n._(t`Save`)}</Button>,
|
<Button
|
||||||
<Button key="cancel" variant="secondary" onClick={this.handleModalToggle}>{(results.length === 0) ? i18n._(t`Close`) : i18n._(t`Cancel`)}</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
|
<PaginatedDataList
|
||||||
@@ -176,7 +194,7 @@ class Lookup extends React.Component {
|
|||||||
onSelect={() => this.toggleSelected(item)}
|
onSelect={() => this.toggleSelected(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderToolbar={(props) => (
|
renderToolbar={props => (
|
||||||
<DataListToolbar {...props} alignToolbarLeft />
|
<DataListToolbar {...props} alignToolbarLeft />
|
||||||
)}
|
)}
|
||||||
showPageSizeOptions={false}
|
showPageSizeOptions={false}
|
||||||
@@ -189,7 +207,7 @@ class Lookup extends React.Component {
|
|||||||
onRemove={this.toggleSelected}
|
onRemove={this.toggleSelected}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{ error ? <div>error</div> : '' }
|
{error ? <div>error</div> : ''}
|
||||||
</Modal>
|
</Modal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
|||||||
import Lookup, { _Lookup } from './Lookup';
|
import Lookup, { _Lookup } from './Lookup';
|
||||||
|
|
||||||
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
|
let mockData = [{ name: 'foo', id: 1, isChecked: false }];
|
||||||
const mockColumns = [
|
const mockColumns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||||
{ name: 'Name', key: 'name', isSortable: true }
|
|
||||||
];
|
|
||||||
describe('<Lookup />', () => {
|
describe('<Lookup />', () => {
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders succesfully', () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
@@ -15,29 +13,33 @@ describe('<Lookup />', () => {
|
|||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
getItems={() => { }}
|
getItems={() => {}}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('API response is formatted properly', (done) => {
|
test('API response is formatted properly', done => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Lookup
|
<Lookup
|
||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
getItems={() => ({ data: { results: [{ name: 'test instance', id: 1 }] } })}
|
getItems={() => ({
|
||||||
|
data: { results: [{ name: 'test instance', id: 1 }] },
|
||||||
|
})}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
|
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
expect(wrapper.state().results).toEqual([{ id: 1, name: 'test instance' }]);
|
expect(wrapper.state().results).toEqual([
|
||||||
|
{ id: 1, name: 'test instance' },
|
||||||
|
]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -51,8 +53,8 @@ describe('<Lookup />', () => {
|
|||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockSelected}
|
value={mockSelected}
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
getItems={() => { }}
|
getItems={() => {}}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
@@ -62,20 +64,20 @@ describe('<Lookup />', () => {
|
|||||||
const searchItem = wrapper.find('button[aria-label="Search"]');
|
const searchItem = wrapper.find('button[aria-label="Search"]');
|
||||||
searchItem.first().simulate('click');
|
searchItem.first().simulate('click');
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
expect(wrapper.state('lookupSelectedItems')).toEqual([{
|
expect(wrapper.state('lookupSelectedItems')).toEqual([
|
||||||
id: 1,
|
{
|
||||||
name: 'foo'
|
id: 1,
|
||||||
}]);
|
name: 'foo',
|
||||||
|
},
|
||||||
|
]);
|
||||||
expect(wrapper.state('isModalOpen')).toEqual(true);
|
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 spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
|
||||||
const mockSelected = [{ name: 'foo', id: 1 }];
|
const mockSelected = [{ name: 'foo', id: 1 }];
|
||||||
const data = {
|
const data = {
|
||||||
results: [
|
results: [{ name: 'test instance', id: 1, url: '/foo' }],
|
||||||
{ name: 'test instance', id: 1, url: '/foo' }
|
|
||||||
],
|
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
@@ -84,7 +86,7 @@ describe('<Lookup />', () => {
|
|||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockSelected}
|
value={mockSelected}
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
getItems={() => ({ data })}
|
getItems={() => ({ data })}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
@@ -103,9 +105,7 @@ describe('<Lookup />', () => {
|
|||||||
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
|
const spy = jest.spyOn(_Lookup.prototype, 'toggleSelected');
|
||||||
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
|
mockData = [{ name: 'foo', id: 1 }, { name: 'bar', id: 2 }];
|
||||||
const data = {
|
const data = {
|
||||||
results: [
|
results: [{ name: 'test instance', id: 1, url: '/foo' }],
|
||||||
{ name: 'test instance', id: 1, url: '/foo' }
|
|
||||||
],
|
|
||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
@@ -114,7 +114,7 @@ describe('<Lookup />', () => {
|
|||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
getItems={() => ({ data })}
|
getItems={() => ({ data })}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
@@ -130,10 +130,10 @@ describe('<Lookup />', () => {
|
|||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Lookup
|
<Lookup
|
||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
selected={[]}
|
selected={[]}
|
||||||
getItems={() => { }}
|
getItems={() => {}}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
@@ -147,24 +147,26 @@ describe('<Lookup />', () => {
|
|||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Lookup
|
<Lookup
|
||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
getItems={() => { }}
|
getItems={() => {}}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
wrapper.instance().toggleSelected({
|
wrapper.instance().toggleSelected({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
});
|
});
|
||||||
expect(wrapper.state('lookupSelectedItems')).toEqual([{
|
expect(wrapper.state('lookupSelectedItems')).toEqual([
|
||||||
id: 1,
|
{
|
||||||
name: 'foo'
|
id: 1,
|
||||||
}]);
|
name: 'foo',
|
||||||
|
},
|
||||||
|
]);
|
||||||
wrapper.instance().toggleSelected({
|
wrapper.instance().toggleSelected({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
});
|
});
|
||||||
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
expect(wrapper.state('lookupSelectedItems')).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -178,23 +180,30 @@ describe('<Lookup />', () => {
|
|||||||
name="fooBar"
|
name="fooBar"
|
||||||
value={mockData}
|
value={mockData}
|
||||||
onLookupSave={onLookupSaveFn}
|
onLookupSave={onLookupSaveFn}
|
||||||
getItems={() => { }}
|
getItems={() => {}}
|
||||||
sortedColumnKey="name"
|
sortedColumnKey="name"
|
||||||
/>
|
/>
|
||||||
).find('Lookup');
|
).find('Lookup');
|
||||||
wrapper.instance().toggleSelected({
|
wrapper.instance().toggleSelected({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
});
|
});
|
||||||
expect(wrapper.state('lookupSelectedItems')).toEqual([{
|
expect(wrapper.state('lookupSelectedItems')).toEqual([
|
||||||
id: 1,
|
{
|
||||||
name: 'foo'
|
id: 1,
|
||||||
}]);
|
name: 'foo',
|
||||||
|
},
|
||||||
|
]);
|
||||||
wrapper.instance().saveModal();
|
wrapper.instance().saveModal();
|
||||||
expect(onLookupSaveFn).toHaveBeenCalledWith([{
|
expect(onLookupSaveFn).toHaveBeenCalledWith(
|
||||||
id: 1,
|
[
|
||||||
name: 'foo'
|
{
|
||||||
}], 'fooBar');
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'fooBar'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should re-fetch data when URL params change', async () => {
|
test('should re-fetch data when URL params change', async () => {
|
||||||
@@ -205,7 +214,7 @@ describe('<Lookup />', () => {
|
|||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<_Lookup
|
<_Lookup
|
||||||
lookupHeader="Foo Bar"
|
lookupHeader="Foo Bar"
|
||||||
onLookupSave={() => { }}
|
onLookupSave={() => {}}
|
||||||
value={mockData}
|
value={mockData}
|
||||||
selected={[]}
|
selected={[]}
|
||||||
columns={mockColumns}
|
columns={mockColumns}
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { withRouter } from 'react-router-dom';
|
||||||
withRouter
|
import { NavExpandable, NavItem } from '@patternfly/react-core';
|
||||||
} from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
NavExpandable,
|
|
||||||
NavItem,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
|
|
||||||
class NavExpandableGroup extends Component {
|
class NavExpandableGroup extends Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { routes } = this.props;
|
const { routes } = this.props;
|
||||||
// Extract a list of paths from the route params and store them for later. This creates
|
// 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);
|
this.isActivePath = this.isActivePath.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActiveGroup () {
|
isActiveGroup() {
|
||||||
return this.navItemPaths.some(this.isActivePath);
|
return this.navItemPaths.some(this.isActivePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActivePath (path) {
|
isActivePath(path) {
|
||||||
const { history } = this.props;
|
const { history } = this.props;
|
||||||
|
|
||||||
return history.location.pathname.startsWith(path);
|
return history.location.pathname.startsWith(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { groupId, groupTitle, routes } = this.props;
|
const { groupId, groupTitle, routes } = this.props;
|
||||||
const isActive = this.isActiveGroup();
|
const isActive = this.isActiveGroup();
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ describe('NavExpandableGroup', () => {
|
|||||||
/>
|
/>
|
||||||
</Nav>
|
</Nav>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('NavExpandableGroup').instance();
|
)
|
||||||
|
.find('NavExpandableGroup')
|
||||||
|
.instance();
|
||||||
|
|
||||||
expect(component.navItemPaths).toEqual(['/foo', '/bar', '/fiz']);
|
expect(component.navItemPaths).toEqual(['/foo', '/bar', '/fiz']);
|
||||||
expect(component.isActiveGroup()).toEqual(true);
|
expect(component.isActiveGroup()).toEqual(true);
|
||||||
@@ -54,7 +56,9 @@ describe('NavExpandableGroup', () => {
|
|||||||
/>
|
/>
|
||||||
</Nav>
|
</Nav>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
).find('NavExpandableGroup').instance();
|
)
|
||||||
|
.find('NavExpandableGroup')
|
||||||
|
.instance();
|
||||||
|
|
||||||
expect(component.isActivePath(path)).toEqual(expected);
|
expect(component.isActivePath(path)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const DataListCell = styled(PFDataListCell)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: ${props => (props.righthalf ? 'flex-start' : 'inherit')};
|
justify-content: ${props => (props.righthalf ? 'flex-start' : 'inherit')};
|
||||||
padding-bottom: ${props => (props.righthalf ? '16px' : '8px')};
|
padding-bottom: ${props => (props.righthalf ? '16px' : '8px')};
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
justify-content: ${props => (props.righthalf ? 'flex-end' : 'inherit')};
|
justify-content: ${props => (props.righthalf ? 'flex-end' : 'inherit')};
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
@@ -30,7 +30,7 @@ const Switch = styled(PFSwitch)`
|
|||||||
flex-wrap: no-wrap;
|
flex-wrap: no-wrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function NotificationListItem (props) {
|
function NotificationListItem(props) {
|
||||||
const {
|
const {
|
||||||
canToggleNotifications,
|
canToggleNotifications,
|
||||||
notification,
|
notification,
|
||||||
@@ -38,7 +38,7 @@ function NotificationListItem (props) {
|
|||||||
successTurnedOn,
|
successTurnedOn,
|
||||||
errorTurnedOn,
|
errorTurnedOn,
|
||||||
toggleNotification,
|
toggleNotification,
|
||||||
i18n
|
i18n,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,50 +47,50 @@ function NotificationListItem (props) {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
>
|
>
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
<DataListItemCells dataListCells={[
|
<DataListItemCells
|
||||||
<DataListCell key="name">
|
dataListCells={[
|
||||||
<Link
|
<DataListCell key="name">
|
||||||
to={{
|
<Link
|
||||||
pathname: detailUrl
|
to={{
|
||||||
}}
|
pathname: detailUrl,
|
||||||
css="margin-right: 1.5em;"
|
}}
|
||||||
>
|
css="margin-right: 1.5em;"
|
||||||
<b id={`items-list-item-${notification.id}`}>{notification.name}</b>
|
>
|
||||||
</Link>
|
<b id={`items-list-item-${notification.id}`}>
|
||||||
<Badge
|
{notification.name}
|
||||||
css="text-transform: capitalize;"
|
</b>
|
||||||
isRead
|
</Link>
|
||||||
>
|
<Badge css="text-transform: capitalize;" isRead>
|
||||||
{notification.notification_type}
|
{notification.notification_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell righthalf="true" key="toggles">
|
<DataListCell righthalf="true" key="toggles">
|
||||||
<Switch
|
<Switch
|
||||||
id={`notification-${notification.id}-success-toggle`}
|
id={`notification-${notification.id}-success-toggle`}
|
||||||
label={i18n._(t`Successful`)}
|
label={i18n._(t`Successful`)}
|
||||||
isChecked={successTurnedOn}
|
isChecked={successTurnedOn}
|
||||||
isDisabled={!canToggleNotifications}
|
isDisabled={!canToggleNotifications}
|
||||||
onChange={() => toggleNotification(
|
onChange={() =>
|
||||||
notification.id,
|
toggleNotification(
|
||||||
successTurnedOn,
|
notification.id,
|
||||||
'success'
|
successTurnedOn,
|
||||||
)}
|
'success'
|
||||||
aria-label={i18n._(t`Toggle notification success`)}
|
)
|
||||||
/>
|
}
|
||||||
<Switch
|
aria-label={i18n._(t`Toggle notification success`)}
|
||||||
id={`notification-${notification.id}-error-toggle`}
|
/>
|
||||||
label={i18n._(t`Failure`)}
|
<Switch
|
||||||
isChecked={errorTurnedOn}
|
id={`notification-${notification.id}-error-toggle`}
|
||||||
isDisabled={!canToggleNotifications}
|
label={i18n._(t`Failure`)}
|
||||||
onChange={() => toggleNotification(
|
isChecked={errorTurnedOn}
|
||||||
notification.id,
|
isDisabled={!canToggleNotifications}
|
||||||
errorTurnedOn,
|
onChange={() =>
|
||||||
'error'
|
toggleNotification(notification.id, errorTurnedOn, 'error')
|
||||||
)}
|
}
|
||||||
aria-label={i18n._(t`Toggle notification failure`)}
|
aria-label={i18n._(t`Toggle notification failure`)}
|
||||||
/>
|
/>
|
||||||
</DataListCell>
|
</DataListCell>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
canToggleNotifications
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').first().find('input').simulate('change');
|
wrapper
|
||||||
|
.find('Switch')
|
||||||
|
.first()
|
||||||
|
.find('input')
|
||||||
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,7 +70,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
canToggleNotifications
|
canToggleNotifications
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('Switch').first().find('input').simulate('change');
|
wrapper
|
||||||
|
.find('Switch')
|
||||||
|
.first()
|
||||||
|
.find('input')
|
||||||
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +92,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
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');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,7 +114,11 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
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');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ import {
|
|||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarGroup,
|
ToolbarGroup,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { QuestionCircleIcon, UserIcon } from '@patternfly/react-icons';
|
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 {
|
class PageHeaderToolbar extends Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
isHelpOpen: false,
|
isHelpOpen: false,
|
||||||
isUserOpen: false
|
isUserOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleHelpSelect = this.handleHelpSelect.bind(this);
|
this.handleHelpSelect = this.handleHelpSelect.bind(this);
|
||||||
@@ -30,34 +31,34 @@ class PageHeaderToolbar extends Component {
|
|||||||
this.handleUserToggle = this.handleUserToggle.bind(this);
|
this.handleUserToggle = this.handleUserToggle.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHelpSelect () {
|
handleHelpSelect() {
|
||||||
const { isHelpOpen } = this.state;
|
const { isHelpOpen } = this.state;
|
||||||
|
|
||||||
this.setState({ isHelpOpen: !isHelpOpen });
|
this.setState({ isHelpOpen: !isHelpOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUserSelect () {
|
handleUserSelect() {
|
||||||
const { isUserOpen } = this.state;
|
const { isUserOpen } = this.state;
|
||||||
|
|
||||||
this.setState({ isUserOpen: !isUserOpen });
|
this.setState({ isUserOpen: !isUserOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHelpToggle (isOpen) {
|
handleHelpToggle(isOpen) {
|
||||||
this.setState({ isHelpOpen: isOpen });
|
this.setState({ isHelpOpen: isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUserToggle (isOpen) {
|
handleUserToggle(isOpen) {
|
||||||
this.setState({ isUserOpen: isOpen });
|
this.setState({ isUserOpen: isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { isHelpOpen, isUserOpen } = this.state;
|
const { isHelpOpen, isUserOpen } = this.state;
|
||||||
const {
|
const {
|
||||||
isAboutDisabled,
|
isAboutDisabled,
|
||||||
onAboutClick,
|
onAboutClick,
|
||||||
onLogoutClick,
|
onLogoutClick,
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
i18n
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,11 +71,11 @@ class PageHeaderToolbar extends Component {
|
|||||||
isOpen={isHelpOpen}
|
isOpen={isHelpOpen}
|
||||||
position={DropdownPosition.right}
|
position={DropdownPosition.right}
|
||||||
onSelect={this.handleHelpSelect}
|
onSelect={this.handleHelpSelect}
|
||||||
toggle={(
|
toggle={
|
||||||
<DropdownToggle onToggle={this.handleHelpToggle}>
|
<DropdownToggle onToggle={this.handleHelpToggle}>
|
||||||
<QuestionCircleIcon />
|
<QuestionCircleIcon />
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
}
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem key="help" target="_blank" href={DOCLINK}>
|
<DropdownItem key="help" target="_blank" href={DOCLINK}>
|
||||||
{i18n._(t`Help`)}
|
{i18n._(t`Help`)}
|
||||||
@@ -86,7 +87,7 @@ class PageHeaderToolbar extends Component {
|
|||||||
onClick={onAboutClick}
|
onClick={onAboutClick}
|
||||||
>
|
>
|
||||||
{i18n._(t`About`)}
|
{i18n._(t`About`)}
|
||||||
</DropdownItem>
|
</DropdownItem>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
@@ -98,7 +99,7 @@ class PageHeaderToolbar extends Component {
|
|||||||
isOpen={isUserOpen}
|
isOpen={isUserOpen}
|
||||||
position={DropdownPosition.right}
|
position={DropdownPosition.right}
|
||||||
onSelect={this.handleUserSelect}
|
onSelect={this.handleUserSelect}
|
||||||
toggle={(
|
toggle={
|
||||||
<DropdownToggle onToggle={this.handleUserToggle}>
|
<DropdownToggle onToggle={this.handleUserToggle}>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
{loggedInUser && (
|
{loggedInUser && (
|
||||||
@@ -107,7 +108,7 @@ class PageHeaderToolbar extends Component {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
}
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem key="user" href="#/home">
|
<DropdownItem key="user" href="#/home">
|
||||||
{i18n._(t`User Details`)}
|
{i18n._(t`User Details`)}
|
||||||
@@ -118,7 +119,7 @@ class PageHeaderToolbar extends Component {
|
|||||||
onClick={onLogoutClick}
|
onClick={onLogoutClick}
|
||||||
>
|
>
|
||||||
{i18n._(t`Logout`)}
|
{i18n._(t`Logout`)}
|
||||||
</DropdownItem>
|
</DropdownItem>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
@@ -132,11 +133,11 @@ class PageHeaderToolbar extends Component {
|
|||||||
PageHeaderToolbar.propTypes = {
|
PageHeaderToolbar.propTypes = {
|
||||||
isAboutDisabled: PropTypes.bool,
|
isAboutDisabled: PropTypes.bool,
|
||||||
onAboutClick: PropTypes.func.isRequired,
|
onAboutClick: PropTypes.func.isRequired,
|
||||||
onLogoutClick: PropTypes.func.isRequired
|
onLogoutClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
PageHeaderToolbar.defaultProps = {
|
PageHeaderToolbar.defaultProps = {
|
||||||
isAboutDisabled: false
|
isAboutDisabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(PageHeaderToolbar);
|
export default withI18n()(PageHeaderToolbar);
|
||||||
|
|||||||
@@ -26,19 +26,19 @@ const EmptyStateControlsWrapper = styled.div`
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
& > :not(:first-child) {
|
& > :not(:first-child) {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
class PaginatedDataList extends React.Component {
|
class PaginatedDataList extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.handleSetPage = this.handleSetPage.bind(this);
|
this.handleSetPage = this.handleSetPage.bind(this);
|
||||||
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
this.handleSetPageSize = this.handleSetPageSize.bind(this);
|
||||||
this.handleSort = this.handleSort.bind(this);
|
this.handleSort = this.handleSort.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortOrder () {
|
getSortOrder() {
|
||||||
const { qsConfig, location } = this.props;
|
const { qsConfig, location } = this.props;
|
||||||
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
const queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||||
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
if (queryParams.order_by && queryParams.order_by.startsWith('-')) {
|
||||||
@@ -47,29 +47,30 @@ class PaginatedDataList extends React.Component {
|
|||||||
return [queryParams.order_by, 'ascending'];
|
return [queryParams.order_by, 'ascending'];
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetPage (event, pageNumber) {
|
handleSetPage(event, pageNumber) {
|
||||||
this.pushHistoryState({ page: pageNumber });
|
this.pushHistoryState({ page: pageNumber });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetPageSize (event, pageSize) {
|
handleSetPageSize(event, pageSize) {
|
||||||
this.pushHistoryState({ page_size: pageSize });
|
this.pushHistoryState({ page_size: pageSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSort (sortedColumnKey, sortOrder) {
|
handleSort(sortedColumnKey, sortOrder) {
|
||||||
this.pushHistoryState({
|
this.pushHistoryState({
|
||||||
order_by: sortOrder === 'ascending' ? sortedColumnKey : `-${sortedColumnKey}`,
|
order_by:
|
||||||
|
sortOrder === 'ascending' ? sortedColumnKey : `-${sortedColumnKey}`,
|
||||||
page: null,
|
page: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pushHistoryState (newParams) {
|
pushHistoryState(newParams) {
|
||||||
const { history, qsConfig } = this.props;
|
const { history, qsConfig } = this.props;
|
||||||
const { pathname, search } = history.location;
|
const { pathname, search } = history.location;
|
||||||
const qs = updateNamespacedQueryString(qsConfig, search, newParams);
|
const qs = updateNamespacedQueryString(qsConfig, search, newParams);
|
||||||
history.push(`${pathname}?${qs}`);
|
history.push(`${pathname}?${qs}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const [orderBy, sortOrder] = this.getSortOrder();
|
const [orderBy, sortOrder] = this.getSortOrder();
|
||||||
const {
|
const {
|
||||||
contentError,
|
contentError,
|
||||||
@@ -87,25 +88,35 @@ class PaginatedDataList extends React.Component {
|
|||||||
i18n,
|
i18n,
|
||||||
renderToolbar,
|
renderToolbar,
|
||||||
} = this.props;
|
} = 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 queryParams = parseNamespacedQueryString(qsConfig, location.search);
|
||||||
|
|
||||||
const itemDisplayName = ucFirst(pluralize(itemName));
|
const itemDisplayName = ucFirst(pluralize(itemName));
|
||||||
const itemDisplayNamePlural = ucFirst(itemNamePlural || pluralize(itemName));
|
const itemDisplayNamePlural = ucFirst(
|
||||||
|
itemNamePlural || pluralize(itemName)
|
||||||
|
);
|
||||||
|
|
||||||
const dataListLabel = i18n._(t`${itemDisplayName} List`);
|
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 `);
|
const emptyContentTitle = i18n._(t`No ${itemDisplayNamePlural} Found `);
|
||||||
|
|
||||||
let Content;
|
let Content;
|
||||||
if (hasContentLoading && items.length <= 0) {
|
if (hasContentLoading && items.length <= 0) {
|
||||||
Content = (<ContentLoading />);
|
Content = <ContentLoading />;
|
||||||
} else if (contentError) {
|
} else if (contentError) {
|
||||||
Content = (<ContentError error={contentError} />);
|
Content = <ContentError error={contentError} />;
|
||||||
} else if (items.length <= 0) {
|
} else if (items.length <= 0) {
|
||||||
Content = (<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />);
|
Content = (
|
||||||
|
<ContentEmpty title={emptyContentTitle} message={emptyContentMessage} />
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Content = (<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>);
|
Content = (
|
||||||
|
<DataList aria-label={dataListLabel}>{items.map(renderItem)}</DataList>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length <= 0) {
|
if (items.length <= 0) {
|
||||||
@@ -116,9 +127,7 @@ class PaginatedDataList extends React.Component {
|
|||||||
{emptyStateControls}
|
{emptyStateControls}
|
||||||
</EmptyStateControlsWrapper>
|
</EmptyStateControlsWrapper>
|
||||||
)}
|
)}
|
||||||
{emptyStateControls && (
|
{emptyStateControls && <div css="border-bottom: 1px solid #d2d2d2" />}
|
||||||
<div css="border-bottom: 1px solid #d2d2d2" />
|
|
||||||
)}
|
|
||||||
{Content}
|
{Content}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -130,7 +139,7 @@ class PaginatedDataList extends React.Component {
|
|||||||
sortedColumnKey: orderBy,
|
sortedColumnKey: orderBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
columns,
|
columns,
|
||||||
onSearch: () => { },
|
onSearch: () => {},
|
||||||
onSort: this.handleSort,
|
onSort: this.handleSort,
|
||||||
})}
|
})}
|
||||||
{Content}
|
{Content}
|
||||||
@@ -139,12 +148,16 @@ class PaginatedDataList extends React.Component {
|
|||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
page={queryParams.page || 1}
|
page={queryParams.page || 1}
|
||||||
perPage={queryParams.page_size}
|
perPage={queryParams.page_size}
|
||||||
perPageOptions={showPageSizeOptions ? [
|
perPageOptions={
|
||||||
{ title: '5', value: 5 },
|
showPageSizeOptions
|
||||||
{ title: '10', value: 10 },
|
? [
|
||||||
{ title: '20', value: 20 },
|
{ title: '5', value: 5 },
|
||||||
{ title: '50', value: 50 }
|
{ title: '10', value: 10 },
|
||||||
] : []}
|
{ title: '20', value: 20 },
|
||||||
|
{ title: '50', value: 50 },
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
onSetPage={this.handleSetPage}
|
onSetPage={this.handleSetPage}
|
||||||
onPerPageSelect={this.handleSetPageSize}
|
onPerPageSelect={this.handleSetPageSize}
|
||||||
/>
|
/>
|
||||||
@@ -166,11 +179,13 @@ PaginatedDataList.propTypes = {
|
|||||||
itemNamePlural: PropTypes.string,
|
itemNamePlural: PropTypes.string,
|
||||||
qsConfig: QSConfig.isRequired,
|
qsConfig: QSConfig.isRequired,
|
||||||
renderItem: PropTypes.func,
|
renderItem: PropTypes.func,
|
||||||
toolbarColumns: arrayOf(shape({
|
toolbarColumns: arrayOf(
|
||||||
name: string.isRequired,
|
shape({
|
||||||
key: string.isRequired,
|
name: string.isRequired,
|
||||||
isSortable: bool,
|
key: string.isRequired,
|
||||||
})),
|
isSortable: bool,
|
||||||
|
})
|
||||||
|
),
|
||||||
showPageSizeOptions: PropTypes.bool,
|
showPageSizeOptions: PropTypes.bool,
|
||||||
renderToolbar: PropTypes.func,
|
renderToolbar: PropTypes.func,
|
||||||
hasContentLoading: PropTypes.bool,
|
hasContentLoading: PropTypes.bool,
|
||||||
@@ -184,8 +199,8 @@ PaginatedDataList.defaultProps = {
|
|||||||
itemName: 'item',
|
itemName: 'item',
|
||||||
itemNamePlural: '',
|
itemNamePlural: '',
|
||||||
showPageSizeOptions: true,
|
showPageSizeOptions: true,
|
||||||
renderItem: (item) => (<PaginatedDataListItem key={item.id} item={item} />),
|
renderItem: item => <PaginatedDataListItem key={item.id} item={item} />,
|
||||||
renderToolbar: (props) => (<DataListToolbar {...props} />),
|
renderToolbar: props => <DataListToolbar {...props} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { PaginatedDataList as _PaginatedDataList };
|
export { PaginatedDataList as _PaginatedDataList };
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ describe('<PaginatedDataList />', () => {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
}}
|
}}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
/>, { context: { router: { history } } }
|
/>,
|
||||||
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const toolbar = wrapper.find('DataListToolbar');
|
const toolbar = wrapper.find('DataListToolbar');
|
||||||
@@ -85,7 +86,8 @@ describe('<PaginatedDataList />', () => {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
}}
|
}}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
/>, { context: { router: { history } } }
|
/>,
|
||||||
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const pagination = wrapper.find('Pagination');
|
const pagination = wrapper.find('Pagination');
|
||||||
@@ -110,7 +112,8 @@ describe('<PaginatedDataList />', () => {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
}}
|
}}
|
||||||
qsConfig={qsConfig}
|
qsConfig={qsConfig}
|
||||||
/>, { context: { router: { history } } }
|
/>,
|
||||||
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
|
|
||||||
const pagination = wrapper.find('Pagination');
|
const pagination = wrapper.find('Pagination');
|
||||||
|
|||||||
@@ -17,24 +17,20 @@ const DetailWrapper = styled(TextContent)`
|
|||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function PaginatedDataListItem ({ item }) {
|
export default function PaginatedDataListItem({ item }) {
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<DataListItem aria-labelledby={`items-list-item-${item.id}`} key={item.id}>
|
||||||
aria-labelledby={`items-list-item-${item.id}`}
|
|
||||||
key={item.id}
|
|
||||||
>
|
|
||||||
<DataListItemRow>
|
<DataListItemRow>
|
||||||
<DataListItemCells dataListCells={[
|
<DataListItemCells
|
||||||
<DataListCell key="team-name">
|
dataListCells={[
|
||||||
<DetailWrapper>
|
<DataListCell key="team-name">
|
||||||
<Link to={{ pathname: item.url }}>
|
<DetailWrapper>
|
||||||
<b id={`items-list-item-${item.id}`}>
|
<Link to={{ pathname: item.url }}>
|
||||||
{item.name}
|
<b id={`items-list-item-${item.id}`}>{item.name}</b>
|
||||||
</b>
|
</Link>
|
||||||
</Link>
|
</DetailWrapper>
|
||||||
</DetailWrapper>
|
</DataListCell>,
|
||||||
</DataListCell>
|
]}
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</DataListItemRow>
|
</DataListItemRow>
|
||||||
</DataListItem>
|
</DataListItem>
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ const Button = styled(PFButton)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function ToolbarAddButton ({ linkTo, onClick, i18n }) {
|
function ToolbarAddButton({ linkTo, onClick, i18n }) {
|
||||||
if (!linkTo && !onClick) {
|
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) {
|
if (linkTo) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip content={i18n._(t`Add`)} position="top">
|
||||||
content={i18n._(t`Add`)}
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to={linkTo}
|
to={linkTo}
|
||||||
@@ -37,11 +36,7 @@ function ToolbarAddButton ({ linkTo, onClick, i18n }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button variant="primary" aria-label={i18n._(t`Add`)} onClick={onClick}>
|
||||||
variant="primary"
|
|
||||||
aria-label={i18n._(t`Add`)}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -52,7 +47,7 @@ ToolbarAddButton.propTypes = {
|
|||||||
};
|
};
|
||||||
ToolbarAddButton.defaultProps = {
|
ToolbarAddButton.defaultProps = {
|
||||||
linkTo: null,
|
linkTo: null,
|
||||||
onClick: null
|
onClick: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(ToolbarAddButton);
|
export default withI18n()(ToolbarAddButton);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import ToolbarAddButton from './ToolbarAddButton';
|
|||||||
describe('<ToolbarAddButton />', () => {
|
describe('<ToolbarAddButton />', () => {
|
||||||
test('should render button', () => {
|
test('should render button', () => {
|
||||||
const onClick = jest.fn();
|
const onClick = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<ToolbarAddButton onClick={onClick} />);
|
||||||
<ToolbarAddButton onClick={onClick} />
|
|
||||||
);
|
|
||||||
const button = wrapper.find('button');
|
const button = wrapper.find('button');
|
||||||
expect(button).toHaveLength(1);
|
expect(button).toHaveLength(1);
|
||||||
button.simulate('click');
|
button.simulate('click');
|
||||||
@@ -15,9 +13,7 @@ describe('<ToolbarAddButton />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render link', () => {
|
test('should render link', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<ToolbarAddButton linkTo="/foo" />);
|
||||||
<ToolbarAddButton linkTo="/foo" />
|
|
||||||
);
|
|
||||||
const link = wrapper.find('Link');
|
const link = wrapper.find('Link');
|
||||||
expect(link).toHaveLength(1);
|
expect(link).toHaveLength(1);
|
||||||
expect(link.prop('to')).toBe('/foo');
|
expect(link.prop('to')).toBe('/foo');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const DeleteButton = styled(Button)`
|
|||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color:#d9534f;
|
background-color: #d9534f;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const ItemToDelete = shape({
|
|||||||
}).isRequired,
|
}).isRequired,
|
||||||
});
|
});
|
||||||
|
|
||||||
function cannotDelete (item) {
|
function cannotDelete(item) {
|
||||||
return !item.summary_fields.user_capabilities.delete;
|
return !item.summary_fields.user_capabilities.delete;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class ToolbarDeleteButton extends React.Component {
|
|||||||
itemName: 'item',
|
itemName: 'item',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -60,34 +60,34 @@ class ToolbarDeleteButton extends React.Component {
|
|||||||
this.handleDelete = this.handleDelete.bind(this);
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmDelete () {
|
handleConfirmDelete() {
|
||||||
this.setState({ isModalOpen: true });
|
this.setState({ isModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancelDelete () {
|
handleCancelDelete() {
|
||||||
this.setState({ isModalOpen: false, });
|
this.setState({ isModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDelete () {
|
handleDelete() {
|
||||||
const { onDelete } = this.props;
|
const { onDelete } = this.props;
|
||||||
onDelete();
|
onDelete();
|
||||||
this.setState({ isModalOpen: false });
|
this.setState({ isModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTooltip () {
|
renderTooltip() {
|
||||||
const { itemsToDelete, itemName, i18n } = this.props;
|
const { itemsToDelete, itemName, i18n } = this.props;
|
||||||
|
|
||||||
const itemsUnableToDelete = itemsToDelete
|
const itemsUnableToDelete = itemsToDelete
|
||||||
.filter(cannotDelete)
|
.filter(cannotDelete)
|
||||||
.map(item => (
|
.map(item => <div key={item.id}>{item.name}</div>);
|
||||||
<div key={item.id}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
if (itemsToDelete.some(cannotDelete)) {
|
if (itemsToDelete.some(cannotDelete)) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,22 +97,19 @@ class ToolbarDeleteButton extends React.Component {
|
|||||||
return i18n._(t`Select a row to delete`);
|
return i18n._(t`Select a row to delete`);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { itemsToDelete, itemName, i18n } = this.props;
|
const { itemsToDelete, itemName, i18n } = this.props;
|
||||||
const { isModalOpen } = this.state;
|
const { isModalOpen } = this.state;
|
||||||
|
|
||||||
const isDisabled = itemsToDelete.length === 0
|
const isDisabled =
|
||||||
|| itemsToDelete.some(cannotDelete);
|
itemsToDelete.length === 0 || itemsToDelete.some(cannotDelete);
|
||||||
|
|
||||||
// NOTE: Once PF supports tooltips on disabled elements,
|
// NOTE: Once PF supports tooltips on disabled elements,
|
||||||
// we can delete the extra <div> around the <DeleteButton> below.
|
// we can delete the extra <div> around the <DeleteButton> below.
|
||||||
// See: https://github.com/patternfly/patternfly-react/issues/1894
|
// See: https://github.com/patternfly/patternfly-react/issues/1894
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Tooltip
|
<Tooltip content={this.renderTooltip()} position="top">
|
||||||
content={this.renderTooltip()}
|
|
||||||
position="top"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -124,12 +121,13 @@ class ToolbarDeleteButton extends React.Component {
|
|||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{ isModalOpen && (
|
{isModalOpen && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title={itemsToDelete === 1
|
title={
|
||||||
? i18n._(t`Delete ${itemName}`)
|
itemsToDelete === 1
|
||||||
: i18n._(t`Delete ${pluralize(itemName)}`)
|
? i18n._(t`Delete ${itemName}`)
|
||||||
|
: i18n._(t`Delete ${pluralize(itemName)}`)
|
||||||
}
|
}
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={this.handleCancelDelete}
|
onClose={this.handleCancelDelete}
|
||||||
@@ -149,16 +147,14 @@ class ToolbarDeleteButton extends React.Component {
|
|||||||
onClick={this.handleCancelDelete}
|
onClick={this.handleCancelDelete}
|
||||||
>
|
>
|
||||||
{i18n._(t`Cancel`)}
|
{i18n._(t`Cancel`)}
|
||||||
</Button>
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{i18n._(t`Are you sure you want to delete:`)}
|
{i18n._(t`Are you sure you want to delete:`)}
|
||||||
<br />
|
<br />
|
||||||
{itemsToDelete.map((item) => (
|
{itemsToDelete.map(item => (
|
||||||
<span key={item.id}>
|
<span key={item.id}>
|
||||||
<strong>
|
<strong>{item.name}</strong>
|
||||||
{item.name}
|
|
||||||
</strong>
|
|
||||||
<br />
|
<br />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ const itemB = {
|
|||||||
describe('<ToolbarDeleteButton />', () => {
|
describe('<ToolbarDeleteButton />', () => {
|
||||||
test('should render button', () => {
|
test('should render button', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[]} />
|
||||||
onDelete={() => {}}
|
|
||||||
itemsToDelete={[]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button')).toHaveLength(1);
|
expect(wrapper.find('button')).toHaveLength(1);
|
||||||
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
|
expect(wrapper.find('ToolbarDeleteButton')).toMatchSnapshot();
|
||||||
@@ -27,14 +24,10 @@ describe('<ToolbarDeleteButton />', () => {
|
|||||||
|
|
||||||
test('should open confirmation modal', () => {
|
test('should open confirmation modal', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
|
||||||
onDelete={() => {}}
|
|
||||||
itemsToDelete={[itemA]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
wrapper.find('button').simulate('click');
|
wrapper.find('button').simulate('click');
|
||||||
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen'))
|
expect(wrapper.find('ToolbarDeleteButton').state('isModalOpen')).toBe(true);
|
||||||
.toBe(true);
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('Modal')).toHaveLength(1);
|
expect(wrapper.find('Modal')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@@ -42,33 +35,26 @@ describe('<ToolbarDeleteButton />', () => {
|
|||||||
test('should invoke onDelete prop', () => {
|
test('should invoke onDelete prop', () => {
|
||||||
const onDelete = jest.fn();
|
const onDelete = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton onDelete={onDelete} itemsToDelete={[itemA]} />
|
||||||
onDelete={onDelete}
|
|
||||||
itemsToDelete={[itemA]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
wrapper.find('ToolbarDeleteButton').setState({ isModalOpen: true });
|
wrapper.find('ToolbarDeleteButton').setState({ isModalOpen: true });
|
||||||
wrapper.find('button.pf-m-danger').simulate('click');
|
wrapper.find('button.pf-m-danger').simulate('click');
|
||||||
expect(onDelete).toHaveBeenCalled();
|
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', () => {
|
test('should disable button when no delete permissions', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemB]} />
|
||||||
onDelete={() => {}}
|
|
||||||
itemsToDelete={[itemB]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(wrapper.find('button[disabled]')).toHaveLength(1);
|
expect(wrapper.find('button[disabled]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should render tooltip', () => {
|
test('should render tooltip', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<ToolbarDeleteButton
|
<ToolbarDeleteButton onDelete={() => {}} itemsToDelete={[itemA]} />
|
||||||
onDelete={() => {}}
|
|
||||||
itemsToDelete={[itemA]}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
expect(wrapper.find('Tooltip')).toHaveLength(1);
|
expect(wrapper.find('Tooltip')).toHaveLength(1);
|
||||||
expect(wrapper.find('Tooltip').prop('content')).toEqual('Delete');
|
expect(wrapper.find('Tooltip').prop('content')).toEqual('Delete');
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled, { css } from 'styled-components';
|
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 { I18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
const AWXPagination = styled(PFPagination)`
|
const AWXPagination = styled(PFPagination)`
|
||||||
${props => (props.perPageOptions && !props.perPageOptions.length && css`
|
${props =>
|
||||||
.pf-c-options-menu__toggle-button {
|
props.perPageOptions &&
|
||||||
|
!props.perPageOptions.length &&
|
||||||
|
css`
|
||||||
|
.pf-c-options-menu__toggle-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`)}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default (props) => (
|
export default props => (
|
||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<AWXPagination
|
<AWXPagination
|
||||||
@@ -27,7 +33,7 @@ export default (props) => (
|
|||||||
toNextPage: i18n._(t`Go to next page`),
|
toNextPage: i18n._(t`Go to next page`),
|
||||||
optionsToggle: i18n._(t`Select`),
|
optionsToggle: i18n._(t`Select`),
|
||||||
currPage: i18n._(t`Current page`),
|
currPage: i18n._(t`Current page`),
|
||||||
paginationTitle: i18n._(t`Pagination`)
|
paginationTitle: i18n._(t`Pagination`),
|
||||||
}}
|
}}
|
||||||
dropDirection={DropdownDirection.up}
|
dropDirection={DropdownDirection.up}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import Pagination from './Pagination';
|
|||||||
|
|
||||||
describe('Pagination', () => {
|
describe('Pagination', () => {
|
||||||
test('renders the expected content', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(<Pagination itemCount={0} max={9000} />);
|
||||||
<Pagination
|
|
||||||
itemCount={0}
|
|
||||||
max={9000}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ const Tabs = styled(PFTabs)`
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
content: "";
|
content: '';
|
||||||
border: solid var(--pf-c-tabs__item--BorderColor);
|
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 { history, tabsArray } = props;
|
||||||
|
|
||||||
const getActiveTabId = () => {
|
const getActiveTabId = () => {
|
||||||
@@ -42,7 +43,7 @@ function RoutedTabs (props) {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleTabSelect (event, eventKey) {
|
function handleTabSelect(event, eventKey) {
|
||||||
const match = tabsArray.find(tab => tab.id === eventKey);
|
const match = tabsArray.find(tab => tab.id === eventKey);
|
||||||
if (match) {
|
if (match) {
|
||||||
history.push(match.link);
|
history.push(match.link);
|
||||||
@@ -50,10 +51,7 @@ function RoutedTabs (props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs activeKey={getActiveTabId()} onSelect={handleTabSelect}>
|
||||||
activeKey={getActiveTabId()}
|
|
||||||
onSelect={handleTabSelect}
|
|
||||||
>
|
|
||||||
{tabsArray.map(tab => (
|
{tabsArray.map(tab => (
|
||||||
<Tab
|
<Tab
|
||||||
className={`${tab.name}`}
|
className={`${tab.name}`}
|
||||||
@@ -70,14 +68,16 @@ function RoutedTabs (props) {
|
|||||||
RoutedTabs.propTypes = {
|
RoutedTabs.propTypes = {
|
||||||
history: shape({
|
history: shape({
|
||||||
location: shape({
|
location: shape({
|
||||||
pathname: string.isRequired
|
pathname: string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
tabsArray: arrayOf(shape({
|
tabsArray: arrayOf(
|
||||||
id: number.isRequired,
|
shape({
|
||||||
link: string.isRequired,
|
id: number.isRequired,
|
||||||
name: string.isRequired,
|
link: string.isRequired,
|
||||||
})).isRequired,
|
name: string.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { RoutedTabs as _RoutedTabs };
|
export { RoutedTabs as _RoutedTabs };
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const tabs = [
|
|||||||
{ name: 'Details', link: '/organizations/19/details', id: 1 },
|
{ name: 'Details', link: '/organizations/19/details', id: 1 },
|
||||||
{ name: 'Access', link: '/organizations/19/access', id: 2 },
|
{ name: 'Access', link: '/organizations/19/access', id: 2 },
|
||||||
{ name: 'Teams', link: '/organizations/19/teams', id: 3 },
|
{ 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 />', () => {
|
describe('<RoutedTabs />', () => {
|
||||||
@@ -24,21 +24,14 @@ describe('<RoutedTabs />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('RoutedTabs renders successfully', () => {
|
test('RoutedTabs renders successfully', () => {
|
||||||
wrapper = shallow(
|
wrapper = shallow(<_RoutedTabs tabsArray={tabs} history={history} />);
|
||||||
<_RoutedTabs
|
|
||||||
tabsArray={tabs}
|
|
||||||
history={history}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(wrapper.find(Tab)).toHaveLength(4);
|
expect(wrapper.find(Tab)).toHaveLength(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Given a URL the correct tab is active', async () => {
|
test('Given a URL the correct tab is active', async () => {
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<RoutedTabs
|
<RoutedTabs tabsArray={tabs} />
|
||||||
tabsArray={tabs}
|
|
||||||
/>
|
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -49,9 +42,7 @@ describe('<RoutedTabs />', () => {
|
|||||||
test('should update history when new tab selected', async () => {
|
test('should update history when new tab selected', async () => {
|
||||||
wrapper = mount(
|
wrapper = mount(
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<RoutedTabs
|
<RoutedTabs tabsArray={tabs} />
|
||||||
tabsArray={tabs}
|
|
||||||
/>
|
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import {
|
|||||||
DropdownPosition,
|
DropdownPosition,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
TextInput as PFTextInput
|
TextInput as PFTextInput,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import { SearchIcon } from '@patternfly/react-icons';
|
||||||
SearchIcon
|
|
||||||
} from '@patternfly/react-icons';
|
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -27,7 +25,8 @@ const Button = styled(PFButton)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Dropdown = styled(PFDropdown)`
|
const Dropdown = styled(PFDropdown)`
|
||||||
&&& { /* Higher specificity required because we are selecting unclassed elements */
|
&&& {
|
||||||
|
/* Higher specificity required because we are selecting unclassed elements */
|
||||||
> button {
|
> button {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
@@ -35,20 +34,22 @@ const Dropdown = styled(PFDropdown)`
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
> span { /* text element */
|
> span {
|
||||||
|
/* text element */
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg { /* caret icon */
|
> svg {
|
||||||
|
/* caret icon */
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
class Search extends React.Component {
|
class Search extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const { sortedColumnKey } = this.props;
|
const { sortedColumnKey } = this.props;
|
||||||
@@ -64,11 +65,11 @@ class Search extends React.Component {
|
|||||||
this.handleSearch = this.handleSearch.bind(this);
|
this.handleSearch = this.handleSearch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownToggle (isSearchDropdownOpen) {
|
handleDropdownToggle(isSearchDropdownOpen) {
|
||||||
this.setState({ isSearchDropdownOpen });
|
this.setState({ isSearchDropdownOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownSelect ({ target }) {
|
handleDropdownSelect({ target }) {
|
||||||
const { columns } = this.props;
|
const { columns } = this.props;
|
||||||
const { innerText } = target;
|
const { innerText } = target;
|
||||||
|
|
||||||
@@ -76,30 +77,25 @@ class Search extends React.Component {
|
|||||||
this.setState({ isSearchDropdownOpen: false, searchKey });
|
this.setState({ isSearchDropdownOpen: false, searchKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearch () {
|
handleSearch() {
|
||||||
const { searchValue } = this.state;
|
const { searchValue } = this.state;
|
||||||
const { onSearch } = this.props;
|
const { onSearch } = this.props;
|
||||||
|
|
||||||
onSearch(searchValue);
|
onSearch(searchValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearchInputChange (searchValue) {
|
handleSearchInputChange(searchValue) {
|
||||||
this.setState({ searchValue });
|
this.setState({ searchValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const {
|
const { columns, i18n } = this.props;
|
||||||
columns,
|
const { isSearchDropdownOpen, searchKey, searchValue } = this.state;
|
||||||
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
|
const searchDropdownItems = columns
|
||||||
.filter(({ key }) => key !== searchKey)
|
.filter(({ key }) => key !== searchKey)
|
||||||
@@ -116,14 +112,14 @@ class Search extends React.Component {
|
|||||||
onSelect={this.handleDropdownSelect}
|
onSelect={this.handleDropdownSelect}
|
||||||
direction={up}
|
direction={up}
|
||||||
isOpen={isSearchDropdownOpen}
|
isOpen={isSearchDropdownOpen}
|
||||||
toggle={(
|
toggle={
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
id="awx-search"
|
id="awx-search"
|
||||||
onToggle={this.handleDropdownToggle}
|
onToggle={this.handleDropdownToggle}
|
||||||
>
|
>
|
||||||
{searchColumnName}
|
{searchColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
}
|
||||||
dropdownItems={searchDropdownItems}
|
dropdownItems={searchDropdownItems}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -153,7 +149,7 @@ Search.propTypes = {
|
|||||||
|
|
||||||
Search.defaultProps = {
|
Search.defaultProps = {
|
||||||
onSearch: null,
|
onSearch: null,
|
||||||
sortedColumnKey: 'name'
|
sortedColumnKey: 'name',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(Search);
|
export default withI18n()(Search);
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ describe('<Search />', () => {
|
|||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
|
|
||||||
search = mountWithContexts(
|
search = mountWithContexts(
|
||||||
<Search
|
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||||
sortedColumnKey="name"
|
|
||||||
columns={columns}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
search.find(searchTextInput).instance().value = 'test-321';
|
search.find(searchTextInput).instance().value = 'test-321';
|
||||||
@@ -39,11 +35,7 @@ describe('<Search />', () => {
|
|||||||
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
const columns = [{ name: 'Name', key: 'name', isSortable: true }];
|
||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Search
|
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||||
sortedColumnKey="name"
|
|
||||||
columns={columns}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>
|
|
||||||
).find('Search');
|
).find('Search');
|
||||||
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
|
expect(wrapper.state('isSearchDropdownOpen')).toEqual(false);
|
||||||
wrapper.instance().handleDropdownToggle(true);
|
wrapper.instance().handleDropdownToggle(true);
|
||||||
@@ -53,18 +45,16 @@ describe('<Search />', () => {
|
|||||||
test('handleDropdownSelect properly updates state', async () => {
|
test('handleDropdownSelect properly updates state', async () => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'Name', key: 'name', isSortable: true },
|
{ name: 'Name', key: 'name', isSortable: true },
|
||||||
{ name: 'Description', key: 'description', isSortable: true }
|
{ name: 'Description', key: 'description', isSortable: true },
|
||||||
];
|
];
|
||||||
const onSearch = jest.fn();
|
const onSearch = jest.fn();
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<Search
|
<Search sortedColumnKey="name" columns={columns} onSearch={onSearch} />
|
||||||
sortedColumnKey="name"
|
|
||||||
columns={columns}
|
|
||||||
onSearch={onSearch}
|
|
||||||
/>
|
|
||||||
).find('Search');
|
).find('Search');
|
||||||
expect(wrapper.state('searchKey')).toEqual('name');
|
expect(wrapper.state('searchKey')).toEqual('name');
|
||||||
wrapper.instance().handleDropdownSelect({ target: { innerText: 'Description' } });
|
wrapper
|
||||||
|
.instance()
|
||||||
|
.handleDropdownSelect({ target: { innerText: 'Description' } });
|
||||||
expect(wrapper.state('searchKey')).toEqual('description');
|
expect(wrapper.state('searchKey')).toEqual('description');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,20 +19,18 @@ const SplitLabelItem = styled(SplitItem)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class SelectedList extends Component {
|
class SelectedList extends Component {
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
showOverflowAfter,
|
showOverflowAfter,
|
||||||
onRemove,
|
onRemove,
|
||||||
displayKey,
|
displayKey,
|
||||||
isReadOnly
|
isReadOnly,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<Split>
|
<Split>
|
||||||
<SplitLabelItem>
|
<SplitLabelItem>{label}</SplitLabelItem>
|
||||||
{label}
|
|
||||||
</SplitLabelItem>
|
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<SplitItem>
|
<SplitItem>
|
||||||
<ChipGroup showOverflowAfter={showOverflowAfter}>
|
<ChipGroup showOverflowAfter={showOverflowAfter}>
|
||||||
@@ -58,7 +56,7 @@ SelectedList.propTypes = {
|
|||||||
onRemove: PropTypes.func,
|
onRemove: PropTypes.func,
|
||||||
selected: PropTypes.arrayOf(PropTypes.object).isRequired,
|
selected: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
showOverflowAfter: PropTypes.number,
|
showOverflowAfter: PropTypes.number,
|
||||||
isReadOnly: PropTypes.bool
|
isReadOnly: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectedList.defaultProps = {
|
SelectedList.defaultProps = {
|
||||||
@@ -66,7 +64,7 @@ SelectedList.defaultProps = {
|
|||||||
label: 'Selected',
|
label: 'Selected',
|
||||||
onRemove: () => null,
|
onRemove: () => null,
|
||||||
showOverflowAfter: 5,
|
showOverflowAfter: 5,
|
||||||
isReadOnly: false
|
isReadOnly: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SelectedList;
|
export default SelectedList;
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ describe('<SelectedList />', () => {
|
|||||||
const mockSelected = [
|
const mockSelected = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'bar'
|
name: 'bar',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
mount(
|
mount(
|
||||||
<SelectedList
|
<SelectedList
|
||||||
@@ -43,8 +44,8 @@ describe('<SelectedList />', () => {
|
|||||||
const mockSelected = [
|
const mockSelected = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
<SelectedList
|
<SelectedList
|
||||||
@@ -54,10 +55,13 @@ describe('<SelectedList />', () => {
|
|||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper.find('.pf-c-chip button').first().simulate('click');
|
wrapper
|
||||||
|
.find('.pf-c-chip button')
|
||||||
|
.first()
|
||||||
|
.simulate('click');
|
||||||
expect(onRemove).toBeCalledWith({
|
expect(onRemove).toBeCalledWith({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo'
|
name: 'foo',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import {
|
|||||||
Dropdown as PFDropdown,
|
Dropdown as PFDropdown,
|
||||||
DropdownPosition,
|
DropdownPosition,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
DropdownItem
|
DropdownItem,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import {
|
import {
|
||||||
SortAlphaDownIcon,
|
SortAlphaDownIcon,
|
||||||
SortAlphaUpIcon,
|
SortAlphaUpIcon,
|
||||||
SortNumericDownIcon,
|
SortNumericDownIcon,
|
||||||
SortNumericUpIcon
|
SortNumericUpIcon,
|
||||||
} from '@patternfly/react-icons';
|
} from '@patternfly/react-icons';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -27,17 +27,19 @@ const Dropdown = styled(PFDropdown)`
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
|
|
||||||
> span { /* text element within dropdown */
|
> span {
|
||||||
|
/* text element within dropdown */
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> svg { /* caret icon */
|
> svg {
|
||||||
|
/* caret icon */
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const IconWrapper = styled.span`
|
const IconWrapper = styled.span`
|
||||||
@@ -47,7 +49,7 @@ const IconWrapper = styled.span`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
class Sort extends React.Component {
|
class Sort extends React.Component {
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -59,40 +61,36 @@ class Sort extends React.Component {
|
|||||||
this.handleSort = this.handleSort.bind(this);
|
this.handleSort = this.handleSort.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownToggle (isSortDropdownOpen) {
|
handleDropdownToggle(isSortDropdownOpen) {
|
||||||
this.setState({ isSortDropdownOpen });
|
this.setState({ isSortDropdownOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDropdownSelect ({ target }) {
|
handleDropdownSelect({ target }) {
|
||||||
const { columns, onSort, sortOrder } = this.props;
|
const { columns, onSort, sortOrder } = this.props;
|
||||||
const { innerText } = target;
|
const { innerText } = target;
|
||||||
|
|
||||||
const [{ key: searchKey }] = columns.filter(({ name }) => name === innerText);
|
const [{ key: searchKey }] = columns.filter(
|
||||||
|
({ name }) => name === innerText
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({ isSortDropdownOpen: false });
|
this.setState({ isSortDropdownOpen: false });
|
||||||
onSort(searchKey, sortOrder);
|
onSort(searchKey, sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSort () {
|
handleSort() {
|
||||||
const { onSort, sortedColumnKey, sortOrder } = this.props;
|
const { onSort, sortedColumnKey, sortOrder } = this.props;
|
||||||
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
|
const newSortOrder = sortOrder === 'ascending' ? 'descending' : 'ascending';
|
||||||
|
|
||||||
onSort(sortedColumnKey, newSortOrder);
|
onSort(sortedColumnKey, newSortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { up } = DropdownPosition;
|
const { up } = DropdownPosition;
|
||||||
const {
|
const { columns, sortedColumnKey, sortOrder, i18n } = this.props;
|
||||||
columns,
|
const { isSortDropdownOpen } = this.state;
|
||||||
sortedColumnKey,
|
const [{ name: sortedColumnName, isNumeric }] = columns.filter(
|
||||||
sortOrder,
|
({ key }) => key === sortedColumnKey
|
||||||
i18n
|
);
|
||||||
} = this.props;
|
|
||||||
const {
|
|
||||||
isSortDropdownOpen
|
|
||||||
} = this.state;
|
|
||||||
const [{ name: sortedColumnName, isNumeric }] = columns
|
|
||||||
.filter(({ key }) => key === sortedColumnKey);
|
|
||||||
|
|
||||||
const sortDropdownItems = columns
|
const sortDropdownItems = columns
|
||||||
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
|
||||||
@@ -104,28 +102,30 @@ class Sort extends React.Component {
|
|||||||
|
|
||||||
let SortIcon;
|
let SortIcon;
|
||||||
if (isNumeric) {
|
if (isNumeric) {
|
||||||
SortIcon = sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
|
SortIcon =
|
||||||
|
sortOrder === 'ascending' ? SortNumericUpIcon : SortNumericDownIcon;
|
||||||
} else {
|
} else {
|
||||||
SortIcon = sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
|
SortIcon =
|
||||||
|
sortOrder === 'ascending' ? SortAlphaUpIcon : SortAlphaDownIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{ sortDropdownItems.length > 1 && (
|
{sortDropdownItems.length > 1 && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
style={{ marginRight: '20px' }}
|
style={{ marginRight: '20px' }}
|
||||||
onToggle={this.handleDropdownToggle}
|
onToggle={this.handleDropdownToggle}
|
||||||
onSelect={this.handleDropdownSelect}
|
onSelect={this.handleDropdownSelect}
|
||||||
direction={up}
|
direction={up}
|
||||||
isOpen={isSortDropdownOpen}
|
isOpen={isSortDropdownOpen}
|
||||||
toggle={(
|
toggle={
|
||||||
<DropdownToggle
|
<DropdownToggle
|
||||||
id="awx-sort"
|
id="awx-sort"
|
||||||
onToggle={this.handleDropdownToggle}
|
onToggle={this.handleDropdownToggle}
|
||||||
>
|
>
|
||||||
{sortedColumnName}
|
{sortedColumnName}
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
)}
|
}
|
||||||
dropdownItems={sortDropdownItems}
|
dropdownItems={sortDropdownItems}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -148,13 +148,13 @@ Sort.propTypes = {
|
|||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onSort: PropTypes.func,
|
onSort: PropTypes.func,
|
||||||
sortOrder: PropTypes.string,
|
sortOrder: PropTypes.string,
|
||||||
sortedColumnKey: PropTypes.string
|
sortedColumnKey: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
Sort.defaultProps = {
|
Sort.defaultProps = {
|
||||||
onSort: null,
|
onSort: null,
|
||||||
sortOrder: 'ascending',
|
sortOrder: 'ascending',
|
||||||
sortedColumnKey: 'name'
|
sortedColumnKey: 'name',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(Sort);
|
export default withI18n()(Sort);
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('<Sort />', () => {
|
|||||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||||
{ name: 'Baz', key: 'baz' }
|
{ name: 'Baz', key: 'baz' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
@@ -62,7 +62,7 @@ describe('<Sort />', () => {
|
|||||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||||
{ name: 'Baz', key: 'baz' }
|
{ name: 'Baz', key: 'baz' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
@@ -86,7 +86,7 @@ describe('<Sort />', () => {
|
|||||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||||
{ name: 'Baz', key: 'baz' }
|
{ name: 'Baz', key: 'baz' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
@@ -109,7 +109,7 @@ describe('<Sort />', () => {
|
|||||||
{ name: 'Foo', key: 'foo', isSortable: true },
|
{ name: 'Foo', key: 'foo', isSortable: true },
|
||||||
{ name: 'Bar', key: 'bar', isSortable: true },
|
{ name: 'Bar', key: 'bar', isSortable: true },
|
||||||
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
{ name: 'Bakery', key: 'bakery', isSortable: true },
|
||||||
{ name: 'Baz', key: 'baz' }
|
{ name: 'Baz', key: 'baz' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
@@ -133,8 +133,12 @@ describe('<Sort />', () => {
|
|||||||
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
const downAlphaIconSelector = 'SortAlphaDownIcon';
|
||||||
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
const upAlphaIconSelector = 'SortAlphaUpIcon';
|
||||||
|
|
||||||
const numericColumns = [{ name: 'ID', key: 'id', isSortable: true, isNumeric: true }];
|
const numericColumns = [
|
||||||
const alphaColumns = [{ name: 'Name', key: 'name', isSortable: true, isNumeric: false }];
|
{ name: 'ID', key: 'id', isSortable: true, isNumeric: true },
|
||||||
|
];
|
||||||
|
const alphaColumns = [
|
||||||
|
{ name: 'Name', key: 'name', isSortable: true, isNumeric: false },
|
||||||
|
];
|
||||||
const onSort = jest.fn();
|
const onSort = jest.fn();
|
||||||
|
|
||||||
sort = mountWithContexts(
|
sort = mountWithContexts(
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import {
|
import { Route, Switch, Redirect } from 'react-router-dom';
|
||||||
Route,
|
import { I18n } from '@lingui/react';
|
||||||
Switch,
|
|
||||||
Redirect
|
|
||||||
} from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
I18n
|
|
||||||
} from '@lingui/react';
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import '@patternfly/react-core/dist/styles/base.css';
|
import '@patternfly/react-core/dist/styles/base.css';
|
||||||
@@ -44,19 +38,21 @@ import RootProvider from './RootProvider';
|
|||||||
import { BrandName } from './variables';
|
import { BrandName } from './variables';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export function main (render) {
|
export function main(render) {
|
||||||
const el = document.getElementById('app');
|
const el = document.getElementById('app');
|
||||||
document.title = `Ansible ${BrandName}`;
|
document.title = `Ansible ${BrandName}`;
|
||||||
|
|
||||||
const defaultRedirect = () => (<Redirect to="/home" />);
|
const defaultRedirect = () => <Redirect to="/home" />;
|
||||||
const removeTrailingSlash = (
|
const removeTrailingSlash = (
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
strict
|
strict
|
||||||
path="/*/"
|
path="/*/"
|
||||||
render={({ history: { location: { pathname, search, hash } } }) => (
|
render={({
|
||||||
<Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />
|
history: {
|
||||||
)}
|
location: { pathname, search, hash },
|
||||||
|
},
|
||||||
|
}) => <Redirect to={`${pathname.slice(0, -1)}${search}${hash}`} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const loginRoutes = (
|
const loginRoutes = (
|
||||||
@@ -64,9 +60,7 @@ export function main (render) {
|
|||||||
{removeTrailingSlash}
|
{removeTrailingSlash}
|
||||||
<Route
|
<Route
|
||||||
path="/login"
|
path="/login"
|
||||||
render={() => (
|
render={() => <Login isAuthenticated={isAuthenticated} />}
|
||||||
<Login isAuthenticated={isAuthenticated} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<Redirect to="/login" />
|
<Redirect to="/login" />
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -77,7 +71,9 @@ export function main (render) {
|
|||||||
<I18n>
|
<I18n>
|
||||||
{({ i18n }) => (
|
{({ i18n }) => (
|
||||||
<Background>
|
<Background>
|
||||||
{!isAuthenticated(document.cookie) ? loginRoutes : (
|
{!isAuthenticated(document.cookie) ? (
|
||||||
|
loginRoutes
|
||||||
|
) : (
|
||||||
<Switch>
|
<Switch>
|
||||||
{removeTrailingSlash}
|
{removeTrailingSlash}
|
||||||
<Route path="/login" render={defaultRedirect} />
|
<Route path="/login" render={defaultRedirect} />
|
||||||
@@ -94,22 +90,22 @@ export function main (render) {
|
|||||||
{
|
{
|
||||||
title: i18n._(t`Dashboard`),
|
title: i18n._(t`Dashboard`),
|
||||||
path: '/home',
|
path: '/home',
|
||||||
component: Dashboard
|
component: Dashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Jobs`),
|
title: i18n._(t`Jobs`),
|
||||||
path: '/jobs',
|
path: '/jobs',
|
||||||
component: Jobs
|
component: Jobs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Schedules`),
|
title: i18n._(t`Schedules`),
|
||||||
path: '/schedules',
|
path: '/schedules',
|
||||||
component: Schedules
|
component: Schedules,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`My View`),
|
title: i18n._(t`My View`),
|
||||||
path: '/portal',
|
path: '/portal',
|
||||||
component: Portal
|
component: Portal,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -120,27 +116,27 @@ export function main (render) {
|
|||||||
{
|
{
|
||||||
title: i18n._(t`Templates`),
|
title: i18n._(t`Templates`),
|
||||||
path: '/templates',
|
path: '/templates',
|
||||||
component: Templates
|
component: Templates,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Credentials`),
|
title: i18n._(t`Credentials`),
|
||||||
path: '/credentials',
|
path: '/credentials',
|
||||||
component: Credentials
|
component: Credentials,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Projects`),
|
title: i18n._(t`Projects`),
|
||||||
path: '/projects',
|
path: '/projects',
|
||||||
component: Projects
|
component: Projects,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Inventories`),
|
title: i18n._(t`Inventories`),
|
||||||
path: '/inventories',
|
path: '/inventories',
|
||||||
component: Inventories
|
component: Inventories,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Inventory Scripts`),
|
title: i18n._(t`Inventory Scripts`),
|
||||||
path: '/inventory_scripts',
|
path: '/inventory_scripts',
|
||||||
component: InventoryScripts
|
component: InventoryScripts,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -151,17 +147,17 @@ export function main (render) {
|
|||||||
{
|
{
|
||||||
title: i18n._(t`Organizations`),
|
title: i18n._(t`Organizations`),
|
||||||
path: '/organizations',
|
path: '/organizations',
|
||||||
component: Organizations
|
component: Organizations,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Users`),
|
title: i18n._(t`Users`),
|
||||||
path: '/users',
|
path: '/users',
|
||||||
component: Users
|
component: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Teams`),
|
title: i18n._(t`Teams`),
|
||||||
path: '/teams',
|
path: '/teams',
|
||||||
component: Teams
|
component: Teams,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -172,27 +168,27 @@ export function main (render) {
|
|||||||
{
|
{
|
||||||
title: i18n._(t`Credential Types`),
|
title: i18n._(t`Credential Types`),
|
||||||
path: '/credential_types',
|
path: '/credential_types',
|
||||||
component: CredentialTypes
|
component: CredentialTypes,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Notifications`),
|
title: i18n._(t`Notifications`),
|
||||||
path: '/notification_templates',
|
path: '/notification_templates',
|
||||||
component: NotificationTemplates
|
component: NotificationTemplates,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Management Jobs`),
|
title: i18n._(t`Management Jobs`),
|
||||||
path: '/management_jobs',
|
path: '/management_jobs',
|
||||||
component: ManagementJobs
|
component: ManagementJobs,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Instance Groups`),
|
title: i18n._(t`Instance Groups`),
|
||||||
path: '/instance_groups',
|
path: '/instance_groups',
|
||||||
component: InstanceGroups
|
component: InstanceGroups,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Integrations`),
|
title: i18n._(t`Integrations`),
|
||||||
path: '/applications',
|
path: '/applications',
|
||||||
component: Applications
|
component: Applications,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -203,34 +199,37 @@ export function main (render) {
|
|||||||
{
|
{
|
||||||
title: i18n._(t`Authentication`),
|
title: i18n._(t`Authentication`),
|
||||||
path: '/auth_settings',
|
path: '/auth_settings',
|
||||||
component: AuthSettings
|
component: AuthSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`Jobs`),
|
title: i18n._(t`Jobs`),
|
||||||
path: '/jobs_settings',
|
path: '/jobs_settings',
|
||||||
component: JobsSettings
|
component: JobsSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`System`),
|
title: i18n._(t`System`),
|
||||||
path: '/system_settings',
|
path: '/system_settings',
|
||||||
component: SystemSettings
|
component: SystemSettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`User Interface`),
|
title: i18n._(t`User Interface`),
|
||||||
path: '/ui_settings',
|
path: '/ui_settings',
|
||||||
component: UISettings
|
component: UISettings,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18n._(t`License`),
|
title: i18n._(t`License`),
|
||||||
path: '/license',
|
path: '/license',
|
||||||
component: License
|
component: License,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
render={({ routeGroups }) => (
|
render={({ routeGroups }) =>
|
||||||
routeGroups
|
routeGroups
|
||||||
.reduce((allRoutes, { routes }) => allRoutes.concat(routes), [])
|
.reduce(
|
||||||
|
(allRoutes, { routes }) => allRoutes.concat(routes),
|
||||||
|
[]
|
||||||
|
)
|
||||||
.map(({ component: PageComponent, path }) => (
|
.map(({ component: PageComponent, path }) => (
|
||||||
<Route
|
<Route
|
||||||
key={path}
|
key={path}
|
||||||
@@ -240,7 +239,7 @@ export function main (render) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -249,7 +248,8 @@ export function main (render) {
|
|||||||
</Background>
|
</Background>
|
||||||
)}
|
)}
|
||||||
</I18n>
|
</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
Reference in New Issue
Block a user