Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-31 15:09:22 +00:00
parent a8632f5099
commit 532c924885
64 changed files with 932 additions and 395 deletions

View File

@ -29,6 +29,11 @@ export default {
return {};
},
},
visible: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
failureHistoryMessage() {
@ -77,6 +82,8 @@ export default {
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
:visible="visible"
@hidden="$emit('hidden')"
>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>

View File

@ -21,13 +21,7 @@ import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import {
DEFAULT_BLOB_INFO,
TEXT_FILE_TYPE,
LFS_STORAGE,
LEGACY_FILE_TYPES,
CODEOWNERS_FILE_NAME,
} from '../constants';
import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
@ -38,7 +32,6 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
GlButton,
ForkSuggestion,
WebIdeLink,
@ -181,9 +174,6 @@ export default {
currentRef() {
return this.originalBranch || this.ref;
},
isCodeownersFile() {
return this.path.includes(CODEOWNERS_FILE_NAME);
},
viewer() {
const { richViewer, simpleViewer } = this.blobInfo;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
@ -418,12 +408,6 @@ export default {
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
<codeowners-validation
v-if="isCodeownersFile"
:current-ref="currentRef"
:project-path="projectPath"
:file-path="path"
/>
<blob-content
v-if="!blobViewer"
class="js-syntax-highlight"
@ -441,6 +425,8 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
:project-path="projectPath"
:current-ref="currentRef"
class="blob-viewer"
@error="onError"
/>

View File

@ -116,5 +116,3 @@ export const POLLING_INTERVAL_BACKOFF = 2;
export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal';
export const FORK_UPDATED_EVENT = 'fork:updated';
export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';

View File

@ -5,6 +5,7 @@ export default {
import(
'~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'
),
MrTestReportWidget: () => import('~/vue_merge_request_widget/extensions/test_report/index.vue'),
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
@ -18,6 +19,10 @@ export default {
},
computed: {
testReportWidget() {
return this.mr.testResultsPath && 'MrTestReportWidget';
},
terraformPlansWidget() {
return this.mr.terraformReportsPath && 'MrTerraformWidget';
},
@ -27,9 +32,12 @@ export default {
},
widgets() {
return [this.codeQualityWidget, this.terraformPlansWidget, 'MrSecurityWidget'].filter(
(w) => w,
);
return [
this.codeQualityWidget,
this.testReportWidget,
this.terraformPlansWidget,
'MrSecurityWidget',
].filter((w) => w);
},
},
};

View File

@ -1,189 +0,0 @@
import { uniqueId } from 'lodash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { EXTENSION_ICONS } from '../../constants';
import {
summaryTextBuilder,
reportTextBuilder,
reportSubTextBuilder,
countRecentlyFailedTests,
recentFailuresTextBuilder,
formatFilePath,
} from './utils';
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
name: 'WidgetTestSummary',
enablePolling: true,
i18n,
props: ['testResultsPath', 'headBlobPath', 'pipeline'],
modalComponent: TestCaseDetails,
computed: {
failedTestNames() {
if (!this.collapsedData?.suites) {
return '';
}
const newFailures = this.collapsedData?.suites.flatMap((suite) => [suite.new_failures || []]);
const fileNames = newFailures.flatMap((newFailure) => {
return newFailure.map((failure) => {
return failure.file;
});
});
return fileNames.join(' ').trim();
},
summary(data) {
if (data.parsingInProgress) {
return this.$options.i18n.loading;
}
if (data.hasSuiteError) {
return this.$options.i18n.error;
}
return {
subject: summaryTextBuilder(this.$options.i18n.label, data.summary),
meta: recentFailuresTextBuilder(data.summary),
};
},
statusIcon(data) {
if (data.status === TESTS_FAILED_STATUS) {
return EXTENSION_ICONS.warning;
}
if (data.hasSuiteError) {
return EXTENSION_ICONS.failed;
}
return EXTENSION_ICONS.success;
},
tertiaryButtons() {
const actionButtons = [];
if (this.failedTestNames().length > 0) {
actionButtons.push({
dataClipboardText: this.failedTestNames(),
id: uniqueId('copy-to-clipboard'),
icon: 'copy-to-clipboard',
testId: 'copy-failed-specs-btn',
text: this.$options.i18n.copyFailedSpecs,
tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
tooltipOnClick: __('Copied'),
});
}
actionButtons.push({
text: this.$options.i18n.fullReport,
href: `${this.pipeline.path}/test_report`,
target: '_blank',
trackFullReportClicked: true,
testId: 'full-report-link',
});
return actionButtons;
},
},
methods: {
fetchCollapsedData() {
return axios.get(this.testResultsPath).then((response) => {
const { data = {}, status } = response;
const { suites = [], summary = {} } = data;
return {
...response,
data: {
hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
...data,
summary: {
recentlyFailed: countRecentlyFailedTests(suites),
...summary,
},
},
};
});
},
fetchFullData() {
return Promise.resolve(this.prepareReports());
},
suiteIcon(suite) {
if (suite.status === ERROR_STATUS) {
return EXTENSION_ICONS.error;
}
if (suite.status === TESTS_FAILED_STATUS) {
return EXTENSION_ICONS.failed;
}
return EXTENSION_ICONS.success;
},
testHeader(test, sectionHeader, index) {
const headers = [];
if (index === 0) {
headers.push(sectionHeader);
}
if (test.recent_failures?.count && test.recent_failures?.base_branch) {
headers.push(i18n.recentFailureCount(test.recent_failures));
}
return headers;
},
mapTestAsChild({ iconName, sectionHeader }) {
return (test, index) => {
return {
id: uniqueId('test-'),
header: this.testHeader(test, sectionHeader, index),
modal: {
text: test.name,
onClick: () => {
this.modalData = {
testCase: {
filePath: test.file && `${this.headBlobPath}/${formatFilePath(test.file)}`,
...test,
},
};
},
},
icon: { name: iconName },
};
};
},
prepareReports() {
return this.collapsedData.suites
.map((suite) => {
return {
...suite,
summary: {
recentlyFailed: countRecentlyFailedTests(suite),
...suite.summary,
},
};
})
.map((suite) => {
return {
id: uniqueId('suite-'),
text: reportTextBuilder(suite),
subtext: reportSubTextBuilder(suite),
icon: {
name: this.suiteIcon(suite),
},
children: [
...[...suite.new_failures, ...suite.new_errors].map(
this.mapTestAsChild({
sectionHeader: i18n.newHeader,
iconName: EXTENSION_ICONS.failed,
}),
),
...[...suite.existing_failures, ...suite.existing_errors].map(
this.mapTestAsChild({
iconName: EXTENSION_ICONS.failed,
}),
),
...[...suite.resolved_failures, ...suite.resolved_errors].map(
this.mapTestAsChild({
sectionHeader: i18n.fixedHeader,
iconName: EXTENSION_ICONS.success,
}),
),
],
};
});
},
},
};

View File

@ -0,0 +1,313 @@
<script>
import { uniqueId, uniq } from 'lodash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import MrWidgetRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
import {
summaryTextBuilder,
reportTextBuilder,
reportSubTextBuilder,
countRecentlyFailedTests,
recentFailuresTextBuilder,
formatFilePath,
} from './utils';
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
name: 'WidgetTestReport',
components: {
MrWidget,
MrWidgetRow,
DynamicScroller,
DynamicScrollerItem,
TestCaseDetails,
},
i18n,
props: {
mr: {
type: Object,
required: true,
},
},
data() {
return {
collapsedData: {},
suites: [],
modalData: null,
};
},
computed: {
failedTestNames() {
const { data: { suites = [] } = {} } = this.collapsedData;
if (!this.hasSuites) {
return '';
}
const newFailures = suites.flatMap((suite) => [suite.new_failures || []]);
const fileNames = newFailures.flatMap((newFailure) => {
return newFailure.map((failure) => {
return failure.file;
});
});
return uniq(fileNames).join(' ').trim();
},
summary() {
const {
data: { parsingInProgress = false, hasSuiteError = false, summary = {} } = {},
} = this.collapsedData;
if (parsingInProgress) {
return { title: this.$options.i18n.loading };
}
if (hasSuiteError) {
return { title: this.$options.i18n.error };
}
return {
title: summaryTextBuilder(this.$options.i18n.label, summary),
subtitle: recentFailuresTextBuilder(summary),
};
},
statusIcon() {
const { data: { status = null, hasSuiteError = false } = {} } = this.collapsedData;
if (status === TESTS_FAILED_STATUS) {
return EXTENSION_ICONS.warning;
}
if (hasSuiteError) {
return EXTENSION_ICONS.failed;
}
return EXTENSION_ICONS.success;
},
tertiaryButtons() {
const actionButtons = [];
if (this.failedTestNames.length > 0) {
actionButtons.push({
dataClipboardText: this.failedTestNames,
id: uniqueId('copy-to-clipboard'),
icon: 'copy-to-clipboard',
testId: 'copy-failed-specs-btn',
text: this.$options.i18n.copyFailedSpecs,
tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
tooltipOnClick: __('Copied'),
});
}
actionButtons.push({
text: this.$options.i18n.fullReport,
href: `${this.mr.pipeline.path}/test_report`,
target: '_blank',
trackFullReportClicked: true,
testId: 'full-report-link',
});
return actionButtons;
},
testResultsPath() {
return this.mr.testResultsPath;
},
hasSuites() {
return this.suites.length > 0;
},
},
methods: {
fetchCollapsedData() {
return axios.get(this.testResultsPath).then((response) => {
const { data = {}, status } = response;
const { suites = [], summary = {} } = data;
this.collapsedData = {
...response,
data: {
hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
...data,
summary: {
recentlyFailed: countRecentlyFailedTests(suites),
...summary,
},
},
};
this.suites = this.prepareSuites(this.collapsedData);
return response;
});
},
suiteIcon(suite) {
if (suite.status === ERROR_STATUS) {
return EXTENSION_ICONS.error;
}
if (suite.status === TESTS_FAILED_STATUS) {
return EXTENSION_ICONS.failed;
}
return EXTENSION_ICONS.success;
},
testHeader(test, sectionHeader, index) {
const headers = [];
if (index === 0) {
headers.push(sectionHeader);
}
if (test.recent_failures?.count && test.recent_failures?.base_branch) {
headers.push(i18n.recentFailureCount(test.recent_failures));
}
return headers;
},
mapTestAsChild({ iconName, sectionHeader }) {
return (test, index) => {
return {
id: uniqueId('test-'),
header: this.testHeader(test, sectionHeader, index),
text: test.name,
actions: [
{
text: __('View details'),
onClick: () => {
this.modalData = {
testCase: {
filePath: test.file && `${this.mr.headBlobPath}/${formatFilePath(test.file)}`,
...test,
},
};
},
},
],
icon: { name: iconName },
};
};
},
onModalHidden() {
this.modalData = null;
},
prepareSuites(collapsedData) {
const {
data: { suites = [] },
} = collapsedData;
return suites
.map((suite) => {
return {
...suite,
summary: {
recentlyFailed: countRecentlyFailedTests(suite),
...suite.summary,
},
};
})
.map((suite) => {
return {
id: uniqueId('suite-'),
text: reportTextBuilder(suite),
subtext: reportSubTextBuilder(suite),
icon: {
name: this.suiteIcon(suite),
},
children: [
...[...suite.new_failures, ...suite.new_errors].map(
this.mapTestAsChild({
sectionHeader: i18n.newHeader,
iconName: EXTENSION_ICONS.failed,
}),
),
...[...suite.existing_failures, ...suite.existing_errors].map(
this.mapTestAsChild({
iconName: EXTENSION_ICONS.failed,
}),
),
...[...suite.resolved_failures, ...suite.resolved_errors].map(
this.mapTestAsChild({
sectionHeader: i18n.fixedHeader,
iconName: EXTENSION_ICONS.success,
}),
),
],
};
});
},
},
};
</script>
<template>
<div>
<mr-widget
:error-text="$options.i18n.error"
:status-icon-name="statusIcon"
:loading-text="$options.i18n.loading"
:action-buttons="tertiaryButtons"
:help-popover="$options.helpPopover"
:widget-name="$options.name"
:summary="summary"
:fetch-collapsed-data="fetchCollapsedData"
:is-collapsible="hasSuites"
>
<template #content>
<mr-widget-row
v-for="suite in suites"
:key="suite.id"
:level="2"
:status-icon-name="suite.icon.name"
:widget-name="$options.name"
data-testid="extension-list-item"
>
<template #header>
<div class="gl-flex-direction-column">
<div>{{ suite.text }}</div>
<div
v-for="(subtext, i) in suite.subtext"
:key="`${suite.id}-subtext-${i}`"
class="gl-font-sm gl-text-gray-700"
>
{{ subtext }}
</div>
</div>
</template>
<template #body>
<div v-if="suite.children.length > 0" class="gl-mt-2 gl-w-full">
<dynamic-scroller
:items="suite.children"
:min-item-size="32"
:style="{ maxHeight: '170px' }"
key-field="id"
class="gl-pr-5"
>
<template #default="{ item, active }">
<dynamic-scroller-item :item="item" :active="active">
<strong
v-for="(headerText, i) in item.header"
:key="`${item.id}-headerText-${i}`"
class="gl-display-block gl-mt-2"
>
{{ headerText }}
</strong>
<mr-widget-row
:key="item.id"
:level="3"
:widget-name="$options.name"
:status-icon-name="item.icon.name"
:action-buttons="item.actions"
class="gl-mt-2"
>
<template #header>{{ item.text }}</template>
</mr-widget-row>
</dynamic-scroller-item>
</template>
</dynamic-scroller>
</div>
</template>
</mr-widget-row>
</template>
</mr-widget>
<test-case-details
:modal-id="`modal${$options.name}`"
:visible="modalData !== null"
v-bind="modalData"
@hidden="onModalHidden"
/>
</div>
</template>

View File

@ -62,7 +62,7 @@ export const reportSubTextBuilder = ({ suite_errors: suiteErrors, summary }) =>
}
return errors;
}
return recentFailuresTextBuilder(summary);
return [recentFailuresTextBuilder(summary)];
};
export const countRecentlyFailedTests = (subject) => {

View File

@ -56,7 +56,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
import accessibilityExtension from './extensions/accessibility';
import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@ -225,9 +224,6 @@ export default {
this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId,
);
},
shouldRenderTestReport() {
return Boolean(this.mr?.testResultsPath);
},
mergeError() {
let { mergeError } = this.mr;
@ -281,11 +277,6 @@ export default {
this.registerAccessibilityExtension();
}
},
shouldRenderTestReport(newVal) {
if (newVal) {
this.registerTestReportExtension();
}
},
},
mounted() {
MRWidgetService.fetchInitialData()
@ -525,11 +516,6 @@ export default {
registerExtension(accessibilityExtension);
}
},
registerTestReportExtension() {
if (this.shouldRenderTestReport) {
registerExtension(testReportExtension);
}
},
},
};
</script>

View File

@ -33,6 +33,7 @@ export default {
components: {
GlLoadingIcon,
Chunk,
CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
mixins: [Tracking.mixin()],
props: {
@ -40,6 +41,14 @@ export default {
type: Object,
required: true,
},
projectPath: {
type: String,
required: true,
},
currentRef: {
type: String,
required: true,
},
},
data() {
return {
@ -66,7 +75,7 @@ export default {
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
return SVELTE_LANGUAGE;
} else if (this.blob.name === this.$options.codeownersFileName) {
} else if (this.isCodeownersFile) {
// override for codeowners files
return this.$options.codeownersLanguage;
}
@ -87,6 +96,9 @@ export default {
totalChunks() {
return Object.keys(this.chunks).length;
},
isCodeownersFile() {
return this.blob.name === CODEOWNERS_FILE_NAME;
},
},
async created() {
if (this.isLfsBlob) {
@ -238,7 +250,6 @@ export default {
},
userColorScheme: window.gon.user_color_scheme,
currentlySelectedLine: null,
codeownersFileName: CODEOWNERS_FILE_NAME,
codeownersLanguage: CODEOWNERS_LANGUAGE,
};
</script>
@ -250,6 +261,13 @@ export default {
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
<codeowners-validation
v-if="isCodeownersFile"
class="gl-text-black-normal"
:current-ref="currentRef"
:project-path="projectPath"
:file-path="blob.path"
/>
<chunk
v-if="firstChunk"
:lines="firstChunk.lines"
@ -263,20 +281,21 @@ export default {
/>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
<chunk
v-for="(chunk, key, index) in chunks"
v-else
:key="key"
:lines="chunk.lines"
:content="chunk.content"
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:is-highlighted="chunk.isHighlighted"
:chunk-index="index"
:language="chunk.language"
:blame-path="blob.blamePath"
:total-chunks="totalChunks"
@appear="highlightChunk"
/>
<template v-else>
<chunk
v-for="(chunk, key, index) in chunks"
:key="key"
:lines="chunk.lines"
:content="chunk.content"
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:is-highlighted="chunk.isHighlighted"
:chunk-index="index"
:language="chunk.language"
:blame-path="blob.blamePath"
:total-chunks="totalChunks"
@appear="highlightChunk"
/>
</template>
</div>
</template>

View File

@ -1,7 +1,7 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
GlDropdownForm,
GlDropdownDivider,
GlModal,
@ -53,8 +53,8 @@ export default {
emailAddressCopied: __('Email address copied'),
},
components: {
GlDropdown,
GlDropdownItem,
GlDisclosureDropdown,
GlDisclosureDropdownItem,
GlDropdownForm,
GlDropdownDivider,
GlModal,
@ -308,7 +308,7 @@ export default {
<template>
<div>
<gl-dropdown
<gl-disclosure-dropdown
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
@ -322,7 +322,7 @@ export default {
class="work-item-notifications-form"
:data-testid="$options.notificationsToggleFormTestId"
>
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<div class="gl-px-4 gl-pb-2 gl-pt-2">
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
@ -335,50 +335,56 @@ export default {
</gl-dropdown-form>
<gl-dropdown-divider />
</template>
<gl-dropdown-item
<gl-disclosure-dropdown-item
v-if="canPromoteToObjective"
:data-testid="$options.promoteActionTestId"
@click="promoteToObjective"
@action="promoteToObjective"
>
{{ __('Promote to objective') }}
</gl-dropdown-item>
<template #list-item>{{ __('Promote to objective') }}</template>
</gl-disclosure-dropdown-item>
<template v-if="canUpdate && !isParentConfidential">
<gl-dropdown-item
<gl-disclosure-dropdown-item
:data-testid="$options.confidentialityTestId"
@click="handleToggleWorkItemConfidentiality"
>{{
@action="handleToggleWorkItemConfidentiality"
><template #list-item>{{
isConfidential
? $options.i18n.disableTaskConfidentiality
: $options.i18n.enableTaskConfidentiality
}}</gl-dropdown-item
}}</template></gl-disclosure-dropdown-item
>
</template>
<gl-dropdown-item
<gl-disclosure-dropdown-item
ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
@click="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
>{{ $options.i18n.copyReference }}</gl-dropdown-item
@action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
><template #list-item>{{
$options.i18n.copyReference
}}</template></gl-disclosure-dropdown-item
>
<template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
<gl-dropdown-item
<gl-disclosure-dropdown-item
ref="workItemCreateNoteEmail"
:data-testid="$options.copyCreateNoteEmailTestId"
:data-clipboard-text="workItemCreateNoteEmail"
@click="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
>{{ i18n.copyCreateNoteEmail }}</gl-dropdown-item
@action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
><template #list-item>{{
i18n.copyCreateNoteEmail
}}</template></gl-disclosure-dropdown-item
>
<gl-dropdown-divider v-if="canDelete" />
</template>
<gl-dropdown-item
<gl-dropdown-divider v-if="canDelete" />
<gl-disclosure-dropdown-item
v-if="canDelete"
:data-testid="$options.deleteActionTestId"
variant="danger"
@click="handleDelete"
@action="handleDelete"
>
{{ i18n.deleteWorkItem }}
</gl-dropdown-item>
</gl-dropdown>
<template #list-item
><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
>
</gl-disclosure-dropdown-item>
</gl-disclosure-dropdown>
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"

View File

@ -94,7 +94,7 @@ class Groups::LabelsController < Groups::ApplicationController
def label_params
allowed = [:title, :description, :color]
allowed << :lock_on_merge if Feature.enabled?(:enforce_locked_labels_on_merge, @project, type: :ops)
allowed << :lock_on_merge if @group.supports_lock_on_merge?
params.require(:label).permit(allowed)
end

View File

@ -156,7 +156,7 @@ class Projects::LabelsController < Projects::ApplicationController
def label_params
allowed = [:title, :description, :color]
allowed << :lock_on_merge if Feature.enabled?(:enforce_locked_labels_on_merge, @project, type: :ops)
allowed << :lock_on_merge if @project.supports_lock_on_merge?
params.require(:label).permit(allowed)
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Ci
class TriggersFinder
def initialize(current_user, project)
@current_user = current_user
@project = project
end
def execute
return Ci::Trigger.none unless Ability.allowed?(@current_user, :admin_build, @project)
@project.triggers
end
end
end

View File

@ -10,7 +10,8 @@ module Resolvers
type Types::Ci::PipelineTriggerType.connection_type, null: false
def resolve_with_lookahead
apply_lookahead(object.triggers)
triggers = ::Ci::TriggersFinder.new(current_user, object).execute
apply_lookahead(triggers)
end
private

View File

@ -904,6 +904,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:linked_work_items)
end
def supports_lock_on_merge?
feature_flag_enabled_for_self_or_ancestor?(:enforce_locked_labels_on_merge, type: :ops)
end
def usage_quotas_enabled?
::Feature.enabled?(:usage_quotas_for_all_editions, self) && root?
end
@ -945,12 +949,12 @@ class Group < Namespace
private
def feature_flag_enabled_for_self_or_ancestor?(feature_flag)
def feature_flag_enabled_for_self_or_ancestor?(feature_flag, type: :development)
actors = [root_ancestor]
actors << self if root_ancestor != self
actors.any? do |actor|
::Feature.enabled?(feature_flag, actor)
::Feature.enabled?(feature_flag, actor, type: type)
end
end

View File

@ -737,7 +737,7 @@ class MergeRequest < ApplicationRecord
def supports_lock_on_merge?
return false unless merged?
Feature.enabled?(:enforce_locked_labels_on_merge, project, type: :ops)
project.supports_lock_on_merge?
end
# Calls `MergeWorker` to proceed with the merge process and

View File

@ -3255,6 +3255,10 @@ class Project < ApplicationRecord
group.crm_enabled?
end
def supports_lock_on_merge?
group&.supports_lock_on_merge? || ::Feature.enabled?(:enforce_locked_labels_on_merge, self, type: :ops)
end
def path_availability
base, _, host = path.partition('.')

View File

@ -13,9 +13,7 @@ module Labels
project_or_group = target_params[:project] || target_params[:group]
if project_or_group.present?
if Feature.disabled?(:enforce_locked_labels_on_merge, project_or_group, type: :ops)
params.delete(:lock_on_merge)
end
params.delete(:lock_on_merge) unless project_or_group.supports_lock_on_merge?
project_or_group.labels.create(params)
elsif target_params[:template]

View File

@ -21,7 +21,7 @@ module Labels
def allow_lock_on_merge?(label)
return if label.template?
return unless label.respond_to?(:parent_container)
return unless Feature.enabled?(:enforce_locked_labels_on_merge, label.parent_container, type: :ops)
return unless label.parent_container.supports_lock_on_merge?
# If we've made it here, then we're allowed to turn it on. However, we do _not_
# want to allow it to be turned off. So if it's already set, then don't allow the possibility

View File

@ -1,7 +1,7 @@
- add_to_breadcrumbs _("Labels"), group_labels_path(@group)
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
- show_lock_on_merge = Feature.enabled?(:enforce_locked_labels_on_merge, @group, type: :ops)
- show_lock_on_merge = @group.supports_lock_on_merge?
%h1.page-title.gl-font-size-h-display
= _('Edit Label')

View File

@ -1,9 +1,17 @@
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents")
- page_description @issue.description_html
- page_card_attributes @issue.card_attributes
- if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
- add_page_specific_style 'page_bundles/design_management'
- add_page_specific_style 'page_bundles/incidents'
- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/issues_show'
= render 'projects/issuable/show', issuable: @issue
- @content_class = "limit-container-width" unless fluid_layout
= render 'projects/issues/details_content', issuable: @issue

View File

@ -1,10 +0,0 @@
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
- page_description issuable.description_html
- page_card_attributes issuable.card_attributes
- if issuable.relocation_target
- page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
- add_page_specific_style 'page_bundles/issuable'
= render "projects/issues/service_desk/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_content', issuable: issuable, api_awards_path: api_awards_path

View File

@ -1,6 +1,8 @@
- related_branches_path = related_branches_project_issue_path(@project, issuable)
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
= render "projects/issues/service_desk/alert_moved_from_service_desk", issue: issuable
.issue-details.issuable-details.js-issue-details
.detail-page-description.content-block.js-detail-page-description.gl-pt-3.gl-pb-0.gl-border-none
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json,
@ -18,10 +20,10 @@
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
.js-issue-widgets
= render 'shared/issue_type/emoji_block', issuable: issuable, api_awards_path: api_awards_path
= render 'projects/issues/emoji_block', issuable: issuable, api_awards_path: api_awards_path
.js-issue-widgets
= render 'shared/issue_type/sentry_stack_trace', issuable: issuable
= render 'projects/issues/sentry_stack_trace', issuable: issuable
= render 'projects/issues/design_management'

View File

@ -1,10 +1,18 @@
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- page_description @issue.description_html
- page_card_attributes @issue.card_attributes
- if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
- add_page_specific_style 'page_bundles/design_management'
- add_page_specific_style 'page_bundles/incidents'
- add_page_specific_style 'page_bundles/issuable'
- add_page_specific_style 'page_bundles/issues_show'
- add_page_specific_style 'page_bundles/work_items'
= render 'projects/issuable/show', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)
- @content_class = "limit-container-width" unless fluid_layout
= render 'projects/issues/details_content', issuable: @issue, api_awards_path: award_emoji_issue_api_path(@issue)

View File

@ -1,7 +1,7 @@
- add_to_breadcrumbs _("Labels"), project_labels_path(@project)
- breadcrumb_title _("Edit")
- page_title _("Edit"), @label.name, _("Labels")
- show_lock_on_merge = Feature.enabled?(:enforce_locked_labels_on_merge, @project, type: :ops)
- show_lock_on_merge = @project.supports_lock_on_merge?
%h1.page-title.gl-font-size-h-display
= _('Edit Label')

View File

@ -81,7 +81,6 @@
- insider_threat
- instance_resiliency
- integrations
- intel_code_security
- interactive_application_security_testing
- internationalization
- logging
@ -112,7 +111,6 @@
- remote_development
- requirements_management
- review_apps
- runbooks
- runner
- runner_fleet
- runner_saas
@ -142,4 +140,3 @@
- web_ide
- webhooks
- wiki
- workflow_automation

View File

@ -0,0 +1,8 @@
---
name: use_primary_and_secondary_stores_for_action_cable
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126451
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423216
milestone: '16.4'
type: development
group: group::scalability
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: use_primary_store_as_default_for_action_cable
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126451
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423216
milestone: '16.4'
type: development
group: group::scalability
default_enabled: false

View File

@ -16,7 +16,10 @@ ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config|
args = config.except(:adapter, :channel_prefix)
.merge(instrumentation_class: ::Gitlab::Instrumentation::Redis::ActionCable)
::Redis.new(args)
primary_store = ::Redis.new(Gitlab::Redis::Pubsub.params)
secondary_store = ::Redis.new(args)
Gitlab::Redis::MultiStore.new(primary_store, secondary_store, "ActionCable")
end
Gitlab::ActionCable::RequestStoreCallbacks.install

View File

@ -3,7 +3,7 @@ table_name: merge_request_predictions
classes:
- MergeRequest::Predictions
feature_categories:
- workflow_automation
- code_review_workflow
description: Includes machine learning model predictions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97622
milestone: '15.4'

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class RemoveNamespaceDetailsDashboardFields < Gitlab::Database::Migration[2.1]
enable_lock_retries!
def change
remove_column :namespace_details, :dashboard_notification_at, :datetime_with_timezone
remove_column :namespace_details, :dashboard_enforcement_at, :datetime_with_timezone
end
end

View File

@ -0,0 +1 @@
a4aec1b330059217fc5f93be7c3e4a6369b402b4919a10a26de4ba8352dee9a7

View File

@ -18974,9 +18974,7 @@ CREATE TABLE namespace_details (
updated_at timestamp with time zone,
cached_markdown_version integer,
description text,
description_html text,
dashboard_notification_at timestamp with time zone,
dashboard_enforcement_at timestamp with time zone
description_html text
);
CREATE TABLE namespace_ldap_settings (

View File

@ -58,7 +58,7 @@ When a PAT is revoked from the credentials inventory, the instance notifies the
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/243833) in GitLab 14.8.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. Select **Credentials**.
1. Select the **Project Access Tokens** tab.
@ -72,7 +72,7 @@ The project access token is revoked and a background worker is queued to delete
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225248) in GitLab 13.5.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. Select **Credentials**.
1. Select the **SSH Keys** tab.

View File

@ -164,6 +164,24 @@ You can also use personal, project, or group access tokens with OAuth-compliant
curl --header "Authorization: Bearer <your_access_token>" "https://gitlab.example.com/api/v4/projects"
```
### Job tokens
You can use job tokens to authenticate with [specific API endpoints](../../ci/jobs/ci_job_token.md)
by passing the token in the `job_token` parameter or the `JOB-TOKEN` header.
To pass the token in GitLab CI/CD jobs, use the `CI_JOB_TOKEN` variable.
Example of using the job token in a parameter:
```shell
curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/42/artifacts?job_token=$CI_JOB_TOKEN"
```
Example of using the job token in a header:
```shell
curl --header "JOB-TOKEN:$CI_JOB_TOKEN" "https://gitlab.example.com/api/v4/projects/1/releases"
```
### Session cookie
Signing in to the main GitLab application sets a `_gitlab_session` cookie. The

View File

@ -13,6 +13,7 @@ module Gitlab
Gitlab::Redis::DbLoadBalancing,
Gitlab::Redis::FeatureFlag,
Gitlab::Redis::Queues,
Gitlab::Redis::Pubsub,
Gitlab::Redis::RateLimiting,
Gitlab::Redis::RepositoryCache,
Gitlab::Redis::Sessions,

View File

@ -19,8 +19,12 @@ module Gitlab
end
class MethodMissingError < StandardError
def initialize(cmd)
@cmd = cmd
end
def message
'Method missing. Falling back to execute method on the redis default store in Rails.env.production.'
"Method missing #{@cmd}. Falling back to execute method on the redis default store in Rails.env.production."
end
end
@ -38,6 +42,17 @@ module Gitlab
SKIP_LOG_METHOD_MISSING_FOR_COMMANDS = %i[info].freeze
# _client and without_reconnect are Redis::Client methods which may be called through multistore
REDIS_CLIENT_COMMANDS = %i[
_client
without_reconnect
].freeze
PUBSUB_SUBSCRIBE_COMMANDS = %i[
subscribe
unsubscribe
].freeze
READ_COMMANDS = %i[
exists
exists?
@ -71,6 +86,7 @@ module Gitlab
incr
incrby
mapped_hmset
publish
rpush
sadd
sadd?
@ -126,7 +142,7 @@ module Gitlab
end
# rubocop:disable GitlabSecurity/PublicSend
READ_COMMANDS.each do |name|
(READ_COMMANDS + REDIS_CLIENT_COMMANDS + PUBSUB_SUBSCRIBE_COMMANDS).each do |name|
define_method(name) do |*args, **kwargs, &block|
if use_primary_and_secondary_stores?
read_command(name, *args, **kwargs, &block)
@ -246,9 +262,9 @@ module Gitlab
def log_method_missing(command_name, *_args)
return if SKIP_LOG_METHOD_MISSING_FOR_COMMANDS.include?(command_name)
raise MethodMissingError if Rails.env.test? || Rails.env.development?
raise MethodMissingError, command_name if Rails.env.test? || Rails.env.development?
log_error(MethodMissingError.new, command_name)
log_error(MethodMissingError.new(command_name), command_name)
increment_method_missing_count(command_name)
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
module Redis
class Pubsub < ::Gitlab::Redis::Wrapper
class << self
def config_fallback
SharedState
end
end
end
end
end

View File

@ -14675,6 +14675,9 @@ msgstr ""
msgid "DORA4Metrics|Merge request throughput"
msgstr ""
msgid "DORA4Metrics|Metrics comparison for %{name}"
msgstr ""
msgid "DORA4Metrics|Metrics comparison for %{name} group"
msgstr ""

View File

@ -136,7 +136,7 @@
"deckar01-task_list": "^2.3.1",
"dexie": "^3.2.3",
"diff": "^3.4.0",
"dompurify": "^2.4.5",
"dompurify": "^3.0.5",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^10.0.0",
@ -210,7 +210,7 @@
"visibilityjs": "^1.2.4",
"vue": "2.7.14",
"vue-apollo": "^3.0.7",
"vue-loader": "15.10.1",
"vue-loader": "15.10.2",
"vue-observe-visibility": "^1.0.0",
"vue-resize": "^1.0.1",
"vue-router": "3.6.5",
@ -223,7 +223,7 @@
"web-streams-polyfill": "^3.2.1",
"web-vitals": "^0.2.4",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.9.0",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^4.10.0",
"webpack-stats-plugin": "^0.3.1",
"worker-loader": "^2.0.0",
@ -290,7 +290,7 @@
"timezone-mock": "^1.0.8",
"vite": "^4.4.9",
"vite-plugin-ruby": "^3.2.2",
"vite-svg-loader": "^3.4.0",
"vite-svg-loader": "^3.6.0",
"vue-loader-vue3": "npm:vue-loader@17",
"vue-test-utils-compat": "0.0.14",
"vuex-mock-store": "^0.1.0",

View File

@ -138,6 +138,10 @@ module QA
element :close_button
end
view 'app/assets/javascripts/jobs/components/table/cells/job_cell.vue' do
element 'fork-icon'
end
def start_review
click_element(:start_review_button)
@ -476,6 +480,10 @@ module QA
def mr_widget_text
find_element(:mr_widget_content).text
end
def has_fork_icon?
has_element?('fork-icon', skip_finished_loading_check: true)
end
end
end
end

View File

@ -149,6 +149,14 @@ module QA
end
end
end
def has_no_skipped_job_in_group?
within_element(:jobs_dropdown_menu) do
all_elements(:job_item_container, minimum: 1).all? do
has_no_selector?('.ci-status-icon-skipped')
end
end
end
end
end
end

View File

@ -27,7 +27,9 @@ module QA
Flow::Login.sign_in_unless_signed_in(user: fork.user)
Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform(&:create_merge_request)
Support::WaitForRequests.wait_for_requests
Support::Waiter.wait_until(message: 'Waiting for fork icon to appear') do
Page::MergeRequest::Show.perform(&:has_fork_icon?)
end
mr_url = current_url
# Sign back in as original user

View File

@ -31,6 +31,7 @@ module QA
incident
framework
delete_issue_button
skipped_job_in_group
].each do |predicate|
RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs|
match do |page_object|

View File

@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Groups::LabelsController, feature_category: :team_planning do
let_it_be(:group) { create(:group) }
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: group) }
@ -142,14 +143,30 @@ RSpec.describe Groups::LabelsController, feature_category: :team_planning do
end
end
context 'when feature flag is enabled' do
it 'allows setting lock_on_merge' do
shared_examples 'allows setting lock_on_merge' do
it do
update_request
expect(response).to redirect_to(group_labels_path)
expect(label.reload.lock_on_merge).to be_truthy
end
end
context 'when feature flag for group is enabled' do
before do
stub_feature_flags(enforce_locked_labels_on_merge: group)
end
it_behaves_like 'allows setting lock_on_merge'
end
context 'when feature flag for ancestor group is enabled' do
before do
stub_feature_flags(enforce_locked_labels_on_merge: root_group)
end
it_behaves_like 'allows setting lock_on_merge'
end
end
end
end

View File

@ -317,14 +317,30 @@ RSpec.describe Projects::LabelsController, feature_category: :team_planning do
end
end
context 'when feature flag is enabled' do
it 'allows setting lock_on_merge' do
shared_examples 'allows setting lock_on_merge' do
it do
update_request
expect(response).to redirect_to(namespace_project_labels_path)
expect(label.reload.lock_on_merge).to be_truthy
end
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(enforce_locked_labels_on_merge: project)
end
it_behaves_like 'allows setting lock_on_merge'
end
context 'when feature flag for ancestor group is enabled' do
before do
stub_feature_flags(enforce_locked_labels_on_merge: group)
end
it_behaves_like 'allows setting lock_on_merge'
end
end
end

View File

@ -718,14 +718,14 @@ RSpec.describe 'New/edit issue', :js, feature_category: :team_planning do
end
describe 'when user has made changes' do
it 'shows a warning and can leave page', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410497' do
it 'shows a warning and can leave page' do
content = 'new issue content'
find('body').send_keys('e')
fill_in 'issue-description', with: content
click_link 'Boards'
page.driver.browser.switch_to.alert.accept
click_link 'Boards' do
page.driver.browser.switch_to.alert.accept
end
expect(page).not_to have_content(content)
end

View File

@ -646,7 +646,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'addTest'
click_button 'View details'
end
end
@ -693,7 +693,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'Test#sum when a is 1 and b is 3 returns summary'
click_button 'View details'
end
end
@ -741,7 +741,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'addTest'
click_button 'View details'
end
end
@ -788,7 +788,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'addTest'
click_button 'View details'
end
end
@ -834,7 +834,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'Test#sum when a is 4 and b is 4 returns summary'
click_button 'View details'
end
end
@ -881,7 +881,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category:
click_expand_button
within('[data-testid="widget-extension-collapsed-section"]') do
click_link 'addTest'
click_button 'View details'
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::TriggersFinder, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:trigger) { create(:ci_trigger, project: project) }
subject { described_class.new(current_user, project).execute }
describe "#execute" do
context 'when the current user is authorized' do
before_all do
project.add_owner(current_user)
end
it 'returns list of trigger tokens' do
expect(subject).to contain_exactly(trigger)
end
end
context 'when the current user is not authorized' do
it 'does not return trigger tokens' do
expect(subject).to be_blank
end
end
end
end

View File

@ -1,13 +1,11 @@
import { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
import testReportExtension from '~/vue_merge_request_widget/extensions/test_report/index.vue';
import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
@ -34,12 +32,10 @@ describe('Test report extension', () => {
let wrapper;
let mock;
registerExtension(testReportExtension);
const endpoint = '/root/repo/-/merge_requests/4/test_reports.json';
const mockApi = (statusCode, data = mixedResultsTestReports) => {
mock.onGet(endpoint).reply(statusCode, data);
mock.onGet(endpoint).reply(statusCode, data, {});
};
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
@ -49,7 +45,7 @@ describe('Test report extension', () => {
const findModal = () => wrapper.findComponent(TestCaseDetails);
const createComponent = () => {
wrapper = mountExtended(extensionsContainer, {
wrapper = mountExtended(testReportExtension, {
propsData: {
mr: {
testResultsPath: endpoint,
@ -84,7 +80,7 @@ describe('Test report extension', () => {
expect(wrapper.text()).toContain(i18n.loading);
});
it('with a 204 response, continues to display loading state', async () => {
it('with a "no content" response, continues to display loading state', async () => {
mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
@ -269,7 +265,7 @@ describe('Test report extension', () => {
beforeEach(async () => {
await createExpandedWidgetWithData();
wrapper.findByTestId('modal-link').trigger('click');
wrapper.findByTestId('extension-actions-button').trigger('click');
});
it('opens a modal to display test case details', () => {

View File

@ -5,6 +5,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import {
@ -55,11 +56,13 @@ describe('Source Viewer component', () => {
const fileType = 'javascript';
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
const currentRef = 'main';
const projectPath = 'test/project';
const createComponent = async (blob = {}) => {
wrapper = shallowMountExtended(SourceViewer, {
router,
propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob }, currentRef, projectPath },
});
await waitForPromises();
};
@ -269,4 +272,18 @@ describe('Source Viewer component', () => {
expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
});
describe('Codeowners validation', () => {
const findCodeownersValidation = () => wrapper.findComponent(CodeownersValidation);
it('does not render codeowners validation when file is not CODEOWNERS', async () => {
await createComponent();
expect(findCodeownersValidation().exists()).toBe(false);
});
it('renders codeowners validation when file is CODEOWNERS', async () => {
await createComponent({ name: CODEOWNERS_FILE_NAME });
expect(findCodeownersValidation().exists()).toBe(true);
});
});
});

View File

@ -1,4 +1,4 @@
import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
import { GlDisclosureDropdown, GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@ -55,7 +55,7 @@ describe('WorkItemActions component', () => {
const findCopyReferenceButton = () => wrapper.findByTestId(TEST_ID_COPY_REFERENCE_ACTION);
const findCopyCreateNoteEmailButton = () =>
wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
const findDropdownItems = () => wrapper.findAll('[data-testid="disclosure-content"] > *');
const findDropdownItemsActual = () =>
findDropdownItems().wrappers.map((x) => {
if (x.is(GlDropdownDivider)) {
@ -143,6 +143,7 @@ describe('WorkItemActions component', () => {
show: modalShowSpy,
},
}),
GlDisclosureDropdown,
},
});
};
@ -208,7 +209,7 @@ describe('WorkItemActions component', () => {
it('emits `toggleWorkItemConfidentiality` event when clicked', () => {
createComponent();
findConfidentialityToggleButton().vm.$emit('click');
findConfidentialityToggleButton().vm.$emit('action');
expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]);
});
@ -228,7 +229,7 @@ describe('WorkItemActions component', () => {
it('shows confirm modal when clicked', () => {
createComponent();
findDeleteButton().vm.$emit('click');
findDeleteButton().vm.$emit('action');
expect(modalShowSpy).toHaveBeenCalled();
});
@ -359,7 +360,7 @@ describe('WorkItemActions component', () => {
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
findPromoteButton().vm.$emit('click');
findPromoteButton().vm.$emit('action');
await waitForPromises();
@ -378,7 +379,7 @@ describe('WorkItemActions component', () => {
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
findPromoteButton().vm.$emit('click');
findPromoteButton().vm.$emit('action');
await waitForPromises();
@ -394,7 +395,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyReferenceButton().exists()).toBe(true);
findCopyReferenceButton().vm.$emit('click');
findCopyReferenceButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Reference copied');
});
@ -416,7 +417,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyCreateNoteEmailButton().exists()).toBe(true);
findCopyCreateNoteEmailButton().vm.$emit('click');
findCopyCreateNoteEmailButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Email address copied');
});

View File

@ -27,7 +27,8 @@ RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
sub = ActionCable.server.pubsub.send(:redis_connection)
expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
expect(sub.is_a?(::Gitlab::Redis::MultiStore)).to eq(true)
expect(sub.secondary_store.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
expect(ActionCable.server.config.cable[:id]).to be_nil
end
end

View File

@ -1130,4 +1130,104 @@ RSpec.describe Gitlab::Redis::MultiStore, feature_category: :redis do
end
end
end
# NOTE: for pub/sub, unit tests are favoured over integration tests to avoid long polling
# with threads which could lead to flaky specs. The multiplexing behaviour are verified in
# 'with WRITE redis commands' and 'with READ redis commands' contexts.
context 'with pub/sub commands' do
let(:channel_name) { 'chanA' }
let(:message) { "msg" }
shared_examples 'publishes to stores' do
it 'publishes to one or more stores' do
expect(stores).to all(receive(:publish))
multi_store.publish(channel_name, message)
end
end
shared_examples 'subscribes and unsubscribes' do
it 'subscribes to the default store' do
expect(default_store).to receive(:subscribe)
expect(non_default_store).not_to receive(:subscribe)
multi_store.subscribe(channel_name)
end
it 'unsubscribes to the default store' do
expect(default_store).to receive(:unsubscribe)
expect(non_default_store).not_to receive(:unsubscribe)
multi_store.unsubscribe
end
end
context 'when using both stores' do
before do
stub_feature_flags(use_primary_and_secondary_stores_for_test_store: true)
end
it_behaves_like 'publishes to stores' do
let(:stores) { [primary_store, secondary_store] }
end
context 'with primary store set as default' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: true)
end
it_behaves_like 'subscribes and unsubscribes' do
let(:default_store) { primary_store }
let(:non_default_store) { secondary_store }
end
end
context 'with secondary store set as default' do
before do
stub_feature_flags(use_primary_store_as_default_for_test_store: false)
end
it_behaves_like 'subscribes and unsubscribes' do
let(:default_store) { secondary_store }
let(:non_default_store) { primary_store }
end
end
end
context 'when only using the primary store' do
before do
stub_feature_flags(
use_primary_and_secondary_stores_for_test_store: false,
use_primary_store_as_default_for_test_store: true
)
end
it_behaves_like 'subscribes and unsubscribes' do
let(:default_store) { primary_store }
let(:non_default_store) { secondary_store }
end
it_behaves_like 'publishes to stores' do
let(:stores) { [primary_store] }
end
end
context 'when only using the secondary store' do
before do
stub_feature_flags(
use_primary_and_secondary_stores_for_test_store: false,
use_primary_store_as_default_for_test_store: false
)
end
it_behaves_like 'subscribes and unsubscribes' do
let(:default_store) { secondary_store }
let(:non_default_store) { primary_store }
end
it_behaves_like 'publishes to stores' do
let(:stores) { [secondary_store] }
end
end
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Redis::Pubsub, feature_category: :redis do
include_examples "redis_new_instance_shared_examples", 'pubsub', Gitlab::Redis::SharedState
include_examples "redis_shared_examples"
end

View File

@ -3309,6 +3309,13 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
describe '#supports_lock_on_merge?' do
it_behaves_like 'checks self and root ancestor feature flag' do
let(:feature_flag) { :enforce_locked_labels_on_merge }
let(:feature_flag_method) { :supports_lock_on_merge? }
end
end
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }

View File

@ -9164,6 +9164,13 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
describe '#supports_lock_on_merge?' do
it_behaves_like 'checks self (project) and root ancestor feature flag' do
let(:feature_flag) { :enforce_locked_labels_on_merge }
let(:feature_flag_method) { :supports_lock_on_merge? }
end
end
private
def finish_job(export_job)

View File

@ -56,15 +56,21 @@ RSpec.describe 'getting project information', feature_category: :groups_and_proj
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(baseline)
end
context 'when other project member is not authorized to see the full token' do
context 'when another project member or owner who is not also the token owner' do
before do
project.add_maintainer(other_user)
project.add_owner(other_user)
post_graphql(query, current_user: other_user)
end
it 'shows truncated token' do
expect(graphql_data_at(:project, :pipeline_triggers,
:nodes).first['token']).to eql pipeline_trigger.token[0, 4]
it 'is not authorized and shows truncated token' do
expect(graphql_data_at(:project, :pipeline_triggers, :nodes).first).to match({
'id' => pipeline_trigger.to_global_id.to_s,
'canAccessProject' => true,
'description' => pipeline_trigger.description,
'hasTokenExposed' => false,
'lastUsed' => nil,
'token' => pipeline_trigger.token[0, 4]
})
end
end

View File

@ -35,9 +35,11 @@ module Capybara
end
module WaitForAllRequestsAfterClickLink
def click_link(locator = nil, **options)
def click_link(locator = nil, **options, &block)
super
yield if block
wait_for_all_requests
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name|
RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name, migration_name|
context 'when filter not provided (all behavior)' do
let(:filters) { {} }
@ -42,4 +42,19 @@ RSpec.shared_examples 'search results filtered by archived' do |feature_flag_nam
end
end
end
if migration_name.present?
context "when the #{migration_name} is not completed" do
let(:filters) { {} }
before do
set_elasticsearch_migration_to(migration_name.to_s, including: false)
end
it 'returns archived and unarchived results' do
expect(results.objects(scope)).to include unarchived_result
expect(results.objects(scope)).to include archived_result
end
end
end
end

View File

@ -3,7 +3,6 @@
RSpec.shared_examples 'checks self and root ancestor feature flag' do
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:project) { create(:project, group: group) }
subject { group.public_send(feature_flag_method) }
@ -41,3 +40,47 @@ RSpec.shared_examples 'checks self and root ancestor feature flag' do
it { is_expected.to be_truthy }
end
end
RSpec.shared_examples 'checks self (project) and root ancestor feature flag' do
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:project) { create(:project, group: group) }
subject { project.public_send(feature_flag_method) }
context 'when FF is enabled for the root group' do
before do
stub_feature_flags(feature_flag => root_group)
end
it { is_expected.to be_truthy }
end
context 'when FF is enabled for the group' do
before do
stub_feature_flags(feature_flag => group)
end
it { is_expected.to be_truthy }
end
context 'when FF is enabled for the project' do
before do
stub_feature_flags(feature_flag => project)
end
it { is_expected.to be_truthy }
end
context 'when FF is disabled globally' do
before do
stub_feature_flags(feature_flag => false)
end
it { is_expected.to be_falsey }
end
context 'when FF is enabled globally' do
it { is_expected.to be_truthy }
end
end

View File

@ -1829,10 +1829,10 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@polka/url@^1.0.0-next.9":
version "1.0.0-next.12"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.12.tgz#431ec342a7195622f86688bbda82e3166ce8cb28"
integrity sha512-6RglhutqrGFMO1MNUXp95RBuYIuc8wTnMAV5MUhLmjTOy78ncwOw7RgeQ/HeymkKXRhZd0s2DNrM1rL7unk3MQ==
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==
"@popperjs/core@^2.11.2", "@popperjs/core@^2.9.0":
version "2.11.5"
@ -5636,11 +5636,6 @@ dommatrix@^1.0.3:
resolved "https://registry.yarnpkg.com/dommatrix/-/dommatrix-1.0.3.tgz#e7c18e8d6f3abdd1fef3dd4aa74c4d2e620a0525"
integrity sha512-l32Xp/TLgWb8ReqbVJAFIvXmY7go4nTxxlWiAFyhoQw9RKEOHBZNnyGvJWqDVSPmq3Y9HlM4npqF/T6VMOXhww==
dompurify@^2.4.5:
version "2.4.5"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87"
integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==
dompurify@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.5.tgz#eb3d9cfa10037b6e73f32c586682c4b2ab01fbed"
@ -8862,6 +8857,11 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
integrity sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==
lodash.find@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
@ -8947,6 +8947,11 @@ lodash.pick@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
lodash.pullall@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.pullall/-/lodash.pullall-4.2.0.tgz#9d98b8518b7c965b0fae4099bd9fb7df8bbf38ba"
integrity sha512-VhqxBKH0ZxPpLhiu68YD1KnHmbhQJQctcipvmFnqIBDYzcIHzf3Zpu0tpeOKtR4x76p9yohc506eGdOjTmyIBg==
lodash.snakecase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
@ -9759,11 +9764,6 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.3.1:
version "2.4.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@ -10014,6 +10014,11 @@ mri@^1.1.0:
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
mrmime@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27"
integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -11991,14 +11996,14 @@ simple-update-notifier@^1.0.7:
dependencies:
semver "~7.0.0"
sirv@^1.0.7:
version "1.0.11"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-1.0.11.tgz#81c19a29202048507d6ec0d8ba8910fda52eb5a4"
integrity sha512-SR36i3/LSWja7AJNRBz4fF/Xjpn7lQFI30tZ434dIy+bitLYSP+ZEenHg36i23V2SGEz+kqjksg0uOGZ5LPiqg==
sirv@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446"
integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==
dependencies:
"@polka/url" "^1.0.0-next.9"
mime "^2.3.1"
totalist "^1.0.0"
"@polka/url" "^1.0.0-next.20"
mrmime "^1.0.0"
totalist "^3.0.0"
sisteransi@^1.0.4:
version "1.0.5"
@ -12785,10 +12790,10 @@ toidentifier@1.0.1:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
totalist@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==
totalist@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
touch@^3.1.0:
version "3.1.0"
@ -13368,7 +13373,7 @@ vite-plugin-ruby@^3.2.2:
debug "^4.3.4"
fast-glob "^3.2.12"
vite-svg-loader@^3.4.0:
vite-svg-loader@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.6.0.tgz#71d246cba5e808c7f183a2a56a9dde6856bb0c92"
integrity sha512-bZJffcgCREW57kNkgMhuNqeDznWXyQwJ3wKrRhHLMMzwDnP5jr3vXW3cqsmquRR7VTP5mLdKj1/zzPPooGUuPw==
@ -13448,10 +13453,10 @@ vue-hot-reload-api@^2.3.0:
hash-sum "^2.0.0"
loader-utils "^2.0.0"
vue-loader@15.10.1:
version "15.10.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.10.1.tgz#c451c4cd05a911aae7b5dbbbc09fb913fb3cca18"
integrity sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==
vue-loader@15.10.2:
version "15.10.2"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.10.2.tgz#6dccfda8661caa7f5415806a5e386fd3258d8112"
integrity sha512-ndeSe/8KQc/nlA7TJ+OBhv2qalmj1s+uBs7yHDRFaAXscFTApBzY9F1jES3bautmgWjDlDct0fw8rPuySDLwxw==
dependencies:
"@vue/component-compiler-utils" "^3.1.0"
hash-sum "^1.0.2"
@ -13647,20 +13652,27 @@ webidl-conversions@^7.0.0:
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
webpack-bundle-analyzer@^4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.0.tgz#fc093c4ab174fd3dcbd1c30b763f56d10141209d"
integrity sha512-+bXGmO1LyiNx0i9enBu3H8mv42sj/BJWhZNFwjz92tVnBa9J3JMGo2an2IXlEleoDOPn/Hofl5hr/xCpObUDtw==
webpack-bundle-analyzer@^4.9.1:
version "4.9.1"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.9.1.tgz#d00bbf3f17500c10985084f22f1a2bf45cb2f09d"
integrity sha512-jnd6EoYrf9yMxCyYDPj8eutJvtjQNp8PHmni/e/ulydHBWhT5J3menXt3HEkScsu9YqMAcG4CfFjs3rj5pVU1w==
dependencies:
"@discoveryjs/json-ext" "0.5.7"
acorn "^8.0.4"
acorn-walk "^8.0.0"
chalk "^4.1.0"
commander "^7.2.0"
escape-string-regexp "^4.0.0"
gzip-size "^6.0.0"
lodash "^4.17.20"
is-plain-object "^5.0.0"
lodash.debounce "^4.0.8"
lodash.escape "^4.0.1"
lodash.flatten "^4.4.0"
lodash.invokemap "^4.6.0"
lodash.pullall "^4.2.0"
lodash.uniqby "^4.7.0"
opener "^1.5.2"
sirv "^1.0.7"
picocolors "^1.0.0"
sirv "^2.0.3"
ws "^7.3.1"
webpack-cli@^4.10.0: