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  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 } - 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 + it 'shows the page' do + subject expect(response).to have_gitlab_http_status(:ok) expect(response).to render_template(:index) end context 'when user is unauthorized' do - it 'redirects to the login page' do - make_request + let(:user) { anonymous } - expect(response).to redirect_to(new_user_session_path) + 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(: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 '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 + + 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 = `
`; + 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 => + `