mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 23:51:09 +03:00
Add job event summary toolbar
This commit is contained in:
parent
6df00e1e4c
commit
b00249b515
@ -5,17 +5,24 @@ import { Button } from '@patternfly/react-core';
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { CardActionsRow } from '@components/Card';
|
||||
|
||||
function DeleteButton({ onConfirm, modalTitle, name, i18n }) {
|
||||
function DeleteButton({
|
||||
onConfirm,
|
||||
modalTitle,
|
||||
name,
|
||||
i18n,
|
||||
variant,
|
||||
children,
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="danger"
|
||||
variant={variant || 'danger'}
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
{children || i18n._(t`Delete`)}
|
||||
</Button>
|
||||
<AlertModal
|
||||
isOpen={isOpen}
|
||||
|
@ -5,7 +5,7 @@ const Separator = styled.span`
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
margin-right: 27px;
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
background-color: #d7d7d7;
|
||||
vertical-align: middle;
|
||||
|
@ -1,4 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
AutoSizer,
|
||||
@ -8,18 +11,39 @@ import {
|
||||
List,
|
||||
} from 'react-virtualized';
|
||||
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import { CardBody } from '@components/Card';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
|
||||
import JobEvent from './JobEvent';
|
||||
import JobEventSkeleton from './JobEventSkeleton';
|
||||
import PageControls from './PageControls';
|
||||
import HostEventModal from './HostEventModal';
|
||||
import { HostStatusBar } from './shared';
|
||||
import { JobsAPI } from '@api';
|
||||
import { HostStatusBar, OutputToolbar } from './shared';
|
||||
import {
|
||||
JobsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
SystemJobsAPI,
|
||||
WorkflowJobsAPI,
|
||||
InventoriesAPI,
|
||||
AdHocCommandsAPI,
|
||||
} from '@api';
|
||||
|
||||
const HeaderTitle = styled.div`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
h1 {
|
||||
margin-left: 10px;
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
}
|
||||
`;
|
||||
|
||||
const OutputHeader = styled.div`
|
||||
font-weight: var(--pf-global--FontWeight--bold);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const OutputWrapper = styled.div`
|
||||
@ -31,6 +55,7 @@ const OutputWrapper = styled.div`
|
||||
height: calc(100vh - 350px);
|
||||
outline: 1px solid #d7d7d7;
|
||||
`;
|
||||
|
||||
const OutputFooter = styled.div`
|
||||
background-color: #ebebeb;
|
||||
border-right: 1px solid #d7d7d7;
|
||||
@ -52,6 +77,7 @@ class JobOutput extends Component {
|
||||
this.listRef = React.createRef();
|
||||
this.state = {
|
||||
contentError: null,
|
||||
deletionError: null,
|
||||
hasContentLoading: true,
|
||||
results: {},
|
||||
currentlyLoading: [],
|
||||
@ -67,6 +93,7 @@ class JobOutput extends Component {
|
||||
|
||||
this._isMounted = false;
|
||||
this.loadJobEvents = this.loadJobEvents.bind(this);
|
||||
this.handleDeleteJob = this.handleDeleteJob.bind(this);
|
||||
this.rowRenderer = this.rowRenderer.bind(this);
|
||||
this.handleHostEventClick = this.handleHostEventClick.bind(this);
|
||||
this.handleHostModalClose = this.handleHostModalClose.bind(this);
|
||||
@ -143,6 +170,34 @@ class JobOutput extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async handleDeleteJob() {
|
||||
const { job, history } = this.props;
|
||||
try {
|
||||
switch (job.type) {
|
||||
case 'project_update':
|
||||
await ProjectUpdatesAPI.destroy(job.idd);
|
||||
break;
|
||||
case 'system_job':
|
||||
await SystemJobsAPI.destroy(job.id);
|
||||
break;
|
||||
case 'workflow_job':
|
||||
await WorkflowJobsAPI.destroy(job.id);
|
||||
break;
|
||||
case 'ad_hoc_command':
|
||||
await AdHocCommandsAPI.destroy(job.id);
|
||||
break;
|
||||
case 'inventory_update':
|
||||
await InventoriesAPI.destroy(job.id);
|
||||
break;
|
||||
default:
|
||||
await JobsAPI.destroy(job.id);
|
||||
}
|
||||
history.push('/jobs');
|
||||
} catch (err) {
|
||||
this.setState({ deletionError: err });
|
||||
}
|
||||
}
|
||||
|
||||
isRowLoaded({ index }) {
|
||||
const { results, currentlyLoading } = this.state;
|
||||
if (results[index]) {
|
||||
@ -279,9 +334,11 @@ class JobOutput extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { job } = this.props;
|
||||
const { job, i18n } = this.props;
|
||||
|
||||
const {
|
||||
contentError,
|
||||
deletionError,
|
||||
hasContentLoading,
|
||||
hostEvent,
|
||||
isHostModalOpen,
|
||||
@ -305,7 +362,13 @@ class JobOutput extends Component {
|
||||
hostEvent={hostEvent}
|
||||
/>
|
||||
)}
|
||||
<OutputHeader>{job.name}</OutputHeader>
|
||||
<OutputHeader>
|
||||
<HeaderTitle>
|
||||
<StatusIcon status={job.status} />
|
||||
<h1>{job.name}</h1>
|
||||
</HeaderTitle>
|
||||
<OutputToolbar job={job} onDelete={this.handleDeleteJob} />
|
||||
</OutputHeader>
|
||||
<HostStatusBar counts={job.host_status_counts} />
|
||||
<PageControls
|
||||
onScrollFirst={this.handleScrollFirst}
|
||||
@ -345,9 +408,20 @@ class JobOutput extends Component {
|
||||
</InfiniteLoader>
|
||||
<OutputFooter />
|
||||
</OutputWrapper>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
onClose={() => this.setState({ deletionError: null })}
|
||||
title={i18n._(t`Job Delete Error`)}
|
||||
>
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default JobOutput;
|
||||
export { JobOutput as _JobOutput };
|
||||
export default withI18n()(withRouter(JobOutput));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import JobOutput from './JobOutput';
|
||||
import JobOutput, { _JobOutput } from './JobOutput';
|
||||
import { JobsAPI } from '@api';
|
||||
import mockJobData from '../shared/data.job.json';
|
||||
import mockJobEventsData from './data.job_events.json';
|
||||
@ -60,7 +60,7 @@ describe('<JobOutput />', () => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders succesfully', async done => {
|
||||
test('initially renders succesfully', async () => {
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
|
||||
await checkOutput(wrapper, [
|
||||
@ -119,12 +119,11 @@ describe('<JobOutput />', () => {
|
||||
]);
|
||||
|
||||
expect(wrapper.find('JobOutput').length).toBe(1);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should call scrollToRow with expected index when scroll "previous" button is clicked', async done => {
|
||||
test('should call scrollToRow with expected index when scroll "previous" button is clicked', async () => {
|
||||
const handleScrollPrevious = jest.spyOn(
|
||||
JobOutput.prototype,
|
||||
_JobOutput.prototype,
|
||||
'handleScrollPrevious'
|
||||
);
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
@ -140,12 +139,11 @@ describe('<JobOutput />', () => {
|
||||
expect(handleScrollPrevious).toHaveBeenCalled();
|
||||
expect(scrollMock).toHaveBeenCalledTimes(2);
|
||||
expect(scrollMock.mock.calls).toEqual([[100], [0]]);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should call scrollToRow with expected indices on when scroll "first" and "last" buttons are clicked', async done => {
|
||||
test('should call scrollToRow with expected indices on when scroll "first" and "last" buttons are clicked', async () => {
|
||||
const handleScrollFirst = jest.spyOn(
|
||||
JobOutput.prototype,
|
||||
_JobOutput.prototype,
|
||||
'handleScrollFirst'
|
||||
);
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
@ -162,12 +160,11 @@ describe('<JobOutput />', () => {
|
||||
expect(handleScrollFirst).toHaveBeenCalled();
|
||||
expect(scrollMock).toHaveBeenCalledTimes(3);
|
||||
expect(scrollMock.mock.calls).toEqual([[0], [100], [0]]);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should call scrollToRow with expected index on when scroll "last" button is clicked', async done => {
|
||||
test('should call scrollToRow with expected index on when scroll "last" button is clicked', async () => {
|
||||
const handleScrollLast = jest.spyOn(
|
||||
JobOutput.prototype,
|
||||
_JobOutput.prototype,
|
||||
'handleScrollLast'
|
||||
);
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
@ -184,13 +181,43 @@ describe('<JobOutput />', () => {
|
||||
expect(handleScrollLast).toHaveBeenCalled();
|
||||
expect(scrollMock).toHaveBeenCalledTimes(1);
|
||||
expect(scrollMock.mock.calls).toEqual([[100]]);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should throw error', async done => {
|
||||
test('should make expected api call for delete', async () => {
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
|
||||
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal',
|
||||
el => el.props().isOpen === true && el.props().title === 'Delete Job'
|
||||
);
|
||||
wrapper.find('Modal button[aria-label="Delete"]').simulate('click');
|
||||
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should show error dialog for failed deletion', async () => {
|
||||
JobsAPI.destroy.mockRejectedValue(new Error({}));
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
await waitForElement(wrapper, 'JobEvent', el => el.length > 0);
|
||||
wrapper.find('button[aria-label="Delete"]').simulate('click');
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'Modal',
|
||||
el => el.props().isOpen === true && el.props().title === 'Delete Job'
|
||||
);
|
||||
wrapper.find('Modal button[aria-label="Delete"]').simulate('click');
|
||||
await waitForElement(wrapper, 'Modal ErrorDetail');
|
||||
const errorModalCloseBtn = wrapper.find(
|
||||
'ModalBox div[aria-label="Job Delete Error"] button[aria-label="Close"]'
|
||||
);
|
||||
errorModalCloseBtn.simulate('click');
|
||||
await waitForElement(wrapper, 'Modal ErrorDetail', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('should throw error', async () => {
|
||||
JobsAPI.readEvents = () => Promise.reject(new Error());
|
||||
wrapper = mountWithContexts(<JobOutput job={mockJob} />);
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
172
awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx
Normal file
172
awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { shape, func } from 'prop-types';
|
||||
import {
|
||||
DownloadIcon,
|
||||
RocketIcon,
|
||||
TrashAltIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import { Badge as PFBadge, Button, Tooltip } from '@patternfly/react-core';
|
||||
import VerticalSeparator from '@components/VerticalSeparator';
|
||||
import DeleteButton from '@components/DeleteButton';
|
||||
import LaunchButton from '@components/LaunchButton';
|
||||
|
||||
const BadgeGroup = styled.div`
|
||||
margin-left: 20px;
|
||||
height: 18px;
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
const Badge = styled(PFBadge)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-left: 10px;
|
||||
${props =>
|
||||
props.color
|
||||
? `
|
||||
background-color: ${props.color}
|
||||
color: white;
|
||||
`
|
||||
: null}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
function toHHMMSS(s) {
|
||||
function pad(n) {
|
||||
return `00${n}`.slice(-2);
|
||||
}
|
||||
|
||||
const secs = s % 60;
|
||||
s = (s - secs) / 60;
|
||||
const mins = s % 60;
|
||||
const hrs = (s - mins) / 60;
|
||||
|
||||
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
||||
}
|
||||
|
||||
const OUTPUT_NO_COUNT_JOB_TYPES = [
|
||||
'ad_hoc_command',
|
||||
'system_job',
|
||||
'inventory_update',
|
||||
];
|
||||
|
||||
const OutputToolbar = ({ i18n, job, onDelete }) => {
|
||||
const hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(job.type);
|
||||
|
||||
const playCount = job?.playbook_counts?.play_count;
|
||||
const taskCount = job?.playbook_counts?.task_count;
|
||||
const darkCount = job?.host_status_counts?.dark;
|
||||
const failureCount = job?.host_status_counts?.failures;
|
||||
const totalHostCount = Object.keys(job?.host_status_counts).reduce(
|
||||
(sum, key) => sum + job?.host_status_counts[key],
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{!hideCounts && (
|
||||
<>
|
||||
{playCount > 0 && (
|
||||
<BadgeGroup aria-label={i18n._(t`Play Count`)}>
|
||||
<div>{i18n._(t`Plays`)}</div>
|
||||
<Badge isRead>{playCount}</Badge>
|
||||
</BadgeGroup>
|
||||
)}
|
||||
{taskCount > 0 && (
|
||||
<BadgeGroup aria-label={i18n._(t`Task Count`)}>
|
||||
<div>{i18n._(t`Tasks`)}</div>
|
||||
<Badge isRead>{taskCount}</Badge>
|
||||
</BadgeGroup>
|
||||
)}
|
||||
{totalHostCount > 0 && (
|
||||
<BadgeGroup aria-label={i18n._(t`Host Count`)}>
|
||||
<div>{i18n._(t`Hosts`)}</div>
|
||||
<Badge isRead>{totalHostCount}</Badge>
|
||||
</BadgeGroup>
|
||||
)}
|
||||
{darkCount > 0 && (
|
||||
<BadgeGroup aria-label={i18n._(t`Unreachable Host Count`)}>
|
||||
<div>{i18n._(t`Unreachable`)}</div>
|
||||
<Tooltip content={i18n._(t`Unreachable Hosts`)}>
|
||||
<Badge color="#470000" isRead>
|
||||
{darkCount}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</BadgeGroup>
|
||||
)}
|
||||
{failureCount > 0 && (
|
||||
<BadgeGroup aria-label={i18n._(t`Failed Host Count`)}>
|
||||
<div>{i18n._(t`Failed`)}</div>
|
||||
<Tooltip content={i18n._(t`Failed Hosts`)}>
|
||||
<Badge color="#C9190B" isRead>
|
||||
{failureCount}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</BadgeGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<BadgeGroup aria-label={i18n._(t`Elapsed Time`)}>
|
||||
<div>{i18n._(t`Elapsed`)}</div>
|
||||
<Tooltip content={i18n._(t`Elapsed time that the job ran`)}>
|
||||
<Badge isRead>{toHHMMSS(job.elapsed)}</Badge>
|
||||
</Tooltip>
|
||||
</BadgeGroup>
|
||||
|
||||
<VerticalSeparator />
|
||||
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities?.start && (
|
||||
<Tooltip content={i18n._(t`Relaunch Job`)}>
|
||||
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button variant="plain" onClick={handleRelaunch}>
|
||||
<RocketIcon />
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{job.related?.stdout && (
|
||||
<Tooltip content={i18n._(t`Download Output`)}>
|
||||
<a href={`${job.related.stdout}?format=txt_download`}>
|
||||
<Button variant="plain">
|
||||
<DownloadIcon />
|
||||
</Button>
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{job.summary_fields.user_capabilities.delete && (
|
||||
<Tooltip content={i18n._(t`Delete Job`)}>
|
||||
<DeleteButton
|
||||
name={job.name}
|
||||
modalTitle={i18n._(t`Delete Job`)}
|
||||
onConfirm={onDelete}
|
||||
variant="plain"
|
||||
>
|
||||
<TrashAltIcon />
|
||||
</DeleteButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
OutputToolbar.propTypes = {
|
||||
job: shape({}).isRequired,
|
||||
onDelete: func.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(OutputToolbar);
|
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { OutputToolbar } from '.';
|
||||
import mockJobData from '../../shared/data.job.json';
|
||||
|
||||
describe('<OutputToolbar />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<OutputToolbar
|
||||
job={{
|
||||
...mockJobData,
|
||||
host_status_counts: {
|
||||
dark: 1,
|
||||
failures: 2,
|
||||
},
|
||||
}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('initially renders without crashing', () => {
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should hide badge counts based on job type', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<OutputToolbar
|
||||
job={{ ...mockJobData, type: 'system_job' }}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('div[aria-label="Play Count"]').length).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Task Count"]').length).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Host Count"]').length).toBe(0);
|
||||
expect(
|
||||
wrapper.find('div[aria-label="Unreachable Host Count"]').length
|
||||
).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Failed Host Count"]').length).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Elapsed Time"]').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should hide badge if count is equal to zero', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<OutputToolbar
|
||||
job={{
|
||||
...mockJobData,
|
||||
host_status_counts: {},
|
||||
playbook_counts: {},
|
||||
}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('div[aria-label="Play Count"]').length).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Task Count"]').length).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Host Count"]').length).toBe(0);
|
||||
expect(
|
||||
wrapper.find('div[aria-label="Unreachable Host Count"]').length
|
||||
).toBe(0);
|
||||
expect(wrapper.find('div[aria-label="Failed Host Count"]').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should hide relaunch button based on user capabilities', () => {
|
||||
expect(wrapper.find('LaunchButton').length).toBe(1);
|
||||
wrapper = mountWithContexts(
|
||||
<OutputToolbar
|
||||
job={{
|
||||
...mockJobData,
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
start: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('LaunchButton').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should hide delete button based on user capabilities', () => {
|
||||
expect(wrapper.find('DeleteButton').length).toBe(1);
|
||||
wrapper = mountWithContexts(
|
||||
<OutputToolbar
|
||||
job={{
|
||||
...mockJobData,
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
delete: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('DeleteButton').length).toBe(0);
|
||||
});
|
||||
});
|
@ -1,5 +1,6 @@
|
||||
export { default as HostStatusBar } from './HostStatusBar';
|
||||
export { default as JobEventLine } from './JobEventLine';
|
||||
export { default as JobEventLineToggle } from './JobEventLineToggle';
|
||||
export { default as JobEventLineNumber } from './JobEventLineNumber';
|
||||
export { default as JobEventLineText } from './JobEventLineText';
|
||||
export { default as HostStatusBar } from './HostStatusBar';
|
||||
export { default as OutputToolbar } from './OutputToolbar';
|
||||
|
Loading…
Reference in New Issue
Block a user