diff --git a/app/components/pipeline/parameters/component.js b/app/components/pipeline/parameters/component.js new file mode 100644 index 000000000..9a9a2f009 --- /dev/null +++ b/app/components/pipeline/parameters/component.js @@ -0,0 +1,98 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { + extractJobParameters, + extractEventParameters, + getNormalizedParameterGroups +} from 'screwdriver-ui/utils/pipeline/parameters'; + +export default class PipelineParametersComponent extends Component { + @tracked parameters; + + @tracked selectedParameters; + + constructor() { + super(...arguments); + + const { pipelineParameters, jobParameters } = extractEventParameters( + this.args.event + ); + + this.parameters = getNormalizedParameterGroups( + pipelineParameters, + this.args.pipeline.parameters, + jobParameters, + extractJobParameters(this.args.jobs), + this.args.job ? this.args.job.name : null + ); + + this.selectedParameters = { + pipeline: { ...pipelineParameters }, + job: { ...jobParameters } + }; + } + + get title() { + return `${this.args.action} pipeline with parameters`.toUpperCase(); + } + + @action + toggleParameterGroup(groupName) { + const updatedParameters = []; + + this.parameters.forEach(group => { + if (group.paramGroupTitle === groupName) { + updatedParameters.push({ ...group, isOpen: !group.isOpen }); + } else { + updatedParameters.push(group); + } + }); + + this.parameters = updatedParameters; + } + + @action + openSelect(powerSelectObject) { + setTimeout(() => { + const container = document.getElementsByClassName('parameters-container'); + const scrollFrame = container[0]; + const optionsBox = document.getElementById( + `ember-power-select-options-${powerSelectObject.uniqueId}` + ); + + if (optionsBox === null) { + return; + } + + const optionsBoxRect = optionsBox.getBoundingClientRect(); + const scrollFrameRect = scrollFrame.getBoundingClientRect(); + const hiddenAreaHeight = optionsBoxRect.bottom - scrollFrameRect.bottom; + + if (hiddenAreaHeight > 0) { + scrollFrame.scrollBy({ top: hiddenAreaHeight + 10 }); + } + }, 100); + } + + @action + onInput(parameterGroup, parameter, event) { + this.updateParameter(parameterGroup, parameter, event.target.value); + } + + @action + updateParameter(parameterGroup, parameter, value) { + if (parameterGroup) { + const jobParameters = { ...this.selectedParameters.job[parameterGroup] }; + + jobParameters[parameter.name] = { value }; + this.selectedParameters.job[parameterGroup] = jobParameters; + } else { + this.selectedParameters.pipeline[parameter.name] = { value }; + } + + const { pipeline, job } = this.selectedParameters; + + this.args.onUpdateParameters({ ...pipeline, ...job }); + } +} diff --git a/app/components/pipeline/parameters/styles.scss b/app/components/pipeline/parameters/styles.scss new file mode 100644 index 000000000..4a8046cad --- /dev/null +++ b/app/components/pipeline/parameters/styles.scss @@ -0,0 +1,61 @@ +@use 'screwdriver-colors' as colors; +@use 'variables'; + +@mixin styles { + .pipeline-parameters { + .parameter-title { + font-size: 1.2rem; + font-weight: variables.$weight-bold; + color: colors.$sd-light-gray; + } + + .parameters-container { + max-height: 50vh; + overflow-y: scroll; + + .parameter-group { + border-radius: 8px; + border: 1px solid colors.$sd-separator; + margin-top: 1rem; + padding: 0.5rem; + + .group-title { + font-size: 1.5rem; + } + + .parameter-list { + &.collapsed { + display: none; + } + + .parameter { + display: flex; + padding: 0.2rem; + + label { + display: flex; + margin-top: auto; + margin-bottom: auto; + padding-right: 0.5rem; + flex-basis: 25%; + + svg { + margin-top: auto; + margin-bottom: auto; + padding-left: 0.25rem; + } + } + + .dropdown-selection-container { + flex: 1; + } + + > input { + flex: 1; + } + } + } + } + } + } +} diff --git a/app/components/pipeline/parameters/template.hbs b/app/components/pipeline/parameters/template.hbs new file mode 100644 index 000000000..27012ba29 --- /dev/null +++ b/app/components/pipeline/parameters/template.hbs @@ -0,0 +1,42 @@ +
+
+ {{this.title}} +
+
+ {{#each this.parameters as |parameterGroup|}} +
+
+ + {{parameterGroup.paramGroupTitle}} +
+
+ {{#each parameterGroup.parameters as |parameter|}} +
+ + {{#if (is-array parameter.defaultValues)}} + + {{else}} + + {{/if}} +
+ {{/each}} +
+
+ {{/each}} +
+
diff --git a/tests/integration/components/pipeline/parameters/component-test.js b/tests/integration/components/pipeline/parameters/component-test.js new file mode 100644 index 000000000..54af81114 --- /dev/null +++ b/tests/integration/components/pipeline/parameters/component-test.js @@ -0,0 +1,281 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'screwdriver-ui/tests/helpers'; +import { fillIn, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { selectChoose } from 'ember-power-select/test-support'; +import sinon from 'sinon'; + +module('Integration | Component | pipeline/parameters', function (hooks) { + setupRenderingTest(hooks); + + test('it renders title with correct action', async function (assert) { + this.setProperties({ + event: { + meta: { + parameters: { + foo: { value: 'bar' }, + job1: { p1: { value: 'abc' }, p2: { value: 'xyz' } } + } + } + }, + job: { name: 'job1' }, + pipeline: { parameters: { foo: { value: 'foofoo' } } }, + jobs: [ + { + name: 'job1', + permutations: [{ p1: { value: 'p1' }, p2: { value: 'p2' } }] + } + ] + }); + await render( + hbs`` + ); + + assert.dom('.parameter-title').hasText('START PIPELINE WITH PARAMETERS'); + }); + + test('it renders parameters with shared group expanded', async function (assert) { + this.setProperties({ + event: { + meta: { + parameters: { + bar: { value: 'barzy' }, + foo: { value: 'foozy' }, + job1: { p1: { value: 'abc' }, p2: { value: 'xyz' } } + } + } + }, + pipeline: { + parameters: { + bar: ['barbar', 'bazbaz'], + foo: { value: 'foofoo', description: 'awesome' } + } + }, + jobs: [ + { + name: 'job1', + permutations: [{ p1: { value: 'p1' }, p2: { value: 'p2' } }] + } + ] + }); + + await render( + hbs`` + ); + + assert.dom('.group-title').exists({ count: 2 }); + assert + .dom(this.element.querySelectorAll('.group-title')[0]) + .hasText('Shared'); + assert.dom('.parameter-list.expanded .parameter').exists({ count: 2 }); + + const parameters = this.element.querySelectorAll( + '.parameter-list.expanded .parameter' + ); + + assert.dom(parameters[0].querySelector('label')).hasText('bar'); + assert + .dom(parameters[0].querySelector('.dropdown-selection-container')) + .hasText('barzy'); + assert.dom(parameters[1].querySelector('label')).hasText('foo awesome'); + assert.dom(parameters[1].querySelector('label svg')).exists({ count: 1 }); + assert.dom(parameters[1].querySelector('input')).hasValue('foozy'); + }); + + test('it renders parameters job group expanded', async function (assert) { + this.setProperties({ + event: { + meta: { + parameters: { + bar: { value: 'barzy' }, + foo: { value: 'foozy' }, + job1: { p1: { value: 'abc' }, p2: { value: 'xyz' } } + } + } + }, + pipeline: { + parameters: { + bar: ['barbar', 'bazbaz'], + foo: { value: 'foofoo' } + } + }, + jobs: [ + { + name: 'job1', + permutations: [{ p1: { value: 'p1' }, p2: { value: 'p2' } }] + } + ], + job: { name: 'job1' } + }); + + await render( + hbs`` + ); + + assert.dom('.group-title').exists({ count: 2 }); + assert + .dom(this.element.querySelectorAll('.group-title')[0]) + .hasText('Job: job1'); + assert.dom('.parameter-list.expanded .parameter').exists({ count: 2 }); + + const parameters = this.element.querySelectorAll( + '.parameter-list.expanded .parameter' + ); + + assert.dom(parameters[0].querySelector('label')).hasText('p1'); + assert.dom(parameters[0].querySelector('input')).hasValue('abc'); + assert.dom(parameters[1].querySelector('label')).hasText('p2'); + assert.dom(parameters[1].querySelector('input')).hasValue('xyz'); + }); + + test('it updates parameter value on input', async function (assert) { + const onUpdateParameters = sinon.spy(); + + this.setProperties({ + event: { + meta: { + parameters: { + foo: { value: 'foozy' }, + job1: { p1: { value: 'abc' } } + } + } + }, + pipeline: { + parameters: { + foo: { value: 'foofoo' } + } + }, + jobs: [ + { + name: 'job1', + permutations: [{ p1: { value: 'p1' } }] + } + ], + onUpdateParameters + }); + + await render( + hbs`` + ); + await fillIn('.parameter-list.expanded input', 'foobar'); + + assert.equal(onUpdateParameters.callCount, 1); + assert.true( + onUpdateParameters.calledWith({ + foo: { value: 'foobar' }, + job1: { p1: { value: 'abc' } } + }) + ); + }); + + test('it updates job parameter value on input', async function (assert) { + const onUpdateParameters = sinon.spy(); + + this.setProperties({ + event: { + meta: { + parameters: { + foo: { value: 'foozy' }, + job1: { p1: { value: 'abc' } } + } + } + }, + pipeline: { + parameters: { + foo: { value: 'foofoo' } + } + }, + jobs: [ + { + name: 'job1', + permutations: [{ p1: { value: 'p1' } }] + } + ], + job: { name: 'job1' }, + onUpdateParameters + }); + + await render( + hbs`` + ); + await fillIn('.parameter-list.expanded input', 'job123abc'); + + assert.equal(onUpdateParameters.callCount, 1); + assert.true( + onUpdateParameters.calledWith({ + foo: { value: 'foozy' }, + job1: { p1: { value: 'job123abc' } } + }) + ); + }); + + test('it updates parameter value on selection', async function (assert) { + const onUpdateParameters = sinon.spy(); + + this.setProperties({ + event: { + meta: { + parameters: { + foo: { value: 'foo' } + } + } + }, + pipeline: { + parameters: { + foo: ['foo', 'bar'] + } + }, + jobs: [], + onUpdateParameters + }); + + await render( + hbs`` + ); + + await selectChoose('.dropdown-selection-container', 'bar'); + + assert.equal(onUpdateParameters.callCount, 1); + assert.true( + onUpdateParameters.calledWith({ + foo: { value: 'bar' } + }) + ); + }); +});