Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
da59ce8b21
commit
beab869416
|
|
@ -214,7 +214,7 @@ export default {
|
|||
<gl-form-checkbox
|
||||
v-model="issueTransitionEnabled"
|
||||
:disabled="isInheriting"
|
||||
data-qa-selector="service_jira_issue_transition_enabled"
|
||||
data-qa-selector="service_jira_issue_transition_enabled_checkbox"
|
||||
>
|
||||
{{ s__('JiraService|Enable Jira transitions') }}
|
||||
</gl-form-checkbox>
|
||||
|
|
@ -232,7 +232,7 @@ export default {
|
|||
name="service[jira_issue_transition_automatic]"
|
||||
:value="issueTransitionOption.value"
|
||||
:disabled="isInheriting"
|
||||
:data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"
|
||||
:data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`"
|
||||
>
|
||||
{{ issueTransitionOption.label }}
|
||||
|
||||
|
|
|
|||
|
|
@ -254,6 +254,37 @@ export const timeIntervalInWords = (intervalInSeconds) => {
|
|||
: secondsText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Similar to `timeIntervalInWords`, but rounds the return value
|
||||
* to 1/10th of the largest time unit. For example:
|
||||
*
|
||||
* 30 => 30 seconds
|
||||
* 90 => 1.5 minutes
|
||||
* 7200 => 2 hours
|
||||
* 86400 => 1 day
|
||||
* ... etc.
|
||||
*
|
||||
* The largest supported unit is "days".
|
||||
*
|
||||
* @param {Number} intervalInSeconds The time interval in seconds
|
||||
* @returns {String} A humanized description of the time interval
|
||||
*/
|
||||
export const humanizeTimeInterval = (intervalInSeconds) => {
|
||||
if (intervalInSeconds < 60 /* = 1 minute */) {
|
||||
const seconds = Math.round(intervalInSeconds * 10) / 10;
|
||||
return n__('%d second', '%d seconds', seconds);
|
||||
} else if (intervalInSeconds < 3600 /* = 1 hour */) {
|
||||
const minutes = Math.round(intervalInSeconds / 6) / 10;
|
||||
return n__('%d minute', '%d minutes', minutes);
|
||||
} else if (intervalInSeconds < 86400 /* = 1 day */) {
|
||||
const hours = Math.round(intervalInSeconds / 360) / 10;
|
||||
return n__('%d hour', '%d hours', hours);
|
||||
}
|
||||
|
||||
const days = Math.round(intervalInSeconds / 8640) / 10;
|
||||
return n__('%d day', '%d days', days);
|
||||
};
|
||||
|
||||
export const dateInWords = (date, abbreviated = false, hideYear = false) => {
|
||||
if (!date) return date;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
<script>
|
||||
import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
name: 'LockPopovers',
|
||||
components: {
|
||||
GlPopover,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
targets: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.targets = [...document.querySelectorAll('.js-cascading-settings-lock-popover-target')].map(
|
||||
(el) => {
|
||||
const {
|
||||
dataset: { popoverData },
|
||||
} = el;
|
||||
|
||||
const {
|
||||
lockedByAncestor,
|
||||
lockedByApplicationSetting,
|
||||
ancestorNamespace,
|
||||
} = convertObjectPropsToCamelCase(JSON.parse(popoverData || '{}'), { deep: true });
|
||||
|
||||
return {
|
||||
el,
|
||||
lockedByAncestor,
|
||||
lockedByApplicationSetting,
|
||||
ancestorNamespace,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<template
|
||||
v-for="(
|
||||
{ el, lockedByApplicationSetting, lockedByAncestor, ancestorNamespace }, index
|
||||
) in targets"
|
||||
>
|
||||
<gl-popover
|
||||
v-if="lockedByApplicationSetting || lockedByAncestor"
|
||||
:key="index"
|
||||
:target="el"
|
||||
placement="top"
|
||||
>
|
||||
<template #title>{{ s__('CascadingSettings|Setting enforced') }}</template>
|
||||
<p data-testid="cascading-settings-lock-popover">
|
||||
<template v-if="lockedByApplicationSetting">{{
|
||||
s__('CascadingSettings|This setting has been enforced by an instance admin.')
|
||||
}}</template>
|
||||
|
||||
<gl-sprintf
|
||||
v-else-if="lockedByAncestor && ancestorNamespace"
|
||||
:message="
|
||||
s__('CascadingSettings|This setting has been enforced by an owner of %{link}.')
|
||||
"
|
||||
>
|
||||
<template #link>
|
||||
<gl-link :href="ancestorNamespace.path" class="gl-font-sm">{{
|
||||
ancestorNamespace.fullName
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
</gl-popover>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import Vue from 'vue';
|
||||
import LockPopovers from './components/lock_popovers.vue';
|
||||
|
||||
export const initCascadingSettingsLockPopovers = () => {
|
||||
const el = document.querySelector('.js-cascading-settings-lock-popovers');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(LockPopovers);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
|
|||
import initFilePickers from '~/file_pickers';
|
||||
import TransferDropdown from '~/groups/transfer_dropdown';
|
||||
import groupsSelect from '~/groups_select';
|
||||
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
|
||||
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
|
||||
import projectSelect from '~/project_select';
|
||||
import initSearchSettings from '~/search_settings';
|
||||
|
|
@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
projectSelect();
|
||||
|
||||
initSearchSettings();
|
||||
initCascadingSettingsLockPopovers();
|
||||
|
||||
return new TransferDropdown();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
hoveredJobName: '',
|
||||
hoveredSourceJobName: '',
|
||||
highlightedJobs: [],
|
||||
measurements: {
|
||||
width: 0,
|
||||
|
|
@ -93,6 +94,9 @@ export default {
|
|||
shouldHideLinks() {
|
||||
return this.isStageView;
|
||||
},
|
||||
shouldShowStageName() {
|
||||
return !this.isStageView;
|
||||
},
|
||||
// The show downstream check prevents showing redundant linked columns
|
||||
showDownstreamPipelines() {
|
||||
return (
|
||||
|
|
@ -148,6 +152,9 @@ export default {
|
|||
setJob(jobName) {
|
||||
this.hoveredJobName = jobName;
|
||||
},
|
||||
setSourceJob(jobName) {
|
||||
this.hoveredSourceJobName = jobName;
|
||||
},
|
||||
slidePipelineContainer() {
|
||||
this.$refs.mainPipelineContainer.scrollBy({
|
||||
left: ONE_COL_WIDTH,
|
||||
|
|
@ -204,11 +211,13 @@ export default {
|
|||
<stage-column-component
|
||||
v-for="column in layout"
|
||||
:key="column.id || column.name"
|
||||
:title="column.name"
|
||||
:name="column.name"
|
||||
:groups="column.groups"
|
||||
:action="column.status.action"
|
||||
:highlighted-jobs="highlightedJobs"
|
||||
:show-stage-name="shouldShowStageName"
|
||||
:job-hovered="hoveredJobName"
|
||||
:source-job-hovered="hoveredSourceJobName"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
:pipeline-id="pipeline.id"
|
||||
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
|
||||
|
|
@ -227,7 +236,7 @@ export default {
|
|||
:column-title="__('Downstream')"
|
||||
:type="$options.pipelineTypeConstants.DOWNSTREAM"
|
||||
:view-type="viewType"
|
||||
@downstreamHovered="setJob"
|
||||
@downstreamHovered="setSourceJob"
|
||||
@pipelineExpandToggle="togglePipelineExpanded"
|
||||
@scrollContainer="slidePipelineContainer"
|
||||
@error="onError"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import JobItem from './job_item.vue';
|
||||
|
||||
|
|
@ -11,12 +9,8 @@ import JobItem from './job_item.vue';
|
|||
*
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
components: {
|
||||
JobItem,
|
||||
CiIcon,
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
|
|
@ -28,6 +22,11 @@ export default {
|
|||
required: false,
|
||||
default: -1,
|
||||
},
|
||||
stageName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedJobId() {
|
||||
|
|
@ -51,22 +50,21 @@ export default {
|
|||
<template>
|
||||
<div :id="computedJobId" class="ci-job-dropdown-container dropdown dropright">
|
||||
<button
|
||||
v-gl-tooltip.hover="{ boundary: 'viewport' }"
|
||||
:title="tooltipText"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
data-display="static"
|
||||
class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
|
||||
<span class="gl-display-flex gl-align-items-center gl-min-w-0">
|
||||
<ci-icon :status="group.status" :size="24" class="gl-line-height-0" />
|
||||
<span class="gl-text-truncate mw-70p gl-pl-3">
|
||||
{{ group.name }}
|
||||
</span>
|
||||
</span>
|
||||
<job-item
|
||||
:dropdown-length="group.size"
|
||||
:group-tooltip="tooltipText"
|
||||
:job="group"
|
||||
:stage-name="stageName"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
||||
<span class="gl-font-weight-100 gl-font-size-lg"> {{ group.size }} </span>
|
||||
<div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4">{{ group.size }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui';
|
|||
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
|
||||
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||
import { sprintf } from '~/locale';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import { reportToSentry } from '../../utils';
|
||||
import ActionComponent from '../jobs_shared/action_component.vue';
|
||||
import JobNameComponent from '../jobs_shared/job_name_component.vue';
|
||||
|
|
@ -38,6 +39,7 @@ export default {
|
|||
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
|
||||
components: {
|
||||
ActionComponent,
|
||||
CiIcon,
|
||||
JobNameComponent,
|
||||
GlLink,
|
||||
},
|
||||
|
|
@ -65,6 +67,11 @@ export default {
|
|||
required: false,
|
||||
default: Infinity,
|
||||
},
|
||||
groupTooltip: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
jobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -80,24 +87,47 @@ export default {
|
|||
required: false,
|
||||
default: -1,
|
||||
},
|
||||
sourceJobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
stageName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
boundary() {
|
||||
return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
|
||||
},
|
||||
computedJobId() {
|
||||
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
|
||||
},
|
||||
detailsPath() {
|
||||
return accessValue(this.dataMethod, 'detailsPath', this.status);
|
||||
},
|
||||
hasDetails() {
|
||||
return accessValue(this.dataMethod, 'hasDetails', this.status);
|
||||
},
|
||||
computedJobId() {
|
||||
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
|
||||
nameComponent() {
|
||||
return this.hasDetails ? 'gl-link' : 'div';
|
||||
},
|
||||
showStageName() {
|
||||
return Boolean(this.stageName);
|
||||
},
|
||||
status() {
|
||||
return this.job && this.job.status ? this.job.status : {};
|
||||
},
|
||||
testId() {
|
||||
return this.hasDetails ? 'job-with-link' : 'job-without-link';
|
||||
},
|
||||
tooltipText() {
|
||||
if (this.groupTooltip) {
|
||||
return this.groupTooltip;
|
||||
}
|
||||
|
||||
const textBuilder = [];
|
||||
const { name: jobName } = this.job;
|
||||
|
||||
|
|
@ -129,7 +159,7 @@ export default {
|
|||
return this.job.status && this.job.status.action && this.job.status.action.path;
|
||||
},
|
||||
relatedDownstreamHovered() {
|
||||
return this.job.name === this.jobHovered;
|
||||
return this.job.name === this.sourceJobHovered;
|
||||
},
|
||||
relatedDownstreamExpanded() {
|
||||
return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded;
|
||||
|
|
@ -156,44 +186,45 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
:id="computedJobId"
|
||||
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
|
||||
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between gl-w-full"
|
||||
data-qa-selector="job_item_container"
|
||||
>
|
||||
<gl-link
|
||||
v-if="hasDetails"
|
||||
<component
|
||||
:is="nameComponent"
|
||||
v-gl-tooltip="{
|
||||
boundary: 'viewport',
|
||||
placement: 'bottom',
|
||||
customClass: 'gl-pointer-events-none',
|
||||
}"
|
||||
:href="detailsPath"
|
||||
:title="tooltipText"
|
||||
:class="jobClasses"
|
||||
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none"
|
||||
data-testid="job-with-link"
|
||||
:href="detailsPath"
|
||||
class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
|
||||
:data-testid="testId"
|
||||
@click.stop="hideTooltips"
|
||||
@mouseout="hideTooltips"
|
||||
>
|
||||
<job-name-component :name="job.name" :status="job.status" :icon-size="24" />
|
||||
</gl-link>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
|
||||
:title="tooltipText"
|
||||
:class="jobClasses"
|
||||
class="js-job-component-tooltip non-details-job-component menu-item"
|
||||
data-testid="job-without-link"
|
||||
@mouseout="hideTooltips"
|
||||
>
|
||||
<job-name-component :name="job.name" :status="job.status" :icon-size="24" />
|
||||
</div>
|
||||
<div class="ci-job-name-component gl-display-flex gl-align-items-center">
|
||||
<ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
|
||||
<div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full">
|
||||
<div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div>
|
||||
<div
|
||||
v-if="showStageName"
|
||||
data-testid="stage-name-in-job"
|
||||
class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal"
|
||||
>
|
||||
{{ stageName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<action-component
|
||||
v-if="hasAction"
|
||||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
class="gl-mr-1"
|
||||
data-qa-selector="action_button"
|
||||
@pipelineActionRequestComplete="pipelineActionRequestComplete"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
pipelineId: {
|
||||
type: Number,
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
pipelineId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
|
|
@ -50,6 +50,16 @@ export default {
|
|||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
showStageName: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
sourceJobHovered: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
titleClasses: [
|
||||
'gl-font-weight-bold',
|
||||
|
|
@ -75,7 +85,7 @@ export default {
|
|||
});
|
||||
},
|
||||
formattedTitle() {
|
||||
return capitalize(escape(this.title));
|
||||
return capitalize(escape(this.name));
|
||||
},
|
||||
hasAction() {
|
||||
return !isEmpty(this.action);
|
||||
|
|
@ -145,14 +155,20 @@ export default {
|
|||
v-if="singleJobExists(group)"
|
||||
:job="group.jobs[0]"
|
||||
:job-hovered="jobHovered"
|
||||
:source-job-hovered="sourceJobHovered"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
:pipeline-id="pipelineId"
|
||||
:stage-name="showStageName ? group.stageName : ''"
|
||||
css-class-job-name="gl-build-content"
|
||||
:class="{ 'gl-opacity-3': isFadedOut(group.name) }"
|
||||
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
|
||||
/>
|
||||
<div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
|
||||
<job-group-dropdown :group="group" :pipeline-id="pipelineId" />
|
||||
<job-group-dropdown
|
||||
:group="group"
|
||||
:stage-name="showStageName ? group.stageName : ''"
|
||||
:pipeline-id="pipelineId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,19 @@ const unwrapGroups = (stages) => {
|
|||
const {
|
||||
groups: { nodes: groups },
|
||||
} = stage;
|
||||
return { node: { ...stage, groups }, lookup: { stageIdx: idx } };
|
||||
|
||||
/*
|
||||
Being peformance conscious here means we don't want to spread and copy the
|
||||
group value just to add one parameter.
|
||||
*/
|
||||
/* eslint-disable no-param-reassign */
|
||||
const groupsWithStageName = groups.map((group) => {
|
||||
group.stageName = stage.name;
|
||||
return group;
|
||||
});
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
return { node: { ...stage, groups: groupsWithStageName }, lookup: { stageIdx: idx } };
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-if="canCherryPick"
|
||||
data-testid="cherry-pick-link"
|
||||
data-qa-selector="cherry_pick_button"
|
||||
@click="showModal($options.openCherryPickModal)"
|
||||
>
|
||||
{{ s__('ChangeTypeAction|Cherry-pick') }}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { GlTabs, GlTab } from '@gitlab/ui';
|
|||
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
|
||||
import PipelineCharts from './pipeline_charts.vue';
|
||||
|
||||
const charts = ['pipelines', 'deployments'];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTabs,
|
||||
|
|
@ -12,6 +10,8 @@ export default {
|
|||
PipelineCharts,
|
||||
DeploymentFrequencyCharts: () =>
|
||||
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
|
||||
LeadTimeCharts: () =>
|
||||
import('ee_component/projects/pipelines/charts/components/lead_time_charts.vue'),
|
||||
},
|
||||
inject: {
|
||||
shouldRenderDeploymentFrequencyCharts: {
|
||||
|
|
@ -24,20 +24,29 @@ export default {
|
|||
selectedTab: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
charts() {
|
||||
if (this.shouldRenderDeploymentFrequencyCharts) {
|
||||
return ['pipelines', 'deployments', 'lead-time'];
|
||||
}
|
||||
|
||||
return ['pipelines', 'lead-time'];
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.selectTab();
|
||||
window.addEventListener('popstate', this.selectTab);
|
||||
},
|
||||
methods: {
|
||||
selectTab() {
|
||||
const [chart] = getParameterValues('chart') || charts;
|
||||
const tab = charts.indexOf(chart);
|
||||
const [chart] = getParameterValues('chart') || this.charts;
|
||||
const tab = this.charts.indexOf(chart);
|
||||
this.selectedTab = tab >= 0 ? tab : 0;
|
||||
},
|
||||
onTabChange(index) {
|
||||
if (index !== this.selectedTab) {
|
||||
this.selectedTab = index;
|
||||
const path = mergeUrlParams({ chart: charts[index] }, window.location.pathname);
|
||||
const path = mergeUrlParams({ chart: this.charts[index] }, window.location.pathname);
|
||||
updateHistory({ url: path, title: window.title });
|
||||
}
|
||||
},
|
||||
|
|
@ -46,14 +55,16 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-tabs v-if="shouldRenderDeploymentFrequencyCharts" :value="selectedTab" @input="onTabChange">
|
||||
<gl-tabs :value="selectedTab" @input="onTabChange">
|
||||
<gl-tab :title="__('Pipelines')">
|
||||
<pipeline-charts />
|
||||
</gl-tab>
|
||||
<gl-tab :title="__('Deployments')">
|
||||
<gl-tab v-if="shouldRenderDeploymentFrequencyCharts" :title="__('Deployments')">
|
||||
<deployment-frequency-charts />
|
||||
</gl-tab>
|
||||
<gl-tab :title="__('Lead Time')">
|
||||
<lead-time-charts />
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
<pipeline-charts v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ export default {
|
|||
:checked="value"
|
||||
:disabled="isDisabled"
|
||||
name="squash"
|
||||
class="qa-squash-checkbox js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center"
|
||||
class="js-squash-checkbox gl-mr-2 gl-display-flex gl-align-items-center"
|
||||
data-qa-selector="squash_checkbox"
|
||||
:title="tooltipTitle"
|
||||
@change="(checked) => $emit('input', checked)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
GlDrawer,
|
||||
GlInfiniteScroll,
|
||||
GlResizeObserverDirective,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
GlBadge,
|
||||
GlLoadingIcon,
|
||||
} from '@gitlab/ui';
|
||||
import { GlDrawer, GlInfiniteScroll, GlResizeObserverDirective } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import Tracking from '~/tracking';
|
||||
import { getDrawerBodyHeight } from '../utils/get_drawer_body_height';
|
||||
|
|
@ -20,37 +12,24 @@ export default {
|
|||
components: {
|
||||
GlDrawer,
|
||||
GlInfiniteScroll,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
SkeletonLoader,
|
||||
Feature,
|
||||
GlBadge,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
GlResizeObserver: GlResizeObserverDirective,
|
||||
},
|
||||
mixins: [trackingMixin],
|
||||
props: {
|
||||
storageKey: {
|
||||
versionDigest: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
gitlabDotCom: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['open', 'features', 'pageInfo', 'drawerBodyHeight', 'fetching']),
|
||||
},
|
||||
mounted() {
|
||||
this.openDrawer(this.storageKey);
|
||||
this.openDrawer(this.versionDigest);
|
||||
this.fetchItems();
|
||||
|
||||
const body = document.querySelector('body');
|
||||
|
|
@ -70,16 +49,6 @@ export default {
|
|||
const height = getDrawerBodyHeight(this.$refs.drawer.$el);
|
||||
this.setDrawerBodyHeight(height);
|
||||
},
|
||||
featuresForVersion(version) {
|
||||
return this.features.filter((feature) => {
|
||||
return feature.release === parseFloat(version);
|
||||
});
|
||||
},
|
||||
fetchVersion(version) {
|
||||
if (this.featuresForVersion(version).length === 0) {
|
||||
this.fetchItems({ version });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -99,7 +68,6 @@ export default {
|
|||
</template>
|
||||
<template v-if="features.length">
|
||||
<gl-infinite-scroll
|
||||
v-if="gitlabDotCom"
|
||||
:fetched-items="features.length"
|
||||
:max-list-height="drawerBodyHeight"
|
||||
class="gl-p-0"
|
||||
|
|
@ -109,26 +77,6 @@ export default {
|
|||
<feature v-for="feature in features" :key="feature.title" :feature="feature" />
|
||||
</template>
|
||||
</gl-infinite-scroll>
|
||||
<gl-tabs v-else :style="{ height: `${drawerBodyHeight}px` }" class="gl-p-0">
|
||||
<gl-tab
|
||||
v-for="(version, index) in versions"
|
||||
:key="version"
|
||||
@click="fetchVersion(version)"
|
||||
>
|
||||
<template #title>
|
||||
<span>{{ version }}</span>
|
||||
<gl-badge v-if="index === 0">{{ __('Your Version') }}</gl-badge>
|
||||
</template>
|
||||
<gl-loading-icon v-if="fetching" size="lg" class="text-center" />
|
||||
<template v-else>
|
||||
<feature
|
||||
v-for="feature in featuresForVersion(version)"
|
||||
:key="feature.title"
|
||||
:feature="feature"
|
||||
/>
|
||||
</template>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
</template>
|
||||
<div v-else class="gl-mt-5">
|
||||
<skeleton-loader />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import Vue from 'vue';
|
|||
import { mapState } from 'vuex';
|
||||
import App from './components/app.vue';
|
||||
import store from './store';
|
||||
import { getStorageKey, setNotification } from './utils/notification';
|
||||
import { getVersionDigest, setNotification } from './utils/notification';
|
||||
|
||||
let whatsNewApp;
|
||||
|
||||
|
|
@ -27,9 +27,7 @@ export default (el) => {
|
|||
render(createElement) {
|
||||
return createElement('app', {
|
||||
props: {
|
||||
storageKey: getStorageKey(el),
|
||||
versions: JSON.parse(el.getAttribute('data-versions')),
|
||||
gitlabDotCom: el.getAttribute('data-gitlab-dot-com'),
|
||||
versionDigest: getVersionDigest(el),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
|
||||
import { STORAGE_KEY } from '../utils/notification';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
closeDrawer({ commit }) {
|
||||
commit(types.CLOSE_DRAWER);
|
||||
},
|
||||
openDrawer({ commit }, storageKey) {
|
||||
openDrawer({ commit }, versionDigest) {
|
||||
commit(types.OPEN_DRAWER);
|
||||
|
||||
if (storageKey) {
|
||||
localStorage.setItem(storageKey, JSON.stringify(false));
|
||||
if (versionDigest) {
|
||||
localStorage.setItem(STORAGE_KEY, versionDigest);
|
||||
}
|
||||
},
|
||||
fetchItems({ commit, state }, { page, version } = { page: null, version: null }) {
|
||||
fetchItems({ commit, state }, { page } = { page: null }) {
|
||||
if (state.fetching) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -24,7 +25,6 @@ export default {
|
|||
.get('/-/whats_new', {
|
||||
params: {
|
||||
page,
|
||||
version,
|
||||
},
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
export const getStorageKey = (appEl) => appEl.getAttribute('data-storage-key');
|
||||
export const STORAGE_KEY = 'display-whats-new-notification';
|
||||
|
||||
export const getVersionDigest = (appEl) => appEl.getAttribute('data-version-digest');
|
||||
|
||||
export const setNotification = (appEl) => {
|
||||
const storageKey = getStorageKey(appEl);
|
||||
const versionDigest = getVersionDigest(appEl);
|
||||
const notificationEl = document.querySelector('.header-help');
|
||||
let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
|
||||
|
||||
if (JSON.parse(localStorage.getItem(storageKey)) === false) {
|
||||
const legacyStorageKey = 'display-whats-new-notification-13.10';
|
||||
const localStoragePairs = [
|
||||
[legacyStorageKey, false],
|
||||
[STORAGE_KEY, versionDigest],
|
||||
];
|
||||
if (localStoragePairs.some((pair) => localStorage.getItem(pair[0]) === pair[1].toString())) {
|
||||
notificationEl.classList.remove('with-notifications');
|
||||
if (notificationCountEl) {
|
||||
notificationCountEl.parentElement.removeChild(notificationCountEl);
|
||||
|
|
|
|||
|
|
@ -148,7 +148,19 @@
|
|||
}
|
||||
|
||||
.gl-build-content {
|
||||
@include build-content();
|
||||
display: inline-block;
|
||||
padding: 8px 10px 9px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--border-color, $border-color);
|
||||
border-radius: 30px;
|
||||
background-color: var(--white, $white);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--gray-50, $gray-50);
|
||||
border: 1px solid $dropdown-toggle-active-border-color;
|
||||
color: var(--gl-text-color, $gl-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.gl-ci-action-icon-container {
|
||||
|
|
|
|||
|
|
@ -5,19 +5,23 @@ module CreatesCommit
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
|
||||
if user_access(@project).can_push_to_branch?(branch_name_or_ref)
|
||||
@project_to_commit_into = @project
|
||||
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil, target_project: nil)
|
||||
target_project ||= @project
|
||||
|
||||
if user_access(target_project).can_push_to_branch?(branch_name_or_ref)
|
||||
@project_to_commit_into = target_project
|
||||
@branch_name ||= @ref
|
||||
else
|
||||
@project_to_commit_into = current_user.fork_of(@project)
|
||||
@project_to_commit_into = current_user.fork_of(target_project)
|
||||
@branch_name ||= @project_to_commit_into.repository.next_branch('patch')
|
||||
end
|
||||
|
||||
@start_branch ||= @ref || @branch_name
|
||||
|
||||
start_project = Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml) ? @project_to_commit_into : @project
|
||||
|
||||
commit_params = @commit_params.merge(
|
||||
start_project: @project,
|
||||
start_project: start_project,
|
||||
start_branch: @start_branch,
|
||||
branch_name: @branch_name
|
||||
)
|
||||
|
|
@ -27,7 +31,7 @@ module CreatesCommit
|
|||
if result[:status] == :success
|
||||
update_flash_notice(success_notice)
|
||||
|
||||
success_path = final_success_path(success_path)
|
||||
success_path = final_success_path(success_path, target_project)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to success_path }
|
||||
|
|
@ -79,9 +83,9 @@ module CreatesCommit
|
|||
end
|
||||
end
|
||||
|
||||
def final_success_path(success_path)
|
||||
def final_success_path(success_path, target_project)
|
||||
if create_merge_request?
|
||||
merge_request_exists? ? existing_merge_request_path : new_merge_request_path
|
||||
merge_request_exists? ? existing_merge_request_path : new_merge_request_path(target_project)
|
||||
else
|
||||
success_path = success_path.call if success_path.respond_to?(:call)
|
||||
|
||||
|
|
@ -90,12 +94,12 @@ module CreatesCommit
|
|||
end
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def new_merge_request_path
|
||||
def new_merge_request_path(target_project)
|
||||
project_new_merge_request_path(
|
||||
@project_to_commit_into,
|
||||
merge_request: {
|
||||
source_project_id: @project_to_commit_into.id,
|
||||
target_project_id: @project.id,
|
||||
target_project_id: target_project.id,
|
||||
source_branch: @branch_name,
|
||||
target_branch: @start_branch
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,13 @@ module RendersCommits
|
|||
def set_commits_for_rendering(commits, commits_count: nil)
|
||||
@total_commit_count = commits_count || commits.size
|
||||
limited, @hidden_commit_count = limited_commits(commits, @total_commit_count)
|
||||
commits.each(&:lazy_author) # preload authors
|
||||
prepare_commits_for_rendering(limited)
|
||||
end
|
||||
# rubocop: enable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
def prepare_commits_for_rendering(commits)
|
||||
commits.each(&:lazy_author) # preload commits' authors
|
||||
|
||||
Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
commits
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
@branch_name = create_new_branch? ? @commit.revert_branch_name : @start_branch
|
||||
|
||||
create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully reverted.",
|
||||
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
||||
success_path: -> { successful_change_path(@project) }, failure_path: failed_change_path)
|
||||
end
|
||||
|
||||
def cherry_pick
|
||||
|
|
@ -122,10 +122,15 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
|
||||
return render_404 if @start_branch.blank?
|
||||
|
||||
target_project = find_cherry_pick_target_project
|
||||
return render_404 unless target_project
|
||||
|
||||
@branch_name = create_new_branch? ? @commit.cherry_pick_branch_name : @start_branch
|
||||
|
||||
create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title(current_user)} has been successfully cherry-picked into #{@branch_name}.",
|
||||
success_path: -> { successful_change_path }, failure_path: failed_change_path)
|
||||
success_path: -> { successful_change_path(target_project) },
|
||||
failure_path: failed_change_path,
|
||||
target_project: target_project)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -138,8 +143,8 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
params[:create_merge_request].present? || !can?(current_user, :push_code, @project)
|
||||
end
|
||||
|
||||
def successful_change_path
|
||||
referenced_merge_request_url || project_commits_url(@project, @branch_name)
|
||||
def successful_change_path(target_project)
|
||||
referenced_merge_request_url || project_commits_url(target_project, @branch_name)
|
||||
end
|
||||
|
||||
def failed_change_path
|
||||
|
|
@ -218,4 +223,14 @@ class Projects::CommitController < Projects::ApplicationController
|
|||
@start_branch = params[:start_branch]
|
||||
@commit_params = { commit: @commit }
|
||||
end
|
||||
|
||||
def find_cherry_pick_target_project
|
||||
return @project if params[:target_project_id].blank?
|
||||
return @project unless Feature.enabled?(:pick_into_project, @project, default_enabled: :yaml)
|
||||
|
||||
MergeRequestTargetProjectFinder
|
||||
.new(current_user: current_user, source_project: @project, project_feature: :repository)
|
||||
.execute
|
||||
.find_by_id(params[:target_project_id])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class WhatsNewController < ApplicationController
|
|||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :check_valid_page_param, :set_pagination_headers, unless: -> { has_version_param? }
|
||||
before_action :check_valid_page_param, :set_pagination_headers
|
||||
|
||||
feature_category :navigation
|
||||
|
||||
|
|
@ -29,19 +29,11 @@ class WhatsNewController < ApplicationController
|
|||
|
||||
def highlights
|
||||
strong_memoize(:highlights) do
|
||||
if has_version_param?
|
||||
ReleaseHighlight.for_version(version: params[:version])
|
||||
else
|
||||
ReleaseHighlight.paginated(page: current_page)
|
||||
end
|
||||
ReleaseHighlight.paginated(page: current_page)
|
||||
end
|
||||
end
|
||||
|
||||
def set_pagination_headers
|
||||
response.set_header('X-Next-Page', highlights.next_page)
|
||||
end
|
||||
|
||||
def has_version_param?
|
||||
params[:version].present?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ module CommitsHelper
|
|||
def cherry_pick_projects_data(project)
|
||||
return [] unless Feature.enabled?(:pick_into_project, project, default_enabled: :yaml)
|
||||
|
||||
target_projects(project).map do |project|
|
||||
[project, project.forked_from_project].compact.map do |project|
|
||||
{
|
||||
id: project.id.to_s,
|
||||
name: project.full_path,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,33 @@ module NamespacesHelper
|
|||
namespaces_options(selected, **options)
|
||||
end
|
||||
|
||||
def cascading_namespace_settings_enabled?
|
||||
NamespaceSetting.cascading_settings_feature_enabled?
|
||||
end
|
||||
|
||||
def cascading_namespace_settings_popover_data(attribute, group, settings_path_helper)
|
||||
locked_by_ancestor = group.namespace_settings.public_send("#{attribute}_locked_by_ancestor?") # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
popover_data = {
|
||||
locked_by_application_setting: group.namespace_settings.public_send("#{attribute}_locked_by_application_setting?"), # rubocop:disable GitlabSecurity/PublicSend
|
||||
locked_by_ancestor: locked_by_ancestor
|
||||
}
|
||||
|
||||
if locked_by_ancestor
|
||||
ancestor_namespace = group.namespace_settings.public_send("#{attribute}_locked_ancestor").namespace # rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
popover_data[:ancestor_namespace] = {
|
||||
full_name: ancestor_namespace.full_name,
|
||||
path: settings_path_helper.call(ancestor_namespace)
|
||||
}
|
||||
end
|
||||
|
||||
{
|
||||
popover_data: popover_data.to_json,
|
||||
testid: 'cascading-settings-lock-icon'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Many importers create a temporary Group, so use the real
|
||||
|
|
|
|||
|
|
@ -5,15 +5,7 @@ module WhatsNewHelper
|
|||
ReleaseHighlight.most_recent_item_count
|
||||
end
|
||||
|
||||
def whats_new_storage_key
|
||||
most_recent_version = ReleaseHighlight.versions&.first
|
||||
|
||||
return unless most_recent_version
|
||||
|
||||
['display-whats-new-notification', most_recent_version].join('-')
|
||||
end
|
||||
|
||||
def whats_new_versions
|
||||
ReleaseHighlight.versions
|
||||
def whats_new_version_digest
|
||||
ReleaseHighlight.most_recent_version_digest
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,17 +3,6 @@
|
|||
class ReleaseHighlight
|
||||
CACHE_DURATION = 1.hour
|
||||
FILES_PATH = Rails.root.join('data', 'whats_new', '*.yml')
|
||||
RELEASE_VERSIONS_IN_A_YEAR = 12
|
||||
|
||||
def self.for_version(version:)
|
||||
index = self.versions.index(version)
|
||||
|
||||
return if index.nil?
|
||||
|
||||
page = index + 1
|
||||
|
||||
self.paginated(page: page)
|
||||
end
|
||||
|
||||
def self.paginated(page: 1)
|
||||
key = self.cache_key("items:page-#{page}")
|
||||
|
|
@ -82,15 +71,15 @@ class ReleaseHighlight
|
|||
end
|
||||
end
|
||||
|
||||
def self.versions
|
||||
key = self.cache_key('versions')
|
||||
def self.most_recent_version_digest
|
||||
key = self.cache_key('most_recent_version_digest')
|
||||
|
||||
Gitlab::ProcessMemoryCache.cache_backend.fetch(key, expires_in: CACHE_DURATION) do
|
||||
versions = self.file_paths.first(RELEASE_VERSIONS_IN_A_YEAR).map do |path|
|
||||
/\d*\_(\d*\_\d*)\.yml$/.match(path).captures[0].gsub(/0(?=\d)/, "").tr("_", ".")
|
||||
end
|
||||
version = self.paginated&.items&.first&.[]('release')&.to_s
|
||||
|
||||
versions.uniq
|
||||
next if version.nil?
|
||||
|
||||
Digest::SHA256.hexdigest(version)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
- page_title _("Broadcast Messages")
|
||||
|
||||
%h3.page-title
|
||||
Broadcast Messages
|
||||
= _('Broadcast Messages')
|
||||
%p.light
|
||||
Broadcast messages are displayed for every user and can be used to notify
|
||||
users about scheduled maintenance, recent upgrades and more.
|
||||
= _('Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.')
|
||||
|
||||
= render 'form'
|
||||
|
||||
|
|
@ -15,12 +14,12 @@
|
|||
%table.table.table-responsive
|
||||
%thead
|
||||
%tr
|
||||
%th Status
|
||||
%th Preview
|
||||
%th Starts
|
||||
%th Ends
|
||||
%th Target Path
|
||||
%th Type
|
||||
%th= _('Status')
|
||||
%th= _('Preview')
|
||||
%th= _('Starts')
|
||||
%th= _('Ends')
|
||||
%th= _(' Target Path')
|
||||
%th= _(' Type')
|
||||
%th
|
||||
%tbody
|
||||
- @broadcast_messages.each do |message|
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
%td
|
||||
= message.broadcast_type.capitalize
|
||||
%td.gl-white-space-nowrap.gl-display-flex
|
||||
= link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-icon gl-button'
|
||||
= link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2'
|
||||
= link_to sprite_icon('pencil-square', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
|
||||
= link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger ml-2'
|
||||
|
||||
= paginate @broadcast_messages, theme: 'gitlab'
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@
|
|||
= nav_link(page: [dashboard_projects_path, root_path]) do
|
||||
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
|
||||
= _("Your projects")
|
||||
%span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count)
|
||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_user_projects_count)
|
||||
= nav_link(page: starred_dashboard_projects_path) do
|
||||
= link_to starred_dashboard_projects_path, data: {placement: 'right'} do
|
||||
= _("Starred projects")
|
||||
%span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count)
|
||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(@total_starred_projects_count)
|
||||
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
|
||||
= link_to explore_root_path, data: {placement: 'right'} do
|
||||
= _("Explore projects")
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@
|
|||
- if @resource.unconfirmed_email.present? || !@resource.created_recently?
|
||||
#content
|
||||
= email_default_heading(@resource.unconfirmed_email || @resource.email)
|
||||
%p Click the link below to confirm your email address.
|
||||
%p= _('Click the link below to confirm your email address.')
|
||||
#cta
|
||||
= link_to 'Confirm your email address', confirmation_link
|
||||
= link_to _('Confirm your email address'), confirmation_link
|
||||
- else
|
||||
#content
|
||||
- if Gitlab.com?
|
||||
= email_default_heading('Thanks for signing up to GitLab!')
|
||||
= email_default_heading(_('Thanks for signing up to GitLab!'))
|
||||
- else
|
||||
= email_default_heading("Welcome, #{@resource.name}!")
|
||||
%p To get started, click the link below to confirm your account.
|
||||
= email_default_heading(_("Welcome, %{name}!") % { name: @resource.name })
|
||||
%p= _("To get started, click the link below to confirm your account.")
|
||||
#cta
|
||||
= link_to 'Confirm your account', confirmation_link
|
||||
= link_to _('Confirm your account'), confirmation_link
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- expanded = expanded_by_default?
|
||||
|
||||
= render 'shared/namespaces/cascading_settings/lock_popovers'
|
||||
|
||||
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
|
||||
.settings-header
|
||||
|
|
|
|||
|
|
@ -8,27 +8,27 @@
|
|||
= render 'shared/allow_request_access', form: f
|
||||
|
||||
.form-group.gl-mb-3
|
||||
.form-check
|
||||
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input'
|
||||
= f.label :share_with_group_lock, class: 'form-check-label' do
|
||||
%span.d-block
|
||||
.gl-form-checkbox.custom-control.custom-checkbox
|
||||
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input'
|
||||
= f.label :share_with_group_lock, class: 'custom-control-label' do
|
||||
%span
|
||||
- group_link = link_to @group.name, group_path(@group)
|
||||
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
|
||||
%span.js-descr.text-muted= share_with_group_lock_help_text(@group)
|
||||
%p.js-descr.help-text= share_with_group_lock_help_text(@group)
|
||||
|
||||
.form-group.gl-mb-3
|
||||
.form-check
|
||||
= f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input'
|
||||
= f.label :emails_disabled, class: 'form-check-label' do
|
||||
%span.d-block= s_('GroupSettings|Disable email notifications')
|
||||
%span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
|
||||
.gl-form-checkbox.custom-control.custom-checkbox
|
||||
= f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input'
|
||||
= f.label :emails_disabled, class: 'custom-control-label' do
|
||||
%span= s_('GroupSettings|Disable email notifications')
|
||||
%p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
|
||||
|
||||
.form-group.gl-mb-3
|
||||
.form-check
|
||||
= f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'form-check-input'
|
||||
= f.label :mentions_disabled, class: 'form-check-label' do
|
||||
%span.d-block= s_('GroupSettings|Disable group mentions')
|
||||
%span.text-muted= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
|
||||
.gl-form-checkbox.custom-control.custom-checkbox
|
||||
= f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input'
|
||||
= f.label :mentions_disabled, class: 'custom-control-label' do
|
||||
%span= s_('GroupSettings|Disable group mentions')
|
||||
%p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
|
||||
|
||||
= render 'groups/settings/project_access_token_creation', f: f, group: @group
|
||||
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
- return unless render_setting_to_allow_project_access_token_creation?(group)
|
||||
|
||||
.form-group.gl-mb-3
|
||||
.form-check
|
||||
= f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'form-check-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
|
||||
= f.label :resource_access_token_creation_allowed, class: 'form-check-label' do
|
||||
%span.gl-display-block= s_('GroupSettings|Allow project access token creation')
|
||||
.gl-form-checkbox.custom-control.custom-checkbox
|
||||
= f.check_box :resource_access_token_creation_allowed, checked: group.namespace_settings.resource_access_token_creation_allowed?, class: 'custom-control-input', data: { qa_selector: 'resource_access_token_creation_allowed_checkbox' }
|
||||
= f.label :resource_access_token_creation_allowed, class: 'custom-control-label' do
|
||||
%span= s_('GroupSettings|Allow project access token creation')
|
||||
- project_access_tokens_link = help_page_path('user/project/settings/project_access_tokens')
|
||||
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_access_tokens_link }
|
||||
%span.text-muted= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
%p.help-text= s_('GroupSettings|Users can create %{link_start}project access tokens%{link_end} for projects in this group.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
|
||||
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
|
||||
|
||||
#whats-new-app{ data: { storage_key: whats_new_storage_key, versions: whats_new_versions, gitlab_dot_com: Gitlab.dev_env_org_or_com? } }
|
||||
#whats-new-app{ data: { version_digest: whats_new_version_digest } }
|
||||
|
||||
- if can?(current_user, :update_user_status, current_user)
|
||||
.js-set-status-modal-wrapper{ data: user_status_data }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
%li
|
||||
%button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', data: { storage_key: whats_new_storage_key }, class: 'gl-display-flex!' }
|
||||
%button.gl-justify-content-space-between.gl-align-items-center.js-whats-new-trigger{ type: 'button', class: 'gl-display-flex!' }
|
||||
= _("What's new")
|
||||
%span.js-whats-new-notification-count.whats-new-notification-count
|
||||
= whats_new_most_recent_release_items_count
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
- attribute = local_assigns.fetch(:attribute, nil)
|
||||
- group = local_assigns.fetch(:group, nil)
|
||||
- form = local_assigns.fetch(:form, nil)
|
||||
|
||||
- return unless attribute && group && form && cascading_namespace_settings_enabled?
|
||||
- return if group.namespace_settings.public_send("#{attribute}_locked?")
|
||||
|
||||
- lock_attribute = "lock_#{attribute}"
|
||||
|
||||
.gl-form-checkbox.custom-control.custom-checkbox
|
||||
= form.check_box lock_attribute, checked: group.namespace_settings.public_send(lock_attribute), class: 'custom-control-input', data: { testid: 'enforce-for-all-subgroups-checkbox' }
|
||||
= form.label lock_attribute, class: 'custom-control-label' do
|
||||
%span= s_('CascadingSettings|Enforce for all subgroups')
|
||||
%p.help-text= s_('CascadingSettings|Subgroups cannot change this setting.')
|
||||
|
|
@ -0,0 +1 @@
|
|||
.js-cascading-settings-lock-popovers
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
- attribute = local_assigns.fetch(:attribute, nil)
|
||||
- group = local_assigns.fetch(:group, nil)
|
||||
- form = local_assigns.fetch(:form, nil)
|
||||
- settings_path_helper = local_assigns.fetch(:settings_path_helper, nil)
|
||||
- help_text = local_assigns.fetch(:help_text, nil)
|
||||
|
||||
- return unless attribute && group && form && settings_path_helper
|
||||
|
||||
- setting_locked = group.namespace_settings.public_send("#{attribute}_locked?")
|
||||
|
||||
= form.label attribute, class: 'custom-control-label', aria: { disabled: setting_locked } do
|
||||
%span.position-relative.gl-pr-6.gl-display-inline-flex
|
||||
= yield
|
||||
- if setting_locked
|
||||
%button.position-absolute.gl-top-3.gl-right-0.gl-translate-y-n50.gl-cursor-default.btn.btn-default.btn-sm.gl-button.btn-default-tertiary.js-cascading-settings-lock-popover-target{ class: 'gl-p-1! gl-text-gray-600! gl-bg-transparent!',
|
||||
type: 'button',
|
||||
data: cascading_namespace_settings_popover_data(attribute, group, settings_path_helper) }
|
||||
= sprite_icon('lock', size: 16)
|
||||
- if help_text
|
||||
%p.help-text
|
||||
= help_text
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix N+1 for searching commits
|
||||
merge_request: 58867
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in broadcast_messages/index.html.haml
|
||||
merge_request: 58146
|
||||
author: nuwe1
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Externalize strings in _confirmation_instructions_account.html.haml
|
||||
merge_request: 58214
|
||||
author: nuwe1
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add gl-badge for badges in dashboard nav
|
||||
merge_request: 57936
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
---
|
||||
stage: Package
|
||||
group: Package
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Go Proxy API
|
||||
|
||||
This is the API documentation for [Go Packages](../../user/packages/go_proxy/index.md).
|
||||
This API is behind a feature flag that is disabled by default. GitLab administrators with access to
|
||||
the GitLab Rails console can [enable](../../administration/feature_flags.md)
|
||||
this API for your GitLab instance.
|
||||
|
||||
WARNING:
|
||||
This API is used by the [Go client](https://maven.apache.org/)
|
||||
and is generally not meant for manual consumption.
|
||||
|
||||
For instructions on how to work with the Go Proxy, see the [Go Proxy package documentation](../../user/packages/go_proxy/index.md).
|
||||
|
||||
NOTE:
|
||||
These endpoints do not adhere to the standard API authentication methods.
|
||||
See the [Go Proxy package documentation](../../user/packages/go_proxy/index.md)
|
||||
for details on which headers and token types are supported.
|
||||
|
||||
## List
|
||||
|
||||
> Introduced in GitLab 13.1.
|
||||
|
||||
Get all tagged versions for a given Go module:
|
||||
|
||||
```plaintext
|
||||
GET projects/:id/packages/go/:module_name/@v/list
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------- | ------ | -------- | ----------- |
|
||||
| `id` | string | yes | The project ID or full path of a project. |
|
||||
| `module_name` | string | yes | The name of the Go module. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/list"
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```shell
|
||||
"v1.0.0\nv1.0.1\nv1.3.8\n2.0.0\n2.1.0\n3.0.0"
|
||||
```
|
||||
|
||||
## Version metadata
|
||||
|
||||
> Introduced in GitLab 13.1.
|
||||
|
||||
Get all tagged versions for a given Go module:
|
||||
|
||||
```plaintext
|
||||
GET projects/:id/packages/go/:module_name/@v/:module_version.info
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
| `id` | string | yes | The project ID or full path of a project. |
|
||||
| `module_name` | string | yes | The name of the Go module. |
|
||||
| `module_version` | string | yes | The version of the Go module. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.info"
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "v1.0.0",
|
||||
"Time": "1617822312 -0600"
|
||||
}
|
||||
```
|
||||
|
||||
## Download module file
|
||||
|
||||
> Introduced in GitLab 13.1.
|
||||
|
||||
Fetch the `.mod` module file:
|
||||
|
||||
```plaintext
|
||||
GET projects/:id/packages/go/:module_name/@v/:module_version.mod
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
| `id` | string | yes | The project ID or full path of a project. |
|
||||
| `module_name` | string | yes | The name of the Go module. |
|
||||
| `module_version` | string | yes | The version of the Go module. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.mod"
|
||||
```
|
||||
|
||||
Write to a file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.mod" >> foo.mod
|
||||
```
|
||||
|
||||
This writes to `foo.mod` in the current directory.
|
||||
|
||||
## Download module source
|
||||
|
||||
> Introduced in GitLab 13.1.
|
||||
|
||||
Fetch the `.zip` of the module source:
|
||||
|
||||
```plaintext
|
||||
GET projects/:id/packages/go/:module_name/@v/:module_version.zip
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------- | ------ | -------- | ----------- |
|
||||
| `id` | string | yes | The project ID or full path of a project. |
|
||||
| `module_name` | string | yes | The name of the Go module. |
|
||||
| `module_version` | string | yes | The version of the Go module. |
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.zip"
|
||||
```
|
||||
|
||||
Write to a file:
|
||||
|
||||
```shell
|
||||
curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/go/my-go-module/@v/1.0.0.zip" >> foo.zip
|
||||
```
|
||||
|
||||
This writes to `foo.zip` in the current directory.
|
||||
|
|
@ -110,21 +110,66 @@ The current method provides several attributes that are sent on each click event
|
|||
| property | text | false | Any additional property of the element, or object being acted on. |
|
||||
| value | decimal | false | Describes a numeric value or something directly related to the event. This could be the value of an input (e.g. `10` when clicking `internal` visibility). |
|
||||
|
||||
### Examples
|
||||
|
||||
| category* | label | action | property** | value |
|
||||
|-------------|------------------|-----------------------|----------|:-----:|
|
||||
| [root:index] | main_navigation | click_navigation_link | `[link_label]` | - |
|
||||
| [groups:boards:show] | toggle_swimlanes | click_toggle_button | - | `[is_active]` |
|
||||
| [projects:registry:index] | registry_delete | click_button | - | - |
|
||||
| [projects:registry:index] | registry_delete | confirm_deletion | - | - |
|
||||
| [projects:blob:show] | congratulate_first_pipeline | click_button | `[human_access]` | - |
|
||||
| [projects:clusters:new] | chart_options | generate_link | `[chart_link]` | - |
|
||||
| [projects:clusters:new] | chart_options | click_add_label_button | `[label_id]` | - |
|
||||
|
||||
_* It's ok to omit the category, and use the default._<br>
|
||||
_** Property is usually the best place for variable strings._
|
||||
|
||||
### Reference SQL
|
||||
|
||||
#### Last 20 `reply_comment_button` events
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
event_id,
|
||||
v_tracker,
|
||||
event_label,
|
||||
event_action,
|
||||
event_property,
|
||||
event_value,
|
||||
event_category,
|
||||
contexts
|
||||
FROM legacy.snowplow_structured_events_all
|
||||
WHERE
|
||||
event_label = 'reply_comment_button'
|
||||
AND event_action = 'click_button'
|
||||
-- AND event_category = 'projects:issues:show'
|
||||
-- AND event_value = 1
|
||||
ORDER BY collector_tstamp DESC
|
||||
LIMIT 20
|
||||
```
|
||||
|
||||
### Web-specific parameters
|
||||
|
||||
Snowplow JS adds many [web-specific parameters](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#Web-specific_parameters) to all web events by default.
|
||||
|
||||
## Implementing Snowplow JS (Frontend) tracking
|
||||
|
||||
GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to use tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy).
|
||||
GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. The simplest way to use it is to add `data-` attributes to clickable elements and dropdowns. There is also a Vue mixin (exposing a `track` method), and the static method `Tracking.event`. Each of these requires at minimum a `category` and an `action`. Additional data can be provided that adheres to our [Structured event taxonomy](#structured-event-taxonomy).
|
||||
|
||||
| field | type | default value | description |
|
||||
|:-----------|:-------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `category` | string | `document.body.dataset.page` | Page or subsection of a page that events are being captured within. |
|
||||
| `action` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
|
||||
| `action` | string | generic | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
|
||||
| `data` | object | `{}` | Additional data such as `label`, `property`, `value`, and `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
|
||||
### Tracking in HAML (or Vue Templates)
|
||||
### Usage recommendations
|
||||
|
||||
- Use [data attributes](#tracking-with-data-attributes) on HTML elements that emits either the `click`, `show.bs.dropdown`, or `hide.bs.dropdown` events.
|
||||
- Use the [Vue mixin](#tracking-within-vue-components) when tracking custom events, or if the supported events for data attributes are not propagating.
|
||||
- Use the [Tracking class directly](#tracking-in-raw-javascript) when tracking on raw JS files.
|
||||
|
||||
### Tracking with data attributes
|
||||
|
||||
When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-action` attribute automatically have event tracking bound on clicks.
|
||||
|
||||
|
|
@ -142,7 +187,7 @@ Below is an example of `data-track-*` attributes assigned to a button:
|
|||
/>
|
||||
```
|
||||
|
||||
Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows them to be properly handled on re-rendering and changes to the DOM. Note that because of the way these events are bound, click events should not be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you need to implement your own listeners and follow the instructions in [Tracking in raw JavaScript](#tracking-in-raw-javascript).
|
||||
Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows them to be properly handled on re-rendering and changes to the DOM. Note that because of the way these events are bound, click events should not be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you need to implement your own listeners and follow the instructions in [Tracking within Vue components](#tracking-within-vue-components) or [Tracking in raw JavaScript](#tracking-in-raw-javascript).
|
||||
|
||||
Below is a list of supported `data-track-*` attributes:
|
||||
|
||||
|
|
@ -154,16 +199,29 @@ Below is a list of supported `data-track-*` attributes:
|
|||
| `data-track-value` | false | The `value` as described in our [Structured event taxonomy](#structured-event-taxonomy). If omitted, this is the element's `value` property or an empty string. For checkboxes, the default value is the element's checked attribute or `false` when unchecked. |
|
||||
| `data-track-context` | false | The `context` as described in our [Structured event taxonomy](#structured-event-taxonomy). |
|
||||
|
||||
#### Available helpers
|
||||
|
||||
```ruby
|
||||
tracking_attrs(label, action, property) # { data: { track_label... } }
|
||||
|
||||
%button{ **tracking_attrs('main_navigation', 'click_button', 'navigation') }
|
||||
```
|
||||
|
||||
#### Caveats
|
||||
|
||||
When using the GitLab helper method [`nav_link`](https://gitlab.com/gitlab-org/gitlab/-/blob/898b286de322e5df6a38d257b10c94974d580df8/app/helpers/tab_helper.rb#L69) be sure to wrap `html_options` under the `html_options` keyword argument.
|
||||
Be careful, as this behavior can be confused with the `ActionView` helper method [`link_to`](https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to) that does not require additional wrapping of `html_options`
|
||||
|
||||
`nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "groups_dropdown", track_action: "click_dropdown" } })`
|
||||
```ruby
|
||||
# Bad
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], data: { track_label: "explore_groups", track_action: "click_button" })
|
||||
|
||||
vs
|
||||
# Good
|
||||
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { data: { track_label: "explore_groups", track_action: "click_button" } })
|
||||
|
||||
`link_to assigned_issues_dashboard_path, title: _('Issues'), data: { track_label: 'main_navigation', track_action: 'click_issues_link' }`
|
||||
# Good (other helpers)
|
||||
= link_to explore_groups_path, title: _("Explore"), data: { track_label: "explore_groups", track_action: "click_button" }
|
||||
```
|
||||
|
||||
### Tracking within Vue components
|
||||
|
||||
|
|
@ -186,17 +244,19 @@ export default {
|
|||
return {
|
||||
expanded: false,
|
||||
tracking: {
|
||||
label: 'left_sidebar'
|
||||
}
|
||||
label: 'left_sidebar',
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
The mixin provides a `track` method that can be called within the template, or from component methods. An example of the whole implementation might look like the following.
|
||||
The mixin provides a `track` method that can be called within the template,
|
||||
or from component methods. An example of the whole implementation might look like this:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
name: 'RightSidebar',
|
||||
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -206,26 +266,84 @@ export default {
|
|||
methods: {
|
||||
toggle() {
|
||||
this.expanded = !this.expanded;
|
||||
this.track('click_toggle', { value: this.expanded })
|
||||
// Additional data will be merged, like `value` below
|
||||
this.track('click_toggle', { value: Number(this.expanded) });
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
And if needed within the template, you can use the `track` method directly as well.
|
||||
The event data can be provided with a `tracking` object, declared in the `data` function,
|
||||
or as a `computed property`.
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
name: 'RightSidebar',
|
||||
mixins: [Tracking.mixin()],
|
||||
data() {
|
||||
return {
|
||||
tracking: {
|
||||
label: 'right_sidebar',
|
||||
// category: '',
|
||||
// property: '',
|
||||
// value: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The event data can be provided directly in the `track` function as well.
|
||||
This object will merge with any previously provided options.
|
||||
|
||||
```javascript
|
||||
this.track('click_button', {
|
||||
label: 'right_sidebar',
|
||||
});
|
||||
```
|
||||
|
||||
Lastly, if needed within the template, you can use the `track` method directly as well.
|
||||
|
||||
```html
|
||||
<template>
|
||||
<div>
|
||||
<a class="toggle" @click.prevent="toggle">Toggle</a>
|
||||
<button data-testid="toggle" @click="toggle">Toggle</button>
|
||||
|
||||
<div v-if="expanded">
|
||||
<p>Hello world!</p>
|
||||
<a @click.prevent="track('click_action')">Track an event</a>
|
||||
<button @click="track('click_action')">Track another event</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### Testing example
|
||||
|
||||
```javascript
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
// mockTracking(category, documentOverride, spyMethod)
|
||||
|
||||
describe('RightSidebar.vue', () => {
|
||||
let trackingSpy;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
const findToggle = () => wrapper.find('[data-testid="toggle"]');
|
||||
|
||||
it('tracks turning off toggle', () => {
|
||||
findToggle().trigger('click');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
|
||||
label: 'right_sidebar',
|
||||
value: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Tracking in raw JavaScript
|
||||
|
||||
Custom event tracking and instrumentation can be added by directly calling the `Tracking.event` static function. The following example demonstrates tracking a click on a button by calling `Tracking.event` manually.
|
||||
|
|
@ -234,64 +352,35 @@ Custom event tracking and instrumentation can be added by directly calling the `
|
|||
import Tracking from '~/tracking';
|
||||
|
||||
const button = document.getElementById('create_from_template_button');
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
Tracking.event('dashboard:projects:index', 'click_button', {
|
||||
label: 'create_from_template',
|
||||
property: 'template_preview',
|
||||
value: 'rails',
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
### Tests and test helpers
|
||||
|
||||
In Jest particularly in Vue tests, you can use the following:
|
||||
|
||||
```javascript
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
|
||||
describe('MyTracking', () => {
|
||||
let spy;
|
||||
|
||||
beforeEach(() => {
|
||||
spy = mockTracking('_category_', wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
it('tracks an event when clicked on feedback', () => {
|
||||
wrapper.find('.discover-feedback-icon').trigger('click');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('_category_', 'click_button', {
|
||||
label: 'security-discover-feedback-cta',
|
||||
property: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
In obsolete Karma tests it's used as below:
|
||||
#### Testing example
|
||||
|
||||
```javascript
|
||||
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
|
||||
import Tracking from '~/tracking';
|
||||
|
||||
describe('my component', () => {
|
||||
let trackingSpy;
|
||||
describe('MyTracking', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
trackingSpy = mockTracking('_category_', vm.$el, spyOn);
|
||||
jest.spyOn(Tracking, 'event');
|
||||
});
|
||||
|
||||
const triggerEvent = () => {
|
||||
// action which should trigger a event
|
||||
};
|
||||
const findButton = () => wrapper.find('[data-testid="create_from_template"]');
|
||||
|
||||
it('tracks an event when toggled', () => {
|
||||
expect(trackingSpy).not.toHaveBeenCalled();
|
||||
it('tracks event', () => {
|
||||
findButton().trigger('click');
|
||||
|
||||
triggerEvent('a.toggle');
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
|
||||
label: 'right_sidebar',
|
||||
property: 'confidentiality',
|
||||
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
|
||||
label: 'create_from_template',
|
||||
property: 'template_preview',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -355,12 +444,6 @@ There are several tools for developing and testing Snowplow Event
|
|||
|
||||
**{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned
|
||||
|
||||
### Preparing your MR for Review
|
||||
|
||||
1. For frontend events, in the MR description section, add a screenshot of the event's relevant section using the [Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) Chrome browser extension.
|
||||
1. For backend events, please use Snowplow Micro and add the output of the Snowplow Micro good events `GET http://localhost:9090/micro/good`.
|
||||
1. Include a member of the Product Intelligence team as a reviewer of your MR. Mention `@gitlab-org/growth/product_intelligence/engineers` in your MR to request a review.
|
||||
|
||||
### Snowplow Analytics Debugger Chrome Extension
|
||||
|
||||
Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging and local development environments.
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ Product Intelligence files.
|
|||
|
||||
### Roles and process
|
||||
|
||||
The merge request **author** should:
|
||||
#### The merge request **author** should
|
||||
|
||||
- Decide whether a Product Intelligence review is needed.
|
||||
- If a Product Intelligence review is needed, add the labels
|
||||
|
|
@ -48,7 +48,15 @@ The merge request **author** should:
|
|||
[Metrics Dictionary](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/usage_ping/dictionary.md) if it is needed.
|
||||
- Add a changelog [according to guidelines](../changelog.md).
|
||||
|
||||
The Product Intelligence **reviewer** should:
|
||||
##### When adding or modifiying Snowplow events
|
||||
|
||||
- For frontend events, when relevant, add a screenshot of the event in
|
||||
the [testing tool](../snowplow.md#developing-and-testing-snowplow) used.
|
||||
- For backend events, when relevant, add the output of the Snowplow Micro
|
||||
good events `GET http://localhost:9090/micro/good` (it might be a good idea
|
||||
to reset with `GET http://localhost:9090/micro/reset` first).
|
||||
|
||||
#### The Product Intelligence **reviewer** should
|
||||
|
||||
- Perform a first-pass review on the merge request and suggest improvements to the author.
|
||||
- Approve the MR, and relabel the MR with `~"product intelligence::approved"`.
|
||||
|
|
@ -71,6 +79,9 @@ Any of the Product Intelligence engineers can be assigned for the Product Intell
|
|||
- For tracking using Redis HLL (HyperLogLog):
|
||||
- Check the Redis slot.
|
||||
- Check if a [feature flag is needed](index.md#recommendations).
|
||||
- For tracking with Snowplow:
|
||||
- Check that the [event taxonomy](../snowplow.md#structured-event-taxonomy) is correct.
|
||||
- Check the [usage recomendations](../snowplow.md#usage-recommendations).
|
||||
- Metrics YAML definitions:
|
||||
- Check the metric `description`.
|
||||
- Check the metrics `key_path`.
|
||||
|
|
|
|||
|
|
@ -55,29 +55,46 @@ Feature.disable(:security_orchestration_policies_configuration, Project.find(<pr
|
|||
|
||||
## Security Policies project
|
||||
|
||||
The Security Policies feature is a repository to store policies. All security policies are stored in
|
||||
the `.gitlab/security-policies` directory as a YAML file with this format:
|
||||
The Security Policies feature is a repository to store policies. All security policies are stored as
|
||||
the `.gitlab/security-policies/policy.yml` YAML file with this format:
|
||||
|
||||
```yaml
|
||||
---
|
||||
type: scan_execution_policy
|
||||
name: Enforce DAST in every pipeline
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: master
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile A
|
||||
site_profile: Site Profile B
|
||||
scan_execution_policy:
|
||||
- name: Enforce DAST in every pipeline
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: master
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile A
|
||||
site_profile: Site Profile B
|
||||
- name: Enforce DAST in every pipeline in main branch
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan for main branch
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: main
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile C
|
||||
site_profile: Site Profile D
|
||||
```
|
||||
|
||||
### Scan Execution Policies Schema
|
||||
|
||||
The YAML file with Scan Execution Policies consists of an array of objects matching Scan Execution Policy Schema nested under the `scan_execution_policy` key. You can configure a maximum of 5 policies under the `scan_execution_policy` key.
|
||||
|
||||
| Field | Type | Possible values | Description |
|
||||
|-------|------|-----------------|-------------|
|
||||
| `scan_execution_policy` | `array` of Scan Execution Policy | | List of scan execution policies (maximum 5) |
|
||||
|
||||
### Scan Execution Policy Schema
|
||||
|
||||
| Field | Type | Possible values | Description |
|
||||
|-------|------|-----------------|-------------|
|
||||
| `type` | `string` | `scan_execution_policy` | The policy's type. |
|
||||
| `name` | `string` | | Name of the policy. |
|
||||
| `description` (optional) | `string` | | Description of the policy. |
|
||||
| `enabled` | `boolean` | `true`, `false` | Flag to enable (`true`) or disable (`false`) the policy. |
|
||||
|
|
@ -107,7 +124,7 @@ rule in the defined policy are met.
|
|||
Note the following:
|
||||
|
||||
- You must create the [site profile](../dast/index.md#site-profile) and [scanner profile](../dast/index.md#scanner-profile)
|
||||
with selected names for the project that is assigned to the selected Security Policy Project.
|
||||
with selected names for each project that is assigned to the selected Security Policy Project.
|
||||
Otherwise, the policy is not applied and a job with an error message is created instead.
|
||||
- Once you associate the site profile and scanner profile by name in the policy, it is not possible
|
||||
to modify or delete them. If you want to modify them, you must first disable the policy by setting
|
||||
|
|
@ -117,22 +134,37 @@ Here's an example:
|
|||
|
||||
```yaml
|
||||
---
|
||||
type: scan_execution_policy
|
||||
name: Enforce DAST in every pipeline
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: release/*
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile A
|
||||
site_profile: Site Profile B
|
||||
scan_execution_policy:
|
||||
- name: Enforce DAST in every release pipeline
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan for release branches
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: release/*
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile A
|
||||
site_profile: Site Profile B
|
||||
- name: Enforce DAST in every pipeline in main branch
|
||||
description: This policy enforces pipeline configuration to have a job with DAST scan for main branch
|
||||
enabled: true
|
||||
rules:
|
||||
- type: pipeline
|
||||
branch: main
|
||||
actions:
|
||||
- scan: dast
|
||||
scanner_profile: Scanner Profile C
|
||||
site_profile: Site Profile D
|
||||
```
|
||||
|
||||
In this example, the DAST scan runs with the scanner profile `Scanner Profile A` and the site
|
||||
profile `Site Profile B`. The scan runs for every pipeline executed on branches that match the
|
||||
`release/*` wildcard (for example, branch name `release/v1.2.1`).
|
||||
profile `Site Profile B` for every pipeline executed on branches that match the
|
||||
`release/*` wildcard (for example, branch name `release/v1.2.1`); and the DAST scan runs with
|
||||
the scanner profile `Scanner Profile C` and the site profile `Site Profile D` for every pipeline executed on `main` branch.
|
||||
|
||||
NOTE:
|
||||
All scanner and site profiles must be configured and created for each project that is assigned to the selected Security Policy Project.
|
||||
If they are not created, the job will fail with the error message.
|
||||
|
||||
## Security Policy project selection
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ Please note that the certificate [fingerprint algorithm](../../../integration/sa
|
|||
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/211962) in GitLab 13.8 with allowing group owners to not go through SSO.
|
||||
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9152) in GitLab 13.11 with enforcing open SSO session to use Git if this setting is switched on.
|
||||
|
||||
With this option enabled, users must go through your group's GitLab single sign-on URL if they wish to access group resources through the UI. Users can't be manually added as members.
|
||||
With this option enabled, users (except owners) must go through your group's GitLab single sign-on URL if they wish to access group resources through the UI. Users can't be manually added as members.
|
||||
|
||||
SSO enforcement does not affect sign in or access to any resources outside of the group. Users can view which groups and projects they are a member of without SSO sign in.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
|
|
@ -16,6 +16,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
With the Go proxy for GitLab, every project in GitLab can be fetched with the
|
||||
[Go proxy protocol](https://proxy.golang.org/).
|
||||
|
||||
For documentation of the specific API endpoints that the Go Proxy uses, see the
|
||||
[Go Proxy API documentation](../../../api/packages/go_proxy.md).
|
||||
|
||||
## Enable the Go proxy
|
||||
|
||||
The Go proxy for GitLab is under development, and isn't ready for production use
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ You can [comment on](#comment-on-snippets), [clone](#clone-snippets), and
|
|||
[syntax highlighting](#filenames), [embedding](#embed-snippets), [downloading](#download-snippets),
|
||||
and you can maintain your snippets with the [snippets API](../api/snippets.md).
|
||||
|
||||

|
||||

|
||||
|
||||
GitLab provides two types of snippets:
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ A single snippet can support up to 10 files, which helps keep related files toge
|
|||
- A `gulpfile.js` file and a `package.json` file, which together can be
|
||||
used to bootstrap a project and manage its dependencies.
|
||||
|
||||
If you need more than 10 files for your snippet, we recommend you a create a
|
||||
If you need more than 10 files for your snippet, we recommend you create a
|
||||
[wiki](project/wiki/index.md) instead. Wikis are available for projects at all
|
||||
subscription levels, and [groups](project/wiki/index.md#group-wikis) for
|
||||
[GitLab Premium](https://about.gitlab.com/pricing).
|
||||
|
|
|
|||
|
|
@ -31,9 +31,15 @@ msgstr ""
|
|||
msgid " Please sign in."
|
||||
msgstr ""
|
||||
|
||||
msgid " Target Path"
|
||||
msgstr ""
|
||||
|
||||
msgid " Try to %{action} this file again."
|
||||
msgstr ""
|
||||
|
||||
msgid " Type"
|
||||
msgstr ""
|
||||
|
||||
msgid " You need to do this before %{grace_period_deadline}."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5245,6 +5251,9 @@ msgstr ""
|
|||
msgid "Broadcast Messages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more."
|
||||
msgstr ""
|
||||
|
||||
msgid "Browse Directory"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -5682,6 +5691,21 @@ msgstr ""
|
|||
msgid "Capacity threshold"
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|Enforce for all subgroups"
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|Setting enforced"
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|Subgroups cannot change this setting."
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|This setting has been enforced by an instance admin."
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|This setting has been enforced by an owner of %{link}."
|
||||
msgstr ""
|
||||
|
||||
msgid "CascadingSettings|cannot be changed because it is locked by an ancestor"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -6378,6 +6402,9 @@ msgstr ""
|
|||
msgid "Click the button below."
|
||||
msgstr ""
|
||||
|
||||
msgid "Click the link below to confirm your email address."
|
||||
msgstr ""
|
||||
|
||||
msgid "Click to expand it."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -8153,6 +8180,12 @@ msgstr ""
|
|||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm your account"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirm your email address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Confirmation email sent to %{email}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9666,18 +9699,39 @@ msgstr ""
|
|||
msgid "DORA4Metrics|Date"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Days from merge to deploy"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Deployments"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Deployments charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Lead time"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Median lead time"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|No merge requests were deployed during this period"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Something went wrong while getting deployment frequency data"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Something went wrong while getting lead time data."
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|These charts display the frequency of deployments to the production environment, as part of the DORA 4 metrics. The environment must be named %{codeStart}production%{codeEnd} for its data to appear in these charts."
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|These charts display the median time between a merge request being merged and deployed to production, as part of the DORA 4 metrics."
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4|Lead time charts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -11850,6 +11904,9 @@ msgstr ""
|
|||
msgid "End Time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ends"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ends at (UTC)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15350,7 +15407,16 @@ msgstr ""
|
|||
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{customization_link} in instance settings"
|
||||
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. Inherited by subgroups."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Projects will be permanently deleted after a %{waiting_period}-day delay. This delay can be %{link_start}customized by an admin%{link_end} in instance settings. Inherited by subgroups."
|
||||
msgstr ""
|
||||
|
||||
msgid "GroupSettings|Select a sub-group as the custom project template source for this group."
|
||||
|
|
@ -25473,6 +25539,9 @@ msgstr ""
|
|||
msgid "Purchase more storage"
|
||||
msgstr ""
|
||||
|
||||
msgid "PurchaseStep|An error occured in the purchase step. If the problem persists please contact support@gitlab.com."
|
||||
msgstr ""
|
||||
|
||||
msgid "Push"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29512,6 +29581,9 @@ msgstr ""
|
|||
msgid "Starting..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Starts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Starts %{startsIn}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -30618,6 +30690,9 @@ msgstr ""
|
|||
msgid "Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can."
|
||||
msgstr ""
|
||||
|
||||
msgid "Thanks for signing up to GitLab!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Thanks for your purchase!"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34907,6 +34982,9 @@ msgstr ""
|
|||
msgid "Welcome to the guided GitLab tour"
|
||||
msgstr ""
|
||||
|
||||
msgid "Welcome, %{name}!"
|
||||
msgstr ""
|
||||
|
||||
msgid "What are you searching for?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35908,9 +35986,6 @@ msgstr ""
|
|||
msgid "Your U2F device was registered!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your WebAuthn device did not send a valid JSON response."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ module QA
|
|||
all(element_selector_css(name), **kwargs)
|
||||
end
|
||||
|
||||
def check_element(name)
|
||||
def check_element(name, click_by_js = false)
|
||||
if find_element(name, visible: false).checked?
|
||||
QA::Runtime::Logger.debug("#{name} is already checked")
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ module QA
|
|||
end
|
||||
|
||||
retry_until(sleep_interval: 1) do
|
||||
find_element(name, visible: false).click
|
||||
click_checkbox_or_radio(name, click_by_js)
|
||||
checked = find_element(name, visible: false).checked?
|
||||
|
||||
QA::Runtime::Logger.debug(checked ? "#{name} was checked" : "#{name} was not checked")
|
||||
|
|
@ -149,7 +149,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
def uncheck_element(name)
|
||||
def uncheck_element(name, click_by_js = false)
|
||||
unless find_element(name, visible: false).checked?
|
||||
QA::Runtime::Logger.debug("#{name} is already unchecked")
|
||||
|
||||
|
|
@ -157,7 +157,7 @@ module QA
|
|||
end
|
||||
|
||||
retry_until(sleep_interval: 1) do
|
||||
find_element(name, visible: false).click
|
||||
click_checkbox_or_radio(name, click_by_js)
|
||||
unchecked = !find_element(name, visible: false).checked?
|
||||
|
||||
QA::Runtime::Logger.debug(unchecked ? "#{name} was unchecked" : "#{name} was not unchecked")
|
||||
|
|
@ -175,9 +175,7 @@ module QA
|
|||
end
|
||||
|
||||
retry_until(sleep_interval: 1) do
|
||||
radio = find_element(name, visible: false)
|
||||
# Some radio buttons are hidden by their labels and cannot be clicked directly
|
||||
click_by_js ? page.execute_script("arguments[0].click();", radio) : radio.click
|
||||
click_checkbox_or_radio(name, click_by_js)
|
||||
selected = find_element(name, visible: false).checked?
|
||||
|
||||
QA::Runtime::Logger.debug(selected ? "#{name} was selected" : "#{name} was not selected")
|
||||
|
|
@ -423,6 +421,14 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def click_checkbox_or_radio(name, click_by_js)
|
||||
box = find_element(name, visible: false)
|
||||
# Some checkboxes and radio buttons are hidden by their labels and cannot be clicked directly
|
||||
click_by_js ? page.execute_script("arguments[0].click();", box) : box.click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -238,18 +238,12 @@ module QA
|
|||
end
|
||||
|
||||
def mark_to_squash
|
||||
# The squash checkbox is disabled on load
|
||||
wait_until do
|
||||
has_element?(:squash_checkbox)
|
||||
end
|
||||
|
||||
# The squash checkbox is enabled via JS
|
||||
wait_until(reload: false) do
|
||||
!find_element(:squash_checkbox).disabled?
|
||||
!find_element(:squash_checkbox, visible: false).disabled?
|
||||
end
|
||||
|
||||
# TODO: Fix workaround for data-qa-selector failure
|
||||
click_element(:squash_checkbox)
|
||||
check_element(:squash_checkbox, true)
|
||||
end
|
||||
|
||||
def merge!
|
||||
|
|
|
|||
|
|
@ -11,10 +11,17 @@ module QA
|
|||
|
||||
view 'app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue' do
|
||||
element :options_button
|
||||
element :cherry_pick_button
|
||||
element :email_patches
|
||||
element :plain_diff
|
||||
end
|
||||
|
||||
def cherry_pick_commit
|
||||
click_element(:options_button)
|
||||
click_element(:cherry_pick_button, Page::Component::CommitModal)
|
||||
click_element(:submit_commit_button)
|
||||
end
|
||||
|
||||
def select_email_patches
|
||||
click_element :options_button
|
||||
visit_link_in_element :email_patches
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ module QA
|
|||
end
|
||||
|
||||
view 'app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue' do
|
||||
element :service_jira_issue_transition_enabled
|
||||
element :service_jira_issue_transition_automatic_true, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
|
||||
element :service_jira_issue_transition_automatic_false, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
|
||||
element :service_jira_issue_transition_enabled_checkbox
|
||||
element :service_jira_issue_transition_automatic_true_radio, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`"' # rubocop:disable QA/ElementWithPattern
|
||||
element :service_jira_issue_transition_automatic_false_radio, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`"' # rubocop:disable QA/ElementWithPattern
|
||||
element :service_jira_issue_transition_id_field
|
||||
end
|
||||
|
||||
|
|
@ -55,15 +55,15 @@ module QA
|
|||
end
|
||||
|
||||
def enable_transitions
|
||||
click_element_coordinates(:service_jira_issue_transition_enabled, visible: false)
|
||||
check_element(:service_jira_issue_transition_enabled_checkbox, true)
|
||||
end
|
||||
|
||||
def use_automatic_transitions
|
||||
click_element_coordinates(:service_jira_issue_transition_automatic_true, visible: false)
|
||||
choose_element(:service_jira_issue_transition_automatic_true_radio, true)
|
||||
end
|
||||
|
||||
def use_custom_transitions
|
||||
click_element_coordinates(:service_jira_issue_transition_automatic_false, visible: false)
|
||||
choose_element(:service_jira_issue_transition_automatic_false_radio, true)
|
||||
end
|
||||
|
||||
def set_transition_ids(transition_ids)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module QA
|
|||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'cherry picks a basic merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1616' do
|
||||
it 'creates a merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1616' do
|
||||
feature_mr.visit!
|
||||
|
||||
Page::MergeRequest::Show.perform do |merge_request|
|
||||
|
|
@ -30,8 +30,11 @@ module QA
|
|||
merge_request.cherry_pick!
|
||||
end
|
||||
|
||||
Page::MergeRequest::New.perform do |merge_request|
|
||||
expect(merge_request).to have_content('The merge request has been successfully cherry-picked')
|
||||
Page::MergeRequest::New.perform(&:create_merge_request)
|
||||
|
||||
Page::MergeRequest::Show.perform do |merge_request|
|
||||
merge_request.click_diffs_tab
|
||||
expect(merge_request).to have_file(feature_mr.file_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Cherry picking a commit' do
|
||||
let(:file_name) { "secret_file.md" }
|
||||
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'project'
|
||||
project.initialize_with_readme = true
|
||||
end
|
||||
end
|
||||
|
||||
let(:commit) do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.branch = "development"
|
||||
commit.start_branch = project.default_branch
|
||||
commit.commit_message = 'Add new file'
|
||||
commit.add_files([
|
||||
{ file_path: file_name, content: 'pssst!' }
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
commit.visit!
|
||||
end
|
||||
|
||||
it 'creates a merge request', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1752' do
|
||||
Page::Project::Commit::Show.perform(&:cherry_pick_commit)
|
||||
Page::MergeRequest::New.perform(&:create_merge_request)
|
||||
|
||||
Page::MergeRequest::Show.perform do |merge_request|
|
||||
merge_request.click_diffs_tab
|
||||
expect(merge_request).to have_file(file_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -52,13 +52,13 @@ module QA
|
|||
elements
|
||||
end
|
||||
|
||||
def check_element(name)
|
||||
def check_element(name, click_by_js = nil)
|
||||
log("checking :#{name}")
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def uncheck_element(name)
|
||||
def uncheck_element(name, click_by_js = nil)
|
||||
log("unchecking :#{name}")
|
||||
|
||||
super
|
||||
|
|
|
|||
|
|
@ -57,4 +57,16 @@ RSpec.describe RendersCommits do
|
|||
end.not_to exceed_all_query_limit(control_count)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.prepare_commits_for_rendering' do
|
||||
it 'avoids N+1' do
|
||||
control = ActiveRecord::QueryRecorder.new do
|
||||
subject.prepare_commits_for_rendering(merge_request.commits.take(1))
|
||||
end
|
||||
|
||||
expect do
|
||||
subject.prepare_commits_for_rendering(merge_request.commits)
|
||||
end.not_to exceed_all_query_limit(control.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::CommitController do
|
||||
include ProjectForksHelper
|
||||
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
|
|
@ -295,6 +297,102 @@ RSpec.describe Projects::CommitController do
|
|||
expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a project has a fork' do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
|
||||
let(:target_project) { project }
|
||||
let(:create_merge_request) { nil }
|
||||
|
||||
def send_request
|
||||
post(:cherry_pick,
|
||||
params: {
|
||||
namespace_id: forked_project.namespace,
|
||||
project_id: forked_project,
|
||||
target_project_id: target_project.id,
|
||||
start_branch: 'feature',
|
||||
id: forked_project.commit.id,
|
||||
create_merge_request: create_merge_request
|
||||
})
|
||||
end
|
||||
|
||||
def merge_request_url(source_project, branch)
|
||||
project_new_merge_request_path(
|
||||
source_project,
|
||||
merge_request: {
|
||||
source_project_id: source_project.id,
|
||||
target_project_id: project.id,
|
||||
source_branch: branch,
|
||||
target_branch: 'feature'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
forked_project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'successfully cherry picks a commit from fork to upstream project' do
|
||||
send_request
|
||||
|
||||
expect(response).to redirect_to project_commits_path(project, 'feature')
|
||||
expect(flash[:notice]).to eq('The commit has been successfully cherry-picked into feature.')
|
||||
expect(project.commit('feature').message).to include(forked_project.commit.id)
|
||||
end
|
||||
|
||||
context 'when the cherry pick is performed via merge request' do
|
||||
let(:create_merge_request) { true }
|
||||
|
||||
it 'successfully cherry picks a commit from fork to a cherry pick branch' do
|
||||
branch = forked_project.commit.cherry_pick_branch_name
|
||||
send_request
|
||||
|
||||
expect(response).to redirect_to merge_request_url(project, branch)
|
||||
expect(flash[:notice]).to start_with("The commit has been successfully cherry-picked into #{branch}")
|
||||
expect(project.commit(branch).message).to include(forked_project.commit.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user cannot push to upstream project' do
|
||||
let(:create_merge_request) { true }
|
||||
|
||||
before do
|
||||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
it 'cherry picks a commit to the fork' do
|
||||
branch = forked_project.commit.cherry_pick_branch_name
|
||||
send_request
|
||||
|
||||
expect(response).to redirect_to merge_request_url(forked_project, branch)
|
||||
expect(flash[:notice]).to start_with("The commit has been successfully cherry-picked into #{branch}")
|
||||
expect(project.commit('feature').message).not_to include(forked_project.commit.id)
|
||||
expect(forked_project.commit(branch).message).to include(forked_project.commit.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user do not have access to the target project' do
|
||||
let(:target_project) { create(:project, :private) }
|
||||
|
||||
it 'cherry picks a commit to the fork' do
|
||||
send_request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'disable pick_into_project feature flag' do
|
||||
before do
|
||||
stub_feature_flags(pick_into_project: false)
|
||||
end
|
||||
|
||||
it 'does not cherry pick a commit from fork to upstream' do
|
||||
send_request
|
||||
|
||||
expect(project.commit('feature').message).not_to include(forked_project.commit.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET diff_for_path' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
import { GlPopover } from '@gitlab/ui';
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import LockPopovers from '~/namespaces/cascading_settings/components/lock_popovers.vue';
|
||||
|
||||
describe('LockPopovers', () => {
|
||||
const mockNamespace = {
|
||||
full_name: 'GitLab Org / GitLab',
|
||||
path: '/gitlab-org/gitlab/-/edit',
|
||||
};
|
||||
|
||||
const createPopoverMountEl = ({
|
||||
lockedByApplicationSetting = false,
|
||||
lockedByAncestor = false,
|
||||
}) => {
|
||||
const popoverMountEl = document.createElement('div');
|
||||
popoverMountEl.classList.add('js-cascading-settings-lock-popover-target');
|
||||
|
||||
const popoverData = {
|
||||
locked_by_application_setting: lockedByApplicationSetting,
|
||||
locked_by_ancestor: lockedByAncestor,
|
||||
};
|
||||
|
||||
if (lockedByApplicationSetting) {
|
||||
popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
|
||||
} else if (lockedByAncestor) {
|
||||
popoverMountEl.setAttribute(
|
||||
'data-popover-data',
|
||||
JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
|
||||
);
|
||||
}
|
||||
|
||||
document.body.appendChild(popoverMountEl);
|
||||
|
||||
return popoverMountEl;
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
const createWrapper = () => {
|
||||
wrapper = mountExtended(LockPopovers);
|
||||
};
|
||||
|
||||
const findPopover = () => extendedWrapper(wrapper.find(GlPopover));
|
||||
const findByTextInPopover = (text, options) =>
|
||||
findPopover().findByText((_, element) => element.textContent === text, options);
|
||||
|
||||
const expectPopoverMessageExists = (message) => {
|
||||
expect(findByTextInPopover(message).exists()).toBe(true);
|
||||
};
|
||||
const expectCorrectPopoverTarget = (popoverMountEl, popover = findPopover()) => {
|
||||
expect(popover.props('target')).toEqual(popoverMountEl);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
describe('when setting is locked by an application setting', () => {
|
||||
let popoverMountEl;
|
||||
|
||||
beforeEach(() => {
|
||||
popoverMountEl = createPopoverMountEl({ lockedByApplicationSetting: true });
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('displays correct popover message', () => {
|
||||
expectPopoverMessageExists('This setting has been enforced by an instance admin.');
|
||||
});
|
||||
|
||||
it('sets `target` prop correctly', () => {
|
||||
expectCorrectPopoverTarget(popoverMountEl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setting is locked by an ancestor namespace', () => {
|
||||
let popoverMountEl;
|
||||
|
||||
beforeEach(() => {
|
||||
popoverMountEl = createPopoverMountEl({ lockedByAncestor: true });
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('displays correct popover message', () => {
|
||||
expectPopoverMessageExists(
|
||||
`This setting has been enforced by an owner of ${mockNamespace.full_name}.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('displays link to ancestor namespace', () => {
|
||||
expect(
|
||||
findByTextInPopover(mockNamespace.full_name, {
|
||||
selector: `a[href="${mockNamespace.path}"]`,
|
||||
}).exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('sets `target` prop correctly', () => {
|
||||
expectCorrectPopoverTarget(popoverMountEl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setting is locked by an application setting and an ancestor namespace', () => {
|
||||
let popoverMountEl;
|
||||
|
||||
beforeEach(() => {
|
||||
popoverMountEl = createPopoverMountEl({
|
||||
lockedByAncestor: true,
|
||||
lockedByApplicationSetting: true,
|
||||
});
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('application setting takes precedence and correct message is shown', () => {
|
||||
expectPopoverMessageExists('This setting has been enforced by an instance admin.');
|
||||
});
|
||||
|
||||
it('sets `target` prop correctly', () => {
|
||||
expectCorrectPopoverTarget(popoverMountEl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setting is not locked', () => {
|
||||
beforeEach(() => {
|
||||
createPopoverMountEl({
|
||||
lockedByAncestor: false,
|
||||
lockedByApplicationSetting: false,
|
||||
});
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('does not render popover', () => {
|
||||
expect(findPopover().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are multiple mount elements', () => {
|
||||
let popoverMountEl1;
|
||||
let popoverMountEl2;
|
||||
|
||||
beforeEach(() => {
|
||||
popoverMountEl1 = createPopoverMountEl({ lockedByApplicationSetting: true });
|
||||
popoverMountEl2 = createPopoverMountEl({ lockedByAncestor: true });
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('mounts multiple popovers', () => {
|
||||
const popovers = wrapper.findAll(GlPopover).wrappers;
|
||||
|
||||
expectCorrectPopoverTarget(popoverMountEl1, popovers[0]);
|
||||
expectCorrectPopoverTarget(popoverMountEl2, popovers[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<div class='whats-new-notification-fixture-root'>
|
||||
<div class='app' data-storage-key='storage-key'></div>
|
||||
<div class='app' data-version-digest='version-digest'></div>
|
||||
<div class='header-help'>
|
||||
<div class='js-whats-new-notification-count'></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -178,6 +178,30 @@ describe('timeIntervalInWords', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('humanizeTimeInterval', () => {
|
||||
it.each`
|
||||
intervalInSeconds | expected
|
||||
${0} | ${'0 seconds'}
|
||||
${1} | ${'1 second'}
|
||||
${1.48} | ${'1.5 seconds'}
|
||||
${2} | ${'2 seconds'}
|
||||
${60} | ${'1 minute'}
|
||||
${91} | ${'1.5 minutes'}
|
||||
${120} | ${'2 minutes'}
|
||||
${3600} | ${'1 hour'}
|
||||
${5401} | ${'1.5 hours'}
|
||||
${7200} | ${'2 hours'}
|
||||
${86400} | ${'1 day'}
|
||||
${129601} | ${'1.5 days'}
|
||||
${172800} | ${'2 days'}
|
||||
`(
|
||||
'returns "$expected" when the time interval is $intervalInSeconds seconds',
|
||||
({ intervalInSeconds, expected }) => {
|
||||
expect(datetimeUtility.humanizeTimeInterval(intervalInSeconds)).toBe(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('dateInWords', () => {
|
||||
const date = new Date('07/01/2016');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GRAPHQL, STAGE_VIEW } from '~/pipelines/components/graph/constants';
|
||||
import { GRAPHQL, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
|
||||
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
|
||||
import JobItem from '~/pipelines/components/graph/job_item.vue';
|
||||
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
|
||||
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
|
||||
import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
|
||||
import { listByLayers } from '~/pipelines/components/parsing_utils';
|
||||
import {
|
||||
generateResponse,
|
||||
mockPipelineResponse,
|
||||
|
|
@ -17,6 +18,7 @@ describe('graph component', () => {
|
|||
const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn);
|
||||
const findLinksLayer = () => wrapper.find(LinksLayer);
|
||||
const findStageColumns = () => wrapper.findAll(StageColumnComponent);
|
||||
const findStageNameInJob = () => wrapper.find('[data-testid="stage-name-in-job"]');
|
||||
|
||||
const defaultProps = {
|
||||
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
|
||||
|
|
@ -82,6 +84,10 @@ describe('graph component', () => {
|
|||
expect(findLinksLayer().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not display stage name on the job in default (stage) mode', () => {
|
||||
expect(findStageNameInJob().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when column requests a refresh', () => {
|
||||
beforeEach(() => {
|
||||
findStageColumns().at(0).vm.$emit('refreshPipelineGraph');
|
||||
|
|
@ -93,7 +99,7 @@ describe('graph component', () => {
|
|||
});
|
||||
|
||||
describe('when links are present', () => {
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mount,
|
||||
stubOverride: { 'job-item': false },
|
||||
|
|
@ -132,4 +138,24 @@ describe('graph component', () => {
|
|||
expect(findLinkedColumns()).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('in layers mode', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mount,
|
||||
stubOverride: {
|
||||
'job-item': false,
|
||||
'job-group-dropdown': false,
|
||||
},
|
||||
props: {
|
||||
viewType: LAYER_VIEW,
|
||||
pipelineLayers: listByLayers(defaultProps.pipeline),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the stage name on the job', () => {
|
||||
expect(findStageNameInJob().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import JobGroupDropdown from '~/pipelines/components/graph/job_group_dropdown.vue';
|
||||
|
||||
describe('job group dropdown component', () => {
|
||||
|
|
@ -65,12 +65,16 @@ describe('job group dropdown component', () => {
|
|||
let wrapper;
|
||||
const findButton = () => wrapper.find('button');
|
||||
|
||||
const createComponent = ({ mountFn = shallowMount }) => {
|
||||
wrapper = mountFn(JobGroupDropdown, { propsData: { group } });
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(JobGroupDropdown, { propsData: { group } });
|
||||
createComponent({ mountFn: mount });
|
||||
});
|
||||
|
||||
it('renders button with group name and size', () => {
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ describe('pipeline graph job item', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toBe('test');
|
||||
expect(findJobWithoutLink().attributes('title')).toBe('test');
|
||||
});
|
||||
|
||||
it('should not render status label when it is provided', () => {
|
||||
|
|
@ -138,7 +138,7 @@ describe('pipeline graph job item', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.js-job-component-tooltip').attributes('title')).toBe('test - success');
|
||||
expect(findJobWithoutLink().attributes('title')).toBe('test - success');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const mockGroups = Array(4)
|
|||
});
|
||||
|
||||
const defaultProps = {
|
||||
title: 'Fish',
|
||||
name: 'Fish',
|
||||
groups: mockGroups,
|
||||
pipelineId: 159,
|
||||
};
|
||||
|
|
@ -62,7 +62,7 @@ describe('stage column component', () => {
|
|||
});
|
||||
|
||||
it('should render provided title', () => {
|
||||
expect(findStageColumnTitle().text()).toBe(defaultProps.title);
|
||||
expect(findStageColumnTitle().text()).toBe(defaultProps.name);
|
||||
});
|
||||
|
||||
it('should render the provided groups', () => {
|
||||
|
|
@ -119,7 +119,7 @@ describe('stage column component', () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
title: 'test <img src=x onerror=alert(document.domain)>',
|
||||
name: 'test <img src=x onerror=alert(document.domain)>',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_char
|
|||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
|
||||
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
|
||||
|
||||
describe('ProjectsPipelinesChartsApp', () => {
|
||||
let wrapper;
|
||||
|
|
@ -25,6 +26,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
},
|
||||
stubs: {
|
||||
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
|
||||
LeadTimeCharts: LeadTimeChartsStub,
|
||||
},
|
||||
},
|
||||
mountOptions,
|
||||
|
|
@ -44,6 +46,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
const findGlTabs = () => wrapper.find(GlTabs);
|
||||
const findAllGlTab = () => wrapper.findAll(GlTab);
|
||||
const findGlTabAt = (i) => findAllGlTab().at(i);
|
||||
const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
|
||||
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
|
||||
const findPipelineCharts = () => wrapper.find(PipelineCharts);
|
||||
|
||||
|
|
@ -51,15 +54,23 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
expect(findPipelineCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the lead time charts', () => {
|
||||
expect(findLeadTimeCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
|
||||
});
|
||||
|
||||
it('renders the deployment frequency charts in a tab', () => {
|
||||
it('renders the expected tabs', () => {
|
||||
expect(findGlTabs().exists()).toBe(true);
|
||||
expect(findGlTabAt(0).attributes('title')).toBe('Pipelines');
|
||||
expect(findGlTabAt(1).attributes('title')).toBe('Deployments');
|
||||
expect(findGlTabAt(2).attributes('title')).toBe('Lead Time');
|
||||
});
|
||||
|
||||
it('renders the deployment frequency charts', () => {
|
||||
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
|
@ -108,6 +119,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
describe('when provided with a query param', () => {
|
||||
it.each`
|
||||
chart | tab
|
||||
${'lead-time'} | ${'2'}
|
||||
${'deployments'} | ${'1'}
|
||||
${'pipelines'} | ${'0'}
|
||||
${'fake'} | ${'0'}
|
||||
|
|
@ -160,8 +172,13 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: false } });
|
||||
});
|
||||
|
||||
it('renders the expected tabs', () => {
|
||||
expect(findGlTabs().exists()).toBe(true);
|
||||
expect(findGlTabAt(0).attributes('title')).toBe('Pipelines');
|
||||
expect(findGlTabAt(1).attributes('title')).toBe('Lead Time');
|
||||
});
|
||||
|
||||
it('does not render the deployment frequency charts in a tab', () => {
|
||||
expect(findGlTabs().exists()).toBe(false);
|
||||
expect(findDeploymentFrequencyCharts().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GlDrawer, GlInfiniteScroll, GlTabs } from '@gitlab/ui';
|
||||
import { GlDrawer, GlInfiniteScroll } from '@gitlab/ui';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
|
||||
|
|
@ -21,12 +21,9 @@ describe('App', () => {
|
|||
let actions;
|
||||
let state;
|
||||
let trackingSpy;
|
||||
let gitlabDotCom = true;
|
||||
|
||||
const buildProps = () => ({
|
||||
storageKey: 'storage-key',
|
||||
versions: ['3.11', '3.10'],
|
||||
gitlabDotCom,
|
||||
versionDigest: 'version-digest',
|
||||
});
|
||||
|
||||
const buildWrapper = () => {
|
||||
|
|
@ -91,7 +88,7 @@ describe('App', () => {
|
|||
});
|
||||
|
||||
it('dispatches openDrawer and tracking calls when mounted', () => {
|
||||
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'storage-key');
|
||||
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'version-digest');
|
||||
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
|
||||
label: 'namespace_id',
|
||||
value: 'namespace-840',
|
||||
|
|
@ -176,54 +173,4 @@ describe('App', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('self managed', () => {
|
||||
const findTabs = () => wrapper.find(GlTabs);
|
||||
|
||||
const clickSecondTab = async () => {
|
||||
const secondTab = wrapper.findAll('.nav-link').at(1);
|
||||
await secondTab.trigger('click');
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
gitlabDotCom = false;
|
||||
setup();
|
||||
});
|
||||
|
||||
it('renders tabs with drawer body height and content', () => {
|
||||
const scroll = findInfiniteScroll();
|
||||
const tabs = findTabs();
|
||||
|
||||
expect(scroll.exists()).toBe(false);
|
||||
expect(tabs.attributes().style).toBe(`height: ${MOCK_DRAWER_BODY_HEIGHT}px;`);
|
||||
expect(wrapper.find('h5').text()).toBe('Whats New Drawer');
|
||||
});
|
||||
|
||||
describe('fetchVersion', () => {
|
||||
beforeEach(() => {
|
||||
actions.fetchItems.mockClear();
|
||||
});
|
||||
|
||||
it('when version isnt fetched, clicking a tab calls fetchItems', async () => {
|
||||
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
|
||||
await clickSecondTab();
|
||||
|
||||
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
|
||||
expect(actions.fetchItems).toHaveBeenCalledWith(expect.anything(), { version: '3.10' });
|
||||
});
|
||||
|
||||
it('when version has been fetched, clicking a tab calls fetchItems', async () => {
|
||||
wrapper.vm.$store.state.features.push({ title: 'GitLab Stories', release: 3.1 });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const fetchVersionSpy = jest.spyOn(wrapper.vm, 'fetchVersion');
|
||||
await clickSecondTab();
|
||||
|
||||
expect(fetchVersionSpy).toHaveBeenCalledWith('3.10');
|
||||
expect(actions.fetchItems).not.toHaveBeenCalled();
|
||||
expect(wrapper.find('.tab-pane.active h5').text()).toBe('GitLab Stories');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,9 +11,12 @@ describe('whats new actions', () => {
|
|||
useLocalStorageSpy();
|
||||
|
||||
it('should commit openDrawer', () => {
|
||||
testAction(actions.openDrawer, 'storage-key', {}, [{ type: types.OPEN_DRAWER }]);
|
||||
testAction(actions.openDrawer, 'digest-hash', {}, [{ type: types.OPEN_DRAWER }]);
|
||||
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith('storage-key', 'false');
|
||||
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||
'display-whats-new-notification',
|
||||
'digest-hash',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -45,12 +48,12 @@ describe('whats new actions', () => {
|
|||
axiosMock.reset();
|
||||
|
||||
axiosMock
|
||||
.onGet('/-/whats_new', { params: { page: 8, version: 40 } })
|
||||
.onGet('/-/whats_new', { params: { page: 8 } })
|
||||
.replyOnce(200, [{ title: 'GitLab Stories' }]);
|
||||
|
||||
testAction(
|
||||
actions.fetchItems,
|
||||
{ page: 8, version: 40 },
|
||||
{ page: 8 },
|
||||
{},
|
||||
expect.arrayContaining([
|
||||
{ type: types.ADD_FEATURES, payload: [{ title: 'GitLab Stories' }] },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import { setNotification, getStorageKey } from '~/whats_new/utils/notification';
|
||||
import { setNotification, getVersionDigest } from '~/whats_new/utils/notification';
|
||||
|
||||
describe('~/whats_new/utils/notification', () => {
|
||||
useLocalStorageSpy();
|
||||
|
|
@ -33,10 +33,23 @@ describe('~/whats_new/utils/notification', () => {
|
|||
expect(notificationEl.classList).toContain('with-notifications');
|
||||
});
|
||||
|
||||
it('removes class and count element when storage key is true', () => {
|
||||
it('removes class and count element when legacy storage key is false', () => {
|
||||
const notificationEl = findNotificationEl();
|
||||
notificationEl.classList.add('with-notifications');
|
||||
localStorage.setItem('storage-key', 'false');
|
||||
localStorage.setItem('display-whats-new-notification-13.10', 'false');
|
||||
|
||||
expect(findNotificationCountEl()).toExist();
|
||||
|
||||
subject();
|
||||
|
||||
expect(findNotificationCountEl()).not.toExist();
|
||||
expect(notificationEl.classList).not.toContain('with-notifications');
|
||||
});
|
||||
|
||||
it('removes class and count element when storage key has current digest', () => {
|
||||
const notificationEl = findNotificationEl();
|
||||
notificationEl.classList.add('with-notifications');
|
||||
localStorage.setItem('display-whats-new-notification', 'version-digest');
|
||||
|
||||
expect(findNotificationCountEl()).toExist();
|
||||
|
||||
|
|
@ -47,9 +60,9 @@ describe('~/whats_new/utils/notification', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getStorageKey', () => {
|
||||
describe('getVersionDigest', () => {
|
||||
it('retrieves the storage key data attribute from the el', () => {
|
||||
expect(getStorageKey(getAppEl())).toBe('storage-key');
|
||||
expect(getVersionDigest(getAppEl())).toBe('version-digest');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ RSpec.describe CommitsHelper do
|
|||
end
|
||||
|
||||
it 'returns data for cherry picking into a project' do
|
||||
expect(helper.cherry_pick_projects_data(project)).to match_array([
|
||||
expect(helper.cherry_pick_projects_data(forked_project)).to match_array([
|
||||
{ id: project.id.to_s, name: project.full_path, refsUrl: refs_project_path(project) },
|
||||
{ id: forked_project.id.to_s, name: forked_project.full_path, refsUrl: refs_project_path(forked_project) }
|
||||
])
|
||||
|
|
|
|||
|
|
@ -194,4 +194,75 @@ RSpec.describe NamespacesHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cascading_namespace_settings_enabled?' do
|
||||
subject { helper.cascading_namespace_settings_enabled? }
|
||||
|
||||
context 'when `cascading_namespace_settings` feature flag is enabled' do
|
||||
it 'returns `true`' do
|
||||
expect(subject).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when `cascading_namespace_settings` feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(cascading_namespace_settings: false)
|
||||
end
|
||||
|
||||
it 'returns `false`' do
|
||||
expect(subject).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cascading_namespace_settings_popover_data' do
|
||||
attribute = :delayed_project_removal
|
||||
|
||||
subject do
|
||||
helper.cascading_namespace_settings_popover_data(
|
||||
attribute,
|
||||
subgroup1,
|
||||
-> (locked_ancestor) { edit_group_path(locked_ancestor, anchor: 'js-permissions-settings') }
|
||||
)
|
||||
end
|
||||
|
||||
context 'when locked by an application setting' do
|
||||
before do
|
||||
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_application_setting?").and_return(true)
|
||||
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_ancestor?").and_return(false)
|
||||
end
|
||||
|
||||
it 'returns expected hash' do
|
||||
expect(subject).to match({
|
||||
popover_data: {
|
||||
locked_by_application_setting: true,
|
||||
locked_by_ancestor: false
|
||||
}.to_json,
|
||||
testid: 'cascading-settings-lock-icon'
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when locked by an ancestor namespace' do
|
||||
before do
|
||||
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_application_setting?").and_return(false)
|
||||
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_by_ancestor?").and_return(true)
|
||||
allow(subgroup1.namespace_settings).to receive("#{attribute}_locked_ancestor").and_return(admin_group.namespace_settings)
|
||||
end
|
||||
|
||||
it 'returns expected hash' do
|
||||
expect(subject).to match({
|
||||
popover_data: {
|
||||
locked_by_application_setting: false,
|
||||
locked_by_ancestor: true,
|
||||
ancestor_namespace: {
|
||||
full_name: admin_group.full_name,
|
||||
path: edit_group_path(admin_group, anchor: 'js-permissions-settings')
|
||||
}
|
||||
}.to_json,
|
||||
testid: 'cascading-settings-lock-icon'
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,25 +3,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WhatsNewHelper do
|
||||
describe '#whats_new_storage_key' do
|
||||
subject { helper.whats_new_storage_key }
|
||||
describe '#whats_new_version_digest' do
|
||||
let(:digest) { 'digest' }
|
||||
|
||||
context 'when version exist' do
|
||||
let(:release_item) { double(:item) }
|
||||
it 'calls ReleaseHighlight.most_recent_version_digest' do
|
||||
expect(ReleaseHighlight).to receive(:most_recent_version_digest).and_return(digest)
|
||||
|
||||
before do
|
||||
allow(ReleaseHighlight).to receive(:versions).and_return([84.0])
|
||||
end
|
||||
|
||||
it { is_expected.to eq('display-whats-new-notification-84.0') }
|
||||
end
|
||||
|
||||
context 'when most recent release highlights do NOT exist' do
|
||||
before do
|
||||
allow(ReleaseHighlight).to receive(:versions).and_return(nil)
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
expect(helper.whats_new_version_digest).to eq(digest)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -44,14 +32,4 @@ RSpec.describe WhatsNewHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#whats_new_versions' do
|
||||
let(:versions) { [84.0] }
|
||||
|
||||
it 'returns ReleaseHighlight.versions' do
|
||||
expect(ReleaseHighlight).to receive(:versions).and_return(versions)
|
||||
|
||||
expect(helper.whats_new_versions).to eq(versions)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,26 +13,6 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
|
|||
ReleaseHighlight.instance_variable_set(:@file_paths, nil)
|
||||
end
|
||||
|
||||
describe '.for_version' do
|
||||
subject { ReleaseHighlight.for_version(version: version) }
|
||||
|
||||
let(:version) { '1.1' }
|
||||
|
||||
context 'with version param that exists' do
|
||||
it 'returns items from that version' do
|
||||
expect(subject.items.first['title']).to eq("It's gonna be a bright")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with version param that does NOT exist' do
|
||||
let(:version) { '84.0' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.paginated' do
|
||||
let(:dot_com) { false }
|
||||
|
||||
|
|
@ -143,28 +123,27 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.versions' do
|
||||
subject { described_class.versions }
|
||||
describe '.most_recent_version_digest' do
|
||||
subject { ReleaseHighlight.most_recent_version_digest }
|
||||
|
||||
it 'uses process memory cache' do
|
||||
expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:versions:#{Gitlab.revision}", { expires_in: described_class::CACHE_DURATION })
|
||||
expect(Gitlab::ProcessMemoryCache.cache_backend).to receive(:fetch).with("release_highlight:most_recent_version_digest:#{Gitlab.revision}", expires_in: described_class::CACHE_DURATION)
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'returns versions from the file paths' do
|
||||
expect(subject).to eq(['1.5', '1.2', '1.1'])
|
||||
context 'when recent release items exist' do
|
||||
it 'returns a digest from the release of the first item of the most recent file' do
|
||||
# this value is coming from fixture data
|
||||
expect(subject).to eq(Digest::SHA256.hexdigest('01.05'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are more than 12 versions' do
|
||||
let(:file_paths) do
|
||||
i = 0
|
||||
Array.new(20) { "20201225_01_#{i += 1}.yml" }
|
||||
end
|
||||
context 'when recent release items do NOT exist' do
|
||||
it 'returns nil' do
|
||||
allow(ReleaseHighlight).to receive(:paginated).and_return(nil)
|
||||
|
||||
it 'limits to 12 versions' do
|
||||
allow(ReleaseHighlight).to receive(:file_paths).and_return(file_paths)
|
||||
expect(subject.count).to eq(12)
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,16 +35,5 @@ RSpec.describe WhatsNewController, :clean_gitlab_redis_cache do
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with version param' do
|
||||
it 'returns items without pagination headers' do
|
||||
allow(ReleaseHighlight).to receive(:for_version).with(version: '42').and_return(highlights)
|
||||
|
||||
get whats_new_path(version: 42), xhr: true
|
||||
|
||||
expect(response.body).to eq(highlights.items.to_json)
|
||||
expect(response.headers['X-Next-Page']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
module CycleAnalyticsHelpers
|
||||
include GitHelpers
|
||||
|
||||
def wait_for_stages_to_load
|
||||
expect(page).to have_selector '.js-stage-table'
|
||||
def wait_for_stages_to_load(selector = '.js-path-navigation')
|
||||
expect(page).to have_selector selector
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def select_group(target_group)
|
||||
def select_group(target_group, ready_selector = '.js-path-navigation')
|
||||
visit group_analytics_cycle_analytics_path(target_group)
|
||||
|
||||
wait_for_stages_to_load
|
||||
wait_for_stages_to_load(ready_selector)
|
||||
end
|
||||
|
||||
def toggle_dropdown(field)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'a cascading setting' do
|
||||
context 'when setting is enforced by an ancestor group' do
|
||||
before do
|
||||
visit group_path
|
||||
|
||||
page.within form_group_selector do
|
||||
find(setting_field_selector).check
|
||||
find('[data-testid="enforce-for-all-subgroups-checkbox"]').check
|
||||
end
|
||||
|
||||
click_save_button
|
||||
end
|
||||
|
||||
it 'disables setting in subgroups' do
|
||||
visit subgroup_path
|
||||
|
||||
expect(find("#{setting_field_selector}[disabled]")).to be_checked
|
||||
end
|
||||
|
||||
it 'does not show enforcement checkbox in subgroups' do
|
||||
visit subgroup_path
|
||||
|
||||
expect(page).not_to have_selector '[data-testid="enforce-for-all-subgroups-checkbox"]'
|
||||
end
|
||||
|
||||
it 'displays lock icon with popover', :js do
|
||||
visit subgroup_path
|
||||
|
||||
page.within form_group_selector do
|
||||
find('[data-testid="cascading-settings-lock-icon"]').click
|
||||
end
|
||||
|
||||
page.within '[data-testid="cascading-settings-lock-popover"]' do
|
||||
expect(page).to have_text 'This setting has been enforced by an owner of Foo bar.'
|
||||
expect(page).to have_link 'Foo bar', href: setting_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue