Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-14 15:07:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-14 15:07:59 +0300
commitfd27e4f95b3ea8668e1e67f5a88dfb061909c893 (patch)
tree439bcc5417be41cd2290485292fe512f6cea8110
parent6aa920eeb4ef61ea7e7c77b5e10595507874b927 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_KAS_VERSION2
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js2
-rw-r--r--app/assets/javascripts/integrations/constants.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue29
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue12
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue15
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/jira_issues.vue33
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/jira_trigger.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js82
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js55
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue16
-rw-r--r--app/controllers/concerns/issuable_actions.rb4
-rw-r--r--app/controllers/groups/application_controller.rb4
-rw-r--r--app/controllers/groups/crm/contacts_controller.rb1
-rw-r--r--app/controllers/groups/crm/organizations_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_controller.rb16
-rw-r--r--app/graphql/mutations/ci/pipeline/retry.rb5
-rw-r--r--app/helpers/groups/crm_settings_helper.rb2
-rw-r--r--app/models/customer_relations/contact.rb18
-rw-r--r--app/models/customer_relations/issue_contact.rb8
-rw-r--r--app/models/customer_relations/organization.rb9
-rw-r--r--app/models/integrations/jira.rb25
-rw-r--r--app/policies/issue_policy.rb2
-rw-r--r--app/serializers/issue_sidebar_basic_entity.rb5
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/ci/retry_pipeline_service.rb17
-rw-r--r--app/services/issues/set_crm_contacts_service.rb2
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/import/shared/_errors.html.haml14
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--config/feature_flags/development/ci_destroy_all_expired_service.yml2
-rw-r--r--config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml2
-rw-r--r--config/feature_flags/development/vulnerability_reads_table.yml8
-rw-r--r--doc/user/crm/index.md7
-rw-r--r--jest.config.base.js2
-rw-r--r--lib/api/ci/pipelines.rb8
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb4
-rw-r--r--lib/sidebars/groups/menus/customer_relations_menu.rb2
-rw-r--r--locale/gitlab.pot22
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb27
-rw-r--r--spec/factories/customer_relations/issue_customer_relations_contacts.rb2
-rw-r--r--spec/features/groups/navbar_spec.rb12
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js23
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_issues_spec.js34
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js34
-rw-r--r--spec/frontend/pipelines/header_component_spec.js25
-rw-r--r--spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js149
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js4
-rw-r--r--spec/helpers/groups/crm_settings_helper_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/customer_relations/contact_spec.rb55
-rw-r--r--spec/models/customer_relations/issue_contact_spec.rb30
-rw-r--r--spec/models/customer_relations/organization_spec.rb17
-rw-r--r--spec/models/group_spec.rb7
-rw-r--r--spec/models/integrations/jira_spec.rb26
-rw-r--r--spec/models/issue_spec.rb1
-rw-r--r--spec/policies/issue_policy_spec.rb32
-rw-r--r--spec/requests/api/ci/pipelines_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb14
-rw-r--r--spec/requests/groups/crm/contacts_controller_spec.rb6
-rw-r--r--spec/requests/groups/crm/organizations_controller_spec.rb6
-rw-r--r--spec/serializers/issue_sidebar_basic_entity_spec.rb28
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb46
-rw-r--r--spec/services/issues/set_crm_contacts_service_spec.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb2
-rw-r--r--spec/views/shared/issuable/_sidebar.html.haml_spec.rb31
-rw-r--r--yarn.lock36
73 files changed, 1059 insertions, 177 deletions
diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION
index 6ebe0c0b057..44a5e718d50 100644
--- a/GITLAB_KAS_VERSION
+++ b/GITLAB_KAS_VERSION
@@ -1 +1 @@
-14.8.1
+14.9.0
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 9dc17fcd570..204ac07d401 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,5 +1,5 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
-import * as lowlight from 'lowlight';
+import { lowlight } from 'lowlight/lib/all';
const extractLanguage = (element) => element.getAttribute('lang');
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index edc355fdc8d..c5ed5bb08a9 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -28,8 +28,12 @@ export const overridesTabTitle = s__('Integrations|Projects using custom setting
export const integrationFormSections = {
CONNECTION: 'connection',
+ JIRA_TRIGGER: 'jira_trigger',
+ JIRA_ISSUES: 'jira_issues',
};
export const integrationFormSectionComponents = {
[integrationFormSections.CONNECTION]: 'IntegrationSectionConnection',
+ [integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger',
+ [integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues',
};
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 872b8d0b2b7..6e89872ff68 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -39,6 +39,14 @@ export default {
import(
/* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
),
+ IntegrationSectionJiraIssues: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionJiraIssues' */ '~/integrations/edit/components/sections/jira_issues.vue'
+ ),
+ IntegrationSectionJiraTrigger: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
+ ),
GlButton,
GlForm,
},
@@ -47,6 +55,11 @@ export default {
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
+ provide() {
+ return {
+ hasSections: this.hasSections,
+ };
+ },
inject: {
helpHtml: {
default: '',
@@ -208,9 +221,9 @@ export default {
<template v-if="hasSections">
<div
- v-for="section in customState.sections"
+ v-for="(section, index) in customState.sections"
:key="section.type"
- class="gl-border-b gl-mb-5"
+ :class="{ 'gl-border-b gl-pb-3 gl-mb-6': index !== customState.sections.length - 1 }"
data-testid="integration-section"
>
<div class="row">
@@ -225,6 +238,7 @@ export default {
:fields="fieldsForSection(section)"
:is-validated="isValidated"
@toggle-integration-active="onToggleIntegrationState"
+ @request-jira-issue-types="onRequestJiraIssueTypes"
/>
</div>
</div>
@@ -244,13 +258,13 @@ export default {
@toggle-integration-active="onToggleIntegrationState"
/>
<jira-trigger-fields
- v-if="isJira"
+ v-if="isJira && !hasSections"
:key="`${currentKey}-jira-trigger-fields`"
v-bind="propsSource.triggerFieldsProps"
:is-validated="isValidated"
/>
<trigger-fields
- v-else-if="propsSource.triggerEvents.length"
+ v-else-if="propsSource.triggerEvents.length && !hasSections"
:key="`${currentKey}-trigger-fields`"
:events="propsSource.triggerEvents"
:type="propsSource.type"
@@ -262,15 +276,18 @@ export default {
:is-validated="isValidated"
/>
<jira-issues-fields
- v-if="isJira && !isInstanceOrGroupLevel"
+ v-if="isJira && !isInstanceOrGroupLevel && !hasSections"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
:is-validated="isValidated"
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
+ </div>
+ </div>
+ <div v-if="isEditable" class="row">
+ <div :class="hasSections ? 'col' : 'col-lg-8 offset-lg-4'">
<div
- v-if="isEditable"
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
>
<div>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 198cabfad81..7cf8e11f162 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -16,6 +16,11 @@ export default {
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
+ inject: {
+ hasSections: {
+ default: false,
+ },
+ },
props: {
showJiraIssuesIntegration: {
type: Boolean,
@@ -101,9 +106,12 @@ export default {
<template>
<div>
- <gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings">
+ <gl-form-group
+ :label="hasSections ? null : $options.i18n.sectionTitle"
+ label-for="jira-issue-settings"
+ >
<div id="jira-issue-settings">
- <p>
+ <p v-if="!hasSections">
{{ $options.i18n.sectionDescription }}
</p>
<template v-if="showJiraIssuesIntegration">
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index bb8d630fb0e..3c06660e7c5 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -62,6 +62,11 @@ export default {
GlLink,
GlSprintf,
},
+ inject: {
+ hasSections: {
+ default: false,
+ },
+ },
props: {
initialTriggerCommit: {
type: Boolean,
@@ -134,12 +139,14 @@ export default {
<template>
<div>
<gl-form-group
- :label="__('Trigger')"
+ :label="hasSections ? null : __('Trigger')"
label-for="service[trigger]"
:description="
- s__(
- 'JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.',
- )
+ hasSections
+ ? null
+ : s__(
+ 'JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.',
+ )
"
>
<input name="service[commit_events]" type="hidden" :value="triggerCommit || false" />
diff --git a/app/assets/javascripts/integrations/edit/components/sections/jira_issues.vue b/app/assets/javascripts/integrations/edit/components/sections/jira_issues.vue
new file mode 100644
index 00000000000..75202209d38
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/sections/jira_issues.vue
@@ -0,0 +1,33 @@
+<script>
+import { mapGetters } from 'vuex';
+
+import JiraIssuesFields from '../jira_issues_fields.vue';
+
+export default {
+ name: 'IntegrationSectionJiraIssues',
+ components: {
+ JiraIssuesFields,
+ },
+ props: {
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['currentKey', 'propsSource']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <jira-issues-fields
+ :key="`${currentKey}-jira-issues-fields`"
+ v-bind="propsSource.jiraIssuesProps"
+ :is-validated="isValidated"
+ @request-jira-issue-types="$emit('request-jira-issue-types')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/sections/jira_trigger.vue b/app/assets/javascripts/integrations/edit/components/sections/jira_trigger.vue
new file mode 100644
index 00000000000..f36d3b1fbda
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/sections/jira_trigger.vue
@@ -0,0 +1,32 @@
+<script>
+import { mapGetters } from 'vuex';
+
+import JiraTriggerFields from '../jira_trigger_fields.vue';
+
+export default {
+ name: 'IntegrationSectionJiraTrigger',
+ components: {
+ JiraTriggerFields,
+ },
+ props: {
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['currentKey', 'propsSource']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <jira-trigger-fields
+ :key="`${currentKey}-jira-trigger-fields`"
+ v-bind="propsSource.triggerFieldsProps"
+ :is-validated="isValidated"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 6a4d1bb44f2..ac97c9d2743 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -174,6 +174,8 @@ export default {
});
if (errors.length > 0) {
+ this.isRetrying = false;
+
this.reportFailure(POST_FAILURE);
} else {
await this.$apollo.queries.pipeline.refetch();
@@ -182,6 +184,8 @@ export default {
}
}
} catch {
+ this.isRetrying = false;
+
this.reportFailure(POST_FAILURE);
}
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 684386883c8..e5a6514624d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -86,7 +86,7 @@ export default {
);
},
statusIconName() {
- if (this.hasFetchError) return EXTENSION_ICONS.error;
+ if (this.hasFetchError) return EXTENSION_ICONS.failed;
if (this.isLoadingSummary) return null;
return this.statusIcon(this.collapsedData);
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
new file mode 100644
index 00000000000..cd5cfb6837c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
@@ -0,0 +1,39 @@
+import { __, n__, s__, sprintf } from '~/locale';
+
+const digitText = (bold = false) => (bold ? '%{strong_start}%d%{strong_end}' : '%d');
+const noText = (bold = false) => (bold ? '%{strong_start}no%{strong_end}' : 'no');
+
+export const TESTS_FAILED_STATUS = 'failed';
+export const ERROR_STATUS = 'error';
+
+export const i18n = {
+ label: s__('Reports|Test summary'),
+ loading: s__('Reports|Test summary results are loading'),
+ error: s__('Reports|Test summary failed to load results'),
+ fullReport: s__('Reports|Full report'),
+
+ noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`),
+ resultsString: (combinedString, resolvedString) =>
+ sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
+ combinedString,
+ resolvedString,
+ }),
+
+ summaryText: (name, resultsString) =>
+ sprintf(__('%{name}: %{resultsString}'), { name, resultsString }),
+
+ failedClause: (failed, bold) =>
+ n__(`${digitText(bold)} failed`, `${digitText(bold)} failed`, failed),
+ erroredClause: (errored, bold) =>
+ n__(`${digitText(bold)} error`, `${digitText(bold)} errors`, errored),
+ resolvedClause: (resolved, bold) =>
+ n__(`${digitText(bold)} fixed test result`, `${digitText(bold)} fixed test results`, resolved),
+ totalClause: (total, bold) =>
+ n__(`${digitText(bold)} total test`, `${digitText(bold)} total tests`, total),
+
+ reportError: s__('Reports|An error occurred while loading report'),
+ reportErrorWithName: (name) =>
+ sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }),
+ headReportParsingError: s__('Reports|Head report parsing error:'),
+ baseReportParsingError: s__('Reports|Base report parsing error:'),
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
new file mode 100644
index 00000000000..65d9257903f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
@@ -0,0 +1,82 @@
+import { uniqueId } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { EXTENSION_ICONS } from '../../constants';
+import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils';
+import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
+
+export default {
+ name: 'WidgetTestSummary',
+ enablePolling: true,
+ i18n,
+ expandEvent: 'i_testing_summary_widget_total',
+ props: ['testResultsPath', 'headBlobPath', 'pipeline'],
+ computed: {
+ summary(data) {
+ if (data.parsingInProgress) {
+ return this.$options.i18n.loading;
+ }
+ if (data.hasSuiteError) {
+ return this.$options.i18n.error;
+ }
+ return summaryTextBuilder(this.$options.i18n.label, data.summary);
+ },
+ statusIcon(data) {
+ if (data.parsingInProgress) {
+ return null;
+ }
+ if (data.status === TESTS_FAILED_STATUS) {
+ return EXTENSION_ICONS.warning;
+ }
+ if (data.hasSuiteError) {
+ return EXTENSION_ICONS.failed;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ tertiaryButtons() {
+ return [
+ {
+ text: this.$options.i18n.fullReport,
+ href: `${this.pipeline.path}/test_report`,
+ target: '_blank',
+ },
+ ];
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.testResultsPath).then(({ data = {}, status }) => {
+ return {
+ data: {
+ hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
+ parsingInProgress: status === 204,
+ ...data,
+ },
+ };
+ });
+ },
+ 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;
+ },
+ prepareReports() {
+ return this.collapsedData.suites.map((suite) => {
+ return {
+ id: uniqueId('suite-'),
+ text: reportTextBuilder(suite),
+ subtext: reportSubTextBuilder(suite),
+ icon: {
+ name: this.suiteIcon(suite),
+ },
+ };
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
new file mode 100644
index 00000000000..a74ed20362f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -0,0 +1,55 @@
+import { i18n } from './constants';
+
+const textBuilder = (results, boldNumbers = false) => {
+ const { failed, errored, resolved, total } = results;
+
+ const failedOrErrored = (failed || 0) + (errored || 0);
+ const failedString = failed ? i18n.failedClause(failed, boldNumbers) : null;
+ const erroredString = errored ? i18n.erroredClause(errored, boldNumbers) : null;
+ const combinedString =
+ failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
+ const resolvedString = resolved ? i18n.resolvedClause(resolved, boldNumbers) : null;
+ const totalString = total ? i18n.totalClause(total, boldNumbers) : null;
+
+ let resultsString = i18n.noChanges(boldNumbers);
+
+ if (failedOrErrored) {
+ if (resolved) {
+ resultsString = i18n.resultsString(combinedString, resolvedString);
+ } else {
+ resultsString = combinedString;
+ }
+ } else if (resolved) {
+ resultsString = resolvedString;
+ }
+
+ return `${resultsString}, ${totalString}`;
+};
+
+export const summaryTextBuilder = (name = '', results = {}) => {
+ const resultsString = textBuilder(results, true);
+ return i18n.summaryText(name, resultsString);
+};
+
+export const reportTextBuilder = ({ name = '', summary = {}, status }) => {
+ if (!name) {
+ return i18n.reportError;
+ }
+ if (status === 'error') {
+ return i18n.reportErrorWithName(name);
+ }
+
+ const resultsString = textBuilder(summary);
+ return i18n.summaryText(name, resultsString);
+};
+
+export const reportSubTextBuilder = ({ suite_errors }) => {
+ const errors = [];
+ if (suite_errors?.head) {
+ errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
+ }
+ if (suite_errors?.base) {
+ errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
+ }
+ return errors.join('<br />');
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index cd4d9398899..bb25fa15626 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -46,6 +46,7 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
+import testReportExtension from './extensions/test_report';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -190,6 +191,9 @@ export default {
shouldRenderTerraformPlans() {
return Boolean(this.mr?.terraformReportsPath);
},
+ shouldRenderTestReport() {
+ return Boolean(this.mr?.testResultsPath);
+ },
mergeError() {
let { mergeError } = this.mr;
@@ -246,6 +250,11 @@ export default {
this.registerAccessibilityExtension();
}
},
+ shouldRenderTestReport(newVal) {
+ if (newVal) {
+ this.registerTestReportExtension();
+ }
+ },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -491,6 +500,11 @@ export default {
registerExtension(accessibilityExtension);
}
},
+ registerTestReportExtension() {
+ if (this.shouldRenderTestReport && this.shouldShowExtension) {
+ registerExtension(testReportExtension);
+ }
+ },
},
};
</script>
@@ -563,7 +577,7 @@ export default {
/>
<grouped-test-reports-app
- v-if="mr.testResultsPath"
+ v-if="mr.testResultsPath && !shouldShowExtension"
class="js-reports-container"
:endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 5d4e93363fa..ae90bd59d01 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -17,10 +17,6 @@ module IssuableActions
def show
respond_to do |format|
format.html do
- @show_crm_contacts = issuable.is_a?(Issue) && # rubocop:disable Gitlab/ModuleWithInstanceVariables
- can?(current_user, :read_crm_contact, issuable.project.group) &&
- CustomerRelations::Contact.exists_for_group?(issuable.project.group)
-
@issuable_sidebar = serializer.represent(issuable, serializer: 'sidebar') # rubocop:disable Gitlab/ModuleWithInstanceVariables
render 'show'
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index f9c875b80b2..bf72ade32d0 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -82,6 +82,10 @@ class Groups::ApplicationController < ApplicationController
def has_project_list?
false
end
+
+ def validate_root_group!
+ render_404 unless group.root?
+ end
end
Groups::ApplicationController.prepend_mod_with('Groups::ApplicationController')
diff --git a/app/controllers/groups/crm/contacts_controller.rb b/app/controllers/groups/crm/contacts_controller.rb
index f00f4d1df25..b59e20d9cea 100644
--- a/app/controllers/groups/crm/contacts_controller.rb
+++ b/app/controllers/groups/crm/contacts_controller.rb
@@ -3,6 +3,7 @@
class Groups::Crm::ContactsController < Groups::ApplicationController
feature_category :team_planning
+ before_action :validate_root_group!
before_action :authorize_read_crm_contact!
def new
diff --git a/app/controllers/groups/crm/organizations_controller.rb b/app/controllers/groups/crm/organizations_controller.rb
index ab720f490be..f8536b4f538 100644
--- a/app/controllers/groups/crm/organizations_controller.rb
+++ b/app/controllers/groups/crm/organizations_controller.rb
@@ -3,6 +3,7 @@
class Groups::Crm::OrganizationsController < Groups::ApplicationController
feature_category :team_planning
+ before_action :validate_root_group!
before_action :authorize_read_crm_organization!
def new
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index b6a9f01c9c8..8279bb20769 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -161,14 +161,20 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def retry
- ::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
+ # Check for access before execution to allow for async execution while still returning access results
+ access_response = ::Ci::RetryPipelineService.new(@project, current_user).check_access(pipeline)
+
+ if access_response.error?
+ response = { json: { errors: [access_response.message] }, status: access_response.http_status }
+ else
+ response = { json: {}, status: :no_content }
+ ::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
+ end
respond_to do |format|
- format.html do
- redirect_back_or_default default: project_pipelines_path(project)
+ format.json do
+ render response
end
-
- format.json { head :no_content }
end
end
diff --git a/app/graphql/mutations/ci/pipeline/retry.rb b/app/graphql/mutations/ci/pipeline/retry.rb
index ee93f99703e..895397a96ab 100644
--- a/app/graphql/mutations/ci/pipeline/retry.rb
+++ b/app/graphql/mutations/ci/pipeline/retry.rb
@@ -17,10 +17,11 @@ module Mutations
pipeline = authorized_find!(id: id)
project = pipeline.project
- ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
+ service_response = ::Ci::RetryPipelineService.new(project, current_user).execute(pipeline)
+
{
pipeline: pipeline,
- errors: errors_on_object(pipeline)
+ errors: errors_on_object(pipeline) + service_response.errors
}
end
end
diff --git a/app/helpers/groups/crm_settings_helper.rb b/app/helpers/groups/crm_settings_helper.rb
index ab47ec40b13..d7ca25a9d1b 100644
--- a/app/helpers/groups/crm_settings_helper.rb
+++ b/app/helpers/groups/crm_settings_helper.rb
@@ -2,7 +2,7 @@
module Groups
module CrmSettingsHelper
- def crm_feature_flag_enabled?(group)
+ def crm_feature_available?(group)
Feature.enabled?(:customer_relations, group)
end
end
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index a981351f4a0..4fa2c3fb8cf 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -23,8 +23,9 @@ class CustomerRelations::Contact < ApplicationRecord
validates :last_name, presence: true, length: { maximum: 255 }
validates :email, length: { maximum: 255 }
validates :description, length: { maximum: 1024 }
+ validates :email, uniqueness: { scope: :group_id }
validate :validate_email_format
- validate :unique_email_for_group_hierarchy
+ validate :validate_root_group
def self.reference_prefix
'[contact:'
@@ -41,14 +42,13 @@ class CustomerRelations::Contact < ApplicationRecord
def self.find_ids_by_emails(group, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
- where(group_id: group.self_and_ancestor_ids, email: emails)
- .pluck(:id)
+ where(group: group, email: emails).pluck(:id)
end
def self.exists_for_group?(group)
return false unless group
- exists?(group_id: group.self_and_ancestor_ids)
+ exists?(group: group)
end
private
@@ -59,13 +59,9 @@ class CustomerRelations::Contact < ApplicationRecord
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
end
- def unique_email_for_group_hierarchy
- return unless group
- return unless email
+ def validate_root_group
+ return if group&.root?
- duplicate_email_exists = CustomerRelations::Contact
- .where(group_id: group.self_and_hierarchy.pluck(:id), email: email)
- .where.not(id: id).exists?
- self.errors.add(:email, _('contact with same email already exists in group hierarchy')) if duplicate_email_exists
+ self.errors.add(:base, _('contacts can only be added to root groups'))
end
end
diff --git a/app/models/customer_relations/issue_contact.rb b/app/models/customer_relations/issue_contact.rb
index 3e9d1e97c8c..dc7a3fd87bc 100644
--- a/app/models/customer_relations/issue_contact.rb
+++ b/app/models/customer_relations/issue_contact.rb
@@ -6,7 +6,7 @@ class CustomerRelations::IssueContact < ApplicationRecord
belongs_to :issue, optional: false, inverse_of: :customer_relations_contacts
belongs_to :contact, optional: false, inverse_of: :issue_contacts
- validate :contact_belongs_to_issue_group_or_ancestor
+ validate :contact_belongs_to_root_group
def self.find_contact_ids_by_emails(issue_id, emails)
raise ArgumentError, "Cannot lookup more than #{MAX_PLUCK} emails" if emails.length > MAX_PLUCK
@@ -24,11 +24,11 @@ class CustomerRelations::IssueContact < ApplicationRecord
private
- def contact_belongs_to_issue_group_or_ancestor
+ def contact_belongs_to_root_group
return unless contact&.group_id
return unless issue&.project&.namespace_id
- return if issue.project.group&.self_and_ancestor_ids&.include?(contact.group_id)
+ return if issue.project.root_ancestor&.id == contact.group_id
- errors.add(:base, _('The contact does not belong to the issue group or an ancestor'))
+ errors.add(:base, _("The contact does not belong to the issue group's root ancestor"))
end
end
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index c206d1e05f5..a23b9d8fe28 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -19,9 +19,18 @@ class CustomerRelations::Organization < ApplicationRecord
validates :name, uniqueness: { case_sensitive: false, scope: [:group_id] }
validates :name, length: { maximum: 255 }
validates :description, length: { maximum: 1024 }
+ validate :validate_root_group
def self.find_by_name(group_id, name)
where(group: group_id)
.where('LOWER(name) = LOWER(?)', name)
end
+
+ private
+
+ def validate_root_group
+ return if group&.root?
+
+ self.errors.add(:base, _('organizations can only be added to root groups'))
+ end
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index d8883be6820..74ece57000f 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -15,6 +15,9 @@ module Integrations
ATLASSIAN_REFERRER_GITLAB_COM = { atlOrigin: 'eyJpIjoiY2QyZTJiZDRkNGZhNGZlMWI3NzRkNTBmZmVlNzNiZTkiLCJwIjoianN3LWdpdGxhYi1pbnQifQ' }.freeze
ATLASSIAN_REFERRER_SELF_MANAGED = { atlOrigin: 'eyJpIjoiYjM0MTA4MzUyYTYxNDVkY2IwMzVjOGQ3ZWQ3NzMwM2QiLCJwIjoianN3LWdpdGxhYlNNLWludCJ9' }.freeze
+ SECTION_TYPE_JIRA_TRIGGER = 'jira_trigger'
+ SECTION_TYPE_JIRA_ISSUES = 'jira_issues'
+
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
@@ -157,13 +160,31 @@ module Integrations
end
def sections
- [
+ jira_issues_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('integration/jira/issues.html') }
+
+ sections = [
{
type: SECTION_TYPE_CONNECTION,
title: s_('Integrations|Connection details'),
description: help
+ },
+ {
+ type: SECTION_TYPE_JIRA_TRIGGER,
+ title: _('Trigger'),
+ description: s_('JiraService|When a Jira issue is mentioned in a commit or merge request, a remote link and comment (if enabled) will be created.')
}
- ].freeze
+ ]
+
+ # Jira issues is currently only configurable on the project level.
+ if project_level?
+ sections.push({
+ type: SECTION_TYPE_JIRA_ISSUES,
+ title: _('Issues'),
+ description: s_('JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}') % { jira_issues_link_start: jira_issues_link_start, link_end: '</a>'.html_safe }
+ })
+ end
+
+ sections
end
def web_url(path = nil, **params)
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index c9c13b29643..a667c843bc6 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -13,7 +13,7 @@ class IssuePolicy < IssuablePolicy
end
desc "User can read contacts belonging to the issue group"
- condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.group) }
+ condition(:can_read_crm_contacts, scope: :subject) { @user.can?(:read_crm_contact, @subject.project.root_ancestor) }
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
diff --git a/app/serializers/issue_sidebar_basic_entity.rb b/app/serializers/issue_sidebar_basic_entity.rb
index 9c6601afd5e..7222b5df425 100644
--- a/app/serializers/issue_sidebar_basic_entity.rb
+++ b/app/serializers/issue_sidebar_basic_entity.rb
@@ -10,6 +10,11 @@ class IssueSidebarBasicEntity < IssuableSidebarBasicEntity
can?(current_user, :update_escalation_status, issue.project)
end
end
+
+ expose :show_crm_contacts do |issuable|
+ current_user&.can?(:read_crm_contact, issuable.project.root_ancestor) &&
+ CustomerRelations::Contact.exists_for_group?(issuable.project.root_ancestor)
+ end
end
IssueSidebarBasicEntity.prepend_mod_with('IssueSidebarBasicEntity')
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 73c5d0163da..906e5cec4f3 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -65,7 +65,7 @@ module Ci
def check_access!(build)
unless can?(current_user, :update_build, build)
- raise Gitlab::Access::AccessDeniedError
+ raise Gitlab::Access::AccessDeniedError, '403 Forbidden'
end
end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 9ad46ca7585..d40643e1513 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -5,9 +5,8 @@ module Ci
include Gitlab::OptimisticLocking
def execute(pipeline)
- unless can?(current_user, :update_pipeline, pipeline)
- raise Gitlab::Access::AccessDeniedError
- end
+ access_response = check_access(pipeline)
+ return access_response if access_response.error?
pipeline.ensure_scheduling_type!
@@ -30,6 +29,18 @@ module Ci
Ci::ProcessPipelineService
.new(pipeline)
.execute
+
+ ServiceResponse.success
+ rescue Gitlab::Access::AccessDeniedError => e
+ ServiceResponse.error(message: e.message, http_status: :forbidden)
+ end
+
+ def check_access(pipeline)
+ if can?(current_user, :update_pipeline, pipeline)
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: '403 Forbidden', http_status: :forbidden)
+ end
end
private
diff --git a/app/services/issues/set_crm_contacts_service.rb b/app/services/issues/set_crm_contacts_service.rb
index 2edc944435b..5836097f1fd 100644
--- a/app/services/issues/set_crm_contacts_service.rb
+++ b/app/services/issues/set_crm_contacts_service.rb
@@ -52,7 +52,7 @@ module Issues
end
def add_by_email
- contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group, emails(:add_emails))
+ contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group.root_ancestor, emails(:add_emails))
add_by_id(contact_ids)
end
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 85dd218942f..dd62c9e118d 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -42,7 +42,7 @@
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render 'groups/settings/membership', f: f, group: @group
- - if crm_feature_flag_enabled?(@group)
+ - if crm_feature_available?(@group)
%h5= _('Customer relations')
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :crm_enabled,
diff --git a/app/views/import/shared/_errors.html.haml b/app/views/import/shared/_errors.html.haml
index badd8c1278f..3e8a99c541a 100644
--- a/app/views/import/shared/_errors.html.haml
+++ b/app/views/import/shared/_errors.html.haml
@@ -1,8 +1,8 @@
- if @errors.present?
- .gl-alert.gl-alert-danger.gl-mb-5
- .gl-alert-container
- = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content
- .gl-alert-body
- - @errors.each do |error|
- = error
+ = render 'shared/global_alert',
+ variant: :danger,
+ dismissible: false,
+ alert_class: 'gl-mb-5' do
+ .gl-alert-body
+ - @errors.each do |error|
+ = error
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f966958d2c7..72fa979392e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -41,7 +41,7 @@
.block{ class: 'gl-pt-0! gl-collapse-empty', data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- - if @show_crm_contacts
+ - if issuable_sidebar[:show_crm_contacts]
.block.contact
#js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id] } }
diff --git a/config/feature_flags/development/ci_destroy_all_expired_service.yml b/config/feature_flags/development/ci_destroy_all_expired_service.yml
index 34c94529f99..0f36a8d7e30 100644
--- a/config/feature_flags/development/ci_destroy_all_expired_service.yml
+++ b/config/feature_flags/development/ci_destroy_all_expired_service.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348786
milestone: '14.6'
type: development
group: group::pipeline execution
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml b/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml
index 8340b347a96..d48747c3bf5 100644
--- a/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml
+++ b/config/feature_flags/development/ci_detect_wrongly_expired_artifacts.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/354955
milestone: '14.9'
type: development
group: group::pipeline insights
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/vulnerability_reads_table.yml b/config/feature_flags/development/vulnerability_reads_table.yml
new file mode 100644
index 00000000000..68e6ffead14
--- /dev/null
+++ b/config/feature_flags/development/vulnerability_reads_table.yml
@@ -0,0 +1,8 @@
+---
+name: vulnerability_reads_table
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76220
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348151
+milestone: '14.9'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md
index 305cca33dd5..1fb628cf505 100644
--- a/doc/user/crm/index.md
+++ b/doc/user/crm/index.md
@@ -6,15 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Customer relations management (CRM) **(FREE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
-
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `customer_relations`.
On GitLab.com, this feature is not available.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.6 [with a flag](../../administration/feature_flags.md) named `customer_relations`. Disabled by default.
+> - In GitLab 14.8 and later, you can [create contacts and organizations only in root groups](https://gitlab.com/gitlab-org/gitlab/-/issues/350634).
+
With customer relations management (CRM) you can create a record of contacts
(individuals) and organizations (companies) and relate them to issues.
+Contacts and organizations can only be created for root groups.
+
You can use contacts and organizations to tie work to customers for billing and reporting purposes.
To read more about what is planned for the future, see [issue 2256](https://gitlab.com/gitlab-org/gitlab/-/issues/2256).
diff --git a/jest.config.base.js b/jest.config.base.js
index f2a7422303d..0eab5caffb0 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -130,7 +130,7 @@ module.exports = (path, options = {}) => {
'^.+\\.(md|zip|png)$': 'jest-raw-loader',
},
transformIgnorePatterns: [
- 'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister|prosemirror-markdown|dateformat)/)',
+ 'node_modules/(?!(@gitlab/ui|@gitlab/favicon-overlay|bootstrap-vue|three|monaco-editor|monaco-yaml|fast-mersenne-twister|prosemirror-markdown|dateformat|lowlight|fault)/)',
],
timers: 'fake',
testEnvironment: '<rootDir>/spec/frontend/environment.js',
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
index 6627d356108..2d7a437ca08 100644
--- a/lib/api/ci/pipelines.rb
+++ b/lib/api/ci/pipelines.rb
@@ -223,9 +223,13 @@ module API
post ':id/pipelines/:pipeline_id/retry', feature_category: :continuous_integration do
authorize! :update_pipeline, pipeline
- pipeline.retry_failed(current_user)
+ response = pipeline.retry_failed(current_user)
- present pipeline, with: Entities::Ci::Pipeline
+ if response.success?
+ present pipeline, with: Entities::Ci::Pipeline
+ else
+ render_api_error!(response.errors.join(', '), response.http_status)
+ end
end
desc 'Cancel all builds in the pipeline' do
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index b44b47eca37..2f89774a257 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -291,7 +291,7 @@ module Gitlab
types Issue
condition do
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
- CustomerRelations::Contact.exists_for_group?(quick_action_target.project.group)
+ CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
end
execution_message do
_('One or more contacts were successfully added.')
@@ -306,7 +306,7 @@ module Gitlab
types Issue
condition do
current_user.can?(:set_issue_crm_contacts, quick_action_target) &&
- CustomerRelations::Contact.exists_for_group?(quick_action_target.project.group)
+ CustomerRelations::Contact.exists_for_group?(quick_action_target.project.root_ancestor)
end
execution_message do
_('One or more contacts were successfully removed.')
diff --git a/lib/sidebars/groups/menus/customer_relations_menu.rb b/lib/sidebars/groups/menus/customer_relations_menu.rb
index 002197965d1..0aaa6ec45f1 100644
--- a/lib/sidebars/groups/menus/customer_relations_menu.rb
+++ b/lib/sidebars/groups/menus/customer_relations_menu.rb
@@ -24,6 +24,8 @@ module Sidebars
override :render?
def render?
+ return false unless context.group.root?
+
can_read_contact? || can_read_organization?
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ce9c5db802e..cc11367df43 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -847,6 +847,9 @@ msgstr ""
msgid "%{name}, confirm your email address now!"
msgstr ""
+msgid "%{name}: %{resultsString}"
+msgstr ""
+
msgid "%{no_of_days} day"
msgid_plural "%{no_of_days} days"
msgstr[0] ""
@@ -21059,6 +21062,9 @@ msgstr ""
msgid "JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues."
msgstr ""
+msgid "JiraService|Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. %{jira_issues_link_start}Learn more.%{link_end}"
+msgstr ""
+
msgid "JiraService|You must configure Jira before enabling this integration. %{jira_doc_link_start}Learn more.%{link_end}"
msgstr ""
@@ -30941,6 +30947,9 @@ msgstr ""
msgid "Reports|Filename"
msgstr ""
+msgid "Reports|Full report"
+msgstr ""
+
msgid "Reports|Head report parsing error:"
msgstr ""
@@ -30983,9 +30992,15 @@ msgstr ""
msgid "Reports|Test summary failed loading results"
msgstr ""
+msgid "Reports|Test summary failed to load results"
+msgstr ""
+
msgid "Reports|Test summary results are being parsed"
msgstr ""
+msgid "Reports|Test summary results are loading"
+msgstr ""
+
msgid "Reports|Tool"
msgstr ""
@@ -36665,7 +36680,7 @@ msgstr ""
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
-msgid "The contact does not belong to the issue group or an ancestor"
+msgid "The contact does not belong to the issue group's root ancestor"
msgstr ""
msgid "The content editor may change the markdown formatting style of the document, which may not match your original markdown style."
@@ -43453,7 +43468,7 @@ msgstr ""
msgid "compliance violation has already been recorded"
msgstr ""
-msgid "contact with same email already exists in group hierarchy"
+msgid "contacts can only be added to root groups"
msgstr ""
msgid "container registry images"
@@ -44363,6 +44378,9 @@ msgstr ""
msgid "or"
msgstr ""
+msgid "organizations can only be added to root groups"
+msgstr ""
+
msgid "other card matches"
msgstr ""
diff --git a/package.json b/package.json
index 8fbf2cf3d8d..80aeb235b6b 100644
--- a/package.json
+++ b/package.json
@@ -141,7 +141,7 @@
"jszip-utils": "^0.0.2",
"katex": "^0.13.2",
"lodash": "^4.17.20",
- "lowlight": "^1.20.0",
+ "lowlight": "^2.5.0",
"marked": "^0.3.12",
"mathjax": "3",
"mermaid": "^8.13.10",
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
index 99a78f15b1f..f721b3326a0 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin do
+ RSpec.describe 'Manage', :reliable, :requires_admin do
describe 'Gitlab migration' do
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index a74eda1596b..d803f5e473c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :github, :requires_admin do
+ RSpec.describe 'Manage', :reliable, :github, :requires_admin do
describe 'Project import' do
let(:github_repo) { 'gitlab-qa-github/import-test' }
let(:api_client) { Runtime::API::Client.as_admin }
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 75d29acfbc4..8fae82d54a2 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -932,6 +932,33 @@ RSpec.describe Projects::PipelinesController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when access denied' do
+ it 'returns an error' do
+ sign_in(create(:user))
+
+ post_retry
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when service returns an error' do
+ before do
+ service_response = ServiceResponse.error(message: 'some error', http_status: 404)
+ allow_next_instance_of(::Ci::RetryPipelineService) do |service|
+ allow(service).to receive(:check_access).and_return(service_response)
+ end
+ end
+
+ it 'does not retry' do
+ post_retry
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to include('some error')
+ expect(::Ci::RetryPipelineWorker).not_to have_received(:perform_async).with(pipeline.id, user.id)
+ end
+ end
end
describe 'POST cancel.json' do
diff --git a/spec/factories/customer_relations/issue_customer_relations_contacts.rb b/spec/factories/customer_relations/issue_customer_relations_contacts.rb
index 6a4fecfb3cf..8ea1a521a33 100644
--- a/spec/factories/customer_relations/issue_customer_relations_contacts.rb
+++ b/spec/factories/customer_relations/issue_customer_relations_contacts.rb
@@ -21,7 +21,7 @@ FactoryBot.define do
trait :for_issue do
issue { raise ArgumentError, '`issue` is manadatory' }
- contact { association(:contact, group: issue.project.group) }
+ contact { association(:contact, group: issue.project.root_ancestor) }
end
end
end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 42b53d941c2..e4b44d65438 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -60,6 +60,18 @@ RSpec.describe 'Group navbar' do
it_behaves_like 'verified navigation bar'
end
+ context 'when customer_relations feature and flag is enabled but subgroup' do
+ let(:group) { create(:group, :crm_enabled, parent: create(:group)) }
+
+ before do
+ stub_feature_flags(customer_relations: true)
+
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
+
context 'when dependency proxy is available' do
before do
stub_config(dependency_proxy: { enabled: true })
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 51b3a9752a7..c4569070d09 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -435,6 +435,29 @@ describe('IntegrationForm', () => {
});
},
);
+
+ describe('when IntegrationSectionConnection emits `request-jira-issue-types` event', () => {
+ beforeEach(() => {
+ jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
+
+ createComponent({
+ provide: {
+ glFeatures: { integrationFormSections: true },
+ },
+ customStateProps: {
+ sections: [mockSectionConnection],
+ testPath: '/test',
+ },
+ mountFn: mountExtended,
+ });
+
+ findConnectionSectionComponent().vm.$emit('request-jira-issue-types');
+ });
+
+ it('dispatches `requestJiraIssueTypes` action', () => {
+ expect(dispatch).toHaveBeenCalledWith('requestJiraIssueTypes', expect.any(FormData));
+ });
+ });
});
describe('ActiveCheckbox', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
new file mode 100644
index 00000000000..a7c1cc2a03f
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionJiraIssue from '~/integrations/edit/components/sections/jira_issues.vue';
+import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+import { createStore } from '~/integrations/edit/store';
+
+import { mockIntegrationProps } from '../../mock_data';
+
+describe('IntegrationSectionJiraIssue', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps },
+ });
+ wrapper = shallowMount(IntegrationSectionJiraIssue, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
+
+ describe('template', () => {
+ it('renders JiraIssuesFields', () => {
+ createComponent();
+
+ expect(findJiraIssuesFields().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
new file mode 100644
index 00000000000..d4ab9864fab
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionJiraTrigger from '~/integrations/edit/components/sections/jira_trigger.vue';
+import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
+import { createStore } from '~/integrations/edit/store';
+
+import { mockIntegrationProps } from '../../mock_data';
+
+describe('IntegrationSectionJiraTrigger', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ const store = createStore({
+ customState: { ...mockIntegrationProps },
+ });
+ wrapper = shallowMount(IntegrationSectionJiraTrigger, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
+
+ describe('template', () => {
+ it('renders JiraTriggerFields', () => {
+ createComponent();
+
+ expect(findJiraTriggerFields().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 1d89f949564..c4639bd8e16 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,5 +1,7 @@
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import HeaderComponent from '~/pipelines/components/header_component.vue';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
@@ -17,6 +19,7 @@ import {
describe('Pipeline details header', () => {
let wrapper;
let glModalDirective;
+ let mutate = jest.fn();
const findDeleteModal = () => wrapper.find(GlModal);
const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]');
@@ -44,7 +47,7 @@ describe('Pipeline details header', () => {
startPolling: jest.fn(),
},
},
- mutate: jest.fn(),
+ mutate,
};
return shallowMount(HeaderComponent, {
@@ -120,6 +123,26 @@ describe('Pipeline details header', () => {
});
});
+ describe('Retry action failed', () => {
+ beforeEach(() => {
+ mutate = jest.fn().mockRejectedValue('error');
+
+ wrapper = createComponent(mockCancelledPipelineHeader);
+ });
+
+ it('retry button loading state should reset on error', async () => {
+ findRetryButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findRetryButton().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findRetryButton().props('loading')).toBe(false);
+ });
+ });
+
describe('Cancel action', () => {
beforeEach(() => {
wrapper = createComponent(mockRunningPipelineHeader);
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
new file mode 100644
index 00000000000..472dbc104ce
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
@@ -0,0 +1,149 @@
+import { GlButton } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
+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 httpStatusCodes from '~/lib/utils/http_status';
+
+import { failedReport } from '../../../reports/mock_data/mock_data';
+import mixedResultsTestReports from '../../../reports/mock_data/new_and_fixed_failures_report.json';
+import newErrorsTestReports from '../../../reports/mock_data/new_errors_report.json';
+import newFailedTestReports from '../../../reports/mock_data/new_failures_report.json';
+import successTestReports from '../../../reports/mock_data/no_failures_report.json';
+import resolvedFailures from '../../../reports/mock_data/resolved_failures.json';
+
+const reportWithParsingErrors = failedReport;
+reportWithParsingErrors.suites[0].suite_errors = {
+ head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
+ base: 'JUnit data parsing failed: string not matched',
+};
+
+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);
+ };
+
+ const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
+ const findTertiaryButton = () => wrapper.find(GlButton);
+ const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+
+ const createComponent = () => {
+ wrapper = mountExtended(extensionsContainer, {
+ propsData: {
+ mr: {
+ testResultsPath: endpoint,
+ headBlobPath: 'head/blob/path',
+ pipeline: { path: 'pipeline/path' },
+ },
+ },
+ });
+ };
+
+ const createExpandedWidgetWithData = async (data = mixedResultsTestReports) => {
+ mockApi(httpStatusCodes.OK, data);
+ createComponent();
+ await waitForPromises();
+ findToggleCollapsedButton().trigger('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('summary', () => {
+ it('displays loading text', () => {
+ mockApi(httpStatusCodes.OK);
+ createComponent();
+
+ expect(wrapper.text()).toContain(i18n.loading);
+ });
+
+ it('displays failed loading text', async () => {
+ mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(i18n.error);
+ });
+
+ it.each`
+ description | mockData | expectedResult
+ ${'mixed test results'} | ${mixedResultsTestReports} | ${'Test summary: 2 failed and 2 fixed test results, 11 total tests'}
+ ${'unchanged test results'} | ${successTestReports} | ${'Test summary: no changed test results, 11 total tests'}
+ ${'tests with errors'} | ${newErrorsTestReports} | ${'Test summary: 2 errors, 11 total tests'}
+ ${'failed test results'} | ${newFailedTestReports} | ${'Test summary: 2 failed, 11 total tests'}
+ ${'resolved failures'} | ${resolvedFailures} | ${'Test summary: 4 fixed test results, 11 total tests'}
+ `('displays summary text for $description', async ({ mockData, expectedResult }) => {
+ mockApi(httpStatusCodes.OK, mockData);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(expectedResult);
+ });
+
+ it('displays a link to the full report', async () => {
+ mockApi(httpStatusCodes.OK);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTertiaryButton().text()).toBe('Full report');
+ expect(findTertiaryButton().attributes('href')).toBe('pipeline/path/test_report');
+ });
+
+ it('shows an error when a suite has a parsing error', async () => {
+ mockApi(httpStatusCodes.OK, reportWithParsingErrors);
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(i18n.error);
+ });
+ });
+
+ describe('expanded data', () => {
+ it('displays summary for each suite', async () => {
+ await createExpandedWidgetWithData();
+
+ expect(trimText(findAllExtensionListItems().at(0).text())).toBe(
+ 'rspec:pg: 1 failed and 2 fixed test results, 8 total tests',
+ );
+ expect(trimText(findAllExtensionListItems().at(1).text())).toBe(
+ 'java ant: 1 failed, 3 total tests',
+ );
+ });
+
+ it('displays suite parsing errors', async () => {
+ await createExpandedWidgetWithData(reportWithParsingErrors);
+
+ const suiteText = trimText(findAllExtensionListItems().at(0).text());
+
+ expect(suiteText).toContain(
+ 'Head report parsing error: JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
+ );
+ expect(suiteText).toContain(
+ 'Base report parsing error: JUnit data parsing failed: string not matched',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 0540107ea5f..9a21389dbb4 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1025,7 +1025,7 @@ describe('MrWidgetOptions', () => {
it('captures sentry error and displays error when poll has failed', () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
});
});
});
@@ -1036,7 +1036,7 @@ describe('MrWidgetOptions', () => {
const itHandlesTheException = () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
};
beforeEach(() => {
diff --git a/spec/helpers/groups/crm_settings_helper_spec.rb b/spec/helpers/groups/crm_settings_helper_spec.rb
index 6376cabda3a..87690e7debc 100644
--- a/spec/helpers/groups/crm_settings_helper_spec.rb
+++ b/spec/helpers/groups/crm_settings_helper_spec.rb
@@ -3,23 +3,45 @@
require 'spec_helper'
RSpec.describe Groups::CrmSettingsHelper do
- let_it_be(:group) { create(:group) }
+ let_it_be(:root_group) { create(:group) }
- describe '#crm_feature_flag_enabled?' do
+ describe '#crm_feature_available?' do
subject do
- helper.crm_feature_flag_enabled?(group)
+ helper.crm_feature_available?(group)
end
- context 'when feature flag is enabled' do
- it { is_expected.to be_truthy }
+ context 'in root group' do
+ let(:group) { root_group }
+
+ context 'when feature flag is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(customer_relations: false)
+ context 'in subgroup' do
+ let_it_be(:subgroup) { create(:group, parent: root_group) }
+
+ let(:group) { subgroup }
+
+ context 'when feature flag is enabled' do
+ it { is_expected.to be_truthy }
end
- it { is_expected.to be_falsy }
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(customer_relations: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e3d468c1b5d..f760b396de8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -610,6 +610,7 @@ project:
- sync_events
- secure_files
- security_trainings
+- vulnerability_reads
award_emoji:
- awardable
- user
diff --git a/spec/models/customer_relations/contact_spec.rb b/spec/models/customer_relations/contact_spec.rb
index c7b0f1bd3d4..18896962261 100644
--- a/spec/models/customer_relations/contact_spec.rb
+++ b/spec/models/customer_relations/contact_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe CustomerRelations::Contact, type: :model do
+ let_it_be(:group) { create(:group) }
+
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:organization).optional }
@@ -23,6 +25,8 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it { is_expected.to validate_length_of(:email).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
+ it { is_expected.to validate_uniqueness_of(:email).scoped_to(:group_id) }
+
it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email
end
@@ -38,33 +42,15 @@ RSpec.describe CustomerRelations::Contact, type: :model do
it { expect(described_class.reference_postfix).to eq(']') }
end
- describe '#unique_email_for_group_hierarchy' do
- let_it_be(:parent) { create(:group) }
- let_it_be(:group) { create(:group, parent: parent) }
- let_it_be(:subgroup) { create(:group, parent: group) }
-
- let_it_be(:existing_contact) { create(:contact, group: group) }
-
- context 'with unique email for group hierarchy' do
+ describe '#root_group' do
+ context 'when root group' do
subject { build(:contact, group: group) }
it { is_expected.to be_valid }
end
- context 'with duplicate email in group' do
- subject { build(:contact, email: existing_contact.email, group: group) }
-
- it { is_expected.to be_invalid }
- end
-
- context 'with duplicate email in parent group' do
- subject { build(:contact, email: existing_contact.email, group: subgroup) }
-
- it { is_expected.to be_invalid }
- end
-
- context 'with duplicate email in subgroup' do
- subject { build(:contact, email: existing_contact.email, group: parent) }
+ context 'when subgroup' do
+ subject { build(:contact, group: create(:group, parent: group)) }
it { is_expected.to be_invalid }
end
@@ -82,7 +68,6 @@ RSpec.describe CustomerRelations::Contact, type: :model do
end
describe '#self.find_ids_by_emails' do
- let_it_be(:group) { create(:group) }
let_it_be(:group_contacts) { create_list(:contact, 2, group: group) }
let_it_be(:other_contacts) { create_list(:contact, 2) }
@@ -92,13 +77,6 @@ RSpec.describe CustomerRelations::Contact, type: :model do
expect(contact_ids).to match_array(group_contacts.pluck(:id))
end
- it 'returns ids of contacts from parent group' do
- subgroup = create(:group, parent: group)
- contact_ids = described_class.find_ids_by_emails(subgroup, group_contacts.pluck(:email))
-
- expect(contact_ids).to match_array(group_contacts.pluck(:id))
- end
-
it 'does not return ids of contacts from other groups' do
contact_ids = described_class.find_ids_by_emails(group, other_contacts.pluck(:email))
@@ -112,28 +90,17 @@ RSpec.describe CustomerRelations::Contact, type: :model do
end
describe '#self.exists_for_group?' do
- let(:group) { create(:group) }
- let(:subgroup) { create(:group, parent: group) }
-
- context 'with no contacts in group or parent' do
+ context 'with no contacts in group' do
it 'returns false' do
- expect(described_class.exists_for_group?(subgroup)).to be_falsey
+ expect(described_class.exists_for_group?(group)).to be_falsey
end
end
context 'with contacts in group' do
it 'returns true' do
- create(:contact, group: subgroup)
-
- expect(described_class.exists_for_group?(subgroup)).to be_truthy
- end
- end
-
- context 'with contacts in parent' do
- it 'returns true' do
create(:contact, group: group)
- expect(described_class.exists_for_group?(subgroup)).to be_truthy
+ expect(described_class.exists_for_group?(group)).to be_truthy
end
end
end
diff --git a/spec/models/customer_relations/issue_contact_spec.rb b/spec/models/customer_relations/issue_contact_spec.rb
index 39da0b64ea0..f1fb574f86f 100644
--- a/spec/models/customer_relations/issue_contact_spec.rb
+++ b/spec/models/customer_relations/issue_contact_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe CustomerRelations::IssueContact do
let_it_be(:issue_contact, reload: true) { create(:issue_customer_relations_contact) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:project) { create(:project, group: subgroup) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:subgroup_project) { create(:project, group: subgroup) }
let_it_be(:issue) { create(:issue, project: project) }
subject { issue_contact }
@@ -27,33 +28,36 @@ RSpec.describe CustomerRelations::IssueContact do
let(:for_issue) { build(:issue_customer_relations_contact, :for_issue, issue: issue) }
let(:for_contact) { build(:issue_customer_relations_contact, :for_contact, contact: contact) }
- it 'uses objects from the same group', :aggregate_failures do
- expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
- expect(built.contact.group).to eq(built.issue.project.group)
- expect(created.contact.group).to eq(created.issue.project.group)
+ context 'for root groups' do
+ it 'uses objects from the same group', :aggregate_failures do
+ expect(stubbed.contact.group).to eq(stubbed.issue.project.group)
+ expect(built.contact.group).to eq(built.issue.project.group)
+ expect(created.contact.group).to eq(created.issue.project.group)
+ end
end
- it 'builds using the same group', :aggregate_failures do
- expect(for_issue.contact.group).to eq(subgroup)
- expect(for_contact.issue.project.group).to eq(group)
+ context 'for subgroups' do
+ it 'builds using the root ancestor' do
+ expect(for_issue.contact.group).to eq(group)
+ end
end
end
describe 'validation' do
- it 'fails when the contact group does not belong to the issue group or ancestors' do
+ it 'fails when the contact group is unrelated to the issue group' do
built = build(:issue_customer_relations_contact, issue: create(:issue), contact: create(:contact))
expect(built).not_to be_valid
end
- it 'succeeds when the contact group is the same as the issue group' do
- built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: subgroup))
+ it 'succeeds when the contact belongs to a root group and is the same as the issue group' do
+ built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: group))
expect(built).to be_valid
end
- it 'succeeds when the contact group is an ancestor of the issue group' do
- built = build(:issue_customer_relations_contact, issue: create(:issue, project: project), contact: create(:contact, group: group))
+ it 'succeeds when the contact belongs to a root group and it is an ancestor of the issue group' do
+ built = build(:issue_customer_relations_contact, issue: create(:issue, project: subgroup_project), contact: create(:contact, group: group))
expect(built).to be_valid
end
diff --git a/spec/models/customer_relations/organization_spec.rb b/spec/models/customer_relations/organization_spec.rb
index 71b455ae8c8..9fe754b7605 100644
--- a/spec/models/customer_relations/organization_spec.rb
+++ b/spec/models/customer_relations/organization_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe CustomerRelations::Organization, type: :model do
+ let_it_be(:group) { create(:group) }
+
describe 'associations' do
it { is_expected.to belong_to(:group).with_foreign_key('group_id') }
end
@@ -17,6 +19,20 @@ RSpec.describe CustomerRelations::Organization, type: :model do
it { is_expected.to validate_length_of(:description).is_at_most(1024) }
end
+ describe '#root_group' do
+ context 'when root group' do
+ subject { build(:organization, group: group) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when subgroup' do
+ subject { build(:organization, group: create(:group, parent: group)) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
describe '#name' do
it 'strips name' do
organization = described_class.new(name: ' GitLab ')
@@ -27,7 +43,6 @@ RSpec.describe CustomerRelations::Organization, type: :model do
end
describe '#find_by_name' do
- let!(:group) { create(:group) }
let!(:organiztion1) { create(:organization, group: group, name: 'Test') }
let!(:organiztion2) { create(:organization, group: create(:group), name: 'Test') }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6a8f9826481..45a2c134077 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -2950,7 +2950,14 @@ RSpec.describe Group do
expect(group.crm_enabled?).to be_truthy
end
+
+ it 'returns true where crm_settings.state is enabled for subgroup' do
+ subgroup = create(:group, :crm_enabled, parent: group)
+
+ expect(subgroup.crm_enabled?).to be_truthy
+ end
end
+
describe '.get_ids_by_ids_or_paths' do
let(:group_path) { 'group_path' }
let!(:group) { create(:group, path: group_path) }
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index 6ce84c28044..08656bfe543 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -109,6 +109,32 @@ RSpec.describe Integrations::Jira do
end
end
+ describe '#sections' do
+ let(:integration) { create(:jira_integration) }
+
+ subject(:sections) { integration.sections.map { |s| s[:type] } }
+
+ context 'when project_level? is true' do
+ before do
+ allow(integration).to receive(:project_level?).and_return(true)
+ end
+
+ it 'includes SECTION_TYPE_JIRA_ISSUES' do
+ expect(sections).to include(described_class::SECTION_TYPE_JIRA_ISSUES)
+ end
+ end
+
+ context 'when project_level? is false' do
+ before do
+ allow(integration).to receive(:project_level?).and_return(false)
+ end
+
+ it 'does not include SECTION_TYPE_JIRA_ISSUES' do
+ expect(sections).not_to include(described_class::SECTION_TYPE_JIRA_ISSUES)
+ end
+ end
+ end
+
describe '.reference_pattern' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 5af42cc67ea..29305ba435c 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1167,7 +1167,6 @@ RSpec.describe Issue do
end
describe '#check_for_spam?' do
- using RSpec::Parameterized::TableSyntax
let_it_be(:support_bot) { ::User.support_bot }
where(:support_bot?, :visibility_level, :confidential, :new_attributes, :check_for_spam?) do
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 3805976b3e7..1fe9e430011 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -396,4 +396,36 @@ RSpec.describe IssuePolicy do
expect(policies).to be_allowed(:read_issue_iid)
end
end
+
+ describe 'set_issue_crm_contacts' do
+ let(:user) { create(:user) }
+ let(:subgroup) { create(:group, :crm_enabled, parent: create(:group, :crm_enabled)) }
+ let(:project) { create(:project, group: subgroup) }
+ let(:issue) { create(:issue, project: project) }
+ let(:policies) { described_class.new(user, issue) }
+
+ context 'when project reporter' do
+ it 'is disallowed' do
+ project.add_reporter(user)
+
+ expect(policies).to be_disallowed(:set_issue_crm_contacts)
+ end
+ end
+
+ context 'when subgroup reporter' do
+ it 'is allowed' do
+ subgroup.add_reporter(user)
+
+ expect(policies).to be_disallowed(:set_issue_crm_contacts)
+ end
+ end
+
+ context 'when root group reporter' do
+ it 'is allowed' do
+ subgroup.parent.add_reporter(user)
+
+ expect(policies).to be_allowed(:set_issue_crm_contacts)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb
index 1b87a5e24f5..12faeec94da 100644
--- a/spec/requests/api/ci/pipelines_spec.rb
+++ b/spec/requests/api/ci/pipelines_spec.rb
@@ -1075,6 +1075,23 @@ RSpec.describe API::Ci::Pipelines do
expect(json_response['id']).to be nil
end
end
+
+ context 'handles errors' do
+ before do
+ service_response = ServiceResponse.error(http_status: 403, message: 'hello world')
+ allow_next_instance_of(::Ci::RetryPipelineService) do |service|
+ allow(service).to receive(:check_access).and_return(service_response)
+ end
+ end
+
+ it 'returns error' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(json_response['message']).to eq 'hello world'
+ expect(json_response['id']).to be nil
+ end
+ end
end
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
index 79d687a2bdb..02b79dac489 100644
--- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
+++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb
@@ -9,12 +9,10 @@ RSpec.describe 'Setting issues crm contacts' do
let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:subgroup) { create(:group, :crm_enabled, parent: group) }
let_it_be(:project) { create(:project, group: subgroup) }
- let_it_be(:group_contacts) { create_list(:contact, 4, group: group) }
- let_it_be(:subgroup_contacts) { create_list(:contact, 4, group: subgroup) }
+ let_it_be(:contacts) { create_list(:contact, 4, group: group) }
let(:issue) { create(:issue, project: project) }
let(:operation_mode) { Types::MutationOperationModeEnum.default_mode }
- let(:contacts) { subgroup_contacts }
let(:initial_contacts) { contacts[0..1] }
let(:mutation_contacts) { contacts[1..2] }
let(:contact_ids) { contact_global_ids(mutation_contacts) }
@@ -116,15 +114,7 @@ RSpec.describe 'Setting issues crm contacts' do
end
end
- context 'with issue group contacts' do
- let(:contacts) { subgroup_contacts }
-
- it_behaves_like 'successful mutation'
- end
-
- context 'with issue ancestor group contacts' do
- it_behaves_like 'successful mutation'
- end
+ it_behaves_like 'successful mutation'
context 'when the contact does not exist' do
let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] }
diff --git a/spec/requests/groups/crm/contacts_controller_spec.rb b/spec/requests/groups/crm/contacts_controller_spec.rb
index 5d126c6ead5..4d8ca0fcd60 100644
--- a/spec/requests/groups/crm/contacts_controller_spec.rb
+++ b/spec/requests/groups/crm/contacts_controller_spec.rb
@@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::ContactsController do
it_behaves_like 'response with 404 status'
end
+
+ context 'when subgroup' do
+ let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) }
+
+ it_behaves_like 'response with 404 status'
+ end
end
context 'with unauthorized user' do
diff --git a/spec/requests/groups/crm/organizations_controller_spec.rb b/spec/requests/groups/crm/organizations_controller_spec.rb
index f38300c3c5b..37ffac71772 100644
--- a/spec/requests/groups/crm/organizations_controller_spec.rb
+++ b/spec/requests/groups/crm/organizations_controller_spec.rb
@@ -49,6 +49,12 @@ RSpec.describe Groups::Crm::OrganizationsController do
it_behaves_like 'response with 404 status'
end
+
+ context 'when subgroup' do
+ let(:group) { create(:group, :private, :crm_enabled, parent: create(:group)) }
+
+ it_behaves_like 'response with 404 status'
+ end
end
context 'with unauthorized user' do
diff --git a/spec/serializers/issue_sidebar_basic_entity_spec.rb b/spec/serializers/issue_sidebar_basic_entity_spec.rb
index da07290f349..716c97f72af 100644
--- a/spec/serializers/issue_sidebar_basic_entity_spec.rb
+++ b/spec/serializers/issue_sidebar_basic_entity_spec.rb
@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe IssueSidebarBasicEntity do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
- let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [user]) }
let(:serializer) { IssueSerializer.new(current_user: user, project: project) }
@@ -71,4 +72,27 @@ RSpec.describe IssueSidebarBasicEntity do
end
end
end
+
+ describe 'show_crm_contacts' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:is_reporter, :contacts_exist_for_group, :expected) do
+ false | false | false
+ false | true | false
+ true | false | false
+ true | true | true
+ end
+
+ with_them do
+ it 'sets proper boolean value for show_crm_contacts' do
+ allow(CustomerRelations::Contact).to receive(:exists_for_group?).with(group).and_return(contacts_exist_for_group)
+
+ if is_reporter
+ project.root_ancestor.add_reporter(user)
+ end
+
+ expect(entity[:show_crm_contacts]).to be(expected)
+ end
+ end
+ end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 12106b70969..df1e159b5c0 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -137,7 +137,7 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
end
end
- context 'when the last stage was skipepd' do
+ context 'when the last stage was skipped' do
before do
create_build('build 1', :success, 0)
create_build('test 2', :failed, 1)
@@ -336,12 +336,32 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
expect(pipeline.reload).to be_running
end
end
+
+ context 'when user is not allowed to retry build' do
+ before do
+ build = create(:ci_build, pipeline: pipeline, status: :failed)
+ allow_next_instance_of(Ci::RetryBuildService) do |service|
+ allow(service).to receive(:can?).with(user, :update_build, build).and_return(false)
+ end
+ end
+
+ it 'returns an error' do
+ response = service.execute(pipeline)
+
+ expect(response.http_status).to eq(:forbidden)
+ expect(response.errors).to include('403 Forbidden')
+ expect(pipeline.reload).not_to be_running
+ end
+ end
end
context 'when user is not allowed to retry pipeline' do
- it 'raises an error' do
- expect { service.execute(pipeline) }
- .to raise_error Gitlab::Access::AccessDeniedError
+ it 'returns an error' do
+ response = service.execute(pipeline)
+
+ expect(response.http_status).to eq(:forbidden)
+ expect(response.errors).to include('403 Forbidden')
+ expect(pipeline.reload).not_to be_running
end
end
@@ -359,9 +379,12 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
create_build('verify', :canceled, 1)
end
- it 'raises an error' do
- expect { service.execute(pipeline) }
- .to raise_error Gitlab::Access::AccessDeniedError
+ it 'returns an error' do
+ response = service.execute(pipeline)
+
+ expect(response.http_status).to eq(:forbidden)
+ expect(response.errors).to include('403 Forbidden')
+ expect(pipeline.reload).not_to be_running
end
end
@@ -372,9 +395,12 @@ RSpec.describe Ci::RetryPipelineService, '#execute' do
create_build('verify', :canceled, 2)
end
- it 'raises an error' do
- expect { service.execute(pipeline) }
- .to raise_error Gitlab::Access::AccessDeniedError
+ it 'returns an error' do
+ response = service.execute(pipeline)
+
+ expect(response.http_status).to eq(:forbidden)
+ expect(response.errors).to include('403 Forbidden')
+ expect(pipeline.reload).not_to be_running
end
end
end
diff --git a/spec/services/issues/set_crm_contacts_service_spec.rb b/spec/services/issues/set_crm_contacts_service_spec.rb
index 64011a7a003..b0befb9f77c 100644
--- a/spec/services/issues/set_crm_contacts_service_spec.rb
+++ b/spec/services/issues/set_crm_contacts_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Issues::SetCrmContactsService do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :crm_enabled) }
- let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:project) { create(:project, group: create(:group, parent: group)) }
let_it_be(:contacts) { create_list(:contact, 4, group: group) }
let_it_be(:issue, reload: true) { create(:issue, project: project) }
let_it_be(:issue_contact_1) do
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 27967850389..b4a71f52092 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -203,7 +203,7 @@ RSpec.shared_context 'group navbar structure' do
nav_sub_items: []
},
{
- nav_item: _('Group information'),
+ nav_item: group.root? ? _('Group information') : _('Subgroup information'),
nav_sub_items: [
_('Activity'),
_('Labels'),
diff --git a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
index 2097b8890cc..43a723dbb2c 100644
--- a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
end
context 'project in a group' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:incident) { create(:incident, project: project) }
@@ -35,5 +35,34 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
expect(rendered).not_to have_css('[data-testid="escalation_status_container"]')
end
end
+
+ context 'crm contacts widget' do
+ let(:issuable) { issue }
+
+ context 'without permission' do
+ it 'is expected not to be shown' do
+ create(:contact, group: group)
+
+ expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ end
+ end
+
+ context 'without contacts' do
+ it 'is expected not to be shown' do
+ group.add_developer(user)
+
+ expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ end
+ end
+
+ context 'with permission and contacts' do
+ it 'is expected to be shown' do
+ create(:contact, group: group)
+ group.add_developer(user)
+
+ expect(rendered).to have_css('#js-issue-crm-contacts')
+ end
+ end
+ end
end
end
diff --git a/yarn.lock b/yarn.lock
index a03a9ea80d8..fdf543d7c81 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1896,6 +1896,13 @@
dependencies:
"@types/node" "*"
+"@types/hast@^2.0.0":
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
+ integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
+ dependencies:
+ "@types/unist" "*"
+
"@types/http-proxy@^1.17.8":
version "1.17.8"
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55"
@@ -2119,6 +2126,11 @@
resolved "https://registry.yarnpkg.com/@types/ungap__global-this/-/ungap__global-this-0.3.1.tgz#18ce9f657da556037a29d50604335614ce703f4c"
integrity sha512-+/DsiV4CxXl6ZWefwHZDXSe1Slitz21tom38qPCaG0DYCS1NnDPIQDTKcmQ/tvK/edJUKkmuIDBJbmKDiB0r/g==
+"@types/unist@*":
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
+ integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
+
"@types/websocket@1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.4.tgz#1dc497280d8049a5450854dd698ee7e6ea9e60b8"
@@ -5768,6 +5780,13 @@ fault@^1.0.0:
dependencies:
format "^0.2.0"
+fault@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c"
+ integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==
+ dependencies:
+ format "^0.2.0"
+
faye-websocket@^0.11.3:
version "0.11.3"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
@@ -6397,10 +6416,10 @@ highlight.js@^10.6.0, highlight.js@~10.7.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360"
integrity sha512-oFLl873u4usRM9K63j4ME9u3etNF0PLiJhSQ8rdfuL51Wn3zkD6drf9ZW0dOzjnZI22YYG24z30JcmfCZjMgYg==
-highlight.js@^11.3.1:
- version "11.3.1"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291"
- integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw==
+highlight.js@^11.3.1, highlight.js@~11.4.0:
+ version "11.4.0"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.4.0.tgz#34ceadd49e1596ee5aba3d99346cdfd4845ee05a"
+ integrity sha512-nawlpCBCSASs7EdvZOYOYVkJpGmAOKMYZgZtUqSRqodZE0GRVcFKwo1RcpeOemqh9hyttTdd5wDBwHkuSyUfnA==
hmac-drbg@^1.0.1:
version "1.0.1"
@@ -8104,6 +8123,15 @@ lowlight@^1.20.0:
fault "^1.0.0"
highlight.js "~10.7.0"
+lowlight@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-2.5.0.tgz#723a39fc0d9b911731a395b320519cbb0790ab14"
+ integrity sha512-OXGUch9JZu4q5r4Ir6QlUp5pBXMxS7NHaclhRiUlxNRcOSK0gtXZcVrsGP4eM7bv0/KDHg/TXQagx/X35EULsA==
+ dependencies:
+ "@types/hast" "^2.0.0"
+ fault "^2.0.0"
+ highlight.js "~11.4.0"
+
lru-cache@^4.1.2, lru-cache@^4.1.5:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"