diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 6b403601d0d..b8214a51177 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -595,7 +595,6 @@ Gitlab/StrongMemoizeAttr: - 'lib/gitlab/ci/reports/accessibility_reports_comparer.rb' - 'lib/gitlab/ci/reports/codequality_reports_comparer.rb' - 'lib/gitlab/ci/reports/security/locations/base.rb' - - 'lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb' - 'lib/gitlab/ci/reports/test_reports_comparer.rb' - 'lib/gitlab/ci/reports/test_suite_comparer.rb' - 'lib/gitlab/ci/reports/test_suite_summary.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 0206346fa0a..cc2252ce579 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4147,7 +4147,6 @@ Layout/LineLength: - 'spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb' - 'spec/lib/gitlab/ci/reports/security/flag_spec.rb' - 'spec/lib/gitlab/ci/reports/security/scanner_spec.rb' - - 'spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb' - 'spec/lib/gitlab/ci/runner_upgrade_check_spec.rb' - 'spec/lib/gitlab/ci/status/bridge/factory_spec.rb' - 'spec/lib/gitlab/ci/status/build/manual_spec.rb' diff --git a/.rubocop_todo/layout/space_inside_parens.yml b/.rubocop_todo/layout/space_inside_parens.yml index 131a56976a6..de88cdc49bc 100644 --- a/.rubocop_todo/layout/space_inside_parens.yml +++ b/.rubocop_todo/layout/space_inside_parens.yml @@ -128,7 +128,6 @@ Layout/SpaceInsideParens: - 'spec/lib/gitlab/ci/parsers/security/common_spec.rb' - 'spec/lib/gitlab/ci/parsers_spec.rb' - 'spec/lib/gitlab/ci/pipeline/seed/build_spec.rb' - - 'spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb' - 'spec/lib/gitlab/ci/reports/test_suite_spec.rb' - 'spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb' - 'spec/lib/gitlab/ci/templates/MATLAB_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index d57f93a1719..a00cfe8e77a 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1730,7 +1730,6 @@ RSpec/ContextWording: - 'spec/lib/gitlab/ci/pipeline_object_hierarchy_spec.rb' - 'spec/lib/gitlab/ci/reports/reports_comparer_spec.rb' - 'spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb' - - 'spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb' - 'spec/lib/gitlab/ci/reports/test_suite_comparer_spec.rb' - 'spec/lib/gitlab/ci/runner_instructions_spec.rb' - 'spec/lib/gitlab/ci/runner_upgrade_check_spec.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index a6461e7b177..91c1999836e 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -490,7 +490,6 @@ Style/GuardClause: - 'lib/gitlab/ci/pipeline/expression/lexeme/base.rb' - 'lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb' - 'lib/gitlab/ci/reports/codequality_reports_comparer.rb' - - 'lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb' - 'lib/gitlab/ci/runner/backoff.rb' - 'lib/gitlab/ci/runner_upgrade_check.rb' - 'lib/gitlab/ci/trace.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 69d1e4c4105..7195c6dbe56 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -4fe33cae7dca4ca605d0f505743ba4aa861fa876 +e41cf4607486623e97e584533158cd4071beff31 diff --git a/Gemfile.checksum b/Gemfile.checksum index 88bb5924fcc..48d27c3d728 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -524,7 +524,7 @@ {"name":"rubocop-rails","version":"2.17.4","platform":"ruby","checksum":"8004149a14372d3d6cededd000357879fa7eb0421403a7a26bc717e2a98bbedb"}, {"name":"rubocop-rspec","version":"2.18.1","platform":"ruby","checksum":"41c6455630fc98b809ebca047413389e2b7e3f68975028365c07bfea878db5ee"}, {"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"}, -{"name":"ruby-magic","version":"0.5.4","platform":"ruby","checksum":"2c17b185130d10a83791f63a40baa358c4b138af37da3f4dab53690121c421d5"}, +{"name":"ruby-magic","version":"0.5.5","platform":"ruby","checksum":"d2cc5b6b719831c3108a4f8a62bf3314c1af6cb09c98e2b5a3f9509bf8814e6c"}, {"name":"ruby-progressbar","version":"1.11.0","platform":"ruby","checksum":"cc127db3866dc414ffccbf92928a241e585b3aa2b758a5563e74a6ee0f57d50a"}, {"name":"ruby-saml","version":"1.13.0","platform":"ruby","checksum":"d31cbdf5fb8fdd6aa3187e48dba3085cfeb751af30276a5739aa3659a66f069c"}, {"name":"ruby-statistics","version":"3.0.0","platform":"ruby","checksum":"610301370346931cb701e3a8d3d3e28eb65681162cae6066c0c11abf20efdc81"}, diff --git a/Gemfile.lock b/Gemfile.lock index 06b10c4867c..e41d13b8d8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1327,8 +1327,8 @@ GEM ruby-fogbugz (0.3.0) crack (~> 0.4) multipart-post (~> 2.0) - ruby-magic (0.5.4) - mini_portile2 (~> 2.6) + ruby-magic (0.5.5) + mini_portile2 (~> 2.8) ruby-progressbar (1.11.0) ruby-saml (1.13.0) nokogiri (>= 1.10.5) diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue index 5c9dded0d71..cc9912c40c1 100644 --- a/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue +++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue @@ -2,15 +2,22 @@ import { setUrlParams, redirectTo, queryToObject, updateHistory } from '~/lib/utils/url_utility'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import { FILTERED_SEARCH_TOKENS } from '~/admin/abuse_reports/constants'; +import { + FILTERED_SEARCH_TOKENS, + DEFAULT_SORT, + SORT_OPTIONS, + isValidSortKey, +} from '~/admin/abuse_reports/constants'; export default { name: 'AbuseReportsFilteredSearchBar', components: { FilteredSearchBar }, tokens: FILTERED_SEARCH_TOKENS, + sortOptions: SORT_OPTIONS, data() { return { initialFilterValue: [], + initialSortBy: DEFAULT_SORT, }; }, created() { @@ -24,6 +31,11 @@ export default { updateHistory({ url: setUrlParams(query), replace: true }); } + const sort = this.currentSortKey(); + if (sort) { + this.initialSortBy = query.sort; + } + const tokens = this.$options.tokens .filter((token) => query[token.type]) .map((token) => ({ @@ -37,8 +49,13 @@ export default { this.initialFilterValue = tokens; }, methods: { + currentSortKey() { + const { sort } = queryToObject(window.location.search); + + return isValidSortKey(sort) ? sort : undefined; + }, handleFilter(tokens) { - const params = tokens.reduce((accumulator, token) => { + let params = tokens.reduce((accumulator, token) => { const { type, value } = token; // We don't support filtering reports by search term for now @@ -52,8 +69,18 @@ export default { }; }, {}); + const sort = this.currentSortKey(); + if (sort) { + params = { ...params, sort }; + } + redirectTo(setUrlParams(params, window.location.href, true)); }, + handleSort(sort) { + const { page, ...query } = queryToObject(window.location.search); + + redirectTo(setUrlParams({ ...query, sort }, window.location.href, true)); + }, }, filteredSearchNamespace: 'abuse_reports', recentSearchesStorageKey: 'abuse_reports', @@ -67,6 +94,9 @@ export default { :recent-searches-storage-key="$options.recentSearchesStorageKey" :search-input-placeholder="__('Filter reports')" :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + :sort-options="$options.sortOptions" @onFilter="handleFilter" + @onSort="handleSort" /> diff --git a/app/assets/javascripts/admin/abuse_reports/constants.js b/app/assets/javascripts/admin/abuse_reports/constants.js index aa833f26df9..2d0712236f1 100644 --- a/app/assets/javascripts/admin/abuse_reports/constants.js +++ b/app/assets/javascripts/admin/abuse_reports/constants.js @@ -33,4 +33,30 @@ export const FILTERED_SEARCH_TOKEN_STATUS = { operators: OPERATORS_IS, }; +export const DEFAULT_SORT = 'created_at_desc'; + +export const SORT_OPTIONS = [ + { + id: 10, + title: __('Created date'), + sortDirection: { + descending: DEFAULT_SORT, + ascending: 'created_at_asc', + }, + }, + { + id: 20, + title: __('Updated date'), + sortDirection: { + descending: 'updated_at_desc', + ascending: 'updated_at_asc', + }, + }, +]; + +export const isValidSortKey = (key) => + SORT_OPTIONS.some( + (sort) => sort.sortDirection.ascending === key || sort.sortDirection.descending === key, + ); + export const FILTERED_SEARCH_TOKENS = [FILTERED_SEARCH_TOKEN_USER, FILTERED_SEARCH_TOKEN_STATUS]; diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue index 24d8370afdd..704b4ce9c8a 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/components/base.vue +++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue @@ -48,12 +48,13 @@ export default { 'selectedStageEvents', 'selectedStageError', 'stageCounts', - 'endpoints', 'features', 'createdBefore', 'createdAfter', 'pagination', 'hasNoAccessError', + 'groupPath', + 'namespace', ]), ...mapGetters(['pathNavigationData', 'filterParams']), isLoaded() { @@ -111,7 +112,8 @@ export default { }, dashboardsPath() { const { - endpoints: { groupPath, fullPath }, + namespace: { fullPath }, + groupPath, } = this; return this.showLinkToDashboard ? generateValueStreamsDashboardLink(groupPath, [fullPath]) @@ -166,7 +168,7 @@ export default {

{{ $options.i18n.pageTitle }}

{ @@ -18,7 +24,7 @@ export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { export const fetchValueStreamStages = ({ commit, state }) => { const { - endpoints: { fullPath }, + namespace: { fullPath }, selectedValueStream: { id }, } = state; commit(types.REQUEST_VALUE_STREAM_STAGES); @@ -41,7 +47,7 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => { export const fetchValueStreams = ({ commit, dispatch, state }) => { const { - endpoints: { fullPath }, + namespace: { fullPath }, } = state; commit(types.REQUEST_VALUE_STREAMS); @@ -180,7 +186,8 @@ export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => { commit(types.INITIALIZE_VSA, initialData); const { - endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' }, + groupPath, + namespace, selectedAuthor, selectedMilestone, selectedAssigneeList, @@ -189,10 +196,10 @@ export const initializeVsa = async ({ commit, dispatch }, initialData = {}) => { } = initialData; dispatch('filters/setEndpoints', { - labelsEndpoint: labelsPath, - milestonesEndpoint: milestonesPath, + labelsEndpoint: constructPathWithNamespace(namespace, LABELS_ENDPOINT), + milestonesEndpoint: constructPathWithNamespace(namespace, MILESTONES_ENDPOINT), groupEndpoint: groupPath, - projectEndpoint: fullPath, + projectEndpoint: namespace.fullPath, }); dispatch('filters/initialize', { diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/getters.js b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js index 30e0ac24ce2..f5ed922c602 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/store/getters.js +++ b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js @@ -15,7 +15,7 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage export const requestParams = (state) => { const { - endpoints: { fullPath }, + namespace: { fullPath }, selectedValueStream: { id: valueStreamId }, selectedStage: { id: stageId = null }, } = state; diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js index 8567529caf2..4af96fc96e3 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js +++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js @@ -1,15 +1,16 @@ import Vue from 'vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants'; import { formatMedianValues } from '../utils'; +import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants'; import * as types from './mutation_types'; export default { [types.INITIALIZE_VSA]( state, - { endpoints, features, createdBefore, createdAfter, pagination = {} }, + { groupPath, features, createdBefore, createdAfter, pagination = {}, namespace = {} }, ) { - state.endpoints = endpoints; + state.groupPath = groupPath; + state.namespace = namespace; state.createdBefore = createdBefore; state.createdAfter = createdAfter; state.features = features; diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js index 00dd2e53883..0c51656c59f 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js +++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js @@ -6,7 +6,11 @@ import { export default () => ({ id: null, features: {}, - endpoints: {}, + groupPath: {}, + namespace: { + name: null, + fullPath: null, + }, createdAfter: null, createdBefore: null, stages: [], diff --git a/app/assets/javascripts/analytics/cycle_analytics/utils.js b/app/assets/javascripts/analytics/cycle_analytics/utils.js index e5a2055c43a..9265ff952e0 100644 --- a/app/assets/javascripts/analytics/cycle_analytics/utils.js +++ b/app/assets/javascripts/analytics/cycle_analytics/utils.js @@ -1,5 +1,6 @@ import { parseSeconds } from '~/lib/utils/datetime_utility'; import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility'; +import { joinPaths } from '~/lib/utils/url_utility'; /** * Takes the stages and median data, combined with the selected stage, to build an @@ -91,25 +92,21 @@ const extractFeatures = (gon) => ({ * @returns {Object} - The initial data to load the app with */ export const buildCycleAnalyticsInitialData = ({ - fullPath, - requestPath, projectId, groupPath, - labelsPath, - milestonesPath, stage, createdAfter, createdBefore, + namespaceName, + namespaceFullPath, gon, } = {}) => { return { projectId: parseInt(projectId, 10), - endpoints: { - requestPath, - fullPath, - labelsPath, - milestonesPath, - groupPath: `groups/${groupPath}`, + groupPath: `groups/${groupPath}`, + namespace: { + name: namespaceName, + fullPath: namespaceFullPath, }, createdAfter: new Date(createdAfter), createdBefore: new Date(createdBefore), @@ -117,3 +114,6 @@ export const buildCycleAnalyticsInitialData = ({ features: extractFeatures(gon), }; }; + +export const constructPathWithNamespace = ({ fullPath }, endpoint) => + joinPaths('/', fullPath, endpoint); diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue index 6e072e12bd9..bdee6c5fe9a 100644 --- a/app/assets/javascripts/issues/show/components/description.vue +++ b/app/assets/javascripts/issues/show/components/description.vue @@ -322,7 +322,9 @@ export default { this.$emit('saveDescription', newDescription); }, renderTaskListItemActions() { - const taskListItems = this.$el.querySelectorAll?.('.task-list-item:not(.inapplicable)'); + const taskListItems = this.$el.querySelectorAll?.( + '.task-list-item:not(.inapplicable, table .task-list-item)', + ); taskListItems?.forEach((item) => { const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index 1f7728e440b..d774ad465c3 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -1,4 +1,5 @@ @@ -156,7 +166,7 @@ export default { v-bind="formFieldProps" ref="textarea" :value="markdown" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input markdown-area" dir="auto" :data-supports-quick-actions="supportsQuickActions" data-qa-selector="markdown_editor_form_field" diff --git a/app/finders/abuse_reports_finder.rb b/app/finders/abuse_reports_finder.rb index 1f313534552..f5ef507f4e6 100644 --- a/app/finders/abuse_reports_finder.rb +++ b/app/finders/abuse_reports_finder.rb @@ -3,6 +3,9 @@ class AbuseReportsFinder attr_reader :params, :reports + DEFAULT_SORT = 'created_at_desc' + ALLOWED_SORT = [DEFAULT_SORT, *%w[created_at_asc updated_at_desc updated_at_asc]].freeze + def initialize(params = {}) @params = params @reports = AbuseReport.all @@ -10,10 +13,9 @@ class AbuseReportsFinder def execute filter_reports + sort_reports - reports.with_order_id_desc - .with_users - .page(params[:page]) + reports.with_users.page(params[:page]) end private @@ -57,4 +59,16 @@ class AbuseReportsFinder @reports = @reports.by_user_id(params[:user_id]) end + + def sort_reports + if Feature.disabled?(:abuse_reports_list) + @reports = @reports.with_order_id_desc + return + end + + sort_by = params[:sort] + sort_by = DEFAULT_SORT unless sort_by.in?(ALLOWED_SORT) + + @reports = @reports.order_by(sort_by) + end end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb new file mode 100644 index 00000000000..f08de3c5d7e --- /dev/null +++ b/app/graphql/resolvers/analytics/cycle_analytics/base_issue_resolver.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Resolvers + module Analytics + module CycleAnalytics + class BaseIssueResolver < BaseResolver + type Types::Analytics::CycleAnalytics::MetricType, null: true + + argument :assignee_usernames, [GraphQL::Types::String], + required: false, + description: 'Usernames of users assigned to the issue.' + + argument :author_username, GraphQL::Types::String, + required: false, + description: 'Username of the author of the issue.' + + argument :milestone_title, GraphQL::Types::String, + required: false, + description: 'Milestone applied to the issue.' + + argument :label_names, [GraphQL::Types::String], + required: false, + description: 'Labels applied to the issue.' + + argument :from, Types::TimeType, + required: true, + description: 'Issues created after the date.' + + argument :to, Types::TimeType, + required: true, + description: 'Issues created before the date.' + + def finder_params + { project_id: object.project.id } + end + + # :project level: no customization, returning the original resolver + # :group level: add the project_ids argument + def self.[](context = :project) + case context + when :project + self + when :group + Class.new(self) do + argument :project_ids, [GraphQL::Types::ID], + required: false, + description: 'Project IDs within the group hierarchy.' + + define_method :finder_params do + { group_id: object.id, include_subgroups: true } + end + end + end + end + end + end + end +end diff --git a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb index 0631ac55857..fd20800ee16 100644 --- a/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb +++ b/app/graphql/resolvers/analytics/cycle_analytics/issue_count_resolver.rb @@ -1,35 +1,10 @@ # frozen_string_literal: true +# rubocop:disable Graphql/ResolverType (inherited from Resolvers::Analytics::CycleAnalytics::BaseIssueResolver) module Resolvers module Analytics module CycleAnalytics - class IssueCountResolver < BaseResolver - type Types::Analytics::CycleAnalytics::MetricType, null: true - - argument :assignee_usernames, [GraphQL::Types::String], - required: false, - description: 'Usernames of users assigned to the issue.' - - argument :author_username, GraphQL::Types::String, - required: false, - description: 'Username of the author of the issue.' - - argument :milestone_title, GraphQL::Types::String, - required: false, - description: 'Milestone applied to the issue.' - - argument :label_names, [GraphQL::Types::String], - required: false, - description: 'Labels applied to the issue.' - - argument :from, Types::TimeType, - required: true, - description: 'Issues created after the date.' - - argument :to, Types::TimeType, - required: true, - description: 'Issues created before the date.' - + class IssueCountResolver < BaseIssueResolver def resolve(**args) value = IssuesFinder .new(current_user, process_params(args)) @@ -55,31 +30,8 @@ module Resolvers params.merge(finder_params) end - - def finder_params - { project_id: object.project.id } - end - - # :project level: no customization, returning the original resolver - # :group level: add the project_ids argument - def self.[](context = :project) - case context - when :project - self - when :group - Class.new(self) do - argument :project_ids, [GraphQL::Types::ID], - required: false, - description: 'Project IDs within the group hierarchy.' - - define_method :finder_params do - { group_id: object.id, include_subgroups: true } - end - end - - end - end end end end end +# rubocop:enable Graphql/ResolverType diff --git a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb index 2645a86a9f8..c9a28767e11 100644 --- a/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb +++ b/app/graphql/types/analytics/cycle_analytics/flow_metrics.rb @@ -25,3 +25,6 @@ module Types end end end + +mod = Types::Analytics::CycleAnalytics::FlowMetrics +mod.prepend_mod_with('Types::Analytics::CycleAnalytics::FlowMetrics') diff --git a/app/graphql/types/analytics/cycle_analytics/link_type.rb b/app/graphql/types/analytics/cycle_analytics/link_type.rb new file mode 100644 index 00000000000..3db6b58ac55 --- /dev/null +++ b/app/graphql/types/analytics/cycle_analytics/link_type.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Types + module Analytics + module CycleAnalytics + # rubocop: disable Graphql/AuthorizeTypes + class LinkType < BaseObject + graphql_name 'ValueStreamMetricLinkType' + + field :name, + GraphQL::Types::String, + null: false, + description: 'Name of the link group.' + + field :label, + GraphQL::Types::String, + null: false, + description: 'Label for the link.' + + field :url, + GraphQL::Types::String, + null: false, + description: 'Drill-down URL.' + + field :docs_link, + GraphQL::Types::Boolean, + null: true, + description: 'Link to the metric documentation.' + end + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/graphql/types/analytics/cycle_analytics/metric_type.rb b/app/graphql/types/analytics/cycle_analytics/metric_type.rb index b880f5029ea..3f1a239019f 100644 --- a/app/graphql/types/analytics/cycle_analytics/metric_type.rb +++ b/app/graphql/types/analytics/cycle_analytics/metric_type.rb @@ -29,7 +29,7 @@ module Types description: 'Title for the metric.' field :links, - [GraphQL::Types::String], + [LinkType], null: false, description: 'Optional links for drilling down.' end diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index d18c216ebd4..60ea78752ca 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -58,7 +58,7 @@ module Types Types::Ci::RunnerJobExecutionStatusEnum, null: true, description: 'Job execution status of the runner.', - deprecated: { milestone: '15.7', reason: :alpha } + alpha: { milestone: '15.7' } field :jobs, ::Types::Ci::JobType.connection_type, null: true, description: 'Jobs assigned to the runner. This field can only be resolved for one runner in any single request.', authorize: :read_builds, @@ -67,7 +67,8 @@ module Types description: 'Indicates the runner is locked.' field :machines, ::Types::Ci::RunnerMachineType.connection_type, null: true, description: 'Machines associated with the runner configuration.', - method: :runner_machines + method: :runner_machines, + alpha: { milestone: '15.10' } field :maintenance_note, GraphQL::Types::String, null: true, description: 'Runner\'s maintenance notes.' field :maximum_timeout, GraphQL::Types::Int, null: true, diff --git a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb index b69a3cc1a2c..714a9d43333 100644 --- a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb @@ -45,12 +45,12 @@ module Projects end def with_timeout - result = { + result = success( original_size: 0, before_delete_size: 0, deleted_size: 0, deleted: [] - } + ) yield Time.zone.now, result diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 20fa6804974..22de00b99bf 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -831,7 +831,7 @@ Gitlab.ee do Settings.cron_jobs['abandoned_trial_emails']['cron'] ||= "0 1 * * *" Settings.cron_jobs['abandoned_trial_emails']['job_class'] = 'Emails::AbandonedTrialEmailsCronWorker' Settings.cron_jobs['package_metadata_sync_worker'] ||= Settingslogic.new({}) - Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 * * * *" + Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "*/5 * * * *" Settings.cron_jobs['package_metadata_sync_worker']['job_class'] = 'PackageMetadata::SyncWorker' Settings.cron_jobs['compliance_violations_consistency_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['compliance_violations_consistency_worker']['cron'] ||= '0 1 * * *' diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 223cc7ba867..50155668dca 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -33,7 +33,8 @@ class Gitlab::Seeder::CycleAnalytics # rubocop:disable Style/ClassAndModuleChild code: 72, test: 5, review: 72, - deployment: 48 + deployment: 48, + lead_time: 32 }.freeze def self.seeder_based_on_env(project) @@ -69,6 +70,7 @@ class Gitlab::Seeder::CycleAnalytics # rubocop:disable Style/ClassAndModuleChild create_developers! create_issues! + seed_lead_time! seed_issue_stage! seed_plan_stage! seed_code_stage! @@ -156,6 +158,13 @@ class Gitlab::Seeder::CycleAnalytics # rubocop:disable Style/ClassAndModuleChild end end + def seed_lead_time! + issues.each do |issue| + created_at = issue.created_at - MAX_DURATIONS[:lead_time].hours + issue.update!(created_at: created_at, closed_at: Time.now) + end + end + def create_issues! @issue_count.times do travel_to(start_time + rand(5).days) do diff --git a/db/migrate/20230313054226_add_status_created_at_and_updated_at_indexes_to_abuse_reports.rb b/db/migrate/20230313054226_add_status_created_at_and_updated_at_indexes_to_abuse_reports.rb new file mode 100644 index 00000000000..b619d6b8732 --- /dev/null +++ b/db/migrate/20230313054226_add_status_created_at_and_updated_at_indexes_to_abuse_reports.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddStatusCreatedAtAndUpdatedAtIndexesToAbuseReports < Gitlab::Database::Migration[2.1] + STATUS_AND_CREATED_AT_INDEX = 'index_abuse_reports_on_status_and_created_at' + STATUS_AND_UPDATED_AT_INDEX = 'index_abuse_reports_on_status_and_updated_at' + + disable_ddl_transaction! + + def up + add_concurrent_index :abuse_reports, [:status, :created_at], name: STATUS_AND_CREATED_AT_INDEX + add_concurrent_index :abuse_reports, [:status, :updated_at], name: STATUS_AND_UPDATED_AT_INDEX + end + + def down + remove_concurrent_index_by_name :abuse_reports, STATUS_AND_CREATED_AT_INDEX + remove_concurrent_index_by_name :abuse_reports, STATUS_AND_UPDATED_AT_INDEX + end +end diff --git a/db/schema_migrations/20230313054226 b/db/schema_migrations/20230313054226 new file mode 100644 index 00000000000..7b174fe3570 --- /dev/null +++ b/db/schema_migrations/20230313054226 @@ -0,0 +1 @@ +15c56632eafda4ab511368001a7bbfdf9f346049ab19a9df3ad2c96adc12f1a0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 8686191f19d..54778d5e635 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29145,8 +29145,12 @@ CREATE INDEX idx_vulnerability_reads_project_id_scanner_id_vulnerability_id ON v CREATE UNIQUE INDEX idx_work_item_types_on_namespace_id_and_name_null_namespace ON work_item_types USING btree (btrim(lower(name)), ((namespace_id IS NULL))) WHERE (namespace_id IS NULL); +CREATE INDEX index_abuse_reports_on_status_and_created_at ON abuse_reports USING btree (status, created_at); + CREATE INDEX index_abuse_reports_on_status_and_id ON abuse_reports USING btree (status, id); +CREATE INDEX index_abuse_reports_on_status_and_updated_at ON abuse_reports USING btree (status, updated_at); + CREATE INDEX index_abuse_reports_on_status_category_and_id ON abuse_reports USING btree (status, category, id); CREATE INDEX index_abuse_reports_on_user_id ON abuse_reports USING btree (user_id); diff --git a/doc/api/commits.md b/doc/api/commits.md index 0859143a542..7c4d15e5d80 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -20,6 +20,8 @@ information: ## List repository commits +> Commits by author [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114417) in GitLab 15.10. + Get a list of repository commits in a project. ```plaintext @@ -33,6 +35,7 @@ GET /projects/:id/repository/commits | `since` | string | no | Only commits after or on this date are returned in ISO 8601 format `YYYY-MM-DDTHH:MM:SSZ` | | `until` | string | no | Only commits before or on this date are returned in ISO 8601 format `YYYY-MM-DDTHH:MM:SSZ` | | `path` | string | no | The file path | +| `author` | string | no | Search commits by commit author.| | `all` | boolean | no | Retrieve every commit from the repository | | `with_stats` | boolean | no | Stats about each commit are added to the response | | `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c4929317221..50b8f4d55de 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -11858,7 +11858,7 @@ CI/CD variables for a project. | `jobCount` | [`Int`](#int) | Number of jobs processed by the runner (limited to 1000, plus one to indicate that more items exist). | | `jobExecutionStatus` **{warning-solid}** | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | **Introduced** in 15.7. This feature is in Alpha. It can be changed or removed at any time. Job execution status of the runner. | | `locked` | [`Boolean`](#boolean) | Indicates the runner is locked. | -| `machines` | [`CiRunnerMachineConnection`](#cirunnermachineconnection) | Machines associated with the runner configuration. (see [Connections](#connections)) | +| `machines` **{warning-solid}** | [`CiRunnerMachineConnection`](#cirunnermachineconnection) | **Introduced** in 15.10. This feature is in Alpha. It can be changed or removed at any time. Machines associated with the runner configuration. | | `maintenanceNote` | [`String`](#string) | Runner's maintenance notes. | | `maintenanceNoteHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `maintenance_note`. | | `maximumTimeout` | [`Int`](#int) | Maximum timeout (in seconds) for jobs processed by the runner. | @@ -15216,6 +15216,24 @@ Exposes aggregated value stream flow metrics. #### Fields with arguments +##### `GroupValueStreamAnalyticsFlowMetrics.cycleTime` + +Median time from first commit to issue closed. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| `authorUsername` | [`String`](#string) | Username of the author of the issue. | +| `from` | [`Time!`](#time) | Issues created after the date. | +| `labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| `milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| `projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | +| `to` | [`Time!`](#time) | Issues created before the date. | + ##### `GroupValueStreamAnalyticsFlowMetrics.deploymentCount` Number of production deployments in the given period. @@ -15248,6 +15266,24 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). | `projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | | `to` | [`Time!`](#time) | Issues created before the date. | +##### `GroupValueStreamAnalyticsFlowMetrics.leadTime` + +Median time from when the issue was created to when it was closed. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| `authorUsername` | [`String`](#string) | Username of the author of the issue. | +| `from` | [`Time!`](#time) | Issues created after the date. | +| `labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| `milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| `projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | +| `to` | [`Time!`](#time) | Issues created before the date. | + ### `GroupWikiRepositoryRegistry` Represents the Geo sync and verification state of a group wiki repository. @@ -19666,6 +19702,23 @@ Exposes aggregated value stream flow metrics. #### Fields with arguments +##### `ProjectValueStreamAnalyticsFlowMetrics.cycleTime` + +Median time from first commit to issue closed. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| `authorUsername` | [`String`](#string) | Username of the author of the issue. | +| `from` | [`Time!`](#time) | Issues created after the date. | +| `labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| `milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| `to` | [`Time!`](#time) | Issues created before the date. | + ##### `ProjectValueStreamAnalyticsFlowMetrics.deploymentCount` Number of production deployments in the given period. @@ -19696,6 +19749,23 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). | `milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | | `to` | [`Time!`](#time) | Issues created before the date. | +##### `ProjectValueStreamAnalyticsFlowMetrics.leadTime` + +Median time from when the issue was created to when it was closed. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| `authorUsername` | [`String`](#string) | Username of the author of the issue. | +| `from` | [`Time!`](#time) | Issues created after the date. | +| `labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| `milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| `to` | [`Time!`](#time) | Issues created before the date. | + ### `PrometheusAlert` The alert condition for Prometheus. @@ -21415,11 +21485,22 @@ fields relate to interactions between the two entities. | Name | Type | Description | | ---- | ---- | ----------- | | `identifier` | [`String!`](#string) | Identifier for the metric. | -| `links` | [`[String!]!`](#string) | Optional links for drilling down. | +| `links` | [`[ValueStreamMetricLinkType!]!`](#valuestreammetriclinktype) | Optional links for drilling down. | | `title` | [`String!`](#string) | Title for the metric. | | `unit` | [`String`](#string) | Unit of measurement. | | `value` | [`Float`](#float) | Value for the metric. | +### `ValueStreamMetricLinkType` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `docsLink` | [`Boolean`](#boolean) | Link to the metric documentation. | +| `label` | [`String!`](#string) | Label for the link. | +| `name` | [`String!`](#string) | Name of the link group. | +| `url` | [`String!`](#string) | Drill-down URL. | + ### `VulnerabilitiesCountByDay` Represents the count of vulnerabilities by severity on a particular day. This data is retained for 365 days. diff --git a/doc/development/stage_group_observability/img/error_budgets_kibana_dashboard_v15_10.png b/doc/development/stage_group_observability/img/error_budgets_kibana_dashboard_v15_10.png new file mode 100644 index 00000000000..e4f54b579c1 Binary files /dev/null and b/doc/development/stage_group_observability/img/error_budgets_kibana_dashboard_v15_10.png differ diff --git a/doc/development/stage_group_observability/index.md b/doc/development/stage_group_observability/index.md index d549123968c..ba17b4cc73a 100644 --- a/doc/development/stage_group_observability/index.md +++ b/doc/development/stage_group_observability/index.md @@ -136,3 +136,39 @@ For example, see the `server` component of the `web-pages` service: ![web-pages-server-component SLI](img/stage_group_dashboards_service_sli_detail.png) To add more SLIs tailored to specific features, you can use an [Application SLI](../application_slis/index.md). + +## Kibana dashboard for error budgets + +For a detailed analysis you can use [a specialized Kibana dashboard](https://log.gprd.gitlab.net/goto/771b5c10-c0ec-11ed-85ed-e7557b0a598c), like this: + +![Kibana dashboard](img/error_budgets_kibana_dashboard_v15_10.png) + +Description: + +- **Apdex requests over limit (graph)** - Displays only requests that exceeded their + target duration. +- **Apdex operations over-limit duration (graph)** - Displays the distribution of duration + components (database, Redis, Gitaly, and Rails app). +- **Apdex requests** (pie chart) - Displays the percentage of `2xx`, `3xx`, `4xx` and + `5xx` requests. +- **Slow request component distribution** - Highlights the component responsible + for Apdex violation. +- **Apdex operations over limit** (table) - Displays a number of operations over + limit for each endpoint. +- **Apdex requests over limit** - Displays a list of individual requests responsible + for Apdex violation. + +### Use the dashboard + +1. Select the feature category you want to investigate. + 1. Scroll to the **Feature Category** section. Enter the feature name. + 1. Select **Apply changes**. Selected results contain only requests related to this feature category. +1. Select the time frame for the investigation. +1. Review dashboard and pay attention to the type of failures. + +Questions to answer: + +1. Does the failure pattern look like a spike? Or does it persist? +1. Does the failure look related to a particular component? (database, Redis, ...) +1. Does the failure affect a specific endpoint? Or is it system-wide? +1. Does the failure appear caused by infrastructure incidents? diff --git a/doc/development/ux/index.md b/doc/development/ux/index.md new file mode 100644 index 00000000000..784a59a3a4a --- /dev/null +++ b/doc/development/ux/index.md @@ -0,0 +1,26 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Contribute to UX design + +## UX Design + +These instructions are specifically for those wanting to make UX design contributions to GitLab. + +The UX department at GitLab uses [Figma](https://www.figma.com/) for all of its designs, and you can see our [Design Repository documentation](https://gitlab.com/gitlab-org/gitlab-design/blob/master/README.md#getting-started) for details on working with our files. + +You may leverage the [Pajamas UI Kit](https://www.figma.com/community/file/781156790581391771) in Figma to create mockups for your proposals. However, we will also gladly accept handmade drawings and sketches, wireframes, manipulated DOM screenshots, or prototypes. You can find design resources documentation in our [Design System](https://design.gitlab.com/). Use it to understand where and when to use common design solutions. + +## Contributing to Pajamas + +To contribute to [Pajamas design system](https://design.gitlab.com/) and the [UI kit](https://www.figma.com/community/file/781156790581391771), follow the [contribution guidelines](https://design.gitlab.com/get-started/contribute) documented in the handbook. While the instructions are code-focused, they will help you understand the overall process of contributing. + +## Contributing to other issues + +1. Review the list of available issues that are currently [accepting UX contribution](https://gitlab.com/groups/gitlab-org/-/issues/?sort=weight&state=opened&label_name%5B%5D=UX&label_name%5B%5D=workflow%3A%3Aready%20for%20design&label_name%5B%5D=Accepting%20UX%20contributions&first_page_size=20). +1. Find an issue that does not have an Assignee to ensure someone else is not working on a solution. Add the `~"workflow::design"` and `~"Community contribution"` labels and mention `@gitlab-com/gitlab-ux/reviewers` to request they assign the issue to you. +1. Add your design proposal to the issue description/[design management](../../user/project/issues/design_management.md) section. Remember to keep the scope of the proposal/change small following our [MVCs guidelines](https://about.gitlab.com/handbook/values/#minimal-viable-change-mvc). +1. If you have any questions or are ready for a review of your proposal, mention `@gitlab-com/gitlab-ux/reviewers` in a comment to make your request. diff --git a/doc/development/workspace/index.md b/doc/development/workspace/index.md new file mode 100644 index 00000000000..ca404702d72 --- /dev/null +++ b/doc/development/workspace/index.md @@ -0,0 +1,11 @@ +--- +redirect_to: '../organization/index.md' +remove_date: '2023-06-13' +--- + +This document was moved to [another location](../organization/index.md). + + + + + diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md new file mode 100644 index 00000000000..ca404702d72 --- /dev/null +++ b/doc/user/workspace/index.md @@ -0,0 +1,11 @@ +--- +redirect_to: '../organization/index.md' +remove_date: '2023-06-13' +--- + +This document was moved to [another location](../organization/index.md). + + + + + diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2f52bf8b701..f884dde3552 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -78,6 +78,10 @@ module API type: String, desc: 'The file path', documentation: { example: 'README.md' } + optional :author, + type: String, + desc: 'Search commits by commit author', + documentation: { example: 'John Smith' } optional :all, type: Boolean, desc: 'Every commit will be returned' optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges' @@ -101,6 +105,7 @@ module API with_stats = params[:with_stats] first_parent = params[:first_parent] order = params[:order] + author = params[:author] commits = user_project.repository.commits(ref, path: path, @@ -111,6 +116,7 @@ module API all: all, first_parent: first_parent, order: order, + author: author, trailers: params[:trailers]) serializer = with_stats ? Entities::CommitWithStats : Entities::Commit diff --git a/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb b/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb deleted file mode 100644 index adc666d9987..00000000000 --- a/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Reports - module Security - class VulnerabilityReportsComparer - include Gitlab::Utils::StrongMemoize - - attr_reader :base_report, :head_report - - ACCEPTABLE_REPORT_AGE = 1.week - MAX_FINDINGS_COUNT = 25 - - def initialize(project, base_report, head_report) - @base_report = base_report - @head_report = head_report - - @signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures) - - if @signatures_enabled - @added_findings = [] - @fixed_findings = [] - calculate_changes - end - end - - def base_report_created_at - @base_report.created_at - end - - def head_report_created_at - @head_report.created_at - end - - def base_report_out_of_date - return false unless @base_report.created_at - - ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at - end - - def added - strong_memoize(:added) do - all_added_findings.take(MAX_FINDINGS_COUNT) # rubocop:disable CodeReuse/ActiveRecord (This is Array#take) - end - end - - def fixed - strong_memoize(:fixed) do - all_fixed_findings.take(MAX_FINDINGS_COUNT) # rubocop:disable CodeReuse/ActiveRecord (This is Array#take) - end - end - - private - - def calculate_changes - # This is a deconstructed version of the eql? method on - # Ci::Reports::Security::Finding. It: - # - # * precomputes for the head_findings (using FindingMatcher): - # * sets of signature shas grouped by priority - # * mappings of signature shas to the head finding object - # - # These are then used when iterating the base findings to perform - # fast(er) prioritized, signature-based comparisons between each base finding - # and the head findings. - # - # Both the head_findings and base_findings arrays are iterated once - - base_findings = base_report.findings - head_findings = head_report.findings - - matcher = FindingMatcher.new(head_findings) - - base_findings.each do |base_finding| - next if base_finding.requires_manual_resolution? - - matched_head_finding = matcher.find_and_remove_match!(base_finding) - - @fixed_findings << base_finding if matched_head_finding.nil? - end - - @added_findings = matcher.unmatched_head_findings.values - end - - def all_added_findings - if @signatures_enabled - @added_findings - else - head_report.findings - base_report.findings - end - end - - def all_fixed_findings - if @signatures_enabled - @fixed_findings - else - base_report.findings - head_report.findings - end - end - end - - class FindingMatcher - attr_reader :unmatched_head_findings, :head_findings - - include Gitlab::Utils::StrongMemoize - - def initialize(head_findings) - @head_findings = head_findings - @unmatched_head_findings = @head_findings.index_by(&:object_id) - end - - def find_and_remove_match!(base_finding) - matched_head_finding = find_matched_head_finding_for(base_finding) - - # no signatures matched, so check the normal uuids of the base and head findings - # for a match - matched_head_finding = head_signatures_shas[base_finding.uuid] if matched_head_finding.nil? - - @unmatched_head_findings.delete(matched_head_finding.object_id) unless matched_head_finding.nil? - - matched_head_finding - end - - private - - def find_matched_head_finding_for(base_finding) - base_signature = sorted_signatures_for(base_finding).find do |signature| - # at this point a head_finding exists that has a signature with a - # matching priority, and a matching sha --> lookup the actual finding - # object from head_signatures_shas - head_signatures_shas[signature.signature_sha].eql?(base_finding) - end - - base_signature.present? ? head_signatures_shas[base_signature.signature_sha] : nil - end - - def sorted_signatures_for(base_finding) - base_finding.signatures.select { |signature| head_finding_signature?(signature) } - .sort_by { |sig| -sig.priority } - end - - def head_finding_signature?(signature) - head_signatures_priorities[signature.priority].include?(signature.signature_sha) - end - - def head_signatures_priorities - strong_memoize(:head_signatures_priorities) do - signatures_priorities = Hash.new { |hash, key| hash[key] = Set.new } - - head_findings.each_with_object(signatures_priorities) do |head_finding, memo| - head_finding.signatures.each do |signature| - memo[signature.priority].add(signature.signature_sha) - end - end - end - end - - def head_signatures_shas - strong_memoize(:head_signatures_shas) do - head_findings.each_with_object({}) do |head_finding, memo| - head_finding.signatures.each do |signature| - memo[signature.signature_sha] = head_finding - end - # for the final uuid check when no signatures have matched - memo[head_finding.uuid] = head_finding - end - end - end - end - end - end - end -end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 544bbd52c6b..b69ac9e4a0b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2427,6 +2427,9 @@ msgstr "" msgid "Add request manually" msgstr "" +msgid "Add start and due date" +msgstr "" + msgid "Add suggestion to batch" msgstr "" diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 20a69c61871..d5f90bb9260 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -48,6 +48,30 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do end end + context 'when issue description has task list items' do + before do + description = '- [ ] I am a task + +| Table | +|-------| +|