diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 15:10:05 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-01 15:10:05 +0000 |
commit | d57e27ef353787dac501d6970c546c9d86dd1f88 (patch) | |
tree | 89044194e81f95aaa15ace8a5d6a5b429179965f | |
parent | a27b8a5c104f492e4b0abac4c84385a615c4f6ba (diff) | |
download | gitlab-ce-d57e27ef353787dac501d6970c546c9d86dd1f88.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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) |