diff --git a/awx/ui/src/components/StatusIcon/StatusIcon.js b/awx/ui/src/components/StatusIcon/StatusIcon.js index 6f4e42c47e..71149f8753 100644 --- a/awx/ui/src/components/StatusIcon/StatusIcon.js +++ b/awx/ui/src/components/StatusIcon/StatusIcon.js @@ -1,136 +1,43 @@ import React from 'react'; import { string } from 'prop-types'; -import styled, { keyframes } from 'styled-components'; +import icons from './icons'; -const Pulse = keyframes` - from { - -webkit-transform:scale(1); - } - to { - -webkit-transform:scale(0); - } -`; +const green = '--pf-global--success-color--100'; +const red = '--pf-global--danger-color--100'; +const blue = '--pf-global--primary-color--100'; +const orange = '--pf-global--palette--orange-300'; +const gray = '--pf-global--Color--300'; +const colors = { + success: green, + successful: green, + healthy: green, + ok: green, + failed: red, + error: red, + unreachable: red, + running: blue, + pending: blue, + skipped: blue, + waiting: gray, + disabled: gray, + canceled: orange, + changed: orange, +}; -const Wrapper = styled.div` - align-items: center; - display: flex; - flex-flow: column nowrap; - height: 14px; - margin: 5px 0; - width: 14px; -`; - -const WhiteTop = styled.div` - border: 1px solid #b7b7b7; - border-bottom: 0; - background: #ffffff; -`; - -const WhiteBottom = styled.div` - border: 1px solid #b7b7b7; - border-top: 0; - background: #ffffff; -`; - -const RunningJob = styled(Wrapper)` - background-color: #5cb85c; - padding-right: 0px; - text-shadow: -1px -1px 0 #ffffff, 1px -1px 0 #ffffff, -1px 1px 0 #ffffff, - 1px 1px 0 #ffffff; - animation: ${Pulse} 1.5s linear infinite alternate; -`; - -const WaitingJob = styled(Wrapper)` - border: 1px solid #d7d7d7; -`; - -const FinishedJob = styled(Wrapper)` - flex: 0 1 auto; - > * { - width: 14px; - height: 7px; - } -`; - -const SuccessfulTop = styled.div` - background-color: #5cb85c; -`; -const SuccessfulBottom = styled(WhiteBottom)``; - -const FailedTop = styled(WhiteTop)``; -const FailedBottom = styled.div` - background-color: #d9534f; -`; - -const UnreachableTop = styled(WhiteTop)``; -const UnreachableBottom = styled.div` - background-color: #ff0000; -`; - -const ChangedTop = styled(WhiteTop)``; -const ChangedBottom = styled.div` - background-color: #ff9900; -`; - -const SkippedTop = styled(WhiteTop)``; -const SkippedBottom = styled.div` - background-color: #2dbaba; -`; - -RunningJob.displayName = 'RunningJob'; -WaitingJob.displayName = 'WaitingJob'; -FinishedJob.displayName = 'FinishedJob'; -SuccessfulTop.displayName = 'SuccessfulTop'; -SuccessfulBottom.displayName = 'SuccessfulBottom'; -FailedTop.displayName = 'FailedTop'; -FailedBottom.displayName = 'FailedBottom'; -UnreachableTop.displayName = 'UnreachableTop'; -UnreachableBottom.displayName = 'UnreachableBottom'; -ChangedTop.displayName = 'ChangedTop'; -ChangedBottom.displayName = 'ChangedBottom'; -SkippedTop.displayName = 'SkippedTop'; -SkippedBottom.displayName = 'SkippedBottom'; - -const StatusIcon = ({ status, ...props }) => ( -
- {status === 'running' &&
-); +function StatusIcon({ status, ...props }) { + const color = colors[status] || '--pf-chart-global--Fill--Color--500'; + const Icon = icons[status]; + return ( +
+ {Icon ? ( +
+ +
+ ) : null} + {status} +
+ ); +} StatusIcon.propTypes = { status: string.isRequired, diff --git a/awx/ui/src/components/StatusIcon/StatusIcon.test.js b/awx/ui/src/components/StatusIcon/StatusIcon.test.js index dd552b3ef4..783bc8745f 100644 --- a/awx/ui/src/components/StatusIcon/StatusIcon.test.js +++ b/awx/ui/src/components/StatusIcon/StatusIcon.test.js @@ -6,53 +6,54 @@ describe('StatusIcon', () => { test('renders the successful status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon SuccessfulTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon SuccessfulBottom')).toHaveLength(1); + expect(wrapper.find('CheckCircleIcon')).toHaveLength(1); }); + test('renders running status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon RunningJob')).toHaveLength(1); + expect(wrapper.find('RunningIcon')).toHaveLength(1); }); + test('renders waiting status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon WaitingJob')).toHaveLength(1); + expect(wrapper.find('ClockIcon')).toHaveLength(1); }); + test('renders failed status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon FailedTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon FailedBottom')).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); }); + test('renders a successful status when host status is "ok"', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon SuccessfulTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon SuccessfulBottom')).toHaveLength(1); + expect(wrapper.find('CheckCircleIcon')).toHaveLength(1); }); + test('renders "failed" host status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon FailedTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon FailedBottom')).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); }); + test('renders "changed" host status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon ChangedTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon ChangedBottom')).toHaveLength(1); + expect(wrapper.find('ExclamationTriangleIcon')).toHaveLength(1); }); + test('renders "skipped" host status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon SkippedTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon SkippedBottom')).toHaveLength(1); + expect(wrapper.find('MinusCircleIcon')).toHaveLength(1); }); + test('renders "unreachable" host status', () => { const wrapper = mount(); expect(wrapper).toHaveLength(1); - expect(wrapper.find('StatusIcon UnreachableTop')).toHaveLength(1); - expect(wrapper.find('StatusIcon UnreachableBottom')).toHaveLength(1); + expect(wrapper.find('ExclamationCircleIcon')).toHaveLength(1); }); }); diff --git a/awx/ui/src/components/StatusIcon/icons.js b/awx/ui/src/components/StatusIcon/icons.js new file mode 100644 index 0000000000..6148c6945f --- /dev/null +++ b/awx/ui/src/components/StatusIcon/icons.js @@ -0,0 +1,41 @@ +import styled, { keyframes } from 'styled-components'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + SyncAltIcon, + ExclamationTriangleIcon, + ClockIcon, + MinusCircleIcon, +} from '@patternfly/react-icons'; + +const Spin = keyframes` + from { + transform: rotate(0); + } + to { + transform: rotate(1turn); + } +`; + +const RunningIcon = styled(SyncAltIcon)` + animation: ${Spin} 1.75s linear infinite; +`; +RunningIcon.displayName = 'RunningIcon'; + +const icons = { + success: CheckCircleIcon, + healthy: CheckCircleIcon, + successful: CheckCircleIcon, + ok: CheckCircleIcon, + failed: ExclamationCircleIcon, + error: ExclamationCircleIcon, + unreachable: ExclamationCircleIcon, + running: RunningIcon, + pending: ClockIcon, + waiting: ClockIcon, + disabled: MinusCircleIcon, + skipped: MinusCircleIcon, + canceled: ExclamationTriangleIcon, + changed: ExclamationTriangleIcon, +}; +export default icons; diff --git a/awx/ui/src/components/StatusLabel/StatusLabel.js b/awx/ui/src/components/StatusLabel/StatusLabel.js index a8486f3de1..cb25d5d0ba 100644 --- a/awx/ui/src/components/StatusLabel/StatusLabel.js +++ b/awx/ui/src/components/StatusLabel/StatusLabel.js @@ -3,52 +3,23 @@ import React from 'react'; import { t } from '@lingui/macro'; import { oneOf } from 'prop-types'; import { Label, Tooltip } from '@patternfly/react-core'; -import { - CheckCircleIcon, - ExclamationCircleIcon, - SyncAltIcon, - ExclamationTriangleIcon, - ClockIcon, - MinusCircleIcon, -} from '@patternfly/react-icons'; -import styled, { keyframes } from 'styled-components'; - -const Spin = keyframes` - from { - transform: rotate(0); - } - to { - transform: rotate(1turn); - } -`; - -const RunningIcon = styled(SyncAltIcon)` - animation: ${Spin} 1.75s linear infinite; -`; +import icons from '../StatusIcon/icons'; const colors = { success: 'green', successful: 'green', + ok: 'green', healthy: 'green', failed: 'red', error: 'red', + unreachable: 'red', running: 'blue', pending: 'blue', + skipped: 'blue', waiting: 'grey', disabled: 'grey', canceled: 'orange', -}; -const icons = { - success: CheckCircleIcon, - healthy: CheckCircleIcon, - successful: CheckCircleIcon, - failed: ExclamationCircleIcon, - error: ExclamationCircleIcon, - running: RunningIcon, - pending: ClockIcon, - waiting: ClockIcon, - disabled: MinusCircleIcon, - canceled: ExclamationTriangleIcon, + changed: 'orange', }; export default function StatusLabel({ status, tooltipContent = '' }) { @@ -56,15 +27,19 @@ export default function StatusLabel({ status, tooltipContent = '' }) { success: t`Success`, healthy: t`Healthy`, successful: t`Successful`, + ok: t`OK`, failed: t`Failed`, error: t`Error`, + unreachable: t`Unreachable`, running: t`Running`, pending: t`Pending`, + skipped: t`Skipped'`, waiting: t`Waiting`, disabled: t`Disabled`, canceled: t`Canceled`, + changed: t`Changed`, }; - const label = upperCaseStatus[status] || t`Undefined`; + const label = upperCaseStatus[status] || status; const color = colors[status] || 'grey'; const Icon = icons[status]; @@ -91,13 +66,17 @@ StatusLabel.propTypes = { status: oneOf([ 'success', 'successful', + 'ok', 'healthy', 'failed', 'error', + 'unreachable', 'running', 'pending', + 'skipped', 'waiting', 'disabled', 'canceled', + 'changed', ]).isRequired, }; diff --git a/awx/ui/src/screens/Job/JobDetail/JobDetail.js b/awx/ui/src/screens/Job/JobDetail/JobDetail.js index 1f7b78101b..1c9d7cac90 100644 --- a/awx/ui/src/screens/Job/JobDetail/JobDetail.js +++ b/awx/ui/src/screens/Job/JobDetail/JobDetail.js @@ -21,11 +21,10 @@ import { VariablesDetail } from 'components/CodeEditor'; import DeleteButton from 'components/DeleteButton'; import ErrorDetail from 'components/ErrorDetail'; import { LaunchButton, ReLaunchDropDown } from 'components/LaunchButton'; -import StatusIcon from 'components/StatusIcon'; +import StatusLabel from 'components/StatusLabel'; import JobCancelButton from 'components/JobCancelButton'; import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail'; import { getJobModel, isJobRunning } from 'util/jobs'; -import { toTitleCase } from 'util/strings'; import { formatDateString } from 'util/dates'; import { Job } from 'types'; @@ -92,25 +91,6 @@ function JobDetail({ job, inventorySourceLabels }) { {item.name} ); - const buildProjectDetailValue = () => { - if (projectUpdate) { - return ( - - - - - {project.name} - - ); - } - return ( - - - {project.name} - - ); - }; - return ( @@ -121,10 +101,7 @@ function JobDetail({ job, inventorySourceLabels }) { label={t`Status`} value={ - {job.status && } - {job.job_explanation - ? job.job_explanation - : toTitleCase(job.status)} + {job.status && } } /> @@ -229,7 +206,7 @@ function JobDetail({ job, inventorySourceLabels }) { value={ {source_project.status && ( - + )} {source_project.name} @@ -239,11 +216,26 @@ function JobDetail({ job, inventorySourceLabels }) { /> )} {project && ( - + <> + {project.name}} + /> + + + + ) : ( + + ) + } + /> + )} {scmBranch && ( ', () => { // StatusIcon adds visibly hidden accessibility text " successful " assertDetail('Job ID', '2'); - assertDetail('Status', ' successful Successful'); + assertDetail('Status', 'Successful'); assertDetail('Started', '8/8/2019, 7:24:18 PM'); assertDetail('Finished', '8/8/2019, 7:24:50 PM'); assertDetail('Job Template', mockJobData.summary_fields.job_template.name); @@ -54,10 +54,7 @@ describe('', () => { assertDetail('Job Type', 'Playbook Run'); assertDetail('Launched By', mockJobData.summary_fields.created_by.username); assertDetail('Inventory', mockJobData.summary_fields.inventory.name); - assertDetail( - 'Project', - ` successful ${mockJobData.summary_fields.project.name}` - ); + assertDetail('Project', mockJobData.summary_fields.project.name); assertDetail('Revision', mockJobData.scm_revision); assertDetail('Playbook', mockJobData.playbook); assertDetail('Verbosity', '0 (Normal)'); @@ -98,16 +95,13 @@ describe('', () => { ).toEqual(true); const statusDetail = wrapper.find('Detail[label="Status"]'); - expect(statusDetail.find('StatusIcon SuccessfulTop')).toHaveLength(1); - expect(statusDetail.find('StatusIcon SuccessfulBottom')).toHaveLength(1); + const statusLabel = statusDetail.find('StatusLabel'); + expect(statusLabel.prop('status')).toEqual('successful'); - const projectStatusDetail = wrapper.find('Detail[label="Project"]'); - expect(projectStatusDetail.find('StatusIcon SuccessfulTop')).toHaveLength( - 1 - ); - expect( - projectStatusDetail.find('StatusIcon SuccessfulBottom') - ).toHaveLength(1); + const projectStatusDetail = wrapper.find('Detail[label="Project Status"]'); + expect(projectStatusDetail.find('StatusLabel')).toHaveLength(1); + const projectStatusLabel = statusDetail.find('StatusLabel'); + expect(projectStatusLabel.prop('status')).toEqual('successful'); }); test('should not display finished date', () => { @@ -537,7 +531,7 @@ describe('', () => { webhook_guid: '', }; wrapper = mountWithContexts(); - assertDetail('Status', ' successful Successful'); + assertDetail('Status', 'Successful'); assertDetail('Started', '7/6/2021, 7:40:17 PM'); assertDetail('Finished', '7/6/2021, 7:40:42 PM'); assertDetail('Job Template', 'Sliced Job Template'); diff --git a/awx/ui/src/screens/Job/JobOutput/HostEventModal.js b/awx/ui/src/screens/Job/JobOutput/HostEventModal.js index 322c9a6b0c..95d8ee989b 100644 --- a/awx/ui/src/screens/Job/JobOutput/HostEventModal.js +++ b/awx/ui/src/screens/Job/JobOutput/HostEventModal.js @@ -3,20 +3,12 @@ import { Modal, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; import PropTypes from 'prop-types'; import { t } from '@lingui/macro'; -import styled from 'styled-components'; import { encode } from 'html-entities'; -import StatusIcon from '../../../components/StatusIcon'; +import StatusLabel from '../../../components/StatusLabel'; import { DetailList, Detail } from '../../../components/DetailList'; import ContentEmpty from '../../../components/ContentEmpty'; import CodeEditor from '../../../components/CodeEditor'; -const HostNameDetailValue = styled.div` - align-items: center; - display: inline-grid; - grid-gap: 10px; - grid-template-columns: auto auto; -`; - const processEventStatus = (event) => { let status = null; if (event.event === 'runner_on_unreachable') { @@ -117,15 +109,13 @@ function HostEventModal({ onClose, hostEvent = {}, isOpen = false }) { style={{ alignItems: 'center', marginTop: '20px' }} gutter="sm" > - - {hostStatus ? : null} - {hostEvent.host_name} - - } - /> + + {hostStatus ? ( + } + /> + ) : null} { -// detailsSection = wrapper.find('section').at(0); -// jsonSection = wrapper.find('section').at(1); -// standardOutSection = wrapper.find('section').at(2); -// standardErrorSection = wrapper.find('section').at(3); -// }; - describe('HostEventModal', () => { test('initially renders successfully', () => { const wrapper = shallow( @@ -105,14 +93,14 @@ describe('HostEventModal', () => { } const detail = wrapper.find('Detail').first(); - expect(detail.prop('value').props.children).toEqual([null, 'foo']); + expect(detail.prop('value')).toEqual('foo'); assertDetail(1, 'Play', 'all'); assertDetail(2, 'Task', 'command'); assertDetail(3, 'Module', 'command'); assertDetail(4, 'Command', hostEvent.event_data.res.cmd); }); - test('should display successful host status icon', () => { + test('should display successful host status label', () => { const successfulHostEvent = { ...hostEvent, changed: false }; const wrapper = mountWithContexts( { isOpen /> ); - const icon = wrapper.find('StatusIcon'); + const icon = wrapper.find('StatusLabel'); expect(icon.prop('status')).toBe('ok'); - expect(icon.find('StatusIcon SuccessfulTop').length).toBe(1); - expect(icon.find('StatusIcon SuccessfulBottom').length).toBe(1); }); - test('should display skipped host status icon', () => { + test('should display skipped host status label', () => { const skippedHostEvent = { ...hostEvent, event: 'runner_on_skipped' }; const wrapper = mountWithContexts( {}} isOpen /> ); - const icon = wrapper.find('StatusIcon'); + const icon = wrapper.find('StatusLabel'); expect(icon.prop('status')).toBe('skipped'); - expect(icon.find('StatusIcon SkippedTop').length).toBe(1); - expect(icon.find('StatusIcon SkippedBottom').length).toBe(1); }); - test('should display unreachable host status icon', () => { + test('should display unreachable host status label', () => { const unreachableHostEvent = { ...hostEvent, event: 'runner_on_unreachable', @@ -153,13 +137,11 @@ describe('HostEventModal', () => { /> ); - const icon = wrapper.find('StatusIcon'); + const icon = wrapper.find('StatusLabel'); expect(icon.prop('status')).toBe('unreachable'); - expect(icon.find('StatusIcon UnreachableTop').length).toBe(1); - expect(icon.find('StatusIcon UnreachableBottom').length).toBe(1); }); - test('should display failed host status icon', () => { + test('should display failed host status label', () => { const unreachableHostEvent = { ...hostEvent, changed: false, @@ -174,10 +156,8 @@ describe('HostEventModal', () => { /> ); - const icon = wrapper.find('StatusIcon'); + const icon = wrapper.find('StatusLabel'); expect(icon.prop('status')).toBe('failed'); - expect(icon.find('StatusIcon FailedTop').length).toBe(1); - expect(icon.find('StatusIcon FailedBottom').length).toBe(1); }); test('should display JSON tab content on tab click', () => { diff --git a/awx/ui/src/screens/Job/JobOutput/JobOutput.js b/awx/ui/src/screens/Job/JobOutput/JobOutput.js index 4e48fc169b..1a26cd9409 100644 --- a/awx/ui/src/screens/Job/JobOutput/JobOutput.js +++ b/awx/ui/src/screens/Job/JobOutput/JobOutput.js @@ -16,7 +16,7 @@ import { CardBody as _CardBody } from 'components/Card'; import ContentError from 'components/ContentError'; import ContentLoading from 'components/ContentLoading'; import ErrorDetail from 'components/ErrorDetail'; -import StatusIcon from 'components/StatusIcon'; +import StatusLabel from 'components/StatusLabel'; import { JobEventsAPI } from 'api'; import { getJobModel, isJobRunning } from 'util/jobs'; @@ -51,7 +51,7 @@ const HeaderTitle = styled.div` display: inline-flex; align-items: center; h1 { - margin-left: 10px; + margin-right: 10px; font-weight: var(--pf-global--FontWeight--bold); } `; @@ -669,8 +669,8 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) { )} -

{job.name}

+
- - {job.name} +

{job.name}

+