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'; ...@@ -7,6 +7,8 @@ import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector'; import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector'; import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector'; import LicenseSelector from './template_selectors/license_selector';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
export default class FileTemplateMediator { export default class FileTemplateMediator {
constructor({ editor, currentAction, projectId }) { constructor({ editor, currentAction, projectId }) {
...@@ -19,6 +21,7 @@ export default class FileTemplateMediator { ...@@ -19,6 +21,7 @@ export default class FileTemplateMediator {
this.initDomElements(); this.initDomElements();
this.initDropdowns(); this.initDropdowns();
this.initPageEvents(); this.initPageEvents();
this.cacheFileContents();
} }
initTemplateSelectors() { initTemplateSelectors() {
...@@ -40,6 +43,7 @@ export default class FileTemplateMediator { ...@@ -40,6 +43,7 @@ export default class FileTemplateMediator {
return { return {
name: cfg.name, name: cfg.name,
key: cfg.key, key: cfg.key,
id: cfg.key,
}; };
}), }),
}); });
...@@ -58,6 +62,7 @@ export default class FileTemplateMediator { ...@@ -58,6 +62,7 @@ export default class FileTemplateMediator {
this.$fileContent = $fileEditor.find('#file-content'); this.$fileContent = $fileEditor.find('#file-content');
this.$commitForm = $fileEditor.find('form'); this.$commitForm = $fileEditor.find('form');
this.$navLinks = $fileEditor.find('.nav-links'); this.$navLinks = $fileEditor.find('.nav-links');
this.$templateTypes = this.$templateSelectors.find('.template-type-selector');
} }
initDropdowns() { initDropdowns() {
...@@ -113,7 +118,11 @@ export default class FileTemplateMediator { ...@@ -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(); this.cacheToggleText();
} }
...@@ -123,15 +132,24 @@ export default class FileTemplateMediator { ...@@ -123,15 +132,24 @@ export default class FileTemplateMediator {
} }
selectTemplateFile(selector, query, data) { selectTemplateFile(selector, query, data) {
const self = this;
selector.renderLoading(); selector.renderLoading();
// in case undo menu is already there
this.destroyUndoMenu();
this.fetchFileTemplate(selector.config.type, query, data) this.fetchFileTemplate(selector.config.type, query, data)
.then(file => { .then(file => {
this.showUndoMenu();
this.setEditorContent(file); this.setEditorContent(file);
this.setFilename(selector.config.name);
selector.renderLoaded(); 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}`)); .catch(err => new Flash(`An error occurred while fetching the template: ${err}`));
} }
...@@ -173,22 +191,6 @@ export default class FileTemplateMediator { ...@@ -173,22 +191,6 @@ export default class FileTemplateMediator {
return this.templateSelectors.find(selector => selector.config.key === key); 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() { hideTemplateSelectorMenu() {
this.$templatesMenu.hide(); this.$templatesMenu.hide();
} }
...@@ -210,6 +212,7 @@ export default class FileTemplateMediator { ...@@ -210,6 +212,7 @@ export default class FileTemplateMediator {
this.setEditorContent(this.cachedContent); this.setEditorContent(this.cachedContent);
this.setFilename(this.cachedFilename); this.setFilename(this.cachedFilename);
this.setTemplateSelectorToggleText(); this.setTemplateSelectorToggleText();
this.setTypeSelectorToggleText(__('Select a template type'));
} }
getTemplateSelectorToggleText() { getTemplateSelectorToggleText() {
...@@ -228,6 +231,10 @@ export default class FileTemplateMediator { ...@@ -228,6 +231,10 @@ export default class FileTemplateMediator {
return this.typeSelector.getToggleText(); return this.typeSelector.getToggleText();
} }
setTypeSelectorToggleText(text) {
this.typeSelector.setToggleText(text);
}
getFilename() { getFilename() {
return this.$filenameInput.val(); return this.$filenameInput.val();
} }
......
...@@ -19,7 +19,6 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { ...@@ -19,7 +19,6 @@ export default class BlobCiYamlSelector extends FileTemplateSelector {
data: this.$dropdown.data('data'), data: this.$dropdown.data('data'),
filterable: true, filterable: true,
selectable: true, selectable: true,
toggleLabel: item => item.name,
search: { search: {
fields: ['name'], fields: ['name'],
}, },
......
...@@ -20,7 +20,6 @@ export default class DockerfileSelector extends FileTemplateSelector { ...@@ -20,7 +20,6 @@ export default class DockerfileSelector extends FileTemplateSelector {
data: this.$dropdown.data('data'), data: this.$dropdown.data('data'),
filterable: true, filterable: true,
selectable: true, selectable: true,
toggleLabel: item => item.name,
search: { search: {
fields: ['name'], fields: ['name'],
}, },
......
...@@ -18,7 +18,6 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { ...@@ -18,7 +18,6 @@ export default class BlobGitignoreSelector extends FileTemplateSelector {
data: this.$dropdown.data('data'), data: this.$dropdown.data('data'),
filterable: true, filterable: true,
selectable: true, selectable: true,
toggleLabel: item => item.name,
search: { search: {
fields: ['name'], fields: ['name'],
}, },
......
...@@ -18,7 +18,6 @@ export default class BlobLicenseSelector extends FileTemplateSelector { ...@@ -18,7 +18,6 @@ export default class BlobLicenseSelector extends FileTemplateSelector {
data: this.$dropdown.data('data'), data: this.$dropdown.data('data'),
filterable: true, filterable: true,
selectable: true, selectable: true,
toggleLabel: item => item.name,
search: { search: {
fields: ['name'], fields: ['name'],
}, },
......
...@@ -16,7 +16,6 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector { ...@@ -16,7 +16,6 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector {
data: this.config.dropdownData, data: this.config.dropdownData,
filterable: false, filterable: false,
selectable: true, selectable: true,
toggleLabel: item => item.name,
clicked: options => this.mediator.selectTemplateTypeOptions(options), clicked: options => this.mediator.selectTemplateTypeOptions(options),
text: item => item.name, text: item => item.name,
}); });
......
<script> <script>
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import GraphMixin from '../../mixins/graph_component_mixin'; 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 { export default {
name: 'PipelineGraph',
components: { components: {
StageColumnComponent, StageColumnComponent,
GlLoadingIcon, 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> </script>
<template> <template>
<div class="build-content middle-block js-pipeline-graph"> <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 <div
:style="{ :style="{
paddingLeft: `${graphLeftPadding}px`, paddingLeft: `${graphLeftPadding}px`,
...@@ -23,21 +123,80 @@ export default { ...@@ -23,21 +123,80 @@ export default {
> >
<gl-loading-icon v-if="isLoading" class="m-auto" :size="3" /> <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 <stage-column-component
v-for="(stage, index) in graph" v-for="(stage, index) in graph"
:key="stage.name" :key="stage.name"
:class="{ :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)" :title="capitalizeStageName(stage.name)"
:groups="stage.groups" :groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
:action="stage.status.action" :action="stage.status.action"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
/> />
</ul> </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> </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> <script>
import _ from 'underscore'; 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 JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue'; import JobGroupDropdown from './job_group_dropdown.vue';
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
......
import Flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
methods: { methods: {
clickTriggeredByPipeline() {}, getExpandedPipelines(pipeline) {
clickTriggeredPipeline() {}, 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() { requestRefreshPipelineGraph() {
// When an action is clicked // When an action is clicked
// (wether in the dropdown or in the main nodes, we refresh the big graph) // (wether in the dropdown or in the main nodes, we refresh the big graph)
this.mediator this.mediator
.refreshPipeline() .refreshPipeline()
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => flash(__('An error occurred while making the request.')));
}, },
}, },
}; };
export default { export default {
props: {
hasTriggeredBy: {
type: Boolean,
required: false,
default: false,
},
},
methods: { methods: {
buildConnnectorClass(index) { 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'; ...@@ -2,8 +2,8 @@ import Vue from 'vue';
import Flash from '~/flash'; import Flash from '~/flash';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { __ } from '~/locale'; import { __ } from '~/locale';
import pipelineGraph from 'ee_else_ce/pipelines/components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import GraphEEMixin from 'ee_else_ce/pipelines/mixins/graph_pipeline_bundle_mixin'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator'; import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue'; import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
...@@ -23,7 +23,7 @@ export default () => { ...@@ -23,7 +23,7 @@ export default () => {
components: { components: {
pipelineGraph, pipelineGraph,
}, },
mixins: [GraphEEMixin], mixins: [GraphBundleMixin],
data() { data() {
return { return {
mediator, mediator,
......
import Visibility from 'visibilityjs'; 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 Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
import { __ } from '../locale'; import { __ } from '../locale';
......
import Vue from 'vue';
import _ from 'underscore';
export default class PipelineStore { export default class PipelineStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.pipeline = {}; 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 = {}) { 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 @@ ...@@ -326,8 +326,9 @@
} }
.dropdown-header { .dropdown-header {
color: $gl-text-color-secondary; color: $black;
font-size: 13px; font-size: 13px;
font-weight: $gl-font-weight-bold;
line-height: $gl-line-height; line-height: $gl-line-height;
padding: $dropdown-item-padding-y $dropdown-item-padding-x; padding: $dropdown-item-padding-y $dropdown-item-padding-x;
} }
......
...@@ -47,14 +47,19 @@ ...@@ -47,14 +47,19 @@
margin-right: 10px; margin-right: 10px;
} }
.new-file-name { .new-file-name,
.new-file-path {
display: inline-block; display: inline-block;
max-width: 420px; max-width: 250px;
float: left; float: left;
@media(max-width: map-get($grid-breakpoints, lg)-1) { @media(max-width: map-get($grid-breakpoints, lg)-1) {
width: 180px; width: 180px;
} }
@media (max-width: 1360px) {
width: auto;
}
} }
.file-buttons { .file-buttons {
...@@ -98,13 +103,14 @@ ...@@ -98,13 +103,14 @@
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(md) {
.file-editor { .file-editor {
.file-title { .file-title {
display: block; display: block;
} }
.new-file-name { .new-file-name,
.new-file-path {
max-width: none; max-width: none;
width: 100%; width: 100%;
margin-bottom: 3px; margin-bottom: 3px;
...@@ -146,20 +152,17 @@ ...@@ -146,20 +152,17 @@
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
@media(max-width: map-get($grid-breakpoints, md)-1) { @media(max-width: map-get($grid-breakpoints, lg)-1) {
display: block; display: block;
margin: 19px 0 12px; margin: 19px 0 12px;
} }
} }
.template-selectors-menu { .template-selectors-menu {
display: inline-block; display: flex;
vertical-align: top; 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; display: block;
width: 100%; width: 100%;
margin: 5px 0; margin: 5px 0;
...@@ -168,24 +171,11 @@ ...@@ -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 { .template-selector-dropdowns-wrap {
display: inline-block; display: inline-block;
margin: 5px 0 0 8px;
vertical-align: top; vertical-align: top;
@media(max-width: map-get($grid-breakpoints, md)-1) { @media(max-width: map-get($grid-breakpoints, lg)-1) {
display: block; display: block;
width: 100%; width: 100%;
margin: 0 0 16px; margin: 0 0 16px;
...@@ -199,9 +189,8 @@ ...@@ -199,9 +189,8 @@
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
font-family: $regular_font; 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; display: block;
width: 100%; width: 100%;
margin: 5px 0; margin: 5px 0;
...@@ -212,30 +201,22 @@ ...@@ -212,30 +201,22 @@
} }
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 250px; width: 200px;
vertical-align: top; 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; display: block;
width: 100%; width: 100%;
margin: 5px 0; margin: 5px 0;
} }
} }
} }
} }
.template-selectors-undo-menu { .editor-title-row {
display: inline-block; margin-bottom: 20px;
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;
}
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Clusters module Clusters
module Applications module Applications
class Knative < ApplicationRecord class Knative < ApplicationRecord
VERSION = '0.6.0' VERSION = '0.7.0'
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts' REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'
METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml' METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'
FETCH_IP_ADDRESS_DELAY = 30.seconds FETCH_IP_ADDRESS_DELAY = 30.seconds
......
...@@ -1850,6 +1850,7 @@ class Project < ApplicationRecord ...@@ -1850,6 +1850,7 @@ class Project < ApplicationRecord
Gitlab::Ci::Variables::Collection.new Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PROJECT_ID', value: id.to_s) .append(key: 'CI_PROJECT_ID', value: id.to_s)
.append(key: 'CI_PROJECT_NAME', value: path) .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', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug) .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
......
...@@ -44,7 +44,7 @@ module Projects ...@@ -44,7 +44,7 @@ module Projects
end end
expose :url do |service| expose :url do |service|
"http://#{service.dig('status', 'domain')}" service.dig('status', 'url')
end end
expose :description do |service| expose :description do |service|
......
...@@ -3,20 +3,22 @@ ...@@ -3,20 +3,22 @@
- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) - is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.append-bottom-default .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 .editor-ref.block-truncated
= sprite_icon('fork', size: 12) = sprite_icon('fork', size: 12)
= ref = ref
- if current_action?(:edit) || current_action?(:update) - if current_action?(:edit) || current_action?(:update)
%span.pull-left.append-right-10 %span.pull-left.append-right-10
= text_field_tag 'file_path', (params[:file_path] || @path), = text_field_tag 'file_path', (params[:file_path] || @path),
class: 'form-control new-file-path js-file-path-name-input' class: 'form-control new-file-path js-file-path-name-input'
= render 'template_selectors'
- if current_action?(:new) || current_action?(:create) - if current_action?(:new) || current_action?(:create)
%span.pull-left.append-right-10 %span.pull-left.append-right-10
\/ \/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name", = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-file-path-name-input' required: true, class: 'form-control new-file-name js-file-path-name-input'
= render 'template_selectors'
.file-buttons .file-buttons
- if is_markdown - if is_markdown
......
.template-selectors-menu .template-selectors-menu.gl-pl-2
.templates-selectors-label
Template
.template-selector-dropdowns-wrap .template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden .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 .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 .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 .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 .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) } } ) = 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) } } )
.template-selectors-undo-menu.hidden
%span.text-info Template applied
%button.btn.btn-sm.btn-info Undo
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
.editor-title-row .editor-title-row
%h3.page-title.blob-edit-page-title %h3.page-title.blob-edit-page-title
Edit file Edit file
= render 'template_selectors'
.file-editor .file-editor
%ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs %ul.nav-links.no-bottom.js-edit-mode.nav.nav-tabs
%li.active %li.active
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
.editor-title-row .editor-title-row
%h3.page-title.blob-new-page-title %h3.page-title.blob-new-page-title
New file New file
= render 'template_selectors'
.file-editor .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 = 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 = 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 @@ ...@@ -61,4 +61,10 @@
- virtualservices.networking.istio.io - virtualservices.networking.istio.io
- rbacconfigs.rbac.istio.io - rbacconfigs.rbac.istio.io
- servicerolebindings.rbac.istio.io - servicerolebindings.rbac.istio.io
- serviceroles.rbac.istio.io - serviceroles.rbac.istio.io
\ No newline at end of file - 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: ...@@ -209,6 +209,6 @@ panel_groups:
weight: 1 weight: 1
metrics: metrics:
- id: system_metrics_knative_function_invocation_count - 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 label: invocations / minute
unit: requests 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" ...@@ -266,6 +266,7 @@ export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-foss"
export CI_PROJECT_ID="34" export CI_PROJECT_ID="34"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-foss" export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-foss"
export CI_PROJECT_NAME="gitlab-foss" export CI_PROJECT_NAME="gitlab-foss"
export CI_PROJECT_TITLE="GitLab FOSS"
export CI_PROJECT_NAMESPACE="gitlab-org" export CI_PROJECT_NAMESPACE="gitlab-org"
export CI_PROJECT_PATH="gitlab-org/gitlab-foss" export CI_PROJECT_PATH="gitlab-org/gitlab-foss"
export CI_PROJECT_URL="https://example.com/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 ...@@ -707,6 +708,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI_PROJECT_ID=17893 ++ CI_PROJECT_ID=17893
++ export CI_PROJECT_NAME=ci-debug-trace ++ export CI_PROJECT_NAME=ci-debug-trace
++ 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 ++ export CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
++ CI_PROJECT_PATH=gitlab-examples/ci-debug-trace ++ CI_PROJECT_PATH=gitlab-examples/ci-debug-trace
++ export CI_PROJECT_NAMESPACE=gitlab-examples ++ export CI_PROJECT_NAMESPACE=gitlab-examples
......
...@@ -87,7 +87,8 @@ future GitLab releases.** ...@@ -87,7 +87,8 @@ future GitLab releases.**
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL | | `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_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_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_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` | 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. | | `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 ...@@ -19,10 +19,16 @@ module Gitlab
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1) @metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
end end
job_thread_cputime_start = get_thread_cputime
realtime = Benchmark.realtime do realtime = Benchmark.realtime do
yield yield
end 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) @metrics[:sidekiq_jobs_completion_seconds].observe(labels, realtime)
rescue Exception # rubocop: disable Lint/RescueException rescue Exception # rubocop: disable Lint/RescueException
@metrics[:sidekiq_jobs_failed_total].increment(labels, 1) @metrics[:sidekiq_jobs_failed_total].increment(labels, 1)
...@@ -35,6 +41,7 @@ module Gitlab ...@@ -35,6 +41,7 @@ module Gitlab
def init_metrics 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_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_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'), sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
...@@ -47,6 +54,10 @@ module Gitlab ...@@ -47,6 +54,10 @@ module Gitlab
queue: queue queue: queue
} }
end end
def get_thread_cputime
defined?(Process::CLOCK_THREAD_CPUTIME_ID) ? Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID) : 0
end
end end
end end
end end
...@@ -13,14 +13,6 @@ module Gitlab ...@@ -13,14 +13,6 @@ module Gitlab
path path
end 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) def force_utf8(str)
str.dup.force_encoding(Encoding::UTF_8) str.dup.force_encoding(Encoding::UTF_8)
end end
......
...@@ -1758,6 +1758,9 @@ msgstr "" ...@@ -1758,6 +1758,9 @@ msgstr ""
msgid "Apply a label" msgid "Apply a label"
msgstr "" msgstr ""
msgid "Apply a template"
msgstr ""
msgid "Apply suggestion" msgid "Apply suggestion"
msgstr "" msgstr ""
...@@ -14372,6 +14375,9 @@ msgstr "" ...@@ -14372,6 +14375,9 @@ msgstr ""
msgid "Select a template repository" msgid "Select a template repository"
msgstr "" msgstr ""
msgid "Select a template type"
msgstr ""
msgid "Select a timezone" msgid "Select a timezone"
msgstr "" msgstr ""
......
...@@ -74,5 +74,3 @@ module QA::Page ...@@ -74,5 +74,3 @@ module QA::Page
end end
end 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 ...@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a Dockerfile file' do
wait_for_requests 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/') expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end end
end end
...@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitignore file' do ...@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitignore file' do
wait_for_requests 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('/.bundle')
expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset') expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
end end
......
...@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do ...@@ -23,7 +23,7 @@ describe 'Projects > Files > User wants to add a .gitlab-ci.yml file' do
wait_for_requests 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('This file is a template, and might need editing before it works on your project')
expect(page).to have_content('jekyll build -d test') expect(page).to have_content('jekyll build -d test')
end end
......
...@@ -64,7 +64,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do ...@@ -64,7 +64,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
def select_template(template) def select_template(template)
page.within('.js-license-selector-wrap') do page.within('.js-license-selector-wrap') do
click_button 'Apply a license template' click_button 'Apply a template'
click_link template click_link template
wait_for_requests wait_for_requests
end end
......
...@@ -37,7 +37,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file ...@@ -37,7 +37,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file
def select_template(template) def select_template(template)
page.within('.js-license-selector-wrap') do page.within('.js-license-selector-wrap') do
click_button 'Apply a license template' click_button 'Apply a template'
click_link template click_link template
wait_for_requests wait_for_requests
end end
......
...@@ -24,8 +24,9 @@ describe 'Projects > Files > Template type dropdown selector', :js do ...@@ -24,8 +24,9 @@ describe 'Projects > Files > Template type dropdown selector', :js do
try_selecting_all_types try_selecting_all_types
end 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' fill_in 'file_path', with: '.gitignore'
select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore') check_type_selector_toggle_text('.gitignore')
end end
end end
...@@ -70,6 +71,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do ...@@ -70,6 +71,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end end
it 'toggle is set to the correct value' do it 'toggle is set to the correct value' do
select_template('gitignore', 'Actionscript')
check_type_selector_toggle_text('.gitignore') check_type_selector_toggle_text('.gitignore')
end end
...@@ -88,7 +90,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do ...@@ -88,7 +90,7 @@ describe 'Projects > Files > Template type dropdown selector', :js do
end end
it 'toggle is set to the proper value' do 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 end
it 'selects every template type correctly' do it 'selects every template type correctly' do
...@@ -103,16 +105,15 @@ def check_type_selector_display(is_visible) ...@@ -103,16 +105,15 @@ def check_type_selector_display(is_visible)
end end
def try_selecting_all_types def try_selecting_all_types
try_selecting_template_type('LICENSE', 'Apply a license template') try_selecting_template_type('LICENSE', 'Apply a template')
try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template') try_selecting_template_type('Dockerfile', 'Apply a template')
try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template') try_selecting_template_type('.gitlab-ci.yml', 'Apply a template')
try_selecting_template_type('.gitignore', 'Apply a .gitignore template') try_selecting_template_type('.gitignore', 'Apply a template')
end end
def try_selecting_template_type(template_type, selector_label) def try_selecting_template_type(template_type, selector_label)
select_template_type(template_type) select_template_type(template_type)
check_template_selector_display(selector_label) check_template_selector_display(selector_label)
check_type_selector_toggle_text(template_type)
end end
def select_template_type(template_type) def select_template_type(template_type)
...@@ -120,6 +121,11 @@ def select_template_type(template_type) ...@@ -120,6 +121,11 @@ def select_template_type(template_type)
find('.dropdown-content li', text: template_type).click find('.dropdown-content li', text: template_type).click
end 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) def check_template_selector_display(content)
expect(page).to have_content(content) expect(page).to have_content(content)
end end
......
...@@ -13,11 +13,12 @@ describe 'Projects > Files > Template Undo Button', :js do ...@@ -13,11 +13,12 @@ describe 'Projects > Files > Template Undo Button', :js do
context 'editing a matching file and applying a template' do context 'editing a matching file and applying a template' do
before do before do
visit project_edit_blob_path(project, File.join(project.default_branch, "LICENSE")) 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') select_file_template('.js-license-selector', 'Apache License 2.0')
end end
it 'reverts template application' do 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 end
...@@ -29,7 +30,7 @@ describe 'Projects > Files > Template Undo Button', :js do ...@@ -29,7 +30,7 @@ describe 'Projects > Files > Template Undo Button', :js do
end end
it 'reverts template application' do 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 end
end end
...@@ -45,12 +46,12 @@ def check_toggle_text_set(neutral_toggle_text) ...@@ -45,12 +46,12 @@ def check_toggle_text_set(neutral_toggle_text)
end end
def check_undo_button_display def check_undo_button_display
expect(page).to have_content('Template applied') expect(page).to have_content('template applied')
expect(page).to have_css('.template-selectors-undo-menu .btn-info') expect(page).to have_css('.toasted-container')
end end
def check_content_reverted(template_content) 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).not_to have_content(template_content)
expect(page).to have_css('.template-type-selector .dropdown-toggle-text') expect(page).to have_css('.template-type-selector .dropdown-toggle-text')
end end
......
...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; ...@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/list/components/release_block.vue'; import ReleaseBlock from '~/releases/list/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore'; import { first } from 'underscore';
import { release } from '../mock_data'; import { release } from '../../mock_data';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
......
import Vue from 'vue'; import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; 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 graphComponent from '~/pipelines/components/graph/graph_component.vue';
import graphJSON from './mock_data'; import graphJSON from './mock_data';
import linkedPipelineJSON from '../linked_pipelines_mock.json';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
describe('graph component', () => { describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent); const GraphComponent = Vue.extend(graphComponent);
const store = new PipelineStore();
store.storePipeline(linkedPipelineJSON);
const mediator = new PipelinesMediator({ endpoint: '' });
let component; let component;
beforeEach(() => { beforeEach(() => {
...@@ -22,6 +29,7 @@ describe('graph component', () => { ...@@ -22,6 +29,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, { component = mountComponent(GraphComponent, {
isLoading: true, isLoading: true,
pipeline: {}, pipeline: {},
mediator,
}); });
expect(component.$el.querySelector('.loading-icon')).toBeDefined(); expect(component.$el.querySelector('.loading-icon')).toBeDefined();
...@@ -33,6 +41,7 @@ describe('graph component', () => { ...@@ -33,6 +41,7 @@ describe('graph component', () => {
component = mountComponent(GraphComponent, { component = mountComponent(GraphComponent, {
isLoading: false, isLoading: false,
pipeline: graphJSON, pipeline: graphJSON,
mediator,
}); });
expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true); expect(component.$el.classList.contains('js-pipeline-graph')).toEqual(true);
...@@ -57,11 +66,205 @@ describe('graph component', () => { ...@@ -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', () => { describe('capitalizeStageName', () => {
it('capitalizes and escapes stage name', () => { it('capitalizes and escapes stage name', () => {
component = mountComponent(GraphComponent, { component = mountComponent(GraphComponent, {
isLoading: false, isLoading: false,
pipeline: graphJSON, pipeline: graphJSON,
mediator,
}); });
expect( 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'; ...@@ -4,7 +4,7 @@ import createStore from '~/releases/list/store';
import api from '~/api'; import api from '~/api';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../store/helpers'; import { resetStore } from '../store/helpers';
import { releases } from '../mock_data'; import { releases } from '../../mock_data';
describe('Releases App ', () => { describe('Releases App ', () => {
const Component = Vue.extend(app); const Component = Vue.extend(app);
......
...@@ -8,7 +8,7 @@ import state from '~/releases/list/store/state'; ...@@ -8,7 +8,7 @@ import state from '~/releases/list/store/state';
import * as types from '~/releases/list/store/mutation_types'; import * as types from '~/releases/list/store/mutation_types';
import api from '~/api'; import api from '~/api';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import { releases } from '../mock_data'; import { releases } from '../../mock_data';
describe('Releases State actions', () => { describe('Releases State actions', () => {
let mockedState; let mockedState;
......
import state from '~/releases/list/store/state'; import state from '~/releases/list/store/state';
import mutations from '~/releases/list/store/mutations'; import mutations from '~/releases/list/store/mutations';
import * as types from '~/releases/list/store/mutation_types'; import * as types from '~/releases/list/store/mutation_types';
import { releases } from '../mock_data'; import { releases } from '../../mock_data';
describe('Releases Store Mutations', () => { describe('Releases Store Mutations', () => {
let stateCopy; let stateCopy;
......
...@@ -8,12 +8,14 @@ describe Gitlab::SidekiqMiddleware::Metrics do ...@@ -8,12 +8,14 @@ describe Gitlab::SidekiqMiddleware::Metrics do
let(:worker) { double(:worker) } let(:worker) { double(:worker) }
let(:completion_seconds_metric) { double('completion seconds metric') } 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(:failed_total_metric) { double('failed total metric') }
let(:retried_total_metric) { double('retried total metric') } let(:retried_total_metric) { double('retried total metric') }
let(:running_jobs_metric) { double('running jobs metric') } let(:running_jobs_metric) { double('running jobs metric') }
before do 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_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_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(: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) 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 ...@@ -23,13 +25,16 @@ describe Gitlab::SidekiqMiddleware::Metrics do
it 'yields block' do it 'yields block' do
allow(completion_seconds_metric).to receive(:observe) 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 expect { |b| middleware.call(worker, {}, :test, &b) }.to yield_control.once
end end
it 'sets metrics' do it 'sets metrics' do
labels = { queue: :test } 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(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)) expect(completion_seconds_metric).to receive(:observe).with(labels, kind_of(Numeric))
...@@ -37,9 +42,17 @@ describe Gitlab::SidekiqMiddleware::Metrics do ...@@ -37,9 +42,17 @@ describe Gitlab::SidekiqMiddleware::Metrics do
middleware.call(worker, {}, :test) { nil } middleware.call(worker, {}, :test) { nil }
end 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 context 'when job is retried' do
it 'sets sidekiq_jobs_retried_total metric' do it 'sets sidekiq_jobs_retried_total metric' do
allow(completion_seconds_metric).to receive(:observe) allow(completion_seconds_metric).to receive(:observe)
expect(user_execution_seconds_metric).to receive(:observe)
expect(retried_total_metric).to receive(:increment) expect(retried_total_metric).to receive(:increment)
......
...@@ -2210,6 +2210,7 @@ describe Ci::Build do ...@@ -2210,6 +2210,7 @@ describe Ci::Build do
{ key: 'CI_BUILD_STAGE', value: 'test', public: true, masked: false }, { 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_ID', value: project.id.to_s, public: true, masked: false },
{ key: 'CI_PROJECT_NAME', value: project.path, 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', 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_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, 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 ...@@ -119,7 +119,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command } subject { knative.install_command }
it 'is initialized with latest version' do it 'is initialized with latest version' do
expect(subject.version).to eq('0.6.0') expect(subject.version).to eq('0.7.0')
end end
it_behaves_like 'a command' it_behaves_like 'a command'
......
...@@ -410,8 +410,10 @@ module KubernetesHelpers ...@@ -410,8 +410,10 @@ module KubernetesHelpers
"generation" => 2 "generation" => 2
}, },
"status" => { "status" => {
"domain" => "#{name}.#{namespace}.#{domain}", "url" => "http://#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local", "address" => {
"url" => "#{name}.#{namespace}.svc.cluster.local"
},
"latestCreatedRevisionName" => "#{name}-00002", "latestCreatedRevisionName" => "#{name}-00002",
"latestReadyRevisionName" => "#{name}-00002", "latestReadyRevisionName" => "#{name}-00002",
"observedGeneration" => 2 "observedGeneration" => 2
...@@ -437,8 +439,10 @@ module KubernetesHelpers ...@@ -437,8 +439,10 @@ module KubernetesHelpers
} }
}, },
"status" => { "status" => {
"domain" => "#{name}.#{namespace}.#{domain}", "url" => "http://#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local", "address" => {
"url" => "#{name}.#{namespace}.svc.cluster.local"
},
"latestCreatedRevisionName" => "#{name}-00002", "latestCreatedRevisionName" => "#{name}-00002",
"latestReadyRevisionName" => "#{name}-00002", "latestReadyRevisionName" => "#{name}-00002",
"observedGeneration" => 2 "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