Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
16515bdfcb
commit
b4b6bff01d
2
Gemfile
2
Gemfile
|
@ -290,7 +290,7 @@ gem 'gitlab_chronic_duration', '~> 0.10.6.2'
|
|||
gem 'rack-proxy', '~> 0.6.0'
|
||||
|
||||
gem 'sassc-rails', '~> 2.1.0'
|
||||
gem 'gitlab-terser', '1.0.1.1'
|
||||
gem 'terser', '1.0.2'
|
||||
|
||||
gem 'addressable', '~> 2.7'
|
||||
gem 'font-awesome-rails', '~> 4.7'
|
||||
|
|
|
@ -452,8 +452,6 @@ GEM
|
|||
rubocop-performance (~> 1.5.2)
|
||||
rubocop-rails (~> 2.5)
|
||||
rubocop-rspec (~> 1.36)
|
||||
gitlab-terser (1.0.1.1)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
gitlab_chronic_duration (0.10.6.2)
|
||||
numerizer (~> 0.2)
|
||||
gitlab_omniauth-ldap (2.1.1)
|
||||
|
@ -1132,6 +1130,8 @@ GEM
|
|||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
terser (1.0.2)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
test-prof (0.12.0)
|
||||
text (1.3.1)
|
||||
thin (1.7.2)
|
||||
|
@ -1333,7 +1333,6 @@ DEPENDENCIES
|
|||
gitlab-puma_worker_killer (~> 0.1.1.gitlab.1)
|
||||
gitlab-sidekiq-fetcher (= 0.5.2)
|
||||
gitlab-styles (~> 4.3.0)
|
||||
gitlab-terser (= 1.0.1.1)
|
||||
gitlab_chronic_duration (~> 0.10.6.2)
|
||||
gitlab_omniauth-ldap (~> 2.1.1)
|
||||
gon (~> 6.2)
|
||||
|
@ -1483,6 +1482,7 @@ DEPENDENCIES
|
|||
stackprof (~> 0.2.15)
|
||||
state_machines-activerecord (~> 0.6.0)
|
||||
sys-filesystem (~> 1.1.6)
|
||||
terser (= 1.0.2)
|
||||
test-prof (~> 0.12.0)
|
||||
thin (~> 1.7.0)
|
||||
timecop (~> 0.9.1)
|
||||
|
|
|
@ -3,9 +3,7 @@ import isEmojiUnicodeSupported from '../emoji/support';
|
|||
import { initEmojiMap, getEmojiInfo, emojiFallbackImageSrc, emojiImageTag } from '../emoji';
|
||||
|
||||
class GlEmoji extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
connectedCallback() {
|
||||
this.initialize();
|
||||
}
|
||||
initialize() {
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
} from '@gitlab/ui';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
|
@ -19,6 +18,7 @@ import { __, s__, sprintf } from '~/locale';
|
|||
import { diffViewerModes } from '~/ide/constants';
|
||||
import DiffStats from './diff_stats.vue';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
import { DIFF_FILE_HEADER } from '../i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -30,13 +30,15 @@ export default {
|
|||
GlButtonGroup,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
GlDropdownSectionHeader,
|
||||
GlDropdownDivider,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
i18n: {
|
||||
...DIFF_FILE_HEADER,
|
||||
},
|
||||
props: {
|
||||
discussionPath: {
|
||||
type: String,
|
||||
|
@ -290,7 +292,7 @@ export default {
|
|||
icon="external-link"
|
||||
/>
|
||||
<gl-dropdown
|
||||
v-gl-tooltip.hover.focus="__('More actions')"
|
||||
v-gl-tooltip.hover.focus="$options.i18n.optionsDropdownTitle"
|
||||
right
|
||||
toggle-class="btn-icon js-diff-more-actions"
|
||||
class="gl-pt-0!"
|
||||
|
@ -299,11 +301,8 @@ export default {
|
|||
>
|
||||
<template #button-content>
|
||||
<gl-icon name="ellipsis_v" class="mr-0" />
|
||||
<span class="sr-only">{{ __('More actions') }}</span>
|
||||
<span class="sr-only">{{ $options.i18n.optionsDropdownTitle }}</span>
|
||||
</template>
|
||||
<gl-dropdown-section-header>
|
||||
{{ __('More actions') }}
|
||||
</gl-dropdown-section-header>
|
||||
<gl-dropdown-item
|
||||
v-if="diffFile.replaced_view_path"
|
||||
ref="replacedFileButton"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const DIFF_FILE_HEADER = {
|
||||
optionsDropdownTitle: __('Options'),
|
||||
};
|
|
@ -14,14 +14,11 @@ import { createUniqueJobId } from '../../utils';
|
|||
|
||||
export const generateLinksData = ({ links }, jobs, containerID) => {
|
||||
const containerEl = document.getElementById(containerID);
|
||||
|
||||
return links.map(link => {
|
||||
const path = d3.path();
|
||||
|
||||
// We can only have one unique job name per stage, so our selector
|
||||
// is: ${stageName}-${jobName}
|
||||
const sourceId = createUniqueJobId(jobs[link.source].stage, link.source);
|
||||
const targetId = createUniqueJobId(jobs[link.target].stage, link.target);
|
||||
const sourceId = jobs[link.source].id;
|
||||
const targetId = jobs[link.target].id;
|
||||
|
||||
const sourceNodeEl = document.getElementById(sourceId);
|
||||
const targetNodeEl = document.getElementById(targetId);
|
||||
|
@ -80,6 +77,12 @@ export const generateLinksData = ({ links }, jobs, containerID) => {
|
|||
targetNodeY,
|
||||
);
|
||||
|
||||
return { ...link, path: path.toString() };
|
||||
return {
|
||||
...link,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
ref: createUniqueJobId(sourceId, targetId),
|
||||
path: path.toString(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
|
@ -14,6 +14,42 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isHighlighted: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isFadedOut: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
handleMouseOver: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
handleMouseLeave: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobPillClasses() {
|
||||
return [
|
||||
{ 'gl-opacity-3': this.isFadedOut },
|
||||
this.isHighlighted ? 'gl-shadow-blue-200-x0-y0-b4-s2' : 'gl-inset-border-2-green-400',
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onMouseEnter() {
|
||||
this.$emit('on-mouse-enter', this.jobId);
|
||||
},
|
||||
onMouseLeave() {
|
||||
this.$emit('on-mouse-leave');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -21,7 +57,10 @@ export default {
|
|||
<tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
|
||||
<div
|
||||
:id="jobId"
|
||||
class="gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-inset-border-1-green-600 gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 pipeline-job-pill "
|
||||
class="pipeline-job-pill gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
|
||||
:class="jobPillClasses"
|
||||
@mouseover="onMouseEnter"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
{{ jobName }}
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import StagePill from './stage_pill.vue';
|
|||
import { generateLinksData } from './drawing_utils';
|
||||
import { parseData } from '../parsing_utils';
|
||||
import { DRAW_FAILURE, DEFAULT } from '../../constants';
|
||||
import { createUniqueJobId } from '../../utils';
|
||||
import { generateJobNeedsDict } from '../../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -31,7 +31,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
failureType: null,
|
||||
highlightedJob: null,
|
||||
links: [],
|
||||
needsObject: null,
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
|
@ -43,6 +45,9 @@ export default {
|
|||
hasError() {
|
||||
return this.failureType;
|
||||
},
|
||||
hasHighlightedJob() {
|
||||
return Boolean(this.highlightedJob);
|
||||
},
|
||||
failure() {
|
||||
const text = this.$options.errorTexts[this.failureType] || this.$options.errorTexts[DEFAULT];
|
||||
|
||||
|
@ -51,8 +56,27 @@ export default {
|
|||
viewBox() {
|
||||
return [0, 0, this.width, this.height];
|
||||
},
|
||||
lineStyle() {
|
||||
return `stroke-width:${this.$options.STROKE_WIDTH}px;`;
|
||||
highlightedJobs() {
|
||||
// If you are hovering on a job, then the jobs we want to highlight are:
|
||||
// The job you are currently hovering + all of its needs.
|
||||
return this.hasHighlightedJob
|
||||
? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
|
||||
: [];
|
||||
},
|
||||
highlightedLinks() {
|
||||
// If you are hovering on a job, then the links we want to highlight are:
|
||||
// All the links whose `source` and `target` are highlighted jobs.
|
||||
if (this.hasHighlightedJob) {
|
||||
const filteredLinks = this.links.filter(link => {
|
||||
return (
|
||||
this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
|
||||
);
|
||||
});
|
||||
|
||||
return filteredLinks.map(link => link.ref);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -62,9 +86,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
createJobId(stageName, jobName) {
|
||||
return createUniqueJobId(stageName, jobName);
|
||||
},
|
||||
drawJobLinks() {
|
||||
const { stages, jobs } = this.pipelineData;
|
||||
const unwrappedGroups = this.unwrapPipelineData(stages);
|
||||
|
@ -76,6 +97,18 @@ export default {
|
|||
this.reportFailure(DRAW_FAILURE);
|
||||
}
|
||||
},
|
||||
highlightNeeds(uniqueJobId) {
|
||||
// The first time we hover, we create the object where
|
||||
// we store all the data to properly highlight the needs.
|
||||
if (!this.needsObject) {
|
||||
this.needsObject = generateJobNeedsDict(this.pipelineData) ?? {};
|
||||
}
|
||||
|
||||
this.highlightedJob = uniqueJobId;
|
||||
},
|
||||
removeHighlightNeeds() {
|
||||
this.highlightedJob = null;
|
||||
},
|
||||
unwrapPipelineData(stages) {
|
||||
return stages
|
||||
.map(({ name, groups }) => {
|
||||
|
@ -95,6 +128,18 @@ export default {
|
|||
resetFailure() {
|
||||
this.failureType = null;
|
||||
},
|
||||
isJobHighlighted(jobName) {
|
||||
return this.highlightedJobs.includes(jobName);
|
||||
},
|
||||
isLinkHighlighted(linkRef) {
|
||||
return this.highlightedLinks.includes(linkRef);
|
||||
},
|
||||
getLinkClasses(link) {
|
||||
return [
|
||||
this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : 'gl-stroke-gray-200',
|
||||
{ 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
|
||||
];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -113,13 +158,17 @@ export default {
|
|||
class="gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
|
||||
>
|
||||
<svg :viewBox="viewBox" :width="width" :height="height" class="gl-absolute">
|
||||
<path
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
:d="link.path"
|
||||
class="gl-stroke-gray-200 gl-fill-transparent"
|
||||
:style="lineStyle"
|
||||
/>
|
||||
<template>
|
||||
<path
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
:ref="link.ref"
|
||||
:d="link.path"
|
||||
class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
|
||||
:class="getLinkClasses(link)"
|
||||
:stroke-width="$options.STROKE_WIDTH"
|
||||
/>
|
||||
</template>
|
||||
</svg>
|
||||
<div
|
||||
v-for="(stage, index) in pipelineData.stages"
|
||||
|
@ -141,8 +190,12 @@ export default {
|
|||
<job-pill
|
||||
v-for="group in stage.groups"
|
||||
:key="group.name"
|
||||
:job-id="createJobId(stage.name, group.name)"
|
||||
:job-id="group.id"
|
||||
:job-name="group.name"
|
||||
:is-highlighted="hasHighlightedJob && isJobHighlighted(group.id)"
|
||||
:is-faded-out="hasHighlightedJob && !isJobHighlighted(group.id)"
|
||||
@on-mouse-enter="highlightNeeds"
|
||||
@on-mouse-leave="removeHighlightNeeds"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,8 @@ export const validateParams = params => {
|
|||
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
|
||||
};
|
||||
|
||||
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
|
||||
|
||||
/**
|
||||
* This function takes a json payload that comes from a yml
|
||||
* file converted to json through `jsyaml` library. Because we
|
||||
|
@ -21,7 +23,10 @@ export const preparePipelineGraphData = jsonData => {
|
|||
// Creates an object with only the valid jobs
|
||||
const jobs = jsonKeys.reduce((acc, val) => {
|
||||
if (jobNames.includes(val)) {
|
||||
return { ...acc, [val]: { ...jsonData[val] } };
|
||||
return {
|
||||
...acc,
|
||||
[val]: { ...jsonData[val], id: createUniqueJobId(jsonData[val].stage, val) },
|
||||
};
|
||||
}
|
||||
return { ...acc };
|
||||
}, {});
|
||||
|
@ -47,7 +52,11 @@ export const preparePipelineGraphData = jsonData => {
|
|||
return {
|
||||
name: stage,
|
||||
groups: stageJobs.map(job => {
|
||||
return { name: job, jobs: [{ ...jsonData[job] }] };
|
||||
return {
|
||||
name: job,
|
||||
jobs: [{ ...jsonData[job] }],
|
||||
id: createUniqueJobId(stage, job),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
@ -55,4 +64,33 @@ export const preparePipelineGraphData = jsonData => {
|
|||
return { stages: pipelineData, jobs };
|
||||
};
|
||||
|
||||
export const createUniqueJobId = (stageName, jobName) => `${stageName}-${jobName}`;
|
||||
export const generateJobNeedsDict = ({ jobs }) => {
|
||||
const arrOfJobNames = Object.keys(jobs);
|
||||
|
||||
return arrOfJobNames.reduce((acc, value) => {
|
||||
const recursiveNeeds = jobName => {
|
||||
if (!jobs[jobName]?.needs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return jobs[jobName].needs
|
||||
.map(job => {
|
||||
const { id } = jobs[job];
|
||||
// If we already have the needs of a job in the accumulator,
|
||||
// then we use the memoized data instead of the recursive call
|
||||
// to save some performance.
|
||||
const newNeeds = acc[id] ?? recursiveNeeds(job);
|
||||
|
||||
return [id, ...newNeeds];
|
||||
})
|
||||
.flat(Infinity);
|
||||
};
|
||||
|
||||
// To ensure we don't have duplicates job relationship when 2 jobs
|
||||
// needed by another both depends on the same jobs, we remove any
|
||||
// duplicates from the array.
|
||||
const uniqueValues = Array.from(new Set(recursiveNeeds(value)));
|
||||
|
||||
return { ...acc, [jobs[value].id]: uniqueValues };
|
||||
}, {});
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
|
||||
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
|
||||
import EditorLite from '~/vue_shared/components/editor_lite.vue';
|
||||
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
|
||||
|
@ -11,8 +11,8 @@ import { sprintf } from '~/locale';
|
|||
export default {
|
||||
components: {
|
||||
BlobHeaderEdit,
|
||||
BlobContentEdit,
|
||||
GlLoadingIcon,
|
||||
EditorLite,
|
||||
},
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
|
@ -85,7 +85,7 @@ export default {
|
|||
size="lg"
|
||||
class="loading-animation prepend-top-20 gl-mb-6"
|
||||
/>
|
||||
<blob-content-edit
|
||||
<editor-lite
|
||||
v-else
|
||||
:value="blob.content"
|
||||
:file-global-id="blob.id"
|
||||
|
|
|
@ -9,7 +9,7 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
|
|||
respectDoNotTrack: true,
|
||||
forceSecureTracker: true,
|
||||
eventMethod: 'post',
|
||||
contexts: { webPage: true },
|
||||
contexts: { webPage: true, performanceTiming: true },
|
||||
formTracking: false,
|
||||
linkClickTracking: false,
|
||||
};
|
||||
|
|
|
@ -24,3 +24,210 @@
|
|||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mini-pipeline-graph-color(
|
||||
$color-background-default,
|
||||
$color-background-hover-focus,
|
||||
$color-background-active,
|
||||
$color-foreground-default,
|
||||
$color-foreground-hover-focus,
|
||||
$color-foreground-active
|
||||
) {
|
||||
background-color: $color-background-default;
|
||||
border-color: $color-foreground-default;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-default;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $color-background-hover-focus;
|
||||
border-color: $color-foreground-hover-focus;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-hover-focus;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $color-background-active;
|
||||
border-color: $color-foreground-active;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-active;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 4px 1px $blue-300;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mini-pipeline-item() {
|
||||
border-radius: 100px;
|
||||
background-color: $white;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
width: $ci-action-icon-size;
|
||||
height: $ci-action-icon-size;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
// Dropdown button animation in mini pipeline graph
|
||||
&.ci-status-icon-success {
|
||||
@include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-failed {
|
||||
@include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-pending,
|
||||
&.ci-status-icon-waiting-for-resource,
|
||||
&.ci-status-icon-success-with-warnings {
|
||||
@include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-preparing,
|
||||
&.ci-status-icon-running {
|
||||
@include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-canceled,
|
||||
&.ci-status-icon-scheduled,
|
||||
&.ci-status-icon-disabled,
|
||||
&.ci-status-icon-not-found,
|
||||
&.ci-status-icon-manual {
|
||||
@include mini-pipeline-graph-color($white, $gray-500, $gray-700, $gray-900, $gray-950, $black);
|
||||
}
|
||||
|
||||
&.ci-status-icon-created,
|
||||
&.ci-status-icon-skipped {
|
||||
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Action icons inside dropdowns:
|
||||
- mini graph in pipelines table
|
||||
- dropdown in big graph
|
||||
- mini graph in MR widget pipeline
|
||||
- mini graph in Commit widget pipeline
|
||||
*/
|
||||
@mixin pipeline-graph-dropdown-menu() {
|
||||
width: 240px;
|
||||
max-width: 240px;
|
||||
|
||||
// override dropdown.scss
|
||||
&.dropdown-menu li button,
|
||||
&.dropdown-menu li a.ci-action-icon-container {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ci-action-icon-container {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
|
||||
&.ci-action-icon-wrapper {
|
||||
height: $ci-action-dropdown-button-size;
|
||||
width: $ci-action-dropdown-button-size;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
|
||||
background-color: $gray-darker;
|
||||
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner,
|
||||
svg {
|
||||
width: $ci-action-dropdown-svg-size;
|
||||
height: $ci-action-dropdown-svg-size;
|
||||
fill: $gl-text-color-secondary;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
vertical-align: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SVGs in the commit widget and mr widget
|
||||
a.ci-action-icon-container.ci-action-icon-wrapper svg {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
padding: 0;
|
||||
max-height: 245px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
|
||||
&:hover > .mini-pipeline-graph-dropdown-item,
|
||||
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
|
||||
@extend .mini-pipeline-graph-dropdown-item:hover;
|
||||
}
|
||||
|
||||
// link to the build
|
||||
.mini-pipeline-graph-dropdown-item {
|
||||
align-items: center;
|
||||
clear: both;
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
line-height: $line-height-base;
|
||||
white-space: nowrap;
|
||||
|
||||
// Match dropdown.scss for all `a` tags
|
||||
&.non-details-job-component {
|
||||
padding: $gl-padding-8 $gl-btn-horz-padding;
|
||||
}
|
||||
|
||||
.ci-job-name-component {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ci-status-icon {
|
||||
@include gl-mr-3;
|
||||
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
width: $pipeline-dropdown-status-icon-size;
|
||||
height: $pipeline-dropdown-status-icon-size;
|
||||
margin: 3px 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
background-color: $gray-darker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -386,7 +386,6 @@
|
|||
top: 8px;
|
||||
}
|
||||
|
||||
|
||||
.split-report-section {
|
||||
border-bottom: 1px solid var(--gray-50, $gray-50);
|
||||
|
||||
|
@ -427,6 +426,43 @@
|
|||
}
|
||||
}
|
||||
|
||||
.big-pipeline-graph-dropdown-menu {
|
||||
@include pipeline-graph-dropdown-menu();
|
||||
width: 195px;
|
||||
min-width: 195px;
|
||||
left: 100%;
|
||||
top: -10px;
|
||||
box-shadow: 0 1px 5px $black-transparent;
|
||||
|
||||
/**
|
||||
* Top arrow in the dropdown in the big pipeline graph
|
||||
*/
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -6px;
|
||||
margin-top: 3px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -5px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.codequality-report {
|
||||
.media {
|
||||
padding: $gl-padding;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
@import 'mixins_and_variables_and_functions';
|
||||
@import './pipeline_mixins';
|
||||
|
||||
/**
|
||||
* Pipelines Bundle
|
||||
*
|
||||
* Styles of pipeline lists
|
||||
*
|
||||
* Should affect pipelines table components rendered by:
|
||||
* app/assets/javascripts/commit/pipelines/pipelines_bundle.js
|
||||
* Pipelines Bundle: Pipeline lists and Mini Pipelines
|
||||
*/
|
||||
|
||||
// Pipelines list
|
||||
// Should affect pipelines table components rendered by:
|
||||
// - app/assets/javascripts/commit/pipelines/pipelines_bundle.js
|
||||
|
||||
.pipelines {
|
||||
.badge {
|
||||
margin-bottom: 3px;
|
||||
|
@ -64,3 +64,132 @@
|
|||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Mini Pipelines
|
||||
|
||||
.stage-cell {
|
||||
.mini-pipeline-graph-dropdown-toggle {
|
||||
svg {
|
||||
height: $ci-action-icon-size;
|
||||
width: $ci-action-icon-size;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
svg {
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: $ci-action-icon-size;
|
||||
margin: 3px 0;
|
||||
|
||||
+ .stage-container {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
// Hack to show a button tooltip inline
|
||||
button.has-tooltip + .tooltip {
|
||||
min-width: 105px;
|
||||
}
|
||||
|
||||
// Bootstrap way of showing the content inline for anchors.
|
||||
a.has-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
content: '';
|
||||
width: 7px;
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
top: 11px;
|
||||
border-bottom: 2px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
//delete when all pipelines are updated to new size
|
||||
&.mr-widget-pipeline-stages {
|
||||
+ .stage-container {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
width: 4px;
|
||||
right: -4px;
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown button in mini pipeline graph
|
||||
button.mini-pipeline-graph-dropdown-toggle {
|
||||
@include mini-pipeline-item();
|
||||
}
|
||||
|
||||
// Action icons inside dropdowns:
|
||||
// mini graph in pipelines table
|
||||
// mini graph in MR widget pipeline
|
||||
// mini graph in Commit widget pipeline
|
||||
.mini-pipeline-graph-dropdown-menu {
|
||||
@include pipeline-graph-dropdown-menu();
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
border-width: 0 5px 6px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
left: 100%;
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-width: 0 5px 5px;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-top: 1px;
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
|
||||
/**
|
||||
* Center dropdown menu in mini graph
|
||||
*/
|
||||
.dropdown &.dropdown-menu {
|
||||
transform: translate(-80%, 0);
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||
transform: translate(-50%, 0);
|
||||
right: auto;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,77 +78,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.stage-cell {
|
||||
.mini-pipeline-graph-dropdown-toggle {
|
||||
svg {
|
||||
height: $ci-action-icon-size;
|
||||
width: $ci-action-icon-size;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
svg {
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stage-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: $ci-action-icon-size;
|
||||
margin: 3px 0;
|
||||
|
||||
+ .stage-container {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
// Hack to show a button tooltip inline
|
||||
button.has-tooltip + .tooltip {
|
||||
min-width: 105px;
|
||||
}
|
||||
|
||||
// Bootstrap way of showing the content inline for anchors.
|
||||
a.has-tooltip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
content: '';
|
||||
width: 7px;
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
top: 11px;
|
||||
border-bottom: 2px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
//delete when all pipelines are updated to new size
|
||||
&.mr-widget-pipeline-stages {
|
||||
+ .stage-container {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
&::after {
|
||||
width: 4px;
|
||||
right: -4px;
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-page='admin:jobs:index'] {
|
||||
.admin-builds-table {
|
||||
td:last-child {
|
||||
|
@ -162,305 +91,6 @@
|
|||
font-weight: 200;
|
||||
}
|
||||
|
||||
@mixin mini-pipeline-graph-color(
|
||||
$color-background-default,
|
||||
$color-background-hover-focus,
|
||||
$color-background-active,
|
||||
$color-foreground-default,
|
||||
$color-foreground-hover-focus,
|
||||
$color-foreground-active
|
||||
) {
|
||||
background-color: $color-background-default;
|
||||
border-color: $color-foreground-default;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-default;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $color-background-hover-focus;
|
||||
border-color: $color-foreground-hover-focus;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-hover-focus;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $color-background-active;
|
||||
border-color: $color-foreground-active;
|
||||
|
||||
svg {
|
||||
fill: $color-foreground-active;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 4px 1px $blue-300;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mini-pipeline-item() {
|
||||
border-radius: 100px;
|
||||
background-color: $white;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
width: $ci-action-icon-size;
|
||||
height: $ci-action-icon-size;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
// Dropdown button animation in mini pipeline graph
|
||||
&.ci-status-icon-success {
|
||||
@include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-failed {
|
||||
@include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-pending,
|
||||
&.ci-status-icon-waiting-for-resource,
|
||||
&.ci-status-icon-success-with-warnings {
|
||||
@include mini-pipeline-graph-color($white, $orange-50, $orange-100, $orange-500, $orange-600, $orange-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-preparing,
|
||||
&.ci-status-icon-running {
|
||||
@include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700);
|
||||
}
|
||||
|
||||
&.ci-status-icon-canceled,
|
||||
&.ci-status-icon-scheduled,
|
||||
&.ci-status-icon-disabled,
|
||||
&.ci-status-icon-not-found,
|
||||
&.ci-status-icon-manual {
|
||||
@include mini-pipeline-graph-color($white, $gray-500, $gray-700, $gray-900, $gray-950, $black);
|
||||
}
|
||||
|
||||
&.ci-status-icon-created,
|
||||
&.ci-status-icon-skipped {
|
||||
@include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown button in mini pipeline graph
|
||||
button.mini-pipeline-graph-dropdown-toggle {
|
||||
@include mini-pipeline-item();
|
||||
}
|
||||
|
||||
/**
|
||||
Action icons inside dropdowns:
|
||||
- mini graph in pipelines table
|
||||
- dropdown in big graph
|
||||
- mini graph in MR widget pipeline
|
||||
- mini graph in Commit widget pipeline
|
||||
*/
|
||||
.big-pipeline-graph-dropdown-menu,
|
||||
.mini-pipeline-graph-dropdown-menu {
|
||||
width: 240px;
|
||||
max-width: 240px;
|
||||
|
||||
// override dropdown.scss
|
||||
&.dropdown-menu li button,
|
||||
&.dropdown-menu li a.ci-action-icon-container {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ci-action-icon-container {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
|
||||
&.ci-action-icon-wrapper {
|
||||
height: $ci-action-dropdown-button-size;
|
||||
width: $ci-action-dropdown-button-size;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
|
||||
background-color: $gray-darker;
|
||||
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner,
|
||||
svg {
|
||||
width: $ci-action-dropdown-svg-size;
|
||||
height: $ci-action-dropdown-svg-size;
|
||||
fill: $gl-text-color-secondary;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
vertical-align: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SVGs in the commit widget and mr widget
|
||||
a.ci-action-icon-container.ci-action-icon-wrapper svg {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.scrollable-menu {
|
||||
padding: 0;
|
||||
max-height: 245px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
|
||||
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
|
||||
&:hover > .mini-pipeline-graph-dropdown-item,
|
||||
&:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
|
||||
@extend .mini-pipeline-graph-dropdown-item:hover;
|
||||
}
|
||||
|
||||
// link to the build
|
||||
.mini-pipeline-graph-dropdown-item {
|
||||
align-items: center;
|
||||
clear: both;
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
line-height: $line-height-base;
|
||||
white-space: nowrap;
|
||||
|
||||
// Match dropdown.scss for all `a` tags
|
||||
&.non-details-job-component {
|
||||
padding: $gl-padding-8 $gl-btn-horz-padding;
|
||||
}
|
||||
|
||||
.ci-job-name-component {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.ci-status-icon {
|
||||
@include gl-mr-3;
|
||||
|
||||
position: relative;
|
||||
|
||||
> svg {
|
||||
width: $pipeline-dropdown-status-icon-size;
|
||||
height: $pipeline-dropdown-status-icon-size;
|
||||
margin: 3px 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
background-color: $gray-darker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown in the big pipeline graph
|
||||
.big-pipeline-graph-dropdown-menu {
|
||||
width: 195px;
|
||||
min-width: 195px;
|
||||
left: 100%;
|
||||
top: -10px;
|
||||
box-shadow: 0 1px 5px $black-transparent;
|
||||
|
||||
/**
|
||||
* Top arrow in the dropdown in the big pipeline graph
|
||||
*/
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -6px;
|
||||
margin-top: 3px;
|
||||
border-width: 7px 5px 7px 0;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -5px;
|
||||
border-width: 10px 7px 10px 0;
|
||||
border-right-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Top arrow in the dropdown in the mini pipeline graph
|
||||
*/
|
||||
.mini-pipeline-graph-dropdown-menu {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
border-width: 0 5px 6px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
left: 100%;
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-width: 0 5px 5px;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
|
||||
&::after {
|
||||
margin-top: 1px;
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
|
||||
/**
|
||||
* Center dropdown menu in mini graph
|
||||
*/
|
||||
.dropdown &.dropdown-menu {
|
||||
transform: translate(-80%, 0);
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||
transform: translate(-50%, 0);
|
||||
right: auto;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
|
|||
).execute
|
||||
|
||||
if service_response.success?
|
||||
Gitlab::UsageDataCounters::StaticSiteEditorCounter.increment_views_count
|
||||
|
||||
@data = serialize_necessary_payload_values_to_json(service_response.payload)
|
||||
else
|
||||
# TODO: For now, if the service returns any error, the user is redirected
|
||||
|
|
|
@ -44,6 +44,7 @@ module ResolvesMergeRequests
|
|||
author: [:author],
|
||||
merged_at: [:metrics],
|
||||
commit_count: [:metrics],
|
||||
diff_stats_summary: [:metrics],
|
||||
approved_by: [:approved_by_users],
|
||||
milestone: [:milestone],
|
||||
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
|
||||
|
|
|
@ -170,6 +170,12 @@ module Types
|
|||
end
|
||||
|
||||
def diff_stats_summary
|
||||
metrics = object.metrics
|
||||
|
||||
if metrics && metrics.added_lines && metrics.removed_lines
|
||||
return { additions: metrics.added_lines, deletions: metrics.removed_lines, file_count: object.merge_request_diff&.files_count || 0 }
|
||||
end
|
||||
|
||||
nil_stats = { additions: 0, deletions: 0, file_count: 0 }
|
||||
return nil_stats unless object.diff_stats.present?
|
||||
|
||||
|
|
|
@ -40,7 +40,12 @@ module Jira
|
|||
build_service_response(response)
|
||||
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
|
||||
error_message = "Jira request error: #{error.message}"
|
||||
log_error("Error sending message", client_url: client.options[:site], error: error_message)
|
||||
log_error("Error sending message", client_url: client.options[:site],
|
||||
error: {
|
||||
exception_class: error.class.name,
|
||||
exception_message: error.message,
|
||||
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
|
||||
})
|
||||
ServiceResponse.error(message: error_message)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module Packages
|
||||
module Composer
|
||||
class ComposerJsonService
|
||||
InvalidJson = Class.new(StandardError)
|
||||
|
||||
def initialize(project, target)
|
||||
@project, @target = project, target
|
||||
end
|
||||
|
@ -20,11 +22,11 @@ module Packages
|
|||
|
||||
Gitlab::Json.parse(composer_file.data)
|
||||
rescue JSON::ParserError
|
||||
raise 'Could not parse composer.json file. Invalid JSON.'
|
||||
raise InvalidJson, 'Could not parse composer.json file. Invalid JSON.'
|
||||
end
|
||||
|
||||
def composer_file_not_found!
|
||||
raise 'The file composer.json was not found.'
|
||||
raise InvalidJson, 'The file composer.json was not found.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
- @content_class = limited_container_width
|
||||
- page_title "#{@commit.title} (#{@commit.short_id})", _('Commits')
|
||||
- page_description @commit.description
|
||||
- add_page_specific_style 'page_bundles/pipelines'
|
||||
|
||||
.container-fluid{ class: [limited_container_width, container_class] }
|
||||
= render "commit_box"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replaced blob-content-edit with editor-lite compoennt for Snippet edit form
|
||||
merge_request: 44994
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add usage ping to count Static Site Editor views
|
||||
merge_request: 44573
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Optionally use merge request metrics association for merge request diff stats
|
||||
in GraphQL
|
||||
merge_request: 44613
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Return 422 error rather than 500 when composer.json is missing or malformed
|
||||
merge_request: 44587
|
||||
author: David Barr @davebarr
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix emoji rendering in certain edge cases
|
||||
merge_request: 44542
|
||||
author:
|
||||
type: fixed
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: ci_bridge_pipeline_details
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41263
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/250683
|
||||
group: group::memory
|
||||
type: development
|
||||
default_enabled: true
|
|
@ -1,3 +1 @@
|
|||
require 'terser'
|
||||
|
||||
Sprockets.register_compressor 'application/javascript', :terser, Terser::Compressor
|
||||
|
|
|
@ -8,8 +8,7 @@ docs_paths_to_review = helper.changes_by_category[:docs]
|
|||
|
||||
return if docs_paths_to_review.empty?
|
||||
|
||||
message 'This merge request adds or changes files that require a review ' \
|
||||
'from the Technical Writing team.'
|
||||
message 'This merge request adds or changes documentation files. A review from the Technical Writing team before you merge is **recommended**. Reviews can happen after you merge.'
|
||||
|
||||
return unless gitlab_danger.ci?
|
||||
|
||||
|
|
|
@ -53,8 +53,14 @@ supporting custom domains a secondary IP is not needed.
|
|||
|
||||
Before proceeding with the Pages configuration, you will need to:
|
||||
|
||||
1. Have an exclusive root domain for serving GitLab Pages. Note that you cannot
|
||||
use a subdomain of your GitLab's instance domain.
|
||||
1. Have a domain for Pages that is not a subdomain of your GitLab's instance domain.
|
||||
|
||||
| GitLab domain | Pages domain | Does it work? |
|
||||
| :---: | :---: | :---: |
|
||||
| `example.com` | `example.io` | **{check-circle}** Yes |
|
||||
| `example.com` | `pages.example.com` | **{dotted-circle}** No |
|
||||
| `gitlab.example.com` | `pages.example.com` | **{check-circle}** Yes |
|
||||
|
||||
1. Configure a **wildcard DNS record**.
|
||||
1. (Optional) Have a **wildcard certificate** for that domain if you decide to
|
||||
serve Pages under HTTPS.
|
||||
|
|
|
@ -3584,10 +3584,11 @@ job split into three separate jobs.
|
|||
Use `matrix:` to configure different variables for jobs that are running in parallel.
|
||||
There can be from 2 to 50 jobs.
|
||||
|
||||
In GitLab 13.5 and later, you can have one-dimensional matrices with a single job.
|
||||
[In GitLab 13.5](https://gitlab.com/gitlab-org/gitlab/-/issues/26362) and later,
|
||||
you can have one-dimensional matrices with a single job.
|
||||
The ability to have one-dimensional matrices is [deployed behind a feature flag](../../user/feature_flags.md),
|
||||
disabled by default. It's disabled on GitLab.com. To use it in a GitLab self-managed
|
||||
instance, ask a GitLab administrator to [enable the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE-ONLY)**
|
||||
instance, ask a GitLab administrator to [enable the `one_dimensional_matrix:` feature flag](../../administration/feature_flags.md). **(CORE ONLY)**
|
||||
|
||||
Every job gets the same `CI_NODE_TOTAL` [environment variable](../variables/README.md#predefined-environment-variables) value, and a unique `CI_NODE_INDEX` value.
|
||||
|
||||
|
|
|
@ -26,6 +26,10 @@ module API
|
|||
render_api_error!(e.message, 400)
|
||||
end
|
||||
|
||||
rescue_from Packages::Composer::ComposerJsonService::InvalidJson do |e|
|
||||
render_api_error!(e.message, 422)
|
||||
end
|
||||
|
||||
helpers do
|
||||
def packages
|
||||
strong_memoize(:packages) do
|
||||
|
|
|
@ -14,7 +14,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def details_path
|
||||
return unless Feature.enabled?(:ci_bridge_pipeline_details, subject.project, default_enabled: true)
|
||||
return unless can?(user, :read_pipeline, downstream_pipeline)
|
||||
|
||||
project_pipeline_path(downstream_project, downstream_pipeline)
|
||||
|
|
|
@ -4,11 +4,7 @@ module Gitlab
|
|||
module Database
|
||||
module Reindexing
|
||||
def self.perform(index_selector)
|
||||
Array.wrap(index_selector).each do |index|
|
||||
ReindexAction.keep_track_of(index) do
|
||||
ConcurrentReindex.new(index).perform
|
||||
end
|
||||
end
|
||||
Coordinator.new(index_selector).perform
|
||||
end
|
||||
|
||||
def self.candidate_indexes
|
||||
|
|
|
@ -5,13 +5,13 @@ module Gitlab
|
|||
module Reindexing
|
||||
class ConcurrentReindex
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include MigrationHelpers
|
||||
|
||||
ReindexError = Class.new(StandardError)
|
||||
|
||||
PG_IDENTIFIER_LENGTH = 63
|
||||
TEMPORARY_INDEX_PREFIX = 'tmp_reindex_'
|
||||
REPLACED_INDEX_PREFIX = 'old_reindex_'
|
||||
STATEMENT_TIMEOUT = 6.hours
|
||||
|
||||
attr_reader :index, :logger
|
||||
|
||||
|
@ -47,7 +47,7 @@ module Gitlab
|
|||
logger.info("creating replacement index #{replacement_index_name}")
|
||||
logger.debug("replacement index definition: #{create_replacement_index_statement}")
|
||||
|
||||
disable_statement_timeout do
|
||||
set_statement_timeout do
|
||||
connection.execute(create_replacement_index_statement)
|
||||
end
|
||||
|
||||
|
@ -88,7 +88,7 @@ module Gitlab
|
|||
def remove_index(schema, name)
|
||||
logger.info("Removing index #{schema}.#{name}")
|
||||
|
||||
disable_statement_timeout do
|
||||
set_statement_timeout do
|
||||
connection.execute(<<~SQL)
|
||||
DROP INDEX CONCURRENTLY
|
||||
IF EXISTS #{quote_table_name(schema)}.#{quote_table_name(name)}
|
||||
|
@ -110,6 +110,13 @@ module Gitlab
|
|||
Gitlab::Database::WithLockRetries.new(**arguments).run(raise_on_exhaustion: true, &block)
|
||||
end
|
||||
|
||||
def set_statement_timeout
|
||||
execute("SET statement_timeout TO #{STATEMENT_TIMEOUT}")
|
||||
yield
|
||||
ensure
|
||||
execute('RESET statement_timeout')
|
||||
end
|
||||
|
||||
delegate :execute, :quote_table_name, to: :connection
|
||||
def connection
|
||||
@connection ||= ActiveRecord::Base.connection
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Database
|
||||
module Reindexing
|
||||
class Coordinator
|
||||
include ExclusiveLeaseGuard
|
||||
|
||||
# Maximum lease time for the global Redis lease
|
||||
# This should be higher than the maximum time for any
|
||||
# long running step in the reindexing process (compare with
|
||||
# statement timeouts).
|
||||
TIMEOUT_PER_ACTION = 1.day
|
||||
|
||||
attr_reader :indexes
|
||||
|
||||
def initialize(indexes)
|
||||
@indexes = indexes
|
||||
end
|
||||
|
||||
def perform
|
||||
indexes.each do |index|
|
||||
# This obtains a global lease such that there's
|
||||
# only one live reindexing process at a time.
|
||||
try_obtain_lease do
|
||||
ReindexAction.keep_track_of(index) do
|
||||
ConcurrentReindex.new(index).perform
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lease_timeout
|
||||
TIMEOUT_PER_ACTION
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -266,7 +266,8 @@ module Gitlab
|
|||
Gitlab::UsageDataCounters::SourceCodeCounter,
|
||||
Gitlab::UsageDataCounters::MergeRequestCounter,
|
||||
Gitlab::UsageDataCounters::DesignsCounter,
|
||||
Gitlab::UsageDataCounters::KubernetesAgentCounter
|
||||
Gitlab::UsageDataCounters::KubernetesAgentCounter,
|
||||
Gitlab::UsageDataCounters::StaticSiteEditorCounter
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module UsageDataCounters
|
||||
class StaticSiteEditorCounter < BaseCounter
|
||||
KNOWN_EVENTS = %w[views].freeze
|
||||
PREFIX = 'static_site_editor'
|
||||
|
||||
class << self
|
||||
def increment_views_count
|
||||
count(:views)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -193,7 +193,7 @@ namespace :gitlab do
|
|||
end
|
||||
|
||||
indexes = if args[:index_name]
|
||||
Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])
|
||||
[Gitlab::Database::PostgresIndex.by_identifier(args[:index_name])]
|
||||
else
|
||||
Gitlab::Database::Reindexing.candidate_indexes.random_few(2)
|
||||
end
|
||||
|
|
|
@ -54,11 +54,16 @@ RSpec.describe Projects::StaticSiteEditorController do
|
|||
|
||||
context "as developer" do
|
||||
before do
|
||||
allow(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to receive(:increment_views_count)
|
||||
project.add_role(user, 'developer')
|
||||
sign_in(user)
|
||||
get :show, params: default_params
|
||||
end
|
||||
|
||||
it 'increases the views counter' do
|
||||
expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to have_received(:increment_views_count)
|
||||
end
|
||||
|
||||
it 'renders the edit page' do
|
||||
expect(response).to render_template(:show)
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { createUniqueJobId } from '~/pipelines/utils';
|
||||
|
||||
export const yamlString = `stages:
|
||||
- empty
|
||||
- build
|
||||
|
@ -39,18 +41,20 @@ deploy_a:
|
|||
script: echo hello
|
||||
`;
|
||||
|
||||
const jobId1 = createUniqueJobId('build', 'build_1');
|
||||
const jobId2 = createUniqueJobId('test', 'test_1');
|
||||
const jobId3 = createUniqueJobId('test', 'test_2');
|
||||
const jobId4 = createUniqueJobId('deploy', 'deploy_1');
|
||||
|
||||
export const pipelineData = {
|
||||
stages: [
|
||||
{
|
||||
name: 'build',
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
name: 'build',
|
||||
groups: [
|
||||
{
|
||||
name: 'build_1',
|
||||
jobs: [{ script: 'echo hello', stage: 'build' }],
|
||||
id: jobId1,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -60,10 +64,12 @@ export const pipelineData = {
|
|||
{
|
||||
name: 'test_1',
|
||||
jobs: [{ script: 'yarn test', stage: 'test' }],
|
||||
id: jobId2,
|
||||
},
|
||||
{
|
||||
name: 'test_2',
|
||||
jobs: [{ script: 'yarn karma', stage: 'test' }],
|
||||
id: jobId3,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -73,8 +79,15 @@ export const pipelineData = {
|
|||
{
|
||||
name: 'deploy_1',
|
||||
jobs: [{ script: 'yarn magick', stage: 'deploy' }],
|
||||
id: jobId4,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: {
|
||||
[jobId1]: {},
|
||||
[jobId2]: {},
|
||||
[jobId3]: {},
|
||||
[jobId4]: {},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,161 +1,211 @@
|
|||
import { preparePipelineGraphData } from '~/pipelines/utils';
|
||||
import {
|
||||
preparePipelineGraphData,
|
||||
createUniqueJobId,
|
||||
generateJobNeedsDict,
|
||||
} from '~/pipelines/utils';
|
||||
|
||||
describe('preparePipelineGraphData', () => {
|
||||
describe('utils functions', () => {
|
||||
const emptyResponse = { stages: [], jobs: {} };
|
||||
const jobName1 = 'build_1';
|
||||
const jobName2 = 'build_2';
|
||||
const jobName3 = 'test_1';
|
||||
const jobName4 = 'deploy_1';
|
||||
const job1 = { [jobName1]: { script: 'echo hello', stage: 'build' } };
|
||||
const job2 = { [jobName2]: { script: 'echo build', stage: 'build' } };
|
||||
const job3 = { [jobName3]: { script: 'echo test', stage: 'test' } };
|
||||
const job4 = { [jobName4]: { script: 'echo deploy', stage: 'deploy' } };
|
||||
const job1 = { script: 'echo hello', stage: 'build' };
|
||||
const job2 = { script: 'echo build', stage: 'build' };
|
||||
const job3 = { script: 'echo test', stage: 'test', needs: [jobName1, jobName2] };
|
||||
const job4 = { script: 'echo deploy', stage: 'deploy', needs: [jobName3] };
|
||||
const userDefinedStage = 'myStage';
|
||||
|
||||
describe('returns an empty array of stages and empty job objects if', () => {
|
||||
it('no data is passed', () => {
|
||||
expect(preparePipelineGraphData({})).toEqual(emptyResponse);
|
||||
const pipelineGraphData = {
|
||||
stages: [
|
||||
{
|
||||
name: userDefinedStage,
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
name: job4.stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName4,
|
||||
jobs: [{ ...job4 }],
|
||||
id: createUniqueJobId(job4.stage, jobName4),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: job1.stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ ...job1 }],
|
||||
id: createUniqueJobId(job1.stage, jobName1),
|
||||
},
|
||||
{
|
||||
name: jobName2,
|
||||
jobs: [{ ...job2 }],
|
||||
id: createUniqueJobId(job2.stage, jobName2),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: job3.stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName3,
|
||||
jobs: [{ ...job3 }],
|
||||
id: createUniqueJobId(job3.stage, jobName3),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: {
|
||||
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
|
||||
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
|
||||
[jobName3]: { ...job3, id: createUniqueJobId(job3.stage, jobName3) },
|
||||
[jobName4]: { ...job4, id: createUniqueJobId(job4.stage, jobName4) },
|
||||
},
|
||||
};
|
||||
|
||||
describe('preparePipelineGraphData', () => {
|
||||
describe('returns an empty array of stages and empty job objects if', () => {
|
||||
it('no data is passed', () => {
|
||||
expect(preparePipelineGraphData({})).toEqual(emptyResponse);
|
||||
});
|
||||
|
||||
it('no stages are found', () => {
|
||||
expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual(
|
||||
emptyResponse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('no stages are found', () => {
|
||||
expect(preparePipelineGraphData({ includes: 'template/myTemplate.gitlab-ci.yml' })).toEqual(
|
||||
emptyResponse,
|
||||
);
|
||||
describe('returns the correct array of stages and object of jobs', () => {
|
||||
it('when multiple jobs are in the same stage', () => {
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: job1.stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ ...job1 }],
|
||||
id: createUniqueJobId(job1.stage, jobName1),
|
||||
},
|
||||
{
|
||||
name: jobName2,
|
||||
jobs: [{ ...job2 }],
|
||||
id: createUniqueJobId(job2.stage, jobName2),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: {
|
||||
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
|
||||
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
|
||||
},
|
||||
};
|
||||
expect(
|
||||
preparePipelineGraphData({ [jobName1]: { ...job1 }, [jobName2]: { ...job2 } }),
|
||||
).toEqual(expectedData);
|
||||
});
|
||||
|
||||
it('when stages are defined by the user', () => {
|
||||
const userDefinedStage2 = 'myStage2';
|
||||
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: userDefinedStage,
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
name: userDefinedStage2,
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
jobs: {},
|
||||
};
|
||||
|
||||
expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual(
|
||||
expectedData,
|
||||
);
|
||||
});
|
||||
|
||||
it('by combining user defined stage and job stages, it preserves user defined order', () => {
|
||||
const userDefinedStageThatOverlaps = 'deploy';
|
||||
|
||||
expect(
|
||||
preparePipelineGraphData({
|
||||
stages: [userDefinedStage, userDefinedStageThatOverlaps],
|
||||
[jobName1]: { ...job1 },
|
||||
[jobName2]: { ...job2 },
|
||||
[jobName3]: { ...job3 },
|
||||
[jobName4]: { ...job4 },
|
||||
}),
|
||||
).toEqual(pipelineGraphData);
|
||||
});
|
||||
|
||||
it('with only unique values', () => {
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: job1.stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ ...job1 }],
|
||||
id: createUniqueJobId(job1.stage, jobName1),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: {
|
||||
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
preparePipelineGraphData({
|
||||
stages: ['build'],
|
||||
[jobName1]: { ...job1 },
|
||||
[jobName1]: { ...job1 },
|
||||
}),
|
||||
).toEqual(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('returns the correct array of stages and object of jobs', () => {
|
||||
it('when multiple jobs are in the same stage', () => {
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: job1[jobName1].stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
|
||||
},
|
||||
{
|
||||
name: jobName2,
|
||||
jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: { ...job1, ...job2 },
|
||||
};
|
||||
|
||||
expect(preparePipelineGraphData({ ...job1, ...job2 })).toEqual(expectedData);
|
||||
describe('generateJobNeedsDict', () => {
|
||||
it('generates an empty object if it receives no jobs', () => {
|
||||
expect(generateJobNeedsDict({ jobs: {} })).toEqual({});
|
||||
});
|
||||
|
||||
it('when stages are defined by the user', () => {
|
||||
const userDefinedStage = 'myStage';
|
||||
const userDefinedStage2 = 'myStage2';
|
||||
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: userDefinedStage,
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
name: userDefinedStage2,
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
jobs: {},
|
||||
};
|
||||
|
||||
expect(preparePipelineGraphData({ stages: [userDefinedStage, userDefinedStage2] })).toEqual(
|
||||
expectedData,
|
||||
);
|
||||
});
|
||||
|
||||
it('by combining user defined stage and job stages, it preserves user defined order', () => {
|
||||
const userDefinedStage = 'myStage';
|
||||
const userDefinedStageThatOverlaps = 'deploy';
|
||||
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: userDefinedStage,
|
||||
groups: [],
|
||||
},
|
||||
{
|
||||
name: job4[jobName4].stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName4,
|
||||
jobs: [{ script: job4[jobName4].script, stage: job4[jobName4].stage }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: job1[jobName1].stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
|
||||
},
|
||||
{
|
||||
name: jobName2,
|
||||
jobs: [{ script: job2[jobName2].script, stage: job2[jobName2].stage }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: job3[jobName3].stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName3,
|
||||
jobs: [{ script: job3[jobName3].script, stage: job3[jobName3].stage }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
it('generates a dict with empty needs if there are no dependencies', () => {
|
||||
const smallGraph = {
|
||||
jobs: {
|
||||
...job1,
|
||||
...job2,
|
||||
...job3,
|
||||
...job4,
|
||||
[jobName1]: { ...job1, id: createUniqueJobId(job1.stage, jobName1) },
|
||||
[jobName2]: { ...job2, id: createUniqueJobId(job2.stage, jobName2) },
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
preparePipelineGraphData({
|
||||
stages: [userDefinedStage, userDefinedStageThatOverlaps],
|
||||
...job1,
|
||||
...job2,
|
||||
...job3,
|
||||
...job4,
|
||||
}),
|
||||
).toEqual(expectedData);
|
||||
expect(generateJobNeedsDict(smallGraph)).toEqual({
|
||||
[pipelineGraphData.jobs[jobName1].id]: [],
|
||||
[pipelineGraphData.jobs[jobName2].id]: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('with only unique values', () => {
|
||||
const expectedData = {
|
||||
stages: [
|
||||
{
|
||||
name: job1[jobName1].stage,
|
||||
groups: [
|
||||
{
|
||||
name: jobName1,
|
||||
jobs: [{ script: job1[jobName1].script, stage: job1[jobName1].stage }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jobs: {
|
||||
...job1,
|
||||
},
|
||||
};
|
||||
it('generates a dict where key is the a job and its value is an array of all its needs', () => {
|
||||
const uniqueJobName1 = pipelineGraphData.jobs[jobName1].id;
|
||||
const uniqueJobName2 = pipelineGraphData.jobs[jobName2].id;
|
||||
const uniqueJobName3 = pipelineGraphData.jobs[jobName3].id;
|
||||
const uniqueJobName4 = pipelineGraphData.jobs[jobName4].id;
|
||||
|
||||
expect(
|
||||
preparePipelineGraphData({
|
||||
stages: ['build'],
|
||||
...job1,
|
||||
...job1,
|
||||
}),
|
||||
).toEqual(expectedData);
|
||||
expect(generateJobNeedsDict(pipelineGraphData)).toEqual({
|
||||
[uniqueJobName1]: [],
|
||||
[uniqueJobName2]: [],
|
||||
[uniqueJobName3]: [uniqueJobName1, uniqueJobName2],
|
||||
[uniqueJobName4]: [uniqueJobName3, uniqueJobName1, uniqueJobName2],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
|
|||
value="foo/bar/test.md"
|
||||
/>
|
||||
|
||||
<blob-content-edit-stub
|
||||
<editor-lite-stub
|
||||
editoroptions="[object Object]"
|
||||
fileglobalid="blob_local_7"
|
||||
filename="foo/bar/test.md"
|
||||
value="Lorem ipsum dolar sit amet,
|
||||
|
|
|
@ -5,7 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
|
||||
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
|
||||
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
|
||||
import EditorLite from '~/vue_shared/components/editor_lite.vue';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
|
@ -48,7 +48,7 @@ describe('Snippet Blob Edit component', () => {
|
|||
|
||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||
const findHeader = () => wrapper.find(BlobHeaderEdit);
|
||||
const findContent = () => wrapper.find(BlobContentEdit);
|
||||
const findContent = () => wrapper.find(EditorLite);
|
||||
const getLastUpdatedArgs = () => {
|
||||
const event = wrapper.emitted()['blob-updated'];
|
||||
|
||||
|
@ -172,11 +172,13 @@ describe('Snippet Blob Edit component', () => {
|
|||
expect(findContent().exists()).toBe(showContent);
|
||||
|
||||
if (showContent) {
|
||||
expect(findContent().props()).toEqual({
|
||||
value: TEST_BLOB_LOADED.content,
|
||||
fileGlobalId: TEST_BLOB_LOADED.id,
|
||||
fileName: TEST_BLOB_LOADED.path,
|
||||
});
|
||||
expect(findContent().props()).toEqual(
|
||||
expect.objectContaining({
|
||||
value: TEST_BLOB_LOADED.content,
|
||||
fileGlobalId: TEST_BLOB_LOADED.id,
|
||||
fileName: TEST_BLOB_LOADED.path,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ describe('Tracking', () => {
|
|||
respectDoNotTrack: true,
|
||||
forceSecureTracker: true,
|
||||
eventMethod: 'post',
|
||||
contexts: { webPage: true },
|
||||
contexts: { webPage: true, performanceTiming: true },
|
||||
formTracking: false,
|
||||
linkClickTracking: false,
|
||||
});
|
||||
|
|
|
@ -38,4 +38,40 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
|
|||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
|
||||
describe '#diff_stats_summary' do
|
||||
subject { GitlabSchema.execute(query, context: { current_user: current_user }).as_json }
|
||||
|
||||
let(:current_user) { create :admin }
|
||||
let(:query) do
|
||||
%(
|
||||
{
|
||||
project(fullPath: "#{project.full_path}") {
|
||||
mergeRequests {
|
||||
nodes {
|
||||
diffStatsSummary {
|
||||
additions, deletions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
|
||||
|
||||
let(:response) { subject.dig('data', 'project', 'mergeRequests', 'nodes').first['diffStatsSummary'] }
|
||||
|
||||
context 'when MR metrics has additions and deletions' do
|
||||
before do
|
||||
merge_request.metrics.update!(added_lines: 5, removed_lines: 8)
|
||||
end
|
||||
|
||||
it 'pulls out data from metrics object' do
|
||||
expect(response).to match('additions' => 5, 'deletions' => 8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,15 +30,6 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do
|
|||
|
||||
it { expect(subject).to have_details }
|
||||
it { expect(subject.details_path).to include "pipelines/#{downstream_pipeline.id}" }
|
||||
|
||||
context 'when ci_bridge_pipeline_details is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_bridge_pipeline_details: false)
|
||||
end
|
||||
|
||||
it { expect(subject).not_to have_details }
|
||||
it { expect(subject.details_path).to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to read downstream pipeline' do
|
||||
|
|
|
@ -107,11 +107,11 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
|
|||
context 'mocked specs' do
|
||||
before do
|
||||
allow(subject).to receive(:connection).and_return(connection)
|
||||
allow(subject).to receive(:disable_statement_timeout).and_yield
|
||||
allow(connection).to receive(:execute).and_call_original
|
||||
end
|
||||
|
||||
it 'replaces the existing index with an identical index' do
|
||||
expect(subject).to receive(:disable_statement_timeout).twice.and_yield
|
||||
expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').twice
|
||||
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
||||
|
@ -136,7 +136,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do
|
|||
end
|
||||
|
||||
it 'replaces the existing index with an identical index' do
|
||||
expect(subject).to receive(:disable_statement_timeout).exactly(3).times.and_yield
|
||||
expect(connection).to receive(:execute).with('SET statement_timeout TO 21600').exactly(3).times
|
||||
|
||||
expect_to_execute_concurrently_in_order(drop_index)
|
||||
expect_to_execute_concurrently_in_order(create_index)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Reindexing::Coordinator do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
describe '.perform' do
|
||||
subject { described_class.new(indexes).perform }
|
||||
|
||||
let(:indexes) { [instance_double(Gitlab::Database::PostgresIndex), instance_double(Gitlab::Database::PostgresIndex)] }
|
||||
let(:reindexers) { [instance_double(Gitlab::Database::Reindexing::ConcurrentReindex), instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)] }
|
||||
|
||||
let!(:lease) { stub_exclusive_lease(lease_key, uuid, timeout: lease_timeout) }
|
||||
let(:lease_key) { 'gitlab/database/reindexing/coordinator' }
|
||||
let(:lease_timeout) { 1.day }
|
||||
let(:uuid) { 'uuid' }
|
||||
|
||||
before do
|
||||
allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield
|
||||
|
||||
indexes.zip(reindexers).each do |index, reindexer|
|
||||
allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer)
|
||||
allow(reindexer).to receive(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
it 'performs concurrent reindexing for each index' do
|
||||
indexes.zip(reindexers).each do |index, reindexer|
|
||||
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer)
|
||||
expect(reindexer).to receive(:perform)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'keeps track of actions and creates ReindexAction records' do
|
||||
indexes.each do |index|
|
||||
expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'locking' do
|
||||
it 'acquires a lock while reindexing' do
|
||||
indexes.each do |index|
|
||||
expect(lease).to receive(:try_obtain).ordered.and_return(uuid)
|
||||
action = instance_double(Gitlab::Database::Reindexing::ConcurrentReindex)
|
||||
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).ordered.with(index).and_return(action)
|
||||
expect(action).to receive(:perform).ordered
|
||||
expect(Gitlab::ExclusiveLease).to receive(:cancel).ordered.with(lease_key, uuid)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'does does not perform reindexing actions if lease is not granted' do
|
||||
indexes.each do |index|
|
||||
expect(lease).to receive(:try_obtain).ordered.and_return(false)
|
||||
expect(Gitlab::Database::Reindexing::ConcurrentReindex).not_to receive(:new)
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,53 +3,19 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Database::Reindexing do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
describe '.perform' do
|
||||
before do
|
||||
allow(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).and_yield
|
||||
end
|
||||
subject { described_class.perform(indexes) }
|
||||
|
||||
shared_examples_for 'reindexing' do
|
||||
before do
|
||||
indexes.zip(reindexers).each do |index, reindexer|
|
||||
allow(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).and_return(reindexer)
|
||||
allow(reindexer).to receive(:perform)
|
||||
end
|
||||
end
|
||||
let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) }
|
||||
let(:indexes) { double }
|
||||
|
||||
it 'performs concurrent reindexing for each index' do
|
||||
indexes.zip(reindexers).each do |index, reindexer|
|
||||
expect(Gitlab::Database::Reindexing::ConcurrentReindex).to receive(:new).with(index).ordered.and_return(reindexer)
|
||||
expect(reindexer).to receive(:perform)
|
||||
end
|
||||
it 'delegates to Coordinator' do
|
||||
expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator)
|
||||
expect(coordinator).to receive(:perform)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'keeps track of actions and creates ReindexAction records' do
|
||||
indexes.each do |index|
|
||||
expect(Gitlab::Database::Reindexing::ReindexAction).to receive(:keep_track_of).with(index).and_yield
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple indexes' do
|
||||
subject { described_class.perform(indexes) }
|
||||
|
||||
let(:indexes) { [instance_double('Gitlab::Database::PostgresIndex'), instance_double('Gitlab::Database::PostgresIndex')] }
|
||||
let(:reindexers) { [instance_double('Gitlab::Database::Reindexing::ConcurrentReindex'), instance_double('Gitlab::Database::Reindexing::ConcurrentReindex')] }
|
||||
|
||||
it_behaves_like 'reindexing'
|
||||
end
|
||||
|
||||
context 'single index' do
|
||||
subject { described_class.perform(indexes.first) }
|
||||
|
||||
let(:indexes) { [instance_double('Gitlab::Database::PostgresIndex')] }
|
||||
let(:reindexers) { [instance_double('Gitlab::Database::Reindexing::ConcurrentReindex')] }
|
||||
|
||||
it_behaves_like 'reindexing'
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::UsageDataCounters::StaticSiteEditorCounter do
|
||||
it_behaves_like 'a redis usage counter', 'StaticSiteEditor', :views
|
||||
|
||||
it_behaves_like 'a redis usage counter with totals', :static_site_editor,
|
||||
views: 3
|
||||
end
|
|
@ -618,6 +618,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
subject { described_class.usage_counters }
|
||||
|
||||
it { is_expected.to include(:kubernetes_agent_gitops_sync) }
|
||||
it { is_expected.to include(:static_site_editor_views) }
|
||||
end
|
||||
|
||||
describe '.usage_data_counters' do
|
||||
|
|
|
@ -289,6 +289,34 @@ RSpec.describe API::ComposerPackages do
|
|||
it_behaves_like 'process Composer api request', :developer, :not_found
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid composer.json' do
|
||||
let(:headers) { basic_auth_header(user.username, personal_access_token.token) }
|
||||
let(:params) { { tag: 'v1.2.99' } }
|
||||
let(:project) { create(:project, :custom_repo, files: files, group: group) }
|
||||
|
||||
before do
|
||||
project.repository.add_tag(user, 'v1.2.99', 'master')
|
||||
end
|
||||
|
||||
context 'with a missing composer.json file' do
|
||||
let(:files) { { 'some_other_file' => '' } }
|
||||
|
||||
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
|
||||
end
|
||||
|
||||
context 'with an empty composer.json file' do
|
||||
let(:files) { { 'composer.json' => '' } }
|
||||
|
||||
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
|
||||
end
|
||||
|
||||
context 'with a malformed composer.json file' do
|
||||
let(:files) { { 'composer.json' => 'not_valid_JSON' } }
|
||||
|
||||
it_behaves_like 'process Composer api request', :developer, :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/composer/archives/*package_name?sha=:sha' do
|
||||
|
|
|
@ -45,6 +45,10 @@ RSpec.describe Jira::Requests::Projects::ListService do
|
|||
end
|
||||
|
||||
it 'returns an error response' do
|
||||
expect(Gitlab::ProjectServiceLogger).to receive(:error).with(
|
||||
hash_including(
|
||||
error: hash_including(:exception_class, :exception_message, :exception_backtrace)))
|
||||
.and_call_original
|
||||
expect(subject.error?).to be_truthy
|
||||
expect(subject.message).to eq('Jira request error: Timeout::Error')
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
|
|||
let(:json) { '{ name": "package-name"}' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(/Invalid/)
|
||||
expect { subject }.to raise_error(described_class::InvalidJson, /Invalid/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -32,7 +32,7 @@ RSpec.describe Packages::Composer::ComposerJsonService do
|
|||
let(:project) { create(:project, :repository) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(/not found/)
|
||||
expect { subject }.to raise_error(described_class::InvalidJson, /not found/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -248,7 +248,7 @@ RSpec.describe 'gitlab:db namespace rake task' do
|
|||
|
||||
it 'calls the index rebuilder with the proper arguments' do
|
||||
expect(Gitlab::Database::PostgresIndex).to receive(:by_identifier).with('public.foo_idx').and_return(index)
|
||||
expect(Gitlab::Database::Reindexing).to receive(:perform).with(index)
|
||||
expect(Gitlab::Database::Reindexing).to receive(:perform).with([index])
|
||||
|
||||
run_rake_task('gitlab:db:reindex', '[public.foo_idx]')
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue