summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/memory.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml1
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/lib/utils/downloader.js20
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue48
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js15
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js22
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js2
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss8
-rw-r--r--app/controllers/jwt_controller.rb5
-rw-r--r--app/finders/alert_management/alerts_finder.rb7
-rw-r--r--app/graphql/resolvers/alert_management_alert_resolver.rb5
-rw-r--r--app/models/alert_management/alert.rb1
-rw-r--r--app/models/concerns/state_eventable.rb9
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/resource_state_event.rb15
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb30
-rw-r--r--app/services/projects/import_service.rb8
-rw-r--r--app/views/groups/_flash_messages.html.haml2
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/incident_management/process_alert_worker.rb2
-rw-r--r--app/workers/merge_request_mergeability_check_worker.rb3
-rw-r--r--changelogs/unreleased/216345-extend-project-group-membership-checks-to-group-ancestors.yml5
-rw-r--r--changelogs/unreleased/216750-open-single-panel-new-tab.yml5
-rw-r--r--changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml5
-rw-r--r--doc/administration/audit_events.md1
-rw-r--r--doc/administration/external_database.md23
-rw-r--r--doc/api/README.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql10
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json36
-rw-r--r--doc/install/aws/index.md2
-rw-r--r--spec/factories/resource_state_event.rb10
-rw-r--r--spec/finders/alert_management/alerts_finder_spec.rb31
-rw-r--r--spec/frontend/ajax_loading_spinner_spec.js (renamed from spec/javascripts/ajax_loading_spinner_spec.js)4
-rw-r--r--spec/frontend/avatar_helper_spec.js (renamed from spec/javascripts/avatar_helper_spec.js)0
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js (renamed from spec/javascripts/bootstrap_linked_tabs_spec.js)4
-rw-r--r--spec/frontend/close_reopen_report_toggle_spec.js (renamed from spec/javascripts/close_reopen_report_toggle_spec.js)56
-rw-r--r--spec/frontend/commit_merge_requests_spec.js (renamed from spec/javascripts/commit_merge_requests_spec.js)0
-rw-r--r--spec/frontend/commits_spec.js (renamed from spec/javascripts/commits_spec.js)6
-rw-r--r--spec/frontend/create_item_dropdown_spec.js (renamed from spec/javascripts/create_item_dropdown_spec.js)0
-rw-r--r--spec/frontend/diff_comments_store_spec.js (renamed from spec/javascripts/diff_comments_store_spec.js)15
-rw-r--r--spec/frontend/emoji_spec.js (renamed from spec/javascripts/emoji_spec.js)0
-rw-r--r--spec/frontend/flash_spec.js (renamed from spec/javascripts/flash_spec.js)15
-rw-r--r--spec/frontend/issuable_spec.js (renamed from spec/javascripts/issuable_spec.js)4
-rw-r--r--spec/frontend/landing_spec.js184
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js40
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js21
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js120
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js27
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js16
-rw-r--r--spec/frontend/monitoring/store_utils.js6
-rw-r--r--spec/frontend/oauth_remember_me_spec.js (renamed from spec/javascripts/oauth_remember_me_spec.js)0
-rw-r--r--spec/frontend/pipelines_spec.js (renamed from spec/javascripts/pipelines_spec.js)0
-rw-r--r--spec/frontend/settings_panels_spec.js (renamed from spec/javascripts/settings_panels_spec.js)0
-rw-r--r--spec/graphql/resolvers/alert_management_alert_resolver_spec.rb6
-rw-r--r--spec/javascripts/landing_spec.js166
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/models/alert_management/alert_spec.rb25
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb4
-rw-r--r--spec/models/resource_state_event_spec.rb14
-rw-r--r--spec/requests/jwt_controller_spec.rb41
-rw-r--r--spec/services/issuable/clone/attributes_rewriter_spec.rb22
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb6
-rw-r--r--spec/workers/merge_request_mergeability_check_worker_spec.rb11
78 files changed, 954 insertions, 266 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 0c26273ef04..303ca19d099 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -29,6 +29,15 @@
- vendor/gitaly-ruby
policy: pull
+.yarn-cache:
+ cache:
+ key:
+ files:
+ - yarn.lock
+ prefix: "v1"
+ paths:
+ - node_modules/
+
.use-pg9:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services:
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml
index 92468925dd7..a24801908f6 100644
--- a/.gitlab/ci/memory.gitlab-ci.yml
+++ b/.gitlab/ci/memory.gitlab-ci.yml
@@ -28,6 +28,7 @@ memory-static:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_metrics.txt
+ expire_in: 31d
# Show memory usage caused by invoking require per gem.
# Unlike `memory-static`, it hits the app with one request to ensure that any last minute require-s have been called.
@@ -54,3 +55,4 @@ memory-on-boot:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_on_boot_metrics.txt
+ expire_in: 31d
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index 38d79ddb090..218ec7043d9 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -15,3 +15,4 @@ pages:
artifacts:
paths:
- public
+ expire_in: 31d
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 1b5280c315b..cba45a009d5 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -194,7 +194,7 @@ gitlab:setup:
when: on_failure
expire_in: 1d
paths:
- - log/development.log
+ - log/*.log
rspec:coverage:
extends:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index ed90837d109..be99fbb906f 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -198,6 +198,7 @@ review-performance:
- sitespeed-results/
reports:
performance: performance.json
+ expire_in: 31d
parallel-spec-reports:
extends:
@@ -223,17 +224,17 @@ parallel-spec-reports:
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
+ expire_in: 31d
danger-review:
extends:
- .default-retry
- - .default-cache
+ - .yarn-cache
- .review:rules:danger
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
needs: []
script:
- - git version
- - node --version
- - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
- danger --fail-on-errors=true --verbose
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index e34728a725e..69314eb665b 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -15,6 +15,7 @@ cache gems:
artifacts:
paths:
- vendor/cache
+ expire_in: 31d
.minimal-job:
extends:
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 57b9fc187c0..2f70731b8aa 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.30.1
+8.31.0
diff --git a/app/assets/javascripts/lib/utils/downloader.js b/app/assets/javascripts/lib/utils/downloader.js
new file mode 100644
index 00000000000..2297f5f90ce
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/downloader.js
@@ -0,0 +1,20 @@
+/**
+ * Helper function to trigger a download.
+ *
+ * - If the `fileName` is `_blank` it will open the file in a new tab.
+ * - If `fileData` is provided, it will inline the content and use data URLs to
+ * download the file. In this case the `url` property will be ignored. Please
+ * note that `fileData` needs to be Base64 encoded.
+ */
+export default ({ fileName, url, fileData }) => {
+ let href = url;
+
+ if (fileData) {
+ href = `data:text/plain;base64,${fileData}`;
+ }
+
+ const anchor = document.createElement('a');
+ anchor.download = fileName;
+ anchor.href = href;
+ anchor.click();
+};
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index e2e950f7790..2b5a10907f5 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -30,6 +30,7 @@ import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
+import VariablesSection from './variables_section.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import {
@@ -64,6 +65,8 @@ export default {
EmptyState,
GroupEmptyState,
DashboardsDropdown,
+
+ VariablesSection,
},
directives: {
GlModal: GlModalDirective,
@@ -222,6 +225,7 @@ export default {
'allDashboards',
'environmentsLoading',
'expandedPanel',
+ 'promVariables',
]),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
@@ -243,6 +247,9 @@ export default {
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
+ shouldShowVariablesSection() {
+ return Object.keys(this.promVariables).length > 0;
+ },
},
watch: {
dashboard(newDashboard) {
@@ -584,7 +591,7 @@ export default {
</div>
</div>
</div>
-
+ <variables-section v-if="shouldShowVariablesSection && !showEmptyState" />
<div v-if="!showEmptyState">
<dashboard-panel
v-show="expandedPanel.panel"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 64e79f86dce..48825fda5c8 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -313,7 +313,12 @@ export default {
<template slot="button-content">
<gl-icon name="ellipsis_v" class="text-secondary" />
</template>
- <gl-dropdown-item v-if="expandBtnAvailable" ref="expandBtn" @click="onExpand">
+ <gl-dropdown-item
+ v-if="expandBtnAvailable"
+ ref="expandBtn"
+ :href="clipboardText"
+ @click.prevent="onExpand"
+ >
{{ s__('Metrics|Expand panel') }}
</gl-dropdown-item>
<gl-dropdown-item
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
new file mode 100644
index 00000000000..a67bc62e196
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['promVariables']),
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['fetchDashboardData', 'setVariableData']),
+ refreshDashboard(event) {
+ const { name, value } = event.target;
+
+ if (this.promVariables[name] !== value) {
+ const changedVariable = { [name]: value };
+
+ this.setVariableData(changedVariable);
+
+ updateHistory({
+ url: mergeUrlParams(this.promVariables, window.location.href),
+ title: document.title,
+ });
+
+ this.fetchDashboardData();
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
+ <div v-for="(val, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
+ <gl-form-group :label="key" class="mb-0 flex-grow-1">
+ <gl-form-input
+ :value="val"
+ :name="key"
+ @keyup.native.enter="refreshDashboard"
+ @blur.native="refreshDashboard"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 9d18629bf34..0134378868b 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -222,14 +222,17 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
*
* @param {metric} metric
*/
-export const fetchPrometheusMetric = ({ commit, state }, { metric, defaultQueryParams }) => {
+export const fetchPrometheusMetric = (
+ { commit, state, getters },
+ { metric, defaultQueryParams },
+) => {
const queryParams = { ...defaultQueryParams };
if (metric.step) {
queryParams.step = metric.step;
}
- if (state.promVariables.length > 0) {
- queryParams.variables = state.promVariables;
+ if (Object.keys(state.promVariables).length > 0) {
+ queryParams.variables = getters.getCustomVariablesArray;
}
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
@@ -390,5 +393,11 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
});
};
+// Variables manipulation
+
+export const setVariableData = ({ commit }, updatedVariable) => {
+ commit(types.UPDATE_VARIABLE_DATA, updatedVariable);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index a6d80c5063e..1cadc287204 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -96,5 +96,17 @@ export const filteredEnvironments = state =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
+/**
+ * Maps an variables object to an array
+ * @param {Object} variables - Custom variables provided by the user
+ * @returns {Array} The custom variables array to be send to the API
+ * in the format of [variable1, variable1_value]
+ */
+
+export const getCustomVariablesArray = state =>
+ Object.entries(state.promVariables)
+ .flat()
+ .map(encodeURIComponent);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index ebe89e93ede..2fd0efa4ab7 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -3,6 +3,7 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
export const SET_PROM_QUERY_VARIABLES = 'SET_PROM_QUERY_VARIABLES';
+export const UPDATE_VARIABLE_DATA = 'UPDATE_VARIABLE_DATA';
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index c4c15993aa0..8de1430302a 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,4 +1,4 @@
-import pick from 'lodash/pick';
+import { pick } from 'lodash';
import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
@@ -51,18 +51,6 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR;
};
-/**
- * Maps an variables object to an array
- * @returns {Array} The custom variables array to be send to the API
- * in the format of [variable1, variable1_value]
- * @param {Object} variables - Custom variables provided by the user
- */
-
-const transformVariablesObjectArray = variables =>
- Object.entries(variables)
- .flat()
- .map(encodeURIComponent);
-
export default {
/**
* Dashboard panels structure and global state
@@ -182,6 +170,12 @@ export default {
state.expandedPanel.panel = panel;
},
[types.SET_PROM_QUERY_VARIABLES](state, variables) {
- state.promVariables = transformVariablesObjectArray(variables);
+ state.promVariables = variables;
+ },
+ [types.UPDATE_VARIABLE_DATA](state, newVariable) {
+ Object.assign(state.promVariables, {
+ ...state.promVariables,
+ ...newVariable,
+ });
},
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 3a63d6279f4..f1b2baf0f74 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -33,7 +33,7 @@ export default () => ({
panel: null,
},
allDashboards: [],
- promVariables: [],
+ promVariables: {},
// Other project data
annotations: [],
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index f61245bed24..d86bf92eac4 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -13,6 +13,14 @@
.form-group {
margin-bottom: map-get($spacing-scale, 3);
}
+
+ .variables-section {
+ input {
+ @include media-breakpoint-up(sm) {
+ width: 160px;
+ }
+ }
+ }
}
.draggable {
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index da39d64c93d..dcd80f4032e 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -75,4 +75,9 @@ class JwtController < ApplicationController
Array(Rack::Utils.parse_query(request.query_string)['scope'])
end
+
+ def auth_user
+ actor = @authentication_result&.actor
+ actor.is_a?(User) ? actor : nil
+ end
end
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index a48dadc7fdb..eeb6b5f8491 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -12,6 +12,7 @@ module AlertManagement
return AlertManagement::Alert.none unless authorized?
collection = project.alert_management_alerts
+ collection = by_status(collection)
collection = by_iid(collection)
sort(collection)
end
@@ -26,6 +27,12 @@ module AlertManagement
collection.for_iid(params[:iid])
end
+ def by_status(collection)
+ values = AlertManagement::Alert::STATUSES.values & Array(params[:status])
+
+ values.present? ? collection.for_status(values) : collection
+ end
+
def sort(collection)
params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection
end
diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb
index 6c8f64cc62b..d2f82ece281 100644
--- a/app/graphql/resolvers/alert_management_alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management_alert_resolver.rb
@@ -6,6 +6,11 @@ module Resolvers
required: false,
description: 'IID of the alert. For example, "1"'
+ argument :statuses, [Types::AlertManagement::StatusEnum],
+ as: :status,
+ required: false,
+ description: 'Alerts with the specified statues. For example, [TRIGGERED]'
+
argument :sort, Types::AlertManagement::AlertSortEnum,
description: 'Sort alerts by this criteria',
required: false
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 6bbdc1645ca..e1a6b507906 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -93,6 +93,7 @@ module AlertManagement
end
scope :for_iid, -> (iid) { where(iid: iid) }
+ scope :for_status, -> (status) { where(status: status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
diff --git a/app/models/concerns/state_eventable.rb b/app/models/concerns/state_eventable.rb
new file mode 100644
index 00000000000..68129798543
--- /dev/null
+++ b/app/models/concerns/state_eventable.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module StateEventable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :resource_state_events
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 82643d8f5d6..90443cba81b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -17,6 +17,7 @@ class Issue < ApplicationRecord
include IgnorableColumns
include MilestoneEventable
include WhereComposite
+ include StateEventable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c962f8c8c26..e24384156c9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -19,6 +19,7 @@ class MergeRequest < ApplicationRecord
include ShaAttribute
include IgnorableColumns
include MilestoneEventable
+ include StateEventable
sha_attribute :squash_commit_sha
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
new file mode 100644
index 00000000000..1d6573b180f
--- /dev/null
+++ b/app/models/resource_state_event.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ResourceStateEvent < ResourceEvent
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
+
+ validate :exactly_one_issuable
+
+ # state is used for issue and merge request states.
+ enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5)
+
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
+ end
+end
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index 78d3fb2e4d2..a78e191c85f 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -20,6 +20,7 @@ module Issuable
copy_resource_label_events
copy_resource_weight_events
copy_resource_milestone_events
+ copy_resource_state_events
end
private
@@ -47,8 +48,6 @@ module Issuable
end
def copy_resource_label_events
- entity_key = new_entity.class.name.underscore.foreign_key
-
copy_events(ResourceLabelEvent.table_name, original_entity.resource_label_events) do |event|
event.attributes
.except('id', 'reference', 'reference_html')
@@ -80,9 +79,18 @@ module Issuable
end
end
- def event_attributes_with_milestone(event, milestone)
- entity_key = new_entity.class.name.underscore.foreign_key
+ def copy_resource_state_events
+ return unless state_events_supported?
+
+ copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
+ event.attributes
+ .except('id')
+ .merge(entity_key => new_entity.id,
+ 'state' => ResourceStateEvent.states[event.state])
+ end
+ end
+ def event_attributes_with_milestone(event, milestone)
event.attributes
.except('id')
.merge(entity_key => new_entity.id,
@@ -102,12 +110,20 @@ module Issuable
end
def entity_key
- new_entity.class.name.parameterize('_').foreign_key
+ new_entity.class.name.underscore.foreign_key
end
def milestone_events_supported?
- original_entity.respond_to?(:resource_milestone_events) &&
- new_entity.respond_to?(:resource_milestone_events)
+ both_respond_to?(:resource_milestone_events)
+ end
+
+ def state_events_supported?
+ both_respond_to?(:resource_state_events)
+ end
+
+ def both_respond_to?(method)
+ original_entity.respond_to?(method) &&
+ new_entity.respond_to?(method)
end
end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index e55697eebcc..a2167be7949 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -22,6 +22,8 @@ module Projects
import_data
+ after_execute_hook
+
success
rescue Gitlab::UrlBlocker::BlockedUrlError => e
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
@@ -37,6 +39,10 @@ module Projects
private
+ def after_execute_hook
+ # Defined in EE::Projects::ImportService
+ end
+
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
@@ -131,3 +137,5 @@ module Projects
end
end
end
+
+Projects::ImportService.prepend_if_ee('EE::Projects::ImportService')
diff --git a/app/views/groups/_flash_messages.html.haml b/app/views/groups/_flash_messages.html.haml
new file mode 100644
index 00000000000..fa1a9d2cca4
--- /dev/null
+++ b/app/views/groups/_flash_messages.html.haml
@@ -0,0 +1,2 @@
+= content_for :flash_message do
+ = render_if_exists 'shared/shared_runners_minutes_limit', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index e1cda7dbacd..032766327ca 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -8,6 +8,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
+= render partial: 'flash_messages'
+
%div{ class: [("limit-container-width" unless fluid_layout)] }
= render_if_exists 'trials/banner', namespace: @group
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b9327c4573f..752ebcc6add 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1150,7 +1150,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
- :name: migrate_external_diffs
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index e63bcc4cb08..2ce9fe359b5 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -35,7 +35,7 @@ module IncidentManagement
return if alert.update(issue_id: issue_id)
- Gitlab::GitLogger.warn(
+ Gitlab::AppLogger.warn(
message: 'Cannot link an Issue with Alert',
issue_id: issue_id,
alert_id: alert_id,
diff --git a/app/workers/merge_request_mergeability_check_worker.rb b/app/workers/merge_request_mergeability_check_worker.rb
index a26c1a886f6..1a84efb4e52 100644
--- a/app/workers/merge_request_mergeability_check_worker.rb
+++ b/app/workers/merge_request_mergeability_check_worker.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-class MergeRequestMergeabilityCheckWorker # rubocop:disable Scalability/IdempotentWorker
+class MergeRequestMergeabilityCheckWorker
include ApplicationWorker
feature_category :source_code_management
+ idempotent!
def perform(merge_request_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
diff --git a/changelogs/unreleased/216345-extend-project-group-membership-checks-to-group-ancestors.yml b/changelogs/unreleased/216345-extend-project-group-membership-checks-to-group-ancestors.yml
new file mode 100644
index 00000000000..6a82569a74b
--- /dev/null
+++ b/changelogs/unreleased/216345-extend-project-group-membership-checks-to-group-ancestors.yml
@@ -0,0 +1,5 @@
+---
+title: Consider project group and group ancestors when processing CODEOWNERS entries
+merge_request: 31152
+author:
+type: added
diff --git a/changelogs/unreleased/216750-open-single-panel-new-tab.yml b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
new file mode 100644
index 00000000000..7a252c8b1db
--- /dev/null
+++ b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Allow monitoring dashboard users to open single panels in a new tab
+merge_request: 31206
+author:
+type: added
diff --git a/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
new file mode 100644
index 00000000000..66cdbdc7a36
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Fix logging of username in /jwt/auth
+merge_request:
+author:
+type: fixed
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 525348464db..d652a5d66e0 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -50,6 +50,7 @@ From there, you can see the following actions:
- User sign-in via [Group SAML](../user/group/saml_sso/index.md)
- Permissions changes of a user assigned to a group
- Removed user from group
+- Project imported in to group
- Project added to group and with which visibility level
- Project removed from group
- [Project shared with group](../user/project/members/share_project_with_groups.md)
diff --git a/doc/administration/external_database.md b/doc/administration/external_database.md
index 4a1059a18d0..13c9ef872f8 100644
--- a/doc/administration/external_database.md
+++ b/doc/administration/external_database.md
@@ -13,5 +13,24 @@ If you use a cloud-managed service, or provide your own PostgreSQL instance:
[database requirements document](../install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
-1. Configure the GitLab application servers with the appropriate details.
- This step is covered in [Configuring GitLab for HA](high_availability/gitlab.md).
+1. Configure the GitLab application servers with the appropriate connection details
+ for your external PostgreSQL service in your `/etc/gitlab/gitlab.rb` file:
+
+ ```ruby
+ # Disable the bundled Omnibus provided PostgreSQL
+ postgresql['enable'] = false
+
+ # PostgreSQL connection details
+ gitlab_rails['db_adapter'] = 'postgresql'
+ gitlab_rails['db_encoding'] = 'unicode'
+ gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
+ gitlab_rails['db_password'] = 'DB password'
+ ```
+
+ For more information on GitLab HA setups, refer to [configuring GitLab for HA](high_availability/gitlab.md).
+
+1. Reconfigure for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
diff --git a/doc/api/README.md b/doc/api/README.md
index 6b9ca5703fe..34d496a37fe 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -75,7 +75,7 @@ end of an API URL.
Most API requests require authentication, or will only return public data when
authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
-for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
+for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md#get-single-project).
There are several ways to authenticate with the GitLab API:
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index cc21b169120..81b27a17aec 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -6883,6 +6883,11 @@ type Project {
Sort alerts by this criteria
"""
sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
): AlertManagementAlert
"""
@@ -6918,6 +6923,11 @@ type Project {
Sort alerts by this criteria
"""
sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
): AlertManagementAlertConnection
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index c7614b8dd6b..344edccb711 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -20591,6 +20591,24 @@
"defaultValue": null
},
{
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "sort",
"description": "Sort alerts by this criteria",
"type": {
@@ -20624,6 +20642,24 @@
"defaultValue": null
},
{
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "sort",
"description": "Sort alerts by this criteria",
"type": {
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 05906520c1c..4307bfb4a74 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -297,6 +297,8 @@ We need a security group for our database that will allow inbound traffic from t
### Create the database
+DANGER: **Danger:** Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
+
Now, it's time to create the database:
1. Navigate to the RDS dashboard, select **Databases** from the left menu, and click **Create database**.
diff --git a/spec/factories/resource_state_event.rb b/spec/factories/resource_state_event.rb
new file mode 100644
index 00000000000..e3de462b797
--- /dev/null
+++ b/spec/factories/resource_state_event.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :resource_state_event do
+ issue { merge_request.nil? ? create(:issue) : nil }
+ merge_request { nil }
+ state { :opened }
+ user { issue&.author || merge_request&.author || create(:user) }
+ end
+end
diff --git a/spec/finders/alert_management/alerts_finder_spec.rb b/spec/finders/alert_management/alerts_finder_spec.rb
index 8ce5216ba9a..70130fbd392 100644
--- a/spec/finders/alert_management/alerts_finder_spec.rb
+++ b/spec/finders/alert_management/alerts_finder_spec.rb
@@ -37,6 +37,37 @@ describe AlertManagement::AlertsFinder, '#execute' do
end
end
+ context 'status given' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it { is_expected.to match_array(alert_1) }
+
+ context 'with an array of statuses' do
+ let(:alert_3) { create(:alert_management_alert) }
+ let(:params) { { status: [AlertManagement::Alert::STATUSES[:resolved]] } }
+
+ it { is_expected.to match_array(alert_1) }
+ end
+
+ context 'with no alerts of status' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:acknowledged] } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with an empty status array' do
+ let(:params) { { status: [] } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+
+ context 'with an nil status' do
+ let(:params) { { status: nil } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+ end
+
describe 'sorting' do
context 'when sorting by created' do
context 'sorts alerts ascending' do
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/frontend/ajax_loading_spinner_spec.js
index 89195a4397f..8ed2ee49ff8 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/frontend/ajax_loading_spinner_spec.js
@@ -11,7 +11,7 @@ describe('Ajax Loading Spinner', () => {
});
it('change current icon with spinner icon and disable link while waiting ajax response', done => {
- spyOn($, 'ajax').and.callFake(req => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
const icon = ajaxLoadingSpinner.querySelector('i');
@@ -34,7 +34,7 @@ describe('Ajax Loading Spinner', () => {
});
it('use original icon again and enabled the link after complete the ajax request', done => {
- spyOn($, 'ajax').and.callFake(req => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
diff --git a/spec/javascripts/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js
index c1ef08e0f1b..c1ef08e0f1b 100644
--- a/spec/javascripts/avatar_helper_spec.js
+++ b/spec/frontend/avatar_helper_spec.js
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
index 1d21637ceae..2d8939e6480 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -9,7 +9,7 @@ describe('Linked Tabs', () => {
describe('when is initialized', () => {
beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function() {});
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
});
it('should activate the tab correspondent to the given action', () => {
@@ -37,7 +37,7 @@ describe('Linked Tabs', () => {
describe('on click', () => {
it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(window.history, 'replaceState').and.callFake(() => {});
+ const historySpy = jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
const linkedTabs = new LinkedTabs({
action: 'show',
diff --git a/spec/javascripts/close_reopen_report_toggle_spec.js b/spec/frontend/close_reopen_report_toggle_spec.js
index 04a7ae7f429..f6b5e4bed87 100644
--- a/spec/javascripts/close_reopen_report_toggle_spec.js
+++ b/spec/frontend/close_reopen_report_toggle_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable jasmine/no-unsafe-spy */
-
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import DropLab from '~/droplab/drop_lab';
@@ -10,7 +8,7 @@ describe('CloseReopenReportToggle', () => {
const button = {};
let commentTypeToggle;
- beforeEach(function() {
+ beforeEach(() => {
commentTypeToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
@@ -18,22 +16,24 @@ describe('CloseReopenReportToggle', () => {
});
});
- it('sets .dropdownTrigger', function() {
+ it('sets .dropdownTrigger', () => {
expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
});
- it('sets .dropdownList', function() {
+ it('sets .dropdownList', () => {
expect(commentTypeToggle.dropdownList).toBe(dropdownList);
});
- it('sets .button', function() {
+ it('sets .button', () => {
expect(commentTypeToggle.button).toBe(button);
});
});
describe('initDroplab', () => {
let closeReopenReportToggle;
- const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
+ const dropdownList = {
+ querySelector: jest.fn(),
+ };
const dropdownTrigger = {};
const button = {};
const reopenItem = {};
@@ -41,8 +41,8 @@ describe('CloseReopenReportToggle', () => {
const config = {};
beforeEach(() => {
- spyOn(DropLab.prototype, 'init');
- dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
+ jest.spyOn(DropLab.prototype, 'init').mockImplementation(() => {});
+ dropdownList.querySelector.mockReturnValueOnce(reopenItem).mockReturnValueOnce(closeItem);
closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
@@ -50,7 +50,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
+ jest.spyOn(closeReopenReportToggle, 'setConfig').mockReturnValue(config);
closeReopenReportToggle.initDroplab();
});
@@ -63,7 +63,7 @@ describe('CloseReopenReportToggle', () => {
});
it('sets .droplab', () => {
- expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
+ expect(closeReopenReportToggle.droplab).toEqual(expect.any(Object));
});
it('calls .setConfig', () => {
@@ -74,7 +74,7 @@ describe('CloseReopenReportToggle', () => {
expect(DropLab.prototype.init).toHaveBeenCalledWith(
dropdownTrigger,
dropdownList,
- jasmine.any(Array),
+ expect.any(Array),
config,
);
});
@@ -84,7 +84,9 @@ describe('CloseReopenReportToggle', () => {
let closeReopenReportToggle;
const dropdownList = {};
const dropdownTrigger = {};
- const button = jasmine.createSpyObj('button', ['blur']);
+ const button = {
+ blur: jest.fn(),
+ };
const isClosed = true;
beforeEach(() => {
@@ -94,7 +96,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'toggleButtonType');
+ jest.spyOn(closeReopenReportToggle, 'toggleButtonType').mockImplementation(() => {});
closeReopenReportToggle.updateButton(isClosed);
});
@@ -114,10 +116,18 @@ describe('CloseReopenReportToggle', () => {
const dropdownTrigger = {};
const button = {};
const isClosed = true;
- const showItem = jasmine.createSpyObj('showItem', ['click']);
+ const showItem = {
+ click: jest.fn(),
+ };
const hideItem = {};
- showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
- hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
+ showItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
+ hideItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
@@ -126,7 +136,7 @@ describe('CloseReopenReportToggle', () => {
button,
});
- spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
+ jest.spyOn(closeReopenReportToggle, 'getButtonTypes').mockReturnValue([showItem, hideItem]);
closeReopenReportToggle.toggleButtonType(isClosed);
});
@@ -182,8 +192,14 @@ describe('CloseReopenReportToggle', () => {
describe('setDisable', () => {
let closeReopenReportToggle;
const dropdownList = {};
- const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
- const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
+ const dropdownTrigger = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
+ const button = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
beforeEach(() => {
closeReopenReportToggle = new CloseReopenReportToggle({
diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/frontend/commit_merge_requests_spec.js
index 82968e028d1..82968e028d1 100644
--- a/spec/javascripts/commit_merge_requests_spec.js
+++ b/spec/frontend/commit_merge_requests_spec.js
diff --git a/spec/javascripts/commits_spec.js b/spec/frontend/commits_spec.js
index 28b89157bd3..42bd37570b1 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -15,7 +15,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
- spyOn(Pager, 'init').and.stub();
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
commitsList = new CommitsList(25);
});
@@ -56,14 +56,14 @@ describe('Commits List', () => {
beforeEach(() => {
commitsList.searchField.val('');
- spyOn(window.history, 'replaceState').and.stub();
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
mock = new MockAdapter(axios);
mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
html: '<li>Result</li>',
});
- ajaxSpy = spyOn(axios, 'get').and.callThrough();
+ ajaxSpy = jest.spyOn(axios, 'get');
});
afterEach(() => {
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index a814952faab..a814952faab 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/frontend/diff_comments_store_spec.js
index a6d363ce88e..82588b1ee7b 100644
--- a/spec/javascripts/diff_comments_store_spec.js
+++ b/spec/frontend/diff_comments_store_spec.js
@@ -1,4 +1,3 @@
-/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown */
/* global CommentsStore */
import '~/diff_notes/models/discussion';
@@ -34,7 +33,7 @@ describe('New discussion', () => {
createDiscussion();
createDiscussion(2);
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
expect(Object.keys(discussion.notes).length).toBe(2);
});
@@ -71,7 +70,7 @@ describe('Delete discussion', () => {
createDiscussion(2);
expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+ expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
CommentsStore.delete('a', 1);
CommentsStore.delete('a', 2);
@@ -102,27 +101,27 @@ describe('Discussion resolved', () => {
});
it('is resolved with single note', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
expect(discussion.isResolved()).toBe(true);
});
it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2, false);
expect(discussion.isResolved()).toBe(false);
});
it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2);
expect(discussion.isResolved()).toBe(true);
});
it('resolve all notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2, false);
discussion.resolveAllNotes();
@@ -131,7 +130,7 @@ describe('Discussion resolved', () => {
});
it('unresolve all notes', () => {
- const discussion = CommentsStore.state['a'];
+ const discussion = CommentsStore.state.a;
createDiscussion(2);
discussion.unResolveAllNotes();
diff --git a/spec/javascripts/emoji_spec.js b/spec/frontend/emoji_spec.js
index 25bc95e0dd6..25bc95e0dd6 100644
--- a/spec/javascripts/emoji_spec.js
+++ b/spec/frontend/emoji_spec.js
diff --git a/spec/javascripts/flash_spec.js b/spec/frontend/flash_spec.js
index 39ca4eedb69..fa7c1904339 100644
--- a/spec/javascripts/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -38,9 +38,7 @@ describe('Flash', () => {
it('sets transition style', () => {
hideFlash(el);
- expect(el.style['transition-property']).toBe('opacity');
-
- expect(el.style['transition-duration']).toBe('0.15s');
+ expect(el.style.transition).toBe('opacity 0.15s');
});
it('sets opacity style', () => {
@@ -53,8 +51,7 @@ describe('Flash', () => {
hideFlash(el, false);
expect(el.style.opacity).toBe('');
-
- expect(el.style.transition).toBe('');
+ expect(el.style.transition).toBeFalsy();
});
it('removes element after transitionend', () => {
@@ -67,7 +64,7 @@ describe('Flash', () => {
});
it('calls event listener callback once', () => {
- spyOn(el, 'remove').and.callThrough();
+ jest.spyOn(el, 'remove');
document.body.appendChild(el);
hideFlash(el);
@@ -75,7 +72,7 @@ describe('Flash', () => {
el.dispatchEvent(new Event('transitionend'));
el.dispatchEvent(new Event('transitionend'));
- expect(el.remove.calls.count()).toBe(1);
+ expect(el.remove.mock.calls.length).toBe(1);
});
});
@@ -195,7 +192,7 @@ describe('Flash', () => {
it('calls actionConfig clickHandler on click', () => {
const actionConfig = {
title: 'test',
- clickHandler: jasmine.createSpy('actionConfig'),
+ clickHandler: jest.fn(),
};
flash('test', 'alert', document, actionConfig);
@@ -226,7 +223,7 @@ describe('Flash', () => {
flashEl.querySelector('.js-close-icon').click();
- setTimeout(() => {
+ setImmediate(() => {
expect(document.querySelector('.flash')).toBeNull();
done();
diff --git a/spec/javascripts/issuable_spec.js b/spec/frontend/issuable_spec.js
index 4d57bfb1b33..63c1fda2fb4 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/frontend/issuable_spec.js
@@ -50,10 +50,10 @@ describe('Issuable', () => {
});
it('should send request to reset email token', done => {
- spyOn(axios, 'put').and.callThrough();
+ jest.spyOn(axios, 'put');
document.querySelector('.incoming-email-token-reset').click();
- setTimeout(() => {
+ setImmediate(() => {
expect(axios.put).toHaveBeenCalledWith('foo');
expect($('#issuable_email').val()).toBe('testing123');
diff --git a/spec/frontend/landing_spec.js b/spec/frontend/landing_spec.js
new file mode 100644
index 00000000000..448d8ee2e81
--- /dev/null
+++ b/spec/frontend/landing_spec.js
@@ -0,0 +1,184 @@
+import Cookies from 'js-cookie';
+import Landing from '~/landing';
+
+describe('Landing', () => {
+ const test = {};
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ test.landingElement = {};
+ test.dismissButton = {};
+ test.cookieName = 'cookie_name';
+
+ test.landing = new Landing(test.landingElement, test.dismissButton, test.cookieName);
+ });
+
+ it('should set .landing', () => {
+ expect(test.landing.landingElement).toBe(test.landingElement);
+ });
+
+ it('should set .cookieName', () => {
+ expect(test.landing.cookieName).toBe(test.cookieName);
+ });
+
+ it('should set .dismissButton', () => {
+ expect(test.landing.dismissButton).toBe(test.dismissButton);
+ });
+
+ it('should set .eventWrapper', () => {
+ expect(test.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ test.isDismissed = false;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should call .isDismissed', () => {
+ expect(test.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', () => {
+ expect(test.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', test.isDismissed);
+ });
+
+ it('should call .addEvents', () => {
+ expect(test.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', () => {
+ beforeEach(() => {
+ test.isDismissed = true;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ test.landing.isDismissed.mockClear();
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should not call .addEvents', () => {
+ expect(test.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ addEventListener: jest.fn(),
+ };
+ test.eventWrapper = {};
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(test.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', () => {
+ expect(test.eventWrapper.dismissLanding).toEqual(expect.any(Function));
+ });
+
+ it('should call .addEventListener', () => {
+ expect(test.dismissButton.addEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('removeEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ removeEventListener: jest.fn(),
+ };
+ test.eventWrapper = { dismissLanding: () => {} };
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(test.landing);
+ });
+
+ it('should call .removeEventListener', () => {
+ expect(test.dismissButton.removeEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('dismissLanding', () => {
+ beforeEach(() => {
+ test.landingElement = {
+ classList: {
+ add: jest.fn(),
+ },
+ };
+ test.cookieName = 'cookie_name';
+ test.landing = { landingElement: test.landingElement, cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'set').mockImplementation(() => {});
+
+ Landing.prototype.dismissLanding.call(test.landing);
+ });
+
+ it('should call .classList.add', () => {
+ expect(test.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', () => {
+ expect(Cookies.set).toHaveBeenCalledWith(test.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', () => {
+ beforeEach(() => {
+ test.cookieName = 'cookie_name';
+ test.landing = { cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'get').mockReturnValue('true');
+
+ test.isDismissed = Landing.prototype.isDismissed.call(test.landing);
+ });
+
+ it('should call Cookies.get', () => {
+ expect(Cookies.get).toHaveBeenCalledWith(test.cookieName);
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof test.isDismissed).toEqual('boolean');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
new file mode 100644
index 00000000000..c14cba3a62b
--- /dev/null
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -0,0 +1,40 @@
+import downloader from '~/lib/utils/downloader';
+
+describe('Downloader', () => {
+ let a;
+
+ beforeEach(() => {
+ a = { click: jest.fn() };
+ jest.spyOn(document, 'createElement').mockImplementation(() => a);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when inline file content is provided', () => {
+ const fileData = 'inline content';
+ const fileName = 'test.csv';
+
+ it('uses the data urls to download the file', () => {
+ downloader({ fileName, fileData });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(`data:text/plain;base64,${fileData}`);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when an endpoint is provided', () => {
+ const url = 'https://gitlab.com/test.csv';
+ const fileName = 'test.csv';
+
+ it('uses the endpoint to download the file', () => {
+ downloader({ fileName, url });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(url);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 1906ad7c6ed..6cb7821b341 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -111,6 +111,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
+ <!---->
+
<empty-state-stub
clusterspath="/path/to/clusters"
documentationpath="/path/to/docs"
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index fb7fb24a341..f8c9bd56721 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -518,8 +518,10 @@ describe('Dashboard Panel', () => {
});
it('emits the `expand` event', () => {
- findExpandBtn().vm.$emit('click');
+ const preventDefault = jest.fn();
+ findExpandBtn().vm.$emit('click', { preventDefault });
expect(wrapper.emitted('expand')).toHaveLength(1);
+ expect(preventDefault).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 78553999705..ff8d75d7693 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -18,7 +18,12 @@ import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils';
+import {
+ setupStoreWithDashboard,
+ setMetricResult,
+ setupStoreWithData,
+ setupStoreWithVariable,
+} from '../store_utils';
import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
import createFlash from '~/flash';
@@ -381,6 +386,20 @@ describe('Dashboard', () => {
});
});
+ describe('variables section', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithVariable(wrapper.vm.$store);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows the variables section', () => {
+ expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
+ });
+ });
+
describe('single panel expands to "full screen" mode', () => {
const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
new file mode 100644
index 00000000000..7271beea50a
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -0,0 +1,120 @@
+import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlFormInput } from '@gitlab/ui';
+import VariablesSection from '~/monitoring/components/variables_section.vue';
+import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
+import { createStore } from '~/monitoring/stores';
+import * as types from '~/monitoring/stores/mutation_types';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ mergeUrlParams: jest.fn(),
+}));
+
+describe('Metrics dashboard/variables section component', () => {
+ let store;
+ let wrapper;
+ const sampleVariables = {
+ label1: 'pod',
+ label2: 'main',
+ };
+
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(VariablesSection, {
+ store,
+ });
+ };
+
+ const findAllFormInputs = () => wrapper.findAll(GlFormInput);
+ const getInputAt = i => findAllFormInputs().at(i);
+
+ beforeEach(() => {
+ store = createStore();
+
+ store.state.monitoringDashboard.showEmptyState = false;
+ });
+
+ it('does not show the variables section', () => {
+ createShallowWrapper();
+ const allInputs = findAllFormInputs();
+
+ expect(allInputs).toHaveLength(0);
+ });
+
+ it('shows the variables section', () => {
+ createShallowWrapper();
+ wrapper.vm.$store.commit(
+ `monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`,
+ sampleVariables,
+ );
+
+ return wrapper.vm.$nextTick(() => {
+ const allInputs = findAllFormInputs();
+
+ expect(allInputs).toHaveLength(Object.keys(sampleVariables).length);
+ });
+ });
+
+ describe('when changing the variable inputs', () => {
+ const fetchDashboardData = jest.fn();
+ const setVariableData = jest.fn();
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ state: {
+ showEmptyState: false,
+ promVariables: sampleVariables,
+ },
+ actions: {
+ fetchDashboardData,
+ setVariableData,
+ },
+ },
+ },
+ });
+
+ createShallowWrapper();
+ });
+
+ it('merges the url params and refreshes the dashboard when a form input is blurred', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.element.value = 'POD';
+ firstInput.vm.$emit('input');
+ firstInput.trigger('blur');
+
+ expect(setVariableData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+
+ it('merges the url params and refreshes the dashboard when a form input has received an enter key press', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.element.value = 'POD';
+ firstInput.vm.$emit('input');
+ firstInput.trigger('keyup.enter');
+
+ expect(setVariableData).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(sampleVariables, window.location.href);
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+
+ it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
+ const firstInput = getInputAt(0);
+
+ firstInput.vm.$emit('input');
+ firstInput.trigger('keyup.enter');
+
+ expect(setVariableData).not.toHaveBeenCalled();
+ expect(mergeUrlParams).not.toHaveBeenCalled();
+ expect(updateHistory).not.toHaveBeenCalled();
+ expect(fetchDashboardData).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index f040876b832..e9622071aeb 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -323,4 +323,31 @@ describe('Monitoring store Getters', () => {
expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
});
});
+
+ describe('getCustomVariablesArray', () => {
+ let state;
+ const sampleVariables = {
+ label1: 'pod',
+ };
+
+ beforeEach(() => {
+ state = {
+ promVariables: {},
+ };
+ });
+
+ it('transforms the promVariables object to an array in the [variable, variable_value] format', () => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](state, sampleVariables);
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual(['label1', 'pod']);
+ });
+
+ it('transforms the promVariables object to an empty array when no keys are present', () => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](state, {});
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index dd0deef486f..e6564f5e329 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -369,13 +369,25 @@ describe('Monitoring mutations', () => {
it('stores an empty variables array when no custom variables are given', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, {});
- expect(stateCopy.promVariables).toEqual([]);
+ expect(stateCopy.promVariables).toEqual({});
});
it('stores variables in the key key_value format in the array', () => {
mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
- expect(stateCopy.promVariables).toEqual(['pod', 'POD', 'stage', 'main%20ops']);
+ expect(stateCopy.promVariables).toEqual({ pod: 'POD', stage: 'main ops' });
+ });
+ });
+
+ describe('UPDATE_VARIABLE_DATA', () => {
+ beforeEach(() => {
+ mutations[types.SET_PROM_QUERY_VARIABLES](stateCopy, { pod: 'POD' });
+ });
+
+ it('sets a new value for an existing key', () => {
+ mutations[types.UPDATE_VARIABLE_DATA](stateCopy, { pod: 'new pod' });
+
+ expect(stateCopy.promVariables).toEqual({ pod: 'new pod' });
});
});
});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index d764a79ccc3..e5b36bf5ad4 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -23,6 +23,12 @@ export const setupStoreWithDashboard = $store => {
);
};
+export const setupStoreWithVariable = $store => {
+ $store.commit(`monitoringDashboard/${types.SET_PROM_QUERY_VARIABLES}`, {
+ label1: 'pod',
+ });
+};
+
export const setupStoreWithData = $store => {
setupStoreWithDashboard($store);
diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 381be82697e..381be82697e 100644
--- a/spec/javascripts/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
diff --git a/spec/javascripts/pipelines_spec.js b/spec/frontend/pipelines_spec.js
index 6d4d634c575..6d4d634c575 100644
--- a/spec/javascripts/pipelines_spec.js
+++ b/spec/frontend/pipelines_spec.js
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 2c5d91a45bc..2c5d91a45bc 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
diff --git a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
index c85d2cbccc6..971a81a826d 100644
--- a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
@@ -32,6 +32,12 @@ describe Resolvers::AlertManagementAlertResolver do
it { is_expected.to contain_exactly(alert_1) }
end
+ context 'finding by status' do
+ let(:args) { { status: [Types::AlertManagement::StatusEnum.values['IGNORED'].value] } }
+
+ it { is_expected.to contain_exactly(alert_2) }
+ end
+
describe 'sorting' do
# Other sorting examples in spec/finders/alert_management/alerts_finder_spec.rb
context 'when sorting by events count' do
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
deleted file mode 100644
index bffef8fc64f..00000000000
--- a/spec/javascripts/landing_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import Cookies from 'js-cookie';
-import Landing from '~/landing';
-
-describe('Landing', function() {
- describe('class constructor', function() {
- beforeEach(function() {
- this.landingElement = {};
- this.dismissButton = {};
- this.cookieName = 'cookie_name';
-
- this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
- });
-
- it('should set .landing', function() {
- expect(this.landing.landingElement).toBe(this.landingElement);
- });
-
- it('should set .cookieName', function() {
- expect(this.landing.cookieName).toBe(this.cookieName);
- });
-
- it('should set .dismissButton', function() {
- expect(this.landing.dismissButton).toBe(this.dismissButton);
- });
-
- it('should set .eventWrapper', function() {
- expect(this.landing.eventWrapper).toEqual({});
- });
- });
-
- describe('toggle', function() {
- beforeEach(function() {
- this.isDismissed = false;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should call .isDismissed', function() {
- expect(this.landing.isDismissed).toHaveBeenCalled();
- });
-
- it('should call .classList.toggle', function() {
- expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
- });
-
- it('should call .addEvents', function() {
- expect(this.landing.addEvents).toHaveBeenCalled();
- });
-
- describe('if isDismissed is true', function() {
- beforeEach(function() {
- this.isDismissed = true;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- this.landing.isDismissed.calls.reset();
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should not call .addEvents', function() {
- expect(this.landing.addEvents).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('addEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
- this.eventWrapper = {};
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- dismissLanding: () => {},
- };
-
- Landing.prototype.addEvents.call(this.landing);
- });
-
- it('should set .eventWrapper.dismissLanding', function() {
- expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
- });
-
- it('should call .addEventListener', function() {
- expect(this.dismissButton.addEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('removeEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
- this.eventWrapper = { dismissLanding: () => {} };
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- };
-
- Landing.prototype.removeEvents.call(this.landing);
- });
-
- it('should call .removeEventListener', function() {
- expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('dismissLanding', function() {
- beforeEach(function() {
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
- this.cookieName = 'cookie_name';
- this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
-
- spyOn(Cookies, 'set');
-
- Landing.prototype.dismissLanding.call(this.landing);
- });
-
- it('should call .classList.add', function() {
- expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
- });
-
- it('should call Cookies.set', function() {
- expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
- });
- });
-
- describe('isDismissed', function() {
- beforeEach(function() {
- this.cookieName = 'cookie_name';
- this.landing = { cookieName: this.cookieName };
-
- spyOn(Cookies, 'get').and.returnValue('true');
-
- this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
- });
-
- it('should call Cookies.get', function() {
- expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
- });
-
- it('should return a boolean', function() {
- expect(typeof this.isDismissed).toEqual('boolean');
- });
- });
-});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ead71631e33..9e822ad51c2 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -11,6 +11,7 @@ issues:
- resource_label_events
- resource_weight_events
- resource_milestone_events
+- resource_state_events
- sent_notifications
- sentry_issue
- label_links
@@ -119,6 +120,7 @@ merge_requests:
- notes
- resource_label_events
- resource_milestone_events
+- resource_state_events
- label_links
- labels
- last_edited_by
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 8c0f1016cac..c82e1617f7d 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -123,14 +123,31 @@ describe AlertManagement::Alert do
it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
end
- describe '.for_iid' do
+ describe 'scopes' do
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
- let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_3) { create(:alert_management_alert, :ignored, project: project) }
+
+ describe '.for_iid' do
+ subject { AlertManagement::Alert.for_iid(alert_1.iid) }
+
+ it { is_expected.to match_array(alert_1) }
+ end
+
+ describe '.for_status' do
+ let(:status) { AlertManagement::Alert::STATUSES[:resolved] }
- subject { AlertManagement::Alert.for_iid(alert_1.iid) }
+ subject { AlertManagement::Alert.for_status(status) }
- it { is_expected.to match_array(alert_1) }
+ it { is_expected.to match_array(alert_2) }
+
+ context 'with multiple statuses' do
+ let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
+
+ it { is_expected.to match_array([alert_2, alert_3]) }
+ end
+ end
end
describe '.for_fingerprint' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 458dce027a8..4f53b6b4418 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -749,7 +749,7 @@ describe Ci::Pipeline, :mailer do
allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
end
- it "is incldued with value 'true'" do
+ it "is included with value 'true'" do
expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index cc7dffb93d2..7bbf421fc8a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -19,6 +19,8 @@ describe Issue do
it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
it { is_expected.to have_one(:alert_management_alert) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
describe 'versions.most_recent' do
it 'returns the most recent version' do
@@ -38,6 +40,8 @@ describe Issue do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 0642d96efa5..e8025fef877 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,6 +20,8 @@ describe MergeRequest do
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
it { is_expected.to belong_to(:milestone) }
it { is_expected.to belong_to(:sprint) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -178,6 +180,8 @@ describe MergeRequest do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
new file mode 100644
index 00000000000..986a13cbd0d
--- /dev/null
+++ b/spec/models/resource_state_event_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceStateEvent, type: :model do
+ subject { build(:resource_state_event, issue: issue) }
+
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
+
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 73dc9d8c63e..5637a56b2a4 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -7,11 +7,25 @@ describe JwtController do
let(:service_class) { double(new: service) }
let(:service_name) { 'test' }
let(:parameters) { { service: service_name } }
+ let(:log_output) { StringIO.new }
+ let(:logger) do
+ Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } }
+ end
+ let(:log_data) { Gitlab::Json.parse(log_output.string) }
before do
+ Lograge.logger = logger
+
stub_const('JwtController::SERVICES', service_name => service_class)
end
+ shared_examples 'user logging' do
+ it 'logs username and ID' do
+ expect(log_data['username']).to eq(user.username)
+ expect(log_data['user_id']).to eq(user.id)
+ end
+ end
+
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@@ -37,14 +51,17 @@ describe JwtController do
end
context 'using CI token' do
- let(:build) { create(:ci_build, :running) }
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, user: user) }
let(:project) { build.project }
let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it { expect(service_class).to have_received(:new).with(project, nil, ActionController::Parameters.new(parameters).permit!) }
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+
+ it_behaves_like 'user logging'
end
context 'project with disabled CI' do
@@ -57,8 +74,23 @@ describe JwtController do
it { expect(response).to have_gitlab_http_status(:unauthorized) }
end
+ context 'using deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
+ let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
+
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
+
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it 'does not log a user' do
+ expect(log_data.keys).not_to include(%w(username user_id))
+ end
+ end
+
context 'using personal access tokens' do
- let(:user) { create(:user) }
let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
@@ -74,6 +106,7 @@ describe JwtController do
end
it_behaves_like 'rejecting a blocked user'
+ it_behaves_like 'user logging'
end
end
@@ -104,6 +137,8 @@ describe JwtController do
end
it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+
+ it_behaves_like 'user logging'
end
context 'when user has 2FA enabled' do
diff --git a/spec/services/issuable/clone/attributes_rewriter_spec.rb b/spec/services/issuable/clone/attributes_rewriter_spec.rb
index 8cb37917239..fb520f828fa 100644
--- a/spec/services/issuable/clone/attributes_rewriter_spec.rb
+++ b/spec/services/issuable/clone/attributes_rewriter_spec.rb
@@ -114,5 +114,27 @@ describe Issuable::Clone::AttributesRewriter do
expect(event.state).to eq(expected_attrs[:state])
end
end
+
+ context 'with existing state events' do
+ let!(:event1) { create(:resource_state_event, issue: original_issue, state: 'opened') }
+ let!(:event2) { create(:resource_state_event, issue: original_issue, state: 'closed') }
+ let!(:event3) { create(:resource_state_event, issue: original_issue, state: 'reopened') }
+
+ it 'copies existing state events as expected' do
+ subject.execute
+
+ state_events = new_issue.reload.resource_state_events
+ expect(state_events.size).to eq(3)
+
+ expect_state_event(state_events.first, issue: new_issue, state: 'opened')
+ expect_state_event(state_events.second, issue: new_issue, state: 'closed')
+ expect_state_event(state_events.third, issue: new_issue, state: 'reopened')
+ end
+
+ def expect_state_event(event, expected_attrs)
+ expect(event.issue_id).to eq(expected_attrs[:issue]&.id)
+ expect(event.state).to eq(expected_attrs[:state])
+ end
+ end
end
end
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
index 2a0c12b010d..938e72aa0f0 100644
--- a/spec/workers/incident_management/process_alert_worker_spec.rb
+++ b/spec/workers/incident_management/process_alert_worker_spec.rb
@@ -54,7 +54,7 @@ describe IncidentManagement::ProcessAlertWorker do
.with(alert_management_alert_id)
.and_return(alert)
- allow(Gitlab::GitLogger).to receive(:warn).and_call_original
+ allow(Gitlab::AppLogger).to receive(:warn).and_call_original
end
context 'when alert can be updated' do
@@ -65,7 +65,7 @@ describe IncidentManagement::ProcessAlertWorker do
it 'does not write a warning to log' do
subject
- expect(Gitlab::GitLogger).not_to have_received(:warn)
+ expect(Gitlab::AppLogger).not_to have_received(:warn)
end
end
@@ -83,7 +83,7 @@ describe IncidentManagement::ProcessAlertWorker do
it 'writes a worning to log' do
subject
- expect(Gitlab::GitLogger).to have_received(:warn).with(
+ expect(Gitlab::AppLogger).to have_received(:warn).with(
message: 'Cannot link an Issue with Alert',
issue_id: new_issue.id,
alert_id: alert_management_alert_id,
diff --git a/spec/workers/merge_request_mergeability_check_worker_spec.rb b/spec/workers/merge_request_mergeability_check_worker_spec.rb
index 2331664215f..8909af1f685 100644
--- a/spec/workers/merge_request_mergeability_check_worker_spec.rb
+++ b/spec/workers/merge_request_mergeability_check_worker_spec.rb
@@ -25,5 +25,16 @@ describe MergeRequestMergeabilityCheckWorker do
subject.perform(merge_request.id)
end
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id] }
+
+ it 'is mergeable' do
+ subject
+
+ expect(merge_request).to be_mergeable
+ end
+ end
end
end