Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-14 18:10:59 +00:00
parent 8106ac487c
commit ea3306a15e
111 changed files with 2014 additions and 937 deletions

View File

@ -1 +1 @@
3083074640633df94cbeee611795a6fc6d8c5607
e9743661a588b99e51cf7f806eaaf137573e5c02

View File

@ -56,10 +56,11 @@ export default {
},
computed: {
...mapState({
noteableData: (state) => state.notes.noteableData,
diffViewType: (state) => state.diffs.diffViewType,
diffViewType: ({ diffs }) => diffs.diffViewType,
showSuggestPopover: ({ diffs }) => diffs.showSuggestPopover,
noteableData: ({ notes }) => notes.noteableData,
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
}),
...mapState('diffs', ['showSuggestPopover']),
...mapGetters('diffs', ['getDiffFileByHash', 'diffLines']),
...mapGetters([
'isLoggedIn',
@ -126,6 +127,10 @@ export default {
this.initAutoSave(this.noteableData, keys);
}
if (this.selectedCommentPosition) {
this.commentLineStart = this.selectedCommentPosition.start;
}
},
methods: {
...mapActions('diffs', [

View File

@ -10,6 +10,7 @@ import {
CONFLICT_THEIR,
CONFLICT_MARKER,
} from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
@ -22,6 +23,7 @@ export default {
GlTooltip: GlTooltipDirective,
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
fileHash: {
type: String,
@ -45,6 +47,15 @@ export default {
required: false,
default: false,
},
index: {
type: Number,
required: true,
},
},
data() {
return {
dragging: false,
};
},
computed: {
...mapGetters('diffs', ['fileLineCoverage']),
@ -52,26 +63,35 @@ export default {
...mapState({
isHighlighted(state) {
const line = this.line.left?.line_code ? this.line.left : this.line.right;
return utils.isHighlighted(state, line, this.isCommented);
return utils.isHighlighted(state, line, false);
},
}),
classNameMap() {
return {
[CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
[PARALLEL_DIFF_VIEW_TYPE]: !this.inline,
commented: this.isCommented,
};
},
parallelViewLeftLineType() {
return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented);
},
coverageState() {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
return utils.classNameMapCell(this.line.left, this.isHighlighted, this.isLoggedIn);
return utils.classNameMapCell({
line: this.line.left,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
});
},
classNameMapCellRight() {
return utils.classNameMapCell(this.line.right, this.isHighlighted, this.isLoggedIn);
return utils.classNameMapCell({
line: this.line.right,
hll: this.isHighlighted || this.isCommented,
isLoggedIn: this.isLoggedIn,
});
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);
@ -131,6 +151,22 @@ export default {
? this.$options.THEIR_CHANGES
: this.$options.OUR_CHANGES;
},
onDragEnd() {
this.dragging = false;
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('stopdragging');
},
onDragEnter(line, index) {
if (!this.glFeatures.dragCommentSelection) return;
this.$emit('enterdragging', { ...line, index });
},
onDragStart(line) {
this.$root.$emit('bv::hide::tooltip');
this.dragging = true;
this.$emit('startdragging', line);
},
},
OUR_CHANGES: 'HEAD//our changes',
THEIR_CHANGES: 'origin//their changes',
@ -143,7 +179,13 @@ export default {
<template>
<div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
<div class="diff-grid-left left-side">
<div
data-testid="left-side"
class="diff-grid-left left-side"
@dragover.prevent
@dragenter="onDragEnter(line.left, index)"
@dragend="onDragEnd"
>
<template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
<div
:class="classNameMapCellLeft"
@ -159,10 +201,13 @@ export default {
:title="addCommentTooltipLeft"
>
<button
:draggable="glFeatures.dragCommentSelection"
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)"
@dragstart="onDragStart({ ...line.left, index })"
>
<gl-icon :size="12" name="comment" />
</button>
@ -234,7 +279,14 @@ export default {
></div>
</template>
</div>
<div v-if="!inline" class="diff-grid-right right-side">
<div
v-if="!inline"
data-testid="right-side"
class="diff-grid-right right-side"
@dragover.prevent
@dragenter="onDragEnter(line.right, index)"
@dragend="onDragEnd"
>
<template v-if="line.right">
<div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
<template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
@ -246,10 +298,13 @@ export default {
:title="addCommentTooltipRight"
>
<button
:draggable="glFeatures.dragCommentSelection"
type="button"
class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)"
@dragstart="onDragStart({ ...line.right, index })"
>
<gl-icon :size="12" name="comment" />
</button>
@ -279,13 +334,21 @@ export default {
<div
v-gl-tooltip.hover
:title="coverageState.text"
:class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
:class="[line.right.type, coverageState.class, { hll: isHighlighted, hll: isCommented }]"
class="diff-td line-coverage right-side"
></div>
<div
:id="line.right.line_code"
:key="line.right.rich_text"
:class="[line.right.type, { hll: isHighlighted, parallel: !inline }]"
v-safe-html="line.right.rich_text"
:class="[
line.right.type,
{
hll: isHighlighted,
hll: isCommented,
parallel: !inline,
},
]"
class="diff-td line_content with-coverage right-side"
@mousedown="handleParallelLineMouseDown"
>

View File

@ -35,7 +35,7 @@ export const lineCode = (line) => {
return line.line_code || line.left?.line_code || line.right?.line_code;
};
export const classNameMapCell = (line, hll, isLoggedIn, isHover) => {
export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
if (!line) return [];
const { type } = line;
@ -54,7 +54,9 @@ export const addCommentTooltip = (line) => {
let tooltip;
if (!line) return tooltip;
tooltip = __('Add a comment to this line');
tooltip = gon.drag_comment_selection
? __('Add a comment to this line or drag for multiple lines')
: __('Add a comment to this line');
const brokenSymlinks = line.commentsDisabled;
if (brokenSymlinks) {

View File

@ -1,5 +1,5 @@
<script>
import { mapGetters, mapState } from 'vuex';
import { mapGetters, mapState, mapActions } from 'vuex';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import DiffRow from './diff_row.vue';
@ -35,6 +35,12 @@ export default {
default: false,
},
},
data() {
return {
dragStart: null,
updatedLineRange: null,
};
},
computed: {
...mapGetters('diffs', ['commitId']),
...mapState({
@ -52,12 +58,39 @@ export default {
},
},
methods: {
...mapActions(['setSelectedCommentPosition']),
...mapActions('diffs', ['showCommentForm']),
showCommentLeft(line) {
return !this.inline || line.left;
},
showCommentRight(line) {
return !this.inline || (line.right && !line.left);
},
onStartDragging(line) {
this.dragStart = line;
},
onDragOver(line) {
if (line.chunk !== this.dragStart.chunk) return;
let start = this.dragStart;
let end = line;
if (this.dragStart.index >= line.index) {
start = line;
end = this.dragStart;
}
this.updatedLineRange = { start, end };
this.setSelectedCommentPosition(this.updatedLineRange);
},
onStopDragging() {
this.showCommentForm({
lineCode: this.updatedLineRange?.end?.line_code,
fileHash: this.diffFile.file_hash,
});
this.dragStart = null;
},
},
userColorScheme: window.gon.user_color_scheme,
};
@ -94,6 +127,10 @@ export default {
:is-bottom="index + 1 === diffLinesLength"
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
:index="index"
@enterdragging="onDragOver"
@startdragging="onStartDragging"
@stopdragging="onStopDragging"
/>
<div
v-if="line.renderCommentRow"

View File

@ -72,7 +72,12 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.new_line);
},
classNameMapCell() {
return classNameMapCell(this.line, this.isHighlighted, this.isLoggedIn, this.isHover);
return classNameMapCell({
line: this.line,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isHover,
});
},
addCommentTooltip() {
return addCommentTooltip(this.line);

View File

@ -68,20 +68,20 @@ export default {
return this.fileLineCoverage(this.filePath, this.line.right.new_line);
},
classNameMapCellLeft() {
return utils.classNameMapCell(
this.line.left,
this.isHighlighted,
this.isLoggedIn,
this.isLeftHover,
);
return utils.classNameMapCell({
line: this.line.left,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isLeftHover,
});
},
classNameMapCellRight() {
return utils.classNameMapCell(
this.line.right,
this.isHighlighted,
this.isLoggedIn,
this.isRightHover,
);
return utils.classNameMapCell({
line: this.line.right,
hll: this.isHighlighted,
isLoggedIn: this.isLoggedIn,
isHover: this.isRightHover,
});
},
addCommentTooltipLeft() {
return utils.addCommentTooltip(this.line.left);

View File

@ -55,8 +55,17 @@ export const parallelizeDiffLines = (diffLines, inline) => {
let conflictStartIndex = -1;
const lines = [];
// `chunk` is used for dragging to select diff lines
// we are restricting commenting to only lines that appear between
// "expansion rows". Here equal chunks are lines grouped together
// inbetween expansion rows.
let chunk = 0;
for (let i = 0, diffLinesLength = diffLines.length, index = 0; i < diffLinesLength; i += 1) {
const line = diffLines[i];
line.chunk = chunk;
if (isMeta(line)) chunk += 1;
if (isRemoved(line) || isConflictOur(line) || inline) {
lines.push({

View File

@ -28,13 +28,13 @@ export default {
if (this.isCurrentUser) {
return sprintf(
s__('Members|Are you sure you want to withdraw your access request for "%{source}"'),
{ source: source.name },
{ source: source.fullName },
);
}
return sprintf(
s__('Members|Are you sure you want to deny %{usersName}\'s request to join "%{source}"'),
{ usersName: user.name, source: source.name },
{ usersName: user.name, source: source.fullName },
);
},
},

View File

@ -25,7 +25,7 @@ export default {
s__(
'Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join "%{source}"',
),
{ inviteEmail: invite.email, source: source.name },
{ inviteEmail: invite.email, source: source.fullName },
);
},
},

View File

@ -36,7 +36,7 @@ export default {
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'),
{
usersName: user.name,
source: source.name,
source: source.fullName,
},
);
}
@ -44,7 +44,7 @@ export default {
return sprintf(
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'),
{
source: source.name,
source: source.fullName,
},
);
},

View File

@ -35,7 +35,7 @@ export default {
return this.memberPath.replace(/:id$/, 'leave');
},
modalTitle() {
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.name });
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
},
},
methods: {
@ -59,7 +59,7 @@ export default {
<gl-form ref="form" :action="leavePath" method="post">
<p>
<gl-sprintf :message="$options.modalContent">
<template #source>{{ member.source.name }}</template>
<template #source>{{ member.source.fullName }}</template>
</gl-sprintf>
</p>

View File

@ -22,6 +22,6 @@ export default {
<template>
<span v-if="isDirectMember">{{ __('Direct member') }}</span>
<a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{
memberSource.name
memberSource.fullName
}}</a>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { GlFormSelect, GlSprintf } from '@gitlab/ui';
import { getSymbol, getLineClasses } from './multiline_comment_utils';
@ -27,12 +27,13 @@ export default {
};
},
computed: {
...mapState({ selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition }),
lineNumber() {
return this.commentLineOptions[this.commentLineOptions.length - 1].text;
},
},
created() {
const line = this.lineRange?.start || this.line;
const line = this.selectedCommentPosition?.start || this.lineRange?.start || this.line;
this.commentLineStart = {
line_code: line.line_code,
@ -40,6 +41,8 @@ export default {
old_line: line.old_line,
new_line: line.new_line,
};
if (this.selectedCommentPosition) return;
this.highlightSelection();
},
destroyed() {

View File

@ -15,7 +15,7 @@ export default () => ({
batchSuggestionsInfo: [],
currentlyFetchingDiscussions: false,
/**
* selectedCommentPosition & selectedCommentPosition structures are the same as `position.line_range`:
* selectedCommentPosition & selectedCommentPositionHover structures are the same as `position.line_range`:
* {
* start: { line_code: string, new_line: number, old_line:number, type: string },
* end: { line_code: string, new_line: number, old_line:number, type: string },

View File

@ -1,38 +1,18 @@
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import getPipelineCountByStatus from '../graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '../graphql/queries/get_project_pipeline_statistics.query.graphql';
import StatisticsList from './statistics_list.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
import PipelineCharts from './pipeline_charts.vue';
import {
CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT,
DEFAULT,
INNER_CHART_HEIGHT,
LOAD_ANALYTICS_FAILURE,
LOAD_PIPELINES_FAILURE,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
PARSE_FAILURE,
UNSUPPORTED_DATA,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
} from '../constants';
const defaultCountValues = {
totalPipelines: {
count: 0,
},
successfulPipelines: {
count: 0,
},
};
const defaultAnalyticsValues = {
weekPipelinesTotals: [],
weekPipelinesLabels: [],
@ -47,36 +27,40 @@ const defaultAnalyticsValues = {
pipelineTimesValues: [],
};
const defaultCountValues = {
totalPipelines: {
count: 0,
},
successfulPipelines: {
count: 0,
},
};
export default {
components: {
GlAlert,
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
CiCdAnalyticsAreaChart,
GlTabs,
GlTab,
PipelineCharts,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
},
inject: {
projectPath: {
type: String,
default: '',
},
shouldRenderDeploymentFrequencyCharts: {
type: Boolean,
default: false,
},
projectPath: {
type: String,
default: '',
},
},
data() {
return {
counts: {
...defaultCountValues,
},
analytics: {
...defaultAnalyticsValues,
},
showFailureAlert: false,
failureType: null,
analytics: { ...defaultAnalyticsValues },
counts: { ...defaultCountValues },
};
},
apollo: {
@ -134,41 +118,6 @@ export default {
};
}
},
successRatio() {
const { successfulPipelines, failedPipelines } = this.counts;
const successfulCount = successfulPipelines?.count;
const failedCount = failedPipelines?.count;
const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
return failedCount === 0 ? 100 : ratio;
},
formattedCounts() {
const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
return {
total: totalPipelines?.count,
success: successfulPipelines?.count,
failed: failedPipelines?.count,
successRatio: this.successRatio,
};
},
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
let areaChartsData = [];
try {
areaChartsData = [
this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData),
];
} catch {
areaChartsData = [];
this.reportFailure(PARSE_FAILURE);
}
return areaChartsData;
},
lastWeekChartData() {
return {
labels: this.analytics.weekPipelinesLabels,
@ -190,39 +139,32 @@ export default {
success: this.analytics.yearPipelinesSuccessful,
};
},
timesChartTransformedData() {
return [
{
name: 'full',
data: this.mergeLabelsAndValues(
this.analytics.pipelineTimesLabels,
this.analytics.pipelineTimesValues,
),
},
];
timesChartData() {
return {
labels: this.analytics.pipelineTimesLabels,
values: this.analytics.pipelineTimesValues,
};
},
successRatio() {
const { successfulPipelines, failedPipelines } = this.counts;
const successfulCount = successfulPipelines?.count;
const failedCount = failedPipelines?.count;
const ratio = (successfulCount / (successfulCount + failedCount)) * 100;
return failedCount === 0 ? 100 : ratio;
},
formattedCounts() {
const { totalPipelines, successfulPipelines, failedPipelines } = this.counts;
return {
total: totalPipelines?.count,
success: successfulPipelines?.count,
failed: failedPipelines?.count,
successRatio: this.successRatio,
};
},
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData(title, data) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
hideAlert() {
this.showFailureAlert = false;
},
@ -231,16 +173,6 @@ export default {
this.failureType = type;
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
errorTexts: {
[LOAD_ANALYTICS_FAILURE]: s__(
'PipelineCharts|An error has ocurred when retrieving the analytics data',
@ -251,74 +183,38 @@ export default {
[PARSE_FAILURE]: s__('PipelineCharts|There was an error parsing the data for the charts.'),
[DEFAULT]: s__('PipelineCharts|An unknown error occurred while processing CI/CD analytics.'),
},
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
};
</script>
<template>
<div>
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
{{ failure.text }}
</gl-alert>
<div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
<h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
<gl-skeleton-loader v-if="$apollo.queries.counts.loading" :lines="5" />
<statistics-list v-else :counts="formattedCounts" />
</div>
<div class="col-md-6">
<strong>
{{ __('Duration for the last 30 commits') }}
</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
<gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">{{
failure.text
}}</gl-alert>
<gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
<gl-tab :title="__('Pipelines')">
<pipeline-charts
:counts="formattedCounts"
:last-week="lastWeekChartData"
:last-month="lastMonthChartData"
:last-year="lastYearChartData"
:times-chart="timesChartData"
:loading="$apollo.queries.counts.loading"
@report-failure="reportFailure"
/>
</div>
</div>
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
<ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
>
{{ chart.title }}
</ci-cd-analytics-area-chart>
<template v-if="shouldRenderDeploymentFrequencyCharts">
<hr />
<deployment-frequency-charts />
</template>
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployment-frequency-charts />
</gl-tab>
</gl-tabs>
<pipeline-charts
v-else
:counts="formattedCounts"
:last-week="lastWeekChartData"
:last-month="lastMonthChartData"
:last-year="lastYearChartData"
:times-chart="timesChartData"
:loading="$apollo.queries.counts.loading"
@report-failure="reportFailure"
/>
</div>
</template>

View File

@ -1,26 +1,12 @@
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
CHART_DATE_FORMAT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
} from '../constants';
import { GlTabs, GlTab } from '@gitlab/ui';
import PipelineCharts from './pipeline_charts.vue';
export default {
components: {
StatisticsList,
GlColumnChart,
CiCdAnalyticsAreaChart,
GlTabs,
GlTab,
PipelineCharts,
DeploymentFrequencyCharts: () =>
import('ee_component/projects/pipelines/charts/components/deployment_frequency_charts.vue'),
},
@ -54,121 +40,41 @@ export default {
},
data() {
return {
timesChartTransformedData: [
{
name: 'full',
data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
],
// this loading flag gives the echarts library just enough time
// to ensure all DOM nodes have been mounted.
//
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1131
loading: true,
};
},
computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
return [
this.buildAreaChartData(lastWeek, this.lastWeekChartData),
this.buildAreaChartData(lastMonth, this.lastMonthChartData),
this.buildAreaChartData(lastYear, this.lastYearChartData),
];
},
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData(title, data) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
async mounted() {
await this.$nextTick();
this.loading = false;
},
};
</script>
<template>
<div>
<div class="mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
<h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
<statistics-list :counts="counts" />
</div>
<div class="col-md-6">
<strong>
{{ __('Duration for the last 30 commits') }}
</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
/>
</div>
</div>
<hr />
<h4 class="my-4">{{ __('Pipelines charts') }}</h4>
<ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
>
{{ chart.title }}
</ci-cd-analytics-area-chart>
<template v-if="shouldRenderDeploymentFrequencyCharts">
<hr />
<gl-tabs v-if="shouldRenderDeploymentFrequencyCharts">
<gl-tab :title="__('Pipelines')">
<pipeline-charts
:counts="counts"
:last-week="lastWeekChartData"
:last-month="lastMonthChartData"
:last-year="lastYearChartData"
:times-chart="timesChartData"
:loading="loading"
/>
</gl-tab>
<gl-tab :title="__('Deployments')">
<deployment-frequency-charts />
</template>
</div>
</gl-tab>
</gl-tabs>
<pipeline-charts
v-else
:counts="counts"
:last-week="lastWeekChartData"
:last-month="lastMonthChartData"
:last-year="lastYearChartData"
:times-chart="timesChartData"
/>
</template>

View File

@ -0,0 +1,176 @@
<script>
import dateFormat from 'dateformat';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { GlSkeletonLoader } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import {
CHART_CONTAINER_HEIGHT,
CHART_DATE_FORMAT,
INNER_CHART_HEIGHT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
PARSE_FAILURE,
} from '../constants';
import StatisticsList from './statistics_list.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
export default {
components: {
GlColumnChart,
GlSkeletonLoader,
StatisticsList,
CiCdAnalyticsAreaChart,
},
props: {
counts: {
required: true,
type: Object,
},
loading: {
required: false,
default: false,
type: Boolean,
},
lastWeek: {
required: true,
type: Object,
},
lastMonth: {
required: true,
type: Object,
},
lastYear: {
required: true,
type: Object,
},
timesChart: {
required: true,
type: Object,
},
},
computed: {
areaCharts() {
const { lastWeek, lastMonth, lastYear } = this.$options.chartTitles;
const charts = [
{ title: lastWeek, data: this.lastWeek },
{ title: lastMonth, data: this.lastMonth },
{ title: lastYear, data: this.lastYear },
];
let areaChartsData = [];
try {
areaChartsData = charts.map(this.buildAreaChartData);
} catch {
areaChartsData = [];
this.vm.$emit('report-failure', PARSE_FAILURE);
}
return areaChartsData;
},
timesChartTransformedData() {
return [
{
name: 'full',
data: this.mergeLabelsAndValues(this.timesChart.labels, this.timesChart.values),
},
];
},
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
buildAreaChartData({ title, data }) {
const { labels, totals, success } = data;
return {
title,
data: [
{
name: 'all',
data: this.mergeLabelsAndValues(labels, totals),
},
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
},
],
};
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
areaChartOptions: {
xAxis: {
name: s__('Pipeline|Date'),
type: 'category',
},
yAxis: {
name: s__('Pipeline|Pipelines'),
},
},
get chartTitles() {
const today = dateFormat(new Date(), CHART_DATE_FORMAT);
const pastDate = (timeScale) =>
dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
return {
lastWeek: sprintf(__('Pipelines for last week (%{oneWeekAgo} - %{today})'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
today,
}),
lastMonth: sprintf(__('Pipelines for last month (%{oneMonthAgo} - %{today})'), {
oneMonthAgo: pastDate(ONE_MONTH_AGO_DAYS),
today,
}),
lastYear: __('Pipelines for last year'),
};
},
};
</script>
<template>
<div>
<div class="gl-mb-3">
<h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
</div>
<h4 class="gl-my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
<gl-skeleton-loader v-if="loading" :lines="5" />
<statistics-list v-else :counts="counts" />
</div>
<div v-if="!loading" class="col-md-6">
<strong>{{ __('Duration for the last 30 commits') }}</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
/>
</div>
</div>
<template v-if="!loading">
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
<ci-cd-analytics-area-chart
v-for="(chart, index) in areaCharts"
:key="index"
:chart-data="chart.data"
:area-chart-options="$options.areaChartOptions"
>{{ chart.title }}</ci-cd-analytics-area-chart
>
</template>
</div>
</template>

View File

@ -986,6 +986,8 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.labels-select-wrapper {
&.is-standalone {
min-width: $input-md-width;
.labels-select-dropdown-contents {
max-height: 350px;

View File

@ -516,9 +516,12 @@ $line-removed-dark-transparent: rgba(246, 53, 85, 0.2);
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
$line-number-commented: #dae5fb;
$line-target-blue: $blue-50;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
$line-commented-blue: #e8effc;
$line-commented-blue-dark: #bccef0;
$dark-diff-match-bg: rgba($white, 0.3);
$dark-diff-match-color: rgba($white, 0.1);
$diff-image-info-color: #808080;

View File

@ -3,7 +3,7 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: false)
push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
end
feature_category :pipeline_authoring

View File

@ -33,6 +33,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)

View File

@ -214,6 +214,29 @@ module EmailsHelper
end
end
def group_membership_expiration_changed_text(member, group)
if member.expires?
days = (member.expires_at - Date.today).to_i
days_formatted = pluralize(days, 'day')
_('Your %{group} membership will now expire in %{days}.') % { group: group.human_name, days: days_formatted }
else
_('Your membership in %{group} no longer expires.') % { group: group.human_name }
end
end
def group_membership_expiration_changed_link(member, group, format: nil)
url = group_group_members_url(group, search: member.user.username)
case format
when :html
link_to = generate_link('group membership', url).html_safe
_('For additional information, review your %{link_to} or contact your group owner.').html_safe % { link_to: link_to }
else
_('For additional information, review your group membership: %{link_to} or contact your group owner.') % { link_to: url }
end
end
def instance_access_request_text(user, format: nil)
gitlab_host = Gitlab.config.gitlab.host

View File

@ -18,7 +18,7 @@ module Groups::GroupMembersHelper
end
def members_data_json(group, members)
members_data(group, members).to_json
MemberSerializer.new.represent(members, { current_user: current_user, group: group }).to_json
end
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
@ -38,84 +38,6 @@ module Groups::GroupMembersHelper
group_id: group.id
}
end
private
def members_data(group, members)
members.map do |member|
user = member.user
source = member.source
data = {
id: member.id,
created_at: member.created_at,
expires_at: member.expires_at&.to_time,
requested_at: member.requested_at,
can_update: member.can_update?,
can_remove: member.can_remove?,
access_level: {
string_value: member.human_access,
integer_value: member.access_level
},
source: {
id: source.id,
name: source.full_name,
web_url: Gitlab::UrlBuilder.build(source)
},
valid_roles: member.valid_level_roles
}.merge(member_created_by_data(member.created_by))
if member.invite?
data[:invite] = member_invite_data(member)
elsif user.present?
data[:user] = member_user_data(user)
end
data
end
end
def member_created_by_data(created_by)
return {} unless created_by.present?
{
created_by: {
name: created_by.name,
web_url: Gitlab::UrlBuilder.build(created_by)
}
}
end
def member_user_data(user)
{
id: user.id,
name: user.name,
username: user.username,
web_url: Gitlab::UrlBuilder.build(user),
avatar_url: avatar_icon_for_user(user, AVATAR_SIZE),
blocked: user.blocked?,
two_factor_enabled: user.two_factor_enabled?
}.merge(member_user_status_data(user.status))
end
def member_user_status_data(status)
return {} unless status.present?
{
status: {
emoji: status.emoji,
message_html: status.message_html
}
}
end
def member_invite_data(member)
{
email: member.invite_email,
avatar_url: avatar_icon_for_email(member.invite_email, AVATAR_SIZE),
can_resend: member.can_resend_invite?
}
end
end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')

View File

@ -114,6 +114,23 @@ module Emails
subject: subject('Invitation declined'))
end
def member_expiration_date_updated_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
return unless member_exists?
subject = if member.expires?
_('Group membership expiration date changed')
else
_('Group membership expiration date removed')
end
member_email_with_layout(
to: member.user.notification_email_for(notification_group),
subject: subject(subject))
end
# rubocop: disable CodeReuse/ActiveRecord
def member
@member ||= Member.find_by(id: @member_id)

View File

@ -209,7 +209,7 @@ class CommitStatus < ApplicationRecord
def group_name
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '')
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')

View File

@ -13,6 +13,8 @@ class Member < ApplicationRecord
include FromUnion
include UpdateHighestRole
AVATAR_SIZE = 40
attr_accessor :raw_invite_token
belongs_to :created_by, class_name: "User"

View File

@ -74,6 +74,10 @@ class GroupMember < Member
run_after_commit { notification_service.update_group_member(self) }
end
if saved_change_to_expires_at?
run_after_commit { notification_service.updated_group_member_expiration(self) }
end
super
end

View File

@ -100,7 +100,7 @@ module Packages
case scope
when :group
api_v4_groups_packages_nuget_metadata_package_name_package_version_path(
api_v4_groups___packages_nuget_metadata_package_name_package_version_path(
params,
true
)
@ -115,7 +115,7 @@ module Packages
def search_service_url
case scope
when :group
api_v4_groups_packages_nuget_query_path(id: @project_or_group.id)
api_v4_groups___packages_nuget_query_path(id: @project_or_group.id)
when :project
api_v4_projects_packages_nuget_query_path(id: @project_or_group.id)
end

View File

@ -18,8 +18,30 @@ class DiffsMetadataEntity < DiffsEntity
options[:merge_request].can_be_merged_by?(request.current_user)
end
expose :project_path
expose :project_name
expose :username
expose :user_full_name
private
def project_path
request.project&.full_path
end
def project_name
request.project&.name
end
def username
request.current_user&.username
end
def user_full_name
request.current_user&.name
end
def presenter(merge_request)
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
class MemberEntity < Grape::Entity
include RequestAwareEntity
include AvatarsHelper
expose :id
expose :created_at
expose :expires_at do |member|
member.expires_at&.to_time
end
expose :requested_at
expose :created_by, if: -> (member) { member.created_by.present? } do |member|
UserEntity.represent(member.created_by, only: [:name, :web_url])
end
expose :can_update do |member|
member.can_update?
end
expose :can_remove do |member|
member.can_remove?
end
expose :access_level do
expose :human_access, as: :string_value
expose :access_level, as: :integer_value
end
expose :source do |member|
GroupEntity.represent(member.source, only: [:id, :full_name, :web_url])
end
expose :valid_level_roles, as: :valid_roles
expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity
expose :invite, if: -> (member) { member.invite? } do
expose :email do |member|
member.invite_email
end
expose :avatar_url do |member|
avatar_icon_for_email(member.invite_email, Member::AVATAR_SIZE)
end
expose :can_resend do |member|
member.can_resend_invite?
end
end
end
MemberEntity.prepend_if_ee('EE::MemberEntity')

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class MemberSerializer < BaseSerializer
entity MemberEntity
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class MemberUserEntity < UserEntity
unexpose :show_status
unexpose :path
unexpose :state
unexpose :status_tooltip_html
expose :avatar_url do |user|
user.avatar_url(size: Member::AVATAR_SIZE, only_path: false)
end
expose :blocked do |user|
user.blocked?
end
expose :two_factor_enabled do |user|
user.two_factor_enabled?
end
expose :status, if: -> (user) { user.status.present? } do
expose :emoji do |user|
user.status.emoji
end
end
end
MemberUserEntity.prepend_if_ee('EE::MemberUserEntity')

View File

@ -481,6 +481,12 @@ class NotificationService
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def updated_group_member_expiration(group_member)
return true unless group_member.notifiable?(:mention)
mailer.member_expiration_date_updated_email(group_member.real_source_type, group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
recipients = project_moved_recipients(project)
recipients = notifiable_users(recipients, :custom, custom_action: :moved_project, project: project)

View File

@ -3,7 +3,8 @@
.container
.gl-mt-3
- if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature, default_enabled: false) && License.feature_available?(:devops_adoption)
- feature_already_in_use = Analytics::DevopsAdoption::Segment.any?
- if Gitlab.ee? && Feature.enabled?(:devops_adoption_feature, default_enabled: feature_already_in_use) && License.feature_available?(:devops_adoption)
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
= render 'report'

View File

@ -300,8 +300,8 @@
= link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
%button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
%button.gl-button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' }
%span.gl-mr-2= _("Got it!")
= sprite_icon('thumb-up')
- if project_nav_tab? :environments

View File

@ -0,0 +1,6 @@
= email_default_heading(say_hi(@member.user))
%p
= group_membership_expiration_changed_text(@member, @member_source)
%p
= group_membership_expiration_changed_link(@member, @member_source, format: :html)

View File

@ -0,0 +1,5 @@
<%= say_hi(@member.user) %>
<%= group_membership_expiration_changed_text(@member, @member_source) %>
<%= group_membership_expiration_changed_link(@member, @member_source) %>

View File

@ -3,7 +3,7 @@
- requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
- requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
- link_end = '</a>'.html_safe
- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- enable_lfs_message = s_("DesignManagement|To upload designs, you'll need to enable LFS and have an admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end }
- if @project.design_management_enabled?
- add_page_startup_graphql_call('design_management/get_design_list', { fullPath: @project.full_path, iid: @issue.iid.to_s, atVersion: nil })

View File

@ -0,0 +1,5 @@
---
title: Add additional fields to diff_metadata.json endpoint
merge_request: 50666
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Send email when group member expiry is updated
merge_request: 50310
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Remove limit of four features per plan
merge_request: 51264
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Enable CI visualization by default
merge_request: 51701
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix typo in notice displayed when Design Management requires LFS to be enabled
merge_request: 51644
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Updates regex for group_name to support numbers in job name
merge_request: 51157
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add gl-button to dismiss feature highlight button
merge_request: 51555
author: Yogi (@yo)
type: other

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290117
milestone: '13.7'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: drag_comment_selection
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49875
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/293945
milestone: '13.7'
type: development
group: group::source code
default_enabled: true

View File

@ -181,7 +181,7 @@ secondary. If the node is paused, be sure to resume before promoting. This
issue has been fixed in GitLab 13.4 and later.
WARNING:
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
@ -220,6 +220,75 @@ Data that was created on the primary while the secondary was paused will be lost
previously for the **secondary**.
1. Success! The **secondary** has now been promoted to **primary**.
#### Promoting a **secondary** node with a Patroni standby cluster
The `gitlab-ctl promote-to-primary-node` command cannot be used yet in
conjunction with a Patroni standby cluster, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
WARNING:
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the
secondary is paused fails. Do not pause replication before promoting a
secondary. If the node is paused, be sure to resume before promoting. This
issue has been fixed in GitLab 13.4 and later.
WARNING:
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
1. SSH in to the Standby Leader database node in the **secondary** and trigger PostgreSQL to
promote to read-write:
```shell
sudo gitlab-ctl promote-db
```
1. Disable Patroni auto-failover:
```shell
sudo gitlab-ctl patroni pause
```
1. Edit `/etc/gitlab/gitlab.rb` on every application and Sidekiq nodes in the secondary to reflect its new status as primary by removing any lines that enabled the `geo_secondary_role`:
```ruby
## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
geo_secondary_role['enable'] = true
## In 11.5+ documentation, the role was enabled as follows. Remove this line.
roles ['geo_secondary_role']
```
1. Edit `/etc/gitlab/gitlab.rb` on every Patroni node in the secondary to disable the standby cluster:
```ruby
patroni['standby_cluster']['enable'] = false
```
1. Reconfigure GitLab on each machine for the changes to take effect:
```shell
sudo gitlab-ctl reconfigure
```
1. Resume Patroni auto-failover:
```shell
sudo gitlab-ctl patroni resume
```
1. Promote the **secondary** to **primary**. SSH into a single application server and execute:
```shell
sudo gitlab-rake geo:set_secondary_as_primary
```
1. Verify you can connect to the newly promoted **primary** using the URL used previously for the **secondary**.
1. Success! The **secondary** has now been promoted to **primary**.
#### Promoting a **secondary** node with an external PostgreSQL database
The `gitlab-ctl promote-to-primary-node` command cannot be used in conjunction with
@ -278,7 +347,7 @@ required:
1. Verify you can connect to the newly promoted **primary** site using the URL used
previously for the **secondary** site.
Success! The **secondary** site has now been promoted to **primary**.
1. Success! The **secondary** site has now been promoted to **primary**.
### Step 4. (Optional) Updating the primary domain DNS record

View File

@ -40,3 +40,20 @@ from your project on the same schedule as the `git gc` operation, freeing up sto
You can find this option under your project's **Settings > General > Advanced**.
![Housekeeping settings](img/housekeeping_settings.png)
## How housekeeping handles pool repositories
Housekeeping for pool repositories is handled differently from standard repositories.
It is ultimately performed by the Gitaly RPC `FetchIntoObjectPool`.
This is the current call stack by which it is invoked:
1. `Projects::HousekeepingService#execute_gitlab_shell_gc`
1. `GitGarbageCollectWorker#perform`
1. `Projects::GitDeduplicationService#fetch_from_source`
1. `ObjectPool#fetch`
1. `ObjectPoolService#fetch`
1. `Gitaly::FetchIntoObjectPoolRequest`
To manually invoke it from a Rails console, if needed, you can call `project.pool_repository.object_pool.fetch`.
This is a potentially long-running task, though Gitaly will timeout in about 8 hours.

View File

@ -76,7 +76,7 @@ Example response:
Creates an association between two epics, designating one as the parent epic and the other as the child epic. A parent epic can have multiple child epics. If the new child epic already belonged to another epic, it is unassigned from that previous parent.
```plaintext
POST /groups/:id/epics/:epic_iid/epics
POST /groups/:id/epics/:epic_iid/epics/:child_epic_id
```
| Attribute | Type | Required | Description |

View File

@ -766,7 +766,6 @@ Parameters:
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
### Options for `default_branch_protection`
@ -778,16 +777,6 @@ The `default_branch_protection` attribute determines whether developers and main
| `1` | Partial protection. Developers and maintainers can: <br>- Push new commits |
| `2` | Full protection. Only maintainers can: <br>- Push new commits |
### Options for `shared_runners_setting`
The `shared_runners_setting` attribute determines whether shared runners are enabled for a group's subgroups and projects.
| Value | Description |
|-------|-------------------------------------------------------------------------------------------------------------|
| `enabled` | Enables shared runners for all projects and subgroups in this group. |
| `disabled_with_override` | Disables shared runners for all projects and subgroups in this group, but allows subgroups to override this setting. |
| `disabled_and_unoverridable` | Disables shared runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting. |
## New Subgroup
This is similar to creating a [New group](#new-group). You need the `parent_id` from the [List groups](#list-groups) call. You can then enter the desired:
@ -852,6 +841,7 @@ PUT /groups/:id
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
| `prevent_forking_outside_group` | boolean | no | **(PREMIUM)** When enabled, users can **not** fork projects from this group to external namespaces
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
NOTE:
The `projects` and `shared_projects` attributes in the response are deprecated and [scheduled for removal in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).
@ -941,6 +931,16 @@ with Rails console access to run the following command:
Feature.disable(:limit_projects_in_groups_api)
```
### Options for `shared_runners_setting`
The `shared_runners_setting` attribute determines whether shared runners are enabled for a group's subgroups and projects.
| Value | Description |
|-------|-------------------------------------------------------------------------------------------------------------|
| `enabled` | Enables shared runners for all projects and subgroups in this group. |
| `disabled_with_override` | Disables shared runners for all projects and subgroups in this group, but allows subgroups to override this setting. |
| `disabled_and_unoverridable` | Disables shared runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting. |
## Remove group
Only available to group owners and administrators.

View File

@ -70,7 +70,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
## Revoke a personal access token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216004) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.3.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/270200) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.6.
Revoke a personal access token.

View File

@ -14,6 +14,9 @@ You can:
- [Restrict write-access to a critical environment](#restrict-write-access-to-a-critical-environment)
- [Prevent deployments during deploy freeze windows](#prevent-deployments-during-deploy-freeze-windows)
- [Set appropriate roles to your project](#setting-appropriate-roles-to-your-project)
- [Protect production secrets](#protect-production-secrets)
- [Separate project for deployments](#separate-project-for-deployments)
If you are using a continuous deployment workflow and want to ensure that concurrent deployments to the same environment do not happen, you should enable the following options:
@ -38,8 +41,8 @@ For example:
```yaml
deploy:
script: deploy-to-prod
resource_group: prod
script: deploy-to-prod
resource_group: prod
```
Example of a problematic pipeline flow **before** using the resource group:
@ -89,6 +92,56 @@ If you want to prevent deployments for a particular period, for example during a
vacation period when most employees are out, you can set up a [Deploy Freeze](../../user/project/releases/index.md#prevent-unintentional-releases-by-setting-a-deploy-freeze).
During a deploy freeze period, no deployment can be executed. This is helpful to
ensure that deployments do not happen unexpectedly.
## Setting appropriate roles to your project
GitLab supports several different roles that can be assigned to your project members. See
[Project members permissions](../../user/permissions.md#project-members-permissions)
for an explanation of these roles and the permissions of each.
<div class="video-fallback">
See the video: <a href="https://www.youtube.com/watch?v=Mq3C1KveDc0">How to secure your CD pipelines</a>.
</div>
<figure class="video-container">
<iframe src="https://www.youtube.com/embed/Mq3C1KveDc0" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
## Protect production secrets
Production secrets are needed to deploy successfully. For example, when deploying to the cloud,
cloud providers require these secrets to connect to their services. In the project settings, you can
define and protect environment variables for these secrets. [Protected variables](../variables/README.md#protect-a-custom-variable)
are only passed to pipelines running on [protected branches](../../user/project/protected_branches.md)
or [protected tags](../../user/project/protected_tags.md).
The other pipelines don't get the protected variable. You can also
[scope variables to specific environments](../variables/where_variables_can_be_used.md#variables-with-an-environment-scope).
We recommend that you use protected variables on protected environments to make sure that the
secrets aren't exposed unintentionally. You can also define production secrets on the
[runner side](../runners/README.md#prevent-runners-from-revealing-sensitive-information).
This prevents other maintainers from reading the secrets and makes sure that the runner only runs on
protected branches.
For more information, see [pipeline security](../pipelines/index.md#pipeline-security-on-protected-branches).
## Separate project for deployments
All project maintainers have access to production secrets. If you need to limit the number of users
that can deploy to a production environment, you can create a separate project and configure a new
permission model that isolates the CD permissions from the original project and prevents the
original project's maintainers from accessing the production secret and CD configuration. You can
connect the CD project to your development projects by using [multi-project pipelines](../multi_project_pipelines.md).
## Protect `gitlab-ci.yml` from change
A `.gitlab-ci.yml` may contain rules to deploy an application to the production server. This
deployment usually runs automatically after pushing a merge request. To prevent developers from
changing the `gitlab-ci.yml`, you can define it in a different repository. The configuration can
reference a file in another project with a completely different set of permissions (similar to
[separating a project for deployments](#separate-project-for-deployments)).
In this scenario, the `gitlab-ci.yml` is publicly accessible, but can only be edited by users with
appropriate permissions in the other project.
For more information, see [Custom CI configuration path](../pipelines/settings.md#custom-ci-configuration-path).
## Troubleshooting
@ -99,15 +152,13 @@ If you have multiple jobs for the same environment (including non-deployment job
```yaml
build:service-a:
environment:
name: production
environment:
name: production
build:service-b:
environment:
name: production
environment:
name: production
```
The [Skip outdated deployment jobs](../pipelines/settings.md#skip-outdated-deployment-jobs) might
not work well with this configuration, and must be disabled.
There is a [plan to introduce a new annotation for environments](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) to address this issue.

View File

@ -35,7 +35,7 @@ You can also view or fork the complete [example source](https://gitlab.com/gitla
}
```
1. Update the `files` key with glob pattern(s) that selects all files that should be included in the published module. More information about `files` can be found [in NPM's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json#files).
1. Update the `files` key with glob pattern(s) that selects all files that should be included in the published module. More information about `files` can be found [in NPM's documentation](https://docs.npmjs.com/cli/v6/configuring-npm/package-json/#files).
1. Add a `.gitignore` file to the project to avoid committing `node_modules`:

View File

@ -59,8 +59,9 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241722) in GitLab 13.5.
> - [Moved to **CI/CD > Editor**](https://gitlab.com/gitlab-org/gitlab/-/issues/263141) in GitLab 13.7.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It was [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.8.
> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
@ -82,16 +83,10 @@ each job depends only on the previous stage being completed successfully.
### Enable or disable CI/CD configuration visualization **(CORE ONLY)**
CI/CD configuration visualization is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
CI/CD configuration visualization is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:ci_config_visualization_tab)
```
can opt to disable it.
To disable it:
@ -99,6 +94,12 @@ To disable it:
Feature.disable(:ci_config_visualization_tab)
```
To enable it:
```ruby
Feature.enable(:ci_config_visualization_tab)
```
## Commit changes to CI configuration
The commit form appears at the bottom of each tab in the editor so you can commit

View File

@ -37,9 +37,9 @@ has a different configuration. Some may enable features A and B, and some may
enable features B and C. This flexibility enables different groups of people to
use different features of the agent in the same cluster.
For example, [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
For example, [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#priyanka-platform-engineer)
may want to use cluster-wide features of the agent, while
[Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
[Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/strategic-marketing/roles-personas/#sasha-software-developer)
uses the agent that only has access to a particular namespace.
Each agent is likely running using a

View File

@ -242,6 +242,24 @@ create the package record. Workhorse provides a variety of file metadata such as
For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md)
in your local development environment.
#### Rate Limits on GitLab.com
Package manager clients can make rapid requests that exceed the
[GitLab.com standard API rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
This results in a `429 Too Many Requests` error.
We have opened a set of paths to allow higher rate limits. Unless it is not possible,
new package managers should follow these conventions so they can take advantage of the
expanded package rate limit.
These route prefixes guarantee a higher rate limit:
```plaintext
/api/v4/packages/
/api/v4/projects/:project_id/packages/
/api/v4/groups/:group_id/-/packages/
```
### Future Work
While working on the MVC, contributors might find features that are not mandatory for the MVC but can provide a better user experience. It's generally a good idea to keep an eye on those and open issues.

View File

@ -132,8 +132,8 @@ general guidelines around how to collect those, due to the individual nature of
There are several types of counters which are all found in `usage_data.rb`:
- **Ordinary Batch Counters:** Simple count of a given ActiveRecord_Relation
- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation on given column
- **Sum Batch Counters:** Sum the values of a given ActiveRecord_Relation on given column
- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation in a given column
- **Sum Batch Counters:** Sum the values of a given ActiveRecord_Relation in a given column
- **Alternative Counters:** Used for settings and configurations
- **Redis Counters:** Used for in-memory counts.
@ -153,7 +153,15 @@ For GitLab.com, there are extremely large tables with 15 second query timeouts,
| `merge_request_diff_files` | 1082 |
| `events` | 514 |
There are two batch counting methods provided, `Ordinary Batch Counters` and `Distinct Batch Counters`. Batch counting requires indexes on columns to calculate max, min, and range queries. In some cases, a specialized index may need to be added on the columns involved in a counter.
We have several batch counting methods available:
- `Ordinary Batch Counters`
- `Distinct Batch Counters`
- `Sum Batch Counters`
- `Estimated Batch Counters`
Batch counting requires indexes on columns to calculate max, min, and range queries. In some cases,
you may need to add a specialized index on the columns involved in a counter.
### Ordinary Batch Counters
@ -248,6 +256,82 @@ sum(Issue.group(:state_id), :weight))
# returns => {1=>3542, 2=>6820}
```
### Estimated Batch Counters
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48233) in GitLab 13.7.
Estimated batch counter functionality handles `ActiveRecord::StatementInvalid` errors
when used through the provided `estimate_batch_distinct_count` method.
Errors return a value of `-1`.
WARNING:
This functionality estimates a distinct count of a specific ActiveRecord_Relation in a given column,
which uses the [HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf) algorithm.
As the HyperLogLog algorithm is probabilistic, the **results always include error**.
The highest encountered error rate is 4.9%.
When correctly used, the `estimate_batch_distinct_count` method enables efficient counting over
columns that contain non-unique values, which can not be assured by other counters.
Method: [`estimate_batch_distinct_count(relation, column = nil, batch_size: nil, start: nil, finish: nil)`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/utils/usage_data.rb#L63)
The method includes the following arguments:
- `relation`: The ActiveRecord_Relation to perform the count.
- `column`: The column to perform the distinct count. The default is the primary key.
- `batch_size`: The default is 10,000, from `Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE`.
- `start`: The custom start of the batch count, to avoid complex minimum calculations.
- `finish`: The custom end of the batch count in order to avoid complex maximum calculations.
The method includes the following prerequisites:
1. The supplied `relation` must include the primary key defined as the numeric column.
For example: `id bigint NOT NULL`.
1. The `estimate_batch_distinct_count` can handle a joined relation. To use its ability to
count non-unique columns, the joined relation **must NOT** have a one-to-many relationship,
such as `has_many :boards`.
1. Both `start` and `finish` arguments should always represent primary key relationship values,
even if the estimated count refers to another column, for example:
```ruby
estimate_batch_distinct_count(::Note, :author_id, start: ::Note.minimum(:id), finish: ::Note.maximum(:id))
```
Examples:
1. Simple execution of estimated batch counter, with only relation provided,
returned value represents estimated number of unique values in `id` column
(which is the primary key) of `Project` relation:
```ruby
estimate_batch_distinct_count(::Project)
```
1. Execution of estimated batch counter, where provided relation has applied
additional filter (`.where(time_period)`), number of unique values estimated
in custom column (`:author_id`), and parameters: `start` and `finish` together
apply boundaries that defines range of provided relation to analyze:
```ruby
estimate_batch_distinct_count(::Note.with_suggestions.where(time_period), :author_id, start: ::Note.minimum(:id), finish: ::Note.maximum(:id))
```
1. Execution of estimated batch counter with joined relation (`joins(:cluster)`),
for a custom column (`'clusters.user_id'`):
```ruby
estimate_batch_distinct_count(::Clusters::Applications::CertManager.where(time_period).available.joins(:cluster), 'clusters.user_id')
```
When instrumenting metric with usage of estimated batch counter please add
`_estimated` suffix to its name, for example:
```ruby
"counts": {
"ci_builds_estimated": estimate_batch_distinct_count(Ci::Build),
...
```
### Redis Counters
Handles `::Redis::CommandError` and `Gitlab::UsageDataCounters::BaseCounter::UnknownEvent`
@ -309,6 +393,10 @@ Examples of implementation:
#### Redis HLL Counters
WARNING:
HyperLogLog (HLL) is a probabilistic algorithm and its **results always includes some small error**. According to [Redis documentation](https://redis.io/commands/pfcount), data from
used HLL implementation is "approximated with a standard error of 0.81%".
With `Gitlab::UsageDataCounters::HLLRedisCounter` we have available data structures used to count unique values.
Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PFCOUNT](https://redis.io/commands/pfcount).
@ -414,7 +502,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
end
```
1. Track event using `track_usage_event(event_name, values) in services and graphql
1. Track event using `track_usage_event(event_name, values) in services and GraphQL
Increment unique values count using Redis HLL, for given event name.
@ -422,7 +510,7 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
[Track usage event for incident created in service](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/issues/update_service.rb)
[Track usage event for incident created in graphql](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/mutations/alert_management/update_alert_status.rb)
[Track usage event for incident created in GraphQL](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/mutations/alert_management/update_alert_status.rb)
```ruby
track_usage_event(:incident_management_incident_created, current_user.id)
@ -549,7 +637,7 @@ For each event we add metrics for the weekly and monthly time frames, and totals
- `#{event_name}_weekly`: Data for 7 days for daily [aggregation](#adding-new-events) events and data for the last complete week for weekly [aggregation](#adding-new-events) events.
- `#{event_name}_monthly`: Data for 28 days for daily [aggregation](#adding-new-events) events and data for the last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
Redis HLL implementation calculates automatic total metrics, if there are more than one metric for the same category, aggregation and Redis slot.
Redis HLL implementation calculates automatic total metrics, if there are more than one metric for the same category, aggregation and Redis slot.
- `#{category}_total_unique_counts_weekly`: Total unique counts for events in the same category for the last 7 days or the last complete week, if events are in the same Redis slot and we have more than one metric.
- `#{category}_total_unique_counts_monthly`: Total unique counts for events in same category for the last 28 days or the last 4 complete weeks, if events are in the same Redis slot and we have more than one metric.
@ -732,7 +820,7 @@ On GitLab.com, we have DangerBot setup to monitor Product Intelligence related f
### 10. Verify your metric
On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "Saas" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
On GitLab.com, the Product Intelligence team regularly monitors Usage Ping. They may alert you that your metrics need further optimization to run quicker and with greater success. You may also use the [Usage Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" & "SaaS" and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you may re-optimize your metric.
### Optional: Test Prometheus based Usage Ping
@ -783,8 +871,6 @@ appear to be associated to any of the services running, since they all appear to
## Aggregated metrics
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45979) in GitLab 13.6.
> - It's [deployed behind a feature flag](../user/feature_flags.md), disabled by default.
> - It's enabled on GitLab.com.
WARNING:
This feature is intended solely for internal GitLab use.

View File

@ -88,6 +88,22 @@ similar to how they appear when viewing a [milestone](../../project/milestones/i
Burndown charts help track completion progress of total scope, and burnup charts track the daily
total count and weight of issues added to and completed in a given timebox.
### Group issues by label
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225500) in GitLab 13.8.
You can group the list of issues by label.
This can help you view issues that have your team's label,
and get a more accurate understanding of scope attributable to each label.
To group issues by label:
1. In the **Group by** dropdown, select **Label**.
1. Select the **Filter by label** dropdown.
1. Select the labels you want to group by in the labels dropdown.
You can also search for labels by typing in the search input.
1. Click or tap outside of the label dropdown. The page is now grouped by the selected labels.
## Disable iterations **(STARTER ONLY)**
GitLab Iterations feature is deployed with a feature flag that is **enabled by default**.

View File

@ -141,11 +141,10 @@ You can use the Dependency Proxy to pull your base image.
bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
```
This can also be other credentials such as:
This can also be a [personal access token](../../../user/profile/personal_access_tokens.md) such as:
```shell
echo -n "my_username:personal_access_token" | base64
echo -n "deploy_token_username:deploy_token" | base64
```
1. Create a [custom environment variables](../../../ci/variables/README.md#custom-environment-variables)

View File

@ -123,7 +123,7 @@ nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/proje
To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with `nuget`:
```shell
nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/groups/<your_group_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
```
- `<source_name>` is the desired source name.
@ -131,7 +131,7 @@ nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/
For example:
```shell
nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/groups/23/packages/nuget/index.json" -UserName carol -Password 12345678asdf
nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/groups/23/-/packages/nuget/index.json" -UserName carol -Password 12345678asdf
```
### Add a source with Visual Studio
@ -173,7 +173,7 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endp
1. Select **Add**.
1. Complete the following fields:
- **Name**: Name for the source.
- **Location**: `https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json`,
- **Location**: `https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json`,
where `<your_group_id>` is your group ID, and `gitlab.example.com` is
your domain name.
- **Username**: Your GitLab username or deploy token username.
@ -227,7 +227,7 @@ To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Re
<configuration>
<packageSources>
<clear />
<add key="gitlab" value="https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json" />
<add key="gitlab" value="https://gitlab.example.com/api/v4/groups/<your_group_id>/-/packages/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<gitlab>

View File

@ -153,8 +153,7 @@ Users are notified of the following events:
| Project access level changed | User | Sent when user project access level is changed |
| User added to group | User | Sent when user is added to group |
| Group access level changed | User | Sent when user group access level is changed |
| Personal Access Tokens expiring soon | User | Security email, always sent. |
<!-- Do not delete or lint this instance of future tense -->
| Personal Access Tokens expiring soon <!-- Do not delete or lint this instance of future tense --> | User | Security email, always sent. |
| Personal Access Tokens have expired | User | Security email, always sent. |
| Project moved | Project members (1) | (1) not disabled |
| New release | Project members | Custom notification |

View File

@ -13,6 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> quick actions when updating the description of issues, epics, and merge requests.
> - Introduced in [GitLab 13.8](https://gitlab.com/gitlab-org/gitlab/-/issues/292393): when you enter
> `/` into a description or comment field, all available quick actions are displayed in a scrollable list.
> - The rebase quick action was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49800) in GitLab 13.8.
Quick actions are textual shortcuts for common actions on issues, epics, merge requests,
and commits that are usually done by clicking buttons or dropdowns in the GitLab UI.
@ -61,7 +62,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/promote` | ✓ | | | Promote issue to epic. **(PREMIUM)** |
| `/publish` | ✓ | | | Publish issue to an associated [Status Page](../../operations/incident_management/status_page.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) **(ULTIMATE)** |
| `/reassign @user1 @user2` | ✓ | ✓ | | Replace current assignees with those specified. **(STARTER)** |
| `/rebase` | | ✓ | | Rebase source branch. This will schedule a background task that attempt to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` will be ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. |
| `/rebase` | | ✓ | | Rebase source branch. This will schedule a background task that attempt to rebase the changes in the source branch on the latest commit of the target branch. If `/rebase` is used, `/merge` will be ignored to avoid a race condition where the source branch is merged or deleted before it is rebased. If there are merge conflicts, GitLab will display a message that a rebase cannot be scheduled. Rebase failures will be displayed with the merge request status. |
| `/reassign_reviewer @user1 @user2` | | ✓ | | Replace current reviewers with those specified. **(STARTER)** |
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace current labels with those specified. |
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related. **(STARTER)** |

View File

@ -33,8 +33,8 @@ start seeing results.
## Command palette
You can see all available commands for manipulating editor content by pressing
the <kbd>F1</kbd> key when the editor is in focus. After that,
you'll see a complete list of available commands for
the <kbd>F1</kbd> key when the editor is in focus. After that, the editor displays
a complete list of available commands for
manipulating editor content. The editor supports commands for multi-cursor
editing, code block folding, commenting, searching and replacing, navigating
editor warnings and suggestions, and more.
@ -47,7 +47,7 @@ the command without having to select it in the command palette.
## Syntax highlighting
As expected from an IDE, syntax highlighting for many languages within
As expected from an IDE, syntax highlighting for many languages in
the Web IDE makes your direct editing even easier.
The Web IDE currently provides:
@ -78,7 +78,7 @@ All the themes GitLab supports for syntax highlighting are added to the Web IDE'
You can pick a theme from your [profile preferences](../../profile/preferences.md).
The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and
the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
the [Solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228),
which apply to the entire Web IDE screen.
| Solarized Light Theme | Solarized Dark Theme | Dark Theme |
@ -144,12 +144,13 @@ schemas:
Each schema entry supports two properties:
- `uri`: please provide an absolute URL for the schema definition file here. The schema from this URL
is loaded when a matching file is open.
- `match`: a list of matching paths or glob expressions. If a schema matches a particular path pattern,
it will be applied to that file. Please enclose the pattern in quotes if it begins with an asterisk (`*`),
it's be applied to that file. If a pattern begins with an asterisk (`*`), enclose it in quotation
marks. Otherwise, the configuration file is not valid YAML.
- `uri`: please provide an absolute URL for the schema definition file here.
The schema from this URL is loaded when a matching file is open.
- `match`: a list of matching paths or glob expressions. If a schema matches a
particular path pattern, it is applied to that file. Please enclose the pattern
in quotes if it begins with an asterisk (`*`), it's be applied to that file.
If a pattern begins with an asterisk (`*`), enclose it in quotation marks.
Otherwise, the configuration file is not valid YAML.
## Configure the Web IDE
@ -180,7 +181,7 @@ The Web IDE currently supports the following `.editorconfig` settings:
After making your changes, click the **Commit** button on the bottom-left to
review the list of changed files.
Once you have finalized your changes, you can add a commit message, commit the
After you have finalized your changes, you can add a commit message, commit the
changes and directly create a merge request. In case you don't have write
access to the selected branch, you see a warning, but can still create
a new branch and start a merge request.
@ -268,7 +269,7 @@ GitLab.com
![Administrator Live Preview setting](img/admin_live_preview_v13_0.png)
Once you have done that, you can preview projects with a `package.json` file and
After you have done that, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE. An example `package.json` is shown
below.
@ -325,7 +326,7 @@ In order to enable the Web IDE terminals you need to create the file
file is fairly similar to the [CI configuration file](../../../ci/yaml/README.md)
syntax but with some restrictions:
- No global blocks can be defined (i.e., `before_script` or `after_script`)
- No global blocks (such as `before_script` or `after_script`) can be defined.
- Only one job named `terminal` can be added to this file.
- Only the keywords `image`, `services`, `tags`, `before_script`, `script`, and
`variables` are allowed to be used to configure the job.
@ -350,7 +351,7 @@ terminal:
NODE_ENV: "test"
```
Once the terminal has started, the console is displayed and we could access
After the terminal has started, the console is displayed and we could access
the project repository files.
**Important**. The terminal job is branch dependent. This means that the
@ -364,7 +365,7 @@ If there is no configuration file in a branch, an error message is shown.
If Interactive Terminals are available for the current user, the **Terminal** button is visible in the right sidebar of the Web IDE. Click this button to open
or close the terminal tab.
Once open, the tab shows the **Start Web Terminal** button. This button may
After opening, the tab shows the **Start Web Terminal** button. This button may
be disabled if the environment is not configured correctly. If so, a status
message describes the issue. Here are some reasons why **Start Web Terminal**
may be disabled:
@ -378,7 +379,7 @@ can be closed and reopened and the state of the terminal is not affected.
When the terminal is started and is successfully connected to the runner, then the
runner's shell prompt appears in the terminal. From here, you can enter
commands executed within the runner's environment. This is similar
commands executed in the runner's environment. This is similar
to running commands in a local terminal or through SSH.
While the terminal is running, it can be stopped by clicking **Stop Terminal**.
@ -426,7 +427,7 @@ terminal:
[predefined environment variable](../../../ci/variables/predefined_variables.md)
for GitLab Runners. This is where your project's repository resides.
Once you have configured the web terminal for file syncing, then when the web
After you have configured the web terminal for file syncing, then when the web
terminal is started, a **Terminal** status is visible in the status bar.
![Web IDE Client Side Evaluation](img/terminal_status.png)
@ -434,7 +435,7 @@ terminal is started, a **Terminal** status is visible in the status bar.
Changes made to your files via the Web IDE sync to the running terminal
when:
- <kbd>Ctrl</kbd> + <kbd>S</kbd> (or <kbd>Cmd</kbd> + <kbd>S</kbd> on Mac)
- <kbd>Control</kbd> + <kbd>S</kbd> (or <kbd>Command</kbd> + <kbd>S</kbd> on Mac)
is pressed while editing a file.
- Anything outside the file editor is clicked after editing a file.
- A file or folder is created, deleted, or renamed.
@ -446,7 +447,7 @@ The Web IDE has a few limitations:
- Interactive Terminals is in a beta phase and continues to be improved in upcoming releases. In the meantime, please note that the user is limited to having only one
active terminal at a time.
- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository will be overwritten with the modified LFS file content.
- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository is overwritten with the modified LFS file content.
### Troubleshooting

View File

@ -18,7 +18,7 @@ The number displayed on their right represents the number of issues and merge re
![issues and MRs dashboard links](img/dashboard_links.png)
When you click **Issues**, you'll see the opened issues assigned to you straight away:
When you click **Issues**, the opened issues assigned to you are shown straight away:
![Issues assigned to you](img/issues_assigned_to_you.png)
@ -29,7 +29,7 @@ You can also filter the results using the search and filter field, as described
### Issues and MRs assigned to you or created by you
You'll also find shortcuts to issues and merge requests created by you or assigned to you
GitLab shows shortcuts to issues and merge requests created by you or assigned to you
on the search field on the top-right of your screen:
![shortcut to your issues and merge requests](img/issues_mrs_shortcut.png)
@ -40,7 +40,7 @@ on the search field on the top-right of your screen:
> - Filtering by child Epics was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9029) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
> - Filtering by Iterations was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.6.
Follow these steps to filter the **Issues** and **Merge Requests** list pages within projects and
Follow these steps to filter the **Issues** and **Merge Requests** list pages in projects and
groups:
1. Click in the field **Search or filter results...**.
@ -74,7 +74,7 @@ Some filter fields like milestone and assignee, allow you to filter by **None**
![filter by none any](img/issues_filter_none_any.png)
Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee.
Selecting **None** returns results that have an empty value for that field. For example: no milestone, no assignee.
Selecting **Any** does the opposite. It returns results that have a non-empty value for that field.
@ -83,11 +83,11 @@ Selecting **Any** does the opposite. It returns results that have a non-empty va
You can filter issues and merge requests by specific terms included in titles or descriptions.
- Syntax
- Searches look for all the words in a query, in any order. E.g.: searching
issues for `display bug` will return all issues matching both those words, in any order.
- Searches look for all the words in a query, in any order. For example: searching
issues for `display bug` returns all issues matching both those words, in any order.
- To find the exact term, use double quotes: `"display bug"`
- Limitation
- For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
- For performance reasons, terms shorter than 3 chars are ignored. For example: searching
issues for `included in titles` is same as `included titles`
- Search is limited to 4096 characters and 64 terms per query.
@ -157,7 +157,7 @@ relevant users or other attributes.
For performance optimization, there is a requirement of a minimum of three
characters to begin your search. For example, if you want to search for
issues that have the assignee "Simone Presley", you'll need to type at
issues that have the assignee "Simone Presley", you must type at
least "Sim" before autocomplete gives any relevant results.
## Search history
@ -170,11 +170,11 @@ You can view recent searches by clicking on the little arrow-clock icon, which i
Individual filters can be removed by clicking on the filter's (x) button or backspacing. The entire search filter can be cleared by clicking on the search box's (x) button or via <kbd></kbd> (Mac) + <kbd></kbd>.
To delete filter tokens one at a time, the <kbd></kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd></kbd> keyboard combination can be used.
To delete filter tokens one at a time, the <kbd></kbd> (Mac) / <kbd>Control</kbd> + <kbd></kbd> keyboard combination can be used.
## Filtering with multiple filters of the same type
Some filters can be added multiple times. These include but are not limited to assignees and labels. When you filter with these multiple filters of the same type, the AND logic is applied. For example, if you were filtering `assignee:@sam assignee:@sarah`, your results will only include entries whereby the assignees are assigned to both Sam and Sarah are returned.
Some filters can be added multiple times. These include but are not limited to assignees and labels. When you filter with these multiple filters of the same type, the AND logic is applied. For example, if you were filtering `assignee:@sam assignee:@sarah`, your results include only entries whereby the assignees are assigned to both Sam and Sarah are returned.
![multiple assignees filtering](img/multiple_assignees.png)
@ -190,7 +190,7 @@ author, type, and action. Also, you can sort them by
You can search through your projects from the left menu, by clicking the menu bar, then **Projects**.
On the field **Filter by name**, type the project or group name you want to find, and GitLab
will filter them for you as you type.
filters them for you as you type.
You can also look for the projects you [starred](../project/index.md#star-a-project) (**Starred projects**), and **Explore** all
public and internal projects available in GitLab.com, from which you can filter by visibility,
@ -207,7 +207,7 @@ Similarly to [projects search](#projects), you can search through your groups fr
the left menu, by clicking the menu bar, then **Groups**.
On the field **Filter by name**, type the group name you want to find, and GitLab
will filter them for you as you type.
filters them for you as you type.
You can also **Explore** all public and internal groups available in GitLab.com,
and sort them by **Last created**, **Oldest created**, **Last updated**, or **Oldest updated**.
@ -219,15 +219,15 @@ You can also filter them by name (issue title), from the field **Filter by name*
When you want to search for issues to add to lists present in your Issue Board, click
the button **Add issues** on the top-right of your screen, opening a modal window from which
you'll be able to, besides filtering them by **Name**, **Author**, **Assignee**, **Milestone**,
you can, besides filtering them by **Name**, **Author**, **Assignee**, **Milestone**,
and **Labels**, select multiple issues to add to a list of your choice:
![search and select issues to add to board](img/search_issues_board.png)
## Shortcut
You'll find a shortcut on the search field on the top-right of the project's dashboard to
quickly access issues and merge requests created or assigned to you within that project:
GitLab shows a shortcut on the search field on the top-right of the project's dashboard to
quickly access issues and merge requests created or assigned to you in that project:
![search per project - shortcut](img/project_search.png)
@ -242,12 +242,12 @@ You can also type in this search bar to see autocomplete suggestions for:
- Recently viewed issues (try and type some word from the title of a recently viewed issue)
- Recently viewed merge requests (try and type some word from the title of a recently viewed merge request)
- Recently viewed epics (try and type some word from the title of a recently viewed epic)
- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues within a project (try and type a GFM reference for an issue)
- [GitLab Flavored Markdown](../markdown.md#special-gitlab-references) (GFM) for issues in a project (try and type a GFM reference for an issue)
## Basic search
The Basic search in GitLab is a global search service that allows you to search
across the entire GitLab instance, within a group, or a single project. Basic search is
across the entire GitLab instance, in a group, or in a single project. Basic search is
backed by the database and allows searching in:
- Projects
@ -262,12 +262,12 @@ backed by the database and allows searching in:
- Wiki (Project only)
To start a search, type into the search bar on the top-right of the screen. You can always search
in all GitLab and may also see the options to search within a group or project if you are in the
in all GitLab and may also see the options to search in a group or project if you are in the
group or project dashboard.
![basic search](img/basic_search.png)
Once the results are returned, you can modify the search, select a different type of data to
After the results are returned, you can modify the search, select a different type of data to
search, or choose a specific group or project.
![basic_search_results](img/basic_search_results.png)
@ -282,11 +282,11 @@ the search field on the top-right of your screen while the project page is open.
### SHA search
You can quickly access a commit from within the project dashboard by entering the SHA
into the search field on the top right of the screen. If a single result is found, you will be
You can quickly access a commit from the project dashboard by entering the SHA
into the search field on the top right of the screen. If a single result is found, you are
redirected to the commit result and given the option to return to the search results page.
![project sha search redirect](img/project_search_sha_redirect.png)
![project SHA search redirect](img/project_search_sha_redirect.png)
## Advanced Search **(STARTER)**
@ -314,7 +314,7 @@ This feature might not be available to you. Check the **version history** note a
You can search inside the projects settings sections by entering a search
term in the search box located at the top of the page. The search results
will appear highlighted in the sections that match the search term.
appear highlighted in the sections that match the search term.
![Search project settings](img/project_search_general_settings_v13_8.png)

View File

@ -69,8 +69,8 @@ new commit to the master branch is recorded. Commit messages are automatically
generated. The snippet's repository has only one branch (master) by default, deleting
it or creating other branches is not supported.
Existing snippets will be automatically migrated in 13.0. Their current
content will be saved as the initial commit to the snippets' repository.
Existing snippets are automatically migrated in 13.0. Their current
content is saved as the initial commit to the snippets' repository.
### Filenames
@ -86,10 +86,10 @@ number increases incrementally when more snippets without an attributed
filename are added.
When upgrading from an earlier version of GitLab to 13.0, existing snippets
without a supported filename will be renamed to a compatible format. For
example, if the snippet's filename is `http://a-weird-filename.me` it will
be changed to `http-a-weird-filename-me` to be included in the snippet's
repository. As snippets are stored by ID, changing their filenames will not break
without a supported filename are renamed to a compatible format. For
example, if the snippet's filename is `http://a-weird-filename.me` it is
changed to `http-a-weird-filename-me` to be included in the snippet's
repository. As snippets are stored by ID, changing their filenames breaks
direct or embedded links to the snippet.
### Multiple files by Snippet
@ -105,8 +105,8 @@ to a certain context. For example:
- A snippet with a `docker-compose.yml` file and its associated `.env` file.
- A `gulpfile.js` file coupled with a `package.json` file, which together can be used to bootstrap a project and manage its dependencies.
Snippets support between 1 and 10 files. They can be managed via Git (since they're [versioned](#versioned-snippets)
by a Git repository), through the [Snippets API](../api/snippets.md), or within the GitLab UI.
Snippets support between 1 and 10 files. They can be managed via Git (because they're [versioned](#versioned-snippets)
by a Git repository), through the [Snippets API](../api/snippets.md), or in the GitLab UI.
![Multi-file Snippet](img/gitlab_snippet_v13_5.png)
@ -139,7 +139,7 @@ master branch.
### Reduce snippets repository size
Since versioned Snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
Because versioned Snippets are considered as part of the [namespace storage size](../user/admin_area/settings/account_and_limit_settings.md),
it's recommended to keep snippets' repositories as compact as possible.
For more information about tools to compact repositories,
@ -151,7 +151,7 @@ see the documentation on [reducing repository size](../user/project/repository/r
- Creating or deleting branches is not supported. Only a default *master* branch is used.
- Git tags are not supported in snippet repositories.
- Snippets' repositories are limited to 10 files. Attempting to push more
than 10 files will result in an error.
than 10 files results in an error.
- Revisions are not *yet* visible to the user on the GitLab UI, but
it's planned to be added in future iterations. See the [revisions tab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/39271)
for updates.
@ -187,9 +187,9 @@ facilitating the collaboration among users.
You can download the raw content of a snippet.
By default snippets will be downloaded with Linux-style line endings (`LF`). If
By default snippets are downloaded with Linux-style line endings (`LF`). If
you want to preserve the original line endings you need to add a parameter `line_ending=raw`
(e.g., `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
(For example: `https://gitlab.com/snippets/SNIPPET_ID/raw?line_ending=raw`). In case a
snippet was created using the GitLab web interface the original line ending is Windows-like (`CRLF`).
## Embedded snippets
@ -207,7 +207,7 @@ To embed a snippet, first make sure that:
- In **Project > Settings > Permissions**, the snippets permissions are
set to **Everyone with access**
Once the above conditions are met, the "Embed" section will appear in your
After the above conditions are met, the "Embed" section appears in your
snippet where you can simply click on the "Copy" button. This copies a one-line
script that you can add to any website or blog post.

View File

@ -45,7 +45,7 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/nuget' do
namespace ':id/-/packages/nuget' do
after_validation do
# This API can't be accessed anonymously
require_authenticated!

View File

@ -724,6 +724,9 @@ msgstr ""
msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities."
msgstr ""
msgid "%{requirementCount} requirements have been selected for export. These will be sent to %{email} as an attachment once finished."
msgstr ""
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
msgstr ""
@ -1627,6 +1630,9 @@ msgstr ""
msgid "Add a comment to this line"
msgstr ""
msgid "Add a comment to this line or drag for multiple lines"
msgstr ""
msgid "Add a general comment to this %{noteableDisplayName}."
msgstr ""
@ -9694,6 +9700,9 @@ msgstr ""
msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data"
msgstr ""
msgid "Deployments"
msgstr ""
msgid "Deployment|API"
msgstr ""
@ -9877,7 +9886,7 @@ msgstr ""
msgid "DesignManagement|There was an error moving your designs. Please upload your designs below."
msgstr ""
msgid "DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
msgid "DesignManagement|To upload designs, you'll need to enable LFS and have an admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
msgstr ""
msgid "DesignManagement|Unresolve thread"
@ -9928,25 +9937,25 @@ msgstr ""
msgid "DevopsAdoptionSegment|The maximum number of selections has been reached"
msgstr ""
msgid "DevopsAdoption|%{selectedCount} group selected (20 max)"
msgid "DevopsAdoption|%{selectedCount} group selected"
msgstr ""
msgid "DevopsAdoption|%{selectedCount} groups selected (20 max)"
msgid "DevopsAdoption|%{selectedCount} groups selected"
msgstr ""
msgid "DevopsAdoption|Add a segment to get started"
msgid "DevopsAdoption|Add Group"
msgstr ""
msgid "DevopsAdoption|Add new segment"
msgid "DevopsAdoption|Add a group to get started"
msgstr ""
msgid "DevopsAdoption|Adopted"
msgstr ""
msgid "DevopsAdoption|An error occured while deleting the segment. Please try again."
msgid "DevopsAdoption|An error occured while deleting the group. Please try again."
msgstr ""
msgid "DevopsAdoption|An error occured while saving the segment. Please try again."
msgid "DevopsAdoption|An error occured while saving the group. Please try again."
msgstr ""
msgid "DevopsAdoption|Approvals"
@ -9973,22 +9982,19 @@ msgstr ""
msgid "DevopsAdoption|At least 1 security scan of any type run in pipeline"
msgstr ""
msgid "DevopsAdoption|Confirm delete segment"
msgid "DevopsAdoption|Confirm delete Group"
msgstr ""
msgid "DevopsAdoption|Create new segment"
msgstr ""
msgid "DevopsAdoption|Delete segment"
msgid "DevopsAdoption|Delete Group"
msgstr ""
msgid "DevopsAdoption|Deploys"
msgstr ""
msgid "DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team."
msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
msgstr ""
msgid "DevopsAdoption|Edit segment"
msgid "DevopsAdoption|Edit Group"
msgstr ""
msgid "DevopsAdoption|Feature adoption is based on usage in the last calendar month. Last updated: %{timestamp}."
@ -9997,24 +10003,24 @@ msgstr ""
msgid "DevopsAdoption|Filter by name"
msgstr ""
msgid "DevopsAdoption|Group data pending until the start of next month"
msgstr ""
msgid "DevopsAdoption|Issues"
msgstr ""
msgid "DevopsAdoption|MRs"
msgstr ""
msgid "DevopsAdoption|Maximum %{maxSegments} segments allowed"
msgid "DevopsAdoption|Maximum %{maxSegments} groups allowed"
msgstr ""
msgid "DevopsAdoption|My segment"
msgid "DevopsAdoption|My group"
msgstr ""
msgid "DevopsAdoption|Name"
msgstr ""
msgid "DevopsAdoption|New segment"
msgstr ""
msgid "DevopsAdoption|No filter results."
msgstr ""
@ -10036,18 +10042,12 @@ msgstr ""
msgid "DevopsAdoption|Scanning"
msgstr ""
msgid "DevopsAdoption|Segment"
msgstr ""
msgid "DevopsAdoption|Segment data pending until the start of next month"
msgid "DevopsAdoption|There was an error fetching Group adoption data. Please refresh the page to try again."
msgstr ""
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page to try again."
msgstr ""
msgid "DevopsAdoption|There was an error fetching Segments. Please refresh the page to try again."
msgstr ""
msgid "DevopsReport|Adoption"
msgstr ""
@ -11799,6 +11799,9 @@ msgstr ""
msgid "Export project"
msgstr ""
msgid "Export requirements"
msgstr ""
msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
msgstr ""
@ -12638,6 +12641,12 @@ msgstr ""
msgid "For a faster browsing experience, some files are collapsed by default."
msgstr ""
msgid "For additional information, review your %{link_to} or contact your group owner."
msgstr ""
msgid "For additional information, review your group membership: %{link_to} or contact your group owner."
msgstr ""
msgid "For help setting up the Service Desk for your instance, please contact an administrator."
msgstr ""
@ -13733,6 +13742,12 @@ msgstr ""
msgid "Group members"
msgstr ""
msgid "Group membership expiration date changed"
msgstr ""
msgid "Group membership expiration date removed"
msgstr ""
msgid "Group milestone"
msgstr ""
@ -26356,6 +26371,9 @@ msgstr ""
msgid "Something went wrong while editing your comment. Please try again."
msgstr ""
msgid "Something went wrong while exporting requirements"
msgstr ""
msgid "Something went wrong while fetching %{listType} list"
msgstr ""
@ -32639,6 +32657,9 @@ msgstr ""
msgid "YouTube URL or ID"
msgstr ""
msgid "Your %{group} membership will now expire in %{days}."
msgstr ""
msgid "Your %{host} account was signed in to from a new location"
msgstr ""
@ -32837,6 +32858,9 @@ msgstr ""
msgid "Your license will be included in your GitLab backup and will survive upgrades, so in normal usage you should never need to re-upload your %{code_open}.gitlab-license%{code_close} file."
msgstr ""
msgid "Your membership in %{group} no longer expires."
msgstr ""
msgid "Your message here"
msgstr ""

View File

@ -45,7 +45,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.178.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "25.8.0",
"@gitlab/ui": "25.11.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",

View File

@ -38,7 +38,7 @@ RSpec.describe 'User uploads new design', :js do
let(:feature_enabled) { false }
it 'shows the message about requirements' do
expect(page).to have_content("To upload designs, you'll need to enable LFS and have admin enable hashed storage.")
expect(page).to have_content("To upload designs, you'll need to enable LFS and have an admin enable hashed storage.")
end
end

View File

@ -24,16 +24,18 @@
"properties": {
"integer_value": { "type": "integer" },
"string_value": { "type": "string" }
}
},
"additionalProperties": false
},
"source": {
"type": "object",
"required": ["id", "name", "web_url"],
"required": ["id", "full_name", "web_url"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"full_name": { "type": "string" },
"web_url": { "type": "string" }
}
},
"additionalProperties": false
},
"valid_roles": { "type": "object" },
"created_by": {
@ -42,39 +44,13 @@
"properties": {
"name": { "type": "string" },
"web_url": { "type": "string" }
}
},
"additionalProperties": false
},
"user": {
"type": "object",
"required": [
"id",
"name",
"username",
"avatar_url",
"web_url",
"blocked",
"two_factor_enabled"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": ["string", "null"] },
"web_url": { "type": "string" },
"blocked": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
"status": {
"type": "object",
"required": [
"emoji",
"message_html"
],
"properties": {
"emoji": { "type": "string" },
"message_html": { "type": "string" }
}
}
}
"allOf": [
{ "$ref": "member_user.json" }
]
},
"invite": {
"type": "object",
@ -83,7 +59,8 @@
"email": { "type": "string" },
"avatar_url": { "type": "string" },
"can_resend": { "type": "boolean" }
}
},
"additionalProperties": false
}
}
}

View File

@ -0,0 +1,22 @@
{
"type": "object",
"required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": ["string", "null"] },
"web_url": { "type": "string" },
"blocked": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
"status": {
"type": "object",
"required": ["emoji"],
"properties": {
"emoji": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}

View File

@ -1,6 +1,6 @@
{
"type": "array",
"items": {
"$ref": "group_member.json"
"$ref": "entities/member.json"
}
}

View File

@ -41,109 +41,119 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
role="menu"
tabindex="-1"
>
<!---->
<li
class="gl-new-dropdown-item"
role="presentation"
<div
class="gl-new-dropdown-inner"
>
<button
class="dropdown-item"
role="menuitem"
type="button"
<!---->
<div
class="gl-new-dropdown-contents"
>
<svg
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon"
data-testid="mobile-issue-close-icon"
<li
class="gl-new-dropdown-item"
role="presentation"
>
<use
href="#mobile-issue-close"
/>
</svg>
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
<button
class="dropdown-item"
role="menuitem"
type="button"
>
<strong>
Remove integration and resources
</strong>
<svg
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon"
data-testid="mobile-issue-close-icon"
>
<use
href="#mobile-issue-close"
/>
</svg>
<div>
Deletes all GitLab resources attached to this cluster during removal
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
<strong>
Remove integration and resources
</strong>
<div>
Deletes all GitLab resources attached to this cluster during removal
</div>
</p>
<!---->
</div>
</p>
<!---->
</div>
<!---->
</button>
</li>
<!---->
</button>
</li>
<li
class="gl-new-dropdown-divider"
role="presentation"
>
<hr
aria-orientation="horizontal"
class="dropdown-divider"
role="separator"
/>
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<button
class="dropdown-item"
role="menuitem"
type="button"
>
<svg
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
data-testid="mobile-issue-close-icon"
<li
class="gl-new-dropdown-divider"
role="presentation"
>
<use
href="#mobile-issue-close"
<hr
aria-orientation="horizontal"
class="dropdown-divider"
role="separator"
/>
</svg>
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
</li>
<li
class="gl-new-dropdown-item"
role="presentation"
>
<p
class="gl-new-dropdown-item-text-primary"
<button
class="dropdown-item"
role="menuitem"
type="button"
>
<strong>
Remove integration
</strong>
<svg
aria-hidden="true"
class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
data-testid="mobile-issue-close-icon"
>
<use
href="#mobile-issue-close"
/>
</svg>
<div>
Removes cluster from project but keeps associated resources
<!---->
<!---->
<div
class="gl-new-dropdown-item-text-wrapper"
>
<p
class="gl-new-dropdown-item-text-primary"
>
<strong>
Remove integration
</strong>
<div>
Removes cluster from project but keeps associated resources
</div>
</p>
<!---->
</div>
</p>
<!---->
</div>
<!---->
</button>
</li>
<!---->
</button>
</li>
<!---->
</div>
<!---->
</div>
</ul>
</div>

View File

@ -4,6 +4,7 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
headertext=""
hideheaderborder="true"
issueiid=""
projectpath=""
size="small"
@ -44,6 +45,7 @@ exports[`Design management design version dropdown component renders design vers
<gl-dropdown-stub
category="primary"
headertext=""
hideheaderborder="true"
issueiid=""
projectpath=""
size="small"

View File

@ -282,14 +282,10 @@ describe('diffs/components/app', () => {
let moveSpy;
let jumpSpy;
function setup(componentProps, featureFlags) {
createComponent(
componentProps,
({ state }) => {
state.diffs.commit = { id: 'SHA123' };
},
{ glFeatures: featureFlags },
);
function setup(componentProps) {
createComponent(componentProps, ({ state }) => {
state.diffs.commit = { id: 'SHA123' };
});
moveSpy = jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
jumpSpy = jest.spyOn(wrapper.vm, 'jumpToFile').mockImplementation(() => {});
@ -298,17 +294,17 @@ describe('diffs/components/app', () => {
describe('visible app', () => {
it.each`
key | name | spy | args | featureFlags
${'['} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
${'k'} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
${']'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
${'j'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'previous' }]} | ${{}}
${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'next' }]} | ${{}}
key | name | spy | args
${'['} | ${'jumpToFile'} | ${0} | ${[-1]}
${'k'} | ${'jumpToFile'} | ${0} | ${[-1]}
${']'} | ${'jumpToFile'} | ${0} | ${[+1]}
${'j'} | ${'jumpToFile'} | ${0} | ${[+1]}
${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'previous' }]}
${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'next' }]}
`(
'calls `$name()` with correct parameters whenever the "$key" key is pressed',
async ({ key, spy, args, featureFlags }) => {
setup({ shouldShow: true }, featureFlags);
async ({ key, spy, args }) => {
setup({ shouldShow: true });
await nextTick();
expect(spies[spy]).not.toHaveBeenCalled();

View File

@ -37,15 +37,12 @@ describe('diffs/components/commit_item', () => {
const getPrevCommitNavElement = () =>
getCommitNavButtonsElement().find('.btn-group > *:first-child');
const mountComponent = (propsData, featureFlags = {}) => {
const mountComponent = (propsData) => {
wrapper = mount(Component, {
propsData: {
commit,
...propsData,
},
provide: {
glFeatures: featureFlags,
},
stubs: {
CommitPipelineStatus: true,
},

View File

@ -11,14 +11,16 @@ describe('DiffLineNoteForm', () => {
let diffLines;
const getDiffFileMock = () => ({ ...diffFileMockData });
beforeEach(() => {
const createComponent = (args = {}) => {
diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines;
const store = createStore();
store.state.notes.userData.id = 1;
store.state.notes.noteableData = noteableDataMock;
wrapper = shallowMount(DiffLineNoteForm, {
store.replaceState({ ...store.state, ...args.state });
return shallowMount(DiffLineNoteForm, {
store,
propsData: {
diffFileHash: diffFile.file_hash,
@ -27,9 +29,13 @@ describe('DiffLineNoteForm', () => {
noteTargetLine: diffLines[0],
},
});
});
};
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('handleCancelCommentForm', () => {
it('should ask for confirmation when shouldConfirm and isDirty passed as truthy', () => {
jest.spyOn(window, 'confirm').mockReturnValue(false);
@ -114,14 +120,39 @@ describe('DiffLineNoteForm', () => {
describe('mounted', () => {
it('should init autosave', () => {
const key = 'autosave/Note/Issue/98//DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
wrapper = createComponent();
expect(wrapper.vm.autosave).toBeDefined();
expect(wrapper.vm.autosave.key).toEqual(key);
});
it('should set selectedCommentPosition', () => {
wrapper = createComponent();
let startLineCode = wrapper.vm.commentLineStart.line_code;
let lineCode = wrapper.vm.line.line_code;
expect(startLineCode).toEqual(lineCode);
wrapper.destroy();
const state = {
notes: {
selectedCommentPosition: {
start: {
line_code: 'test',
},
},
},
};
wrapper = createComponent({ state });
startLineCode = wrapper.vm.commentLineStart.line_code;
lineCode = state.notes.selectedCommentPosition.start.line_code;
expect(startLineCode).toEqual(lineCode);
});
});
describe('template', () => {
it('should have note form', () => {
wrapper = createComponent();
expect(wrapper.find(NoteForm).exists()).toBe(true);
});
});

View File

@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getByTestId, fireEvent } from '@testing-library/dom';
import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue';
@ -42,16 +43,16 @@ describe('DiffRow', () => {
fileHash: 'abc',
filePath: 'abc',
line: {},
index: 0,
...props,
};
return shallowMount(DiffRow, { propsData, localVue, store });
};
it('isHighlighted returns true if isCommented is true', () => {
const props = { isCommented: true };
const wrapper = createWrapper({ props });
expect(wrapper.vm.isHighlighted).toBe(true);
});
const provide = {
glFeatures: { dragCommentSelection: true },
};
return shallowMount(DiffRow, { propsData, localVue, store, provide });
};
it('isHighlighted returns true given line.left', () => {
const props = {
@ -124,4 +125,36 @@ describe('DiffRow', () => {
const lineNumber = testLines[0].right.new_line;
expect(wrapper.find(`[data-linenumber="${lineNumber}"]`).exists()).toBe(true);
});
describe('drag operations', () => {
let line;
beforeEach(() => {
line = { ...testLines[0] };
});
it.each`
side
${'left'}
${'right'}
`('emits `enterdragging` onDragEnter $side side', ({ side }) => {
const expectation = { ...line[side], index: 0 };
const wrapper = createWrapper({ props: { line } });
fireEvent.dragEnter(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().enterdragging).toBeTruthy();
expect(wrapper.emitted().enterdragging[0]).toEqual([expectation]);
});
it.each`
side
${'left'}
${'right'}
`('emits `stopdragging` onDrop $side side', ({ side }) => {
const wrapper = createWrapper({ props: { line } });
fireEvent.dragEnd(getByTestId(wrapper.element, `${side}-side`));
expect(wrapper.emitted().stopdragging).toBeTruthy();
});
});
});

View File

@ -126,14 +126,14 @@ describe('lineCode', () => {
describe('classNameMapCell', () => {
it.each`
line | hll | loggedIn | hovered | expectation
${undefined} | ${true} | ${true} | ${true} | ${[]}
${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
`('should return $expectation', ({ line, hll, loggedIn, hovered, expectation }) => {
const classes = utils.classNameMapCell(line, hll, loggedIn, hovered);
line | hll | isLoggedIn | isHover | expectation
${undefined} | ${true} | ${true} | ${true} | ${[]}
${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
`('should return $expectation', ({ line, hll, isLoggedIn, isHover, expectation }) => {
const classes = utils.classNameMapCell({ line, hll, isLoggedIn, isHover });
expect(classes).toEqual(expectation);
});
});

View File

@ -1,19 +1,19 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import DiffView from '~/diffs/components/diff_view.vue';
// import DraftNote from '~/batch_comments/components/draft_note.vue';
// import DiffRow from '~/diffs/components/diff_row.vue';
// import DiffCommentCell from '~/diffs/components/diff_comment_cell.vue';
// import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
describe('DiffView', () => {
const DiffExpansionCell = { template: `<div/>` };
const DiffRow = { template: `<div/>` };
const DiffCommentCell = { template: `<div/>` };
const DraftNote = { template: `<div/>` };
const showCommentForm = jest.fn();
const setSelectedCommentPosition = jest.fn();
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
const createWrapper = (props) => {
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
const batchComments = {
getters: {
@ -26,8 +26,13 @@ describe('DiffView', () => {
},
namespaced: true,
};
const diffs = { getters: { commitId: () => 'abc123' }, namespaced: true };
const diffs = {
actions: { showCommentForm },
getters: { commitId: () => 'abc123' },
namespaced: true,
};
const notes = {
actions: { setSelectedCommentPosition },
state: { selectedCommentPosition: null, selectedCommentPositionHover: null },
};
@ -41,7 +46,7 @@ describe('DiffView', () => {
...props,
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
return shallowMount(DiffView, { propsData, store, localVue, stubs });
return shallowMount(DiffView, { propsData, store, stubs });
};
it('renders a match line', () => {
@ -74,4 +79,55 @@ describe('DiffView', () => {
});
expect(wrapper.find(DraftNote).exists()).toBe(true);
});
describe('drag operations', () => {
it('sets `dragStart` onStartDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
wrapper.findComponent(DiffRow).vm.$emit('startdragging', { test: true });
expect(wrapper.vm.dragStart).toEqual({ test: true });
});
it('does not call `setSelectedCommentPosition` on different chunks onDragOver', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 0 });
diffRow.$emit('enterdragging', { chunk: 1 });
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
});
it.each`
start | end | expectation
${1} | ${2} | ${{ start: { index: 1 }, end: { index: 2 } }}
${2} | ${1} | ${{ start: { index: 1 }, end: { index: 2 } }}
${1} | ${1} | ${{ start: { index: 1 }, end: { index: 1 } }}
`(
'calls `setSelectedCommentPosition` with correct `updatedLineRange`',
({ start, end, expectation }) => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { chunk: 1, index: start });
diffRow.$emit('enterdragging', { chunk: 1, index: end });
const arg = setSelectedCommentPosition.mock.calls[0][1];
expect(arg).toMatchObject(expectation);
},
);
it('sets `dragStart` to null onStopDragging', () => {
const wrapper = createWrapper({ diffLines: [{}] });
const diffRow = getDiffRow(wrapper);
diffRow.$emit('startdragging', { test: true });
expect(wrapper.vm.dragStart).toEqual({ test: true });
diffRow.$emit('stopdragging');
expect(wrapper.vm.dragStart).toBeNull();
expect(showCommentForm).toHaveBeenCalled();
});
});
});

View File

@ -1159,7 +1159,7 @@ describe('DiffsStoreUtils', () => {
it('converts inline diff lines to parallel diff lines', () => {
const file = getDiffFileMock();
expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toEqual(
expect(utils.parallelizeDiffLines(file[INLINE_DIFF_LINES_KEY])).toMatchObject(
file.parallel_diff_lines,
);
});
@ -1178,16 +1178,17 @@ describe('DiffsStoreUtils', () => {
{
left: null,
right: {
chunk: 0,
type: 'new',
},
},
{
left: { type: 'conflict_marker_our' },
right: { type: 'conflict_marker_their' },
left: { chunk: 0, type: 'conflict_marker_our' },
right: { chunk: 0, type: 'conflict_marker_their' },
},
{
left: { type: 'conflict_our' },
right: { type: 'conflict_their' },
left: { chunk: 0, type: 'conflict_our' },
right: { chunk: 0, type: 'conflict_their' },
},
]);
});
@ -1196,9 +1197,9 @@ describe('DiffsStoreUtils', () => {
const file = getDiffFileMock();
const files = utils.parallelizeDiffLines(file.highlighted_diff_lines, true);
expect(files[5].left).toEqual(file.parallel_diff_lines[5].left);
expect(files[5].left).toMatchObject(file.parallel_diff_lines[5].left);
expect(files[5].right).toBeNull();
expect(files[6].left).toEqual(file.parallel_diff_lines[5].right);
expect(files[6].left).toMatchObject(file.parallel_diff_lines[5].right);
expect(files[6].right).toBeNull();
});
});

View File

@ -50,6 +50,7 @@ exports[`Alert integration settings form default state should match the default
category="primary"
data-qa-selector="incident_templates_dropdown"
headertext=""
hideheaderborder="true"
id="alert-integration-settings-issue-template"
size="medium"
text="selecte_tmpl"

View File

@ -114,49 +114,59 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="menu"
tabindex="-1"
>
<!---->
<div
class="gl-search-box-by-type"
class="gl-new-dropdown-inner"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<!---->
<div
class="gl-search-box-by-type-right-icons"
class="gl-new-dropdown-contents"
>
<!---->
<!---->
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
No matches found
</p>
</li>
</p>
</li>
</div>
<!---->
</div>
</ul>
</div>
@ -229,49 +239,59 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="menu"
tabindex="-1"
>
<!---->
<div
class="gl-search-box-by-type"
class="gl-new-dropdown-inner"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<!---->
<div
class="gl-search-box-by-type-right-icons"
class="gl-new-dropdown-contents"
>
<!---->
<!---->
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
class="gl-search-box-by-type-search-icon gl-icon s16"
data-testid="search-icon"
>
<use
href="#search"
/>
</svg>
<input
aria-label="Search"
class="gl-form-input gl-search-box-by-type-input form-control"
placeholder="Search"
type="text"
/>
<div
class="gl-search-box-by-type-right-icons"
>
<!---->
<!---->
</div>
</div>
<li
class="gl-new-dropdown-text text-secondary"
role="presentation"
>
<p
class="b-dropdown-text"
>
No matches found
</p>
</li>
</p>
</li>
</div>
<!---->
</div>
</ul>
</div>

View File

@ -49,7 +49,7 @@ describe('AccessRequestActionButtons', () => {
describe('when member is the current user', () => {
it('sets `message` prop correctly', () => {
expect(findRemoveMemberButton().props('message')).toBe(
`Are you sure you want to withdraw your access request for "${member.source.name}"`,
`Are you sure you want to withdraw your access request for "${member.source.fullName}"`,
);
});
});
@ -64,7 +64,7 @@ describe('AccessRequestActionButtons', () => {
});
expect(findRemoveMemberButton().props('message')).toBe(
`Are you sure you want to deny ${member.user.name}'s request to join "${member.source.name}"`,
`Are you sure you want to deny ${member.user.name}'s request to join "${member.source.fullName}"`,
);
});
});

View File

@ -39,7 +39,7 @@ describe('InviteActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.name}"`,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
title: 'Revoke invite',
isAccessRequest: false,
icon: 'remove',

View File

@ -39,7 +39,7 @@ describe('UserActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toEqual({
memberId: member.id,
message: `Are you sure you want to remove ${member.user.name} from "${member.source.name}"`,
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`,
title: 'Remove member',
isAccessRequest: false,
icon: 'remove',
@ -56,7 +56,7 @@ describe('UserActionButtons', () => {
});
expect(findRemoveMemberButton().props('message')).toBe(
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.name}"`,
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"`,
);
});
});

View File

@ -60,11 +60,11 @@ describe('LeaveModal', () => {
});
it('displays modal title', () => {
expect(getByText(`Leave "${member.source.name}"`).exists()).toBe(true);
expect(getByText(`Leave "${member.source.fullName}"`).exists()).toBe(true);
});
it('displays modal body', () => {
expect(getByText(`Are you sure you want to leave "${member.source.name}"?`).exists()).toBe(
expect(getByText(`Are you sure you want to leave "${member.source.fullName}"?`).exists()).toBe(
true,
);
});

View File

@ -11,7 +11,7 @@ describe('MemberSource', () => {
propsData: {
memberSource: {
id: 102,
name: 'Foo bar',
fullName: 'Foo bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
...propsData,

View File

@ -7,7 +7,7 @@ export const member = {
accessLevel: { integerValue: 50, stringValue: 'Owner' },
source: {
id: 178,
name: 'Foo Bar',
fullName: 'Foo Bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
user: {

View File

@ -37,6 +37,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
class="flex-grow-1"
data-qa-selector="environments_dropdown"
headertext=""
hideheaderborder="true"
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
size="medium"

View File

@ -0,0 +1,89 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { GlFormSelect } from '@gitlab/ui';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import notesModule from '~/notes/stores/modules';
describe('MultilineCommentForm', () => {
Vue.use(Vuex);
const setSelectedCommentPosition = jest.fn();
const testLine = {
line_code: 'test',
type: 'test',
old_line: 'test',
new_line: 'test',
};
const createWrapper = (props = {}, state) => {
setSelectedCommentPosition.mockReset();
const store = new Vuex.Store({
modules: { notes: notesModule() },
actions: { setSelectedCommentPosition },
});
if (state) store.replaceState({ ...store.state, ...state });
const propsData = {
line: { ...testLine },
commentLineOptions: [{ text: '1' }],
...props,
};
return mount(MultilineCommentForm, { propsData, store });
};
describe('created', () => {
it('sets commentLineStart to line', () => {
const line = { ...testLine };
const wrapper = createWrapper({ line });
expect(wrapper.vm.commentLineStart).toEqual(line);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
it('sets commentLineStart to lineRange', () => {
const lineRange = {
start: { ...testLine },
};
const wrapper = createWrapper({ lineRange });
expect(wrapper.vm.commentLineStart).toEqual(lineRange.start);
expect(setSelectedCommentPosition).toHaveBeenCalled();
});
it('sets commentLineStart to selectedCommentPosition', () => {
const notes = {
selectedCommentPosition: {
start: { ...testLine },
},
};
const wrapper = createWrapper({}, { notes });
expect(wrapper.vm.commentLineStart).toEqual(wrapper.vm.selectedCommentPosition.start);
expect(setSelectedCommentPosition).not.toHaveBeenCalled();
});
});
describe('destroyed', () => {
it('calls setSelectedCommentPosition', () => {
const wrapper = createWrapper();
wrapper.destroy();
// Once during created, once during destroyed
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
});
});
it('handles changing the start line', () => {
const line = { ...testLine };
const wrapper = createWrapper({ line });
const glSelect = wrapper.findComponent(GlFormSelect);
glSelect.vm.$emit('change', { ...testLine });
expect(wrapper.vm.commentLineStart).toEqual(line);
expect(wrapper.emitted('input')).toBeTruthy();
// Once during created, once during updateCommentLineStart
expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2);
});
});

View File

@ -12,6 +12,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<gl-dropdown-stub
category="primary"
headertext=""
hideheaderborder="true"
size="medium"
text="rspec"
variant="default"

View File

@ -1,8 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app_legacy.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import {
counts,
timesChartData,
@ -38,41 +36,17 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBeTruthy();
expect(list.props('counts')).toBe(counts);
});
it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart);
expect(chart.exists()).toBeTruthy();
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(CiCdAnalyticsAreaChart).length).toBe(3);
});
it('displays the pipeline charts', () => {
const chart = wrapper.find(PipelineCharts);
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i);
expect(chart.exists()).toBeTruthy();
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
}
expect(chart.exists()).toBe(true);
expect(chart.props()).toMatchObject({
counts,
lastWeek: lastWeekChartData,
lastMonth: lastMonthChartData,
lastYear: lastYearChartData,
timesChart: timesChartData,
});
});
});

View File

@ -1,11 +1,10 @@
import { merge } from 'lodash';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlTabs, GlTab } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
@ -58,60 +57,62 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList);
describe('pipelines charts', () => {
it('displays the pipeline charts', () => {
const chart = wrapper.find(PipelineCharts);
const analytics = mockPipelineStatistics.data.project.pipelineAnalytics;
expect(list.exists()).toBe(true);
expect(list.props('counts')).toMatchObject({
failed: 1,
success: 23,
total: 34,
successRatio: 95.83333333333334,
});
});
it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart);
const {
totalPipelines: total,
successfulPipelines: success,
failedPipelines: failed,
} = mockPipelineCount.data.project;
expect(chart.exists()).toBe(true);
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i);
expect(chart.exists()).toBe(true);
// TODO: Refactor this to use the mocked data instead of the vm data
// https://gitlab.com/gitlab-org/gitlab/-/issues/292085
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
}
expect(chart.props()).toMatchObject({
counts: {
failed: failed.count,
success: success.count,
total: total.count,
successRatio: (success.count / (success.count + failed.count)) * 100,
},
lastWeek: {
labels: analytics.weekPipelinesLabels,
totals: analytics.weekPipelinesTotals,
success: analytics.weekPipelinesSuccessful,
},
lastMonth: {
labels: analytics.monthPipelinesLabels,
totals: analytics.monthPipelinesTotals,
success: analytics.monthPipelinesSuccessful,
},
lastYear: {
labels: analytics.yearPipelinesLabels,
totals: analytics.yearPipelinesTotals,
success: analytics.yearPipelinesSuccessful,
},
timesChart: {
labels: analytics.pipelineTimesLabels,
values: analytics.pipelineTimesValues,
},
});
});
});
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findGlTabs = () => wrapper.find(GlTabs);
const findAllGlTab = () => wrapper.findAll(GlTab);
const findGlTabAt = (i) => findAllGlTab().at(i);
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
});
it('renders the deployment frequency charts', () => {
it('renders the deployment frequency charts in a tab', () => {
expect(findGlTabs().exists()).toBe(true);
expect(findGlTabAt(0).attributes('title')).toBe('Pipelines');
expect(findGlTabAt(1).attributes('title')).toBe('Deployments');
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
});
});
@ -121,7 +122,8 @@ describe('ProjectsPipelinesChartsApp', () => {
createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: false } });
});
it('does not render the deployment frequency charts', () => {
it('does not render the deployment frequency charts in a tab', () => {
expect(findGlTabs().exists()).toBe(false);
expect(findDeploymentFrequencyCharts().exists()).toBe(false);
});
});

View File

@ -0,0 +1,78 @@
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import {
counts,
timesChartData as timesChart,
areaChartData as lastWeek,
areaChartData as lastMonth,
lastYearChartData as lastYear,
} from '../mock_data';
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(PipelineCharts, {
propsData: {
counts,
timesChart,
lastWeek,
lastMonth,
lastYear,
},
provide: {
projectPath: 'test/project',
shouldRenderDeploymentFrequencyCharts: true,
},
stubs: {
DeploymentFrequencyCharts: true,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBe(true);
expect(list.props('counts')).toBe(counts);
});
it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart);
expect(chart.exists()).toBeTruthy();
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
describe('pipelines charts', () => {
it('displays 3 area charts', () => {
expect(wrapper.findAll(CiCdAnalyticsAreaChart)).toHaveLength(3);
});
describe('displays individual correctly', () => {
it('renders with the correct data', () => {
const charts = wrapper.findAll(CiCdAnalyticsAreaChart);
for (let i = 0; i < charts.length; i += 1) {
const chart = charts.at(i);
expect(chart.exists()).toBeTruthy();
expect(chart.props('chartData')).toBe(wrapper.vm.areaCharts[i].data);
expect(chart.text()).toBe(wrapper.vm.areaCharts[i].title);
}
});
});
});
});

View File

@ -4,6 +4,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-dropdown-stub
category="primary"
headertext=""
hideheaderborder="true"
right="true"
size="medium"
text="Clone"

View File

@ -4,6 +4,7 @@ exports[`SplitButton renders actionItems 1`] = `
<gl-dropdown-stub
category="primary"
headertext=""
hideheaderborder="true"
menu-class=""
size="medium"
split="true"

View File

@ -1,5 +1,6 @@
import { waitForText } from 'helpers/wait_for_text';
import waitForPromises from 'helpers/wait_for_promises';
import { setTestTimeout } from 'helpers/timeout';
import { useOverclockTimers } from 'test_helpers/utils/overclock_timers';
import { createCommitId } from 'test_helpers/factories/commit_id';
import * as ideHelper from './helpers/ide_helper';
@ -12,6 +13,9 @@ describe('WebIDE', () => {
let container;
beforeEach(() => {
// For some reason these tests were timing out in CI.
// We will investigate in https://gitlab.com/gitlab-org/gitlab/-/issues/298714
setTestTimeout(20000);
setFixtures('<div class="webide-container"></div>');
container = document.querySelector('.webide-container');
});

Some files were not shown because too many files have changed in this diff Show More