Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-09 15:13:16 +00:00
parent 704ed7ea39
commit 427dbb30f0
79 changed files with 1660 additions and 179 deletions

View File

@ -49,15 +49,26 @@ export default {
:class="getMenuSectionClasses(sectionIndex)" :class="getMenuSectionClasses(sectionIndex)"
data-testid="menu-section" data-testid="menu-section"
> >
<top-nav-menu-item <template v-for="(menuItem, menuItemIndex) in menuItems">
v-for="(menuItem, menuItemIndex) in menuItems" <strong
:key="menuItem.id" v-if="menuItem.type == 'header'"
:menu-item="menuItem" :key="menuItem.title"
data-testid="menu-item" class="gl-px-4 gl-py-2 gl-text-gray-900 gl-display-block"
class="gl-w-full" :class="{ 'gl-pt-3!': menuItemIndex > 0 }"
:class="{ 'gl-mt-1': menuItemIndex > 0 }" data-testid="menu-header"
@click="onClick(menuItem)" >
/> {{ menuItem.title }}
</strong>
<top-nav-menu-item
v-else
:key="menuItem.id"
:menu-item="menuItem"
data-testid="menu-item"
class="gl-w-full"
:class="{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
/>
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@ -6,6 +6,7 @@ import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request'; import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
import initCheckFormState from './check_form_state'; import initCheckFormState from './check_form_state';
import initFormUpdate from './update_form';
function initTargetBranchSelector() { function initTargetBranchSelector() {
const targetBranch = document.querySelector('.js-target-branch'); const targetBranch = document.querySelector('.js-target-branch');
@ -68,5 +69,6 @@ function initTargetBranchSelector() {
} }
initMergeRequest(); initMergeRequest();
initFormUpdate();
initCheckFormState(); initCheckFormState();
initTargetBranchSelector(); initTargetBranchSelector();

View File

@ -0,0 +1,23 @@
const findForm = () => document.querySelector('.merge-request-form');
const removeHiddenCheckbox = (node) => {
const checkboxWrapper = node.closest('.form-check');
const hiddenCheckbox = checkboxWrapper.querySelector('input[type="hidden"]');
hiddenCheckbox.remove();
};
export default () => {
const updateCheckboxes = () => {
const checkboxes = document.querySelectorAll('.js-form-update');
if (!checkboxes.length) return;
checkboxes.forEach((checkbox) => {
if (checkbox.checked) {
removeHiddenCheckbox(checkbox);
}
});
};
findForm().addEventListener('submit', () => updateCheckboxes());
};

View File

@ -385,7 +385,13 @@ export default {
@loadingSuccess="enableSwitchEditingControl" @loadingSuccess="enableSwitchEditingControl"
@loadingError="enableSwitchEditingControl" @loadingError="enableSwitchEditingControl"
/> />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" /> <input
id="wiki_content"
v-model.trim="content"
type="hidden"
name="wiki[content]"
data-qa-selector="wiki_hidden_content"
/>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -10,6 +10,8 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import { TRACKING_CATEGORIES } from '../../constants';
export const i18n = { export const i18n = {
downloadArtifacts: __('Download artifacts'), downloadArtifacts: __('Download artifacts'),
@ -29,6 +31,7 @@ export default {
GlSearchBoxByType, GlSearchBoxByType,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [Tracking.mixin()],
inject: { inject: {
artifactsEndpoint: { artifactsEndpoint: {
default: '', default: '',
@ -60,6 +63,10 @@ export default {
}, },
methods: { methods: {
fetchArtifacts() { fetchArtifacts() {
// refactor tracking based on action once this dropdown supports
// actions other than artifacts
this.track('click_artifacts_dropdown', { label: TRACKING_CATEGORIES.index });
this.isLoading = true; this.isLoading = true;
// Replace the placeholder with the ID of the pipeline we are viewing // Replace the placeholder with the ID of the pipeline we are viewing
const endpoint = this.artifactsEndpoint.replace( const endpoint = this.artifactsEndpoint.replace(

View File

@ -1,7 +1,8 @@
<script> <script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '../../constants'; import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants';
import PipelineMultiActions from './pipeline_multi_actions.vue'; import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue'; import PipelinesManualActions from './pipelines_manual_actions.vue';
@ -17,6 +18,7 @@ export default {
PipelineMultiActions, PipelineMultiActions,
PipelinesManualActions, PipelinesManualActions,
}, },
mixins: [Tracking.mixin()],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
@ -52,6 +54,7 @@ export default {
}, },
methods: { methods: {
handleCancelClick() { handleCancelClick() {
this.trackClick('click_cancel_button');
eventHub.$emit('openConfirmationModal', { eventHub.$emit('openConfirmationModal', {
pipeline: this.pipeline, pipeline: this.pipeline,
endpoint: this.pipeline.cancel_path, endpoint: this.pipeline.cancel_path,
@ -59,8 +62,12 @@ export default {
}, },
handleRetryClick() { handleRetryClick() {
this.isRetrying = true; this.isRetrying = true;
this.trackClick('click_retry_button');
eventHub.$emit('retryPipeline', this.pipeline.retry_path); eventHub.$emit('retryPipeline', this.pipeline.retry_path);
}, },
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.index });
},
}, },
}; };
</script> </script>

View File

@ -1,9 +1,10 @@
<script> <script>
import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Tracking from '~/tracking';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { ICONS } from '../../constants'; import { ICONS, TRACKING_CATEGORIES } from '../../constants';
import PipelineLabels from './pipeline_labels.vue'; import PipelineLabels from './pipeline_labels.vue';
export default { export default {
@ -17,6 +18,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [Tracking.mixin()],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
@ -114,6 +116,11 @@ export default {
return this.pipeline?.commit?.title; return this.pipeline?.commit?.title;
}, },
}, },
methods: {
trackClick(action) {
this.track(action, { label: TRACKING_CATEGORIES.index });
},
},
}; };
</script> </script>
<template> <template>
@ -125,6 +132,7 @@ export default {
:href="commitUrl" :href="commitUrl"
class="commit-row-message gl-text-gray-900" class="commit-row-message gl-text-gray-900"
data-testid="commit-title" data-testid="commit-title"
@click="trackClick('click_commit_title')"
>{{ commitTitle }}</gl-link >{{ commitTitle }}</gl-link
> >
</tooltip-on-truncate> </tooltip-on-truncate>
@ -137,6 +145,7 @@ export default {
class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3" class="gl-text-decoration-underline gl-text-blue-600! gl-mr-3"
data-testid="pipeline-url-link" data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link" data-qa-selector="pipeline_url_link"
@click="trackClick('click_pipeline_id')"
>#{{ pipeline[pipelineKey] }}</gl-link >#{{ pipeline[pipelineKey] }}</gl-link
> >
<!--Commit row--> <!--Commit row-->
@ -154,11 +163,17 @@ export default {
:href="mergeRequestRef.path" :href="mergeRequestRef.path"
class="ref-name gl-mr-3" class="ref-name gl-mr-3"
data-testid="merge-request-ref" data-testid="merge-request-ref"
@click="trackClick('click_mr_ref')"
>{{ mergeRequestRef.iid }}</gl-link >{{ mergeRequestRef.iid }}</gl-link
> >
<gl-link v-else :href="refUrl" class="ref-name gl-mr-3" data-testid="commit-ref-name">{{ <gl-link
commitRef.name v-else
}}</gl-link> :href="refUrl"
class="ref-name gl-mr-3"
data-testid="commit-ref-name"
@click="trackClick('click_commit_name')"
>{{ commitRef.name }}</gl-link
>
</tooltip-on-truncate> </tooltip-on-truncate>
<gl-icon <gl-icon
v-gl-tooltip v-gl-tooltip
@ -167,9 +182,13 @@ export default {
:title="__('Commit')" :title="__('Commit')"
data-testid="commit-icon" data-testid="commit-icon"
/> />
<gl-link :href="commitUrl" class="commit-sha mr-0" data-testid="commit-short-sha">{{ <gl-link
commitShortSha :href="commitUrl"
}}</gl-link> class="commit-sha mr-0"
data-testid="commit-short-sha"
@click="trackClick('click_commit_sha')"
>{{ commitShortSha }}</gl-link
>
<user-avatar-link <user-avatar-link
v-if="commitAuthor" v-if="commitAuthor"
:link-href="commitAuthor.path" :link-href="commitAuthor.path"

View File

@ -2,7 +2,9 @@
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
import { map } from 'lodash'; import { map } from 'lodash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '../../constants';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue'; import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineSourceToken from './tokens/pipeline_source_token.vue'; import PipelineSourceToken from './tokens/pipeline_source_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue'; import PipelineStatusToken from './tokens/pipeline_status_token.vue';
@ -19,6 +21,7 @@ export default {
components: { components: {
GlFilteredSearch, GlFilteredSearch,
}, },
mixins: [Tracking.mixin()],
props: { props: {
projectId: { projectId: {
type: String, type: String,
@ -110,6 +113,7 @@ export default {
}, },
methods: { methods: {
onSubmit(filters) { onSubmit(filters) {
this.track('click_filtered_search', { label: TRACKING_CATEGORIES.index });
this.$emit('filterPipelines', filters); this.$emit('filterPipelines', filters);
}, },
}, },

View File

@ -4,8 +4,10 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
export default { export default {
directives: { directives: {
@ -17,6 +19,7 @@ export default {
GlDropdownItem, GlDropdownItem,
GlIcon, GlIcon,
}, },
mixins: [Tracking.mixin()],
props: { props: {
actions: { actions: {
type: Array, type: Array,
@ -66,7 +69,6 @@ export default {
createFlash({ message: __('An error occurred while making the request.') }); createFlash({ message: __('An error occurred while making the request.') });
}); });
}, },
isActionDisabled(action) { isActionDisabled(action) {
if (action.playable === undefined) { if (action.playable === undefined) {
return false; return false;
@ -74,6 +76,9 @@ export default {
return !action.playable; return !action.playable;
}, },
trackClick() {
this.track('click_manual_actions', { label: TRACKING_CATEGORIES.index });
},
}, },
}; };
</script> </script>
@ -86,6 +91,7 @@ export default {
right right
lazy lazy
icon="play" icon="play"
@shown="trackClick"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="action in actions" v-for="action in actions"

View File

@ -1,6 +1,7 @@
<script> <script>
import { CHILD_VIEW } from '~/pipelines/constants'; import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue'; import PipelinesTimeago from './time_ago.vue';
export default { export default {
@ -8,6 +9,7 @@ export default {
CiBadge, CiBadge,
PipelinesTimeago, PipelinesTimeago,
}, },
mixins: [Tracking.mixin()],
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
@ -26,6 +28,11 @@ export default {
return this.viewType === CHILD_VIEW; return this.viewType === CHILD_VIEW;
}, },
}, },
methods: {
trackClick() {
this.track('click_ci_status_badge', { label: TRACKING_CATEGORIES.index });
},
},
}; };
</script> </script>
@ -37,6 +44,7 @@ export default {
:show-text="!isChildView" :show-text="!isChildView"
:icon-classes="'gl-vertical-align-middle!'" :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status" data-qa-selector="pipeline_commit_status"
@ciStatusBadgeClick="trackClick"
/> />
<pipelines-timeago class="gl-mt-3" :pipeline="pipeline" /> <pipelines-timeago class="gl-mt-3" :pipeline="pipeline" />
</div> </div>

View File

@ -109,3 +109,7 @@ export const DEFAULT_FIELDS = [
columnClass: 'gl-w-20p', columnClass: 'gl-w-20p',
}, },
]; ];
export const TRACKING_CATEGORIES = {
index: 'pipelines_table_component',
};

View File

@ -1,5 +1,6 @@
<script> <script>
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import CiIcon from './ci_icon.vue'; import CiIcon from './ci_icon.vue';
/** /**
* Renders CI Badge link with CI icon and status text based on * Renders CI Badge link with CI icon and status text based on
@ -60,15 +61,24 @@ export default {
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
methods: {
navigateToPipeline() {
visitUrl(this.detailsPath);
// event used for tracking
this.$emit('ciStatusBadgeClick');
},
},
}; };
</script> </script>
<template> <template>
<a <a
v-gl-tooltip v-gl-tooltip
:href="detailsPath"
:class="cssClass" :class="cssClass"
class="gl-cursor-pointer"
:title="title" :title="title"
data-qa-selector="status_badge_link" data-qa-selector="status_badge_link"
@click="navigateToPipeline"
> >
<ci-icon :status="status" :css-classes="iconClasses" /> <ci-icon :status="status" :css-classes="iconClasses" />

View File

@ -11,6 +11,9 @@ html {
font-family: sans-serif; font-family: sans-serif;
line-height: 1.15; line-height: 1.15;
} }
header {
display: block;
}
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
@ -28,7 +31,8 @@ hr {
height: 0; height: 0;
overflow: visible; overflow: visible;
} }
h1 { h1,
h3 {
margin-top: 0; margin-top: 0;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@ -49,26 +53,49 @@ img {
vertical-align: middle; vertical-align: middle;
border-style: none; border-style: none;
} }
svg {
overflow: hidden;
vertical-align: middle;
}
label { label {
display: inline-block; display: inline-block;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
input { button {
border-radius: 0;
}
input,
button {
margin: 0; margin: 0;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
} }
button,
input { input {
overflow: visible; overflow: visible;
} }
button {
text-transform: none;
}
[role="button"] {
cursor: pointer;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="submit"]:not(:disabled) { [type="submit"]:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner { [type="submit"]::-moz-focus-inner {
padding: 0; padding: 0;
border-style: none; border-style: none;
} }
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
fieldset { fieldset {
min-width: 0; min-width: 0;
padding: 0; padding: 0;
@ -78,7 +105,8 @@ fieldset {
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
h1 { h1,
h3 {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
font-weight: 600; font-weight: 600;
line-height: 1.2; line-height: 1.2;
@ -87,6 +115,9 @@ h1 {
h1 { h1 {
font-size: 2.1875rem; font-size: 2.1875rem;
} }
h3 {
font-size: 1.53125rem;
}
hr { hr {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@ -120,23 +151,42 @@ hr {
max-width: 1140px; max-width: 1140px;
} }
} }
.col-sm-12, .row {
.col { display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-md-6,
.col-sm-12 {
position: relative; position: relative;
width: 100%; width: 100%;
padding-right: 15px; padding-right: 15px;
padding-left: 15px; padding-left: 15px;
} }
.col { .order-1 {
flex-basis: 0; order: 1;
flex-grow: 1; }
max-width: 100%; .order-12 {
order: 12;
} }
@media (min-width: 576px) { @media (min-width: 576px) {
.col-sm-12 { .col-sm-12 {
flex: 0 0 100%; flex: 0 0 100%;
max-width: 100%; max-width: 100%;
} }
.order-sm-1 {
order: 1;
}
.order-sm-12 {
order: 12;
}
}
@media (min-width: 768px) {
.col-md-6 {
flex: 0 0 50%;
max-width: 50%;
}
} }
.form-control { .form-control {
display: block; display: block;
@ -169,16 +219,6 @@ hr {
.form-group { .form-group {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.form-row {
display: flex;
flex-wrap: wrap;
margin-right: -5px;
margin-left: -5px;
}
.form-row > .col {
padding-right: 5px;
padding-left: 5px;
}
.btn { .btn {
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;
@ -204,6 +244,137 @@ hr {
fieldset:disabled a.btn { fieldset:disabled a.btn {
pointer-events: none; pointer-events: none;
} }
.btn-block {
display: block;
width: 100%;
}
.btn-block + .btn-block {
margin-top: 0.5rem;
}
input.btn-block[type="submit"],
input.btn-block[type="button"] {
width: 100%;
}
.custom-control {
position: relative;
z-index: 1;
display: block;
min-height: 1.5rem;
padding-left: 1.5rem;
color-adjust: exact;
}
.custom-control-input {
position: absolute;
left: 0;
z-index: -1;
width: 1rem;
height: 1.25rem;
opacity: 0;
}
.custom-control-input:checked ~ .custom-control-label::before {
color: #fff;
border-color: #007bff;
background-color: #007bff;
}
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
color: #fff;
background-color: #b3d7ff;
border-color: #b3d7ff;
}
.custom-control-input:disabled ~ .custom-control-label {
color: #5e5e5e;
}
.custom-control-input:disabled ~ .custom-control-label::before {
background-color: #fafafa;
}
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-label::before {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #666 solid 1px;
}
.custom-control-label::after {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50% / 50% 50%;
}
.custom-checkbox .custom-control-label::before {
border-radius: 0.25rem;
}
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
}
.custom-checkbox
.custom-control-input:indeterminate
~ .custom-control-label::before {
border-color: #007bff;
background-color: #007bff;
}
.custom-checkbox
.custom-control-input:indeterminate
~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e");
}
.custom-checkbox
.custom-control-input:disabled:checked
~ .custom-control-label::before {
background-color: rgba(0, 123, 255, 0.5);
}
.custom-checkbox
.custom-control-input:disabled:indeterminate
~ .custom-control-label::before {
background-color: rgba(0, 123, 255, 0.5);
}
@media (prefers-reduced-motion: reduce) {
}
.tab-content > .tab-pane {
display: none;
}
.tab-content > .active {
display: block;
}
.navbar {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0.5rem;
}
.navbar .container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
.clearfix::after {
display: block;
clear: both;
content: "";
}
.fixed-top {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1030;
}
.mt-3 { .mt-3 {
margin-top: 1rem !important; margin-top: 1rem !important;
} }
@ -213,8 +384,8 @@ fieldset:disabled a.btn {
.text-nowrap { .text-nowrap {
white-space: nowrap !important; white-space: nowrap !important;
} }
.text-center { .font-weight-normal {
text-align: center !important; font-weight: 400 !important;
} }
.gl-form-input, .gl-form-input,
.gl-form-input.form-control { .gl-form-input.form-control {
@ -251,13 +422,103 @@ fieldset:disabled a.btn {
.gl-form-input.form-control::placeholder { .gl-form-input.form-control::placeholder {
color: #868686; color: #868686;
} }
.gl-form-checkbox {
font-size: 0.875rem;
line-height: 1rem;
color: #303030;
}
.gl-form-checkbox .custom-control-input:disabled,
.gl-form-checkbox .custom-control-input:disabled ~ .custom-control-label {
cursor: not-allowed;
color: #868686;
}
.gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label {
cursor: pointer;
}
.gl-form-checkbox.custom-control
.custom-control-input
~ .custom-control-label::before,
.gl-form-checkbox.custom-control
.custom-control-input
~ .custom-control-label::after {
top: 0;
}
.gl-form-checkbox.custom-control
.custom-control-input
~ .custom-control-label::before {
background-color: #fff;
border-color: #868686;
}
.gl-form-checkbox.custom-control
.custom-control-input:checked
~ .custom-control-label::before {
background-color: #1f75cb;
border-color: #1f75cb;
}
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:checked
~ .custom-control-label::after,
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:indeterminate
~ .custom-control-label::after {
background: none;
background-color: #fff;
mask-repeat: no-repeat;
mask-position: center center;
}
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:checked
~ .custom-control-label::after {
mask-image: url('data:image/svg+xml,%3Csvg width="8" height="7" viewBox="0 0 8 7" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 3.05299L2.99123 5L7 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
}
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:indeterminate
~ .custom-control-label::after {
mask-image: url('data:image/svg+xml,%3Csvg width="8" height="2" viewBox="0 0 8 2" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M0 1L8 1" stroke="white" stroke-width="2"/%3E%3C/svg%3E%0A');
}
.gl-form-checkbox.custom-control.custom-checkbox
.custom-control-input:indeterminate
~ .custom-control-label::before {
background-color: #1f75cb;
border-color: #1f75cb;
}
.gl-form-checkbox.custom-control
.custom-control-input:disabled
~ .custom-control-label {
cursor: not-allowed;
}
.gl-form-checkbox.custom-control
.custom-control-input:disabled
~ .custom-control-label::before {
background-color: #f0f0f0;
border-color: #dbdbdb;
pointer-events: auto;
}
.gl-form-checkbox.custom-control
.custom-control-input:checked:disabled
~ .custom-control-label::before,
.gl-form-checkbox.custom-control
.custom-control-input:indeterminate:disabled
~ .custom-control-label::before {
background-color: #dbdbdb;
border-color: #dbdbdb;
}
.gl-form-checkbox.custom-control
.custom-control-input:checked:disabled
~ .custom-control-label::after,
.gl-form-checkbox.custom-control
.custom-control-input:indeterminate:disabled
~ .custom-control-label::after {
background-color: #5e5e5e;
}
.gl-button { .gl-button {
display: inline-flex; display: inline-flex;
} }
.gl-button:not(.btn-link):active { .gl-button:not(.btn-link):active {
text-decoration: none; text-decoration: none;
} }
.gl-button.gl-button { .gl-button.gl-button,
.gl-button.gl-button.btn-block {
border-width: 0; border-width: 0;
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -273,7 +534,8 @@ fieldset:disabled a.btn {
font-size: 0.875rem; font-size: 0.875rem;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.gl-button.gl-button .gl-button-text { .gl-button.gl-button .gl-button-text,
.gl-button.gl-button.btn-block .gl-button-text {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@ -282,29 +544,39 @@ fieldset:disabled a.btn {
margin-top: -1px; margin-top: -1px;
margin-bottom: -1px; margin-bottom: -1px;
} }
.gl-button.gl-button .gl-button-icon { .gl-button.gl-button .gl-button-icon,
.gl-button.gl-button.btn-block .gl-button-icon {
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
flex-shrink: 0; flex-shrink: 0;
margin-right: 0.25rem; margin-right: 0.25rem;
top: auto; top: auto;
} }
.gl-button.gl-button.btn-default { .gl-button.gl-button.btn-default,
.gl-button.gl-button.btn-block.btn-default {
background-color: #fff; background-color: #fff;
} }
.gl-button.gl-button.btn-default:active { .gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active,
.gl-button.gl-button.btn-block.btn-default:active,
.gl-button.gl-button.btn-block.btn-default.active {
box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc; box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none; outline: none;
background-color: #dbdbdb; background-color: #dbdbdb;
} }
.gl-button.gl-button.btn-confirm { .gl-button.gl-button.btn-confirm,
.gl-button.gl-button.btn-block.btn-confirm {
color: #fff; color: #fff;
} }
.gl-button.gl-button.btn-confirm { .gl-button.gl-button.btn-confirm,
.gl-button.gl-button.btn-block.btn-confirm {
background-color: #1f75cb; background-color: #1f75cb;
box-shadow: inset 0 0 0 1px #1068bf; box-shadow: inset 0 0 0 1px #1068bf;
} }
.gl-button.gl-button.btn-confirm:active { .gl-button.gl-button.btn-confirm:active,
.gl-button.gl-button.btn-confirm.active,
.gl-button.gl-button.btn-block.btn-confirm:active,
.gl-button.gl-button.btn-block.btn-confirm.active {
box-shadow: inset 0 0 0 1px #033464, 0 0 0 1px #fff, 0 0 0 3px #428fdc; box-shadow: inset 0 0 0 1px #033464, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none; outline: none;
background-color: #0b5cad; background-color: #0b5cad;
@ -312,10 +584,14 @@ fieldset:disabled a.btn {
body { body {
font-size: 0.875rem; font-size: 0.875rem;
} }
[type="submit"] { button,
html [type="button"],
[type="submit"],
[role="button"] {
cursor: pointer; cursor: pointer;
} }
h1 { h1,
h3 {
margin-top: 20px; margin-top: 20px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -325,6 +601,9 @@ a {
hr { hr {
overflow: hidden; overflow: hidden;
} }
svg {
vertical-align: baseline;
}
.form-control { .form-control {
font-size: 0.875rem; font-size: 0.875rem;
} }
@ -332,9 +611,6 @@ hr {
display: none !important; display: none !important;
visibility: hidden !important; visibility: hidden !important;
} }
.hide {
display: none;
}
html { html {
overflow-y: scroll; overflow-y: scroll;
} }
@ -372,13 +648,34 @@ body.navless {
background-color: #f0f0f0; background-color: #f0f0f0;
box-shadow: none; box-shadow: none;
} }
.btn:active { .btn:active,
.btn.active {
background-color: #eaeaea; background-color: #eaeaea;
border-color: #e3e3e3; border-color: #e3e3e3;
color: #303030; color: #303030;
} }
.light { .btn svg {
color: #303030; height: 15px;
width: 15px;
}
.btn svg:not(:last-child) {
margin-right: 5px;
}
.btn-block {
width: 100%;
margin: 0;
margin-bottom: 1rem;
}
.btn-block.btn {
padding: 6px 0;
}
.tab-content {
overflow: visible;
}
@media (max-width: 767.98px) {
.tab-content {
isolation: isolate;
}
} }
hr { hr {
margin: 1.5rem 0; margin: 1.5rem 0;
@ -416,6 +713,9 @@ input {
label { label {
font-weight: 600; font-weight: 600;
} }
label.custom-control-label {
font-weight: 400;
}
label.label-bold { label.label-bold {
font-weight: 600; font-weight: 600;
} }
@ -429,8 +729,25 @@ label.label-bold {
.gl-show-field-errors .form-control:not(textarea) { .gl-show-field-errors .form-control:not(textarea) {
height: 34px; height: 34px;
} }
.gl-show-field-errors .gl-field-hint { .navbar-empty {
color: #303030; justify-content: center;
height: var(--header-height, 48px);
background: #fff;
border-bottom: 1px solid #dbdbdb;
}
.navbar-empty .tanuki-logo,
.navbar-empty .brand-header-logo {
max-height: 100%;
}
.tanuki-logo .tanuki {
fill: #e24329;
}
.tanuki-logo .left-cheek,
.tanuki-logo .right-cheek {
fill: #fc6d26;
}
.tanuki-logo .chin {
fill: #fca326;
} }
input::-moz-placeholder { input::-moz-placeholder {
color: #868686; color: #868686;
@ -442,6 +759,9 @@ input::-ms-input-placeholder {
input:-ms-input-placeholder { input:-ms-input-placeholder {
color: #868686; color: #868686;
} }
svg {
fill: currentColor;
}
.login-page .container { .login-page .container {
max-width: 960px; max-width: 960px;
} }
@ -649,14 +969,17 @@ input:-ms-input-placeholder {
} }
} }
.gl-text-green-600 { .gl-display-flex {
color: #217645; display: flex;
} }
.gl-text-red-500 { .gl-display-inline-block {
color: #dd2b0e; display: inline-block;
} }
.gl-display-block { .gl-flex-wrap {
display: block; flex-wrap: wrap;
}
.gl-float-right {
float: right;
} }
.gl-w-10 { .gl-w-10 {
width: 3.5rem; width: 3.5rem;
@ -675,14 +998,18 @@ input:-ms-input-placeholder {
width: 100%; width: 100%;
} }
} }
.gl-p-4 { .gl-p-5 {
padding: 0.75rem; padding: 1rem;
}
.gl-px-5 {
padding-left: 1rem;
padding-right: 1rem;
} }
.gl-pt-5 { .gl-pt-5 {
padding-top: 1rem; padding-top: 1rem;
} }
.gl-mt-2 { .gl-mt-3 {
margin-top: 0.25rem; margin-top: 0.5rem;
} }
.gl-mt-5 { .gl-mt-5 {
margin-top: 1rem; margin-top: 1rem;
@ -702,15 +1029,17 @@ input:-ms-input-placeholder {
.gl-mb-3 { .gl-mb-3 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.gl-mb-5 {
margin-bottom: 1rem;
}
.gl-ml-auto { .gl-ml-auto {
margin-left: auto; margin-left: auto;
} }
.gl-ml-2 { .gl-ml-2 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;
}
}
.gl-text-center { .gl-text-center {
text-align: center; text-align: center;
} }
@ -720,6 +1049,9 @@ input:-ms-input-placeholder {
.gl-font-weight-normal { .gl-font-weight-normal {
font-weight: 400; font-weight: 400;
} }
.gl-font-weight-bold {
font-weight: 600;
}
@import "startup/cloaking"; @import "startup/cloaking";
@include cloak-startup-scss(none); @include cloak-startup-scss(none);

View File

@ -6,6 +6,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
# See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details. # See https://gitlab.com/gitlab-org/gitlab/-/issues/226002 for more details.
include MetricsDashboard include MetricsDashboard
include ProductAnalyticsTracking
layout 'project' layout 'project'
@ -26,6 +27,18 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? } before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
after_action :expire_etag_cache, only: [:cancel_auto_stop] after_action :expire_etag_cache, only: [:cancel_auto_stop]
track_event :index,
:folder,
:show,
:new,
:edit,
:create,
:update,
:stop,
:cancel_auto_stop,
:terminal,
name: 'users_visiting_environments_pages'
feature_category :continuous_delivery feature_category :continuous_delivery
urgency :low urgency :low

View File

@ -48,6 +48,13 @@ module Nav
private private
def top_nav_localized_headers
{
explore: s_('TopNav|Explore'),
switch_to: s_('TopNav|Switch to')
}.freeze
end
def build_base_view_model(builder:, project:, group:) def build_base_view_model(builder:, project:, group:)
if current_user if current_user
build_view_model(builder: builder, project: project, group: group) build_view_model(builder: builder, project: project, group: group)
@ -60,6 +67,7 @@ module Nav
# These come from `app/views/layouts/nav/_explore.html.ham` # These come from `app/views/layouts/nav/_explore.html.ham`
if explore_nav_link?(:projects) if explore_nav_link?(:projects)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
href: explore_root_path, href: explore_root_path,
active: nav == 'project' || active_nav_link?(path: %w[dashboard#show root#show projects#trending projects#starred projects#index]), active: nav == 'project' || active_nav_link?(path: %w[dashboard#show root#show projects#trending projects#starred projects#index]),
**projects_menu_item_attrs **projects_menu_item_attrs
@ -68,6 +76,7 @@ module Nav
if explore_nav_link?(:groups) if explore_nav_link?(:groups)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
href: explore_groups_path, href: explore_groups_path,
active: nav == 'group' || active_nav_link?(controller: [:groups, 'groups/milestones', 'groups/group_members']), active: nav == 'group' || active_nav_link?(controller: [:groups, 'groups/milestones', 'groups/group_members']),
**groups_menu_item_attrs **groups_menu_item_attrs
@ -76,6 +85,7 @@ module Nav
if explore_nav_link?(:snippets) if explore_nav_link?(:snippets)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
active: active_nav_link?(controller: :snippets), active: active_nav_link?(controller: :snippets),
href: explore_snippets_path, href: explore_snippets_path,
**snippets_menu_item_attrs **snippets_menu_item_attrs
@ -89,6 +99,7 @@ module Nav
current_item = project ? current_project(project: project) : {} current_item = project ? current_project(project: project) : {}
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'project' || active_nav_link?(path: %w[root#index projects#trending projects#starred dashboard/projects#index]), active: nav == 'project' || active_nav_link?(path: %w[root#index projects#trending projects#starred dashboard/projects#index]),
css_class: 'qa-projects-dropdown', css_class: 'qa-projects-dropdown',
data: { track_label: "projects_dropdown", track_action: "click_dropdown" }, data: { track_label: "projects_dropdown", track_action: "click_dropdown" },
@ -103,6 +114,7 @@ module Nav
current_item = group ? current_group(group: group) : {} current_item = group ? current_group(group: group) : {}
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'group' || active_nav_link?(path: %w[dashboard/groups explore/groups]), active: nav == 'group' || active_nav_link?(path: %w[dashboard/groups explore/groups]),
css_class: 'qa-groups-dropdown', css_class: 'qa-groups-dropdown',
data: { track_label: "groups_dropdown", track_action: "click_dropdown" }, data: { track_label: "groups_dropdown", track_action: "click_dropdown" },
@ -116,6 +128,7 @@ module Nav
if dashboard_nav_link?(:milestones) if dashboard_nav_link?(:milestones)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
id: 'milestones', id: 'milestones',
header: top_nav_localized_headers[:explore],
title: _('Milestones'), title: _('Milestones'),
href: dashboard_milestones_path, href: dashboard_milestones_path,
active: active_nav_link?(controller: 'dashboard/milestones'), active: active_nav_link?(controller: 'dashboard/milestones'),
@ -127,6 +140,7 @@ module Nav
if dashboard_nav_link?(:snippets) if dashboard_nav_link?(:snippets)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
active: active_nav_link?(controller: 'dashboard/snippets'), active: active_nav_link?(controller: 'dashboard/snippets'),
data: { qa_selector: 'snippets_link', **menu_data_tracking_attrs('snippets') }, data: { qa_selector: 'snippets_link', **menu_data_tracking_attrs('snippets') },
href: dashboard_snippets_path, href: dashboard_snippets_path,
@ -137,6 +151,7 @@ module Nav
if dashboard_nav_link?(:activity) if dashboard_nav_link?(:activity)
builder.add_primary_menu_item_with_shortcut( builder.add_primary_menu_item_with_shortcut(
id: 'activity', id: 'activity',
header: top_nav_localized_headers[:explore],
title: _('Activity'), title: _('Activity'),
href: activity_dashboard_path, href: activity_dashboard_path,
active: active_nav_link?(path: 'dashboard#activity'), active: active_nav_link?(path: 'dashboard#activity'),

View File

@ -10,11 +10,7 @@ class ApplicationSetting < ApplicationRecord
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22' ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22' ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18' ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22'
ignore_column :enforce_ssh_key_expiration, remove_with: '15.2', remove_after: '2022-07-22'
ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22'
INSTANCE_REVIEW_MIN_USERS = 50 INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \

View File

@ -1073,7 +1073,7 @@ module Ci
latest_test_report_builds.failed.limit(limit) latest_test_report_builds.failed.limit(limit)
end end
def has_reports?(reports_scope) def complete_and_has_reports?(reports_scope)
if Feature.enabled?(:mr_show_reports_immediately, project, type: :development) if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
latest_report_builds(reports_scope).exists? latest_report_builds(reports_scope).exists?
else else
@ -1090,7 +1090,7 @@ module Ci
end end
def can_generate_codequality_reports? def can_generate_codequality_reports?
has_reports?(Ci::JobArtifact.of_report_type(:codequality)) complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end end
def test_report_summary def test_report_summary
@ -1313,7 +1313,7 @@ module Ci
def has_test_reports? def has_test_reports?
strong_memoize(:has_test_reports) do strong_memoize(:has_test_reports) do
has_reports?(::Ci::JobArtifact.of_report_type(:test)) complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:test))
end end
end end

View File

@ -8,15 +8,12 @@ module Ci
include ChronicDurationAttribute include ChronicDurationAttribute
include FromUnion include FromUnion
include TokenAuthenticatable include TokenAuthenticatable
include IgnorableColumns
include FeatureGate include FeatureGate
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include TaggableQueries include TaggableQueries
include Presentable include Presentable
include EachBatch include EachBatch
ignore_column :semver, remove_with: '15.4', remove_after: '2022-08-22'
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced? add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
enum access_level: { enum access_level: {

View File

@ -16,14 +16,10 @@ module Clusters
include ::Clusters::Concerns::ApplicationData include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue include AfterCommitQueue
include UsageStatistics include UsageStatistics
include IgnorableColumns
default_value_for :ingress_type, :nginx default_value_for :ingress_type, :nginx
default_value_for :version, VERSION default_value_for :version, VERSION
ignore_column :modsecurity_enabled, remove_with: '14.2', remove_after: '2021-07-22'
ignore_column :modsecurity_mode, remove_with: '14.2', remove_after: '2021-07-22'
enum ingress_type: { enum ingress_type: {
nginx: 1 nginx: 1
} }

View File

@ -34,7 +34,7 @@ module Ci
end end
def ensure_metadata def ensure_metadata
metadata || build_metadata(project: project) metadata || build_metadata(project: project, partition_id: partition_id)
end end
def degenerated? def degenerated?

View File

@ -1566,7 +1566,7 @@ class MergeRequest < ApplicationRecord
end end
def has_test_reports? def has_test_reports?
actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:test)) actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test))
end end
def predefined_variables def predefined_variables
@ -1596,7 +1596,7 @@ class MergeRequest < ApplicationRecord
end end
def has_accessibility_reports? def has_accessibility_reports?
actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.of_report_type(:accessibility)) actual_head_pipeline.present? && actual_head_pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:accessibility))
end end
def has_coverage_reports? def has_coverage_reports?
@ -1604,7 +1604,7 @@ class MergeRequest < ApplicationRecord
end end
def has_terraform_reports? def has_terraform_reports?
actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:terraform)) actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:terraform))
end end
def compare_accessibility_reports def compare_accessibility_reports
@ -1644,7 +1644,7 @@ class MergeRequest < ApplicationRecord
end end
def has_codequality_reports? def has_codequality_reports?
actual_head_pipeline&.has_reports?(Ci::JobArtifact.of_report_type(:codequality)) actual_head_pipeline&.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end end
def compare_codequality_reports def compare_codequality_reports
@ -1694,11 +1694,11 @@ class MergeRequest < ApplicationRecord
end end
def has_sast_reports? def has_sast_reports?
!!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.of_report_type(:sast)) !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:sast))
end end
def has_secret_detection_reports? def has_secret_detection_reports?
!!actual_head_pipeline&.has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection)) !!actual_head_pipeline&.complete_and_has_reports?(::Ci::JobArtifact.of_report_type(:secret_detection))
end end
def compare_sast_reports(current_user) def compare_sast_reports(current_user)

View File

@ -51,8 +51,6 @@ class Project < ApplicationRecord
BoardLimitExceeded = Class.new(StandardError) BoardLimitExceeded = Class.new(StandardError)
ExportLimitExceeded = Class.new(StandardError) ExportLimitExceeded = Class.new(StandardError)
ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2021-09-22', remove_with: '14.4'
ignore_columns :pull_mirror_branch_prefix, remove_after: '2021-09-22', remove_with: '14.4'
ignore_columns :build_coverage_regex, remove_after: '2022-10-22', remove_with: '15.5' ignore_columns :build_coverage_regex, remove_after: '2022-10-22', remove_with: '15.5'
STATISTICS_ATTRIBUTE = 'repositories_count' STATISTICS_ATTRIBUTE = 'repositories_count'

View File

@ -23,6 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs, Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
Gitlab::Ci::Pipeline::Chain::SeedBlock, Gitlab::Ci::Pipeline::Chain::SeedBlock,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules, Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::AssignPartition,
Gitlab::Ci::Pipeline::Chain::Seed, Gitlab::Ci::Pipeline::Chain::Seed,
Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Limit::Deployments, Gitlab::Ci::Pipeline::Chain::Limit::Deployments,

View File

@ -10,7 +10,7 @@ module Ci
def execute(pipeline) def execute(pipeline)
REPORT_TRACKED.each do |report| REPORT_TRACKED.each do |report|
if pipeline.has_reports?(Ci::JobArtifact.of_report_type(report)) if pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(report))
track_usage_event(event_name(report), [pipeline.id, pipeline.user_id].join(VALUES_DELIMITER)) track_usage_event(event_name(report), [pipeline.id, pipeline.user_id].join(VALUES_DELIMITER))
end end
end end

View File

@ -39,11 +39,13 @@ module Ci
job.pipeline = pipeline job.pipeline = pipeline
job.project = pipeline.project job.project = pipeline.project
job.ref = pipeline.ref job.ref = pipeline.ref
job.partition_id = pipeline.partition_id
# update metadata since it might have been lazily initialised before this call # update metadata since it might have been lazily initialised before this call
# metadata is present on `Ci::Processable` # metadata is present on `Ci::Processable`
if job.respond_to?(:metadata) && job.metadata if job.respond_to?(:metadata) && job.metadata
job.metadata.project = pipeline.project job.metadata.project = pipeline.project
job.metadata.partition_id = pipeline.partition_id
end end
end end
end end

View File

@ -5,20 +5,20 @@ module Issues
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
include GitlabRoutingHelper include GitlabRoutingHelper
def initialize(issuables_relation, project) def initialize(issuables_relation, project, user = nil)
super super(issuables_relation, project)
@labels = @issuables.labels_hash.transform_values { |labels| labels.sort.join(',').presence } @labels = @issuables.labels_hash.transform_values { |labels| labels.sort.join(',').presence }
end end
def email(user) def email(mail_to_user)
Notify.issues_csv_email(user, project, csv_data, csv_builder.status).deliver_now Notify.issues_csv_email(mail_to_user, project, csv_data, csv_builder.status).deliver_now
end end
private private
def associations_to_preload def associations_to_preload
%i(author assignees timelogs milestone project) [:author, :assignees, :timelogs, :milestone, { project: { namespace: :route } }]
end end
def header_to_value_hash def header_to_value_hash

View File

@ -42,7 +42,7 @@
#js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json), #js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json), suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(@project, @pipeline.sha), blob_path: project_blob_path(@project, @pipeline.sha),
has_test_report: @pipeline.has_reports?(Ci::JobArtifact.of_report_type(:test)).to_s, has_test_report: @pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)).to_s,
empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'), empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
artifacts_expired_image_path: image_path('illustrations/pipeline.svg') } } artifacts_expired_image_path: image_path('illustrations/pipeline.svg') } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project

View File

@ -11,7 +11,7 @@
- if issuable.can_remove_source_branch?(current_user) - if issuable.can_remove_source_branch?(current_user)
.form-check.gl-mb-3 .form-check.gl-mb-3
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input' = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input js-form-update'
= label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do = label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do
= _("Delete source branch when merge request is accepted.") = _("Delete source branch when merge request is accepted.")
- if !project.squash_never? - if !project.squash_never?
@ -21,7 +21,7 @@
= check_box_tag 'merge_request[squash]', '1', project.squash_enabled_by_default?, class: 'form-check-input', disabled: 'true' = check_box_tag 'merge_request[squash]', '1', project.squash_enabled_by_default?, class: 'form-check-input', disabled: 'true'
- else - else
= hidden_field_tag 'merge_request[squash]', '0', id: nil = hidden_field_tag 'merge_request[squash]', '0', id: nil
= check_box_tag 'merge_request[squash]', '1', issuable_squash_option?(issuable, project), class: 'form-check-input' = check_box_tag 'merge_request[squash]', '1', issuable_squash_option?(issuable, project), class: 'form-check-input js-form-update'
= label_tag 'merge_request[squash]', class: 'form-check-label' do = label_tag 'merge_request[squash]', class: 'form-check-label' do
= _("Squash commits when merge request is accepted.") = _("Squash commits when merge request is accepted.")
= link_to sprite_icon('question-o'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank', rel: 'noopener noreferrer' = link_to sprite_icon('question-o'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank', rel: 'noopener noreferrer'

View File

@ -127,3 +127,4 @@ end
Sidekiq::Scheduled::Poller.prepend Gitlab::Patch::SidekiqPoller Sidekiq::Scheduled::Poller.prepend Gitlab::Patch::SidekiqPoller
Sidekiq::Cron::Poller.prepend Gitlab::Patch::SidekiqPoller Sidekiq::Cron::Poller.prepend Gitlab::Patch::SidekiqPoller
Sidekiq::Cron::Poller.prepend Gitlab::Patch::SidekiqCronPoller

View File

@ -1,3 +1,27 @@
# This file contains code based on the wikicloth project:
# https://github.com/nricciar/wikicloth
#
# Copyright (c) 2009 The wikicloth authors.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# frozen_string_literal: true # frozen_string_literal: true
require 'wikicloth' require 'wikicloth'
@ -20,7 +44,10 @@ require 'digest/sha2'
# - https://gitlab.com/gitlab-org/gitlab/-/issues/361266 # - https://gitlab.com/gitlab-org/gitlab/-/issues/361266
# Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth # Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth
raise 'New version of WikiCloth detected, please remove this patch' unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1') unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1')
raise 'New version of WikiCloth detected, please either update the version for this check, ' \
'or remove this patch if no longer needed'
end
# rubocop:disable Style/ClassAndModuleChildren # rubocop:disable Style/ClassAndModuleChildren
# rubocop:disable Layout/SpaceAroundEqualsInParameterDefault # rubocop:disable Layout/SpaceAroundEqualsInParameterDefault
@ -43,6 +70,12 @@ raise 'New version of WikiCloth detected, please remove this patch' unless Gem::
# rubocop:disable Style/RegexpLiteralMixedPreserve # rubocop:disable Style/RegexpLiteralMixedPreserve
# rubocop:disable Style/RedundantRegexpCharacterClass # rubocop:disable Style/RedundantRegexpCharacterClass
# rubocop:disable Performance/StringInclude # rubocop:disable Performance/StringInclude
# rubocop:disable Layout/LineLength
# rubocop:disable Style/RedundantSelf
# rubocop:disable Style/SymbolProc
# rubocop:disable Layout/SpaceInsideParens
# rubocop:disable Style/GuardClause
# rubocop:disable Style/RedundantRegexpEscape
module WikiCloth module WikiCloth
class WikiCloth class WikiCloth
def render(opt={}) def render(opt={})
@ -218,3 +251,9 @@ end
# rubocop:enable Style/RegexpLiteralMixedPreserve # rubocop:enable Style/RegexpLiteralMixedPreserve
# rubocop:enable Style/RedundantRegexpCharacterClass # rubocop:enable Style/RedundantRegexpCharacterClass
# rubocop:enable Performance/StringInclude # rubocop:enable Performance/StringInclude
# rubocop:enable Layout/LineLength
# rubocop:enable Style/RedundantSelf
# rubocop:enable Style/SymbolProc
# rubocop:enable Layout/SpaceInsideParens
# rubocop:enable Style/GuardClause
# rubocop:enable Style/RedundantRegexpEscape

View File

@ -0,0 +1,272 @@
# This file contains code based on the wikicloth project:
# https://github.com/nricciar/wikicloth
#
# Copyright (c) 2009 The wikicloth authors.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# frozen_string_literal: true
require 'wikicloth'
require 'wikicloth/wiki_buffer/var'
# Adds patch for changes in this PRs:
#
# https://github.com/nricciar/wikicloth/pull/110
#
# The maintainers are not releasing new versions, so we
# need to patch it here.
#
# If they ever do release a version, then we can remove this file.
#
# See:
# - https://gitlab.com/gitlab-org/gitlab/-/issues/372400
# Guard to ensure we remember to delete this patch if they ever release a new version of wikicloth
unless Gem::Version.new(WikiCloth::VERSION) == Gem::Version.new('0.8.1')
raise 'New version of WikiCloth detected, please either update the version for this check, ' \
'or remove this patch if no longer needed'
end
# rubocop:disable Style/ClassAndModuleChildren
# rubocop:disable Style/HashSyntax
# rubocop:disable Layout/SpaceAfterComma
# rubocop:disable Style/RescueStandardError
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Cop/LineBreakAroundConditionalBlock
# rubocop:disable Layout/EmptyLineAfterGuardClause
# rubocop:disable Performance/ReverseEach
# rubocop:disable Style/PerlBackrefs
# rubocop:disable Style/RedundantRegexpCharacterClass
# rubocop:disable Performance/StringInclude
# rubocop:disable Style/IfUnlessModifier
# rubocop:disable Layout/LineLength
# rubocop:disable Lint/DeprecatedClassMethods
# rubocop:disable Lint/UselessAssignment
# rubocop:disable Lint/RedundantStringCoercion
# rubocop:disable Style/StringLiteralsInInterpolation
# rubocop:disable Lint/UriEscapeUnescape
# rubocop:disable Style/For
# rubocop:disable Style/SlicingWithRange
# rubocop:disable Style/GuardClause
# rubocop:disable Style/ZeroLengthPredicate
# rubocop:disable Cop/LineBreakAfterGuardClauses
# rubocop:disable Layout/MultilineHashBraceLayout
module WikiCloth
class WikiCloth
class MathExtension < Extension
# <math>latex markup</math>
#
element 'math', :skip_html => true, :run_globals => false do |buffer|
blahtex_path = @options[:blahtex_path] || '/usr/bin/blahtex'
blahtex_png_path = @options[:blahtex_png_path] || '/tmp'
blahtex_options = @options[:blahtex_options] || '--texvc-compatible-commands --mathml-version-1-fonts --disallow-plane-1 --spacing strict'
if File.exists?(blahtex_path) && @options[:math_formatter] != :google
begin
# pass tex markup to blahtex
response = IO.popen("#{blahtex_path} #{blahtex_options} --png --mathml --png-directory #{blahtex_png_path}","w+") do |pipe|
pipe.write(buffer.element_content)
pipe.close_write
pipe.gets
end
xml_response = REXML::Document.new(response).root
if @options[:blahtex_html_prefix]
# render as embedded image
file_md5 = xml_response.elements["png/md5"].text
"<img src=\"#{File.join(@options[:blahtex_html_prefix],"#{file_md5}.png")}\" />"
else
# render as mathml
html = xml_response.elements["mathml/markup"].text
"<math xmlns=\"http://www.w3.org/1998/Math/MathML\">#{xml_response.elements["mathml/markup"].children.to_s}</math>"
end
rescue => err
# blahtex error
"<span class=\"error\">#{I18n.t("unable to parse mathml", :error => err)}</span>"
end
else
# if blahtex does not exist fallback to google charts api
# This is the patched line from:
# https://github.com/nricciar/wikicloth/pull/110/files#diff-f0cb4c400957bbdcc4c97d69d2aa7f48d8ba56c5943e484863f620605d7d17d4R37
encoded_string = URI.encode_www_form_component(buffer.element_content)
"<img src=\"https://chart.googleapis.com/chart?cht=tx&chl=#{encoded_string}\" />"
end
end
end
class WikiBuffer::Var < WikiBuffer
def default_functions(name,params)
case name
when "#if"
params.first.blank? ? params[2] : params[1]
when "#switch"
match = params.first
default = nil
for p in params[1..-1]
temp = p.split("=")
if p !~ /=/ && temp.length == 1 && p == params.last
return p
elsif temp.instance_of?(Array) && temp.length > 0
test = temp.first.strip
default = temp[1..-1].join("=").strip if test == "#default"
return temp[1..-1].join("=").strip if test == match || (test == "none" && match.blank?)
end
end
default.nil? ? "" : default
when "#expr"
begin
ExpressionParser::Parser.new.parse(params.first)
rescue RuntimeError
I18n.t('expression error', :error => $!)
end
when "#ifexpr"
val = false
begin
val = ExpressionParser::Parser.new.parse(params.first)
rescue RuntimeError
end
if val
params[1]
else
params[2]
end
when "#ifeq"
if params[0] =~ /^[0-9A-Fa-f]+$/ && params[1] =~ /^[0-9A-Fa-f]+$/
params[0].to_i == params[1].to_i ? params[2] : params[3]
else
params[0] == params[1] ? params[2] : params[3]
end
when "#len"
params.first.length
when "#sub"
params.first[params[1].to_i,params[2].to_i]
when "#pad"
case params[3]
when "right"
params[0].ljust(params[1].to_i,params[2])
when "center"
params[0].center(params[1].to_i,params[2])
else
params[0].rjust(params[1].to_i,params[2])
end
when "#iferror"
params.first =~ /error/ ? params[1] : params[2]
when "#capture"
@options[:params][params.first] = params[1]
""
when "urlencode"
# This is the patched line from:
# https://github.com/nricciar/wikicloth/pull/110/files#diff-f262faf4fadb222cca87185be0fb65b3f49659abc840794cc83a736d41310fb1R170
URI.encode_www_form_component(params.first)
when "lc"
params.first.downcase
when "uc"
params.first.upcase
when "ucfirst"
params.first.capitalize
when "lcfirst"
params.first[0,1].downcase + params.first[1..-1]
when "anchorencode"
params.first.gsub(/\s+/,'_')
when "plural"
begin
expr_value = ExpressionParser::Parser.new.parse(params.first)
expr_value.to_i == 1 ? params[1] : params[2]
rescue RuntimeError
I18n.t('expression error', :error => $!)
end
when "ns"
values = {
"" => "", "0" => "",
"1" => localise_ns("Talk"), "talk" => localise_ns("Talk"),
"6" => localise_ns("File"), "file" => localise_ns("File"), "image" => localise_ns("File"),
"10" => localise_ns("Template"), "template" => localise_ns("Template"),
"14" => localise_ns("Category"), "category" => localise_ns("Category"),
"-1" => localise_ns("Special"), "special" => localise_ns("Special"),
"12" => localise_ns("Help"), "help" => localise_ns("Help"),
"-2" => localise_ns("Media"), "media" => localise_ns("Media") }
values[localise_ns(params.first,:en).gsub(/\s+/,'_').downcase]
when "#language"
WikiNamespaces.language_name(params.first)
when "#tag"
return "" if params.empty?
elem = Builder::XmlMarkup.new
return elem.tag!(params.first) if params.length == 1
return elem.tag!(params.first) { |e| e << params.last } if params.length == 2
tag_attrs = {}
params[1..-2].each do |attr|
tag_attrs[$1] = $2 if attr =~ /^\s*([\w]+)\s*=\s*"(.*)"\s*$/
end
elem.tag!(params.first,tag_attrs) { |e| e << params.last }
when "debug"
ret = nil
case params.first
when "param"
@options[:buffer].buffers.reverse.each do |b|
if b.instance_of?(WikiBuffer::HTMLElement) && b.element_name == "template"
ret = b.get_param(params[1])
end
end
ret
when "buffer"
ret = "<pre>"
buffer = @options[:buffer].buffers
buffer.each do |b|
ret += " --- #{b.class}"
ret += b.instance_of?(WikiBuffer::HTMLElement) ? " -- #{b.element_name}\n" : " -- #{b.data}\n"
end
"#{ret}</pre>"
end
end
end
end
end
end
# rubocop:enable Style/ClassAndModuleChildren
# rubocop:enable Style/HashSyntax
# rubocop:enable Layout/SpaceAfterComma
# rubocop:enable Style/RescueStandardError
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Cop/LineBreakAroundConditionalBlock
# rubocop:enable Layout/EmptyLineAfterGuardClause
# rubocop:enable Performance/ReverseEach
# rubocop:enable Style/PerlBackrefs
# rubocop:enable Style/RedundantRegexpCharacterClass
# rubocop:enable Performance/StringInclude
# rubocop:enable Style/IfUnlessModifier
# rubocop:enable Layout/LineLength
# rubocop:enable Lint/DeprecatedClassMethods
# rubocop:enable Lint/UselessAssignment
# rubocop:enable Lint/RedundantStringCoercion
# rubocop:enable Style/StringLiteralsInInterpolation
# rubocop:enable Lint/UriEscapeUnescape
# rubocop:enable Style/For
# rubocop:enable Style/SlicingWithRange
# rubocop:enable Style/GuardClause
# rubocop:enable Style/ZeroLengthPredicate
# rubocop:enable Cop/LineBreakAfterGuardClauses
# rubocop:enable Layout/MultilineHashBraceLayout

View File

@ -48,7 +48,6 @@ tier:
- premium - premium
- ultimate - ultimate
performance_indicator_type: performance_indicator_type:
- smau
- gmau - gmau
- paid_gmau - paid_gmau
milestone: "<13.9" milestone: "<13.9"

View File

@ -0,0 +1,26 @@
---
key_path: redis_hll_counters.environments.users_visiting_environments_pages_monthly
description: Monthly count of unique users visiting environments pages
product_section: ops
product_stage: release
product_group: release
product_category: environment_management
value_type: number
status: active
milestone: "15.4"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97063
time_frame: 28d
data_source: redis_hll
data_category: optional
instrumentation_class: RedisHLLMetric
performance_indicator_type: []
options:
events:
- users_visiting_environments_pages
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -13,7 +13,8 @@ time_frame: all
data_source: database data_source: database
instrumentation_class: CountUserAuthMetric instrumentation_class: CountUserAuthMetric
data_category: optional data_category: optional
performance_indicator_type: [] performance_indicator_type:
- smau
distribution: distribution:
- ce - ce
- ee - ee

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddColumnBranchFilterStrategyToWebHooks < Gitlab::Database::Migration[2.0]
def change
add_column :web_hooks, :branch_filter_strategy, :integer, null: false, default: 0, limit: 2
end
end

View File

@ -0,0 +1 @@
394f346e3a93f8a6b74fd0461eb59f569c6a18f90ae653c330a38e3a3706b5f6

View File

@ -22922,7 +22922,8 @@ CREATE TABLE web_hooks (
disabled_until timestamp with time zone, disabled_until timestamp with time zone,
encrypted_url_variables bytea, encrypted_url_variables bytea,
encrypted_url_variables_iv bytea, encrypted_url_variables_iv bytea,
integration_id integer integration_id integer,
branch_filter_strategy smallint DEFAULT 0 NOT NULL
); );
CREATE SEQUENCE web_hooks_id_seq CREATE SEQUENCE web_hooks_id_seq

View File

@ -744,8 +744,8 @@ You can send backups to a locally-mounted share (for example, `NFS`,`CIFS`, or `
To do this, you must set the following configuration keys: To do this, you must set the following configuration keys:
- `backup_upload_remote_directory`: mounted directory that backups are copied to. - `backup_upload_connection.local_root`: mounted directory that backups are copied to.
- `backup_upload_connection.local_root`: subdirectory of the `backup_upload_remote_directory` directory. It is created if it doesn't exist. - `backup_upload_remote_directory`: subdirectory of the `backup_upload_connection.local_root` directory. It is created if it doesn't exist.
If you want to copy the tarballs to the root of your mounted directory, use `.`. If you want to copy the tarballs to the root of your mounted directory, use `.`.
When mounted, the directory set in the `local_root` key must be owned by either: When mounted, the directory set in the `local_root` key must be owned by either:

View File

@ -201,7 +201,7 @@ role on an ancestor group, add the user to the subgroup again with a higher role
## Mention subgroups ## Mention subgroups
Mentioning subgroups ([`@<subgroup_name>`](../../discussions/index.md#mentions)) in issues, commits, and merge requests Mentioning subgroups ([`@<subgroup_name>`](../../discussions/index.md#mentions)) in issues, commits, and merge requests
notifies all members of that group. Mentioning works the same as for projects and groups, and you can choose the group notifies all direct members of that group. Inherited members of a sub-group are not notified by mentions. Mentioning works the same as for projects and groups, and you can choose the group
of people to be notified. of people to be notified.
<!-- ## Troubleshooting <!-- ## Troubleshooting

View File

@ -27,9 +27,33 @@ module API
end end
end end
end end
resources 'batched_background_migrations/:id/resume' do
desc 'Resume a batched background migration'
params do
optional :database,
type: String,
values: Gitlab::Database.all_database_names,
desc: 'The name of the database',
default: 'main'
requires :id,
type: Integer,
desc: 'The batched background migration id'
end
put do
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
batched_background_migration.execute!
present_entity(batched_background_migration)
end
end
end
end end
helpers do helpers do
def batched_background_migration
@batched_background_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.find(params[:id])
end
def base_model def base_model
database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
@base_model ||= Gitlab::Database.database_base_models[database] @base_model ||= Gitlab::Database.database_base_models[database]

View File

@ -34,9 +34,9 @@ module Gitlab
def expand_value(value) def expand_value(value)
if value.is_a?(Hash) if value.is_a?(Hash)
{ value: value[:value].to_s, description: value[:description] } { value: value[:value].to_s, description: value[:description] }.compact
else else
{ value: value.to_s, description: nil } { value: value.to_s }
end end
end end
end end

View File

@ -30,7 +30,7 @@ module Gitlab
end end
def value_with_data def value_with_data
{ value: @config.to_s, description: nil } { value: @config.to_s }
end end
end end
@ -66,7 +66,7 @@ module Gitlab
end end
def value_with_data def value_with_data
{ value: value, description: config_description } { value: value, description: config_description }.compact
end end
def config_value def config_value

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
class AssignPartition < Chain::Base
include Chain::Helpers
DEFAULT_PARTITION_ID = 100
def perform!
@pipeline.partition_id = find_partition_id
end
def break?
@pipeline.errors.any?
end
private
# TODO handle parent-child pipelines
def find_partition_id
DEFAULT_PARTITION_ID
end
end
end
end
end
end

View File

@ -148,7 +148,9 @@ module Gitlab
ref: @pipeline.ref, ref: @pipeline.ref,
tag: @pipeline.tag, tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger, trigger_request: @pipeline.legacy_trigger,
protected: @pipeline.protected_ref? protected: @pipeline.protected_ref?,
partition_id: @pipeline.partition_id,
metadata_attributes: { partition_id: @pipeline.partition_id }
} }
end end

View File

@ -25,7 +25,8 @@ module Gitlab
{ name: @attributes.fetch(:name), { name: @attributes.fetch(:name),
position: @attributes.fetch(:index), position: @attributes.fetch(:index),
pipeline: @pipeline, pipeline: @pipeline,
project: @pipeline.project } project: @pipeline.project,
partition_id: @pipeline.partition_id }
end end
def seeds def seeds

View File

@ -8,6 +8,7 @@ module Gitlab
BATCH_CLASS_MODULE = "#{JOB_CLASS_MODULE}::BatchingStrategies" BATCH_CLASS_MODULE = "#{JOB_CLASS_MODULE}::BatchingStrategies"
MAXIMUM_FAILED_RATIO = 0.5 MAXIMUM_FAILED_RATIO = 0.5
MINIMUM_JOBS = 50 MINIMUM_JOBS = 50
FINISHED_PROGRESS_VALUE = 100
self.table_name = :batched_background_migrations self.table_name = :batched_background_migrations
@ -232,7 +233,15 @@ module Gitlab
"BatchedMigration[id: #{id}]" "BatchedMigration[id: #{id}]"
end end
# Computes an estimation of the progress of the migration in percents.
#
# Because `total_tuple_count` is an estimation of the tuples based on DB statistics
# when the migration is complete there can actually be more or less tuples that initially
# estimated as `total_tuple_count` so the progress may not show 100%. For that reason when
# we know migration completed successfully, we just return the 100 value
def progress def progress
return FINISHED_PROGRESS_VALUE if finished?
return unless total_tuple_count.to_i > 0 return unless total_tuple_count.to_i > 0
100 * migrated_tuple_count / total_tuple_count 100 * migrated_tuple_count / total_tuple_count

View File

@ -6,9 +6,15 @@ module Gitlab
def initialize def initialize
@primary = [] @primary = []
@secondary = [] @secondary = []
@last_header_added = nil
end end
def add_primary_menu_item(**args) def add_primary_menu_item(header: nil, **args)
if header && (header != @last_header_added)
add_menu_header(dest: @primary, title: header)
@last_header_added = header
end
add_menu_item(dest: @primary, **args) add_menu_item(dest: @primary, **args)
end end
@ -30,6 +36,12 @@ module Gitlab
dest.push(item) dest.push(item)
end end
def add_menu_header(dest:, **args)
header = ::Gitlab::Nav::TopNavMenuHeader.build(**args)
dest.push(header)
end
end end
end end
end end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module Gitlab
module Nav
class TopNavMenuHeader
def self.build(title:)
{
type: :header,
title: title
}
end
end
end
end

View File

@ -11,6 +11,7 @@ module Gitlab
def self.build(id:, title:, active: false, icon: '', href: '', view: '', css_class: nil, data: nil, emoji: nil) def self.build(id:, title:, active: false, icon: '', href: '', view: '', css_class: nil, data: nil, emoji: nil)
{ {
id: id, id: id,
type: :item,
title: title, title: title,
active: active, active: active,
icon: icon, icon: icon,

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
# Patch to address https://github.com/ondrejbartas/sidekiq-cron/issues/361
# This restores the poll interval to v1.2.0 behavior
# https://github.com/ondrejbartas/sidekiq-cron/blob/v1.2.0/lib/sidekiq/cron/poller.rb#L36-L38
# This patch only applies to v1.4.0
require 'sidekiq/cron/version'
if Gem::Version.new(Sidekiq::Cron::VERSION) != Gem::Version.new('1.4.0')
raise 'New version of sidekiq-cron detected, please remove or update this patch'
end
module Gitlab
module Patch
module SidekiqCronPoller
def poll_interval_average
Sidekiq.options[:poll_interval] || Sidekiq::Cron::POLL_INTERVAL
end
end
end
end

View File

@ -357,3 +357,8 @@
category: manage category: manage
aggregation: weekly aggregation: weekly
expiry: 42 expiry: 42
# Environments page
- name: users_visiting_environments_pages
category: environments
redis_slot: users
aggregation: weekly

View File

@ -41407,9 +41407,18 @@ msgstr ""
msgid "Too many users found. Quick actions are limited to at most %{max_count} users" msgid "Too many users found. Quick actions are limited to at most %{max_count} users"
msgstr "" msgstr ""
msgid "TopNav|Explore"
msgstr ""
msgid "TopNav|Go back" msgid "TopNav|Go back"
msgstr "" msgstr ""
msgid "TopNav|Switch to"
msgstr ""
msgid "TopNav|Your dashboard"
msgstr ""
msgid "Topic %{source_topic} was successfully merged into topic %{target_topic}." msgid "Topic %{source_topic} was successfully merged into topic %{target_topic}."
msgstr "" msgstr ""

View File

@ -21,6 +21,10 @@ module QA
base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do
element :file_upload_field element :file_upload_field
end end
base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do
element :wiki_hidden_content
end
end end
def add_heading(heading, text) def add_heading(heading, text)
@ -41,6 +45,13 @@ module QA
text_area.send_keys(:return) text_area.send_keys(:return)
find_element(:file_upload_field, visible: false).send_keys(image_path) find_element(:file_upload_field, visible: false).send_keys(image_path)
end end
wait_for_requests
QA::Support::Retrier.retry_on_exception do
source = find_element(:wiki_hidden_content, visible: false)
source.value =~ %r{uploads/.*#{::File.basename(image_path)}}
end
end end
private private

View File

@ -41,6 +41,7 @@ const ROOT_RAILS = IS_EE ? path.join(ROOT, 'ee') : ROOT;
const FIXTURES_FOLDER_NAME = IS_EE ? 'fixtures-ee' : 'fixtures'; const FIXTURES_FOLDER_NAME = IS_EE ? 'fixtures-ee' : 'fixtures';
const FIXTURES_ROOT = path.join(ROOT, 'tmp/tests/frontend', FIXTURES_FOLDER_NAME); const FIXTURES_ROOT = path.join(ROOT, 'tmp/tests/frontend', FIXTURES_FOLDER_NAME);
const PATH_SIGNIN_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in.html'); const PATH_SIGNIN_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in.html');
const PATH_SIGNIN_OLD_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in-old.html');
const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets'); const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets');
const PATH_STARTUP_SCSS = path.join(ROOT_RAILS, 'app/assets/stylesheets/startup'); const PATH_STARTUP_SCSS = path.join(ROOT_RAILS, 'app/assets/stylesheets/startup');
@ -80,7 +81,7 @@ const OUTPUTS = [
}), }),
{ {
outFile: 'startup-signin', outFile: 'startup-signin',
htmlPaths: [PATH_SIGNIN_HTML], htmlPaths: [PATH_SIGNIN_HTML, PATH_SIGNIN_OLD_HTML],
cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX], cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
purgeOptions: { purgeOptions: {
safelist: { safelist: {

View File

@ -32,6 +32,11 @@ RSpec.describe Projects::EnvironmentsController do
get :index, params: environment_params get :index, params: environment_params
end end
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context 'when requesting JSON response for folders' do context 'when requesting JSON response for folders' do
@ -169,6 +174,18 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to be_ok expect(response).to be_ok
expect(response).to render_template 'folder' expect(response).to render_template 'folder'
end end
it_behaves_like 'tracking unique visits', :folder do
let(:request_params) do
{
namespace_id: project.namespace,
project_id: project,
id: 'staging-1.0'
}
end
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context 'when using JSON format' do context 'when using JSON format' do
@ -197,6 +214,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to be_ok expect(response).to be_ok
end end
it_behaves_like 'tracking unique visits', :show do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context 'with invalid id' do context 'with invalid id' do
@ -210,12 +232,30 @@ RSpec.describe Projects::EnvironmentsController do
end end
end end
describe 'GET new' do
it 'responds with a status code 200' do
get :new, params: environment_params
expect(response).to be_ok
end
it_behaves_like 'tracking unique visits', :new do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
describe 'GET edit' do describe 'GET edit' do
it 'responds with a status code 200' do it 'responds with a status code 200' do
get :edit, params: environment_params get :edit, params: environment_params
expect(response).to be_ok expect(response).to be_ok
end end
it_behaves_like 'tracking unique visits', :edit do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
describe 'PATCH #update' do describe 'PATCH #update' do
@ -230,6 +270,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{environment.id}") expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{environment.id}")
end end
it_behaves_like 'tracking unique visits', :update do
let(:request_params) { params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context "when environment params are invalid" do context "when environment params are invalid" do
@ -294,6 +339,11 @@ RSpec.describe Projects::EnvironmentsController do
{ 'redirect_url' => { 'redirect_url' =>
project_environment_url(project, environment) }) project_environment_url(project, environment) })
end end
it_behaves_like 'tracking unique visits', :stop do
let(:request_params) { environment_params(format: :json) }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context 'when no stop action' do context 'when no stop action' do
@ -321,6 +371,11 @@ RSpec.describe Projects::EnvironmentsController do
it_behaves_like 'successful response for #cancel_auto_stop' it_behaves_like 'successful response for #cancel_auto_stop'
it_behaves_like 'tracking unique visits', :cancel_auto_stop do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
context 'when user is reporter' do context 'when user is reporter' do
let(:user) { reporter } let(:user) { reporter }
@ -357,6 +412,11 @@ RSpec.describe Projects::EnvironmentsController do
get :terminal, params: environment_params get :terminal, params: environment_params
end end
it_behaves_like 'tracking unique visits', :terminal do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context 'with invalid id' do context 'with invalid id' do
@ -859,6 +919,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{json_response['environment']['id']}") expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{json_response['environment']['id']}")
end end
it_behaves_like 'tracking unique visits', :create do
let(:request_params) { params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end end
context "when environment params are invalid" do context "when environment params are invalid" do

View File

@ -69,11 +69,25 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
it_behaves_like 'startup css project fixtures', 'dark' it_behaves_like 'startup css project fixtures', 'dark'
end end
describe RegistrationsController, '(Startup CSS fixtures)', type: :controller do describe SessionsController, '(Startup CSS fixtures)', type: :controller do
include DeviseHelpers
before do
set_devise_mapping(context: request)
end
it 'startup_css/sign-in.html' do it 'startup_css/sign-in.html' do
get :new get :new
expect(response).to be_successful expect(response).to be_successful
end end
it 'startup_css/sign-in-old.html' do
stub_feature_flags(restyle_login_page: false)
get :new
expect(response).to be_successful
end
end end
end end

View File

@ -4,11 +4,20 @@ import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
const TEST_SECTIONS = [ const TEST_SECTIONS = [
{ {
id: 'primary', id: 'primary',
menuItems: [{ id: 'test', href: '/test/href' }, { id: 'foo' }, { id: 'bar' }], menuItems: [
{ type: 'header', title: 'Heading' },
{ type: 'item', id: 'test', href: '/test/href' },
{ type: 'header', title: 'Another Heading' },
{ type: 'item', id: 'foo' },
{ type: 'item', id: 'bar' },
],
}, },
{ {
id: 'secondary', id: 'secondary',
menuItems: [{ id: 'lorem' }, { id: 'ipsum' }], menuItems: [
{ type: 'item', id: 'lorem' },
{ type: 'item', id: 'ipsum' },
],
}, },
]; ];
@ -25,10 +34,20 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
}; };
const findMenuItemModels = (parent) => const findMenuItemModels = (parent) =>
parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({ parent.findAll('[data-testid="menu-header"],[data-testid="menu-item"]').wrappers.map((x) => {
menuItem: x.props('menuItem'), return {
classes: x.classes(), menuItem: x.vm
})); ? {
type: 'item',
...x.props('menuItem'),
}
: {
type: 'header',
title: x.text(),
},
classes: x.classes(),
};
});
const findSectionModels = () => const findSectionModels = () =>
wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({ wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({
classes: x.classes(), classes: x.classes(),
@ -45,32 +64,31 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
}); });
it('renders sections with menu items', () => { it('renders sections with menu items', () => {
const headerClasses = ['gl-px-4', 'gl-py-2', 'gl-text-gray-900', 'gl-display-block'];
const itemClasses = ['gl-w-full'];
expect(findSectionModels()).toEqual([ expect(findSectionModels()).toEqual([
{ {
classes: [], classes: [],
menuItems: [ menuItems: TEST_SECTIONS[0].menuItems.map((menuItem, index) => {
{ const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
menuItem: TEST_SECTIONS[0].menuItems[0], if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
classes: ['gl-w-full'], return {
},
...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
menuItem, menuItem,
classes: ['gl-w-full', 'gl-mt-1'], classes,
})), };
], }),
}, },
{ {
classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'], classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
menuItems: [ menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
{ const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
menuItem: TEST_SECTIONS[1].menuItems[0], if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
classes: ['gl-w-full'], return {
},
...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
menuItem, menuItem,
classes: ['gl-w-full', 'gl-mt-1'], classes,
})), };
], }),
}, },
]); ]);
}); });
@ -88,7 +106,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItem.vm.$emit('click'); menuItem.vm.$emit('click');
expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[1]]]); expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[3]]]);
}); });
}); });

View File

@ -0,0 +1,59 @@
import { setHTMLFixture, resetHTMLFixture } from 'jest/__helpers__/fixtures';
import initFormUpdate from '~/pages/projects/merge_requests/edit/update_form';
describe('Update form state', () => {
const submitEvent = new Event('submit', {
bubbles: true,
cancelable: true,
});
const submitForm = () => document.querySelector('.merge-request-form').dispatchEvent(submitEvent);
const hiddenInputs = () => document.querySelectorAll('input[type="hidden"]');
const checkboxes = () => document.querySelectorAll('.js-form-update');
beforeEach(() => {
setHTMLFixture(`
<form class="merge-request-form">
<div class="form-check">
<input type="hidden" name="merge_request[force_remove_source_branch]" value="0" autocomplete="off">
<input type="checkbox" name="merge_request[force_remove_source_branch]" id="merge_request_force_remove_source_branch" value="1" class="form-check-input js-form-update">
</div>
<div class="form-check">
<input type="hidden" name="merge_request[squash]" value="0" autocomplete="off">
<input type="checkbox" name="merge_request[squash]" id="merge_request_squash" value="1" class="form-check-input js-form-update">
</div>
</form>`);
initFormUpdate();
});
afterEach(() => {
resetHTMLFixture();
});
it('at initial state', () => {
submitForm();
expect(hiddenInputs()).toHaveLength(2);
});
it('when one element is checked', () => {
checkboxes()[0].setAttribute('checked', true);
submitForm();
expect(hiddenInputs()).toHaveLength(1);
});
it('when all elements are checked', () => {
checkboxes()[0].setAttribute('checked', true);
checkboxes()[1].setAttribute('checked', true);
submitForm();
expect(hiddenInputs()).toHaveLength(0);
});
it('when checked and then unchecked', () => {
checkboxes()[0].setAttribute('checked', true);
checkboxes()[0].removeAttribute('checked');
checkboxes()[1].setAttribute('checked', true);
checkboxes()[1].removeAttribute('checked');
submitForm();
expect(hiddenInputs()).toHaveLength(2);
});
});

View File

@ -2,10 +2,12 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api'; import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue'; import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { users, mockSearch, branches, tags } from '../mock_data'; import { users, mockSearch, branches, tags } from '../mock_data';
describe('Pipelines filtered search', () => { describe('Pipelines filtered search', () => {
@ -177,4 +179,20 @@ describe('Pipelines filtered search', () => {
expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length); expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
}); });
}); });
describe('tracking', () => {
afterEach(() => {
unmockTracking();
});
it('tracks filtered search click', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findFilteredSearch().vm.$emit('submit', mockSearch);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filtered_search', {
label: TRACKING_CATEGORIES.index,
});
});
});
}); });

View File

@ -1,12 +1,14 @@
import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PipelineMultiActions, { import PipelineMultiActions, {
i18n, i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue'; } from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
describe('Pipeline Multi Actions Dropdown', () => { describe('Pipeline Multi Actions Dropdown', () => {
let wrapper; let wrapper;
@ -136,4 +138,22 @@ describe('Pipeline Multi Actions Dropdown', () => {
}); });
}); });
}); });
describe('tracking', () => {
afterEach(() => {
unmockTracking();
});
it('tracks artifacts dropdown click', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
createComponent();
findDropdown().vm.$emit('show');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_artifacts_dropdown', {
label: TRACKING_CATEGORIES.index,
});
});
});
}); });

View File

@ -1,12 +1,15 @@
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data'; import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
const projectPath = 'test/test'; const projectPath = 'test/test';
describe('Pipeline Url Component', () => { describe('Pipeline Url Component', () => {
let wrapper; let wrapper;
let trackingSpy;
const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell'); const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell');
const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link'); const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link');
@ -14,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha'); const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha');
const findCommitIcon = () => wrapper.findByTestId('commit-icon'); const findCommitIcon = () => wrapper.findByTestId('commit-icon');
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type'); const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
const findCommitRefName = () => wrapper.findByTestId('commit-ref-name');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]'); const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
@ -31,7 +35,6 @@ describe('Pipeline Url Component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it('should render pipeline url table cell', () => { it('should render pipeline url table cell', () => {
@ -49,7 +52,7 @@ describe('Pipeline Url Component', () => {
}); });
it('should render the commit title, commit reference and commit-short-sha', () => { it('should render the commit title, commit reference and commit-short-sha', () => {
createComponent({}, true); createComponent();
const commitWrapper = findCommitTitleContainer(); const commitWrapper = findCommitTitleContainer();
@ -83,7 +86,7 @@ describe('Pipeline Url Component', () => {
}); });
it('should render commit icon tooltip', () => { it('should render commit icon tooltip', () => {
createComponent({}, true); createComponent();
expect(findCommitIcon().attributes('title')).toBe('Commit'); expect(findCommitIcon().attributes('title')).toBe('Commit');
}); });
@ -94,8 +97,68 @@ describe('Pipeline Url Component', () => {
${mockPipelineBranch()} | ${'Branch'} ${mockPipelineBranch()} | ${'Branch'}
${mockPipeline()} | ${'Merge Request'} ${mockPipeline()} | ${'Merge Request'}
`('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => { `('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
createComponent(pipeline, true); createComponent(pipeline);
expect(findCommitIconType().attributes('title')).toBe(expectedTitle); expect(findCommitIconType().attributes('title')).toBe(expectedTitle);
}); });
describe('tracking', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('tracks pipeline id click', () => {
createComponent();
findPipelineUrlLink().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_pipeline_id', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks merge request ref click', () => {
createComponent();
findRefName().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_mr_ref', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks commit ref name click', () => {
createComponent(mockPipelineBranch());
findCommitRefName().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_name', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks commit title click', () => {
createComponent(mockPipelineBranch());
findCommitTitle(findCommitTitleContainer()).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_title', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks commit short sha click', () => {
createComponent(mockPipelineBranch());
findCommitShortSha().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_sha', {
label: TRACKING_CATEGORIES.index,
});
});
});
}); });

View File

@ -2,6 +2,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
@ -9,6 +10,7 @@ import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => { jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
@ -96,6 +98,22 @@ describe('Pipelines Actions dropdown', () => {
expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledTimes(1);
}); });
}); });
describe('tracking', () => {
afterEach(() => {
unmockTracking();
});
it('tracks manual actions click', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
findDropdown().vm.$emit('shown');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
label: TRACKING_CATEGORIES.index,
});
});
});
}); });
describe('scheduled jobs', () => { describe('scheduled jobs', () => {

View File

@ -2,6 +2,7 @@ import '~/commons';
import { GlTableLite } from '@gitlab/ui'; import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json'; import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
@ -13,6 +14,7 @@ import {
PipelineKeyOptions, PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL, BUTTON_TOOLTIP_CANCEL,
TRACKING_CATEGORIES,
} from '~/pipelines/constants'; } from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub'; import eventHub from '~/pipelines/event_hub';
@ -23,6 +25,7 @@ jest.mock('~/pipelines/event_hub');
describe('Pipelines Table', () => { describe('Pipelines Table', () => {
let pipeline; let pipeline;
let wrapper; let wrapper;
let trackingSpy;
const defaultProps = { const defaultProps = {
pipelines: [], pipelines: [],
@ -69,6 +72,7 @@ describe('Pipelines Table', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
@ -96,10 +100,6 @@ describe('Pipelines Table', () => {
it('should render a status badge', () => { it('should render a status badge', () => {
expect(findStatusBadge().exists()).toBe(true); expect(findStatusBadge().exists()).toBe(true);
}); });
it('should render status badge with correct path', () => {
expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
});
}); });
describe('pipeline cell', () => { describe('pipeline cell', () => {
@ -167,5 +167,39 @@ describe('Pipelines Table', () => {
expect(findTriggerer().exists()).toBe(true); expect(findTriggerer().exists()).toBe(true);
}); });
}); });
describe('tracking', () => {
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
unmockTracking();
});
it('tracks status badge click', () => {
findStatusBadge().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks retry pipeline button click', () => {
findRetryBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
label: TRACKING_CATEGORIES.index,
});
});
it('tracks cancel pipeline button click', () => {
findCancelBtn().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
label: TRACKING_CATEGORIES.index,
});
});
});
}); });
}); });

View File

@ -1,6 +1,11 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
describe('CI Badge Link Component', () => { describe('CI Badge Link Component', () => {
let wrapper; let wrapper;
@ -79,17 +84,20 @@ describe('CI Badge Link Component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
it.each(Object.keys(statuses))('should render badge for status: %s', (status) => { it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => {
createComponent({ status: statuses[status] }); createComponent({ status: statuses[status] });
expect(wrapper.attributes('href')).toBe(statuses[status].details_path); expect(wrapper.attributes('href')).toBe();
expect(wrapper.text()).toBe(statuses[status].text); expect(wrapper.text()).toBe(statuses[status].text);
expect(wrapper.classes()).toContain('ci-status'); expect(wrapper.classes()).toContain('ci-status');
expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`); expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
expect(findIcon().exists()).toBe(true); expect(findIcon().exists()).toBe(true);
await wrapper.trigger('click');
expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path);
}); });
it('should not render label', () => { it('should not render label', () => {
@ -97,4 +105,12 @@ describe('CI Badge Link Component', () => {
expect(wrapper.text()).toBe(''); expect(wrapper.text()).toBe('');
}); });
it('should emit ciStatusBadgeClick event', async () => {
createComponent({ status: statuses.success });
await wrapper.trigger('click');
expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]);
});
}); });

View File

@ -52,6 +52,9 @@ RSpec.describe Nav::TopNavHelper do
context 'when current_user is nil (anonymous)' do context 'when current_user is nil (anonymous)' do
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = [ expected_primary = [
{ href: '/explore', icon: 'project', id: 'project', title: 'Projects' }, { href: '/explore', icon: 'project', id: 'project', title: 'Projects' },
{ href: '/explore/groups', icon: 'group', id: 'groups', title: 'Groups' }, { href: '/explore/groups', icon: 'group', id: 'groups', title: 'Groups' },
@ -60,7 +63,7 @@ RSpec.describe Nav::TopNavHelper do
::Gitlab::Nav::TopNavMenuItem.build(**item) ::Gitlab::Nav::TopNavMenuItem.build(**item)
end end
expect(subject[:primary]).to eq(expected_primary) expect(subject[:primary]).to eq([expected_header, *expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do
@ -117,6 +120,9 @@ RSpec.describe Nav::TopNavHelper do
let(:projects_view) { subject[:views][:projects] } let(:projects_view) { subject[:views][:projects] }
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-projects-dropdown', css_class: 'qa-projects-dropdown',
data: { data: {
@ -128,7 +134,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Projects', title: 'Projects',
view: 'projects' view: 'projects'
) )
expect(subject[:primary]).to eq([expected_primary]) expect(subject[:primary]).to eq([expected_header, expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do
@ -253,6 +259,9 @@ RSpec.describe Nav::TopNavHelper do
let(:groups_view) { subject[:views][:groups] } let(:groups_view) { subject[:views][:groups] }
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-groups-dropdown', css_class: 'qa-groups-dropdown',
data: { data: {
@ -264,7 +273,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Groups', title: 'Groups',
view: 'groups' view: 'groups'
) )
expect(subject[:primary]).to eq([expected_primary]) expect(subject[:primary]).to eq([expected_header, expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do
@ -376,6 +385,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_milestones) { true } let(:with_milestones) { true }
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: { data: {
qa_selector: 'milestones_link', qa_selector: 'milestones_link',
@ -386,7 +398,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'milestones', id: 'milestones',
title: 'Milestones' title: 'Milestones'
) )
expect(subject[:primary]).to eq([expected_primary]) expect(subject[:primary]).to eq([expected_header, expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do
@ -404,6 +416,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_snippets) { true } let(:with_snippets) { true }
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: { data: {
qa_selector: 'snippets_link', qa_selector: 'snippets_link',
@ -414,7 +429,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'snippets', id: 'snippets',
title: 'Snippets' title: 'Snippets'
) )
expect(subject[:primary]).to eq([expected_primary]) expect(subject[:primary]).to eq([expected_header, expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do
@ -432,6 +447,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_activity) { true } let(:with_activity) { true }
it 'has expected :primary' do it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: { data: {
qa_selector: 'activity_link', qa_selector: 'activity_link',
@ -442,7 +460,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'activity', id: 'activity',
title: 'Activity' title: 'Activity'
) )
expect(subject[:primary]).to eq([expected_primary]) expect(subject[:primary]).to eq([expected_header, expected_primary])
end end
it 'has expected :shortcuts' do it 'has expected :shortcuts' do

View File

@ -30,7 +30,7 @@ RSpec.describe Projects::PipelineHelper do
summary_endpoint: summary_project_pipeline_tests_path(project, pipeline, format: :json), summary_endpoint: summary_project_pipeline_tests_path(project, pipeline, format: :json),
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json), suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(project, pipeline.sha), blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.has_reports?(Ci::JobArtifact.of_report_type(:test)), has_test_report: pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)),
empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'), empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'),
artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'), artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count] tests_count: pipeline.test_report_summary.total[:count]

View File

@ -61,8 +61,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do
describe '#value_with_data' do describe '#value_with_data' do
it 'returns variable with data' do it 'returns variable with data' do
expect(entry.value_with_data).to eq( expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: nil }, 'VARIABLE_1' => { value: 'value 1' },
'VARIABLE_2' => { value: 'value 2', description: nil } 'VARIABLE_2' => { value: 'value 2' }
) )
end end
end end
@ -125,7 +125,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do
it 'returns variable with data' do it 'returns variable with data' do
expect(entry.value_with_data).to eq( expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => { value: 'value 2', description: nil } 'VARIABLE_2' => { value: 'value 2' }
) )
end end
end end

View File

@ -204,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do describe '#value_with_data' do
subject(:value_with_data) { entry.value_with_data } subject(:value_with_data) { entry.value_with_data }
it { is_expected.to eq(value: 'value', description: nil) } it { is_expected.to eq(value: 'value') }
end end
end end
end end

View File

@ -61,8 +61,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_data' do describe '#value_with_data' do
it 'returns variable with data' do it 'returns variable with data' do
expect(entry.value_with_data).to eq( expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: nil }, 'VARIABLE_1' => { value: 'value 1' },
'VARIABLE_2' => { value: 'value 2', description: nil } 'VARIABLE_2' => { value: 'value 2' }
) )
end end
end end
@ -119,7 +119,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
it 'returns variable with data' do it 'returns variable with data' do
expect(entry.value_with_data).to eq( expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => { value: 'value 2', description: nil } 'VARIABLE_2' => { value: 'value 2' }
) )
end end
end end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::AssignPartition do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
end
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:step) { described_class.new(pipeline, command) }
let(:current_partition_id) { 123 }
describe '#perform!' do
before do
stub_const("#{described_class}::DEFAULT_PARTITION_ID", current_partition_id)
end
subject { step.perform! }
it 'assigns partition_id to pipeline' do
expect { subject }.to change(pipeline, :partition_id).to(current_partition_id)
end
end
end

View File

@ -630,7 +630,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
describe '#progress' do describe '#progress' do
subject { migration.progress } subject { migration.progress }
context 'when the migration is finished' do context 'when the migration is completed' do
let(:migration) do let(:migration) do
create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record| create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record|
create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 1) create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 1)
@ -642,6 +642,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
end end
end end
context 'when the status is finished' do
let(:migration) do
create(:batched_background_migration, :finished, total_tuple_count: 100).tap do |record|
create(:batched_background_migration_job, :succeeded, batched_migration: record, batch_size: 5)
end
end
it 'returns 100' do
expect(subject).to be 100
end
end
context 'when the migration does not have jobs' do context 'when the migration does not have jobs' do
let(:migration) { create(:batched_background_migration, :active) } let(:migration) { create(:batched_background_migration, :active) }

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe ::Gitlab::Nav::TopNavMenuHeader do
describe '.build' do
it 'builds a hash from with the given header' do
title = 'Test Header'
expected = {
title: title,
type: :header
}
expect(described_class.build(title: title)).to eq(expected)
end
end
end

View File

@ -17,7 +17,7 @@ RSpec.describe ::Gitlab::Nav::TopNavMenuItem do
emoji: 'smile' emoji: 'smile'
} }
expect(described_class.build(**item)).to eq(item) expect(described_class.build(**item)).to eq(item.merge(type: :item))
end end
end end
end end

View File

@ -4211,8 +4211,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end end
end end
describe '#has_reports?' do describe '#complete_and_has_reports?' do
subject { pipeline.has_reports?(Ci::JobArtifact.of_report_type(:test)) } subject { pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)) }
context 'when pipeline has builds with test reports' do context 'when pipeline has builds with test reports' do
before do before do

View File

@ -75,4 +75,56 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations do
end end
end end
end end
describe 'PUT /admin/batched_background_migrations/:id/resume' do
let!(:migration) { create(:batched_background_migration, :paused) }
let(:database) { :main }
subject(:resume) do
put api("/admin/batched_background_migrations/#{migration.id}/resume", admin), params: { database: database }
end
it 'pauses the batched background migration' do
resume
aggregate_failures "testing response" do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(migration.id)
expect(json_response['status']).to eq('active')
end
end
context 'when the batched background migration does not exist' do
let(:params) { { database: database } }
it 'returns 404' do
put api("/admin/batched_background_migrations/#{non_existing_record_id}/resume", admin), params: params
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when multiple database is enabled' do
let(:ci_model) { Ci::ApplicationRecord }
let(:database) { :ci }
before do
skip_if_multiple_databases_not_setup
end
it 'uses the correct connection' do
expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(ci_model.connection).and_yield
resume
end
end
context 'when authenticated as a non-admin user' do
it 'returns 403' do
put api("/admin/batched_background_migrations/#{migration.id}/resume", unauthorized_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end end

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectness, :aggregate_failures do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.first_owner }
let(:service) { described_class.new(project, user, { ref: 'master' }) }
let(:config) do
<<-YAML
stages:
- build
- test
- deploy
build:
stage: build
script: make build
test:
stage: test
trigger:
include: child.yml
deploy:
stage: deploy
script: make deploy
environment: review/$CI_JOB_NAME
YAML
end
let(:pipeline) { service.execute(:push).payload }
let(:current_partition_id) { 123 }
before do
stub_ci_pipeline_yaml_file(config)
stub_const(
'Gitlab::Ci::Pipeline::Chain::AssignPartition::DEFAULT_PARTITION_ID',
current_partition_id)
end
it 'assigns partition_id to pipeline' do
expect(pipeline).to be_created_successfully
expect(pipeline.partition_id).to eq(current_partition_id)
end
it 'assigns partition_id to stages' do
stage_partition_ids = pipeline.stages.map(&:partition_id).uniq
expect(stage_partition_ids).to eq([current_partition_id])
end
it 'assigns partition_id to processables' do
processables_partition_ids = pipeline.processables.map(&:partition_id).uniq
expect(processables_partition_ids).to eq([current_partition_id])
end
it 'assigns partition_id to metadata' do
metadata_partition_ids = pipeline.processables.map { |job| job.metadata.partition_id }.uniq
expect(metadata_partition_ids).to eq([current_partition_id])
end
it 'correctly assigns partition and environment' do
metadata = find_metadata('deploy')
expect(metadata.partition_id).to eq(current_partition_id)
expect(metadata.expanded_environment_name).to eq('review/deploy')
end
def find_metadata(name)
pipeline
.processables
.find { |job| job.name == name }
.metadata
end
end

View File

@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac
it 'returns variable list' do it 'returns variable list' do
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' }) expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' }) expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
expect(subject['KEY3']).to eq({ value: 'val 3', description: nil }) expect(subject['KEY3']).to eq({ value: 'val 3' })
expect(subject['KEY4']).to eq({ value: 'val 4', description: nil }) expect(subject['KEY4']).to eq({ value: 'val 4' })
end end
end end

View File

@ -34,6 +34,14 @@ RSpec.describe Ci::Pipelines::AddJobService do
).and change { job.metadata.project }.to(pipeline.project) ).and change { job.metadata.project }.to(pipeline.project)
end end
it 'assigns partition_id to job and metadata' do
pipeline.partition_id = 123
expect { execute }
.to change(job, :partition_id).to(pipeline.partition_id)
.and change { job.metadata.partition_id }.to(pipeline.partition_id)
end
it 'returns a service response with the job as payload' do it 'returns a service response with the job as payload' do
expect(execute).to be_success expect(execute).to be_success
expect(execute.payload[:job]).to eq(job) expect(execute.payload[:job]).to eq(job)

View File

@ -80,13 +80,13 @@ func (u *uploader) Consume(outerCtx context.Context, reader io.Reader, deadline
cr := &countReader{r: reader} cr := &countReader{r: reader}
if err := u.strategy.Upload(uploadCtx, cr); err != nil { if err := u.strategy.Upload(uploadCtx, cr); err != nil {
return cr.n, err return 0, err
} }
if u.checkETag { if u.checkETag {
if err := compareMD5(hexString(hasher), u.strategy.ETag()); err != nil { if err := compareMD5(hexString(hasher), u.strategy.ETag()); err != nil {
log.ContextLogger(uploadCtx).WithError(err).Error("error comparing MD5 checksum") log.ContextLogger(uploadCtx).WithError(err).Error("error comparing MD5 checksum")
return cr.n, err return 0, err
} }
} }