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>2020-08-26 18:10:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-26 18:10:29 +0300
commitc82ca12a1c5a359325cb45aaf01b483d1fa0efcb (patch)
tree86bba20fccbaf79f3277ffcba125201492f3e92b
parentff579119e2ecf2608370a1f24c4d791d28f269d9 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue28
-rw-r--r--app/assets/javascripts/alert_management/components/system_notes/system_note.vue2
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue50
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/index.js20
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js6
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue16
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue59
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue32
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue48
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js11
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js69
-rw-r--r--app/assets/javascripts/import_projects/utils.js11
-rw-r--r--app/assets/javascripts/tooltips/components/tooltips.vue70
-rw-r--r--app/assets/javascripts/tooltips/index.js58
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue2
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss4
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/blob_helper.rb7
-rw-r--r--app/helpers/operations_helper.rb2
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/concerns/enums/user_callout.rb1
-rw-r--r--app/models/issuable_severity.rb16
-rw-r--r--app/models/project.rb8
-rw-r--r--app/presenters/project_presenter.rb13
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb2
-rw-r--r--app/services/concerns/incident_management/settings.rb4
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--changelogs/unreleased/225819-combine-alert-overview-details.yml5
-rw-r--r--changelogs/unreleased/238562-create-issuable_severities-table.yml5
-rw-r--r--changelogs/unreleased/fix-toggle_button-vue.yml5
-rw-r--r--changelogs/unreleased/sh-fix-project-deletion-warning-name.yml5
-rw-r--r--changelogs/unreleased/web-ide-preferred-ci.yml5
-rw-r--r--db/migrate/20200824124623_create_issuable_severities.rb22
-rw-r--r--db/schema_migrations/202008241246231
-rw-r--r--db/structure.sql25
-rw-r--r--doc/development/documentation/feature_flags.md8
-rw-r--r--doc/development/pipelines.md4
-rw-r--r--doc/user/group/epics/manage_epics.md12
-rw-r--r--doc/user/group/saml_sso/group_managed_accounts.md2
-rw-r--r--lib/gitlab/tracking/incident_management.rb3
-rw-r--r--lib/gitlab/usage_data_counters/known_events.yml17
-rw-r--r--locale/gitlab.pot21
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb4
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb7
-rw-r--r--spec/controllers/projects_controller_spec.rb12
-rw-r--r--spec/factories/issuable_severity.rb7
-rw-r--r--spec/features/projects/settings/user_renames_a_project_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb2
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js10
-rw-r--r--spec/frontend/alert_management/components/alert_details_spec.js9
-rw-r--r--spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js67
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js28
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js44
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js151
-rw-r--r--spec/frontend/import_projects/store/getters_spec.js13
-rw-r--r--spec/frontend/import_projects/store/mutations_spec.js145
-rw-r--r--spec/frontend/import_projects/utils_spec.js47
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js94
-rw-r--r--spec/frontend/tooltips/index_spec.js58
-rw-r--r--spec/helpers/blob_helper_spec.rb55
-rw-r--r--spec/helpers/operations_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/tracking/incident_management_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb89
-rw-r--r--spec/models/issuable_severity_spec.rb25
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb84
-rw-r--r--spec/support/helpers/dns_helpers.rb2
67 files changed, 1185 insertions, 470 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index b54d39e67d4..c1ebd234088 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -43,16 +43,16 @@ export default {
tabsConfig: [
{
id: 'overview',
- title: s__('AlertManagement|Overview'),
- },
- {
- id: 'fullDetails',
title: s__('AlertManagement|Alert details'),
},
{
id: 'metrics',
title: s__('AlertManagement|Metrics'),
},
+ {
+ id: 'activity',
+ title: s__('AlertManagement|Activity feed'),
+ },
],
components: {
GlBadge,
@@ -331,18 +331,9 @@ export default {
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
- <template>
- <div v-if="alert.notes.nodes" class="issuable-discussion py-5">
- <ul class="notes main-notes-list timeline">
- <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
- </ul>
- </div>
- </template>
- </gl-tab>
- <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<gl-table
class="alert-management-details-table"
- :items="[{ key: 'Value', ...alert }]"
+ :items="[{ 'Full Alert Payload': 'Value', ...alert }]"
:show-empty="true"
:busy="loading"
stacked
@@ -355,9 +346,16 @@ export default {
</template>
</gl-table>
</gl-tab>
- <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
+ <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
+ <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
+ <div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
+ <ul class="notes main-notes-list timeline">
+ <system-note v-for="note in alert.notes.nodes" :key="note.id" :note="note" />
+ </ul>
+ </div>
+ </gl-tab>
</gl-tabs>
<alert-sidebar
:alert="alert"
diff --git a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
index 7d0885126b8..0b206ce42f4 100644
--- a/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/alert_management/components/system_notes/system_note.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
- <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper">
+ <li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-px-0!">
<div class="timeline-entry-inner">
<div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
new file mode 100644
index 00000000000..1308ca53e74
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlButton } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ },
+ props: {
+ dismissEndpoint: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ editPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showAlert: true,
+ };
+ },
+ methods: {
+ dismissAlert() {
+ this.showAlert = false;
+
+ return axios.post(this.dismissEndpoint, {
+ feature_name: this.featureId,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert">
+ {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }}
+ <div class="gl-mt-5">
+ <gl-button :href="editPath" category="primary" variant="info">{{
+ __('Open Web IDE')
+ }}</gl-button>
+ </div>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
new file mode 100644
index 00000000000..eadf3cd6216
--- /dev/null
+++ b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import WebIdeAlert from './components/web_ide_alert.vue';
+
+export default el => {
+ const { dismissEndpoint, featureId, editPath } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createElement) {
+ return createElement(WebIdeAlert, {
+ props: {
+ dismissEndpoint,
+ featureId,
+ editPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 9b9ade28623..89a4b6ec3e3 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
+import initWebIdeAlert from '~/blob/suggest_web_ide_ci';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
+ const alertEl = document.getElementById('js-suggest-web-ide-ci');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@@ -80,4 +82,8 @@ export default () => {
});
}
}
+
+ if (alertEl) {
+ initWebIdeAlert(alertEl);
+ }
};
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 72fdaca7e24..5beb8d2edc5 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -4,20 +4,15 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
-import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
import PageQueryParamSync from './page_query_param_sync.vue';
-import { isProjectImportable } from '../utils';
const reposFetchThrottleDelay = 1000;
export default {
name: 'ImportProjectsTable',
components: {
- ImportedProjectTableRow,
ProviderRepoTableRow,
- IncompatibleRepoTableRow,
PageQueryParamSync,
GlLoadingIcon,
GlButton,
@@ -109,8 +104,6 @@ export default {
throttledFetchRepos: throttle(function fetch() {
this.fetchRepos();
}, reposFetchThrottleDelay),
-
- isProjectImportable,
},
};
</script>
@@ -165,18 +158,11 @@ export default {
</thead>
<tbody>
<template v-for="repo in repositories">
- <incompatible-repo-table-row
- v-if="repo.importSource.incompatible"
- :key="repo.importSource.id"
- :repo="repo"
- />
<provider-repo-table-row
- v-else-if="isProjectImportable(repo)"
- :key="repo.importSource.id"
+ :key="repo.importSource.providerLink"
:repo="repo"
:available-namespaces="availableNamespaces"
/>
- <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
</template>
</tbody>
</table>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
deleted file mode 100644
index 50e735b4478..00000000000
--- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import ImportStatus from './import_status.vue';
-import { STATUSES } from '../constants';
-
-export default {
- name: 'ImportedProjectTableRow',
- components: {
- ImportStatus,
- GlIcon,
- },
- props: {
- project: {
- type: Object,
- required: true,
- },
- },
-
- computed: {
- displayFullPath() {
- return this.project.importedProject.fullPath.replace(/^\//, '');
- },
-
- isFinished() {
- return this.project.importStatus === STATUSES.FINISHED;
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a
- :href="project.importSource.providerLink"
- rel="noreferrer noopener"
- target="_blank"
- data-testid="providerLink"
- >{{ project.importSource.fullName }}
- <gl-icon v-if="project.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td data-testid="fullPath">{{ displayFullPath }}</td>
- <td>
- <import-status :status="project.importStatus" />
- </td>
- <td>
- <a
- v-if="isFinished"
- class="btn btn-default"
- data-testid="goToProject"
- :href="project.importedProject.fullPath"
- rel="noreferrer noopener"
- target="_blank"
- >{{ __('Go to project') }}
- </a>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
deleted file mode 100644
index 3140585ccd7..00000000000
--- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlIcon, GlBadge } from '@gitlab/ui';
-
-export default {
- components: {
- GlBadge,
- GlIcon,
- },
- props: {
- repo: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <tr class="import-row">
- <td>
- <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
- >{{ repo.importSource.fullName }}
- <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
- </a>
- </td>
- <td></td>
- <td></td>
- <td>
- <gl-badge variant="danger">{{ __('Incompatible project') }}</gl-badge>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
index d8cffc6a7d5..18971313dfe 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -1,9 +1,11 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlBadge } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
import ImportStatus from './import_status.vue';
+import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
@@ -11,6 +13,7 @@ export default {
Select2Select,
ImportStatus,
GlIcon,
+ GlBadge,
},
props: {
repo: {
@@ -27,6 +30,26 @@ export default {
...mapState(['ciCdOnly']),
...mapGetters(['getImportTarget']),
+ displayFullPath() {
+ return this.repo.importedProject.fullPath.replace(/^\//, '');
+ },
+
+ isFinished() {
+ return this.repo.importedProject?.importStatus === STATUSES.FINISHED;
+ },
+
+ isImportNotStarted() {
+ return isProjectImportable(this.repo);
+ },
+
+ isIncompatible() {
+ return isIncompatible(this.repo);
+ },
+
+ importStatus() {
+ return getImportStatus(this.repo);
+ },
+
importTarget() {
return this.getImportTarget(this.repo.importSource.id);
},
@@ -85,9 +108,9 @@ export default {
<gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
- <td class="d-flex flex-wrap flex-lg-nowrap">
- <template v-if="repo.target">{{ repo.target }}</template>
- <template v-else>
+ <td class="d-flex flex-wrap flex-lg-nowrap" data-testid="fullPath">
+ <template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
+ <template v-else-if="isImportNotStarted">
<select2-select v-model="targetNamespaceSelect" :options="select2Options" />
<span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
>/</span
@@ -98,18 +121,31 @@ export default {
class="form-control import-project-name-input qa-project-path-field"
/>
</template>
+ <template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</td>
<td>
- <import-status :status="repo.importStatus" />
+ <import-status :status="importStatus" />
</td>
- <td>
+ <td data-testid="actions">
+ <a
+ v-if="isFinished"
+ class="btn btn-default"
+ :href="repo.importedProject.fullPath"
+ rel="noreferrer noopener"
+ target="_blank"
+ >{{ __('Go to project') }}
+ </a>
<button
+ v-if="isImportNotStarted"
type="button"
class="qa-import-button btn btn-default"
@click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
</button>
+ <gl-badge v-else-if="isIncompatible" variant="danger">{{
+ __('Incompatible project')
+ }}</gl-badge>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index 7d529c94d7d..fded478bae2 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,17 +1,18 @@
import { STATUSES } from '../constants';
+import { isProjectImportable, isIncompatible } from '../utils';
export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
export const isImportingAnyRepo = state =>
state.repositories.some(repo =>
- [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
+ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(
+ repo.importedProject?.importStatus,
+ ),
);
-export const hasIncompatibleRepos = state =>
- state.repositories.some(repo => repo.importSource.incompatible);
+export const hasIncompatibleRepos = state => state.repositories.some(isIncompatible);
-export const hasImportableRepos = state =>
- state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
+export const hasImportableRepos = state => state.repositories.some(isProjectImportable);
export const getImportTarget = state => repoId => {
if (state.customImportTargets[repoId]) {
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index b3dbef896a6..ea719e11d86 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -11,37 +11,36 @@ export default {
state.isLoadingRepos = true;
},
- [types.RECEIVE_REPOS_SUCCESS](
- state,
- { importedProjects, providerRepos, incompatibleRepos = [] },
- ) {
- // Normalizing structure to support legacy backend format
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
-
+ [types.RECEIVE_REPOS_SUCCESS](state, repositories) {
state.isLoadingRepos = false;
- state.repositories = [
- ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
- importSource: {
- id: `finished-${project.id}`,
- fullName: importSource,
- sanitizedName: project.name,
- providerLink,
- },
- importStatus,
- importedProject: project,
- })),
- ...providerRepos.map(project => ({
- importSource: project,
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ...incompatibleRepos.map(project => ({
- importSource: { ...project, incompatible: true },
- importStatus: STATUSES.NONE,
- importedProject: null,
- })),
- ];
+ if (!Array.isArray(repositories)) {
+ // Legacy code path, will be removed when all importers will be switched to new pagination format
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
+ state.repositories = [
+ ...repositories.importedProjects.map(importedProject => ({
+ importSource: {
+ id: importedProject.id,
+ fullName: importedProject.importSource,
+ sanitizedName: importedProject.name,
+ providerLink: importedProject.providerLink,
+ },
+ importedProject,
+ })),
+ ...repositories.providerRepos.map(project => ({
+ importSource: project,
+ importedProject: null,
+ })),
+ ...(repositories.incompatibleRepos ?? []).map(project => ({
+ importSource: { ...project, incompatible: true },
+ importedProject: null,
+ })),
+ ];
+
+ return;
+ }
+
+ state.repositories = repositories;
},
[types.RECEIVE_REPOS_ERROR](state) {
@@ -50,31 +49,27 @@ export default {
[types.REQUEST_IMPORT](state, { repoId, importTarget }) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.SCHEDULING;
existingRepo.importedProject = {
+ importStatus: STATUSES.SCHEDULING,
fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
};
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
- const { importStatus, ...project } = importedProject;
-
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = importStatus;
- existingRepo.importedProject = project;
+ existingRepo.importedProject = importedProject;
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
- existingRepo.importStatus = STATUSES.NONE;
existingRepo.importedProject = null;
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => {
const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
- if (repo) {
- repo.importStatus = updatedProject.importStatus;
+ if (repo?.importedProject) {
+ repo.importedProject.importStatus = updatedProject.importStatus;
}
});
},
diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js
index 02128c72cb0..695b12cbcba 100644
--- a/app/assets/javascripts/import_projects/utils.js
+++ b/app/assets/javascripts/import_projects/utils.js
@@ -1,6 +1,13 @@
import { STATUSES } from './constants';
-// Will be expanded in future
+export function isIncompatible(project) {
+ return project.importSource.incompatible;
+}
+
+export function getImportStatus(project) {
+ return project.importedProject?.importStatus ?? STATUSES.NONE;
+}
+
export function isProjectImportable(project) {
- return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
+ return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
}
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue
new file mode 100644
index 00000000000..4edfddd0f85
--- /dev/null
+++ b/app/assets/javascripts/tooltips/components/tooltips.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+const getTooltipTitle = element => {
+ return element.getAttribute('title') || element.dataset.title;
+};
+
+const newTooltip = (element, config = {}) => {
+ const { placement, container, boundary, html, triggers } = element.dataset;
+ const title = getTooltipTitle(element);
+
+ return {
+ id: uniqueId('gl-tooltip'),
+ target: element,
+ title,
+ html,
+ placement,
+ container,
+ boundary,
+ triggers,
+ disabled: !title,
+ ...config,
+ };
+};
+
+export default {
+ components: {
+ GlTooltip,
+ },
+ directives: {
+ SafeHtml,
+ },
+ data() {
+ return {
+ tooltips: [],
+ };
+ },
+ methods: {
+ addTooltips(elements, config) {
+ const newTooltips = elements
+ .filter(element => !this.tooltipExists(element))
+ .map(element => newTooltip(element, config));
+
+ this.tooltips.push(...newTooltips);
+ },
+ tooltipExists(element) {
+ return this.tooltips.some(tooltip => tooltip.target === element);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-tooltip
+ v-for="(tooltip, index) in tooltips"
+ :id="tooltip.id"
+ :key="index"
+ :target="tooltip.target"
+ :triggers="tooltip.triggers"
+ :placement="tooltip.placement"
+ :container="tooltip.container"
+ :boundary="tooltip.boundary"
+ :disabled="tooltip.disabled"
+ >
+ <span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
+ <span v-else>{{ tooltip.title }}</span>
+ </gl-tooltip>
+ </div>
+</template>
diff --git a/app/assets/javascripts/tooltips/index.js b/app/assets/javascripts/tooltips/index.js
new file mode 100644
index 00000000000..985f3350a62
--- /dev/null
+++ b/app/assets/javascripts/tooltips/index.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import Tooltips from './components/tooltips.vue';
+
+let app;
+
+const EVENTS_MAP = {
+ hover: 'mouseenter',
+ click: 'click',
+ focus: 'focus',
+};
+
+const DEFAULT_TRIGGER = 'hover focus';
+
+const tooltipsApp = () => {
+ if (!app) {
+ app = new Vue({
+ render(h) {
+ return h(Tooltips, {
+ props: {
+ elements: this.elements,
+ },
+ ref: 'tooltips',
+ });
+ },
+ }).$mount();
+ }
+
+ return app;
+};
+
+const isTooltip = (node, selector) => node.matches && node.matches(selector);
+
+const addTooltips = (elements, config) => {
+ tooltipsApp().$refs.tooltips.addTooltips(Array.from(elements), config);
+};
+
+const handleTooltipEvent = (rootTarget, e, selector, config = {}) => {
+ for (let { target } = e; target && target !== rootTarget; target = target.parentNode) {
+ if (isTooltip(target, selector)) {
+ addTooltips([target], {
+ show: true,
+ ...config,
+ });
+ break;
+ }
+ }
+};
+
+export const initTooltips = (selector, config = {}) => {
+ const triggers = config?.triggers || DEFAULT_TRIGGER;
+ const events = triggers.split(' ').map(trigger => EVENTS_MAP[trigger]);
+
+ events.forEach(event => {
+ document.addEventListener(event, e => handleTooltipEvent(document, e, selector, config), true);
+ });
+
+ return tooltipsApp();
+};
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 540edc9f61c..29d4516bece 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -73,7 +73,7 @@ export default {
'is-disabled': disabledInput,
'is-loading': isLoading,
}"
- @click="toggleFeature"
+ @click.prevent="toggleFeature"
>
<gl-loading-icon class="loading-icon" />
<span class="toggle-icon">
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
index 0f889935583..1e6ad81009a 100644
--- a/app/assets/stylesheets/pages/alert_management/details.scss
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -67,6 +67,10 @@
}
}
+ .main-notes-list::before {
+ left: 15px !important;
+ }
+
.note-header-info {
@include gl-mt-1;
}
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ba21fbddde1..3d0465977fa 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -98,6 +98,7 @@ class ProjectsController < Projects::ApplicationController
end
else
flash.now[:alert] = result[:message]
+ @project.reset
format.html { render_edit }
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 615c834c529..2eff87ae0ec 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -41,7 +41,7 @@ module BlobHelper
end
def encode_ide_path(path)
- url_encode(path).gsub('%2F', '/')
+ ERB::Util.url_encode(path).gsub('%2F', '/')
end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
@@ -375,4 +375,9 @@ module BlobHelper
def human_access
@project.team.human_max_access(current_user&.id).try(:downcase)
end
+
+ def editing_ci_config?
+ @path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) ||
+ @path.to_s == @project.ci_config_path_or_default
+ end
end
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index d38bc4a9940..521f394a920 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -43,7 +43,7 @@ module OperationsHelper
create_issue: setting.create_issue.to_s,
issue_template_key: setting.issue_template_key.to_s,
send_email: setting.send_email.to_s,
- auto_close_incident: 'true',
+ auto_close_incident: setting.auto_close_incident.to_s,
pagerduty_active: setting.pagerduty_active.to_s,
pagerduty_token: setting.pagerduty_token.to_s,
pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(@project, token: setting.pagerduty_token),
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index b0cfda67ad4..967271a8431 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -9,6 +9,7 @@ module UserCalloutsHelper
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
+ WEB_IDE_ALERT_DISMISSED = 'web_ide_alert_dismissed'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
@@ -50,6 +51,10 @@ module UserCalloutsHelper
customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end
+ def show_web_ide_alert?
+ !user_dismissed?(WEB_IDE_ALERT_DISMISSED)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f33e68bd1d7..549b831b3bb 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -19,6 +19,8 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }]
}.freeze
+ CONFIG_EXTENSION = '.gitlab-ci.yml'
+ DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
BridgeStatusError = Class.new(StandardError)
@@ -647,7 +649,7 @@ module Ci
def config_path
return unless repository_source? || unknown_source?
- project.ci_config_path.presence || '.gitlab-ci.yml'
+ project.ci_config_path_or_default
end
def has_yaml_errors?
diff --git a/app/models/concerns/enums/user_callout.rb b/app/models/concerns/enums/user_callout.rb
index 3c687ccf8ed..f91cc19d3ff 100644
--- a/app/models/concerns/enums/user_callout.rb
+++ b/app/models/concerns/enums/user_callout.rb
@@ -20,6 +20,7 @@ module Enums
webhooks_moved: 13,
service_templates_deprecated: 14,
admin_integrations_moved: 15,
+ web_ide_alert_dismissed: 16,
personal_access_token_expiry: 21, # EE-only
suggest_pipeline: 22,
customize_homepage: 23
diff --git a/app/models/issuable_severity.rb b/app/models/issuable_severity.rb
new file mode 100644
index 00000000000..cd3e967946b
--- /dev/null
+++ b/app/models/issuable_severity.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class IssuableSeverity < ApplicationRecord
+ belongs_to :issue
+
+ validates :issue, presence: true, uniqueness: true
+ validates :severity, presence: true
+
+ enum severity: {
+ unknown: 0,
+ low: 1,
+ medium: 2,
+ high: 3,
+ critical: 4
+ }
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index f55bb30964a..ab687896e2e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2518,6 +2518,14 @@ class Project < ApplicationRecord
.exists?
end
+ def default_branch_or_master
+ default_branch || 'master'
+ end
+
+ def ci_config_path_or_default
+ ci_config_path.presence || Ci::Pipeline::DEFAULT_CONFIG_PATH
+ end
+
private
def find_service(services, name)
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 86fd405812e..ef75c160b2d 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -7,6 +7,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include StorageHelper
include TreeHelper
include IconsHelper
+ include BlobHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
@@ -114,7 +115,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def add_ci_yml_path
- add_special_file_path(file_name: '.gitlab-ci.yml')
+ add_special_file_path(file_name: ci_config_path_or_default)
+ end
+
+ def add_ci_yml_ide_path
+ ide_edit_path(project, default_branch_or_master, ci_config_path_or_default)
end
def add_readme_path
@@ -219,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false,
statistic_icon + _('New file'),
- project_new_blob_path(project, default_branch || 'master'),
+ project_new_blob_path(project, default_branch_or_master),
'missing')
end
end
@@ -325,7 +330,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if cicd_missing?
AnchorData.new(false,
statistic_icon + _('Set up CI/CD'),
- add_ci_yml_path)
+ add_ci_yml_ide_path)
elsif repository.gitlab_ci_yml.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CI/CD configuration'),
@@ -397,7 +402,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
- project.default_branch || 'master',
+ default_branch_or_master,
file_name: file_name,
commit_message: commit_message,
branch_name: branch_name
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index c233ea4e2e5..6ed5bd6726f 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -3,6 +3,7 @@
module AlertManagement
class ProcessPrometheusAlertService < BaseService
include Gitlab::Utils::StrongMemoize
+ include ::IncidentManagement::Settings
def execute
return bad_request unless parsed_alert.valid?
@@ -69,6 +70,7 @@ module AlertManagement
def process_resolved_alert_management_alert
return if am_alert.blank?
+ return unless auto_close_incident?
if am_alert.resolve(ends_at)
close_issue(am_alert.issue)
diff --git a/app/services/concerns/incident_management/settings.rb b/app/services/concerns/incident_management/settings.rb
index 13a047ec106..93dfd6a306d 100644
--- a/app/services/concerns/incident_management/settings.rb
+++ b/app/services/concerns/incident_management/settings.rb
@@ -16,5 +16,9 @@ module IncidentManagement
def process_issues?
incident_management_setting.create_issue?
end
+
+ def auto_close_incident?
+ incident_management_setting.auto_close_incident?
+ end
end
end
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7d072ba5899..38a1f86930e 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -6,6 +6,10 @@
Someone edited the file the same time you did. Please check out
= link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
+
+- if editing_ci_config? && show_web_ide_alert?
+ #js-suggest-web-ide-ci{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::WEB_IDE_ALERT_DISMISSED, edit_path: ide_edit_path } }
+
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
diff --git a/changelogs/unreleased/225819-combine-alert-overview-details.yml b/changelogs/unreleased/225819-combine-alert-overview-details.yml
new file mode 100644
index 00000000000..1641a7fd247
--- /dev/null
+++ b/changelogs/unreleased/225819-combine-alert-overview-details.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Combine the Overview and Alert Detail sections
+merge_request: 39714
+author:
+type: changed
diff --git a/changelogs/unreleased/238562-create-issuable_severities-table.yml b/changelogs/unreleased/238562-create-issuable_severities-table.yml
new file mode 100644
index 00000000000..f475f481fef
--- /dev/null
+++ b/changelogs/unreleased/238562-create-issuable_severities-table.yml
@@ -0,0 +1,5 @@
+---
+title: Add IssuableSeverity to store Incident severity level
+merge_request: 40272
+author:
+type: added
diff --git a/changelogs/unreleased/fix-toggle_button-vue.yml b/changelogs/unreleased/fix-toggle_button-vue.yml
new file mode 100644
index 00000000000..158ebd291e1
--- /dev/null
+++ b/changelogs/unreleased/fix-toggle_button-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Improve click surface area of toggle buttons
+merge_request: 40231
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-fix-project-deletion-warning-name.yml b/changelogs/unreleased/sh-fix-project-deletion-warning-name.yml
new file mode 100644
index 00000000000..0a28eb96718
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-project-deletion-warning-name.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect project path warning after failed project path rename
+merge_request: 40422
+author:
+type: fixed
diff --git a/changelogs/unreleased/web-ide-preferred-ci.yml b/changelogs/unreleased/web-ide-preferred-ci.yml
new file mode 100644
index 00000000000..5e323f65f58
--- /dev/null
+++ b/changelogs/unreleased/web-ide-preferred-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert when editing .gitlab-ci.yml
+merge_request: 39508
+author:
+type: added
diff --git a/db/migrate/20200824124623_create_issuable_severities.rb b/db/migrate/20200824124623_create_issuable_severities.rb
new file mode 100644
index 00000000000..674a54fc16d
--- /dev/null
+++ b/db/migrate/20200824124623_create_issuable_severities.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateIssuableSeverities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ create_table :issuable_severities do |t|
+ t.references :issue, index: { unique: true }, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :severity, null: false, default: 0, limit: 2 # 0 - will stand for unknown
+ end
+ end
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :issuable_severities
+ end
+ end
+end
diff --git a/db/schema_migrations/20200824124623 b/db/schema_migrations/20200824124623
new file mode 100644
index 00000000000..367de6bda2e
--- /dev/null
+++ b/db/schema_migrations/20200824124623
@@ -0,0 +1 @@
+b8fcbdab769758753efae992e64bed7f79149c74f08294035a48a03c59bb1c5d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8d71b8d94fc..5c958d7c165 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12481,6 +12481,21 @@ CREATE SEQUENCE public.ip_restrictions_id_seq
ALTER SEQUENCE public.ip_restrictions_id_seq OWNED BY public.ip_restrictions.id;
+CREATE TABLE public.issuable_severities (
+ id bigint NOT NULL,
+ issue_id bigint NOT NULL,
+ severity smallint DEFAULT 0 NOT NULL
+);
+
+CREATE SEQUENCE public.issuable_severities_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.issuable_severities_id_seq OWNED BY public.issuable_severities.id;
+
CREATE TABLE public.issue_assignees (
user_id integer NOT NULL,
issue_id integer NOT NULL
@@ -17054,6 +17069,8 @@ ALTER TABLE ONLY public.internal_ids ALTER COLUMN id SET DEFAULT nextval('public
ALTER TABLE ONLY public.ip_restrictions ALTER COLUMN id SET DEFAULT nextval('public.ip_restrictions_id_seq'::regclass);
+ALTER TABLE ONLY public.issuable_severities ALTER COLUMN id SET DEFAULT nextval('public.issuable_severities_id_seq'::regclass);
+
ALTER TABLE ONLY public.issue_links ALTER COLUMN id SET DEFAULT nextval('public.issue_links_id_seq'::regclass);
ALTER TABLE ONLY public.issue_metrics ALTER COLUMN id SET DEFAULT nextval('public.issue_metrics_id_seq'::regclass);
@@ -18134,6 +18151,9 @@ ALTER TABLE ONLY public.internal_ids
ALTER TABLE ONLY public.ip_restrictions
ADD CONSTRAINT ip_restrictions_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.issuable_severities
+ ADD CONSTRAINT issuable_severities_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.issue_links
ADD CONSTRAINT issue_links_pkey PRIMARY KEY (id);
@@ -19855,6 +19875,8 @@ CREATE UNIQUE INDEX index_internal_ids_on_usage_and_project_id ON public.interna
CREATE INDEX index_ip_restrictions_on_group_id ON public.ip_restrictions USING btree (group_id);
+CREATE UNIQUE INDEX index_issuable_severities_on_issue_id ON public.issuable_severities USING btree (issue_id);
+
CREATE UNIQUE INDEX index_issue_assignees_on_issue_id_and_user_id ON public.issue_assignees USING btree (issue_id, user_id);
CREATE INDEX index_issue_assignees_on_user_id ON public.issue_assignees USING btree (user_id);
@@ -22192,6 +22214,9 @@ ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ALTER TABLE ONLY public.ci_freeze_periods
ADD CONSTRAINT fk_rails_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES public.projects(id);
+ALTER TABLE ONLY public.issuable_severities
+ ADD CONSTRAINT fk_rails_2fbb74ad6d FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.saml_providers
ADD CONSTRAINT fk_rails_306d459be7 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
diff --git a/doc/development/documentation/feature_flags.md b/doc/development/documentation/feature_flags.md
index 392f478273e..7d6a99d1623 100644
--- a/doc/development/documentation/feature_flags.md
+++ b/doc/development/documentation/feature_flags.md
@@ -66,7 +66,7 @@ be enabled for a single project, and is not ready for production use:
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#anchor-to-section). **(CORE ONLY)**
-CAUTION: **Warning:**
+CAUTION: **Caution:**
This feature might not be available to you. Check the **version history** note above for details.
(...Regular content goes here...)
@@ -125,7 +125,7 @@ use:
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
-CAUTION: **Warning:**
+CAUTION: **Caution:**
This feature might not be available to you. Check the **version history** note above for details.
(...Regular content goes here...)
@@ -181,7 +181,7 @@ cannot be enabled for a single project, and is ready for production use:
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
-CAUTION: **Warning:**
+CAUTION: **Caution:**
This feature might not be available to you. Check the **version history** note above for details.
(...Regular content goes here...)
@@ -254,7 +254,7 @@ be enabled by project, and is ready for production use:
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
-CAUTION: **Warning:**
+CAUTION: **Caution:**
This feature might not be available to you. Check the **version history** note above for details.
(...Regular content goes here...)
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 69ea664f4c1..1aa6cc7c570 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -524,8 +524,10 @@ The current stages are:
later used by the (Helm) Review App deployment (see
[Review Apps](testing_guide/review_apps.md) for details).
- `review`: This stage includes jobs that deploy the GitLab and Docs Review Apps.
+- `dast`: This stage includes jobs that run a DAST full scan against the Review App
+that is deployed in stage `review`.
- `qa`: This stage includes jobs that perform QA tasks against the Review App
- that is deployed in the previous stage.
+ that is deployed in stage `review`.
- `post-qa`: This stage includes jobs that build reports or gather data from
the `qa` stage's jobs (e.g. Review App performance report).
- `pages`: This stage includes a job that deploys the various reports as
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index aaa5d3a3034..3dfa6a33255 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -178,7 +178,7 @@ Feature.disable(:confidential_epics)
## Manage issues assigned to an epic
-### Add an issue to an epic
+### Add a new issue to an epic
You can add an existing issue to an epic, or, create a new issue that's
automatically added to the epic.
@@ -190,13 +190,13 @@ subgroups, are eligible to be added to the epic. Newly added issues appear at th
issues in the **Epics and Issues** tab.
An epic contains a list of issues and an issue can be associated with at most one epic.
-When you add an issue that's already linked to an epic, the issue is automatically unlinked from its
+When you add a new issue that's already linked to an epic, the issue is automatically unlinked from its
current parent.
-To add an issue to an epic:
+To add a new issue to an epic:
1. Click the **Add** dropdown button.
-1. Click **Add an issue**.
+1. Click **Add a new issue**.
1. Identify the issue to be added, using either of the following methods:
- Paste the link of the issue.
- Search for the desired issue by entering part of the issue's title, then selecting the desired
@@ -298,7 +298,7 @@ For more on epic templates, see [Epic Templates - Repeatable sets of issues](htt
To add a child epic to an epic:
1. Click the **Add** dropdown button.
-1. Click **Add an epic**.
+1. Click **Add a new epic**.
1. Identify the epic to be added, using either of the following methods:
- Paste the link of the epic.
- Search for the desired issue by entering part of the epic's title, then selecting the desired
@@ -313,7 +313,7 @@ To add a child epic to an epic:
New child epics appear at the top of the list in the **Epics and Issues** tab.
You can move child epics from one epic to another.
-When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
+When you add a new epic that's already linked to a parent epic, the link to its current parent is removed.
Issues and child epics cannot be intermingled.
To move child epics to another epic:
diff --git a/doc/user/group/saml_sso/group_managed_accounts.md b/doc/user/group/saml_sso/group_managed_accounts.md
index 2cf27e7c103..218570cf872 100644
--- a/doc/user/group/saml_sso/group_managed_accounts.md
+++ b/doc/user/group/saml_sso/group_managed_accounts.md
@@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Group Managed Accounts **(PREMIUM)**
CAUTION: **Caution:**
-This [Closed Beta](https://about.gitlab.com/handbook/product/#closed-beta) feature is being re-evaluated in favor of a different
+This [Closed Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#sts=Closed%20Beta) feature is being re-evaluated in favor of a different
[identity model](https://gitlab.com/gitlab-org/gitlab/-/issues/218631) that does not require separate accounts.
We recommend that group administrators who haven't yet implemented this feature wait for
the new solution.
diff --git a/lib/gitlab/tracking/incident_management.rb b/lib/gitlab/tracking/incident_management.rb
index 5fa819b3696..df2a0658b36 100644
--- a/lib/gitlab/tracking/incident_management.rb
+++ b/lib/gitlab/tracking/incident_management.rb
@@ -35,6 +35,9 @@ module Gitlab
},
pagerduty_active: {
name: 'pagerduty_webhook'
+ },
+ auto_close_incident: {
+ name: 'auto_close_incident'
}
}.with_indifferent_access.freeze
end
diff --git a/lib/gitlab/usage_data_counters/known_events.yml b/lib/gitlab/usage_data_counters/known_events.yml
index b7e516fa8b1..a910f998d86 100644
--- a/lib/gitlab/usage_data_counters/known_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events.yml
@@ -3,86 +3,69 @@
- name: g_compliance_dashboard
redis_slot: compliance
category: compliance
- expiry: 84 # expiration time in days, equivalent to 12 weeks
aggregation: weekly
- name: g_compliance_audit_events
category: compliance
redis_slot: compliance
- expiry: 84
aggregation: weekly
- name: i_compliance_audit_events
category: compliance
redis_slot: compliance
- expiry: 84
aggregation: weekly
- name: i_compliance_credential_inventory
category: compliance
redis_slot: compliance
- expiry: 84
aggregation: weekly
# Analytics category
- name: g_analytics_contribution
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_insights
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_issues
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_productivity
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: g_analytics_valuestream
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_pipelines
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_code_reviews
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_valuestream
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_insights
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_issues
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: p_analytics_repo
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: i_analytics_cohorts
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
- name: i_analytics_dev_ops_score
category: analytics
redis_slot: analytics
- expiry: 84
aggregation: weekly
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9847a35e7b9..69c13c02c03 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1557,6 +1557,9 @@ msgstr ""
msgid "Add environment"
msgstr ""
+msgid "Add existing confidential %{issuableType}"
+msgstr ""
+
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr ""
@@ -2099,6 +2102,9 @@ msgstr[1] ""
msgid "AlertManagement|Acknowledged"
msgstr ""
+msgid "AlertManagement|Activity feed"
+msgstr ""
+
msgid "AlertManagement|Alert"
msgstr ""
@@ -2186,9 +2192,6 @@ msgstr ""
msgid "AlertManagement|Opsgenie is enabled"
msgstr ""
-msgid "AlertManagement|Overview"
-msgstr ""
-
msgid "AlertManagement|Please try again."
msgstr ""
@@ -7201,6 +7204,9 @@ msgstr ""
msgid "Create new branch"
msgstr ""
+msgid "Create new confidential %{issuableType}"
+msgstr ""
+
msgid "Create new directory"
msgstr ""
@@ -17125,6 +17131,9 @@ msgstr ""
msgid "Open Selection"
msgstr ""
+msgid "Open Web IDE"
+msgstr ""
+
msgid "Open comment type dropdown"
msgstr ""
@@ -24330,6 +24339,9 @@ msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
+msgid "The Web IDE offers advanced syntax highlighting capabilities and more."
+msgstr ""
+
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
@@ -24533,6 +24545,9 @@ msgstr ""
msgid "The one place for your designs"
msgstr ""
+msgid "The parent epic is confidential and can only contain confidential epics and issues"
+msgstr ""
+
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb
index 59ec2774d26..e3ec8629c54 100644
--- a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb
+++ b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb
@@ -2,7 +2,7 @@
require_relative 'cluster_with_prometheus.rb'
module QA
- RSpec.describe 'Monitor', :orchestrated, :kubernetes, :requires_admin do
+ RSpec.describe 'Monitor', :orchestrated, :kubernetes, :requires_admin, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241448', type: :investigating } do
include_context "cluster with Prometheus installed"
before do
@@ -65,7 +65,7 @@ module QA
end
end
- it 'uses templating variables for metrics dashboards', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/240984', type: :investigating } do
+ it 'uses templating variables for metrics dashboards' do
templating_dashboard_yml = Pathname
.new(__dir__)
.join('../../../../fixtures/metrics_dashboards/templating.yml')
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 191b718af56..ca1b0d2fe15 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -152,7 +152,8 @@ RSpec.describe Projects::Settings::OperationsController do
create_issue: 'false',
send_email: 'false',
issue_template_key: 'some-other-template',
- pagerduty_active: 'true'
+ pagerduty_active: 'true',
+ auto_close_incident: 'true'
}
}
end
@@ -171,7 +172,7 @@ RSpec.describe Projects::Settings::OperationsController do
new_incident_management_settings = params
expect(Gitlab::Tracking).to receive(:event)
- .with('IncidentManagement::Settings', event_key, kind_of(Hash))
+ .with('IncidentManagement::Settings', event_key, any_args)
patch :update, params: project_params(project, incident_management_setting_attributes: new_incident_management_settings)
@@ -187,6 +188,8 @@ RSpec.describe Projects::Settings::OperationsController do
it_behaves_like 'a gitlab tracking event', { send_email: '0' }, 'disabled_sending_emails'
it_behaves_like 'a gitlab tracking event', { pagerduty_active: '1' }, 'enabled_pagerduty_webhook'
it_behaves_like 'a gitlab tracking event', { pagerduty_active: '0' }, 'disabled_pagerduty_webhook'
+ it_behaves_like 'a gitlab tracking event', { auto_close_incident: '1' }, 'enabled_auto_close_incident'
+ it_behaves_like 'a gitlab tracking event', { auto_close_incident: '0' }, 'disabled_auto_close_incident'
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e59493827ba..d0e7045e9e0 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -555,6 +555,18 @@ RSpec.describe ProjectsController do
end
shared_examples_for 'updating a project' do
+ context 'when there is a conflicting project path' do
+ let(:random_name) { "project-#{SecureRandom.hex(8)}" }
+ let!(:conflict_project) { create(:project, name: random_name, path: random_name, namespace: project.namespace) }
+
+ it 'does not show any references to the conflicting path' do
+ expect { update_project(path: random_name) }.not_to change { project.reload.path }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).not_to include(random_name)
+ end
+ end
+
context 'when only renaming a project path' do
it "sets the repository to the right path after a rename" do
original_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
diff --git a/spec/factories/issuable_severity.rb b/spec/factories/issuable_severity.rb
new file mode 100644
index 00000000000..10ce58f7944
--- /dev/null
+++ b/spec/factories/issuable_severity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :issuable_severity do
+ association :issue, factory: :incident
+ end
+end
diff --git a/spec/features/projects/settings/user_renames_a_project_spec.rb b/spec/features/projects/settings/user_renames_a_project_spec.rb
index 6088ea31661..1ff976eb800 100644
--- a/spec/features/projects/settings/user_renames_a_project_spec.rb
+++ b/spec/features/projects/settings/user_renames_a_project_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe 'Projects > Settings > User renames a project' do
it 'shows errors for invalid project path' do
change_path(project, 'foo&bar')
- expect(page).to have_field 'Path', with: 'foo&bar'
+ expect(page).to have_field 'Path', with: 'gitlab'
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index afa9de5ce86..81736fefae9 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -226,7 +226,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.gitlab_ci_yml).to be_nil
page.within('.project-buttons') do
- expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path)
+ expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_ide_path)
end
end
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index ea36f1dabaf..237f8b408f5 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -18,8 +18,16 @@ jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
}));
jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
+ props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled'],
render(h) {
- return h('div', this.$attrs, this.$slots.default);
+ return h(
+ 'div',
+ {
+ class: 'gl-tooltip',
+ ...this.$attrs,
+ },
+ this.$slots.default,
+ );
},
}));
diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/alert_management/components/alert_details_spec.js
index 2c4ed100a56..6eb88d13cf6 100644
--- a/spec/frontend/alert_management/components/alert_details_spec.js
+++ b/spec/frontend/alert_management/components/alert_details_spec.js
@@ -87,8 +87,8 @@ describe('AlertDetails', () => {
expect(wrapper.find('[data-testid="overview"]').exists()).toBe(true);
});
- it('renders a tab with full alert information', () => {
- expect(wrapper.find('[data-testid="fullDetails"]').exists()).toBe(true);
+ it('renders a tab with an activity feed', () => {
+ expect(wrapper.find('[data-testid="activity"]').exists()).toBe(true);
});
it('renders severity', () => {
@@ -198,7 +198,6 @@ describe('AlertDetails', () => {
mountComponent({ data: { alert: mockAlert } });
});
it('should display a table of raw alert details data', () => {
- wrapper.find('[data-testid="fullDetails"]').trigger('click');
expect(findDetailsTable().exists()).toBe(true);
});
});
@@ -268,8 +267,8 @@ describe('AlertDetails', () => {
it.each`
index | tabId
${0} | ${'overview'}
- ${1} | ${'fullDetails'}
- ${2} | ${'metrics'}
+ ${1} | ${'metrics'}
+ ${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
wrapper.setData({ currentTabIndex: index });
expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
diff --git a/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js b/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js
new file mode 100644
index 00000000000..8dc71f99010
--- /dev/null
+++ b/spec/frontend/blob/suggest_web_ide_ci/web_ide_alert_spec.js
@@ -0,0 +1,67 @@
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlAlert } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import WebIdeAlert from '~/blob/suggest_web_ide_ci/components/web_ide_alert.vue';
+
+const dismissEndpoint = '/-/user_callouts';
+const featureId = 'web_ide_alert_dismissed';
+const editPath = 'edit/master/-/.gitlab-ci.yml';
+
+describe('WebIdeAlert', () => {
+ let wrapper;
+ let mock;
+
+ const findButton = () => wrapper.find(GlButton);
+ const findAlert = () => wrapper.find(GlAlert);
+ const dismissAlert = alertWrapper => alertWrapper.vm.$emit('dismiss');
+ const getPostPayload = () => JSON.parse(mock.history.post[0].data);
+
+ const createComponent = () => {
+ wrapper = shallowMount(WebIdeAlert, {
+ propsData: {
+ dismissEndpoint,
+ featureId,
+ editPath,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost(dismissEndpoint).reply(200);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mock.restore();
+ });
+
+ describe('with defaults', () => {
+ it('displays alert correctly', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('web ide button link has correct path', () => {
+ expect(findButton().attributes('href')).toBe(editPath);
+ });
+
+ it('dismisses alert correctly', async () => {
+ const alertWrapper = findAlert();
+
+ dismissAlert(alertWrapper);
+
+ await waitForPromises();
+
+ expect(alertWrapper.exists()).toBe(false);
+ expect(mock.history.post).toHaveLength(1);
+ expect(getPostPayload()).toEqual({ feature_name: featureId });
+ });
+ });
+});
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index 7ae4d650e36..a5810431afe 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -6,9 +6,7 @@ import state from '~/import_projects/store/state';
import * as getters from '~/import_projects/store/getters';
import { STATUSES } from '~/import_projects/constants';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
-import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
describe('ImportProjectsTable', () => {
@@ -88,20 +86,15 @@ describe('ImportProjectsTable', () => {
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
});
- it('renders a table with imported projects and provider repos', () => {
+ it('renders a table with provider repos', () => {
+ const repositories = [
+ { importSource: { id: 1 }, importedProject: null },
+ { importSource: { id: 2 }, importedProject: { importStatus: STATUSES.FINISHED } },
+ { importSource: { id: 3, incompatible: true }, importedProject: {} },
+ ];
+
createComponent({
- state: {
- namespaces: [{ fullPath: 'path' }],
- repositories: [
- { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
- { importSource: { id: 2 }, importedProject: {}, importStatus: STATUSES.FINISHED },
- {
- importSource: { id: 3, incompatible: true },
- importedProject: {},
- importStatus: STATUSES.NONE,
- },
- ],
- },
+ state: { namespaces: [{ fullPath: 'path' }], repositories },
});
expect(wrapper.contains(GlLoadingIcon)).toBe(false);
@@ -113,9 +106,7 @@ describe('ImportProjectsTable', () => {
.exists(),
).toBe(true);
- expect(wrapper.contains(ProviderRepoTableRow)).toBe(true);
- expect(wrapper.contains(ImportedProjectTableRow)).toBe(true);
- expect(wrapper.contains(IncompatibleRepoTableRow)).toBe(true);
+ expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length);
});
it.each`
@@ -142,7 +133,6 @@ describe('ImportProjectsTable', () => {
createComponent({ state: { repositories: [] } });
expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
- expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`);
});
diff --git a/spec/frontend/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
deleted file mode 100644
index 8890c352826..00000000000
--- a/spec/frontend/import_projects/components/imported_project_table_row_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { mount } from '@vue/test-utils';
-import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
-import ImportStatus from '~/import_projects/components/import_status.vue';
-import { STATUSES } from '~/import_projects/constants';
-
-describe('ImportedProjectTableRow', () => {
- let wrapper;
- const project = {
- importSource: {
- fullName: 'fullName',
- providerLink: 'providerLink',
- },
- importedProject: {
- id: 1,
- fullPath: 'fullPath',
- importSource: 'importSource',
- },
- importStatus: STATUSES.FINISHED,
- };
-
- function mountComponent() {
- wrapper = mount(ImportedProjectTableRow, { propsData: { project } });
- }
-
- beforeEach(() => {
- mountComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders an imported project table row', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
-
- expect(providerLink.attributes().href).toMatch(project.importSource.providerLink);
- expect(providerLink.text()).toMatch(project.importSource.fullName);
- expect(wrapper.find('[data-testid=fullPath]').text()).toMatch(project.importedProject.fullPath);
- expect(wrapper.find(ImportStatus).props().status).toBe(project.importStatus);
- expect(wrapper.find('[data-testid=goToProject').attributes().href).toMatch(
- project.importedProject.fullPath,
- );
- });
-});
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index bd9cd07db78..7a743b5dc04 100644
--- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,6 +1,7 @@
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import ImportStatus from '~/import_projects/components/import_status.vue';
import { STATUSES } from '~/import_projects/constants';
@@ -14,20 +15,6 @@ describe('ProviderRepoTableRow', () => {
targetNamespace: 'target',
newName: 'newName',
};
- const ciCdOnly = false;
- const repo = {
- importSource: {
- id: 'remote-1',
- fullName: 'fullName',
- providerLink: 'providerLink',
- },
- importedProject: {
- id: 1,
- fullPath: 'fullPath',
- importSource: 'importSource',
- },
- importStatus: STATUSES.FINISHED,
- };
const availableNamespaces = [
{ text: 'Groups', children: [{ id: 'test', text: 'test' }] },
@@ -46,55 +33,137 @@ describe('ProviderRepoTableRow', () => {
return store;
}
- const findImportButton = () =>
- wrapper
- .findAll('button')
- .filter(node => node.text() === 'Import')
- .at(0);
+ const findImportButton = () => {
+ const buttons = wrapper.findAll('button').filter(node => node.text() === 'Import');
+
+ return buttons.length ? buttons.at(0) : buttons;
+ };
- function mountComponent(initialState) {
+ function mountComponent(props) {
const localVue = createLocalVue();
localVue.use(Vuex);
- const store = initStore({ ciCdOnly, ...initialState });
+ const store = initStore();
wrapper = shallowMount(ProviderRepoTableRow, {
localVue,
store,
- propsData: { repo, availableNamespaces },
+ propsData: { availableNamespaces, ...props },
});
}
- beforeEach(() => {
- mountComponent();
- });
-
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
- it('renders a provider repo table row', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
+ describe('when rendering importable project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders empty import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE);
+ });
+
+ it('renders a select2 namespace select', () => {
+ expect(wrapper.contains(Select2Select)).toBe(true);
+ expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ });
+
+ it('renders import button', () => {
+ expect(findImportButton().exists()).toBe(true);
+ });
+
+ it('imports repo when clicking import button', async () => {
+ findImportButton().trigger('click');
+
+ await nextTick();
+
+ const { calls } = fetchImport.mock;
- expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
- expect(providerLink.text()).toMatch(repo.importSource.fullName);
- expect(wrapper.find(ImportStatus).props().status).toBe(repo.importStatus);
- expect(wrapper.contains('button')).toBe(true);
+ expect(calls).toHaveLength(1);
+ expect(calls[0][1]).toBe(repo.importSource.id);
+ });
});
- it('renders a select2 namespace select', () => {
- expect(wrapper.contains(Select2Select)).toBe(true);
- expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
+ describe('when rendering imported project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.FINISHED,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
+
+ it('renders proper import status', () => {
+ expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus);
+ });
+
+ it('does not renders a namespace select', () => {
+ expect(wrapper.contains(Select2Select)).toBe(false);
+ });
+
+ it('does not render import button', () => {
+ expect(findImportButton().exists()).toBe(false);
+ });
});
- it('imports repo when clicking import button', async () => {
- findImportButton().trigger('click');
+ describe('when rendering incompatible project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ incompatible: true,
+ },
+ };
- await nextTick();
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('renders project information', () => {
+ const providerLink = wrapper.find('[data-testid=providerLink]');
- const { calls } = fetchImport.mock;
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ });
- expect(calls).toHaveLength(1);
- expect(calls[0][1]).toBe(repo.importSource.id);
+ it('renders badge with error', () => {
+ expect(wrapper.find(GlBadge).text()).toBe('Incompatible project');
+ });
});
});
diff --git a/spec/frontend/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js
index 5c1ea25a684..5b2a8ea06f0 100644
--- a/spec/frontend/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_projects/store/getters_spec.js
@@ -10,13 +10,12 @@ import state from '~/import_projects/store/state';
const IMPORTED_REPO = {
importSource: {},
- importedProject: { fullPath: 'some/path' },
+ importedProject: { fullPath: 'some/path', importStatus: STATUSES.FINISHED },
};
const IMPORTABLE_REPO = {
importSource: { id: 'some-id', sanitizedName: 'sanitized' },
importedProject: null,
- importStatus: STATUSES.NONE,
};
const INCOMPATIBLE_REPO = {
@@ -56,14 +55,20 @@ describe('import_projects store getters', () => {
${STATUSES.STARTED} | ${true}
${STATUSES.FINISHED} | ${false}
`(
- 'isImportingAnyRepo returns $value when repo with $importStatus status is available',
+ 'isImportingAnyRepo returns $value when project with $importStatus status is available',
({ importStatus, value }) => {
- localState.repositories = [{ importStatus }];
+ localState.repositories = [{ importedProject: { importStatus } }];
expect(isImportingAnyRepo(localState)).toBe(value);
},
);
+ it('isImportingAnyRepo returns false when project with no defined importStatus status is available', () => {
+ localState.repositories = [{ importSource: {} }];
+
+ expect(isImportingAnyRepo(localState)).toBe(false);
+ });
+
describe('hasIncompatibleRepos', () => {
it('returns true if there are any incompatible projects', () => {
localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
diff --git a/spec/frontend/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js
index 3672ec9f2c0..7b85e1a5298 100644
--- a/spec/frontend/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_projects/store/mutations_spec.js
@@ -40,93 +40,100 @@ describe('import_projects store mutations', () => {
});
describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
- describe('for imported projects', () => {
- const response = {
- importedProjects: [IMPORTED_PROJECT],
- providerRepos: [],
- };
+ describe('with legacy response format', () => {
+ describe('for imported projects', () => {
+ const response = {
+ importedProjects: [IMPORTED_PROJECT],
+ providerRepos: [],
+ };
- it('picks import status from response', () => {
- state = {};
+ it('recreates importSource from response', () => {
+ state = {};
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
- });
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining({
+ fullName: IMPORTED_PROJECT.importSource,
+ sanitizedName: IMPORTED_PROJECT.name,
+ providerLink: IMPORTED_PROJECT.providerLink,
+ }),
+ );
+ });
- it('recreates importSource from response', () => {
- state = {};
+ it('passes project to importProject', () => {
+ state = {};
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining({
- fullName: IMPORTED_PROJECT.importSource,
- sanitizedName: IMPORTED_PROJECT.name,
- providerLink: IMPORTED_PROJECT.providerLink,
- }),
- );
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
});
- it('passes project to importProject', () => {
- state = {};
-
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
-
- expect(IMPORTED_PROJECT).toStrictEqual(
- expect.objectContaining(state.repositories[0].importedProject),
- );
+ describe('for importable projects', () => {
+ beforeEach(() => {
+ state = {};
+ const response = {
+ importedProjects: [],
+ providerRepos: [SOURCE_PROJECT],
+ };
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
+ });
});
- });
- describe('for importable projects', () => {
- beforeEach(() => {
- state = {};
+ describe('for incompatible projects', () => {
const response = {
importedProjects: [],
- providerRepos: [SOURCE_PROJECT],
+ providerRepos: [],
+ incompatibleRepos: [SOURCE_PROJECT],
};
- mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- });
- it('sets import status to none', () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
- });
+ beforeEach(() => {
+ state = {};
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
- it('sets importSource to project', () => {
- expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
- });
- });
+ it('sets incompatible flag', () => {
+ expect(state.repositories[0].importSource.incompatible).toBe(true);
+ });
- describe('for incompatible projects', () => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- incompatibleRepos: [SOURCE_PROJECT],
- };
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining(SOURCE_PROJECT),
+ );
+ });
+ });
- beforeEach(() => {
+ it('sets repos loading flag to false', () => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ };
state = {};
+
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- });
- it('sets incompatible flag', () => {
- expect(state.repositories[0].importSource.incompatible).toBe(true);
+ expect(state.isLoadingRepos).toBe(false);
});
+ });
- it('sets importSource to project', () => {
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining(SOURCE_PROJECT),
- );
- });
+ it('passes response as it is', () => {
+ const response = [];
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories).toBe(response);
});
it('sets repos loading flag to false', () => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- };
- state = {};
+ const response = [];
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
@@ -154,7 +161,7 @@ describe('import_projects store mutations', () => {
});
it(`sets status to ${STATUSES.SCHEDULING}`, () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.SCHEDULING);
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.SCHEDULING);
});
});
@@ -170,7 +177,9 @@ describe('import_projects store mutations', () => {
});
it('sets import status', () => {
- expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ IMPORTED_PROJECT.importStatus,
+ );
});
it('sets imported project', () => {
@@ -188,8 +197,8 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
});
- it(`resets import status to ${STATUSES.NONE}`, () => {
- expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
+ it(`removes importedProject entry`, () => {
+ expect(state.repositories[0].importedProject).toBeNull();
});
});
@@ -203,7 +212,9 @@ describe('import_projects store mutations', () => {
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
- expect(state.repositories[0].importStatus).toBe(updatedProjects[0].importStatus);
+ expect(state.repositories[0].importedProject.importStatus).toBe(
+ updatedProjects[0].importStatus,
+ );
});
});
diff --git a/spec/frontend/import_projects/utils_spec.js b/spec/frontend/import_projects/utils_spec.js
index 826b06d5a70..4e1e16a3184 100644
--- a/spec/frontend/import_projects/utils_spec.js
+++ b/spec/frontend/import_projects/utils_spec.js
@@ -1,7 +1,16 @@
-import { isProjectImportable } from '~/import_projects/utils';
+import { isProjectImportable, isIncompatible, getImportStatus } from '~/import_projects/utils';
import { STATUSES } from '~/import_projects/constants';
describe('import_projects utils', () => {
+ const COMPATIBLE_PROJECT = {
+ importSource: { incompatible: false },
+ };
+
+ const INCOMPATIBLE_PROJECT = {
+ importSource: { incompatible: true },
+ importedProject: null,
+ };
+
describe('isProjectImportable', () => {
it.each`
status | result
@@ -14,19 +23,43 @@ describe('import_projects utils', () => {
`('returns $result when project is compatible and status is $status', ({ status, result }) => {
expect(
isProjectImportable({
- importStatus: status,
- importSource: { incompatible: false },
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: status },
}),
).toBe(result);
});
+ it('returns true if import status is not defined', () => {
+ expect(isProjectImportable({ importSource: {} })).toBe(true);
+ });
+
it('returns false if project is not compatible', () => {
+ expect(isProjectImportable(INCOMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('isIncompatible', () => {
+ it('returns true for incompatible project', () => {
+ expect(isIncompatible(INCOMPATIBLE_PROJECT)).toBe(true);
+ });
+
+ it('returns false for compatible project', () => {
+ expect(isIncompatible(COMPATIBLE_PROJECT)).toBe(false);
+ });
+ });
+
+ describe('getImportStatus', () => {
+ it('returns actual status when project status is provided', () => {
expect(
- isProjectImportable({
- importStatus: STATUSES.NONE,
- importSource: { incompatible: true },
+ getImportStatus({
+ ...COMPATIBLE_PROJECT,
+ importedProject: { importStatus: STATUSES.FINISHED },
}),
- ).toBe(false);
+ ).toBe(STATUSES.FINISHED);
+ });
+
+ it('returns NONE as status if import status is not provided', () => {
+ expect(getImportStatus(COMPATIBLE_PROJECT)).toBe(STATUSES.NONE);
});
});
});
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
new file mode 100644
index 00000000000..5bc9599c299
--- /dev/null
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -0,0 +1,94 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlTooltip } from '@gitlab/ui';
+import Tooltips from '~/tooltips/components/tooltips.vue';
+
+describe('tooltips/components/tooltips.vue', () => {
+ let wrapper;
+
+ const buildWrapper = () => {
+ wrapper = shallowMount(Tooltips);
+ };
+
+ const createTooltipTarget = (attributes = {}) => {
+ const target = document.createElement('button');
+ const defaults = {
+ title: 'default title',
+ ...attributes,
+ };
+
+ Object.keys(defaults).forEach(name => {
+ target.setAttribute(name, defaults[name]);
+ });
+
+ return target;
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('addTooltips', () => {
+ let target;
+
+ beforeEach(() => {
+ buildWrapper();
+
+ target = createTooltipTarget();
+ });
+
+ it('attaches tooltips to the targets specified', async () => {
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).props('target')).toBe(target);
+ });
+
+ it('does not attach a tooltip twice to the same element', async () => {
+ wrapper.vm.addTooltips([target]);
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.findAll(GlTooltip)).toHaveLength(1);
+ });
+
+ it('sets tooltip content from title attribute', async () => {
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).text()).toBe(target.getAttribute('title'));
+ });
+
+ it('supports HTML content', async () => {
+ target = createTooltipTarget({
+ title: 'content with <b>HTML</b>',
+ 'data-html': true,
+ });
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).html()).toContain(target.getAttribute('title'));
+ });
+
+ it.each`
+ attribute | value | prop
+ ${'data-placement'} | ${'bottom'} | ${'placement'}
+ ${'data-container'} | ${'custom-container'} | ${'container'}
+ ${'data-boundary'} | ${'viewport'} | ${'boundary'}
+ ${'data-triggers'} | ${'manual'} | ${'triggers'}
+ `(
+ 'sets $prop to $value when $attribute is set in target',
+ async ({ attribute, value, prop }) => {
+ target = createTooltipTarget({ [attribute]: value });
+ wrapper.vm.addTooltips([target]);
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.find(GlTooltip).props(prop)).toBe(value);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/tooltips/index_spec.js b/spec/frontend/tooltips/index_spec.js
new file mode 100644
index 00000000000..eb29884a4e8
--- /dev/null
+++ b/spec/frontend/tooltips/index_spec.js
@@ -0,0 +1,58 @@
+import { initTooltips } from '~/tooltips';
+
+describe('tooltips/index.js', () => {
+ const createTooltipTarget = () => {
+ const target = document.createElement('button');
+ const attributes = {
+ title: 'default title',
+ };
+
+ Object.keys(attributes).forEach(name => {
+ target.setAttribute(name, attributes[name]);
+ });
+
+ target.classList.add('has-tooltip');
+
+ return target;
+ };
+
+ const triggerEvent = (target, eventName = 'mouseenter') => {
+ const event = new Event(eventName);
+
+ target.dispatchEvent(event);
+ };
+
+ describe('initTooltip', () => {
+ it('attaches a GlTooltip for the elements specified in the selector', async () => {
+ const target = createTooltipTarget();
+ const tooltipsApp = initTooltips('.has-tooltip');
+
+ document.body.appendChild(tooltipsApp.$el);
+ document.body.appendChild(target);
+
+ triggerEvent(target);
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).not.toBe(null);
+ expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
+ });
+
+ it('supports triggering a tooltip in custom events', async () => {
+ const target = createTooltipTarget();
+ const tooltipsApp = initTooltips('.has-tooltip', {
+ triggers: 'click',
+ });
+
+ document.body.appendChild(tooltipsApp.$el);
+ document.body.appendChild(target);
+
+ triggerEvent(target, 'click');
+
+ await tooltipsApp.$nextTick();
+
+ expect(document.querySelector('.gl-tooltip')).not.toBe(null);
+ expect(document.querySelector('.gl-tooltip').innerHTML).toContain('default title');
+ });
+ });
+});
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 3ba9f39d21a..06f86e7716a 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -481,4 +481,59 @@ RSpec.describe BlobHelper do
end
end
end
+
+ describe '#editing_ci_config?' do
+ let(:project) { build(:project) }
+
+ subject { helper.editing_ci_config? }
+
+ before do
+ assign(:project, project)
+ assign(:path, path)
+ end
+
+ context 'when path is nil' do
+ let(:path) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when path is not a ci file' do
+ let(:path) { 'some-file.txt' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when path ends is gitlab-ci.yml' do
+ let(:path) { '.gitlab-ci.yml' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when path ends with gitlab-ci.yml' do
+ let(:path) { 'template.gitlab-ci.yml' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with custom ci paths' do
+ let(:path) { 'path/to/ci.yaml' }
+
+ before do
+ project.ci_config_path = 'path/to/ci.yaml'
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with custom ci config and path' do
+ let(:path) { 'path/to/template.gitlab-ci.yml' }
+
+ before do
+ project.ci_config_path = 'ci/path/.gitlab-ci.yml@another-group/another-project'
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/helpers/operations_helper_spec.rb b/spec/helpers/operations_helper_spec.rb
index 6fda2f0474d..3dac2cf54dc 100644
--- a/spec/helpers/operations_helper_spec.rb
+++ b/spec/helpers/operations_helper_spec.rb
@@ -137,7 +137,8 @@ RSpec.describe OperationsHelper do
:project_incident_management_setting,
project: project,
issue_template_key: 'template-key',
- pagerduty_active: true
+ pagerduty_active: true,
+ auto_close_incident: false
)
end
@@ -150,7 +151,7 @@ RSpec.describe OperationsHelper do
create_issue: 'false',
issue_template_key: 'template-key',
send_email: 'false',
- auto_close_incident: 'true',
+ auto_close_incident: 'false',
pagerduty_active: 'true',
pagerduty_token: operations_settings.pagerduty_token,
pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(project, token: operations_settings.pagerduty_token),
diff --git a/spec/lib/gitlab/tracking/incident_management_spec.rb b/spec/lib/gitlab/tracking/incident_management_spec.rb
index e8131b4eeee..9c49c76ead7 100644
--- a/spec/lib/gitlab/tracking/incident_management_spec.rb
+++ b/spec/lib/gitlab/tracking/incident_management_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Tracking::IncidentManagement do
.with(
'IncidentManagement::Settings',
label,
- value || kind_of(Hash)
+ value || any_args
)
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 1ec7ddad817..44320e7ed89 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -9,17 +9,23 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:entity4) { '8b9a2671-2abf-4bec-a682-22f6a8f7bf31' }
let(:weekly_event) { 'g_analytics_contribution' }
- let(:daily_event) { 'g_search' }
+ let(:daily_event) { 'g_analytics_search' }
+ let(:analytics_slot_event) { 'g_analytics_contribution' }
+ let(:compliance_slot_event) { 'g_compliance_dashboard' }
+ let(:category_analytics) { 'g_analytics_search' }
+ let(:category_productivity) { 'g_analytics_productivity' }
+ let(:no_slot) { 'no_slot' }
let(:different_aggregation) { 'different_aggregation' }
+ let(:custom_daily_event) { 'g_analytics_custom' }
let(:known_events) do
[
- { name: "g_analytics_contribution", redis_slot: "analytics", category: "analytics", expiry: 84, aggregation: "weekly" },
- { name: "g_analytics_valuestream", redis_slot: "analytics", category: "analytics", expiry: 84, aggregation: "daily" },
- { name: "g_analytics_productivity", redis_slot: "analytics", category: "productivity", expiry: 84, aggregation: "weekly" },
- { name: "g_compliance_dashboard", redis_slot: "compliance", category: "compliance", aggregation: "weekly" },
- { name: "g_search", category: "global", aggregation: "daily" },
- { name: "different_aggregation", category: "global", aggregation: "monthly" }
+ { name: weekly_event, redis_slot: "analytics", category: "analytics", expiry: 84, aggregation: "weekly" },
+ { name: daily_event, redis_slot: "analytics", category: "analytics", expiry: 84, aggregation: "daily" },
+ { name: category_productivity, redis_slot: "analytics", category: "productivity", aggregation: "weekly" },
+ { name: compliance_slot_event, redis_slot: "compliance", category: "compliance", aggregation: "weekly" },
+ { name: no_slot, category: "global", aggregation: "daily" },
+ { name: different_aggregation, category: "global", aggregation: "monthly" }
].map(&:with_indifferent_access)
end
@@ -46,28 +52,58 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
expect { described_class.track_event(entity1, 'unknown', Date.current) } .to raise_error(Gitlab::UsageDataCounters::HLLRedisCounter::UnknownEvent)
end
- it 'sets the keys in Redis to expire automatically after 12 weeks' do
- described_class.track_event(entity1, "g_analytics_contribution")
+ context 'for weekly events' do
+ it 'sets the keys in Redis to expire automatically after the given expiry time' do
+ described_class.track_event(entity1, "g_analytics_contribution")
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a
- expect(keys).not_to be_empty
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "g_{analytics}_contribution-*").to_a
+ expect(keys).not_to be_empty
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(12.weeks)
+ end
+ end
+ end
+
+ it 'sets the keys in Redis to expire automatically after 6 weeks by default' do
+ described_class.track_event(entity1, "g_compliance_dashboard")
+
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a
+ expect(keys).not_to be_empty
+
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(6.weeks)
+ end
end
end
end
- it 'sets the keys in Redis to expire automatically after 6 weeks by default' do
- described_class.track_event(entity1, "g_compliance_dashboard")
+ context 'for daily events' do
+ it 'sets the keys in Redis to expire after the given expiry time' do
+ described_class.track_event(entity1, "g_analytics_search")
+
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "*-g_{analytics}_search").to_a
+ expect(keys).not_to be_empty
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "g_{compliance}_dashboard-*").to_a
- expect(keys).not_to be_empty
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(84.days)
+ end
+ end
+ end
+
+ it 'sets the keys in Redis to expire after 29 days by default' do
+ described_class.track_event(entity1, "no_slot")
- keys.each do |key|
- expect(redis.ttl(key)).to be_within(5.seconds).of(6.weeks)
+ Gitlab::Redis::SharedState.with do |redis|
+ keys = redis.scan_each(match: "*-{no_slot}").to_a
+ expect(keys).not_to be_empty
+
+ keys.each do |key|
+ expect(redis.ttl(key)).to be_within(5.seconds).of(29.days)
+ end
end
end
end
@@ -82,6 +118,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
# Events last week
described_class.track_event(entity1, weekly_event, 2.days.ago)
described_class.track_event(entity1, weekly_event, 2.days.ago)
+ described_class.track_event(entity1, no_slot, 2.days.ago)
# Events 2 weeks ago
described_class.track_event(entity1, weekly_event, 2.weeks.ago)
@@ -107,15 +144,15 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
it 'raise error if metrics are not in the same slot' do
- expect { described_class.unique_events(event_names: %w(g_analytics_contribution g_compliance_dashboard), start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should be in same slot')
+ expect { described_class.unique_events(event_names: [compliance_slot_event, analytics_slot_event], start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should be in same slot')
end
it 'raise error if metrics are not in the same category' do
- expect { described_class.unique_events(event_names: %w(g_analytics_contribution g_analytics_productivity), start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should be in same category')
+ expect { described_class.unique_events(event_names: [category_analytics, category_productivity], start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should be in same category')
end
it "raise error if metrics don't have same aggregation" do
- expect { described_class.unique_events(event_names: %w(g_analytics_contribution g_analytics_valuestream), start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should have same aggregation level')
+ expect { described_class.unique_events(event_names: [daily_event, weekly_event], start_date: 4.weeks.ago, end_date: Date.current) }.to raise_error('Events should have same aggregation level')
end
context 'when data for the last complete week' do
@@ -135,5 +172,9 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: Date.current)).to eq(3) }
it { expect(described_class.unique_events(event_names: daily_event, start_date: 28.days.ago, end_date: 21.days.ago)).to eq(1) }
end
+
+ context 'when no slot is set' do
+ it { expect(described_class.unique_events(event_names: no_slot, start_date: 7.days.ago, end_date: Date.current)).to eq(1) }
+ end
end
end
diff --git a/spec/models/issuable_severity_spec.rb b/spec/models/issuable_severity_spec.rb
new file mode 100644
index 00000000000..4dfd19756cb
--- /dev/null
+++ b/spec/models/issuable_severity_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IssuableSeverity, type: :model do
+ let_it_be(:issuable_severity) { create(:issuable_severity) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:severity) }
+ it { is_expected.to validate_presence_of(:issue) }
+ it { is_expected.to validate_uniqueness_of(:issue) }
+ end
+
+ describe 'enums' do
+ let(:severity_values) do
+ { unknown: 0, low: 1, medium: 2, high: 3, critical: 4 }
+ end
+
+ it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
+ end
+end
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
index 533e2473cb8..039ff2d0eef 100644
--- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe AlertManagement::ProcessPrometheusAlertService do
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project, reload: true) { create(:project, :repository) }
before do
allow(ProjectServiceWorker).to receive(:perform_async)
@@ -128,55 +128,67 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
let(:status) { 'resolved' }
let!(:alert) { create(:alert_management_alert, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
- context 'when status can be changed' do
- it 'resolves an existing alert' do
- expect { execute }.to change { alert.reload.resolved? }.to(true)
- end
+ context 'when auto_resolve_incident set to true' do
+ let_it_be(:operations_settings) { create(:project_incident_management_setting, project: project, auto_close_incident: true) }
- [true, false].each do |state_tracking_enabled|
- context 'existing issue' do
- before do
- stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
- end
+ context 'when status can be changed' do
+ it 'resolves an existing alert' do
+ expect { execute }.to change { alert.reload.resolved? }.to(true)
+ end
- let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
+ [true, false].each do |state_tracking_enabled|
+ context 'existing issue' do
+ before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
+ end
- it 'closes the issue' do
- issue = alert.issue
+ let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
- expect { execute }
- .to change { issue.reload.state }
- .from('opened')
- .to('closed')
- end
+ it 'closes the issue' do
+ issue = alert.issue
+
+ expect { execute }
+ .to change { issue.reload.state }
+ .from('opened')
+ .to('closed')
+ end
- if state_tracking_enabled
- specify { expect { execute }.to change(ResourceStateEvent, :count).by(1) }
- else
- specify { expect { execute }.to change(Note, :count).by(1) }
+ if state_tracking_enabled
+ specify { expect { execute }.to change(ResourceStateEvent, :count).by(1) }
+ else
+ specify { expect { execute }.to change(Note, :count).by(1) }
+ end
end
end
end
- end
- context 'when status change did not succeed' do
- before do
- allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
- allow(alert).to receive(:resolve).and_return(false)
- end
+ context 'when status change did not succeed' do
+ before do
+ allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
+ allow(alert).to receive(:resolve).and_return(false)
+ end
- it 'writes a warning to the log' do
- expect(Gitlab::AppLogger).to receive(:warn).with(
- message: 'Unable to update AlertManagement::Alert status to resolved',
- project_id: project.id,
- alert_id: alert.id
- )
+ it 'writes a warning to the log' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Unable to update AlertManagement::Alert status to resolved',
+ project_id: project.id,
+ alert_id: alert.id
+ )
- execute
+ execute
+ end
end
+
+ it { is_expected.to be_success }
end
- it { is_expected.to be_success }
+ context 'when auto_resolve_incident set to false' do
+ let_it_be(:operations_settings) { create(:project_incident_management_setting, project: project, auto_close_incident: false) }
+
+ it 'does not resolve an existing alert' do
+ expect { execute }.not_to change { alert.reload.resolved? }
+ end
+ end
end
context 'environment given' do
diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb
index 29be4da6902..1795b0a9ac3 100644
--- a/spec/support/helpers/dns_helpers.rb
+++ b/spec/support/helpers/dns_helpers.rb
@@ -25,6 +25,6 @@ module DnsHelpers
def permit_local_dns!
local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i
allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original
- allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything).and_call_original
+ allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original
end
end