mirror of
https://github.com/ansible/awx.git
synced 2026-05-12 03:47:36 -02:30
Merge pull request #5366 from marshmalien/inv-hosts-list
Inventory Host List Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -14,6 +14,10 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
|||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readHosts(id, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/hosts/`, { params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Inventories;
|
export default Inventories;
|
||||||
|
|||||||
@@ -123,8 +123,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(1)
|
.at(1)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification success"]')
|
||||||
.at(1)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -151,8 +150,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(0)
|
.at(0)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification failure"]')
|
||||||
.at(2)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -180,8 +178,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(0)
|
.at(0)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification start"]')
|
||||||
.at(0)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.associateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -208,8 +205,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(0)
|
.at(0)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification success"]')
|
||||||
.at(1)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -236,8 +232,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(1)
|
.at(1)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification failure"]')
|
||||||
.at(2)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
@@ -264,8 +259,7 @@ describe('<NotificationList />', () => {
|
|||||||
const items = wrapper.find('NotificationListItem');
|
const items = wrapper.find('NotificationListItem');
|
||||||
items
|
items
|
||||||
.at(2)
|
.at(2)
|
||||||
.find('Switch')
|
.find('PFSwitch[aria-label="Toggle notification start"]')
|
||||||
.at(0)
|
|
||||||
.prop('onChange')();
|
.prop('onChange')();
|
||||||
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
expect(MockModelAPI.disassociateNotificationTemplate).toHaveBeenCalledWith(
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Switch as PFSwitch,
|
|
||||||
DataListItem,
|
DataListItem,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
DataListCell as PFDataListCell,
|
DataListCell as PFDataListCell,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import Switch from '@components/Switch';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -24,13 +24,6 @@ const DataListCell = styled(PFDataListCell)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Switch = styled(PFSwitch)`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: no-wrap;
|
|
||||||
/* workaround PF bug; used in calculating switch width: */
|
|
||||||
--pf-c-switch__toggle-icon--Offset: 0.125rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
function NotificationListItem(props) {
|
function NotificationListItem(props) {
|
||||||
const {
|
const {
|
||||||
canToggleNotifications,
|
canToggleNotifications,
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
.find('Switch')
|
.find('Switch[aria-label="Toggle notification start"]')
|
||||||
.first()
|
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'started');
|
||||||
@@ -108,8 +107,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
.find('Switch')
|
.find('Switch[aria-label="Toggle notification success"]')
|
||||||
.at(1)
|
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'success');
|
||||||
@@ -127,8 +125,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
.find('Switch')
|
.find('Switch[aria-label="Toggle notification success"]')
|
||||||
.at(1)
|
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'success');
|
||||||
@@ -146,8 +143,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
.find('Switch')
|
.find('Switch[aria-label="Toggle notification failure"]')
|
||||||
.at(2)
|
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'error');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, true, 'error');
|
||||||
@@ -165,8 +161,7 @@ describe('<NotificationListItem canToggleNotifications />', () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
wrapper
|
wrapper
|
||||||
.find('Switch')
|
.find('Switch[aria-label="Toggle notification failure"]')
|
||||||
.at(2)
|
|
||||||
.find('input')
|
.find('input')
|
||||||
.simulate('change');
|
.simulate('change');
|
||||||
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'error');
|
expect(toggleNotification).toHaveBeenCalledWith(9000, false, 'error');
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 dXsFLF"
|
className="pf-c-data-list__cell NotificationListItem__DataListCell-w674ng-0 dXsFLF"
|
||||||
righthalf="true"
|
righthalf="true"
|
||||||
>
|
>
|
||||||
<NotificationListItem__Switch
|
<Switch
|
||||||
aria-label="Toggle notification start"
|
aria-label="Toggle notification start"
|
||||||
id="notification-9000-started-toggle"
|
id="notification-9000-started-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
@@ -329,17 +329,17 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
"attrs": Array [],
|
"attrs": Array [],
|
||||||
"componentStyle": ComponentStyle {
|
"componentStyle": ComponentStyle {
|
||||||
"componentId": "NotificationListItem__Switch-w674ng-1",
|
"componentId": "Switch-sc-1xwas62-0",
|
||||||
"isStatic": true,
|
"isStatic": true,
|
||||||
"lastClassName": "hfzRow",
|
"lastClassName": "eJQXYh",
|
||||||
"rules": Array [
|
"rules": Array [
|
||||||
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"displayName": "NotificationListItem__Switch",
|
"displayName": "Switch",
|
||||||
"foldedComponentIds": Array [],
|
"foldedComponentIds": Array [],
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
|
"styledComponentId": "Switch-sc-1xwas62-0",
|
||||||
"target": [Function],
|
"target": [Function],
|
||||||
"toString": [Function],
|
"toString": [Function],
|
||||||
"warnTooManyClasses": [Function],
|
"warnTooManyClasses": [Function],
|
||||||
@@ -354,9 +354,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
labelOff="Start"
|
labelOff="Start"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
>
|
>
|
||||||
<Component
|
<PFSwitch
|
||||||
aria-label="Toggle notification start"
|
aria-label="Toggle notification start"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-started-toggle"
|
id="notification-9000-started-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -369,7 +369,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
componentProps={
|
componentProps={
|
||||||
Object {
|
Object {
|
||||||
"aria-label": "Toggle notification start",
|
"aria-label": "Toggle notification start",
|
||||||
"className": "NotificationListItem__Switch-w674ng-1 hfzRow",
|
"className": "Switch-sc-1xwas62-0 eJQXYh",
|
||||||
"id": "notification-9000-started-toggle",
|
"id": "notification-9000-started-toggle",
|
||||||
"isChecked": false,
|
"isChecked": false,
|
||||||
"isDisabled": false,
|
"isDisabled": false,
|
||||||
@@ -382,7 +382,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle notification start"
|
aria-label="Toggle notification start"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-started-toggle"
|
id="notification-9000-started-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -397,7 +397,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="pf-c-switch Switch-sc-1xwas62-0 eJQXYh"
|
||||||
htmlFor="notification-9000-started-toggle"
|
htmlFor="notification-9000-started-toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -430,10 +430,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
</label>
|
</label>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ComponentWithOuia>
|
</ComponentWithOuia>
|
||||||
</Component>
|
</PFSwitch>
|
||||||
</StyledComponent>
|
</StyledComponent>
|
||||||
</NotificationListItem__Switch>
|
</Switch>
|
||||||
<NotificationListItem__Switch
|
<Switch
|
||||||
aria-label="Toggle notification success"
|
aria-label="Toggle notification success"
|
||||||
id="notification-9000-success-toggle"
|
id="notification-9000-success-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
@@ -449,17 +449,17 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
"attrs": Array [],
|
"attrs": Array [],
|
||||||
"componentStyle": ComponentStyle {
|
"componentStyle": ComponentStyle {
|
||||||
"componentId": "NotificationListItem__Switch-w674ng-1",
|
"componentId": "Switch-sc-1xwas62-0",
|
||||||
"isStatic": true,
|
"isStatic": true,
|
||||||
"lastClassName": "hfzRow",
|
"lastClassName": "eJQXYh",
|
||||||
"rules": Array [
|
"rules": Array [
|
||||||
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"displayName": "NotificationListItem__Switch",
|
"displayName": "Switch",
|
||||||
"foldedComponentIds": Array [],
|
"foldedComponentIds": Array [],
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
|
"styledComponentId": "Switch-sc-1xwas62-0",
|
||||||
"target": [Function],
|
"target": [Function],
|
||||||
"toString": [Function],
|
"toString": [Function],
|
||||||
"warnTooManyClasses": [Function],
|
"warnTooManyClasses": [Function],
|
||||||
@@ -474,9 +474,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
labelOff="Success"
|
labelOff="Success"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
>
|
>
|
||||||
<Component
|
<PFSwitch
|
||||||
aria-label="Toggle notification success"
|
aria-label="Toggle notification success"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-success-toggle"
|
id="notification-9000-success-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -489,7 +489,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
componentProps={
|
componentProps={
|
||||||
Object {
|
Object {
|
||||||
"aria-label": "Toggle notification success",
|
"aria-label": "Toggle notification success",
|
||||||
"className": "NotificationListItem__Switch-w674ng-1 hfzRow",
|
"className": "Switch-sc-1xwas62-0 eJQXYh",
|
||||||
"id": "notification-9000-success-toggle",
|
"id": "notification-9000-success-toggle",
|
||||||
"isChecked": false,
|
"isChecked": false,
|
||||||
"isDisabled": false,
|
"isDisabled": false,
|
||||||
@@ -502,7 +502,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle notification success"
|
aria-label="Toggle notification success"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-success-toggle"
|
id="notification-9000-success-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -517,7 +517,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="pf-c-switch Switch-sc-1xwas62-0 eJQXYh"
|
||||||
htmlFor="notification-9000-success-toggle"
|
htmlFor="notification-9000-success-toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -550,10 +550,10 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
</label>
|
</label>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ComponentWithOuia>
|
</ComponentWithOuia>
|
||||||
</Component>
|
</PFSwitch>
|
||||||
</StyledComponent>
|
</StyledComponent>
|
||||||
</NotificationListItem__Switch>
|
</Switch>
|
||||||
<NotificationListItem__Switch
|
<Switch
|
||||||
aria-label="Toggle notification failure"
|
aria-label="Toggle notification failure"
|
||||||
id="notification-9000-error-toggle"
|
id="notification-9000-error-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
@@ -569,17 +569,17 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
"$$typeof": Symbol(react.forward_ref),
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
"attrs": Array [],
|
"attrs": Array [],
|
||||||
"componentStyle": ComponentStyle {
|
"componentStyle": ComponentStyle {
|
||||||
"componentId": "NotificationListItem__Switch-w674ng-1",
|
"componentId": "Switch-sc-1xwas62-0",
|
||||||
"isStatic": true,
|
"isStatic": true,
|
||||||
"lastClassName": "hfzRow",
|
"lastClassName": "eJQXYh",
|
||||||
"rules": Array [
|
"rules": Array [
|
||||||
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
"display:flex;flex-wrap:no-wrap;--pf-c-switch__toggle-icon--Offset:0.125rem;",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"displayName": "NotificationListItem__Switch",
|
"displayName": "Switch",
|
||||||
"foldedComponentIds": Array [],
|
"foldedComponentIds": Array [],
|
||||||
"render": [Function],
|
"render": [Function],
|
||||||
"styledComponentId": "NotificationListItem__Switch-w674ng-1",
|
"styledComponentId": "Switch-sc-1xwas62-0",
|
||||||
"target": [Function],
|
"target": [Function],
|
||||||
"toString": [Function],
|
"toString": [Function],
|
||||||
"warnTooManyClasses": [Function],
|
"warnTooManyClasses": [Function],
|
||||||
@@ -594,9 +594,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
labelOff="Failure"
|
labelOff="Failure"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
>
|
>
|
||||||
<Component
|
<PFSwitch
|
||||||
aria-label="Toggle notification failure"
|
aria-label="Toggle notification failure"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-error-toggle"
|
id="notification-9000-error-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -609,7 +609,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
componentProps={
|
componentProps={
|
||||||
Object {
|
Object {
|
||||||
"aria-label": "Toggle notification failure",
|
"aria-label": "Toggle notification failure",
|
||||||
"className": "NotificationListItem__Switch-w674ng-1 hfzRow",
|
"className": "Switch-sc-1xwas62-0 eJQXYh",
|
||||||
"id": "notification-9000-error-toggle",
|
"id": "notification-9000-error-toggle",
|
||||||
"isChecked": false,
|
"isChecked": false,
|
||||||
"isDisabled": false,
|
"isDisabled": false,
|
||||||
@@ -622,7 +622,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle notification failure"
|
aria-label="Toggle notification failure"
|
||||||
className="NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="Switch-sc-1xwas62-0 eJQXYh"
|
||||||
id="notification-9000-error-toggle"
|
id="notification-9000-error-toggle"
|
||||||
isChecked={false}
|
isChecked={false}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
@@ -637,7 +637,7 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="pf-c-switch NotificationListItem__Switch-w674ng-1 hfzRow"
|
className="pf-c-switch Switch-sc-1xwas62-0 eJQXYh"
|
||||||
htmlFor="notification-9000-error-toggle"
|
htmlFor="notification-9000-error-toggle"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -670,9 +670,9 @@ exports[`<NotificationListItem canToggleNotifications /> initially renders succe
|
|||||||
</label>
|
</label>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ComponentWithOuia>
|
</ComponentWithOuia>
|
||||||
</Component>
|
</PFSwitch>
|
||||||
</StyledComponent>
|
</StyledComponent>
|
||||||
</NotificationListItem__Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</DataListCell>
|
</DataListCell>
|
||||||
</StyledComponent>
|
</StyledComponent>
|
||||||
|
|||||||
10
awx/ui_next/src/components/Switch/Switch.jsx
Normal file
10
awx/ui_next/src/components/Switch/Switch.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Switch } from '@patternfly/react-core';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
Switch.displayName = 'PFSwitch';
|
||||||
|
export default styled(Switch)`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: no-wrap;
|
||||||
|
/* workaround PF bug; used in calculating switch width: */
|
||||||
|
--pf-c-switch__toggle-icon--Offset: 0.125rem;
|
||||||
|
`;
|
||||||
1
awx/ui_next/src/components/Switch/index.js
Normal file
1
awx/ui_next/src/components/Switch/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './Switch';
|
||||||
@@ -35,7 +35,7 @@ class HostsList extends Component {
|
|||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
actions: null,
|
actions: null,
|
||||||
toggleError: false,
|
toggleError: false,
|
||||||
toggleLoading: false,
|
toggleLoading: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleSelectAll = this.handleSelectAll.bind(this);
|
this.handleSelectAll = this.handleSelectAll.bind(this);
|
||||||
@@ -101,7 +101,7 @@ class HostsList extends Component {
|
|||||||
|
|
||||||
async handleHostToggle(hostToToggle) {
|
async handleHostToggle(hostToToggle) {
|
||||||
const { hosts } = this.state;
|
const { hosts } = this.state;
|
||||||
this.setState({ toggleLoading: true });
|
this.setState({ toggleLoading: hostToToggle.id });
|
||||||
try {
|
try {
|
||||||
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
||||||
enabled: !hostToToggle.enabled,
|
enabled: !hostToToggle.enabled,
|
||||||
@@ -114,7 +114,7 @@ class HostsList extends Component {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ toggleError: true });
|
this.setState({ toggleError: true });
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ toggleLoading: false });
|
this.setState({ toggleLoading: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ class HostsList extends Component {
|
|||||||
isSelected={selected.some(row => row.id === o.id)}
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
onSelect={() => this.handleSelect(o)}
|
onSelect={() => this.handleSelect(o)}
|
||||||
toggleHost={this.handleHostToggle}
|
toggleHost={this.handleHostToggle}
|
||||||
toggleLoading={toggleLoading}
|
toggleLoading={toggleLoading === o.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
DataListItem,
|
DataListItem,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
Switch as PFSwitch,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@@ -17,18 +16,10 @@ import DataListCell from '@components/DataListCell';
|
|||||||
import DataListCheck from '@components/DataListCheck';
|
import DataListCheck from '@components/DataListCheck';
|
||||||
import ListActionButton from '@components/ListActionButton';
|
import ListActionButton from '@components/ListActionButton';
|
||||||
import { Sparkline } from '@components/Sparkline';
|
import { Sparkline } from '@components/Sparkline';
|
||||||
|
import Switch from '@components/Switch';
|
||||||
import VerticalSeparator from '@components/VerticalSeparator';
|
import VerticalSeparator from '@components/VerticalSeparator';
|
||||||
import { Host } from '@types';
|
import { Host } from '@types';
|
||||||
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const Switch = styled(PFSwitch)`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: no-wrap;
|
|
||||||
/* workaround PF bug; used in calculating switch width: */
|
|
||||||
--pf-c-switch__toggle-icon--Offset: 0.125rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
class HostListItem extends React.Component {
|
class HostListItem extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
host: Host.isRequired,
|
host: Host.isRequired,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './HostList';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { t } from '@lingui/macro';
|
|||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
|
||||||
|
|
||||||
import HostsList from './HostList/HostList';
|
import HostList from './HostList';
|
||||||
import HostAdd from './HostAdd/HostAdd';
|
import HostAdd from './HostAdd';
|
||||||
import Host from './Host';
|
import Host from './Host';
|
||||||
|
|
||||||
class Hosts extends Component {
|
class Hosts extends Component {
|
||||||
@@ -69,7 +69,7 @@ class Hosts extends Component {
|
|||||||
</Config>
|
</Config>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Route path={`${match.path}`} render={() => <HostsList />} />
|
<Route path={`${match.path}`} render={() => <HostList />} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ class Inventories extends Component {
|
|||||||
t`Completed Jobs`
|
t`Completed Jobs`
|
||||||
),
|
),
|
||||||
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts`]: i18n._(t`Hosts`),
|
||||||
|
[`/inventories/${inventoryKind}/${inventory.id}/hosts/add`]: i18n._(
|
||||||
|
t`Create New Host`
|
||||||
|
),
|
||||||
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
[`/inventories/inventory/${inventory.id}/sources`]: i18n._(t`Sources`),
|
||||||
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
[`/inventories/inventory/${inventory.id}/groups`]: i18n._(t`Groups`),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
||||||
@@ -9,184 +9,159 @@ import RoutedTabs from '@components/RoutedTabs';
|
|||||||
import { ResourceAccessList } from '@components/ResourceAccessList';
|
import { ResourceAccessList } from '@components/ResourceAccessList';
|
||||||
import InventoryDetail from './InventoryDetail';
|
import InventoryDetail from './InventoryDetail';
|
||||||
import InventoryHosts from './InventoryHosts';
|
import InventoryHosts from './InventoryHosts';
|
||||||
|
import InventoryHostAdd from './InventoryHostAdd';
|
||||||
import InventoryGroups from './InventoryGroups';
|
import InventoryGroups from './InventoryGroups';
|
||||||
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
import InventoryCompletedJobs from './InventoryCompletedJobs';
|
||||||
import InventorySources from './InventorySources';
|
import InventorySources from './InventorySources';
|
||||||
import { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
import InventoryEdit from './InventoryEdit';
|
import InventoryEdit from './InventoryEdit';
|
||||||
|
|
||||||
class Inventory extends Component {
|
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||||
constructor(props) {
|
const [contentError, setContentError] = useState(null);
|
||||||
super(props);
|
const [hasContentLoading, setHasContentLoading] = useState(true);
|
||||||
|
const [inventory, setInventory] = useState(null);
|
||||||
|
|
||||||
this.state = {
|
useEffect(() => {
|
||||||
contentError: null,
|
async function fetchData() {
|
||||||
hasContentLoading: true,
|
try {
|
||||||
inventory: null,
|
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
||||||
};
|
setBreadcrumb(data);
|
||||||
this.loadInventory = this.loadInventory.bind(this);
|
setInventory(data);
|
||||||
}
|
} catch (error) {
|
||||||
|
setContentError(error);
|
||||||
async componentDidMount() {
|
} finally {
|
||||||
await this.loadInventory();
|
setHasContentLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate(prevProps) {
|
|
||||||
const { location, match } = this.props;
|
|
||||||
const url = `/inventories/inventory/${match.params.id}/`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.location.pathname.startsWith(url) &&
|
|
||||||
prevProps.location !== location &&
|
|
||||||
location.pathname === `${url}details`
|
|
||||||
) {
|
|
||||||
await this.loadInventory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInventory() {
|
|
||||||
const { setBreadcrumb, match } = this.props;
|
|
||||||
const { id } = match.params;
|
|
||||||
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
|
||||||
try {
|
|
||||||
const { data } = await InventoriesAPI.readDetail(id);
|
|
||||||
setBreadcrumb(data);
|
|
||||||
this.setState({ inventory: data });
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ contentError: err });
|
|
||||||
} finally {
|
|
||||||
this.setState({ hasContentLoading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { history, i18n, location, match } = this.props;
|
|
||||||
const { contentError, hasContentLoading, inventory } = this.state;
|
|
||||||
|
|
||||||
const tabsArray = [
|
|
||||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
|
||||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
|
||||||
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
|
||||||
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 3 },
|
|
||||||
{ name: i18n._(t`Sources`), link: `${match.url}/sources`, id: 4 },
|
|
||||||
{
|
|
||||||
name: i18n._(t`Completed Jobs`),
|
|
||||||
link: `${match.url}/completed_jobs`,
|
|
||||||
id: 5,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let cardHeader = hasContentLoading ? null : (
|
|
||||||
<CardHeader style={{ padding: 0 }}>
|
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
|
||||||
<CardCloseButton linkTo="/inventories" />
|
|
||||||
</CardHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (location.pathname.endsWith('edit')) {
|
|
||||||
cardHeader = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
fetchData();
|
||||||
return (
|
}, [match.params.id, setBreadcrumb]);
|
||||||
<PageSection>
|
|
||||||
<Card className="awx-c-card">
|
|
||||||
<ContentError error={contentError}>
|
|
||||||
{contentError.response.status === 404 && (
|
|
||||||
<span>
|
|
||||||
{i18n._(`Inventory not found.`)}{' '}
|
|
||||||
<Link to="/inventories">
|
|
||||||
{i18n._(`View all Inventories.`)}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
||||||
|
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
||||||
|
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
||||||
|
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 3 },
|
||||||
|
{ name: i18n._(t`Sources`), link: `${match.url}/sources`, id: 4 },
|
||||||
|
{
|
||||||
|
name: i18n._(t`Completed Jobs`),
|
||||||
|
link: `${match.url}/completed_jobs`,
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let cardHeader = hasContentLoading ? null : (
|
||||||
|
<CardHeader style={{ padding: 0 }}>
|
||||||
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
|
<CardCloseButton linkTo="/inventories" />
|
||||||
|
</CardHeader>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (location.pathname.endsWith('edit') || location.pathname.endsWith('add')) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
{cardHeader}
|
<ContentError error={contentError}>
|
||||||
<Switch>
|
{contentError.response.status === 404 && (
|
||||||
<Redirect
|
<span>
|
||||||
from="/inventories/inventory/:id"
|
{i18n._(`Inventory not found.`)}{' '}
|
||||||
to="/inventories/inventory/:id/details"
|
<Link to="/inventories">{i18n._(`View all Inventories.`)}</Link>
|
||||||
exact
|
</span>
|
||||||
/>
|
)}
|
||||||
{inventory && [
|
</ContentError>
|
||||||
<Route
|
|
||||||
key="details"
|
|
||||||
path="/inventories/inventory/:id/details"
|
|
||||||
render={() => (
|
|
||||||
<InventoryDetail
|
|
||||||
match={match}
|
|
||||||
hasInventoryLoading={hasContentLoading}
|
|
||||||
inventory={inventory}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="edit"
|
|
||||||
path="/inventories/inventory/:id/edit"
|
|
||||||
render={() => <InventoryEdit inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="access"
|
|
||||||
path="/inventories/inventory/:id/access"
|
|
||||||
render={() => (
|
|
||||||
<ResourceAccessList
|
|
||||||
resource={inventory}
|
|
||||||
apiModel={InventoriesAPI}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="groups"
|
|
||||||
path="/inventories/inventory/:id/groups"
|
|
||||||
render={() => <InventoryGroups inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="hosts"
|
|
||||||
path="/inventories/inventory/:id/hosts"
|
|
||||||
render={() => <InventoryHosts inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="sources"
|
|
||||||
path="/inventories/inventory/:id/sources"
|
|
||||||
render={() => <InventorySources inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="completed_jobs"
|
|
||||||
path="/inventories/inventory/:id/completed_jobs"
|
|
||||||
render={() => <InventoryCompletedJobs inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="not-found"
|
|
||||||
path="*"
|
|
||||||
render={() =>
|
|
||||||
!hasContentLoading && (
|
|
||||||
<ContentError isNotFound>
|
|
||||||
{match.params.id && (
|
|
||||||
<Link
|
|
||||||
to={`/inventories/inventory/${match.params.id}/details`}
|
|
||||||
>
|
|
||||||
{i18n._(`View Inventory Details`)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
{cardHeader}
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/inventories/inventory/:id"
|
||||||
|
to="/inventories/inventory/:id/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{inventory && [
|
||||||
|
<Route
|
||||||
|
key="details"
|
||||||
|
path="/inventories/inventory/:id/details"
|
||||||
|
render={() => (
|
||||||
|
<InventoryDetail
|
||||||
|
match={match}
|
||||||
|
hasInventoryLoading={hasContentLoading}
|
||||||
|
inventory={inventory}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="edit"
|
||||||
|
path="/inventories/inventory/:id/edit"
|
||||||
|
render={() => <InventoryEdit inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="host-add"
|
||||||
|
path="/inventories/inventory/:id/hosts/add"
|
||||||
|
render={() => <InventoryHostAdd />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="access"
|
||||||
|
path="/inventories/inventory/:id/access"
|
||||||
|
render={() => (
|
||||||
|
<ResourceAccessList
|
||||||
|
resource={inventory}
|
||||||
|
apiModel={InventoriesAPI}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="groups"
|
||||||
|
path="/inventories/inventory/:id/groups"
|
||||||
|
render={() => <InventoryGroups inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="hosts"
|
||||||
|
path="/inventories/inventory/:id/hosts"
|
||||||
|
render={() => <InventoryHosts />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="sources"
|
||||||
|
path="/inventories/inventory/:id/sources"
|
||||||
|
render={() => <InventorySources inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="completed_jobs"
|
||||||
|
path="/inventories/inventory/:id/completed_jobs"
|
||||||
|
render={() => <InventoryCompletedJobs inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="not-found"
|
||||||
|
path="*"
|
||||||
|
render={() =>
|
||||||
|
!hasContentLoading && (
|
||||||
|
<ContentError isNotFound>
|
||||||
|
{match.params.id && (
|
||||||
|
<Link
|
||||||
|
to={`/inventories/inventory/${match.params.id}/details`}
|
||||||
|
>
|
||||||
|
{i18n._(`View Inventory Details`)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Inventory as _Inventory };
|
export { Inventory as _Inventory };
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
@@ -12,41 +13,38 @@ InventoriesAPI.readDetail.mockResolvedValue({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('<Inventory />', () => {
|
describe('<Inventory />', () => {
|
||||||
test('initially renders succesfully', async done => {
|
let wrapper;
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<Inventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
test('initially renders succesfully', async () => {
|
||||||
);
|
await act(async () => {
|
||||||
await waitForElement(
|
wrapper = mountWithContexts(
|
||||||
wrapper,
|
<Inventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
||||||
'Inventory',
|
);
|
||||||
el => el.state('hasContentLoading') === true
|
});
|
||||||
);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'Inventory',
|
|
||||||
el => el.state('hasContentLoading') === false
|
|
||||||
);
|
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 6);
|
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 6);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/inventories/inventory/1/foobar'],
|
initialEntries: ['/inventories/inventory/1/foobar'],
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />, {
|
await act(async () => {
|
||||||
context: {
|
wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />, {
|
||||||
router: {
|
context: {
|
||||||
history,
|
router: {
|
||||||
route: {
|
history,
|
||||||
location: history.location,
|
route: {
|
||||||
match: {
|
location: history.location,
|
||||||
params: { id: 1 },
|
match: {
|
||||||
url: '/inventories/inventory/1/foobar',
|
params: { id: 1 },
|
||||||
path: '/inventories/inventory/1/foobar',
|
url: '/inventories/inventory/1/foobar',
|
||||||
|
path: '/inventories/inventory/1/foobar',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CardBody } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
function InventoryHostAdd() {
|
||||||
|
return <CardBody>Coming soon :)</CardBody>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InventoryHostAdd;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './InventoryHostAdd';
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { string, bool, func } from 'prop-types';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
DataListItem,
|
||||||
|
DataListItemRow,
|
||||||
|
DataListItemCells,
|
||||||
|
Tooltip,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { PencilAltIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
import ActionButtonCell from '@components/ActionButtonCell';
|
||||||
|
import DataListCell from '@components/DataListCell';
|
||||||
|
import DataListCheck from '@components/DataListCheck';
|
||||||
|
import ListActionButton from '@components/ListActionButton';
|
||||||
|
import { Sparkline } from '@components/Sparkline';
|
||||||
|
import Switch from '@components/Switch';
|
||||||
|
import VerticalSeparator from '@components/VerticalSeparator';
|
||||||
|
import { Host } from '@types';
|
||||||
|
|
||||||
|
function InventoryHostItem(props) {
|
||||||
|
const {
|
||||||
|
detailUrl,
|
||||||
|
host,
|
||||||
|
i18n,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
toggleHost,
|
||||||
|
toggleLoading,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const labelId = `check-action-${host.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataListItem key={host.id} aria-labelledby={labelId}>
|
||||||
|
<DataListItemRow>
|
||||||
|
<DataListCheck
|
||||||
|
id={`select-host-${host.id}`}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={onSelect}
|
||||||
|
aria-labelledby={labelId}
|
||||||
|
/>
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell key="divider">
|
||||||
|
<VerticalSeparator />
|
||||||
|
<Link to={`${detailUrl}`}>
|
||||||
|
<b>{host.name}</b>
|
||||||
|
</Link>
|
||||||
|
</DataListCell>,
|
||||||
|
<DataListCell key="recentJobs">
|
||||||
|
<Sparkline jobs={host.summary_fields.recent_jobs} />
|
||||||
|
</DataListCell>,
|
||||||
|
<ActionButtonCell lastcolumn="true" key="action">
|
||||||
|
<Tooltip
|
||||||
|
content={i18n._(
|
||||||
|
t`Indicates if a host is available and should be included
|
||||||
|
in running jobs. For hosts that are part of an external
|
||||||
|
inventory, this may be reset by the inventory sync process.`
|
||||||
|
)}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
id={`host-${host.id}-toggle`}
|
||||||
|
label={i18n._(t`On`)}
|
||||||
|
labelOff={i18n._(t`Off`)}
|
||||||
|
isChecked={host.enabled}
|
||||||
|
isDisabled={
|
||||||
|
toggleLoading || !host.summary_fields.user_capabilities.edit
|
||||||
|
}
|
||||||
|
onChange={() => toggleHost(host)}
|
||||||
|
aria-label={i18n._(t`Toggle host`)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{host.summary_fields.user_capabilities.edit && (
|
||||||
|
<Tooltip content={i18n._(t`Edit Host`)} position="top">
|
||||||
|
<ListActionButton
|
||||||
|
variant="plain"
|
||||||
|
component={Link}
|
||||||
|
to={`/hosts/${host.id}/edit`}
|
||||||
|
>
|
||||||
|
<PencilAltIcon />
|
||||||
|
</ListActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</ActionButtonCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
InventoryHostItem.propTypes = {
|
||||||
|
detailUrl: string.isRequired,
|
||||||
|
host: Host.isRequired,
|
||||||
|
isSelected: bool.isRequired,
|
||||||
|
onSelect: func.isRequired,
|
||||||
|
toggleHost: func.isRequired,
|
||||||
|
toggleLoading: bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(InventoryHostItem);
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import InventoryHostItem from './InventoryHostItem';
|
||||||
|
|
||||||
|
let toggleHost;
|
||||||
|
|
||||||
|
const mockHost = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Host 1',
|
||||||
|
url: '/api/v2/hosts/1',
|
||||||
|
inventory: 1,
|
||||||
|
summary_fields: {
|
||||||
|
inventory: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Inv 1',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe.only('<InventoryHostItem />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
toggleHost = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit button shown to users with edit capabilities', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<InventoryHostItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl="/host/1"
|
||||||
|
onSelect={() => {}}
|
||||||
|
host={mockHost}
|
||||||
|
toggleHost={toggleHost}
|
||||||
|
toggleLoading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PencilAltIcon').exists()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edit button hidden from users without edit capabilities', () => {
|
||||||
|
const copyMockHost = Object.assign({}, mockHost);
|
||||||
|
copyMockHost.summary_fields.user_capabilities.edit = false;
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<InventoryHostItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl="/host/1"
|
||||||
|
onSelect={() => {}}
|
||||||
|
host={copyMockHost}
|
||||||
|
toggleHost={toggleHost}
|
||||||
|
toggleLoading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles toggle click when host is enabled', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<InventoryHostItem
|
||||||
|
isSelected={false}
|
||||||
|
detailUrl="/host/1"
|
||||||
|
onSelect={() => {}}
|
||||||
|
host={mockHost}
|
||||||
|
toggleHost={toggleHost}
|
||||||
|
toggleLoading={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
wrapper
|
||||||
|
.find('Switch')
|
||||||
|
.first()
|
||||||
|
.find('input')
|
||||||
|
.simulate('change');
|
||||||
|
expect(toggleHost).toHaveBeenCalledWith(mockHost);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,225 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { CardBody } from '@patternfly/react-core';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { getQSConfig, parseQueryString } from '@util/qs';
|
||||||
|
import { InventoriesAPI, HostsAPI } from '@api';
|
||||||
|
|
||||||
class InventoryHosts extends Component {
|
import AlertModal from '@components/AlertModal';
|
||||||
render() {
|
import DataListToolbar from '@components/DataListToolbar';
|
||||||
return <CardBody>Coming soon :)</CardBody>;
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
}
|
import PaginatedDataList, {
|
||||||
|
ToolbarAddButton,
|
||||||
|
ToolbarDeleteButton,
|
||||||
|
} from '@components/PaginatedDataList';
|
||||||
|
import InventoryHostItem from './InventoryHostItem';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('host', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 20,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function InventoryHosts({ i18n, location, match }) {
|
||||||
|
const [actions, setActions] = useState(null);
|
||||||
|
const [contentError, setContentError] = useState(null);
|
||||||
|
const [deletionError, setDeletionError] = useState(null);
|
||||||
|
const [hostCount, setHostCount] = useState(0);
|
||||||
|
const [hosts, setHosts] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [toggleError, setToggleError] = useState(null);
|
||||||
|
const [toggleLoading, setToggleLoading] = useState(null);
|
||||||
|
|
||||||
|
const fetchHosts = (id, queryString) => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, queryString);
|
||||||
|
return InventoriesAPI.readHosts(id, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
data: { count, results },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: { actions: optionActions },
|
||||||
|
},
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchHosts(match.params.id, location.search),
|
||||||
|
InventoriesAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setHosts(results);
|
||||||
|
setHostCount(count);
|
||||||
|
setActions(optionActions);
|
||||||
|
} catch (error) {
|
||||||
|
setContentError(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [match.params.id, location]);
|
||||||
|
|
||||||
|
const handleSelectAll = isSelected => {
|
||||||
|
setSelected(isSelected ? [...hosts] : []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = row => {
|
||||||
|
if (selected.some(s => s.id === row.id)) {
|
||||||
|
setSelected(selected.filter(s => s.id !== row.id));
|
||||||
|
} else {
|
||||||
|
setSelected(selected.concat(row));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(selected.map(host => HostsAPI.destroy(host.id)));
|
||||||
|
} catch (error) {
|
||||||
|
setDeletionError(error);
|
||||||
|
} finally {
|
||||||
|
setSelected([]);
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { count, results },
|
||||||
|
} = await fetchHosts(match.params.id, location.search);
|
||||||
|
|
||||||
|
setHosts(results);
|
||||||
|
setHostCount(count);
|
||||||
|
} catch (error) {
|
||||||
|
setContentError(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = async hostToToggle => {
|
||||||
|
setToggleLoading(hostToToggle.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: updatedHost } = await HostsAPI.update(hostToToggle.id, {
|
||||||
|
enabled: !hostToToggle.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
setHosts(
|
||||||
|
hosts.map(host => (host.id === updatedHost.id ? updatedHost : host))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setToggleError(error);
|
||||||
|
} finally {
|
||||||
|
setToggleLoading(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAdd =
|
||||||
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const isAllSelected = selected.length > 0 && selected.length === hosts.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PaginatedDataList
|
||||||
|
contentError={contentError}
|
||||||
|
hasContentLoading={isLoading}
|
||||||
|
items={hosts}
|
||||||
|
itemCount={hostCount}
|
||||||
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
toolbarColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
isSortable: true,
|
||||||
|
isSearchable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Modified`),
|
||||||
|
key: 'modified',
|
||||||
|
isSortable: true,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n._(t`Created`),
|
||||||
|
key: 'created',
|
||||||
|
isSortable: true,
|
||||||
|
isNumeric: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderToolbar={props => (
|
||||||
|
<DataListToolbar
|
||||||
|
{...props}
|
||||||
|
showSelectAll
|
||||||
|
isAllSelected={isAllSelected}
|
||||||
|
onSelectAll={handleSelectAll}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
additionalControls={[
|
||||||
|
<ToolbarDeleteButton
|
||||||
|
key="delete"
|
||||||
|
onDelete={handleDelete}
|
||||||
|
itemsToDelete={selected}
|
||||||
|
pluralizedItemName={i18n._(t`Hosts`)}
|
||||||
|
/>,
|
||||||
|
canAdd && (
|
||||||
|
<ToolbarAddButton
|
||||||
|
key="add"
|
||||||
|
linkTo={`/inventories/inventory/${match.params.id}/hosts/add`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderItem={o => (
|
||||||
|
<InventoryHostItem
|
||||||
|
key={o.id}
|
||||||
|
host={o}
|
||||||
|
detailUrl={`/hosts/${o.id}/details`}
|
||||||
|
isSelected={selected.some(row => row.id === o.id)}
|
||||||
|
onSelect={() => handleSelect(o)}
|
||||||
|
toggleHost={handleToggle}
|
||||||
|
toggleLoading={toggleLoading === o.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
emptyStateControls={
|
||||||
|
canAdd && (
|
||||||
|
<ToolbarAddButton
|
||||||
|
key="add"
|
||||||
|
linkTo={`/inventories/inventory/${match.params.id}/add`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{toggleError && !toggleLoading && (
|
||||||
|
<AlertModal
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
isOpen={toggleError && !toggleLoading}
|
||||||
|
onClose={() => setToggleError(false)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to toggle host.`)}
|
||||||
|
<ErrorDetail error={toggleError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{deletionError && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={deletionError}
|
||||||
|
variant="danger"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={() => setDeletionError(null)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete one or more hosts.`)}
|
||||||
|
<ErrorDetail error={deletionError} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InventoryHosts;
|
export default withI18n()(withRouter(InventoryHosts));
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { InventoriesAPI, HostsAPI } from '@api';
|
||||||
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
|
import InventoryHosts from './InventoryHosts';
|
||||||
|
import mockInventory from '../shared/data.inventory.json';
|
||||||
|
|
||||||
|
jest.mock('@api');
|
||||||
|
|
||||||
|
const mockHosts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Host 1',
|
||||||
|
url: '/api/v2/hosts/1',
|
||||||
|
inventory: 1,
|
||||||
|
enabled: true,
|
||||||
|
summary_fields: {
|
||||||
|
inventory: {
|
||||||
|
id: 1,
|
||||||
|
name: 'inv 1',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
delete: true,
|
||||||
|
update: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Host 2',
|
||||||
|
url: '/api/v2/hosts/2',
|
||||||
|
inventory: 1,
|
||||||
|
enabled: true,
|
||||||
|
summary_fields: {
|
||||||
|
inventory: {
|
||||||
|
id: 1,
|
||||||
|
name: 'inv 1',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
update: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Host 3',
|
||||||
|
url: '/api/v2/hosts/3',
|
||||||
|
inventory: 1,
|
||||||
|
enabled: true,
|
||||||
|
summary_fields: {
|
||||||
|
inventory: {
|
||||||
|
id: 1,
|
||||||
|
name: 'inv 1',
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
delete: false,
|
||||||
|
update: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('<InventoryHosts />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
InventoriesAPI.readHosts.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: mockHosts.length,
|
||||||
|
results: mockHosts,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryHosts />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initially renders successfully', () => {
|
||||||
|
expect(wrapper.find('InventoryHosts').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fetch hosts from api and render them in the list', async () => {
|
||||||
|
expect(InventoriesAPI.readHosts).toHaveBeenCalled();
|
||||||
|
expect(wrapper.find('InventoryHostItem').length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should check and uncheck the row item', async () => {
|
||||||
|
expect(
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
expect(
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').props().checked
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should check all row items when select all is checked', async () => {
|
||||||
|
wrapper.find('PFDataListCheck').forEach(el => {
|
||||||
|
expect(el.props().checked).toBe(false);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Checkbox#select-all').invoke('onChange')(true);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find('PFDataListCheck').forEach(el => {
|
||||||
|
expect(el.props().checked).toBe(true);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Checkbox#select-all').invoke('onChange')(false);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
wrapper.find('PFDataListCheck').forEach(el => {
|
||||||
|
expect(el.props().checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api if host toggle is clicked', async () => {
|
||||||
|
HostsAPI.update.mockResolvedValueOnce({
|
||||||
|
data: { ...mockHosts[1], enabled: false },
|
||||||
|
});
|
||||||
|
expect(wrapper.find('PFSwitch[id="host-2-toggle"]').props().isChecked).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFSwitch[id="host-2-toggle"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('PFSwitch[id="host-2-toggle"]').props().isChecked).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(HostsAPI.update).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error modal if host is not successfully toggled', async () => {
|
||||||
|
HostsAPI.update.mockImplementationOnce(() => Promise.reject(new Error()));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFSwitch[id="host-2-toggle"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Modal',
|
||||||
|
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'Modal', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete button is disabled if user does not have delete capabilities on a selected host', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-3"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ToolbarDeleteButton button').props().disabled).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call api delete hosts for each selected host', async () => {
|
||||||
|
HostsAPI.destroy = jest.fn();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(HostsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show error modal when host is not successfully deleted from api', async () => {
|
||||||
|
HostsAPI.destroy.mockRejectedValue(
|
||||||
|
new Error({
|
||||||
|
response: {
|
||||||
|
config: {
|
||||||
|
method: 'delete',
|
||||||
|
url: '/api/v2/hosts/1',
|
||||||
|
},
|
||||||
|
data: 'An error occurred',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Modal',
|
||||||
|
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'Modal', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error if hosts are not successfully fetched from api', async () => {
|
||||||
|
InventoriesAPI.readHosts.mockImplementation(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('PFDataListCheck[id="select-host-1"]').invoke('onChange')();
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('ToolbarDeleteButton').invoke('onDelete')();
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show Add button for users with ability to POST', async () => {
|
||||||
|
expect(wrapper.find('ToolbarAddButton').length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide Add button for users without ability to POST', async () => {
|
||||||
|
InventoriesAPI.readOptions.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('ToolbarAddButton').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when api throws error on initial render', async () => {
|
||||||
|
InventoriesAPI.readOptions.mockImplementation(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user