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)"
data-testid="menu-section"
>
<top-nav-menu-item
v-for="(menuItem, menuItemIndex) in menuItems"
:key="menuItem.id"
:menu-item="menuItem"
data-testid="menu-item"
class="gl-w-full"
:class="{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
/>
<template v-for="(menuItem, menuItemIndex) in menuItems">
<strong
v-if="menuItem.type == 'header'"
:key="menuItem.title"
class="gl-px-4 gl-py-2 gl-text-gray-900 gl-display-block"
:class="{ 'gl-pt-3!': menuItemIndex > 0 }"
data-testid="menu-header"
>
{{ 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>
</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 initCheckFormState from './check_form_state';
import initFormUpdate from './update_form';
function initTargetBranchSelector() {
const targetBranch = document.querySelector('.js-target-branch');
@ -68,5 +69,6 @@ function initTargetBranchSelector() {
}
initMergeRequest();
initFormUpdate();
initCheckFormState();
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"
@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 class="clearfix"></div>

View File

@ -10,6 +10,8 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import { TRACKING_CATEGORIES } from '../../constants';
export const i18n = {
downloadArtifacts: __('Download artifacts'),
@ -29,6 +31,7 @@ export default {
GlSearchBoxByType,
GlLoadingIcon,
},
mixins: [Tracking.mixin()],
inject: {
artifactsEndpoint: {
default: '',
@ -60,6 +63,10 @@ export default {
},
methods: {
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;
// Replace the placeholder with the ID of the pipeline we are viewing
const endpoint = this.artifactsEndpoint.replace(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import CiIcon from './ci_icon.vue';
/**
* 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';
},
},
methods: {
navigateToPipeline() {
visitUrl(this.detailsPath);
// event used for tracking
this.$emit('ciStatusBadgeClick');
},
},
};
</script>
<template>
<a
v-gl-tooltip
:href="detailsPath"
:class="cssClass"
class="gl-cursor-pointer"
:title="title"
data-qa-selector="status_badge_link"
@click="navigateToPipeline"
>
<ci-icon :status="status" :css-classes="iconClasses" />

View File

@ -11,6 +11,9 @@ html {
font-family: sans-serif;
line-height: 1.15;
}
header {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
@ -28,7 +31,8 @@ hr {
height: 0;
overflow: visible;
}
h1 {
h1,
h3 {
margin-top: 0;
margin-bottom: 0.25rem;
}
@ -49,26 +53,49 @@ img {
vertical-align: middle;
border-style: none;
}
svg {
overflow: hidden;
vertical-align: middle;
}
label {
display: inline-block;
margin-bottom: 0.5rem;
}
input {
button {
border-radius: 0;
}
input,
button {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button {
text-transform: none;
}
[role="button"] {
cursor: pointer;
}
button:not(:disabled),
[type="button"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
fieldset {
min-width: 0;
padding: 0;
@ -78,7 +105,8 @@ fieldset {
[hidden] {
display: none !important;
}
h1 {
h1,
h3 {
margin-bottom: 0.25rem;
font-weight: 600;
line-height: 1.2;
@ -87,6 +115,9 @@ h1 {
h1 {
font-size: 2.1875rem;
}
h3 {
font-size: 1.53125rem;
}
hr {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
@ -120,23 +151,42 @@ hr {
max-width: 1140px;
}
}
.col-sm-12,
.col {
.row {
display: flex;
flex-wrap: wrap;
margin-right: -15px;
margin-left: -15px;
}
.col-md-6,
.col-sm-12 {
position: relative;
width: 100%;
padding-right: 15px;
padding-left: 15px;
}
.col {
flex-basis: 0;
flex-grow: 1;
max-width: 100%;
.order-1 {
order: 1;
}
.order-12 {
order: 12;
}
@media (min-width: 576px) {
.col-sm-12 {
flex: 0 0 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 {
display: block;
@ -169,16 +219,6 @@ hr {
.form-group {
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 {
display: inline-block;
font-weight: 400;
@ -204,6 +244,137 @@ hr {
fieldset:disabled a.btn {
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 {
margin-top: 1rem !important;
}
@ -213,8 +384,8 @@ fieldset:disabled a.btn {
.text-nowrap {
white-space: nowrap !important;
}
.text-center {
text-align: center !important;
.font-weight-normal {
font-weight: 400 !important;
}
.gl-form-input,
.gl-form-input.form-control {
@ -251,13 +422,103 @@ fieldset:disabled a.btn {
.gl-form-input.form-control::placeholder {
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 {
display: inline-flex;
}
.gl-button:not(.btn-link):active {
text-decoration: none;
}
.gl-button.gl-button {
.gl-button.gl-button,
.gl-button.gl-button.btn-block {
border-width: 0;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -273,7 +534,8 @@ fieldset:disabled a.btn {
font-size: 0.875rem;
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;
text-overflow: ellipsis;
white-space: nowrap;
@ -282,29 +544,39 @@ fieldset:disabled a.btn {
margin-top: -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;
width: 1rem;
flex-shrink: 0;
margin-right: 0.25rem;
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;
}
.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;
outline: none;
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;
}
.gl-button.gl-button.btn-confirm {
.gl-button.gl-button.btn-confirm,
.gl-button.gl-button.btn-block.btn-confirm {
background-color: #1f75cb;
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;
outline: none;
background-color: #0b5cad;
@ -312,10 +584,14 @@ fieldset:disabled a.btn {
body {
font-size: 0.875rem;
}
[type="submit"] {
button,
html [type="button"],
[type="submit"],
[role="button"] {
cursor: pointer;
}
h1 {
h1,
h3 {
margin-top: 20px;
margin-bottom: 10px;
}
@ -325,6 +601,9 @@ a {
hr {
overflow: hidden;
}
svg {
vertical-align: baseline;
}
.form-control {
font-size: 0.875rem;
}
@ -332,9 +611,6 @@ hr {
display: none !important;
visibility: hidden !important;
}
.hide {
display: none;
}
html {
overflow-y: scroll;
}
@ -372,13 +648,34 @@ body.navless {
background-color: #f0f0f0;
box-shadow: none;
}
.btn:active {
.btn:active,
.btn.active {
background-color: #eaeaea;
border-color: #e3e3e3;
color: #303030;
}
.light {
color: #303030;
.btn svg {
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 {
margin: 1.5rem 0;
@ -416,6 +713,9 @@ input {
label {
font-weight: 600;
}
label.custom-control-label {
font-weight: 400;
}
label.label-bold {
font-weight: 600;
}
@ -429,8 +729,25 @@ label.label-bold {
.gl-show-field-errors .form-control:not(textarea) {
height: 34px;
}
.gl-show-field-errors .gl-field-hint {
color: #303030;
.navbar-empty {
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 {
color: #868686;
@ -442,6 +759,9 @@ input::-ms-input-placeholder {
input:-ms-input-placeholder {
color: #868686;
}
svg {
fill: currentColor;
}
.login-page .container {
max-width: 960px;
}
@ -649,14 +969,17 @@ input:-ms-input-placeholder {
}
}
.gl-text-green-600 {
color: #217645;
.gl-display-flex {
display: flex;
}
.gl-text-red-500 {
color: #dd2b0e;
.gl-display-inline-block {
display: inline-block;
}
.gl-display-block {
display: block;
.gl-flex-wrap {
flex-wrap: wrap;
}
.gl-float-right {
float: right;
}
.gl-w-10 {
width: 3.5rem;
@ -675,14 +998,18 @@ input:-ms-input-placeholder {
width: 100%;
}
}
.gl-p-4 {
padding: 0.75rem;
.gl-p-5 {
padding: 1rem;
}
.gl-px-5 {
padding-left: 1rem;
padding-right: 1rem;
}
.gl-pt-5 {
padding-top: 1rem;
}
.gl-mt-2 {
margin-top: 0.25rem;
.gl-mt-3 {
margin-top: 0.5rem;
}
.gl-mt-5 {
margin-top: 1rem;
@ -702,15 +1029,17 @@ input:-ms-input-placeholder {
.gl-mb-3 {
margin-bottom: 0.5rem;
}
.gl-mb-5 {
margin-bottom: 1rem;
}
.gl-ml-auto {
margin-left: auto;
}
.gl-ml-2 {
margin-left: 0.25rem;
}
@media (min-width: 576px) {
.gl-sm-mt-0 {
margin-top: 0;
}
}
.gl-text-center {
text-align: center;
}
@ -720,6 +1049,9 @@ input:-ms-input-placeholder {
.gl-font-weight-normal {
font-weight: 400;
}
.gl-font-weight-bold {
font-weight: 600;
}
@import "startup/cloaking";
@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.
include MetricsDashboard
include ProductAnalyticsTracking
layout 'project'
@ -26,6 +27,18 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
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
urgency :low

View File

@ -48,6 +48,13 @@ module Nav
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:)
if current_user
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`
if explore_nav_link?(:projects)
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
href: explore_root_path,
active: nav == 'project' || active_nav_link?(path: %w[dashboard#show root#show projects#trending projects#starred projects#index]),
**projects_menu_item_attrs
@ -68,6 +76,7 @@ module Nav
if explore_nav_link?(:groups)
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
href: explore_groups_path,
active: nav == 'group' || active_nav_link?(controller: [:groups, 'groups/milestones', 'groups/group_members']),
**groups_menu_item_attrs
@ -76,6 +85,7 @@ module Nav
if explore_nav_link?(:snippets)
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
active: active_nav_link?(controller: :snippets),
href: explore_snippets_path,
**snippets_menu_item_attrs
@ -89,6 +99,7 @@ module Nav
current_item = project ? current_project(project: project) : {}
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]),
css_class: 'qa-projects-dropdown',
data: { track_label: "projects_dropdown", track_action: "click_dropdown" },
@ -103,6 +114,7 @@ module Nav
current_item = group ? current_group(group: group) : {}
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]),
css_class: 'qa-groups-dropdown',
data: { track_label: "groups_dropdown", track_action: "click_dropdown" },
@ -116,6 +128,7 @@ module Nav
if dashboard_nav_link?(:milestones)
builder.add_primary_menu_item_with_shortcut(
id: 'milestones',
header: top_nav_localized_headers[:explore],
title: _('Milestones'),
href: dashboard_milestones_path,
active: active_nav_link?(controller: 'dashboard/milestones'),
@ -127,6 +140,7 @@ module Nav
if dashboard_nav_link?(:snippets)
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:explore],
active: active_nav_link?(controller: 'dashboard/snippets'),
data: { qa_selector: 'snippets_link', **menu_data_tracking_attrs('snippets') },
href: dashboard_snippets_path,
@ -137,6 +151,7 @@ module Nav
if dashboard_nav_link?(:activity)
builder.add_primary_menu_item_with_shortcut(
id: 'activity',
header: top_nav_localized_headers[:explore],
title: _('Activity'),
href: activity_dashboard_path,
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[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 :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
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)
end
def has_reports?(reports_scope)
def complete_and_has_reports?(reports_scope)
if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
latest_report_builds(reports_scope).exists?
else
@ -1090,7 +1090,7 @@ module Ci
end
def can_generate_codequality_reports?
has_reports?(Ci::JobArtifact.of_report_type(:codequality))
complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end
def test_report_summary
@ -1313,7 +1313,7 @@ module Ci
def has_test_reports?
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

View File

@ -8,15 +8,12 @@ module Ci
include ChronicDurationAttribute
include FromUnion
include TokenAuthenticatable
include IgnorableColumns
include FeatureGate
include Gitlab::Utils::StrongMemoize
include TaggableQueries
include Presentable
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?
enum access_level: {

View File

@ -16,14 +16,10 @@ module Clusters
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
include UsageStatistics
include IgnorableColumns
default_value_for :ingress_type, :nginx
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: {
nginx: 1
}

View File

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

View File

@ -1566,7 +1566,7 @@ class MergeRequest < ApplicationRecord
end
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
def predefined_variables
@ -1596,7 +1596,7 @@ class MergeRequest < ApplicationRecord
end
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
def has_coverage_reports?
@ -1604,7 +1604,7 @@ class MergeRequest < ApplicationRecord
end
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
def compare_accessibility_reports
@ -1644,7 +1644,7 @@ class MergeRequest < ApplicationRecord
end
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
def compare_codequality_reports
@ -1694,11 +1694,11 @@ class MergeRequest < ApplicationRecord
end
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
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
def compare_sast_reports(current_user)

View File

@ -51,8 +51,6 @@ class Project < ApplicationRecord
BoardLimitExceeded = 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'
STATISTICS_ATTRIBUTE = 'repositories_count'

View File

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

View File

@ -10,7 +10,7 @@ module Ci
def execute(pipeline)
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))
end
end

View File

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

View File

@ -5,20 +5,20 @@ module Issues
include Gitlab::Routing.url_helpers
include GitlabRoutingHelper
def initialize(issuables_relation, project)
super
def initialize(issuables_relation, project, user = nil)
super(issuables_relation, project)
@labels = @issuables.labels_hash.transform_values { |labels| labels.sort.join(',').presence }
end
def email(user)
Notify.issues_csv_email(user, project, csv_data, csv_builder.status).deliver_now
def email(mail_to_user)
Notify.issues_csv_email(mail_to_user, project, csv_data, csv_builder.status).deliver_now
end
private
def associations_to_preload
%i(author assignees timelogs milestone project)
[:author, :assignees, :timelogs, :milestone, { project: { namespace: :route } }]
end
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),
suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: 'suite', format: :json),
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'),
artifacts_expired_image_path: image_path('illustrations/pipeline.svg') } }
= 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)
.form-check.gl-mb-3
= 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
= _("Delete source branch when merge request is accepted.")
- 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'
- else
= 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
= _("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'

View File

@ -127,3 +127,4 @@ end
Sidekiq::Scheduled::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
require 'wikicloth'
@ -20,7 +44,10 @@ require 'digest/sha2'
# - 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
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 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/RedundantRegexpCharacterClass
# 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
class WikiCloth
def render(opt={})
@ -218,3 +251,9 @@ end
# rubocop:enable Style/RegexpLiteralMixedPreserve
# rubocop:enable Style/RedundantRegexpCharacterClass
# 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
- ultimate
performance_indicator_type:
- smau
- gmau
- paid_gmau
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
instrumentation_class: CountUserAuthMetric
data_category: optional
performance_indicator_type: []
performance_indicator_type:
- smau
distribution:
- ce
- 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,
encrypted_url_variables 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

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:
- `backup_upload_remote_directory`: 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_connection.local_root`: mounted directory that backups are copied to.
- `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 `.`.
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
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.
<!-- ## Troubleshooting

View File

@ -27,9 +27,33 @@ module API
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
helpers do
def batched_background_migration
@batched_background_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.find(params[:id])
end
def base_model
database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
@base_model ||= Gitlab::Database.database_base_models[database]

View File

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

View File

@ -30,7 +30,7 @@ module Gitlab
end
def value_with_data
{ value: @config.to_s, description: nil }
{ value: @config.to_s }
end
end
@ -66,7 +66,7 @@ module Gitlab
end
def value_with_data
{ value: value, description: config_description }
{ value: value, description: config_description }.compact
end
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,
tag: @pipeline.tag,
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

View File

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

View File

@ -8,6 +8,7 @@ module Gitlab
BATCH_CLASS_MODULE = "#{JOB_CLASS_MODULE}::BatchingStrategies"
MAXIMUM_FAILED_RATIO = 0.5
MINIMUM_JOBS = 50
FINISHED_PROGRESS_VALUE = 100
self.table_name = :batched_background_migrations
@ -232,7 +233,15 @@ module Gitlab
"BatchedMigration[id: #{id}]"
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
return FINISHED_PROGRESS_VALUE if finished?
return unless total_tuple_count.to_i > 0
100 * migrated_tuple_count / total_tuple_count

View File

@ -6,9 +6,15 @@ module Gitlab
def initialize
@primary = []
@secondary = []
@last_header_added = nil
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)
end
@ -30,6 +36,12 @@ module Gitlab
dest.push(item)
end
def add_menu_header(dest:, **args)
header = ::Gitlab::Nav::TopNavMenuHeader.build(**args)
dest.push(header)
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)
{
id: id,
type: :item,
title: title,
active: active,
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
aggregation: weekly
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"
msgstr ""
msgid "TopNav|Explore"
msgstr ""
msgid "TopNav|Go back"
msgstr ""
msgid "TopNav|Switch to"
msgstr ""
msgid "TopNav|Your dashboard"
msgstr ""
msgid "Topic %{source_topic} was successfully merged into topic %{target_topic}."
msgstr ""

View File

@ -21,6 +21,10 @@ module QA
base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do
element :file_upload_field
end
base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do
element :wiki_hidden_content
end
end
def add_heading(heading, text)
@ -41,6 +45,13 @@ module QA
text_area.send_keys(:return)
find_element(:file_upload_field, visible: false).send_keys(image_path)
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
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_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_OLD_HTML = path.join(FIXTURES_ROOT, 'startup_css/sign-in-old.html');
const PATH_ASSETS = path.join(ROOT, 'tmp/startup_css_assets');
const PATH_STARTUP_SCSS = path.join(ROOT_RAILS, 'app/assets/stylesheets/startup');
@ -80,7 +81,7 @@ const OUTPUTS = [
}),
{
outFile: 'startup-signin',
htmlPaths: [PATH_SIGNIN_HTML],
htmlPaths: [PATH_SIGNIN_HTML, PATH_SIGNIN_OLD_HTML],
cssKeys: [APPLICATION_CSS_PREFIX, UTILITIES_CSS_PREFIX],
purgeOptions: {
safelist: {

View File

@ -32,6 +32,11 @@ RSpec.describe Projects::EnvironmentsController do
get :index, params: environment_params
end
it_behaves_like 'tracking unique visits', :index do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
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 render_template 'folder'
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
context 'when using JSON format' do
@ -197,6 +214,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to be_ok
end
it_behaves_like 'tracking unique visits', :show do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
context 'with invalid id' do
@ -210,12 +232,30 @@ RSpec.describe Projects::EnvironmentsController do
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
it 'responds with a status code 200' do
get :edit, params: environment_params
expect(response).to be_ok
end
it_behaves_like 'tracking unique visits', :edit do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
describe 'PATCH #update' do
@ -230,6 +270,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{environment.id}")
end
it_behaves_like 'tracking unique visits', :update do
let(:request_params) { params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
context "when environment params are invalid" do
@ -294,6 +339,11 @@ RSpec.describe Projects::EnvironmentsController do
{ 'redirect_url' =>
project_environment_url(project, environment) })
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
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 '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
let(:user) { reporter }
@ -357,6 +412,11 @@ RSpec.describe Projects::EnvironmentsController do
get :terminal, params: environment_params
end
it_behaves_like 'tracking unique visits', :terminal do
let(:request_params) { environment_params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
context 'with invalid id' do
@ -859,6 +919,11 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['path']).to eq("/#{project.full_path}/-/environments/#{json_response['environment']['id']}")
end
it_behaves_like 'tracking unique visits', :create do
let(:request_params) { params }
let(:target_id) { 'users_visiting_environments_pages' }
end
end
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'
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
get :new
expect(response).to be_successful
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

View File

@ -4,11 +4,20 @@ import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
const TEST_SECTIONS = [
{
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',
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) =>
parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({
menuItem: x.props('menuItem'),
classes: x.classes(),
}));
parent.findAll('[data-testid="menu-header"],[data-testid="menu-item"]').wrappers.map((x) => {
return {
menuItem: x.vm
? {
type: 'item',
...x.props('menuItem'),
}
: {
type: 'header',
title: x.text(),
},
classes: x.classes(),
};
});
const findSectionModels = () =>
wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({
classes: x.classes(),
@ -45,32 +64,31 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
});
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([
{
classes: [],
menuItems: [
{
menuItem: TEST_SECTIONS[0].menuItems[0],
classes: ['gl-w-full'],
},
...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
menuItems: TEST_SECTIONS[0].menuItems.map((menuItem, index) => {
const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
return {
menuItem,
classes: ['gl-w-full', 'gl-mt-1'],
})),
],
classes,
};
}),
},
{
classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
menuItems: [
{
menuItem: TEST_SECTIONS[1].menuItems[0],
classes: ['gl-w-full'],
},
...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
return {
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');
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 MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
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';
describe('Pipelines filtered search', () => {
@ -177,4 +179,20 @@ describe('Pipelines filtered search', () => {
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 { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineMultiActions, {
i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
describe('Pipeline Multi Actions Dropdown', () => {
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 PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.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';
const projectPath = 'test/test';
describe('Pipeline Url Component', () => {
let wrapper;
let trackingSpy;
const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell');
const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link');
@ -14,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha');
const findCommitIcon = () => wrapper.findByTestId('commit-icon');
const findCommitIconType = () => wrapper.findByTestId('commit-icon-type');
const findCommitRefName = () => wrapper.findByTestId('commit-ref-name');
const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container');
const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]');
@ -31,7 +35,6 @@ describe('Pipeline Url Component', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
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', () => {
createComponent({}, true);
createComponent();
const commitWrapper = findCommitTitleContainer();
@ -83,7 +86,7 @@ describe('Pipeline Url Component', () => {
});
it('should render commit icon tooltip', () => {
createComponent({}, true);
createComponent();
expect(findCommitIcon().attributes('title')).toBe('Commit');
});
@ -94,8 +97,68 @@ describe('Pipeline Url Component', () => {
${mockPipelineBranch()} | ${'Branch'}
${mockPipeline()} | ${'Merge Request'}
`('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => {
createComponent(pipeline, true);
createComponent(pipeline);
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 MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
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 PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { TRACKING_CATEGORIES } from '~/pipelines/constants';
jest.mock('~/flash');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => {
@ -96,6 +98,22 @@ describe('Pipelines Actions dropdown', () => {
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', () => {

View File

@ -2,6 +2,7 @@ import '~/commons';
import { GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
@ -13,6 +14,7 @@ import {
PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
import eventHub from '~/pipelines/event_hub';
@ -23,6 +25,7 @@ jest.mock('~/pipelines/event_hub');
describe('Pipelines Table', () => {
let pipeline;
let wrapper;
let trackingSpy;
const defaultProps = {
pipelines: [],
@ -69,6 +72,7 @@ describe('Pipelines Table', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
@ -96,10 +100,6 @@ describe('Pipelines Table', () => {
it('should render a status badge', () => {
expect(findStatusBadge().exists()).toBe(true);
});
it('should render status badge with correct path', () => {
expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
});
});
describe('pipeline cell', () => {
@ -167,5 +167,39 @@ describe('Pipelines Table', () => {
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 CiBadge from '~/vue_shared/components/ci_badge_link.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', () => {
let wrapper;
@ -79,17 +84,20 @@ describe('CI Badge Link Component', () => {
afterEach(() => {
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] });
expect(wrapper.attributes('href')).toBe(statuses[status].details_path);
expect(wrapper.attributes('href')).toBe();
expect(wrapper.text()).toBe(statuses[status].text);
expect(wrapper.classes()).toContain('ci-status');
expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`);
expect(findIcon().exists()).toBe(true);
await wrapper.trigger('click');
expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path);
});
it('should not render label', () => {
@ -97,4 +105,12 @@ describe('CI Badge Link Component', () => {
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
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = [
{ href: '/explore', icon: 'project', id: 'project', title: 'Projects' },
{ href: '/explore/groups', icon: 'group', id: 'groups', title: 'Groups' },
@ -60,7 +63,7 @@ RSpec.describe Nav::TopNavHelper do
::Gitlab::Nav::TopNavMenuItem.build(**item)
end
expect(subject[:primary]).to eq(expected_primary)
expect(subject[:primary]).to eq([expected_header, *expected_primary])
end
it 'has expected :shortcuts' do
@ -117,6 +120,9 @@ RSpec.describe Nav::TopNavHelper do
let(:projects_view) { subject[:views][:projects] }
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-projects-dropdown',
data: {
@ -128,7 +134,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Projects',
view: 'projects'
)
expect(subject[:primary]).to eq([expected_primary])
expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@ -253,6 +259,9 @@ RSpec.describe Nav::TopNavHelper do
let(:groups_view) { subject[:views][:groups] }
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Switch to'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
css_class: 'qa-groups-dropdown',
data: {
@ -264,7 +273,7 @@ RSpec.describe Nav::TopNavHelper do
title: 'Groups',
view: 'groups'
)
expect(subject[:primary]).to eq([expected_primary])
expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@ -376,6 +385,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_milestones) { true }
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'milestones_link',
@ -386,7 +398,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'milestones',
title: 'Milestones'
)
expect(subject[:primary]).to eq([expected_primary])
expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@ -404,6 +416,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_snippets) { true }
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'snippets_link',
@ -414,7 +429,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'snippets',
title: 'Snippets'
)
expect(subject[:primary]).to eq([expected_primary])
expect(subject[:primary]).to eq([expected_header, expected_primary])
end
it 'has expected :shortcuts' do
@ -432,6 +447,9 @@ RSpec.describe Nav::TopNavHelper do
let(:with_activity) { true }
it 'has expected :primary' do
expected_header = ::Gitlab::Nav::TopNavMenuHeader.build(
title: 'Explore'
)
expected_primary = ::Gitlab::Nav::TopNavMenuItem.build(
data: {
qa_selector: 'activity_link',
@ -442,7 +460,7 @@ RSpec.describe Nav::TopNavHelper do
id: 'activity',
title: 'Activity'
)
expect(subject[:primary]).to eq([expected_primary])
expect(subject[:primary]).to eq([expected_header, expected_primary])
end
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),
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
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'),
artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'),
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
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: nil },
'VARIABLE_2' => { value: 'value 2', description: nil }
'VARIABLE_1' => { value: 'value 1' },
'VARIABLE_2' => { value: 'value 2' }
)
end
end
@ -125,7 +125,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => { value: 'value 2', description: nil }
'VARIABLE_2' => { value: 'value 2' }
)
end
end

View File

@ -204,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do
describe '#value_with_data' do
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

View File

@ -61,8 +61,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
describe '#value_with_data' do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: nil },
'VARIABLE_2' => { value: 'value 2', description: nil }
'VARIABLE_1' => { value: 'value 1' },
'VARIABLE_2' => { value: 'value 2' }
)
end
end
@ -119,7 +119,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do
it 'returns variable with data' do
expect(entry.value_with_data).to eq(
'VARIABLE_1' => { value: 'value 1', description: 'variable 1' },
'VARIABLE_2' => { value: 'value 2', description: nil }
'VARIABLE_2' => { value: 'value 2' }
)
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
subject { migration.progress }
context 'when the migration is finished' do
context 'when the migration is completed' do
let(:migration) do
create(:batched_background_migration, :finished, total_tuple_count: 1).tap do |record|
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
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
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'
}
expect(described_class.build(**item)).to eq(item)
expect(described_class.build(**item)).to eq(item.merge(type: :item))
end
end
end

View File

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

View File

@ -75,4 +75,56 @@ RSpec.describe API::Admin::BatchedBackgroundMigrations do
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

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
expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' })
expect(subject['KEY2']).to eq({ value: 'val 2', description: '' })
expect(subject['KEY3']).to eq({ value: 'val 3', description: nil })
expect(subject['KEY4']).to eq({ value: 'val 4', description: nil })
expect(subject['KEY3']).to eq({ value: 'val 3' })
expect(subject['KEY4']).to eq({ value: 'val 4' })
end
end

View File

@ -34,6 +34,14 @@ RSpec.describe Ci::Pipelines::AddJobService do
).and change { job.metadata.project }.to(pipeline.project)
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
expect(execute).to be_success
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}
if err := u.strategy.Upload(uploadCtx, cr); err != nil {
return cr.n, err
return 0, err
}
if u.checkETag {
if err := compareMD5(hexString(hasher), u.strategy.ETag()); err != nil {
log.ContextLogger(uploadCtx).WithError(err).Error("error comparing MD5 checksum")
return cr.n, err
return 0, err
}
}