Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
704ed7ea39
commit
427dbb30f0
|
@ -49,8 +49,18 @@ export default {
|
|||
:class="getMenuSectionClasses(sectionIndex)"
|
||||
data-testid="menu-section"
|
||||
>
|
||||
<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-for="(menuItem, menuItemIndex) in menuItems"
|
||||
v-else
|
||||
:key="menuItem.id"
|
||||
:menu-item="menuItem"
|
||||
data-testid="menu-item"
|
||||
|
@ -58,6 +68,7 @@ export default {
|
|||
:class="{ 'gl-mt-1': menuItemIndex > 0 }"
|
||||
@click="onClick(menuItem)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -109,3 +109,7 @@ export const DEFAULT_FIELDS = [
|
|||
columnClass: 'gl-w-20p',
|
||||
},
|
||||
];
|
||||
|
||||
export const TRACKING_CATEGORIES = {
|
||||
index: 'pipelines_table_component',
|
||||
};
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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 ' \
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -48,7 +48,6 @@ tier:
|
|||
- premium
|
||||
- ultimate
|
||||
performance_indicator_type:
|
||||
- smau
|
||||
- gmau
|
||||
- paid_gmau
|
||||
milestone: "<13.9"
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
394f346e3a93f8a6b74fd0461eb59f569c6a18f90ae653c330a38e3a3706b5f6
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,9 +6,15 @@ module Gitlab
|
|||
def initialize
|
||||
@primary = []
|
||||
@secondary = []
|
||||
@last_header_added = nil
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def add_primary_menu_item(**args)
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -357,3 +357,8 @@
|
|||
category: manage
|
||||
aggregation: weekly
|
||||
expiry: 42
|
||||
# Environments page
|
||||
- name: users_visiting_environments_pages
|
||||
category: environments
|
||||
redis_slot: users
|
||||
aggregation: weekly
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
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]]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([[]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue