From ec1fa4dae6a94faf6628d09d85a574f1bf45c6b9 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 25 Jun 2019 15:28:07 -0400 Subject: [PATCH] 240 jt details skeleton v2 (#273) * adding package-lock.json * deleted unsured file * Adds a Bottom Border Component * Updates dependencies * Adds JT Details and tests for it * merge and rebase * addresses UI PR issues * Addresses PR Issues and fixes failing tests. * Updates to code, fixes package and package-lock.json addresses PR Issues * fixes package files --- src/api/models/JobTemplates.js | 3 +- src/components/DetailList/Detail.jsx | 1 - src/screens/Organization/Organization.jsx | 9 +- .../OrganizationAccessItem.test.jsx.snap | 20 +- .../JobTemplateDetail/JobTemplateDetail.jsx | 340 ++++++++++++++++++ .../JobTemplateDetail.test.jsx | 114 ++++++ .../JobTemplateDetail.test.jsx.snap | 199 ++++++++++ .../Template/JobTemplateDetail/index.js | 4 + src/screens/Template/Template.jsx | 7 +- src/screens/Template/Template.test.jsx | 17 + src/util/strings.js | 2 +- 11 files changed, 697 insertions(+), 19 deletions(-) create mode 100644 src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx create mode 100644 src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx create mode 100644 src/screens/Template/JobTemplateDetail/__snapshots__/JobTemplateDetail.test.jsx.snap create mode 100644 src/screens/Template/JobTemplateDetail/index.js create mode 100644 src/screens/Template/Template.test.jsx diff --git a/src/api/models/JobTemplates.js b/src/api/models/JobTemplates.js index 3158d48c41..dd1e5269fa 100644 --- a/src/api/models/JobTemplates.js +++ b/src/api/models/JobTemplates.js @@ -1,6 +1,7 @@ import Base from '../Base'; +import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin'; -class JobTemplates extends Base { +class JobTemplates extends InstanceGroupsMixin(Base) { constructor (http) { super(http); this.baseUrl = '/api/v2/job_templates/'; diff --git a/src/components/DetailList/Detail.jsx b/src/components/DetailList/Detail.jsx index 1c6e4b4579..9f09fd4ed2 100644 --- a/src/components/DetailList/Detail.jsx +++ b/src/components/DetailList/Detail.jsx @@ -7,7 +7,6 @@ const DetailName = styled(({ fullWidth, ...props }) => ( ))` font-weight: var(--pf-global--FontWeight--bold); - text-align: right; ${props => props.fullWidth && ` grid-column: 1; `} diff --git a/src/screens/Organization/Organization.jsx b/src/screens/Organization/Organization.jsx index 14d8a44c8f..920e66126d 100644 --- a/src/screens/Organization/Organization.jsx +++ b/src/screens/Organization/Organization.jsx @@ -4,17 +4,15 @@ import { t } from '@lingui/macro'; import { Switch, Route, withRouter, Redirect } from 'react-router-dom'; import { Card, CardHeader as PFCardHeader, PageSection } from '@patternfly/react-core'; import styled from 'styled-components'; - -import { OrganizationsAPI } from '@api'; import CardCloseButton from '@components/CardCloseButton'; -import ContentError from '@components/ContentError'; import RoutedTabs from '@components/RoutedTabs'; - +import ContentError from '@components/ContentError'; import { OrganizationAccess } from './OrganizationAccess'; import OrganizationDetail from './OrganizationDetail'; import OrganizationEdit from './OrganizationEdit'; import OrganizationNotifications from './OrganizationNotifications'; import OrganizationTeams from './OrganizationTeams'; +import { OrganizationsAPI } from '@api'; class Organization extends Component { constructor (props) { @@ -141,10 +139,11 @@ class Organization extends Component { `; let cardHeader = ( - + diff --git a/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccessItem.test.jsx.snap b/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccessItem.test.jsx.snap index 996cbf29fe..5d523bfb30 100644 --- a/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccessItem.test.jsx.snap +++ b/src/screens/Organization/OrganizationAccess/__snapshots__/OrganizationAccessItem.test.jsx.snap @@ -426,9 +426,9 @@ exports[` initially renders succesfully 1`] = ` "componentStyle": ComponentStyle { "componentId": "Detail__DetailName-sc-16ypsyv-0", "isStatic": false, - "lastClassName": "gRioSK", + "lastClassName": "erdIBg", "rules": Array [ - "font-weight:var(--pf-global--FontWeight--bold);text-align:right;", + "font-weight:var(--pf-global--FontWeight--bold);", [Function], ], }, @@ -446,16 +446,16 @@ exports[` initially renders succesfully 1`] = ` fullWidth={false} >
Name @@ -603,9 +603,9 @@ exports[` initially renders succesfully 1`] = ` "componentStyle": ComponentStyle { "componentId": "Detail__DetailName-sc-16ypsyv-0", "isStatic": false, - "lastClassName": "gRioSK", + "lastClassName": "erdIBg", "rules": Array [ - "font-weight:var(--pf-global--FontWeight--bold);text-align:right;", + "font-weight:var(--pf-global--FontWeight--bold);", [Function], ], }, @@ -623,16 +623,16 @@ exports[` initially renders succesfully 1`] = ` fullWidth={false} >
Team Roles diff --git a/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx new file mode 100644 index 0000000000..b7e1764f18 --- /dev/null +++ b/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -0,0 +1,340 @@ +import React, { Component } from 'react'; +import { Link, withRouter } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { CardBody, Button, TextList, TextListItem, TextListItemVariants, TextListVariants } from '@patternfly/react-core'; +import styled from 'styled-components'; +import { t } from '@lingui/macro'; + +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; +import { ChipGroup, Chip } from '../../../components/Chip'; +import { DetailList, Detail } from '../../../components/DetailList'; +import { JobTemplatesAPI } from '../../../api'; +import { toTitleCase } from '../../../util/strings'; + +const ButtonGroup = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 20px; + & > :not(:first-child){ + margin-left: 20px; + } +`; +class JobTemplateDetail extends Component { + constructor (props) { + super(props); + this.state = { + contentError: false, + hasContentLoading: true, + instanceGroups: [] + }; + this.readInstanceGroups = this.readInstanceGroups.bind(this); + } + + componentDidMount () { + this.readInstanceGroups(); + } + + async readInstanceGroups () { + const { match } = this.props; + try { + const { data } = await JobTemplatesAPI.readInstanceGroups(match.params.id); + this.setState({ instanceGroups: [...data.results] }); + } catch { + this.setState({ contentError: true }); + } finally { + this.setState({ hasContentLoading: false }); + } + } + + render () { + const { + template: { + allow_simultaneous, + become_enabled, + created, + description, + diff_mode, + forks, + host_config_key, + job_slice_count, + job_tags, + job_type, + inventory, + name, + limit, + modified, + playbook, + project, + skip_tags, + timeout, + summary_fields, + use_fact_cache, + url, + verbosity + }, + hasTemplateLoading, + i18n, + } = this.props; + const { instanceGroups, hasContentLoading, contentError } = this.state; + const verbosityOptions = [ + { verbosity: 0, details: i18n._(t`0 (Normal)`) }, + { verbosity: 1, details: i18n._(t`1 (Verbose)`) }, + { verbosity: 2, details: i18n._(t`2 (More Verbose)`) }, + { verbosity: 3, details: i18n._(t`3 (Debug)`) }, + { verbosity: 4, details: i18n._(t`4 (Connection Debug)`) }, + { verbosity: 5, details: i18n._(t`5 (WinRM Debug)`) }, + ]; + const verbosityDetails = verbosityOptions.filter( + option => option.verbosity === verbosity + ); + const generateCallBackUrl = `${window.location.origin + url}callback/`; + const isInitialized = !hasTemplateLoading && !hasContentLoading; + + const credentialType = (c) => (c === 'aws' || c === 'ssh' ? c.toUpperCase() : toTitleCase(c)); + + const renderOptionsField = become_enabled || host_config_key || allow_simultaneous + || use_fact_cache; + + const renderOptions = ( + + {become_enabled && ( + + {i18n._(t`Enable Privilege Escalation`)} + + )} + {host_config_key && ( + + {i18n._(t`Allow Provisioning Callbacks`)} + + )} + {allow_simultaneous && ( + + {i18n._(t`Enable Concurrent Jobs`)} + + )} + {use_fact_cache && ( + + {i18n._(t`Use Fact Cache`)} + + )} + + ); + + if (contentError) { + return (); + } + + if (hasContentLoading) { + return (); + } + + return ( + isInitialized && ( + + + + + + {inventory && ( + + )} + {project && ( + + )} + + + + + + + + + + {host_config_key && ( + + + + + )} + {renderOptionsField && ( + + )} + {(summary_fields.credentials && summary_fields.credentials.length > 0) && ( + + {summary_fields.credentials.map(c => ( + + + {c.kind ? credentialType(c.kind) : i18n._(t`Cloud`)} +: + + {` ${c.name}`} + + )) + } + + )} + /> + )} + {(summary_fields.labels && summary_fields.labels.results.length > 0) && ( + + {summary_fields.labels.results.map(l => ( + + {l.name} + + )) + } + + )} + /> + )} + {(instanceGroups.length > 0) && ( + + {instanceGroups.map(ig => ( + + {ig.name} + + ))} + + )} + /> + )} + {(job_tags && job_tags.length > 0) && ( + + {job_tags.split(',').map(jobTag => ( + + {jobTag} + + )) + } + + )} + /> + )} + {(skip_tags && skip_tags.length > 0) && ( + + {skip_tags.split(',').map(skipTag => ( + + {skipTag} + + )) + } + + )} + /> + )} + + + {summary_fields.user_capabilities.edit && ( + + )} + + + + + ) + ); + } +} +export { JobTemplateDetail as _JobTemplateDetail }; +export default withI18n()(withRouter(JobTemplateDetail)); diff --git a/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx b/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx new file mode 100644 index 0000000000..59f8f9e5a2 --- /dev/null +++ b/src/screens/Template/JobTemplateDetail/JobTemplateDetail.test.jsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { mountWithContexts, waitForElement } from '../../../../testUtils/enzymeHelpers'; +import JobTemplateDetail, { _JobTemplateDetail } from './JobTemplateDetail'; + +describe('', () => { + const template = { + forks: 1, + host_config_key: 'ssh', + name: 'Temp 1', + job_type: 'run', + inventory: 1, + limit: '1', + project: 7, + playbook: '', + id: 1, + verbosity: 1, + summary_fields: { + user_capabilities: { edit: true }, + created_by: { username: 'Joe' }, + modified_by: { username: 'Joe' }, + credentials: [ + { id: 1, kind: 'ssh', name: 'Credential 1' }, + { id: 2, kind: 'awx', name: 'Credential 2' } + ], + inventory: { name: 'Inventory' }, + project: { name: 'Project' } + } + }; + + const mockInstanceGroups = { + count: 5, + data: { + results: [ + { id: 1, name: 'IG1' }, + { id: 2, name: 'IG2' }] + } + }; + + const readInstanceGroups = jest.spyOn(_JobTemplateDetail.prototype, 'readInstanceGroups'); + + test('initially renders succesfully', () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper).toMatchSnapshot(); + }); + test('When component mounts API is called to get instance groups', async (done) => { + const wrapper = mountWithContexts( + + ); + await waitForElement(wrapper, 'JobTemplateDetail', (el) => el.state('hasContentLoading') === true); + expect(readInstanceGroups).toHaveBeenCalled(); + await waitForElement(wrapper, 'JobTemplateDetail', (el) => el.state('hasContentLoading') === false); + done(); + }); + test('Edit button is absent when user does not have edit privilege', async (done) => { + const regularUser = { + forks: 1, + host_config_key: 'ssh', + name: 'Temp 1', + job_tags: 'cookies,pizza', + job_type: 'run', + inventory: 1, + limit: '1', + project: 7, + playbook: '', + id: 1, + verbosity: 0, + created_by: 'Alex', + skip_tags: 'coffe,tea', + summary_fields: { + user_capabilities: { edit: false }, + created_by: { username: 'Joe' }, + modified_by: { username: 'Joe' }, + inventory: { name: 'Inventory' }, + project: { name: 'Project' }, + labels: { count: 1, results: [{ name: 'Label', id: 1 }] } + } + }; + const wrapper = mountWithContexts( + + ); + const jobTemplateDetail = wrapper.find('JobTemplateDetail'); + const editButton = jobTemplateDetail.find('button[aria-label="Edit"]'); + + jobTemplateDetail.setState({ + instanceGroups: mockInstanceGroups, hasContentLoading: false, contentError: false + }); + expect(editButton.length).toBe(0); + done(); + }); + + test('Credential type is Cloud if credential.kind is null', async (done) => { + template.summary_fields.credentials = [{ id: 1, name: 'cred', kind: null, }]; + const wrapper = mountWithContexts( + + ); + const jobTemplateDetail = wrapper.find('JobTemplateDetail'); + jobTemplateDetail.setState({ + instanceGroups: mockInstanceGroups.data.results, hasContentLoading: false, contentError: false + }); + const cred = wrapper.find('strong.credential'); + expect(cred.text()).toContain('Cloud:'); + done(); + }); +}); diff --git a/src/screens/Template/JobTemplateDetail/__snapshots__/JobTemplateDetail.test.jsx.snap b/src/screens/Template/JobTemplateDetail/__snapshots__/JobTemplateDetail.test.jsx.snap new file mode 100644 index 0000000000..2bc21e093c --- /dev/null +++ b/src/screens/Template/JobTemplateDetail/__snapshots__/JobTemplateDetail.test.jsx.snap @@ -0,0 +1,199 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` initially renders succesfully 1`] = ` + + + + + + + + + + +
+ +

+ Loading... +

+
+
+
+
+
+
+
+
+
+
+
+
+`; diff --git a/src/screens/Template/JobTemplateDetail/index.js b/src/screens/Template/JobTemplateDetail/index.js new file mode 100644 index 0000000000..fedf6e28d5 --- /dev/null +++ b/src/screens/Template/JobTemplateDetail/index.js @@ -0,0 +1,4 @@ +import JobTemplateDetail from './JobTemplateDetail'; + +export { JobTemplateDetail as _JobTemplateDetail }; +export default JobTemplateDetail; diff --git a/src/screens/Template/Template.jsx b/src/screens/Template/Template.jsx index 432ad89b35..5e7717bf5d 100644 --- a/src/screens/Template/Template.jsx +++ b/src/screens/Template/Template.jsx @@ -7,6 +7,7 @@ import { Switch, Route, Redirect, withRouter } from 'react-router-dom'; import CardCloseButton from '@components/CardCloseButton'; import ContentError from '@components/ContentError'; import RoutedTabs from '@components/RoutedTabs'; +import JobTemplateDetail from './JobTemplateDetail'; import { JobTemplatesAPI } from '@api'; class Template extends Component { @@ -85,7 +86,11 @@ class Template extends Component { ( - Coming soon! + )} /> )} diff --git a/src/screens/Template/Template.test.jsx b/src/screens/Template/Template.test.jsx new file mode 100644 index 0000000000..c0b7574433 --- /dev/null +++ b/src/screens/Template/Template.test.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; +import Template, { _Template } from './Template'; + +describe('