summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/alert_handler.js22
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue8
-rw-r--r--app/assets/javascripts/incidents/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js1
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js7
-rw-r--r--app/assets/javascripts/repository/index.js56
-rw-r--r--app/assets/javascripts/repository/queries/path_last_commit.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js6
-rw-r--r--app/controllers/projects/incidents_controller.rb44
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb2
-rw-r--r--app/graphql/types/merge_request_type.rb4
-rw-r--r--app/helpers/nav_helper.rb3
-rw-r--r--app/helpers/startupjs_helper.rb13
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request_context_commit.rb4
-rw-r--r--app/services/design_management/generate_image_versions_service.rb3
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb2
-rw-r--r--app/views/layouts/_startup_js.html.haml22
-rw-r--r--app/views/projects/ci/builds/_build.html.haml10
-rw-r--r--app/views/projects/incidents/_new_branch.html.haml1
-rw-r--r--app/views/projects/incidents/show.html.haml1
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--changelogs/unreleased/alipniagov-update-wh-to-8-49.yml5
-rw-r--r--changelogs/unreleased/fix-wrong-scope-in-approved-by.yml5
-rw-r--r--changelogs/unreleased/gaga5lala-227175-carrierwave-error-handle.yml5
-rw-r--r--changelogs/unreleased/gitlab_buttons_ci_builds.yml5
-rw-r--r--config/feature_flags/development/issues_incident_details.yml7
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/routes/project.rb2
-rw-r--r--db/fixtures/development/29_instance_statistics.rb6
-rw-r--r--doc/administration/reference_architectures/25k_users.md2
-rw-r--r--doc/user/application_security/security_dashboard/index.md8
-rw-r--r--doc/user/compliance/license_compliance/index.md5
-rw-r--r--doc/user/permissions.md10
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb7
-rw-r--r--lib/gitlab/usage_data.rb5
-rw-r--r--spec/controllers/projects/incidents_controller_spec.rb111
-rw-r--r--spec/features/incidents/incident_details_spec.rb32
-rw-r--r--spec/frontend/alert_handler_spec.js65
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js5
-rw-r--r--spec/helpers/startupjs_helper_spec.rb20
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb11
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb16
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb35
-rw-r--r--spec/services/design_management/generate_image_versions_service_spec.rb51
-rw-r--r--spec/support/helpers/graphql_helpers.rb3
50 files changed, 605 insertions, 157 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5b06b4f3f5b..b912e995966 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-3f5e218def93024f3aafe590c22cd1b29f744105
+cf1ceffbf8056281864432c8f472140fb83f5949
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 7b7e68c163e..610d619cfcb 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.48.0
+8.49.0
diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js
index 8fffb61d1dd..26b0142f6a2 100644
--- a/app/assets/javascripts/alert_handler.js
+++ b/app/assets/javascripts/alert_handler.js
@@ -1,13 +1,21 @@
-// This allows us to dismiss alerts that we've migrated from bootstrap
-// Note: This ONLY works on alerts that are created on page load
+// This allows us to dismiss alerts and banners that we've migrated from bootstrap
+// Note: This ONLY works on elements that are created on page load
// You can follow this effort in the following epic
// https://gitlab.com/groups/gitlab-org/-/epics/4070
export default function initAlertHandler() {
- const ALERT_SELECTOR = '.gl-alert';
- const CLOSE_SELECTOR = '.gl-alert-dismiss';
+ const DISMISSIBLE_SELECTORS = ['.gl-alert', '.gl-banner'];
+ const DISMISS_LABEL = '[aria-label="Dismiss"]';
+ const DISMISS_CLASS = '.gl-alert-dismiss';
- const dismissAlert = ({ target }) => target.closest(ALERT_SELECTOR).remove();
- const closeButtons = document.querySelectorAll(`${ALERT_SELECTOR} ${CLOSE_SELECTOR}`);
- closeButtons.forEach(alert => alert.addEventListener('click', dismissAlert));
+ DISMISSIBLE_SELECTORS.forEach(selector => {
+ const elements = document.querySelectorAll(selector);
+ elements.forEach(element => {
+ const button = element.querySelector(DISMISS_LABEL) || element.querySelector(DISMISS_CLASS);
+ if (!button) {
+ return;
+ }
+ button.addEventListener('click', () => element.remove());
+ });
+ });
}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 5688d2b5575..78eb828fd19 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -19,6 +19,7 @@ import Api from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import { urlParamsToObject } from '~/lib/utils/common_utils';
@@ -40,6 +41,7 @@ import {
TH_CREATED_AT_TEST_ID,
TH_SEVERITY_TEST_ID,
TH_PUBLISHED_TEST_ID,
+ INCIDENT_DETAILS_PATH,
} from '../constants';
const tdClass =
@@ -111,6 +113,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
inject: [
'projectPath',
'newIssuePath',
@@ -332,7 +335,10 @@ export default {
return Boolean(assignees.nodes?.length);
},
navigateToIncidentDetails({ iid }) {
- return visitUrl(joinPaths(this.issuePath, iid));
+ const path = this.glFeatures.issuesIncidentDetails
+ ? joinPaths(this.issuePath, INCIDENT_DETAILS_PATH)
+ : this.issuePath;
+ return visitUrl(joinPaths(path, iid));
},
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo;
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 40f8814d9da..797439495e3 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -38,3 +38,4 @@ export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
+export const INCIDENT_DETAILS_PATH = 'incident';
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index bcf302cc262..28b624168d5 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -44,6 +44,7 @@ export const checkPageAndAction = (page, action) => {
return pagePath === page && actionPath === action;
};
+export const isInIncidentPage = () => checkPageAndAction('issues', 'incident');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
new file mode 100644
index 00000000000..540b0dd1de8
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -0,0 +1,7 @@
+import initRelatedIssues from '~/related_issues';
+import initShow from '../../issues/show';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initShow();
+ initRelatedIssues();
+});
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 65da8f70b40..1d6749654f5 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -12,12 +12,17 @@ import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
+import PathLastCommitQuery from './queries/path_last_commit.query.graphql';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
+ const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
+ const matches = window.location.href.match(pathRegex);
+
+ const currentRoutePath = matches ? matches[1] : '';
apolloProvider.clients.defaultClient.cache.writeData({
data: {
@@ -29,6 +34,43 @@ export default function setupVueRepositoryList() {
},
});
+ const initLastCommitApp = () =>
+ new Vue({
+ el: document.getElementById('js-last-commit'),
+ router,
+ apolloProvider,
+ render(h) {
+ return h(LastCommit, {
+ props: {
+ currentPath: this.$route.params.path,
+ },
+ });
+ },
+ });
+
+ if (window.gl.startup_graphql_calls) {
+ const query = window.gl.startup_graphql_calls.find(
+ call => call.operationName === 'pathLastCommit',
+ );
+ query.fetchCall
+ .then(res => res.json())
+ .then(res => {
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: PathLastCommitQuery,
+ data: res.data,
+ variables: {
+ projectPath,
+ ref,
+ path: currentRoutePath,
+ },
+ });
+ })
+ .catch(() => {})
+ .finally(() => initLastCommitApp());
+ } else {
+ initLastCommitApp();
+ }
+
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
});
@@ -77,20 +119,6 @@ export default function setupVueRepositoryList() {
});
}
- // eslint-disable-next-line no-new
- new Vue({
- el: document.getElementById('js-last-commit'),
- router,
- apolloProvider,
- render(h) {
- return h(LastCommit, {
- props: {
- currentPath: this.$route.params.path,
- },
- });
- },
- });
-
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset;
diff --git a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql b/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
index 51f3f790a5d..d845f7c6224 100644
--- a/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
+++ b/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
@@ -1,8 +1,12 @@
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
project(fullPath: $projectPath) {
+ __typename
repository {
+ __typename
tree(path: $path, ref: $ref) {
+ __typename
lastCommit {
+ __typename
sha
title
titleHtml
@@ -13,15 +17,20 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
authorName
authorGravatar
author {
+ __typename
name
avatarUrl
webPath
}
signatureHtml
pipelines(ref: $ref, first: 1) {
+ __typename
edges {
+ __typename
node {
+ __typename
detailedStatus {
+ __typename
detailsPath
icon
tooltip
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index ffc7f2c07ba..a25a7b0b2fe 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -14,7 +14,7 @@ import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptio
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
-import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
+import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
@@ -51,7 +51,7 @@ function mountAssigneesComponent(mediator) {
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
- issuableType: isInIssuePage() ? 'issue' : 'merge_request',
+ issuableType: isInIssuePage() || isInIncidentPage() ? 'issue' : 'merge_request',
},
}),
});
@@ -158,7 +158,7 @@ function mountLockComponent() {
const initialData = JSON.parse(dataNode.innerHTML);
let importStore;
- if (isInIssuePage()) {
+ if (isInIssuePage() || isInIncidentPage()) {
importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
({ store }) => store,
);
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index c0760f01db2..1c915842e61 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -1,8 +1,52 @@
# frozen_string_literal: true
class Projects::IncidentsController < Projects::ApplicationController
+ include IssuableActions
+ include Gitlab::Utils::StrongMemoize
+
before_action :authorize_read_issue!
+ before_action :check_feature_flag, only: [:show]
+ before_action :load_incident, only: [:show]
+
+ before_action do
+ push_frontend_feature_flag(:issues_incident_details, @project)
+ end
def index
end
+
+ private
+
+ def incident
+ strong_memoize(:incident) do
+ incident_finder
+ .execute
+ .inc_relations_for_view
+ .iid_in(params[:id])
+ .without_order
+ .first
+ end
+ end
+
+ def load_incident
+ @issue = incident # needed by rendered view
+ return render_404 unless can?(current_user, :read_issue, incident)
+
+ @noteable = incident
+ @note = incident.project.notes.new(noteable: issuable)
+ end
+
+ alias_method :issuable, :incident
+
+ def incident_finder
+ IssuesFinder.new(current_user, project_id: @project.id, issue_types: :incident)
+ end
+
+ def serializer
+ IssueSerializer.new(current_user: current_user, project: incident.project)
+ end
+
+ def check_feature_flag
+ render_404 unless Feature.enabled?(:issues_incident_details, @project)
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c5a50349144..319a5183429 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -239,7 +239,7 @@ class Projects::IssuesController < Projects::ApplicationController
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
- @issuable = @noteable = @issue ||= @project.issues.includes(author: :status).where(iid: params[:id]).reorder(nil).take!
+ @issuable = @noteable = @issue ||= @project.issues.inc_relations_for_view.iid_in(params[:id]).without_order.take!
@note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 0c01efd4f9a..3d845c8e9df 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -40,7 +40,7 @@ module ResolvesMergeRequests
author: [:author],
merged_at: [:metrics],
commit_count: [:metrics],
- approved_by: [:approver_users],
+ approved_by: [:approved_by_users],
milestone: [:milestone],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
}
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 56c88491684..573818b1b7a 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -174,10 +174,6 @@ module Types
def commit_count
object&.metrics&.commits_count
end
-
- def approvers
- object.approver_users
- end
end
end
Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 578c7ae7923..3c757a4ef26 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -55,7 +55,8 @@ module NavHelper
current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') ||
current_path?('milestones#show') ||
- current_path?('issues#designs')
+ current_path?('issues#designs') ||
+ current_path?('incidents#show')
end
def admin_monitoring_nav_links
diff --git a/app/helpers/startupjs_helper.rb b/app/helpers/startupjs_helper.rb
new file mode 100644
index 00000000000..da95cfe03ee
--- /dev/null
+++ b/app/helpers/startupjs_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module StartupjsHelper
+ def page_startup_graphql_calls
+ @graphql_startup_calls
+ end
+
+ def add_page_startup_graphql_call(query, variables = {})
+ @graphql_startup_calls ||= []
+ query_str = File.read(File.join(Rails.root, "app/assets/javascripts/#{query}.query.graphql"))
+ @graphql_startup_calls << { query: query_str, variables: variables }
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index a49296e711f..ad2a6981b71 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -126,6 +126,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
+ scope :inc_relations_for_view, -> { includes(author: :status) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index a2982a5dd73..59cc82cfaf5 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -22,8 +22,8 @@ class MergeRequestContextCommit < ApplicationRecord
end
# create MergeRequestContextCommit by given commit sha and it's diff file record
- def self.bulk_insert(*args)
- Gitlab::Database.bulk_insert('merge_request_context_commits', *args) # rubocop:disable Gitlab/BulkInsert
+ def self.bulk_insert(rows, **args)
+ Gitlab::Database.bulk_insert('merge_request_context_commits', rows, **args) # rubocop:disable Gitlab/BulkInsert
end
def to_commit
diff --git a/app/services/design_management/generate_image_versions_service.rb b/app/services/design_management/generate_image_versions_service.rb
index 213aac164ff..e56d163c461 100644
--- a/app/services/design_management/generate_image_versions_service.rb
+++ b/app/services/design_management/generate_image_versions_service.rb
@@ -48,6 +48,9 @@ module DesignManagement
# Store and process the file
action.image_v432x230.store!(raw_file)
action.save!
+ rescue CarrierWave::IntegrityError => e
+ Gitlab::ErrorTracking.log_exception(e, project_id: project.id, design_id: action.design_id, version_id: action.version_id)
+ log_error(e.message)
rescue CarrierWave::UploadError => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id, design_id: action.design_id, version_id: action.version_id)
log_error(e.message)
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index b185ab592ff..c84074039ea 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -56,7 +56,7 @@ module Issuable
end
def copy_resource_weight_events
- return unless original_entity.respond_to?(:resource_weight_events)
+ return unless both_respond_to?(:resource_weight_events)
copy_events(ResourceWeightEvent.table_name, original_entity.resource_weight_events) do |event|
event.attributes
diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml
index 33c759b7a7c..f312e00c394 100644
--- a/app/views/layouts/_startup_js.html.haml
+++ b/app/views/layouts/_startup_js.html.haml
@@ -1,9 +1,11 @@
-- return unless page_startup_api_calls.present?
+- return unless page_startup_api_calls.present? || page_startup_graphql_calls.present?
= javascript_tag nonce: true do
:plain
var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json};
+ gl.startup_graphql_calls = #{page_startup_graphql_calls.to_json};
+
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
// fetch won’t send cookies in older browsers, unless you set the credentials init option.
@@ -14,3 +16,21 @@
};
});
}
+ if (gl.startup_graphql_calls && window.fetch) {
+ const url = `#{api_graphql_url}`
+
+ const opts = {
+ method: "POST",
+ headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
+ };
+
+ gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
+ operationName: call.query.match(/^query (.+)\(/)[1],
+ fetchCall: fetch(url, {
+ ...opts,
+ credentials: 'same-origin',
+ body: JSON.stringify(call)
+ })
+ }))
+ }
+
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 90db2eb3518..b01665daff4 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -101,11 +101,11 @@
= sprite_icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
- = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn btn-build' do
+ = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn gl-button btn-build' do
= sprite_icon('close')
- elsif job.scheduled?
.btn-group
- .btn.btn-default{ disabled: true }
+ .btn.gl-button.btn-default{ disabled: true }
= sprite_icon('planning')
%time.js-remaining-time{ datetime: job.scheduled_at.utc.iso8601 }
= duration_in_numbers(job.execute_in)
@@ -113,17 +113,17 @@
= link_to play_project_job_path(job.project, job, return_to: request.original_url),
method: :post,
title: s_('DelayedJobs|Start now'),
- class: 'btn btn-default btn-build has-tooltip',
+ class: 'btn gl-button btn-default btn-build has-tooltip',
data: { confirm: confirmation_message } do
= sprite_icon('play')
= link_to unschedule_project_job_path(job.project, job, return_to: request.original_url),
method: :post,
title: s_('DelayedJobs|Unschedule'),
- class: 'btn btn-default btn-build has-tooltip' do
+ class: 'btn gl-button btn-default btn-build has-tooltip' do
= sprite_icon('time-out')
- elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job)
- = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn btn-build' do
+ = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn gl-button btn-build' do
= custom_icon('icon_play')
- elsif job.retryable?
= link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do
diff --git a/app/views/projects/incidents/_new_branch.html.haml b/app/views/projects/incidents/_new_branch.html.haml
new file mode 100644
index 00000000000..f250fbc4b8b
--- /dev/null
+++ b/app/views/projects/incidents/_new_branch.html.haml
@@ -0,0 +1 @@
+= render 'projects/issues/new_branch'
diff --git a/app/views/projects/incidents/show.html.haml b/app/views/projects/incidents/show.html.haml
new file mode 100644
index 00000000000..b0ddc85df5d
--- /dev/null
+++ b/app/views/projects/incidents/show.html.haml
@@ -0,0 +1 @@
+= render template: 'projects/issues/show'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 3dd12a7b641..3f5904391ad 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,3 +1,5 @@
+- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
+- add_page_startup_graphql_call('repository/queries/path_last_commit', { projectPath: @project.full_path, ref: current_ref, currentRoutePath: current_route_path })
- breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
diff --git a/changelogs/unreleased/alipniagov-update-wh-to-8-49.yml b/changelogs/unreleased/alipniagov-update-wh-to-8-49.yml
new file mode 100644
index 00000000000..8eff6983c67
--- /dev/null
+++ b/changelogs/unreleased/alipniagov-update-wh-to-8-49.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Workhorse to v8.49.0
+merge_request: 43999
+author:
+type: other
diff --git a/changelogs/unreleased/fix-wrong-scope-in-approved-by.yml b/changelogs/unreleased/fix-wrong-scope-in-approved-by.yml
new file mode 100644
index 00000000000..b2d9106737c
--- /dev/null
+++ b/changelogs/unreleased/fix-wrong-scope-in-approved-by.yml
@@ -0,0 +1,5 @@
+---
+title: Fix approvedBy filed in MR GraphQL API
+merge_request: 43705
+author:
+type: fixed
diff --git a/changelogs/unreleased/gaga5lala-227175-carrierwave-error-handle.yml b/changelogs/unreleased/gaga5lala-227175-carrierwave-error-handle.yml
new file mode 100644
index 00000000000..8e134642cb0
--- /dev/null
+++ b/changelogs/unreleased/gaga5lala-227175-carrierwave-error-handle.yml
@@ -0,0 +1,5 @@
+---
+title: Log CarrierWave::IntegrityError without sending exception
+merge_request: 43750
+author: gaga5lala
+type: other
diff --git a/changelogs/unreleased/gitlab_buttons_ci_builds.yml b/changelogs/unreleased/gitlab_buttons_ci_builds.yml
new file mode 100644
index 00000000000..ce5f78e1e46
--- /dev/null
+++ b/changelogs/unreleased/gitlab_buttons_ci_builds.yml
@@ -0,0 +1,5 @@
+---
+title: Apply GitLab UI button styles to HAML buttons app/views/projects/ci/builds
+merge_request: 43728
+author: Andrei Kyrnich @kyrnich
+type: other
diff --git a/config/feature_flags/development/issues_incident_details.yml b/config/feature_flags/development/issues_incident_details.yml
new file mode 100644
index 00000000000..d9f7d16312f
--- /dev/null
+++ b/config/feature_flags/development/issues_incident_details.yml
@@ -0,0 +1,7 @@
+---
+name: issues_incident_details
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43459
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/257842
+type: development
+group: group::health
+default_enabled: false
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7ff4e3bf7da..fb024b7ba2a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -20,6 +20,8 @@ en:
token: "Grafana HTTP API Token"
grafana_url: "Grafana API URL"
grafana_enabled: "Grafana integration enabled"
+ service_desk_setting:
+ project_key: "Project name suffix"
user/user_detail:
job_title: 'Job title'
user/user_detail:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f6de73975a2..c8fd5dc7e9e 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -311,6 +311,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :incidents, only: [:index]
+ get 'issues/incident/:id' => 'incidents#show', as: :issues_incident
+
namespace :error_tracking do
resources :projects, only: :index
end
diff --git a/db/fixtures/development/29_instance_statistics.rb b/db/fixtures/development/29_instance_statistics.rb
index c4af13d0f4d..e4ef0f26be0 100644
--- a/db/fixtures/development/29_instance_statistics.rb
+++ b/db/fixtures/development/29_instance_statistics.rb
@@ -6,9 +6,9 @@ Gitlab::Seeder.quiet do
model_class = Analytics::InstanceStatistics::Measurement
recorded_at = Date.today
- # Insert random counts for the last 10 weeks
- measurements = 10.times.flat_map do
- recorded_at = (recorded_at - 1.week).end_of_week.end_of_day - 5.minutes
+ # Insert random counts for the last 60 days
+ measurements = 60.times.flat_map do
+ recorded_at = (recorded_at - 1.day).end_of_day - 5.minutes
model_class.identifiers.map do |_, id|
{
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index 3e5357130a8..8fec26425e2 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -21,7 +21,7 @@ full list of reference architectures, see
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
-| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index b71220532e4..f21f53e6895 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -174,14 +174,18 @@ thousands of vulnerabilities. Don't close the page until the download finishes.
The fields in the export include:
+- Group Name
+- Project Name
- Scanner Type
- Scanner Name
- Status
-- Name
+- Vulnerability
- Details
+- Additional Info
- Severity
- [CVE](https://cve.mitre.org/)
-- Additional Info
+- [CWE](https://cwe.mitre.org/)
+- Other Identifiers
![Export vulnerabilities](img/instance_security_dashboard_export_csv_v13_4.png)
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 7689be00c3b..382d536be74 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -127,6 +127,11 @@ is used to detect the languages/frameworks and in turn analyzes the licenses.
The License Compliance settings can be changed through [environment variables](#available-variables) by using the
[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
+### When License Compliance runs
+
+When using the GitLab `License-Scanning.gitlab-ci.yml` template, the License Compliance job doesn't
+wait for other stages to complete.
+
### Available variables
License Compliance can be configured using environment variables.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 7fdd4eb1cc8..3c11de76b81 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -386,6 +386,16 @@ with the permissions described on the documentation on [auditor users permission
[Read more about Auditor users.](../administration/auditor_users.md)
+## Users with minimal access **(PREMIUM ONLY)**
+
+>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40942) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
+
+Administrators can add members with a "minimal access" role to a parent group. Such users don't
+automatically have access to projects and subgroups underneath. To support such access, administrators must explicitly add these "minimal access" users to the specific subgroups/projects.
+
+Users with minimal access can list the group in the UI and through the API. However, they cannot see
+details such as projects or subgroups. They do not have access to the group's page or list any of itssubgroups or projects.
+
## Project features
Project features like wiki and issues can be hidden from users depending on
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
index d438728ff35..577f59911f5 100644
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb
@@ -94,6 +94,8 @@ module Gitlab
[order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr]
elsif ordering_by_similarity?(order_value)
['similarity', order_value.direction, order_value.expr]
+ elsif ordering_by_case?(order_value)
+ [order_value.expr.case.name.to_s, order_value.direction, order_value.expr]
else
[order_value.expr.name, order_value.direction, nil]
end
@@ -108,6 +110,11 @@ module Gitlab
def ordering_by_similarity?(order_value)
Gitlab::Database::SimilarityScore.order_by_similarity?(order_value)
end
+
+ # determine if ordering using CASE
+ def ordering_by_case?(order_value)
+ order_value.expr.is_a?(Arel::Nodes::Case)
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 212643901f2..7fe8c596b13 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -167,8 +167,7 @@ module Gitlab
user_preferences_usage,
ingress_modsecurity_usage,
container_expiration_policies_usage,
- service_desk_counts,
- snowplow_event_counts
+ service_desk_counts
).tap do |data|
data[:snippets] = data[:personal_snippets] + data[:project_snippets]
end
@@ -176,7 +175,7 @@ module Gitlab
end
# rubocop: enable Metrics/AbcSize
- def snowplow_event_counts(time_period: {})
+ def snowplow_event_counts(time_period)
return {} unless report_snowplow_events?
{
diff --git a/spec/controllers/projects/incidents_controller_spec.rb b/spec/controllers/projects/incidents_controller_spec.rb
index f2c2f3be10c..1b47f9f6abf 100644
--- a/spec/controllers/projects/incidents_controller_spec.rb
+++ b/spec/controllers/projects/incidents_controller_spec.rb
@@ -3,42 +3,127 @@
require 'spec_helper'
RSpec.describe Projects::IncidentsController do
- let_it_be(:project) { create(:project) }
+ let_it_be_with_refind(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
+ let_it_be(:anonymous) { nil }
before_all do
project.add_guest(guest)
project.add_developer(developer)
end
+ before do
+ sign_in(user) if user
+ end
+
+ subject { make_request }
+
+ shared_examples 'not found' do
+ include_examples 'returning response status', :not_found
+ end
+
+ shared_examples 'login required' do
+ it 'redirects to the login page' do
+ subject
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
describe 'GET #index' do
def make_request
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ get :index, params: project_params
end
- it 'shows the page for users with guest role' do
- sign_in(guest)
- make_request
+ let(:user) { developer }
+
+ it 'shows the page' do
+ subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
- it 'shows the page for users with developer role' do
- sign_in(developer)
- make_request
+ context 'when user is unauthorized' do
+ let(:user) { anonymous }
+
+ it_behaves_like 'login required'
+ end
+
+ context 'when user is a guest' do
+ let(:user) { guest }
+
+ it 'shows the page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ end
+ end
+ end
+
+ describe 'GET #show' do
+ def make_request
+ get :show, params: project_params(id: resource)
+ end
+
+ let_it_be(:resource) { create(:incident, project: project) }
+ let(:user) { developer }
+
+ it 'renders incident page' do
+ subject
expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:index)
+ expect(response).to render_template(:show)
+
+ expect(assigns(:incident)).to be_present
+ expect(assigns(:incident).author.association(:status)).to be_loaded
+ expect(assigns(:issue)).to be_present
+ expect(assigns(:noteable)).to eq(assigns(:incident))
end
- context 'when user is unauthorized' do
- it 'redirects to the login page' do
- make_request
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(issues_incident_details: false)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'with non existing id' do
+ let(:resource) { non_existing_record_id }
+
+ it_behaves_like 'not found'
+ end
- expect(response).to redirect_to(new_user_session_path)
+ context 'for issue' do
+ let_it_be(:resource) { create(:issue, project: project) }
+
+ it_behaves_like 'not found'
+ end
+
+ context 'when user is a guest' do
+ let(:user) { guest }
+
+ it 'shows the page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
end
end
+
+ context 'when unauthorized' do
+ let(:user) { anonymous }
+
+ it_behaves_like 'login required'
+ end
+ end
+
+ private
+
+ def project_params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace, project_id: project)
end
end
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
new file mode 100644
index 00000000000..6db767dbddb
--- /dev/null
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Incident details', :js do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:incident) { create(:incident, project: project, author: developer) }
+
+ before_all do
+ project.add_developer(developer)
+ end
+
+ before do
+ sign_in(developer)
+
+ visit project_issues_incident_path(project, incident)
+ wait_for_requests
+ end
+
+ context 'when a developer+ displays the incident' do
+ it 'shows the incident' do
+ page.within('.issuable-details') do
+ expect(find('h2')).to have_content(incident.title)
+ end
+ end
+
+ it 'does not show design management' do
+ expect(page).not_to have_selector('.js-design-management')
+ end
+ end
+end
diff --git a/spec/frontend/alert_handler_spec.js b/spec/frontend/alert_handler_spec.js
index ba2f4f24aa5..0cee28112a8 100644
--- a/spec/frontend/alert_handler_spec.js
+++ b/spec/frontend/alert_handler_spec.js
@@ -2,18 +2,26 @@ import { setHTMLFixture } from 'helpers/fixtures';
import initAlertHandler from '~/alert_handler';
describe('Alert Handler', () => {
- const ALERT_SELECTOR = 'gl-alert';
- const CLOSE_SELECTOR = 'gl-alert-dismiss';
- const ALERT_HTML = `<div class="${ALERT_SELECTOR}"><button class="${CLOSE_SELECTOR}">Dismiss</button></div>`;
+ const ALERT_CLASS = 'gl-alert';
+ const BANNER_CLASS = 'gl-banner';
+ const DISMISS_CLASS = 'gl-alert-dismiss';
+ const DISMISS_LABEL = 'Dismiss';
- const findFirstAlert = () => document.querySelector(`.${ALERT_SELECTOR}`);
- const findAllAlerts = () => document.querySelectorAll(`.${ALERT_SELECTOR}`);
- const findFirstCloseButton = () => document.querySelector(`.${CLOSE_SELECTOR}`);
+ const generateHtml = parentClass =>
+ `<div class="${parentClass}">
+ <button aria-label="${DISMISS_LABEL}">Dismiss</button>
+ </div>`;
+
+ const findFirstAlert = () => document.querySelector(`.${ALERT_CLASS}`);
+ const findFirstBanner = () => document.querySelector(`.${BANNER_CLASS}`);
+ const findAllAlerts = () => document.querySelectorAll(`.${ALERT_CLASS}`);
+ const findFirstDismissButton = () => document.querySelector(`[aria-label="${DISMISS_LABEL}"]`);
+ const findFirstDismissButtonByClass = () => document.querySelector(`.${DISMISS_CLASS}`);
describe('initAlertHandler', () => {
describe('with one alert', () => {
beforeEach(() => {
- setHTMLFixture(ALERT_HTML);
+ setHTMLFixture(generateHtml(ALERT_CLASS));
initAlertHandler();
});
@@ -22,14 +30,14 @@ describe('Alert Handler', () => {
});
it('should dismiss the alert on click', () => {
- findFirstCloseButton().click();
+ findFirstDismissButton().click();
expect(findFirstAlert()).not.toExist();
});
});
describe('with two alerts', () => {
beforeEach(() => {
- setHTMLFixture(ALERT_HTML + ALERT_HTML);
+ setHTMLFixture(generateHtml(ALERT_CLASS) + generateHtml(ALERT_CLASS));
initAlertHandler();
});
@@ -38,9 +46,46 @@ describe('Alert Handler', () => {
});
it('should dismiss only one alert on click', () => {
- findFirstCloseButton().click();
+ findFirstDismissButton().click();
expect(findAllAlerts()).toHaveLength(1);
});
});
+
+ describe('with a dismissible banner', () => {
+ beforeEach(() => {
+ setHTMLFixture(generateHtml(BANNER_CLASS));
+ initAlertHandler();
+ });
+
+ it('should render the banner', () => {
+ expect(findFirstBanner()).toExist();
+ });
+
+ it('should dismiss the banner on click', () => {
+ findFirstDismissButton().click();
+ expect(findFirstBanner()).not.toExist();
+ });
+ });
+
+ // Dismiss buttons *should* have the correct aria labels, but some of them won't
+ // because legacy code isn't always a11y compliant.
+ // This tests that the fallback for the incorrectly labelled buttons works.
+ describe('with a mislabelled dismiss button', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div class="${ALERT_CLASS}">
+ <button class="${DISMISS_CLASS}">Dismiss</button>
+ </div>`);
+ initAlertHandler();
+ });
+
+ it('should render the banner', () => {
+ expect(findFirstAlert()).toExist();
+ });
+
+ it('should dismiss the banner on click', () => {
+ findFirstDismissButtonByClass().click();
+ expect(findFirstAlert()).not.toExist();
+ });
+ });
});
});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 012eaffcb7c..1b98d488854 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -28,10 +28,10 @@ import mockFilters from '../mocks/incidents_filter.json';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
- joinPaths: jest.fn().mockName('joinPaths'),
- mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
- setUrlParams: jest.fn().mockName('setUrlParams'),
- updateHistory: jest.fn().mockName('updateHistory'),
+ joinPaths: jest.fn(),
+ mergeUrlParams: jest.fn(),
+ setUrlParams: jest.fn(),
+ updateHistory: jest.fn(),
}));
describe('Incidents List', () => {
@@ -81,12 +81,13 @@ describe('Incidents List', () => {
newIssuePath,
incidentTemplateName,
incidentType,
- issuePath: '/project/isssues',
+ issuePath: '/project/issues',
publishedAvailable: true,
emptyListSvgPath,
textQuery: '',
authorUsernamesQuery: '',
assigneeUsernamesQuery: '',
+ issuesIncidentDetails: false,
},
stubs: {
GlButton: true,
@@ -182,13 +183,6 @@ describe('Incidents List', () => {
expect(src).toBe(avatarUrl);
});
- it('contains a link to the issue details', () => {
- findTableRows()
- .at(0)
- .trigger('click');
- expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/isssues/`, mockIncidents[0].iid));
- });
-
it('renders a closed icon for closed incidents', () => {
expect(findClosedIcon().length).toBe(
mockIncidents.filter(({ state }) => state === 'closed').length,
@@ -199,6 +193,30 @@ describe('Incidents List', () => {
it('renders severity per row', () => {
expect(findSeverity().length).toBe(mockIncidents.length);
});
+
+ it('contains a link to the issue details page', () => {
+ findTableRows()
+ .at(0)
+ .trigger('click');
+ expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/issues/`, mockIncidents[0].iid));
+ });
+
+ it('contains a link to the incident details page', async () => {
+ beforeEach(() =>
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount: {} },
+ loading: false,
+ provide: { glFeatures: { issuesIncidentDetails: true } },
+ }),
+ );
+
+ findTableRows()
+ .at(0)
+ .trigger('click');
+ expect(visitUrl).toHaveBeenCalledWith(
+ joinPaths(`/project/issues/incident`, mockIncidents[0].iid),
+ );
+ });
});
describe('Create Incident', () => {
@@ -218,11 +236,10 @@ describe('Incidents List', () => {
);
});
- it('sets button loading on click', () => {
+ it('sets button loading on click', async () => {
findCreateIncidentBtn().vm.$emit('click');
- return wrapper.vm.$nextTick().then(() => {
- expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
- });
+ await wrapper.vm.$nextTick();
+ expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
});
it("doesn't show the button when list is empty", () => {
@@ -254,51 +271,47 @@ describe('Incidents List', () => {
});
describe('prevPage', () => {
- it('returns prevPage button', () => {
+ it('returns prevPage button', async () => {
findPagination().vm.$emit('input', 3);
- return wrapper.vm.$nextTick(() => {
- expect(
- findPagination()
- .findAll('.page-item')
- .at(0)
- .text(),
- ).toBe('Prev');
- });
+ await wrapper.vm.$nextTick();
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(0)
+ .text(),
+ ).toBe('Prev');
});
- it('returns prevPage number', () => {
+ it('returns prevPage number', async () => {
findPagination().vm.$emit('input', 3);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.prevPage).toBe(2);
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.prevPage).toBe(2);
});
- it('returns 0 when it is the first page', () => {
+ it('returns 0 when it is the first page', async () => {
findPagination().vm.$emit('input', 1);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.prevPage).toBe(0);
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.prevPage).toBe(0);
});
});
describe('nextPage', () => {
- it('returns nextPage button', () => {
+ it('returns nextPage button', async () => {
findPagination().vm.$emit('input', 3);
- return wrapper.vm.$nextTick(() => {
- expect(
- findPagination()
- .findAll('.page-item')
- .at(1)
- .text(),
- ).toBe('Next');
- });
+ await wrapper.vm.$nextTick();
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(1)
+ .text(),
+ ).toBe('Next');
});
- it('returns nextPage number', () => {
+ it('returns nextPage number', async () => {
mountComponent({
data: {
incidents: {
@@ -312,17 +325,15 @@ describe('Incidents List', () => {
});
findPagination().vm.$emit('input', 1);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.nextPage).toBe(2);
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.nextPage).toBe(2);
});
- it('returns `null` when currentPage is already last page', () => {
+ it('returns `null` when currentPage is already last page', async () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.nextPage).toBeNull();
- });
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.nextPage).toBeNull();
});
});
diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
index 79d5129b5ef..6c0ba8afede 100644
--- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
@@ -4,7 +4,7 @@ import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
-describe('MemberList', () => {
+describe('UserAvatar', () => {
let wrapper;
const { user } = memberMock;
@@ -68,11 +68,8 @@ describe('MemberList', () => {
describe('badges', () => {
it.each`
member | badgeText
- ${{ ...memberMock, usingLicense: true }} | ${'Is using seat'}
${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'}
- ${{ ...memberMock, groupSso: true }} | ${'SAML'}
- ${{ ...memberMock, groupManagedAccount: true }} | ${'Managed Account'}
`('renders the "$badgeText" badge', ({ member, badgeText }) => {
createComponent({ member });
diff --git a/spec/helpers/startupjs_helper_spec.rb b/spec/helpers/startupjs_helper_spec.rb
new file mode 100644
index 00000000000..c23b71590b2
--- /dev/null
+++ b/spec/helpers/startupjs_helper_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe StartupjsHelper do
+ describe '#page_startup_graphql_calls' do
+ let(:query_location) { 'repository/queries/path_last_commit' }
+ let(:query_content) do
+ File.read(File.join(Rails.root, 'app/assets/javascripts', "#{query_location}.query.graphql"))
+ end
+
+ it 'returns an array containing GraphQL Page Startup Calls' do
+ helper.add_page_startup_graphql_call(query_location, { ref: 'foo' })
+
+ startup_graphql_calls = helper.page_startup_graphql_calls
+
+ expect(startup_graphql_calls).to include({ query: query_content, variables: { ref: 'foo' } })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
index 444c10074a0..77a8588e2cb 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
@@ -63,6 +63,17 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do
expect(order_list.first.sort_direction).to eq :desc
end
end
+
+ context 'when ordering by CASE', :aggregate_failuers do
+ let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) }
+
+ it 'assigns the right attribute name, named function, and direction' do
+ expect(order_list.count).to eq 1
+ expect(order_list.first.attribute_name).to eq 'pending_delete'
+ expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case)
+ expect(order_list.first.sort_direction).to eq :asc
+ end
+ end
end
describe '#validate_ordering' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 36cdd1558b8..57de6e49a08 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1213,6 +1213,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
describe '.snowplow_event_counts' do
+ let_it_be(:time_period) { { collector_tstamp: 8.days.ago..1.day.ago } }
+
context 'when self-monitoring project exists' do
let_it_be(:project) { create(:project) }
@@ -1225,14 +1227,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
stub_feature_flags(product_analytics: project)
create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote')
- create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 28.days.ago)
+ create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 2.days.ago)
+ create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 9.days.ago)
+
+ create(:product_analytics_event, project: project, se_category: 'foo', se_action: 'bar', collector_tstamp: 2.days.ago)
end
it 'returns promoted_issues for the time period' do
- expect(described_class.snowplow_event_counts[:promoted_issues]).to eq(2)
- expect(described_class.snowplow_event_counts(
- time_period: described_class.last_28_days_time_period(column: :collector_tstamp)
- )[:promoted_issues]).to eq(1)
+ expect(described_class.snowplow_event_counts(time_period)[:promoted_issues]).to eq(1)
end
end
@@ -1242,14 +1244,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
it 'returns an empty hash' do
- expect(described_class.snowplow_event_counts).to eq({})
+ expect(described_class.snowplow_event_counts(time_period)).to eq({})
end
end
end
context 'when self-monitoring project does not exist' do
it 'returns an empty hash' do
- expect(described_class.snowplow_event_counts).to eq({})
+ expect(described_class.snowplow_event_counts(time_period)).to eq({})
end
end
end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 5d4276f47ca..40fec6ba068 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -53,16 +53,37 @@ RSpec.describe 'getting an issue list for a project' do
context 'when limiting the number of results' do
let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- "issues(first: 1) { #{fields} }"
- )
+ <<~GQL
+ query($path: ID!, $n: Int) {
+ project(fullPath: $path) {
+ issues(first: $n) { #{fields} }
+ }
+ }
+ GQL
+ end
+
+ let(:issue_limit) { 1 }
+ let(:variables) do
+ { path: project.full_path, n: issue_limit }
end
it_behaves_like 'a working graphql query' do
before do
- post_graphql(query, current_user: current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
+ end
+
+ it 'only returns N issues' do
+ expect(issues_data.size).to eq(issue_limit)
+ end
+ end
+
+ context 'no limit is provided' do
+ let(:issue_limit) { nil }
+
+ it 'returns all issues' do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ expect(issues_data.size).to be > 1
end
end
@@ -71,7 +92,7 @@ RSpec.describe 'getting an issue list for a project' do
# Newest first, we only want to see the newest checked
expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
- post_graphql(query, current_user: current_user)
+ post_graphql(query, current_user: current_user, variables: variables)
end
end
diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb
index 631eec97e5a..749030af97d 100644
--- a/spec/services/design_management/generate_image_versions_service_spec.rb
+++ b/spec/services/design_management/generate_image_versions_service_spec.rb
@@ -52,25 +52,50 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do
end
context 'when an error is encountered when generating the image versions' do
- before do
- expect_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader|
- expect(uploader).to receive(:cache!).and_raise(CarrierWave::DownloadError, 'foo')
+ context "CarrierWave::IntegrityError" do
+ before do
+ expect_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader|
+ expect(uploader).to receive(:cache!).and_raise(CarrierWave::IntegrityError, 'foo')
+ end
+ end
+
+ it 'logs the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
+ instance_of(CarrierWave::IntegrityError),
+ project_id: project.id, version_id: version.id, design_id: version.designs.first.id
+ )
+
+ described_class.new(version).execute
end
- end
- it 'logs the error' do
- expect(Gitlab::AppLogger).to receive(:error).with('foo')
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error).with('foo')
- described_class.new(version).execute
+ described_class.new(version).execute
+ end
end
- it 'tracks the error' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
- instance_of(CarrierWave::DownloadError),
- project_id: project.id, version_id: version.id, design_id: version.designs.first.id
- )
+ context "CarrierWave::UploadError" do
+ before do
+ expect_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader|
+ expect(uploader).to receive(:cache!).and_raise(CarrierWave::UploadError, 'foo')
+ end
+ end
- described_class.new(version).execute
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error).with('foo')
+
+ described_class.new(version).execute
+ end
+
+ it 'tracks the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(CarrierWave::UploadError),
+ project_id: project.id, version_id: version.id, design_id: version.designs.first.id
+ )
+
+ described_class.new(version).execute
+ end
end
end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 5635ba3df05..e754a24417c 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -234,7 +234,8 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
- post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
+ params = { query: query, variables: variables&.to_json }
+ post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
def post_graphql_mutation(mutation, current_user: nil)