Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-10 12:06:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-10 12:06:08 +0300
commitc157f963db87a40a3ba7b94b339530ee83194bc8 (patch)
tree9f8f9468daf727cce39bc7487af8bd9a53b8c59d /app
parentbd1e1afde56a9bd97e03ca24298e260dc071999e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js49
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js1
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js1
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js1
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js1
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue169
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue65
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue52
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js60
-rw-r--r--app/assets/javascripts/pipelines/mixins/stage_column_mixin.js9
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js190
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/pages/editor.scss65
-rw-r--r--app/models/clusters/applications/knative.rb2
-rw-r--r--app/models/project.rb1
-rw-r--r--app/serializers/projects/serverless/service_entity.rb2
-rw-r--r--app/views/projects/blob/_editor.html.haml8
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml17
-rw-r--r--app/views/projects/blob/edit.html.haml1
-rw-r--r--app/views/projects/blob/new.html.haml1
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 }} &#8226; #{{ 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