diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-10 09:06:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-10 09:06:08 +0000 |
commit | c157f963db87a40a3ba7b94b339530ee83194bc8 (patch) | |
tree | 9f8f9468daf727cce39bc7487af8bd9a53b8c59d /app | |
parent | bd1e1afde56a9bd97e03ca24298e260dc071999e (diff) | |
download | gitlab-ce-c157f963db87a40a3ba7b94b339530ee83194bc8.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
24 files changed, 605 insertions, 104 deletions
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 106fe2e0cef..b371f6be268 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -7,6 +7,8 @@ import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; import DockerfileSelector from './template_selectors/dockerfile_selector'; import GitignoreSelector from './template_selectors/gitignore_selector'; import LicenseSelector from './template_selectors/license_selector'; +import toast from '~/vue_shared/plugins/global_toast'; +import { __ } from '~/locale'; export default class FileTemplateMediator { constructor({ editor, currentAction, projectId }) { @@ -19,6 +21,7 @@ export default class FileTemplateMediator { this.initDomElements(); this.initDropdowns(); this.initPageEvents(); + this.cacheFileContents(); } initTemplateSelectors() { @@ -40,6 +43,7 @@ export default class FileTemplateMediator { return { name: cfg.name, key: cfg.key, + id: cfg.key, }; }), }); @@ -58,6 +62,7 @@ export default class FileTemplateMediator { this.$fileContent = $fileEditor.find('#file-content'); this.$commitForm = $fileEditor.find('form'); this.$navLinks = $fileEditor.find('.nav-links'); + this.$templateTypes = this.$templateSelectors.find('.template-type-selector'); } initDropdowns() { @@ -113,7 +118,11 @@ export default class FileTemplateMediator { } }); - this.typeSelector.setToggleText(item.name); + this.setFilename(item.name); + + if (this.editor.getValue() !== '') { + this.setTypeSelectorToggleText(item.name); + } this.cacheToggleText(); } @@ -123,15 +132,24 @@ export default class FileTemplateMediator { } selectTemplateFile(selector, query, data) { + const self = this; + selector.renderLoading(); - // in case undo menu is already there - this.destroyUndoMenu(); + this.fetchFileTemplate(selector.config.type, query, data) .then(file => { - this.showUndoMenu(); this.setEditorContent(file); - this.setFilename(selector.config.name); selector.renderLoaded(); + this.typeSelector.setToggleText(selector.config.name); + toast(__(`${query} template applied`), { + action: { + text: __('Undo'), + onClick: (e, toastObj) => { + self.restoreFromCache(); + toastObj.goAway(0); + }, + }, + }); }) .catch(err => new Flash(`An error occurred while fetching the template: ${err}`)); } @@ -173,22 +191,6 @@ export default class FileTemplateMediator { return this.templateSelectors.find(selector => selector.config.key === key); } - showUndoMenu() { - this.$undoMenu.removeClass('hidden'); - - this.$undoBtn.on('click', () => { - this.restoreFromCache(); - this.destroyUndoMenu(); - }); - } - - destroyUndoMenu() { - this.cacheFileContents(); - this.cacheToggleText(); - this.$undoMenu.addClass('hidden'); - this.$undoBtn.off('click'); - } - hideTemplateSelectorMenu() { this.$templatesMenu.hide(); } @@ -210,6 +212,7 @@ export default class FileTemplateMediator { this.setEditorContent(this.cachedContent); this.setFilename(this.cachedFilename); this.setTemplateSelectorToggleText(); + this.setTypeSelectorToggleText(__('Select a template type')); } getTemplateSelectorToggleText() { @@ -228,6 +231,10 @@ export default class FileTemplateMediator { return this.typeSelector.getToggleText(); } + setTypeSelectorToggleText(text) { + this.typeSelector.setToggleText(text); + } + getFilename() { return this.$filenameInput.val(); } diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index 43f7aead8b9..d819452df68 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -19,7 +19,6 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { data: this.$dropdown.data('data'), filterable: true, selectable: true, - toggleLabel: item => item.name, search: { fields: ['name'], }, diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 659d57e6a6f..7d5e98889d3 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -20,7 +20,6 @@ export default class DockerfileSelector extends FileTemplateSelector { data: this.$dropdown.data('data'), filterable: true, selectable: true, - toggleLabel: item => item.name, search: { fields: ['name'], }, diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index a8067ec5c84..39a8937641d 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -18,7 +18,6 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { data: this.$dropdown.data('data'), filterable: true, selectable: true, - toggleLabel: item => item.name, search: { fields: ['name'], }, diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index d01ab9257d6..f4041835a7d 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -18,7 +18,6 @@ export default class BlobLicenseSelector extends FileTemplateSelector { data: this.$dropdown.data('data'), filterable: true, selectable: true, - toggleLabel: item => item.name, search: { fields: ['name'], }, diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js index db3c144cbe3..cb4e1aaa9ac 100644 --- a/app/assets/javascripts/blob/template_selectors/type_selector.js +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -16,7 +16,6 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector { data: this.config.dropdownData, filterable: false, selectable: true, - toggleLabel: item => item.name, clicked: options => this.mediator.selectTemplateTypeOptions(options), text: item => item.name, }); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index cfc72327ef7..e29509ce074 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,20 +1,120 @@ <script> +import _ from 'underscore'; import { GlLoadingIcon } from '@gitlab/ui'; import StageColumnComponent from './stage_column_component.vue'; import GraphMixin from '../../mixins/graph_component_mixin'; -import GraphWidthMixin from '~/pipelines/mixins/graph_width_mixin'; +import GraphWidthMixin from '../../mixins/graph_width_mixin'; +import LinkedPipelinesColumn from './linked_pipelines_column.vue'; +import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; export default { + name: 'PipelineGraph', components: { StageColumnComponent, GlLoadingIcon, + LinkedPipelinesColumn, + }, + mixins: [GraphMixin, GraphWidthMixin, GraphBundleMixin], + props: { + isLoading: { + type: Boolean, + required: true, + }, + pipeline: { + type: Object, + required: true, + }, + isLinkedPipeline: { + type: Boolean, + required: false, + default: false, + }, + mediator: { + type: Object, + required: true, + }, + type: { + type: String, + required: false, + default: 'main', + }, + }, + upstream: 'upstream', + downstream: 'downstream', + data() { + return { + triggeredTopIndex: 1, + }; + }, + computed: { + hasTriggeredBy() { + return ( + this.type !== this.$options.downstream && + this.triggeredByPipelines && + this.pipeline.triggered_by !== null + ); + }, + triggeredByPipelines() { + return this.pipeline.triggered_by; + }, + hasTriggered() { + return ( + this.type !== this.$options.upstream && + this.triggeredPipelines && + this.pipeline.triggered.length > 0 + ); + }, + triggeredPipelines() { + return this.pipeline.triggered; + }, + expandedTriggeredBy() { + return ( + this.pipeline.triggered_by && + _.isArray(this.pipeline.triggered_by) && + this.pipeline.triggered_by.find(el => el.isExpanded) + ); + }, + expandedTriggered() { + return this.pipeline.triggered && this.pipeline.triggered.find(el => el.isExpanded); + }, + + /** + * Calculates the margin top of the clicked downstream pipeline by + * adding the height of each linked pipeline and the margin + */ + marginTop() { + return `${this.triggeredTopIndex * 52}px`; + }, + pipelineTypeUpstream() { + return this.type !== this.$options.downstream && this.expandedTriggeredBy; + }, + pipelineTypeDownstream() { + return this.type !== this.$options.upstream && this.expandedTriggered; + }, + }, + methods: { + handleClickedDownstream(pipeline, clickedIndex) { + this.triggeredTopIndex = clickedIndex; + this.$emit('onClickTriggered', this.pipeline, pipeline); + }, + hasOnlyOneJob(stage) { + return stage.groups.length === 1; + }, + hasDownstream(index, length) { + return index === length - 1 && this.hasTriggered; + }, + hasUpstream(index) { + return index === 0 && this.hasTriggeredBy; + }, }, - mixins: [GraphMixin, GraphWidthMixin], }; </script> <template> <div class="build-content middle-block js-pipeline-graph"> - <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> + <div + class="pipeline-visualization pipeline-graph" + :class="{ 'pipeline-tab-content': !isLinkedPipeline }" + > <div :style="{ paddingLeft: `${graphLeftPadding}px`, @@ -23,21 +123,80 @@ export default { > <gl-loading-icon v-if="isLoading" class="m-auto" :size="3" /> - <ul v-if="!isLoading" class="stage-column-list"> + <pipeline-graph + v-if="pipelineTypeUpstream" + type="upstream" + class="d-inline-block upstream-pipeline" + :class="`js-upstream-pipeline-${expandedTriggeredBy.id}`" + :is-loading="false" + :pipeline="expandedTriggeredBy" + :is-linked-pipeline="true" + :mediator="mediator" + @onClickTriggeredBy=" + (parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline) + " + @refreshPipelineGraph="requestRefreshPipelineGraph" + /> + + <linked-pipelines-column + v-if="hasTriggeredBy" + :linked-pipelines="triggeredByPipelines" + :column-title="__('Upstream')" + graph-position="left" + @linkedPipelineClick=" + linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline) + " + /> + + <ul + v-if="!isLoading" + :class="{ + 'inline js-has-linked-pipelines': hasTriggered || hasTriggeredBy, + }" + class="stage-column-list align-top" + > <stage-column-component v-for="(stage, index) in graph" :key="stage.name" :class="{ - 'append-right-48': shouldAddRightMargin(index), + 'has-upstream prepend-left-64': hasUpstream(index), + 'has-downstream': hasDownstream(index, graph.length), + 'has-only-one-job': hasOnlyOneJob(stage), + 'append-right-46': shouldAddRightMargin(index), }" :title="capitalizeStageName(stage.name)" :groups="stage.groups" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" + :has-triggered-by="hasTriggeredBy" :action="stage.status.action" @refreshPipelineGraph="refreshPipelineGraph" /> </ul> + + <linked-pipelines-column + v-if="hasTriggered" + :linked-pipelines="triggeredPipelines" + :column-title="__('Downstream')" + graph-position="right" + @linkedPipelineClick="handleClickedDownstream" + /> + + <pipeline-graph + v-if="pipelineTypeDownstream" + type="downstream" + class="d-inline-block" + :class="`js-downstream-pipeline-${expandedTriggered.id}`" + :is-loading="false" + :pipeline="expandedTriggered" + :is-linked-pipeline="true" + :style="{ 'margin-top': marginTop }" + :mediator="mediator" + @onClickTriggered=" + (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline) + " + @refreshPipelineGraph="requestRefreshPipelineGraph" + /> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue new file mode 100644 index 00000000000..4e7d77863b9 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -0,0 +1,65 @@ +<script> +import { GlLoadingIcon, GlTooltipDirective, GlButton } from '@gitlab/ui'; +import CiStatus from '~/vue_shared/components/ci_icon.vue'; + +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + CiStatus, + GlLoadingIcon, + GlButton, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + }, + computed: { + tooltipText() { + return `${this.projectName} - ${this.pipelineStatus.label}`; + }, + buttonId() { + return `js-linked-pipeline-${this.pipeline.id}`; + }, + pipelineStatus() { + return this.pipeline.details.status; + }, + projectName() { + return this.pipeline.project.name; + }, + }, + methods: { + onClickLinkedPipeline() { + this.$root.$emit('bv::hide::tooltip', this.buttonId); + this.$emit('pipelineClicked'); + }, + }, +}; +</script> + +<template> + <li class="linked-pipeline build"> + <div class="curve"></div> + <gl-button + :id="buttonId" + v-gl-tooltip + :title="tooltipText" + class="js-linked-pipeline-content linked-pipeline-content" + data-qa-selector="linked_pipeline_button" + :class="`js-pipeline-expand-${pipeline.id}`" + @click="onClickLinkedPipeline" + > + <gl-loading-icon v-if="pipeline.isLoading" class="js-linked-pipeline-loading d-inline" /> + <ci-status + v-else + :status="pipelineStatus" + css-classes="position-top-0" + class="js-linked-pipeline-status" + /> + <span class="str-truncated align-bottom"> {{ projectName }} • #{{ pipeline.id }} </span> + </gl-button> + </li> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue new file mode 100644 index 00000000000..6efdde2b17e --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -0,0 +1,52 @@ +<script> +import LinkedPipeline from './linked_pipeline.vue'; + +export default { + components: { + LinkedPipeline, + }, + props: { + columnTitle: { + type: String, + required: true, + }, + linkedPipelines: { + type: Array, + required: true, + }, + graphPosition: { + type: String, + required: true, + }, + }, + computed: { + columnClass() { + const positionValues = { + right: 'prepend-left-64', + left: 'append-right-32', + }; + return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; + }, + }, +}; +</script> + +<template> + <div :class="columnClass" class="stage-column linked-pipelines-column"> + <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div> + <div class="cross-project-triangle"></div> + <ul> + <linked-pipeline + v-for="(pipeline, index) in linkedPipelines" + :key="pipeline.id" + :class="{ + 'flat-connector-before': index === 0 && graphPosition === 'right', + active: pipeline.isExpanded, + 'left-connector': pipeline.isExpanded && graphPosition === 'left', + }" + :pipeline="pipeline" + @pipelineClicked="$emit('linkedPipelineClick', pipeline, index)" + /> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index d5c124dc0ca..db7714808fd 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,6 +1,6 @@ <script> import _ from 'underscore'; -import stageColumnMixin from 'ee_else_ce/pipelines/mixins/stage_column_mixin'; +import stageColumnMixin from '../../mixins/stage_column_mixin'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js index dd79ade5bc9..c76869d90d5 100644 --- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js @@ -1,16 +1,68 @@ -import Flash from '~/flash'; +import flash from '~/flash'; import { __ } from '~/locale'; export default { methods: { - clickTriggeredByPipeline() {}, - clickTriggeredPipeline() {}, + getExpandedPipelines(pipeline) { + this.mediator.service + .getPipeline(this.mediator.getExpandedParameters()) + .then(response => { + this.mediator.store.toggleLoading(pipeline); + this.mediator.store.storePipeline(response.data); + this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() }); + }) + .catch(() => { + this.mediator.store.toggleLoading(pipeline); + flash(__('An error occurred while fetching the pipeline.')); + }); + }, + /** + * Called when a linked pipeline is clicked. + * + * If the pipeline is collapsed we will start polling it & we will reset the other pipelines. + * If the pipeline is expanded we will close it. + * + * @param {String} method Method to fetch the pipeline + * @param {String} storeKey Store property that will be updates + * @param {String} resetStoreKey Store key for the visible pipeline that will need to be reset + * @param {Object} pipeline The clicked pipeline + */ + clickPipeline(parentPipeline, pipeline, openMethod, closeMethod) { + if (!pipeline.isExpanded) { + this.mediator.store[openMethod](parentPipeline, pipeline); + this.mediator.store.toggleLoading(pipeline); + this.mediator.poll.stop(); + + this.getExpandedPipelines(pipeline); + } else { + this.mediator.store[closeMethod](pipeline); + this.mediator.poll.stop(); + + this.mediator.poll.enable({ data: this.mediator.getExpandedParameters() }); + } + }, + clickTriggeredByPipeline(parentPipeline, pipeline) { + this.clickPipeline( + parentPipeline, + pipeline, + 'openTriggeredByPipeline', + 'closeTriggeredByPipeline', + ); + }, + clickTriggeredPipeline(parentPipeline, pipeline) { + this.clickPipeline( + parentPipeline, + pipeline, + 'openTriggeredPipeline', + 'closeTriggeredPipeline', + ); + }, requestRefreshPipelineGraph() { // When an action is clicked // (wether in the dropdown or in the main nodes, we refresh the big graph) this.mediator .refreshPipeline() - .catch(() => Flash(__('An error occurred while making the request.'))); + .catch(() => flash(__('An error occurred while making the request.'))); }, }, }; diff --git a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js index 64283ed0e58..3f3007ba11a 100644 --- a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js +++ b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js @@ -1,7 +1,14 @@ export default { + props: { + hasTriggeredBy: { + type: Boolean, + required: false, + default: false, + }, + }, methods: { buildConnnectorClass(index) { - return index === 0 && !this.isFirstColumn ? 'left-connector' : ''; + return index === 0 && (!this.isFirstColumn || this.hasTriggeredBy) ? 'left-connector' : ''; }, }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index b8976f77bac..b6f8716d37d 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -2,8 +2,8 @@ import Vue from 'vue'; import Flash from '~/flash'; import Translate from '~/vue_shared/translate'; import { __ } from '~/locale'; -import pipelineGraph from 'ee_else_ce/pipelines/components/graph/graph_component.vue'; -import GraphEEMixin from 'ee_else_ce/pipelines/mixins/graph_pipeline_bundle_mixin'; +import pipelineGraph from './components/graph/graph_component.vue'; +import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import PipelinesMediator from './pipeline_details_mediator'; import pipelineHeader from './components/header_component.vue'; import eventHub from './event_hub'; @@ -23,7 +23,7 @@ export default () => { components: { pipelineGraph, }, - mixins: [GraphEEMixin], + mixins: [GraphBundleMixin], data() { return { mediator, diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js index c8819cf35cf..bf021a0b447 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js @@ -1,5 +1,5 @@ import Visibility from 'visibilityjs'; -import PipelineStore from 'ee_else_ce/pipelines/stores/pipeline_store'; +import PipelineStore from './stores/pipeline_store'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; import { __ } from '../locale'; diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js index 259278b6410..441c9f3c25f 100644 --- a/app/assets/javascripts/pipelines/stores/pipeline_store.js +++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js @@ -1,10 +1,196 @@ +import Vue from 'vue'; +import _ from 'underscore'; + export default class PipelineStore { constructor() { this.state = {}; this.state.pipeline = {}; + this.state.expandedPipelines = []; } - + /** + * For the triggered pipelines adds the `isExpanded` key + * + * For the triggered_by pipeline adds the `isExpanded` key + * and saves it as an array + * + * @param {Object} pipeline + */ storePipeline(pipeline = {}) { - this.state.pipeline = pipeline; + const pipelineCopy = Object.assign({}, pipeline); + + if (pipelineCopy.triggered_by) { + pipelineCopy.triggered_by = [pipelineCopy.triggered_by]; + + const oldTriggeredBy = + this.state.pipeline && + this.state.pipeline.triggered_by && + this.state.pipeline.triggered_by[0]; + + this.parseTriggeredByPipelines(oldTriggeredBy, pipelineCopy.triggered_by[0]); + } + + if (pipelineCopy.triggered && pipelineCopy.triggered.length) { + pipelineCopy.triggered.forEach(el => { + const oldPipeline = + this.state.pipeline && + this.state.pipeline.triggered && + this.state.pipeline.triggered.find(element => element.id === el.id); + + this.parseTriggeredPipelines(oldPipeline, el); + }); + } + + this.state.pipeline = pipelineCopy; + } + + /** + * Recursiverly parses the triggered by pipelines. + * + * Sets triggered_by as an array, there is always only 1 triggered_by pipeline. + * Adds key `isExpanding` + * Keeps old isExpading value due to polling + * + * @param {Array} parentPipeline + * @param {Object} pipeline + */ + parseTriggeredByPipelines(oldPipeline = {}, newPipeline) { + // keep old value in case it's opened because we're polling + + Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false); + // add isLoading property + Vue.set(newPipeline, 'isLoading', false); + + if (newPipeline.triggered_by) { + if (!_.isArray(newPipeline.triggered_by)) { + Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] }); + } + this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]); + } + } + + /** + * Recursively parses the triggered pipelines + * @param {Array} parentPipeline + * @param {Object} pipeline + */ + parseTriggeredPipelines(oldPipeline = {}, newPipeline) { + // keep old value in case it's opened because we're polling + Vue.set(newPipeline, 'isExpanded', oldPipeline.isExpanded || false); + + // add isLoading property + Vue.set(newPipeline, 'isLoading', false); + + if (newPipeline.triggered && newPipeline.triggered.length > 0) { + newPipeline.triggered.forEach(el => { + const oldTriggered = + oldPipeline.triggered && oldPipeline.triggered.find(element => element.id === el.id); + this.parseTriggeredPipelines(oldTriggered, el); + }); + } + } + + /** + * Recursively resets all triggered by pipelines + * + * @param {Object} pipeline + */ + resetTriggeredByPipeline(parentPipeline, pipeline) { + parentPipeline.triggered_by.forEach(el => this.closePipeline(el)); + + if (pipeline.triggered_by && pipeline.triggered_by) { + this.resetTriggeredByPipeline(pipeline, pipeline.triggered_by); + } + } + + /** + * Opens the clicked pipeline and closes all other ones. + * @param {Object} pipeline + */ + openTriggeredByPipeline(parentPipeline, pipeline) { + // first we need to reset all triggeredBy pipelines + this.resetTriggeredByPipeline(parentPipeline, pipeline); + + this.openPipeline(pipeline); + } + + /** + * On click, will close the given pipeline and all nested triggered by pipelines + * + * @param {Object} pipeline + */ + closeTriggeredByPipeline(pipeline) { + this.closePipeline(pipeline); + + if (pipeline.triggered_by && pipeline.triggered_by.length) { + pipeline.triggered_by.forEach(triggeredBy => this.closeTriggeredByPipeline(triggeredBy)); + } + } + + /** + * Recursively closes all triggered pipelines for the given one. + * + * @param {Object} pipeline + */ + resetTriggeredPipelines(parentPipeline, pipeline) { + parentPipeline.triggered.forEach(el => this.closePipeline(el)); + + if (pipeline.triggered && pipeline.triggered.length) { + pipeline.triggered.forEach(el => this.resetTriggeredPipelines(pipeline, el)); + } + } + + /** + * Opens the clicked triggered pipeline and closes all other ones. + * + * @param {Object} pipeline + */ + openTriggeredPipeline(parentPipeline, pipeline) { + this.resetTriggeredPipelines(parentPipeline, pipeline); + + this.openPipeline(pipeline); + } + + /** + * On click, will close the given pipeline and all the nested triggered ones + * @param {Object} pipeline + */ + closeTriggeredPipeline(pipeline) { + this.closePipeline(pipeline); + + if (pipeline.triggered && pipeline.triggered.length) { + pipeline.triggered.forEach(triggered => this.closeTriggeredPipeline(triggered)); + } + } + + /** + * Utility function, Closes the given pipeline + * @param {Object} pipeline + */ + closePipeline(pipeline) { + Vue.set(pipeline, 'isExpanded', false); + // remove the pipeline from the parameters + this.removeExpandedPipelineToRequestData(pipeline.id); + } + + /** + * Utility function, Opens the given pipeline + * @param {Object} pipeline + */ + openPipeline(pipeline) { + Vue.set(pipeline, 'isExpanded', true); + // add the pipeline to the parameters + this.addExpandedPipelineToRequestData(pipeline.id); + } + // eslint-disable-next-line class-methods-use-this + toggleLoading(pipeline) { + Vue.set(pipeline, 'isLoading', !pipeline.isLoading); + } + + addExpandedPipelineToRequestData(id) { + this.state.expandedPipelines.push(id); + } + + removeExpandedPipelineToRequestData(id) { + this.state.expandedPipelines.splice(this.state.expandedPipelines.findIndex(el => el === id), 1); } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 29f63e9578d..ce74aa6ed02 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -326,8 +326,9 @@ } .dropdown-header { - color: $gl-text-color-secondary; + color: $black; font-size: 13px; + font-weight: $gl-font-weight-bold; line-height: $gl-line-height; padding: $dropdown-item-padding-y $dropdown-item-padding-x; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 655b297295a..65d0ce8c52e 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -47,14 +47,19 @@ margin-right: 10px; } - .new-file-name { + .new-file-name, + .new-file-path { display: inline-block; - max-width: 420px; + max-width: 250px; float: left; @media(max-width: map-get($grid-breakpoints, lg)-1) { width: 180px; } + + @media (max-width: 1360px) { + width: auto; + } } .file-buttons { @@ -98,13 +103,14 @@ } -@include media-breakpoint-down(sm) { +@include media-breakpoint-down(md) { .file-editor { .file-title { display: block; } - .new-file-name { + .new-file-name, + .new-file-path { max-width: none; width: 100%; margin-bottom: 3px; @@ -146,20 +152,17 @@ vertical-align: top; display: inline-block; - @media(max-width: map-get($grid-breakpoints, md)-1) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { display: block; margin: 19px 0 12px; } } .template-selectors-menu { - display: inline-block; + display: flex; vertical-align: top; - margin: 14px 0 0 16px; - padding: 0 0 0 14px; - border-left: 1px solid $border-color; - @media(max-width: map-get($grid-breakpoints, md)-1) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { display: block; width: 100%; margin: 5px 0; @@ -168,24 +171,11 @@ } } -.templates-selectors-label { - display: inline-block; - vertical-align: top; - margin-top: 6px; - line-height: 21px; - - @media(max-width: map-get($grid-breakpoints, md)-1) { - display: block; - margin: 5px 0; - } -} - .template-selector-dropdowns-wrap { display: inline-block; - margin: 5px 0 0 8px; vertical-align: top; - @media(max-width: map-get($grid-breakpoints, md)-1) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { display: block; width: 100%; margin: 0 0 16px; @@ -199,9 +189,8 @@ display: inline-block; vertical-align: top; font-family: $regular_font; - margin-top: -5px; - @media(max-width: map-get($grid-breakpoints, md)-1) { + @media(max-width: map-get($grid-breakpoints, lg)-1) { display: block; width: 100%; margin: 5px 0; @@ -212,30 +201,22 @@ } .dropdown-menu-toggle { - width: 250px; + width: 200px; vertical-align: top; - @media(max-width: map-get($grid-breakpoints, md)-1) { + @media (max-width: map-get($grid-breakpoints, xl)-1) { + width: auto; + } + + @media(max-width: map-get($grid-breakpoints, lg)-1) { display: block; width: 100%; margin: 5px 0; } } - } } -.template-selectors-undo-menu { - display: inline-block; - margin: 7px 0 0 10px; - - @media(max-width: map-get($grid-breakpoints, md)-1) { - display: block; - width: 100%; - margin: 20px 0; - } - - button { - margin: -4px 0 0 15px; - } +.editor-title-row { + margin-bottom: 20px; } diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index f2a3695d2eb..1093efee85a 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Knative < ApplicationRecord - VERSION = '0.6.0' + VERSION = '0.7.0' REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts' METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml' FETCH_IP_ADDRESS_DELAY = 30.seconds diff --git a/app/models/project.rb b/app/models/project.rb index 9bd9f969331..80628cc07df 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1850,6 +1850,7 @@ class Project < ApplicationRecord Gitlab::Ci::Variables::Collection.new .append(key: 'CI_PROJECT_ID', value: id.to_s) .append(key: 'CI_PROJECT_NAME', value: path) + .append(key: 'CI_PROJECT_TITLE', value: title) .append(key: 'CI_PROJECT_PATH', value: full_path) .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug) .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb index a46f8af1466..40ac52d96af 100644 --- a/app/serializers/projects/serverless/service_entity.rb +++ b/app/serializers/projects/serverless/service_entity.rb @@ -44,7 +44,7 @@ module Projects end expose :url do |service| - "http://#{service.dig('status', 'domain')}" + service.dig('status', 'url') end expose :description do |service| diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 283b845e40d..961b873b571 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -3,20 +3,22 @@ - is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) .file-holder-bottom-radius.file-holder.file.append-bottom-default - .js-file-title.file-title.clearfix{ data: { current_action: action } } + .js-file-title.file-title.align-items-center.clearfix{ data: { current_action: action } } .editor-ref.block-truncated = sprite_icon('fork', size: 12) = ref - if current_action?(:edit) || current_action?(:update) %span.pull-left.append-right-10 - = text_field_tag 'file_path', (params[:file_path] || @path), - class: 'form-control new-file-path js-file-path-name-input' + = text_field_tag 'file_path', (params[:file_path] || @path), + class: 'form-control new-file-path js-file-path-name-input' + = render 'template_selectors' - if current_action?(:new) || current_action?(:create) %span.pull-left.append-right-10 \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name js-file-path-name-input' + = render 'template_selectors' .file-buttons - if is_markdown diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index bd46f5a4349..5ecfa135446 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -1,17 +1,12 @@ -.template-selectors-menu - .templates-selectors-label - Template +.template-selectors-menu.gl-pl-2 .template-selector-dropdowns-wrap .template-type-selector.js-template-type-selector-wrap.hidden - = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', title: "Choose a template type" } ) + = dropdown_tag(_("Select a template type"), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable'} ) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector qa-license-dropdown', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project) } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } ) - .template-selectors-undo-menu.hidden - %span.text-info Template applied - %button.btn.btn-sm.btn-info Undo + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } ) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 51e42091ab8..870e37488cf 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -11,7 +11,6 @@ .editor-title-row %h3.page-title.blob-edit-page-title Edit file - = render 'template_selectors' .file-editor %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs %li.active diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 4be87b9e074..c5e3923f1e8 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -5,7 +5,6 @@ .editor-title-row %h3.page-title.blob-new-page-title New file - = render 'template_selectors' .file-editor = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do = render 'projects/blob/editor', ref: @ref |