Commit c157f963 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent bd1e1afd
......@@ -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();
}
......
......@@ -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'],
},
......
......@@ -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'],
},
......
......@@ -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'],
},
......
......@@ -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'],
},
......
......@@ -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,
});
......
<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>
......
<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>
<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>
<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';
......
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.')));
},
},
};
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' : '';
},
},
};
......@@ -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,
......
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';
......
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);
}
}
......@@ -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;
}
......
......@@ -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;
}
......@@ -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
......
......@@ -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)
......
......@@ -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|
......
......@@ -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
......
.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) } } )
......@@ -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
......
......@@ -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
......
---
title: Fix empty security dashboard for public projects
merge_request: 17915
author:
type: fixed
---
title: Fix usability problems with the file template picker
merge_request: 17522
author:
type: changed
---
title: Port over EE pipeline functionality to CE
merge_request: 18136
author:
type: changed
---
title: Introduce CI_PROJECT_TITLE as predefined environment variable
merge_request: 17849
author: Nejc Habjan
type: added
---
title: Knative version bump 0.6 -> 0.7
merge_request: 17367
author: Chris Baumbauer
type: changed
......@@ -61,4 +61,10 @@
- virtualservices.networking.istio.io
- rbacconfigs.rbac.istio.io
- servicerolebindings.rbac.istio.io
- serviceroles.rbac.istio.io
\ No newline at end of file
- serviceroles.rbac.istio.io
- cloudwatches.config.istio.io
- clusterrbacconfigs.rbac.istio.io
- dogstatsds.config.istio.io
- ingresses.networking.internal.knative.dev
- sidecars.networking.istio.io
- zipkins.config.istio.io
......@@ -209,6 +209,6 @@ panel_groups:
weight: 1
metrics:
- id: system_metrics_knative_function_invocation_count
query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])/3))'
query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_app=~"%{function_name}.*"}[1m])*60))'
label: invocations / minute
unit: requests
# frozen_string_literal: true
class UpdateKnativePrometheusQueryForInvocationCount < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
# no-op
end
end
......@@ -266,6 +266,7 @@ export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-foss"
export CI_PROJECT_ID="34"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-foss"
export CI_PROJECT_NAME="gitlab-foss"
export CI_PROJECT_TITLE="GitLab FOSS"
export CI_PROJECT_NAMESPACE="gitlab-org"
export CI_PROJECT_PATH="gitlab-org/gitlab-foss"
export CI_PROJECT_URL="https://example.com/gitlab-org/gitlab-foss"
......@@ -707,6 +708,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace
++ CI_PROJECT_NAME=ci-debug-trace
++ export 'CI_PROJECT_TITLE="GitLab FOSS'
++ CI_PROJECT_TITLE='GitLab FOSS'
++ export CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
++ CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
++ export CI_PROJECT_NAMESPACE=gitlab-examples
......
......@@ -87,7 +87,8 @@ future GitLab releases.**
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
| `CI_PROJECT_ID` | all | all | The unique id of the current project that GitLab CI uses internally |
| `CI_PROJECT_NAME` | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
| `CI_PROJECT_NAME` | 8.10 | 0.5 | The name of the directory for the project that is currently being built. For example, if the project URL is `gitlab.example.com/group-name/project-1`, the `CI_PROJECT_NAME` would be `project-1`. |
| `CI_PROJECT_TITLE` | 12.4 | all | The human-readable project name as displayed in the GitLab web interface. |
| `CI_PROJECT_NAMESPACE` | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
| `CI_PROJECT_PATH` | 8.10 | 0.5 | The namespace with project name |
| `CI_PROJECT_PATH_SLUG` | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
......
......@@ -19,10 +19,16 @@ module Gitlab
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
end
job_thread_cputime_start = get_thread_cputime
realtime = Benchmark.realtime do
yield
end
job_thread_cputime_end = get_thread_cputime
job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
@metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
@metrics[:sidekiq_jobs_completion_seconds].observe(labels, realtime)
rescue Exception # rubocop: disable Lint/RescueException
@metrics[:sidekiq_jobs_failed_total].increment(labels, 1)
......@@ -35,6 +41,7 @@ module Gitlab
def init_metrics
{
sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
......@@ -47,6 +54,10 @@ module Gitlab
queue: queue
}
end
def get_thread_cputime
defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0
end
end
end
end
......@@ -13,14 +13,6 @@ module Gitlab
path
end
# Run system command without outputting to stdout.
#
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
Popen.popen(cmd).last.zero?
end
def force_utf8(str)
str.dup.force_encoding(Encoding::UTF_8)
end
......
......@@ -1758,6 +1758,9 @@ msgstr ""
msgid "Apply a label"
msgstr ""
msgid "Apply a template"
msgstr ""
msgid "Apply suggestion"
msgstr ""
......@@ -14372,6 +14375,9 @@ msgstr ""
msgid "Select a template repository"
msgstr ""
msgid "Select a template type"
msgstr ""
msgid "Select a timezone"
msgstr ""
......
......@@ -74,5 +74,3 @@ module QA::Page
end
end
end
QA::Page::Project::Pipeline::Show.prepend_if_ee('QA::EE::Page::Project::Pipeline::Show')
......@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a Dockerfile file' do
wait_for_requests
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end
end
......@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitignore file' do
wait_for_requests
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Rails')
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('/.bundle')
expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
end
......
......@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
wait_for_requests
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Jekyll')
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
expect(page).to have_content('This file is a template, and might need editing before it works on your project')
expect(page).to have_content('jekyll build -d test')
end
......
......@@ -64,7 +64,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Apply a license template'
click_button 'Apply a template'
click_link template
wait_for_requests
end
......
......@@ -37,7 +37,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file
def select_template(template)
page.within('.js-license-selector-wrap') do
click_button 'Apply a license template'
click_button 'Apply a template'
click_link template
wait_for_requests
end
......
......@@ -24,8 +24,9 @@ describe 'Projects > Files > Template type dropdown selector', :js do
try_selecting_all_types
end
it 'updates toggle value when input matches' do
it 'updates template type toggle value when template is chosen' do
fill_in 'file_path', with: '.gitignore'
select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore')
end
end
......@@ -70,6 +71,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end
it 'toggle is set to the correct value' do
select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore')
end
......@@ -88,7 +90,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end
it 'toggle is set to the proper value' do
check_type_selector_toggle_text('Choose type')
check_type_selector_toggle_text('Select a template type')
end
it 'selects every template type correctly' do
......@@ -103,16 +105,15 @@ def check_type_selector_display(is_visible)
end
def try_selecting_all_types
try_selecting_template_type('LICENSE', 'Apply a license template')
try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template')
try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template')
try_selecting_template_type('.gitignore', 'Apply a .gitignore template')
try_selecting_template_type('LICENSE', 'Apply a template')
try_selecting_template_type('Dockerfile', 'Apply a template')
try_selecting_template_type('.gitlab-ci.yml', 'Apply a template')
try_selecting_template_type('.gitignore', 'Apply a template')
end
def try_selecting_template_type(template_type, selector_label)
select_template_type(template_type)
check_template_selector_display(selector_label)
check_type_selector_toggle_text(template_type)
end
def select_template_type(template_type)
......@@ -120,6 +121,11 @@ def select_template_type(template_type)
find('.dropdown-content li', text: template_type).click
end
def select_template(type, template)
find(".js-#{type}-selector-wrap").click
find('.dropdown-content li', text: template).click
end
def check_template_selector_display(content)
expect(page).to have_content(content)
end
......
......@@ -13,11 +13,12 @@ describe 'Projects > Files > Template Undo Button', :js do
context 'editing a matching file and applying a template' do
before do
visit project_edit_blob_path(project, File.join(project.default_branch, "LICENSE"))
select_file_template_type('LICENSE')
select_file_template('.js-license-selector', 'Apache License 2.0')
end
it 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
try_template_undo('http://www.apache.org/licenses/', 'Apply a template')
end
end
......@@ -29,7 +30,7 @@ describe 'Projects > Files > Template Undo Button', :js do
end
it 'reverts template application' do
try_template_undo('http://www.apache.org/licenses/', 'Apply a license template')
try_template_undo('http://www.apache.org/licenses/', 'Apply a template')
end
end
end
......@@ -45,12 +46,12 @@ def check_toggle_text_set(neutral_toggle_text)
end
def check_undo_button_display
expect(page).to have_content('Template applied')
expect(page).to have_css('.template-selectors-undo-menu .btn-info')
expect(page).to have_content('template applied')
expect(page).to have_css('.toasted-container')
end
def check_content_reverted(template_content)
find('.template-selectors-undo-menu .btn-info').click
find('.toasted-container a', text: 'Undo').click
expect(page).not_to have_content(template_content)
expect(page).to have_css('.template-type-selector .dropdown-toggle-text')
end
......
......@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/list/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore';
import { release } from '../mock_data';
import { release } from '../../mock_data';
import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
......
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import PipelineStore from '~/pipelines/stores/pipeline_store';
import graphComponent from '~/pipelines/components/graph/graph_component.vue';
import graphJSON from './mock_data';
import linkedPipelineJSON from '../linked_pipelines_mock.json';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent);
const store = new PipelineStore();
store.storePipeline(linkedPipelineJSON);
const mediator = new PipelinesMediator({ endpoint: '' });
let component;
beforeEach(() => {
......@@ -22,6 +29,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: true,
pipeline: {},
mediator,
});
expect(component.$el.querySelector('.loading-icon')).toBeDefined();
......@@ -33,6 +41,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
mediator,
});
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
......@@ -57,11 +66,205 @@ describe('graph component', () => {
});
});
describe('when linked pipelines are present', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: store.state.pipeline,
mediator,
});
});
describe('rendered output', () => {
it('should include the pipelines graph', () => {
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
});
it('should not include the loading icon', () => {
expect(component.$el.querySelector('.fa-spinner')).toBeNull();
});
it('should include the stage column list', () => {
expect(component.$el.querySelector('.stage-column-list')).not.toBeNull();
});
it('should include the no-margin class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('no-margin')).toEqual(true);
});
it('should include the has-only-one-job class on the first child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column',
);
expect(firstStageColumnElement.classList.contains('has-only-one-job')).toEqual(true);
});
it('should include the left-margin class on the second child', () => {
const firstStageColumnElement = component.$el.querySelector(
'.stage-column-list .stage-column:last-child',
);
expect(firstStageColumnElement.classList.contains('left-margin')).toEqual(true);
});
it('should include the js-has-linked-pipelines flag', () => {
expect(component.$el.querySelector('.js-has-linked-pipelines')).not.toBeNull();
});
});
describe('computeds and methods', () => {
describe('capitalizeStageName', () => {
it('it capitalizes the stage name', () => {
expect(component.capitalizeStageName('mystage')).toBe('Mystage');
});
});
describe('stageConnectorClass', () => {
it('it returns left-margin when there is a triggerer', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
});
});
describe('linked pipelines components', () => {
beforeEach(() => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: store.state.pipeline,
mediator,
});
});
it('should render an upstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Upstream');
});
it('should render a downstream pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).not.toBeNull();
expect(component.$el.innerHTML).toContain('Downstream');
});
describe('triggered by', () => {
describe('on click', () => {
it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-12').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggeredBy',
component.pipeline,
component.pipeline.triggered_by[0],
);
});
});
describe('with expanded pipeline', () => {
it('should render expanded pipeline', done => {
// expand the pipeline
store.state.pipeline.triggered_by[0].isExpanded = true;
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: store.state.pipeline,
mediator,
});
Vue.nextTick()
.then(() => {
expect(component.$el.querySelector('.js-upstream-pipeline-12')).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
});
});
describe('triggered', () => {
describe('on click', () => {
it('should emit `onClickTriggered`', () => {
spyOn(component, '$emit');
component.$el.querySelector('#js-linked-pipeline-34993051').click();
expect(component.$emit).toHaveBeenCalledWith(
'onClickTriggered',
component.pipeline,
component.pipeline.triggered[0],
);
});
});
describe('with expanded pipeline', () => {
it('should render expanded pipeline', done => {
// expand the pipeline
store.state.pipeline.triggered[0].isExpanded = true;
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: store.state.pipeline,
mediator,
});
Vue.nextTick()
.then(() => {
expect(
component.$el.querySelector('.js-downstream-pipeline-34993051'),
).not.toBeNull();
})
.then(done)
.catch(done.fail);
});
});
});
});
});
describe('when linked pipelines are not present', () => {
beforeEach(() => {
const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null });
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline,
mediator,
});
});
describe('rendered output', () => {
it('should include the first column with a no margin', () => {
const firstColumn = component.$el.querySelector('.stage-column:first-child');
expect(firstColumn.classList.contains('no-margin')).toEqual(true);
});
it('should not render a linked pipelines column', () => {
expect(component.$el.querySelector('.linked-pipelines-column')).toBeNull();
});
});
describe('stageConnectorClass', () => {
it('it returns left-margin when no triggerer and there is one job', () => {
expect(component.stageConnectorClass(0, { groups: ['job'] })).toBe('no-margin');
});
it('it returns left-margin when no triggerer and not the first stage', () => {
expect(component.stageConnectorClass(99, { groups: ['job'] })).toBe('left-margin');
});
});
});
describe('capitalizeStageName', () => {
it('capitalizes and escapes stage name', () => {
component = mountComponent(GraphComponent, {
isLoading: false,
pipeline: graphJSON,
mediator,
});
expect(
......
import Vue from 'vue';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './linked_pipelines_mock_data';
const mockPipeline = mockData.triggered[0];
describe('Linked pipeline', () => {
const Component = Vue.extend(LinkedPipelineComponent);
let vm;
afterEach(() => {
vm.$destroy();
});
describe('rendered output', () => {
const props = {
pipeline: mockPipeline,
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('should render a list item as the containing element', () => {
expect(vm.$el.tagName).toBe('LI');
});
it('should render a button', () => {
const linkElement = vm.$el.querySelector('.js-linked-pipeline-content');
expect(linkElement).not.toBeNull();
});
it('should render the project name', () => {
expect(vm.$el.innerText).toContain(props.pipeline.project.name);
});
it('should render an svg within the status container', () => {
const pipelineStatusElement = vm.$el.querySelector('.js-linked-pipeline-status');
expect(pipelineStatusElement.querySelector('svg')).not.toBeNull();
});
it('should render the pipeline status icon svg', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-running')).not.toBeNull();
expect(vm.$el.querySelector('.js-ci-status-icon-running').innerHTML).toContain('<svg');
});
it('should have a ci-status child component', () => {
expect(vm.$el.querySelector('.js-linked-pipeline-status')).not.toBeNull();
});
it('should render the pipeline id', () => {
expect(vm.$el.innerText).toContain(`#${props.pipeline.id}`);
});
it('should correctly compute the tooltip text', () => {
expect(vm.tooltipText).toContain(mockPipeline.project.name);
expect(vm.tooltipText).toContain(mockPipeline.details.status.label);
});
it('should render the tooltip text as the title attribute', () => {
const tooltipRef = vm.$el.querySelector('.js-linked-pipeline-content');
const titleAttr = tooltipRef.getAttribute('data-original-title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
it('does not render the loading icon when isLoading is false', () => {
expect(vm.$el.querySelector('.js-linked-pipeline-loading')).toBeNull();
});
});
describe('when isLoading is true', () => {
const props = {
pipeline: { ...mockPipeline, isLoading: true },
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('renders a loading icon', () => {
expect(vm.$el.querySelector('.js-linked-pipeline-loading')).not.toBeNull();
});
});
describe('on click', () => {
const props = {
pipeline: mockPipeline,
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
it('emits `pipelineClicked` event', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('button').click();
expect(vm.$emit).toHaveBeenCalledWith('pipelineClicked');
});
it('should emit `bv::hide::tooltip` to close the tooltip', () => {
spyOn(vm.$root, '$emit');
vm.$el.querySelector('button').click();
expect(vm.$root.$emit.calls.argsFor(0)).toEqual([
'bv::hide::tooltip',
'js-linked-pipeline-132',
]);
});
});
});
import Vue from 'vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './linked_pipelines_mock_data';
describe('Linked Pipelines Column', () => {
const Component = Vue.extend(LinkedPipelinesColumn);
const props = {
columnTitle: 'Upstream',
linkedPipelines: mockData.triggered,
graphPosition: 'right',
};
let vm;
beforeEach(() => {
vm = mountComponent(Component, props);
});
afterEach(() => {
vm.$destroy();
});
it('renders the pipeline orientation', () => {
const titleElement = vm.$el.querySelector('.linked-pipelines-column-title');
expect(titleElement.innerText).toContain(props.columnTitle);
});
it('has the correct number of linked pipeline child components', () => {
expect(vm.$children.length).toBe(props.linkedPipelines.length);
});
it('renders the correct number of linked pipelines', () => {
const linkedPipelineElements = vm.$el.querySelectorAll('.linked-pipeline');
expect(linkedPipelineElements.length).toBe(props.linkedPipelines.length);
});
});
This diff is collapsed.
This diff is collapsed.
{
"id": 37232567,
"user": {
"id": 113870,
"name": "Phil Hughes",
"username": "iamphill",
"state": "active",
"avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
"web_url": "https://gitlab.com/iamphill",
"status_tooltip_html": null,
"path": "/iamphill"
},
"active": false,
"coverage": null,
"source": "push",
"created_at": "2018-11-20T10:22:52.617Z",
"updated_at": "2018-11-20T10:24:09.511Z",
"path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
"flags": {
"latest": true,
"stuck": false,
"auto_devops": false,
"yaml_errors": false,
"retryable": false,
"cancelable": false,
"failure_reason": false
},
"details": {
"status": {
"icon": "status_success",
"text": "passed",
"label": "passed",
"group": "success",
"tooltip": "passed",
"has_details": true,
"details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
"illustration": null,
"favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
},
"duration": 65,
"finished_at": "2018-11-20T10:24:09.483Z",
"stages": [
{
"name": "test",
"title": "test: passed",
"groups": [
{
"name": "eslint",
"size": 1,
"status": {
"icon": "status_success",
"text": "passed",
"label": "passed",
"group": "success",
"tooltip": "passed",
"has_details": true,
"details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
"illustration": {
"image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
"size": "svg-430",
"title": "This job does not have a trace."
},
"favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
"action": {
"icon": "retry",
"title": "Retry",
"path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
"method": "post",
"button_title": "Retry this job"
}
},
"jobs": [
{
"id": 122845352,
"name": "eslint",
"started": "2018-11-20T10:22:53.369Z",
"archived": false,
"build_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
"retry_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
"playable": false,
"scheduled": false,
"created_at": "2018-11-20T10:22:52.630Z",
"updated_at": "2018-11-20T10:23:58.948Z",
"status": {
"icon": "status_success",
"text": "passed",
"label": "passed",
"group": "success",
"tooltip": "passed",
"has_details": true,
"details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
"illustration": {
"image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
"size": "svg-430",
"title": "This job does not have a trace."
},
"favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
"action": {
"icon": "retry",
"title": "Retry",
"path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
"method": "post",
"button_title": "Retry this job"
}
}
}
]
}
],
"status": {
"icon": "status_success",
"text": "passed",
"label": "passed",
"group": "success",
"tooltip": "passed",
"has_details": true,
"details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
"illustration": null,
"favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
},
"path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
"dropdown_path": "/gitlab-org/gl-vue-cli/pipelines/37232567/stage.json?stage=test"
}
],
"artifacts": [],
"manual_actions": [],
"scheduled_actions": []
},
"ref": {
"name": "master",
"path": "/gitlab-org/gl-vue-cli/commits/master",
"tag": false,
"branch": true
},
"commit": {
"id": "8f179601d481950bcb67032caeb33d1c24dde6bd",
"short_id": "8f179601",
"title": "Merge branch 'gl-cli-startt' into 'master'",
"created_at": "2018-11-20T10:22:51.000Z",
"parent_ids": [
"781d78fcd3d6c17ccf208f0cf0ab47c3e5397118",
"d227a0bb858c48eeee7393fcade1a33748f35183"
],
"message": "Merge branch 'gl-cli-startt' into 'master'\n\nFirst iteration of the CLI\n\nCloses gitlab-foss#53657\n\nSee merge request gitlab-org/gl-vue-cli!2",
"author_name": "Phil Hughes",
"author_email": "me@iamphill.com",
"authored_date": "2018-11-20T10:22:51.000Z",
"committer_name": "Phil Hughes",
"committer_email": "me@iamphill.com",
"committed_date": "2018-11-20T10:22:51.000Z",
"author": {
"id": 113870,
"name": "Phil Hughes",
"username": "iamphill",
"state": "active",
"avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
"web_url": "https://gitlab.com/iamphill",
"status_tooltip_html": null,
"path": "/iamphill"
},
"author_gravatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
"commit_url": "https://gitlab.com/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd",
"commit_path": "/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd"
},
"triggered_by": null,
"triggered": []
}
import PipelineStore from '~/pipelines/stores/pipeline_store';
import LinkedPipelines from '../linked_pipelines_mock.json';
describe('EE Pipeline store', () => {
let store;
let data;
beforeEach(() => {
store = new PipelineStore();
data = Object.assign({}, LinkedPipelines);
});
describe('storePipeline', () => {
beforeAll(() => {
store.storePipeline(data);
});
describe('triggered_by', () => {
it('sets triggered_by as an array', () => {
expect(store.state.pipeline.triggered_by.length).toEqual(1);
});
it('adds isExpanding & isLoading keys set to false', () => {
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].isLoading).toEqual(false);
});
it('parses nested triggered_by', () => {
expect(store.state.pipeline.triggered_by[0].triggered_by.length).toEqual(1);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isLoading).toEqual(false);
});
});
describe('triggered', () => {
it('adds isExpanding & isLoading keys set to false for each triggered pipeline', () => {
store.state.pipeline.triggered.forEach(pipeline => {
expect(pipeline.isExpanded).toEqual(false);
expect(pipeline.isLoading).toEqual(false);
});
});
it('parses nested triggered pipelines', () => {
store.state.pipeline.triggered[1].triggered.forEach(pipeline => {
expect(pipeline.isExpanded).toEqual(false);
expect(pipeline.isLoading).toEqual(false);
});
});
});
});
describe('resetTriggeredByPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('closes the pipeline & nested ones', () => {
store.state.pipeline.triggered_by[0].isExpanded = true;
store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true;
store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
});
});
describe('openTriggeredByPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('opens the given pipeline', () => {
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
});
});
describe('closeTriggeredByPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('closes the given pipeline', () => {
// open it first
store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
});
});
describe('resetTriggeredPipelines', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('closes the pipeline & nested ones', () => {
store.state.pipeline.triggered[0].isExpanded = true;
store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
store.resetTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
});
});
describe('openTriggeredPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('opens the given pipeline', () => {
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
});
});
describe('closeTriggeredPipeline', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('closes the given pipeline', () => {
// open it first
store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
});
});
describe('toggleLoading', () => {
beforeEach(() => {
store.storePipeline(data);
});
it('toggles the isLoading property for the given pipeline', () => {
store.togglePipeline(store.state.pipeline.triggered[0]);
expect(store.state.pipeline.triggered[0].isLoading).toEqual(true);
});
});
describe('addExpandedPipelineToRequestData', () => {
it('pushes the given id to expandedPipelines array', () => {
store.addExpandedPipelineToRequestData('213231');
expect(store.state.expandedPipelines).toEqual(['213231']);
});
});
describe('removeExpandedPipelineToRequestData', () => {
it('pushes the given id to expandedPipelines array', () => {
store.removeExpandedPipelineToRequestData('213231');
expect(store.state.expandedPipelines).toEqual([]);
});
});
});
This diff is collapsed.
......@@ -4,7 +4,7 @@ import createStore from '~/releases/list/store';
import api from '~/api';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers';
import { releases } from '../mock_data';
import { releases } from '../../mock_data';
describe('Releases App ', () => {
const Component = Vue.extend(app);
......
......@@ -8,7 +8,7 @@ import state from '~/releases/list/store/state';
import * as types from '~/releases/list/store/mutation_types';
import api from '~/api';
import testAction from 'spec/helpers/vuex_action_helper';
import { releases } from '../mock_data';
import { releases } from '../../mock_data';
describe('Releases State actions', () => {
let mockedState;
......
import state from '~/releases/list/store/state';
import mutations from '~/releases/list/store/mutations';
import * as types from '~/releases/list/store/mutation_types';
import { releases } from '../mock_data';
import { releases } from '../../mock_data';
describe('Releases Store Mutations', () => {
let stateCopy;
......
......@@ -8,12 +8,14 @@ describe Gitlab::SidekiqMiddleware::Metrics do
let(:worker) { double(:worker) }
let(:completion_seconds_metric) { double('completion seconds metric') }
let(:user_execution_seconds_metric) { double('user execution seconds metric') }
let(:failed_total_metric) { double('failed total metric') }
let(:retried_total_metric) { double('retried total metric') }
let(:running_jobs_metric) { double('running jobs metric') }
before do
allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric)
......@@ -23,13 +25,16 @@ describe Gitlab::SidekiqMiddleware::Metrics do
it 'yields block' do
allow(completion_seconds_metric).to receive(:observe)
allow(user_execution_seconds_metric).to receive(:observe)
expect { |b| middleware.call(worker, {}, :test, &b) }.to yield_control.once
end
it 'sets metrics' do
labels = { queue: :test }
allow(middleware).to receive(:get_thread_cputime).and_return(1, 3)
expect(user_execution_seconds_metric).to receive(:observe).with(labels, 2)
expect(running_jobs_metric).to receive(:increment).with(labels, 1)
expect(running_jobs_metric).to receive(:increment).with(labels, -1)
expect(completion_seconds_metric).to receive(:observe).with(labels, kind_of(Numeric))
......@@ -37,9 +42,17 @@ describe Gitlab::SidekiqMiddleware::Metrics do
middleware.call(worker, {}, :test) { nil }
end
it 'ignore user execution when measured 0' do
allow(completion_seconds_metric).to receive(:observe)
allow(middleware).to receive(:get_thread_cputime).and_return(0, 0)
expect(user_execution_seconds_metric).not_to receive(:observe)
end
context 'when job is retried' do
it 'sets sidekiq_jobs_retried_total metric' do
allow(completion_seconds_metric).to receive(:observe)
expect(user_execution_seconds_metric).to receive(:observe)
expect(retried_total_metric).to receive(:increment)
......
......@@ -2210,6 +2210,7 @@ describe Ci::Build do
{ key: 'CI_BUILD_STAGE', value: 'test', public: true, masked: false },
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true, masked: false },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true, masked: false },
{ key: 'CI_PROJECT_TITLE', value: project.title, public: true, masked: false },
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
{ key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
......
......@@ -119,7 +119,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command }
it 'is initialized with latest version' do
expect(subject.version).to eq('0.6.0')
expect(subject.version).to eq('0.7.0')
end
it_behaves_like 'a command'
......
......@@ -410,8 +410,10 @@ module KubernetesHelpers
"generation" => 2
},
"status" => {
"domain" => "#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
"url" => "http://#{name}.#{namespace}.#{domain}",
"address" => {
"url" => "#{name}.#{namespace}.svc.cluster.local"
},
"latestCreatedRevisionName" => "#{name}-00002",
"latestReadyRevisionName" => "#{name}-00002",
"observedGeneration" => 2
......@@ -437,8 +439,10 @@ module KubernetesHelpers
}
},
"status" => {
"domain" => "#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
"url" => "http://#{name}.#{namespace}.#{domain}",
"address" => {
"url" => "#{name}.#{namespace}.svc.cluster.local"
},
"latestCreatedRevisionName" => "#{name}-00002",
"latestReadyRevisionName" => "#{name}-00002",
"observedGeneration" => 2
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment