diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-07 18:11:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-07 18:11:06 +0300 |
commit | 255fcf9df95e1ad5d8a93e8538aed5a152970187 (patch) | |
tree | a17736846f5d5a36e33d1ca83eccaad325fd5e39 | |
parent | 325245c6f5803227b13051883d00da5b3c235ab0 (diff) |
Add latest changes from gitlab-org/gitlab@master
69 files changed, 724 insertions, 225 deletions
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js index 84189b675f2..52901d4c5bb 100644 --- a/app/assets/javascripts/analytics/shared/utils.js +++ b/app/assets/javascripts/analytics/shared/utils.js @@ -1,4 +1,9 @@ +import dateFormat from 'dateformat'; +import { dateFormats } from './constants'; + export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => { if (!searchTerm?.length) return data; return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase())); }; + +export const toYmd = (date) => dateFormat(date, dateFormats.isoDate); diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue index de6c233407b..6e025a8444c 100644 --- a/app/assets/javascripts/cycle_analytics/components/base.vue +++ b/app/assets/javascripts/cycle_analytics/components/base.vue @@ -1,9 +1,10 @@ <script> -import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import Cookies from 'js-cookie'; import { mapActions, mapState, mapGetters } from 'vuex'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue'; +import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue'; import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue'; import { __ } from '~/locale'; import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants'; @@ -13,11 +14,10 @@ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; export default { name: 'CycleAnalytics', components: { - GlIcon, GlLoadingIcon, - GlSprintf, PathNavigation, StageTable, + ValueStreamFilters, ValueStreamMetrics, }, props: { @@ -45,11 +45,12 @@ export default { 'selectedStageError', 'stages', 'summary', - 'daysInPast', 'permissions', 'stageCounts', 'endpoints', 'features', + 'createdBefore', + 'createdAfter', ]), ...mapGetters(['pathNavigationData', 'filterParams']), displayStageEvents() { @@ -99,8 +100,11 @@ export default { }, methods: { ...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']), - handleDateSelect(daysInPast) { - this.setDateRange(daysInPast); + onSetDateRange({ startDate, endDate }) { + this.setDateRange({ + createdAfter: new Date(startDate), + createdBefore: new Date(endDate), + }); }, onSelectStage(stage) { this.setSelectedStage(stage); @@ -134,29 +138,15 @@ export default { :selected-stage="selectedStage" @selected="onSelectStage" /> - <div class="gl-flex-grow gl-align-self-end"> - <div class="js-ca-dropdown dropdown inline"> - <!-- eslint-disable-next-line @gitlab/vue-no-data-toggle --> - <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span class="dropdown-label"> - <gl-sprintf :message="$options.i18n.dropdownText"> - <template #days>{{ daysInPast }}</template> - </gl-sprintf> - <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" /> - </span> - </button> - <ul class="dropdown-menu dropdown-menu-right"> - <li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`"> - <a href="#" @click.prevent="handleDateSelect(days)"> - <gl-sprintf :message="$options.i18n.dropdownText"> - <template #days>{{ days }}</template> - </gl-sprintf> - </a> - </li> - </ul> - </div> - </div> </div> + <value-stream-filters + :group-id="endpoints.groupId" + :group-path="endpoints.groupPath" + :has-project-filter="false" + :start-date="createdAfter" + :end-date="createdBefore" + @setDateRange="onSetDateRange" + /> <value-stream-metrics :request-path="endpoints.fullPath" :request-params="filterParams" diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue index 6b1e537dc77..0474c2ab423 100644 --- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue +++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue @@ -68,26 +68,30 @@ export default { v-if="hasDateRangeFilter || hasProjectFilter" class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between" > - <projects-dropdown-filter - v-if="hasProjectFilter" - :key="groupId" - class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0" - :group-id="groupId" - :group-namespace="groupPath" - :query-params="projectsQueryParams" - :multi-select="$options.multiProjectSelect" - :default-projects="selectedProjects" - @selected="$emit('selectProject', $event)" - /> - <date-range - v-if="hasDateRangeFilter" - :start-date="startDate" - :end-date="endDate" - :max-date-range="$options.maxDateRange" - :include-selected-date="true" - class="js-daterange-picker" - @change="$emit('setDateRange', $event)" - /> + <div> + <projects-dropdown-filter + v-if="hasProjectFilter" + :key="groupId" + class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0" + :group-id="groupId" + :group-namespace="groupPath" + :query-params="projectsQueryParams" + :multi-select="$options.multiProjectSelect" + :default-projects="selectedProjects" + @selected="$emit('selectProject', $event)" + /> + </div> + <div> + <date-range + v-if="hasDateRangeFilter" + :start-date="startDate" + :end-date="endDate" + :max-date-range="$options.maxDateRange" + :include-selected-date="true" + class="js-daterange-picker" + @change="$emit('setDateRange', $event)" + /> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js index 3827db4d9b2..620da0104e0 100644 --- a/app/assets/javascripts/cycle_analytics/index.js +++ b/app/assets/javascripts/cycle_analytics/index.js @@ -1,7 +1,9 @@ import Vue from 'vue'; import Translate from '../vue_shared/translate'; import CycleAnalytics from './components/base.vue'; +import { DEFAULT_DAYS_TO_DISPLAY } from './constants'; import createStore from './store'; +import { calculateFormattedDayInPast } from './utils'; Vue.use(Translate); @@ -14,19 +16,29 @@ export default () => { requestPath, fullPath, projectId, + groupId, groupPath, + labelsPath, + milestonesPath, } = el.dataset; + const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); + store.dispatch('initializeVsa', { projectId: parseInt(projectId, 10), - groupPath, endpoints: { requestPath, fullPath, + labelsPath, + milestonesPath, + groupId: parseInt(groupId, 10), + groupPath, }, features: { cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups), }, + createdBefore: new Date(now), + createdAfter: new Date(past), }); // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js index a7a2c8ea9d3..5aa86d24dd2 100644 --- a/app/assets/javascripts/cycle_analytics/store/actions.js +++ b/app/assets/javascripts/cycle_analytics/store/actions.js @@ -9,6 +9,7 @@ import { import createFlash from '~/flash'; import { __ } from '~/locale'; import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants'; +import { appendExtension } from '../utils'; import * as types from './mutation_types'; export const setSelectedValueStream = ({ commit, dispatch }, valueStream) => { @@ -163,6 +164,7 @@ const refetchStageData = (dispatch) => { dispatch('fetchCycleAnalyticsData'), dispatch('fetchStageData'), dispatch('fetchStageMedians'), + dispatch('fetchStageCountValues'), ]), ) .finally(() => dispatch('setLoading', false)); @@ -170,14 +172,24 @@ const refetchStageData = (dispatch) => { export const setFilters = ({ dispatch }) => refetchStageData(dispatch); -export const setDateRange = ({ dispatch, commit }, daysInPast) => { - commit(types.SET_DATE_RANGE, daysInPast); +export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => { + commit(types.SET_DATE_RANGE, { createdAfter, createdBefore }); return refetchStageData(dispatch); }; export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { commit(types.INITIALIZE_VSA, initialData); + const { + endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' }, + } = initialData; + dispatch('filters/setEndpoints', { + labelsEndpoint: appendExtension(labelsPath), + milestonesEndpoint: appendExtension(milestonesPath), + groupEndpoint: groupPath, + projectEndpoint: fullPath, + }); + return dispatch('setLoading', true) .then(() => dispatch('fetchValueStreams')) .finally(() => dispatch('setLoading', false)); diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js index 9faccabcaad..77c285f5ce0 100644 --- a/app/assets/javascripts/cycle_analytics/store/getters.js +++ b/app/assets/javascripts/cycle_analytics/store/getters.js @@ -1,5 +1,6 @@ import dateFormat from 'dateformat'; import { dateFormats } from '~/analytics/shared/constants'; +import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils'; export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => { @@ -20,6 +21,21 @@ export const requestParams = (state) => { return { requestPath: fullPath, valueStreamId, stageId }; }; +const filterBarParams = ({ filters }) => { + const { + authors: { selected: selectedAuthor }, + milestones: { selected: selectedMilestone }, + assignees: { selectedList: selectedAssigneeList }, + labels: { selectedList: selectedLabelList }, + } = filters; + return filterToQueryObject({ + milestone_title: selectedMilestone, + author_username: selectedAuthor, + label_name: selectedLabelList, + assignee_username: selectedAssigneeList, + }); +}; + const dateRangeParams = ({ createdAfter, createdBefore }) => ({ created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null, created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null, @@ -33,6 +49,7 @@ export const legacyFilterParams = ({ daysInPast }) => { export const filterParams = (state) => { return { + ...filterBarParams(state), ...dateRangeParams(state), }; }; diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js index e41de85c1fa..301e7d95f8c 100644 --- a/app/assets/javascripts/cycle_analytics/store/mutations.js +++ b/app/assets/javascripts/cycle_analytics/store/mutations.js @@ -1,14 +1,12 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; -import { formatMedianValues, calculateFormattedDayInPast } from '../utils'; +import { formatMedianValues } from '../utils'; import * as types from './mutation_types'; export default { - [types.INITIALIZE_VSA](state, { endpoints, features }) { + [types.INITIALIZE_VSA](state, { endpoints, features, createdBefore, createdAfter }) { state.endpoints = endpoints; - const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY); - state.createdBefore = now; - state.createdAfter = past; + state.createdBefore = createdBefore; + state.createdAfter = createdAfter; state.features = features; }, [types.SET_LOADING](state, loadingState) { @@ -20,11 +18,9 @@ export default { [types.SET_SELECTED_STAGE](state, stage) { state.selectedStage = stage; }, - [types.SET_DATE_RANGE](state, daysInPast) { - state.daysInPast = daysInPast; - const { now, past } = calculateFormattedDayInPast(daysInPast); - state.createdBefore = now; - state.createdAfter = past; + [types.SET_DATE_RANGE](state, { createdAfter, createdBefore }) { + state.createdBefore = createdBefore; + state.createdAfter = createdAfter; }, [types.REQUEST_VALUE_STREAMS](state) { state.valueStreams = []; diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js index e6da3f609b2..0882db51218 100644 --- a/app/assets/javascripts/cycle_analytics/store/state.js +++ b/app/assets/javascripts/cycle_analytics/store/state.js @@ -1,10 +1,7 @@ -import { DEFAULT_DAYS_TO_DISPLAY } from '../constants'; - export default () => ({ id: null, features: {}, endpoints: {}, - daysInPast: DEFAULT_DAYS_TO_DISPLAY, createdAfter: null, createdBefore: null, stages: [], diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js index fa02fdf914a..888ef68e69a 100644 --- a/app/assets/javascripts/cycle_analytics/utils.js +++ b/app/assets/javascripts/cycle_analytics/utils.js @@ -149,3 +149,5 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) => description: popoverContent[key]?.description || '', }; }); + +export const appendExtension = (path) => (path.indexOf('.') > -1 ? path : `${path}.json`); diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 1924326da1f..a6e3a7dc08a 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,4 +1,5 @@ import initVariableList from '~/ci_variable_list'; +import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; import selfMonitor from '~/self_monitor'; import initSettingsPanels from '~/settings_panels'; @@ -7,4 +8,5 @@ initVariableList('js-instance-variables'); selfMonitor(); // Initialize expandable settings panels initSettingsPanels(); +projectSelect(); initSearchSettings(); diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js index 230ed9b1dba..3e09b1796b1 100644 --- a/app/assets/javascripts/pages/dashboard/issues/index.js +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -2,6 +2,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import initManualOrdering from '~/manual_ordering'; import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import projectSelect from '~/project_select'; initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, @@ -9,4 +10,5 @@ initFilteredSearch({ useDefaultState: true, }); +projectSelect(); initManualOrdering(); diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index c0d026422cb..6c134e4fad6 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -2,6 +2,7 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import projectSelect from '~/project_select'; addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true); @@ -10,3 +11,5 @@ initFilteredSearch({ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, useDefaultState: true, }); + +projectSelect(); diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js new file mode 100644 index 00000000000..b526fce6f7b --- /dev/null +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -0,0 +1,3 @@ +import projectSelect from '~/project_select'; + +projectSelect(); diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index af36139dd13..342c054471d 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -4,6 +4,7 @@ import { mountIssuablesListApp } from '~/issues_list'; import initManualOrdering from '~/manual_ordering'; import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import projectSelect from '~/project_select'; const ISSUE_BULK_UPDATE_PREFIX = 'issue_'; @@ -17,6 +18,7 @@ initFilteredSearch({ useDefaultState: true, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); +projectSelect(); initManualOrdering(); if (gon.features?.vueIssuablesList) { diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index b1fcc110a90..02a0a50f984 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -3,6 +3,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar'; import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import projectSelect from '~/project_select'; const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_'; @@ -15,3 +16,4 @@ initFilteredSearch({ useDefaultState: true, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); +projectSelect(); diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 8ccc0102c3d..8f4d5406da8 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -369,6 +369,7 @@ export default { :text="dropdownText" :loading="loading" class="gl-w-full" + toggle-class="gl-max-w-100" @shown="setFocus" > <gl-search-box-by-type ref="search" v-model="searchTerm" /> diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 7b8d71a5575..54ecb146364 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -2,6 +2,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_two_factor_requirement + before_action :ensure_verified_primary_email, only: [:show, :create] before_action do push_frontend_feature_flag(:webauthn) end @@ -218,4 +219,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.}) .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe } end + + def ensure_verified_primary_email + return unless Feature.enabled?(:ensure_verified_primary_email_for_2fa) + + unless current_user.two_factor_enabled? || current_user.primary_email_verified? + redirect_to profile_emails_path, notice: s_('You need to verify your primary email first before enabling Two-Factor Authentication.') + end + end end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 42feb8a8076..c8db2b84ff2 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -53,6 +53,9 @@ module Types description: 'Due date of the issue.' field :confidential, GraphQL::Types::Boolean, null: false, description: 'Indicates the issue is confidential.' + field :hidden, GraphQL::Types::Boolean, null: true, resolver_method: :hidden?, + description: 'Indicates the issue is hidden because the author has been banned. ' \ + 'Will always return `null` if `ban_user_feature_flag` feature flag is disabled.' field :discussion_locked, GraphQL::Types::Boolean, null: false, description: 'Indicates discussion is locked on the issue.' @@ -156,6 +159,10 @@ module Types def create_note_email object.creatable_note_email_address(context[:current_user]) end + + def hidden? + object.hidden? if Feature.enabled?(:ban_user_feature_flag) + end end end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index 6422f6bddab..72e0ca22ac2 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -7,7 +7,7 @@ module Integrations extend Gitlab::Utils::Override DEFAULT_DOMAIN = 'datadoghq.com' - URL_TEMPLATE = 'https://webhooks-http-intake.logs.%{datadog_domain}/api/v2/webhook' + URL_TEMPLATE = 'https://webhook-intake.%{datadog_domain}/api/v2/webhook' URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/" SUPPORTED_EVENTS = %w[ diff --git a/app/models/repository.rb b/app/models/repository.rb index 8d2f0648a05..c714ed3b0ef 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -721,18 +721,9 @@ class Repository end def tags_sorted_by(value) - case value - when 'name_asc' - VersionSorter.sort(tags) { |tag| tag.name } - when 'name_desc' - VersionSorter.rsort(tags) { |tag| tag.name } - when 'updated_desc' - tags_sorted_by_committed_date.reverse - when 'updated_asc' - tags_sorted_by_committed_date - else - tags - end + return raw_repository.tags(sort_by: value) if Feature.enabled?(:gitaly_tags_finder, project, default_enabled: :yaml) + + tags_ruby_sort(value) end # Params: @@ -1164,6 +1155,23 @@ class Repository @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end + # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741 + def tags_ruby_sort(value) + case value + when 'name_asc' + VersionSorter.sort(tags) { |tag| tag.name } + when 'name_desc' + VersionSorter.rsort(tags) { |tag| tag.name } + when 'updated_desc' + tags_sorted_by_committed_date.reverse + when 'updated_asc' + tags_sorted_by_committed_date + else + tags + end + end + + # Deprecated: https://gitlab.com/gitlab-org/gitlab/-/issues/339741 def tags_sorted_by_committed_date # Annotated tags can point to any object (e.g. a blob), but generally # tags point to a commit. If we don't have a commit, then just default diff --git a/app/models/user.rb b/app/models/user.rb index 0c7355e6e6d..ddaa540a92f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -187,7 +187,7 @@ class User < ApplicationRecord has_many :todos has_many :notification_settings has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent + has_many :triggers, class_name: 'Ci::Trigger', foreign_key: :owner_id has_many :issue_assignees, inverse_of: :assignee has_many :merge_request_assignees, inverse_of: :assignee diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb index 3e5122a1523..d0d0737fd66 100644 --- a/app/services/protected_branches/api_service.rb +++ b/app/services/protected_branches/api_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ProtectedBranches - class ApiService < BaseService + class ApiService < ProtectedBranches::BaseService def create ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute end diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb new file mode 100644 index 00000000000..f48e02ab4b5 --- /dev/null +++ b/app/services/protected_branches/base_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ProtectedBranches + class BaseService < ::BaseService + # current_user - The user that performs the action + # params - A hash of parameters + def initialize(project, current_user = nil, params = {}) + @project = project + @current_user = current_user + @params = params + end + + def after_execute(*) + # overridden in EE::ProtectedBranches module + end + end +end diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb index 37083a4a9e4..dada449989a 100644 --- a/app/services/protected_branches/create_service.rb +++ b/app/services/protected_branches/create_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ProtectedBranches - class CreateService < BaseService + class CreateService < ProtectedBranches::BaseService def execute(skip_authorization: false) raise Gitlab::Access::AccessDeniedError unless skip_authorization || authorized? diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb index dc177f0ac09..47332ace417 100644 --- a/app/services/protected_branches/destroy_service.rb +++ b/app/services/protected_branches/destroy_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ProtectedBranches - class DestroyService < BaseService + class DestroyService < ProtectedBranches::BaseService def execute(protected_branch) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_protected_branch, protected_branch) diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb index 1815d92421e..1e70f2d9793 100644 --- a/app/services/protected_branches/update_service.rb +++ b/app/services/protected_branches/update_service.rb @@ -1,11 +1,17 @@ # frozen_string_literal: true module ProtectedBranches - class UpdateService < BaseService + class UpdateService < ProtectedBranches::BaseService def execute(protected_branch) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :update_protected_branch, protected_branch) - protected_branch.update(params) + old_merge_access_levels = protected_branch.merge_access_levels.map(&:clone) + old_push_access_levels = protected_branch.push_access_levels.map(&:clone) + + if protected_branch.update(params) + after_execute(protected_branch: protected_branch, old_merge_access_levels: old_merge_access_levels, old_push_access_levels: old_push_access_levels) + end + protected_branch end end diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 80b1db4abef..5a7eb46771b 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -9,6 +9,10 @@ .page-title-holder.d-flex.align-items-center %h1.page-title= _('Issues') + - if current_user + .page-title-controls + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues + .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 25d5c063846..ae557b73620 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -7,6 +7,10 @@ .page-title-holder.d-flex.align-items-start.flex-column.flex-sm-row.align-items-sm-center %h1.page-title= _('Merge requests') + - if current_user + .page-title-controls.ml-0.mb-3.ml-sm-auto.mb-sm-0 + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests + .top-area = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index 4a714b5a197..872099f98fc 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -6,6 +6,12 @@ .page-title-holder.d-flex.align-items-center %h1.page-title= _('Milestones') + - if current_user + .page-title-controls + = render 'shared/new_project_item_select', + path: '-/milestones/new', label: 'New milestone', + include_groups: true, type: :milestones + .top-area = render 'shared/milestones_filter', counts: @milestone_states .nav-controls diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 723ca5e70d2..fdd6962eb21 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -13,6 +13,8 @@ - if @can_bulk_update = render_if_exists 'shared/issuable/bulk_update_button', type: :issues + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true + = render 'shared/issuable/search_bar', type: :issues - if @can_bulk_update diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 9f7aa54395e..ad916e3aeec 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -12,6 +12,8 @@ - if @can_bulk_update = render_if_exists 'shared/issuable/bulk_update_button', type: :merge_requests + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true + = render 'shared/issuable/search_bar', type: :merge_requests - if @can_bulk_update diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 3c9762e200a..4e7899664fd 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,6 +1,7 @@ - page_title _("Value Stream Analytics") - add_page_specific_style 'page_bundles/cycle_analytics' - svgs = { empty_state_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_data_svg_path: image_path("illustrations/analytics/cycle-analytics-empty-chart.svg"), no_access_svg_path: image_path("illustrations/analytics/no-access.svg") } -- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs) +- api_paths = @group.present? ? { milestones_path: group_milestones_path(@group), labels_path: group_labels_path(@group), group_path: group_path(@group), group_id: @group&.id } : { milestones_path: project_milestones_path(@project), labels_path: project_labels_path(@project), group_path: @project.parent&.path, group_id: @project.parent&.id } +- initial_data = { project_id: @project.id, group_path: @project.group&.path, request_path: project_cycle_analytics_path(@project), full_path: @project.full_path }.merge!(svgs, api_paths) #js-cycle-analytics{ data: initial_data } diff --git a/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml b/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml new file mode 100644 index 00000000000..65c2253585c --- /dev/null +++ b/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml @@ -0,0 +1,8 @@ +--- +name: ensure_verified_primary_email_for_2fa +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69593 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340151 +milestone: '14.3' +type: development +group: group::access +default_enabled: false diff --git a/config/feature_flags/development/gitaly_tags_finder.yml b/config/feature_flags/development/gitaly_tags_finder.yml new file mode 100644 index 00000000000..a0a1791e584 --- /dev/null +++ b/config/feature_flags/development/gitaly_tags_finder.yml @@ -0,0 +1,8 @@ +--- +name: gitaly_tags_finder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69101 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339741 +milestone: '14.3' +type: development +group: group::source code +default_enabled: false diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 3cee463f41e..9d8b170d661 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -39,7 +39,7 @@ There are two kinds of events logged: ### Impersonation data -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/536) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/536) in GitLab 13.0. When a user is being [impersonated](../user/admin_area/index.md#user-impersonation), their actions are logged as audit events as usual, with two additional details: @@ -48,7 +48,7 @@ When a user is being [impersonated](../user/admin_area/index.md#user-impersonati ![audit events](img/impersonated_audit_events_v13_8.png) -### Group events **(PREMIUM)** +### Group events A user with: @@ -86,7 +86,7 @@ From there, you can see the following actions: Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events) -### Project events **(PREMIUM)** +### Project events A user with a Maintainer role (or above) can retrieve project audit events of all users. A user with a Developer role is limited to project audit events based on their individual actions. @@ -127,6 +127,8 @@ From there, you can see the following actions: - Permission to modify merge requests approval rules in merge requests was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2) - New approvals requirement when new commits are added to an MR was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/336211) in GitLab 14.2) - When [strategies for feature flags](../operations/feature_flags.md#feature-flag-strategies) are changed ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68408) in GitLab 14.3) +- Changed allow push force and code owner approval requirement ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/338873) in GitLab 14.3) +- Added or removed users and groups from protected branch allow to merge and allow to push ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/338873) in GitLab 14.3) Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events). @@ -134,7 +136,7 @@ Project event queries are limited to a maximum of 30 days. ### Instance events **(PREMIUM SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2336) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2336) in GitLab 9.3. Server-wide audit events introduce the ability to observe user actions across the entire instance of your GitLab server, making it easy to understand who @@ -243,8 +245,8 @@ The search filters you can see depends on which audit level you are at. ## Export to CSV **(PREMIUM SELF)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4. -> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/285441) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1449) in GitLab 13.4. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/285441) in GitLab 13.7. Export to CSV allows customers to export the current filter view of your audit events as a CSV file, which stores tabular data in plain text. The data provides a comprehensive view with respect to diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md index 7d17b22a4d7..45f27a8a8f2 100644 --- a/doc/administration/database_load_balancing.md +++ b/doc/administration/database_load_balancing.md @@ -117,9 +117,9 @@ For Sidekiq, we can define [data consistency](../development/sidekiq_style_guide.md#job-data-consistency-strategies) requirements for a specific job. -## Service Discovery +## Service Discovery **(PREMIUM SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883) in GitLab 11.0. Service discovery allows GitLab to automatically retrieve a list of secondary databases to use, instead of having to manually specify these in the @@ -237,9 +237,9 @@ For example: {"severity":"INFO","time":"2019-09-02T12:12:01.728Z","correlation_id":"abcdefg","event":"host_online","message":"Host came back online","db_host":"111.222.333.444","db_port":null,"tag":"rails.database_load_balancing","environment":"production","hostname":"web-example-1","fqdn":"gitlab.example.com","path":null,"params":null} ``` -## Handling Stale Reads +## Handling Stale Reads **(PREMIUM SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3526) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3526) in GitLab 10.3. To prevent reading from an outdated secondary the load balancer checks if it is in sync with the primary. If the data is determined to be recent enough the diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md index 6fe66aa1642..5623bc77c44 100644 --- a/doc/administration/get_started.md +++ b/doc/administration/get_started.md @@ -177,7 +177,7 @@ The EC2 instance meets the requirements for an application data backup by taking In general, if you're running GitLab on a virtualized server, you can create VM snapshots of the entire GitLab server. It is common for a VM snapshot to require you to power down the server. -#### Option 2: GitLab Geo +#### Option 2: GitLab Geo **(PREMIUM SELF)** Geo provides local, read-only instances of your GitLab instances. @@ -191,7 +191,7 @@ Learn more about [replication limitations](../administration/geo/replication/dat GitLab provides support for self-managed GitLab through different channels. -- Priority support: Premium and Ultimate self-managed customers receive priority support with tiered response times. +- Priority support: [Premium and Ultimate](https://about.gitlab.com/pricing/) self-managed customers receive priority support with tiered response times. Learn more about [upgrading to priority support](https://about.gitlab.com/support/#upgrading-to-priority-support). - Live upgrade assistance: Get one-on-one expert guidance during a production upgrade. With your **priority support plan**, you're eligible for a live, scheduled screen-sharing session with a member of our support team. diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md index e3e2db81fb0..c8c532e9a01 100644 --- a/doc/administration/git_protocol.md +++ b/doc/administration/git_protocol.md @@ -99,7 +99,7 @@ $ GIT_TRACE_PACKET=1 git -c protocol.version=2 ls-remote https://your-gitlab-ins Verify Git v2 is used by the client: ```shell -GIT_SSH_COMMAND="ssh -v" git -c protocol.version=2 ls-remote ssh://git@your-gitlab-instance.com/group/repo.git 2>&1 |grep GIT_PROTOCOL +GIT_SSH_COMMAND="ssh -v" git -c protocol.version=2 ls-remote ssh://git@your-gitlab-instance.com/group/repo.git 2>&1 | grep GIT_PROTOCOL ``` You should see that the `GIT_PROTOCOL` environment variable is sent: diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md index c5cabc5794a..19a3fc56698 100644 --- a/doc/administration/incoming_email.md +++ b/doc/administration/incoming_email.md @@ -638,7 +638,7 @@ incoming_email: #### Microsoft Graph -> Introduced in [GitLab 13.11](https://gitlab.com/gitlab-org/gitlab/-/issues/214900). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11. GitLab can read incoming email using the Microsoft Graph API instead of IMAP. Because [Microsoft is deprecating IMAP usage with Basic Authentication](https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-support-for-imap-and-smtp-auth-protocols-in/ba-p/1330432), the Microsoft Graph API will soon be required for new Microsoft Exchange Online diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 7d3e9c55b0c..ef0126a2c17 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -772,7 +772,7 @@ Set the limit to `0` to allow any file size. When asking for versions of a given NuGet package name, the GitLab Package Registry returns a maximum of 300 versions. -## Branch retargeting on merge **(FREE SELF)** +## Branch retargeting on merge > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/320902) in GitLab 13.9. diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md index f2ba3a5a562..b166bb32aa1 100644 --- a/doc/administration/instance_review.md +++ b/doc/administration/instance_review.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Instance Review **(FREE SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Free](https://about.gitlab.com/pricing/) 11.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in GitLab 11.3. If you run a medium-sized self-managed instance (50+ users) of a free version of GitLab, [either Community Edition or unlicensed Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/), diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md index a4fdf734c2e..64d9248cb16 100644 --- a/doc/administration/job_logs.md +++ b/doc/administration/job_logs.md @@ -138,7 +138,7 @@ For more information, see [delete references to missing artifacts](raketasks/che > - Enabled on GitLab.com. > - [Recommended for production use](https://gitlab.com/groups/gitlab-org/-/epics/4275) in GitLab 13.6. > - [Recommended for production use with AWS S3](https://gitlab.com/gitlab-org/gitlab/-/issues/273498) in GitLab 13.7. -> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-incremental-logging). **(FREE SELF)** +> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-incremental-logging). By default job logs are sent from the GitLab Runner in chunks and cached temporarily on disk in `/var/opt/gitlab/gitlab-ci/builds` by Omnibus GitLab. After the job completes, @@ -183,7 +183,7 @@ Here is the detailed data flow: to disk, and there is no protection against misconfiguration. - There is [an epic tracking other potential limitations and improvements](https://gitlab.com/groups/gitlab-org/-/epics/3791). -### Enable or disable incremental logging **(FREE SELF)** +### Enable or disable incremental logging Incremental logging is under development, but [ready for production use as of GitLab 13.6](https://gitlab.com/groups/gitlab-org/-/epics/4275). It is deployed behind a feature flag that is **disabled by default**. diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 7a832509bc2..f5e2ac573dd 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -683,8 +683,8 @@ configuration. #### Encrypted S3 buckets -> - Introduced in [GitLab 13.1](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/466) for instance profiles only and [S3 default encryption](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html). -> - Introduced in [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34460) for static credentials when [consolidated object storage configuration](#consolidated-object-storage-configuration) and [S3 default encryption](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html) are used. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-workhorse/-/merge_requests/466) in GitLab 13.1 for instance profiles only and [S3 default encryption](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html). +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34460) in GitLab 13.2 for static credentials when [consolidated object storage configuration](#consolidated-object-storage-configuration) and [S3 default encryption](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-encryption.html) are used. When configured either with an instance profile or with the consolidated object configuration, GitLab Workhorse properly uploads files to S3 @@ -695,7 +695,7 @@ supported since this requires sending the encryption keys in every request](http ##### Server-side encryption headers -> Introduced in [GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38240). +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38240) in GitLab 13.3. Setting a default encryption on an S3 bucket is the easiest way to enable encryption, but you may want to [set a bucket policy to ensure diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md index 533ebe0ad2f..da3a2e4b34c 100644 --- a/doc/administration/pseudonymizer.md +++ b/doc/administration/pseudonymizer.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Pseudonymizer **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5532) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5532) in GitLab 11.1. As the GitLab database hosts sensitive information, using it unfiltered for analytics implies high security requirements. To help alleviate this constraint, the Pseudonymizer diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 949687cfa0a..15ef024647c 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -53,8 +53,8 @@ _The uploads are stored by default in > **Notes:** > -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Free](https://about.gitlab.com/pricing/) 10.7. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in GitLab 10.5. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) from GitLab Premium to GitLab Free in 10.7. > - Since version 11.1, we support direct_upload to S3. If you don't want to use the local disk where GitLab is installed to store the diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index da2133e1f37..9c7a5a768b6 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1502,6 +1502,7 @@ Input type: `DastProfileUpdateInput` | ---- | ---- | ----------- | | <a id="mutationdastprofileupdatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. | | <a id="mutationdastprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationdastprofileupdatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST profile schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="mutationdastprofileupdatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID`](#dastscannerprofileid) | ID of the scanner profile to be associated. | | <a id="mutationdastprofileupdatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile to be associated. | | <a id="mutationdastprofileupdatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. | @@ -9429,6 +9430,7 @@ Relationship between an epic and an issue. | <a id="epicissueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | <a id="epicissueepicissueid"></a>`epicIssueId` | [`ID!`](#id) | ID of the epic-issue relation. | | <a id="epicissuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | +| <a id="epicissuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="epicissuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | | <a id="epicissuehumantotaltimespent"></a>`humanTotalTimeSpent` | [`String`](#string) | Human-readable total time reported as spent on the issue. | | <a id="epicissueid"></a>`id` | [`ID`](#id) | Global ID of the epic-issue relation. | @@ -10555,6 +10557,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | <a id="issueemailsdisabled"></a>`emailsDisabled` | [`Boolean!`](#boolean) | Indicates if a project has email notifications disabled: `true` if email notifications are disabled. | | <a id="issueepic"></a>`epic` | [`Epic`](#epic) | Epic to which this issue belongs. | | <a id="issuehealthstatus"></a>`healthStatus` | [`HealthStatus`](#healthstatus) | Current health status. | +| <a id="issuehidden"></a>`hidden` | [`Boolean`](#boolean) | Indicates the issue is hidden because the author has been banned. Will always return `null` if `ban_user_feature_flag` feature flag is disabled. | | <a id="issuehumantimeestimate"></a>`humanTimeEstimate` | [`String`](#string) | Human-readable time estimate of the issue. | | <a id="issuehumantotaltimespent"></a>`humanTotalTimeSpent` | [`String`](#string) | Human-readable total time reported as spent on the issue. | | <a id="issueid"></a>`id` | [`ID!`](#id) | ID of the issue. | diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 0e2cca507ba..4e588ee9db8 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -191,9 +191,9 @@ module Gitlab # Returns an Array of Tags # - def tags + def tags(sort_by: nil) wrapped_gitaly_errors do - gitaly_ref_client.tags + gitaly_ref_client.tags(sort_by: sort_by) end end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index efcbbb17d86..6ba68a92dcd 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -5,6 +5,16 @@ module Gitlab class RefService include Gitlab::EncodingHelper + TAGS_SORT_KEY = { + 'name' => Gitaly::FindAllTagsRequest::SortBy::Key::REFNAME, + 'updated' => Gitaly::FindAllTagsRequest::SortBy::Key::CREATORDATE + }.freeze + + TAGS_SORT_DIRECTION = { + 'asc' => Gitaly::SortDirection::ASCENDING, + 'desc' => Gitaly::SortDirection::DESCENDING + }.freeze + # 'repository' is a Gitlab::Git::Repository def initialize(repository) @repository = repository @@ -84,13 +94,15 @@ module Gitlab def local_branches(sort_by: nil, pagination_params: nil) request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo, pagination_params: pagination_params) - request.sort_by = sort_by_param(sort_by) if sort_by + request.sort_by = sort_local_branches_by_param(sort_by) if sort_by response = GitalyClient.call(@storage, :ref_service, :find_local_branches, request, timeout: GitalyClient.fast_timeout) consume_find_local_branches_response(response) end - def tags + def tags(sort_by: nil) request = Gitaly::FindAllTagsRequest.new(repository: @gitaly_repo) + request.sort_by = sort_tags_by_param(sort_by) if sort_by + response = GitalyClient.call(@storage, :ref_service, :find_all_tags, request, timeout: GitalyClient.medium_timeout) consume_tags_response(response) end @@ -201,7 +213,7 @@ module Gitlab response.flat_map { |message| message.names.map { |name| yield(name) } } end - def sort_by_param(sort_by) + def sort_local_branches_by_param(sort_by) sort_by = 'name' if sort_by == 'name_asc' enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) @@ -210,6 +222,17 @@ module Gitlab enum_value end + def sort_tags_by_param(sort_by) + match = sort_by.match(/^(?<key>name|updated)_(?<direction>asc|desc)$/) + + return unless match + + Gitaly::FindAllTagsRequest::SortBy.new( + key: TAGS_SORT_KEY[match[:key]], + direction: TAGS_SORT_DIRECTION[match[:direction]] + ) + end + def consume_find_local_branches_response(response) response.flat_map do |message| message.branches.map do |gitaly_branch| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d288e93ee60..f9d9a219e3c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5397,6 +5397,21 @@ msgstr "" msgid "Board scope affects which issues are displayed for anyone who visits this board" msgstr "" +msgid "BoardNewEpic|Groups" +msgstr "" + +msgid "BoardNewEpic|Loading groups" +msgstr "" + +msgid "BoardNewEpic|No matching results" +msgstr "" + +msgid "BoardNewEpic|Search groups" +msgstr "" + +msgid "BoardNewEpic|Select a group" +msgstr "" + msgid "BoardNewIssue|No matching results" msgstr "" @@ -5474,6 +5489,9 @@ msgstr "" msgid "Boards|An error occurred while creating the list. Please try again." msgstr "" +msgid "Boards|An error occurred while fetching child groups. Please try again." +msgstr "" + msgid "Boards|An error occurred while fetching group projects. Please try again." msgstr "" @@ -38567,6 +38585,9 @@ msgstr "" msgid "You need to upload a GitLab project export archive (ending in .gz)." msgstr "" +msgid "You need to verify your primary email first before enabling Two-Factor Authentication." +msgstr "" + msgid "You successfully declined the invitation" msgstr "" @@ -36,6 +36,7 @@ module QA "rspec" => "RSpec", "web_ide" => "WebIDE", "ci_cd" => "CiCd", + "project_imported_from_url" => "ProjectImportedFromURL", "repo_by_url" => "RepoByURL", "oauth" => "OAuth", "saml_sso_sign_in" => "SamlSSOSignIn", diff --git a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb index 347d4d6732d..3ec76e8afad 100644 --- a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Package' do - describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337791', type: :waiting_on } do + describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] } do let(:group) { Resource::Group.fabricate_via_api! } let(:imported_project) do diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 9df88e860be..073180cbafd 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -10,8 +10,33 @@ RSpec.describe Profiles::TwoFactorAuthsController do allow(subject).to receive(:current_user).and_return(user) end + shared_examples 'user must first verify their primary email address' do + before do + allow(user).to receive(:primary_email_verified?).and_return(false) + end + + it 'redirects to profile_emails_path' do + go + + expect(response).to redirect_to(profile_emails_path) + end + + it 'displays a notice' do + go + + expect(flash[:notice]) + .to eq _('You need to verify your primary email first before enabling Two-Factor Authentication.') + end + + it 'does not redirect when the `ensure_verified_primary_email_for_2fa` feature flag is disabled' do + stub_feature_flags(ensure_verified_primary_email_for_2fa: false) + + expect(response).not_to redirect_to(profile_emails_path) + end + end + describe 'GET show' do - let(:user) { create(:user) } + let_it_be_with_reload(:user) { create(:user) } it 'generates otp_secret for user' do expect(User).to receive(:generate_otp_secret).with(32).and_call_original.once @@ -34,11 +59,16 @@ RSpec.describe Profiles::TwoFactorAuthsController do get :show end end + + it_behaves_like 'user must first verify their primary email address' do + let(:go) { get :show } + end end describe 'POST create' do - let(:user) { create(:user) } - let(:pin) { 'pin-code' } + let_it_be_with_reload(:user) { create(:user) } + + let(:pin) { 'pin-code' } def go post :create, params: { pin_code: pin } @@ -105,10 +135,12 @@ RSpec.describe Profiles::TwoFactorAuthsController do expect(response).to render_template(:show) end end + + it_behaves_like 'user must first verify their primary email address' end describe 'POST codes' do - let(:user) { create(:user, :two_factor) } + let_it_be_with_reload(:user) { create(:user, :two_factor) } it 'presents plaintext codes for the user to save' do expect(user).to receive(:generate_otp_backup_codes!).and_return(%w(a b c)) @@ -135,7 +167,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do subject { delete :destroy } context 'for a user that has 2FA enabled' do - let(:user) { create(:user, :two_factor) } + let_it_be_with_reload(:user) { create(:user, :two_factor) } it 'disables two factor' do subject @@ -158,7 +190,7 @@ RSpec.describe Profiles::TwoFactorAuthsController do end context 'for a user that does not have 2FA enabled' do - let(:user) { create(:user) } + let_it_be_with_reload(:user) { create(:user) } it 'redirects to profile_account_path' do subject diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 20b3ec9d9bb..bec474f6cfe 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -5,35 +5,40 @@ require 'spec_helper' RSpec.describe 'Value Stream Analytics', :js do let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) } - let_it_be(:project) { create(:project, :repository) } let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' } + let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' } let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" } + let_it_be(:metric_value_selector) { "[data-testid='displayValue']" } + let(:stage_table) { page.find(stage_table_selector) } + let(:project) { create(:project, :repository) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:milestone) { create(:milestone, project: project) } let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } + def metrics_values + page.find(metrics_selector).all(metric_value_selector).collect(&:text) + end + + def set_daterange(from_date, to_date) + page.find(".js-daterange-picker-from input").set(from_date) + page.find(".js-daterange-picker-to input").set(to_date) + wait_for_all_requests + end + context 'as an allowed user' do context 'when project is new' do - before(:all) do - project.add_maintainer(user) - end - before do + project.add_maintainer(user) sign_in(user) visit project_cycle_analytics_path(project) wait_for_requests end - it 'displays metrics' do - aggregate_failures 'with relevant values' do - expect(new_issues_counter).to have_content('-') - expect(commits_counter).to have_content('-') - expect(deploys_counter).to have_content('-') - expect(deployment_frequency_counter).to have_content('-') - end + it 'displays metrics with relevant values' do + expect(metrics_values).to eq(['-'] * 4) end it 'shows active stage with empty message' do @@ -43,14 +48,21 @@ RSpec.describe 'Value Stream Analytics', :js do end context "when there's value stream analytics data" do + # NOTE: in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68595 travel back + # 5 days in time before we create data for these specs, to mitigate some flakiness + # So setting the date range to be the last 2 days should skip past the existing data + from = 2.days.ago.strftime("%Y-%m-%d") + to = 1.day.ago.strftime("%Y-%m-%d") + around do |example| travel_to(5.days.ago) { example.run } end before do project.add_maintainer(user) + create_list(:issue, 2, project: project, created_at: 2.weeks.ago, milestone: milestone) - @build = create_cycle(user, project, issue, mr, milestone, pipeline) + create_cycle(user, project, issue, mr, milestone, pipeline) deploy_master(user, project) issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour) @@ -65,6 +77,8 @@ RSpec.describe 'Value Stream Analytics', :js do sign_in(user) visit project_cycle_analytics_path(project) + + wait_for_requests end it 'displays metrics' do @@ -97,18 +111,20 @@ RSpec.describe 'Value Stream Analytics', :js do expect_merge_request_to_be_present end - context "when I change the time period observed" do - before do - _two_weeks_old_issue = create(:issue, project: project, created_at: 2.weeks.ago) + it 'can filter the issues by date' do + expect(stage_table.all(stage_table_event_selector).length).to eq(3) - click_button('Last 30 days') - click_link('Last 7 days') - wait_for_requests - end + set_daterange(from, to) - it 'shows only relevant data' do - expect(new_issue_counter).to have_content('1') - end + expect(stage_table.all(stage_table_event_selector).length).to eq(0) + end + + it 'can filter the metrics by date' do + expect(metrics_values).to eq(["3.0", "2.0", "1.0", "0.0"]) + + set_daterange(from, to) + + expect(metrics_values).to eq(['-'] * 4) end end end @@ -141,31 +157,6 @@ RSpec.describe 'Value Stream Analytics', :js do end end - def find_metric_tile(sel) - page.find("#{metrics_selector} #{sel}") - end - - # When now use proper pluralization for the metric names, which affects the id - def new_issue_counter - find_metric_tile("#new-issue") - end - - def new_issues_counter - find_metric_tile("#new-issues") - end - - def commits_counter - find_metric_tile("#commits") - end - - def deploys_counter - find_metric_tile("#deploys") - end - - def deployment_frequency_counter - find_metric_tile("#deployment-frequency") - end - def expect_issue_to_be_present expect(find(stage_table_selector)).to have_content(issue.title) expect(find(stage_table_selector)).to have_content(issue.author.name) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index fa77dd03b09..0b2811618b5 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -46,4 +46,38 @@ RSpec.describe 'Dashboard Issues' do it_behaves_like "it has an RSS button with current_user's feed token" it_behaves_like "an autodiscoverable RSS feed with current_user's feed token" end + + describe 'new issue dropdown' do + it 'shows projects only with issues feature enabled', :js do + find('.new-project-item-select-button').click + + page.within('.select2-results') do + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_issues_disabled.full_name) + end + end + + it 'shows the new issue page', :js do + find('.new-project-item-select-button').click + + wait_for_requests + + project_path = "/#{project.full_path}" + project_json = { name: project.full_name, url: project_path }.to_json + + # simulate selection, and prevent overlap by dropdown menu + first('.project-item-select', visible: false) + execute_script("$('.project-item-select').val('#{project_json}').trigger('change');") + find('#select2-drop-mask', visible: false) + execute_script("$('#select2-drop-mask').remove();") + + find('.new-project-item-link').click + + expect(page).to have_current_path("#{project_path}/-/issues/new") + + page.within('#content-body') do + expect(page).to have_selector('.issue-form') + end + end + end end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 95e87933110..aa2485d4236 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -25,6 +25,24 @@ RSpec.describe 'Dashboard Merge Requests' do expect(page).not_to have_selector('#js-dropdown-target-branch', visible: false) end + context 'new merge request dropdown' do + let(:project_with_disabled_merge_requests) { create(:project, :merge_requests_disabled) } + + before do + project_with_disabled_merge_requests.add_maintainer(current_user) + visit merge_requests_dashboard_path + end + + it 'shows projects only with merge requests feature enabled', :js do + find('.new-project-item-select-button').click + + page.within('.select2-results') do + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_disabled_merge_requests.full_name) + end + end + end + context 'no merge requests exist' do it 'shows an empty state' do visit merge_requests_dashboard_path(assignee_username: current_user.username) diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index 3f8d048efd6..992ed2f2ce6 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -32,6 +32,20 @@ RSpec.describe 'Dashboard > Milestones' do expect(page).to have_content(group.name) expect(first('.milestone')).to have_content('Merge requests') end + + describe 'new milestones dropdown', :js do + it 'takes user to a new milestone page', :js do + find('.new-project-item-select-button').click + + page.within('.select2-results') do + first('.select2-result-label').click + end + + find('.new-project-item-link').click + + expect(current_path).to eq(new_group_milestone_path(group)) + end + end end describe 'with merge requests disabled' do diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index 092e6179732..4488f53a03f 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -92,6 +92,20 @@ RSpec.describe 'Group empty states' do it 'displays an empty state' do expect(page).to have_selector('.empty-state') end + + it "shows a new #{issuable_name} button" do + within '.empty-state' do + expect(page).to have_content("create #{issuable_name}") + end + end + + it "the new #{issuable_name} button opens a project dropdown" do + within '.empty-state' do + find('.new-project-item-select-button').click + end + + expect(page).to have_selector('.ajax-project-dropdown') + end end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 455d6638f2d..d2cdaaa43b9 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -131,6 +131,28 @@ RSpec.describe 'Group issues page' do end end + context 'projects with issues disabled' do + describe 'issue dropdown' do + let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user } + + before do + [project, project_with_issues_disabled].each { |project| project.add_maintainer(user_in_group) } + sign_in(user_in_group) + visit issues_group_path(group) + end + + it 'shows projects only with issues feature enabled', :js do + find('.empty-state .js-lazy-loaded') + find('.empty-state .new-project-item-link').click + + page.within('.select2-results') do + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_issues_disabled.full_name) + end + end + end + end + context 'manual ordering', :js do let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user } diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index aad9be0ec8d..077f680629f 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -59,6 +59,23 @@ RSpec.describe 'Group merge requests page' do end end + describe 'new merge request dropdown' do + let(:project_with_merge_requests_disabled) { create(:project, :merge_requests_disabled, group: group) } + + before do + visit path + end + + it 'shows projects only with merge requests feature enabled', :js do + find('.new-project-item-link').click + + page.within('.select2-results') do + expect(page).to have_content(project.name_with_namespace) + expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace) + end + end + end + context 'empty state with no merge requests' do before do MergeRequest.delete_all @@ -68,6 +85,7 @@ RSpec.describe 'Group merge requests page' do visit path expect(page).to have_selector('.empty-state') + expect(page).to have_link('Select project to create merge request') expect(page).not_to have_selector('.issues-filters') end @@ -77,6 +95,7 @@ RSpec.describe 'Group merge requests page' do visit path expect(page).to have_selector('.empty-state') + expect(page).to have_link('Select project to create merge request') expect(page).to have_selector('.issues-filters') end end diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index a2ba1e9eb5e..96306b966fa 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -545,7 +545,7 @@ describe('Board Store Mutations', () => { expect(state.groupProjectsFlags.isLoading).toBe(true); }); - it('Should set isLoading in groupProjectsFlags to true in state when fetchNext is true', () => { + it('Should set isLoadingMore in groupProjectsFlags to true in state when fetchNext is true', () => { mutations[types.REQUEST_GROUP_PROJECTS](state, true); expect(state.groupProjectsFlags.isLoadingMore).toBe(true); diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js index 71830eed3ef..5d3361bfa35 100644 --- a/spec/frontend/cycle_analytics/base_spec.js +++ b/spec/frontend/cycle_analytics/base_spec.js @@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import BaseComponent from '~/cycle_analytics/components/base.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue'; +import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue'; import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue'; import { NOT_ENOUGH_DATA_ERROR } from '~/cycle_analytics/constants'; import initState from '~/cycle_analytics/store/state'; @@ -30,13 +31,14 @@ Vue.use(Vuex); let wrapper; +const { id: groupId, path: groupPath } = currentGroup; const defaultState = { permissions, currentGroup, createdBefore, createdAfter, stageCounts, - endpoints: { fullPath }, + endpoints: { fullPath, groupId, groupPath }, }; function createStore({ initialState = {}, initialGetters = {} }) { @@ -74,6 +76,7 @@ function createComponent({ initialState, initialGetters } = {}) { const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPathNavigation = () => wrapper.findComponent(PathNavigation); +const findFilters = () => wrapper.findComponent(ValueStreamFilters); const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics); const findStageTable = () => wrapper.findComponent(StageTable); const findStageEvents = () => findStageTable().props('stageEvents'); @@ -123,6 +126,29 @@ describe('Value stream analytics component', () => { expect(findStageEvents()).toEqual(selectedStageEvents); }); + it('renders the filters', () => { + expect(findFilters().exists()).toBe(true); + }); + + it('displays the date range selector and hides the project selector', () => { + expect(findFilters().props()).toMatchObject({ + hasProjectFilter: false, + hasDateRangeFilter: true, + }); + }); + + it('passes the paths to the filter bar', () => { + expect(findFilters().props()).toEqual({ + groupId, + groupPath, + endDate: createdBefore, + hasDateRangeFilter: true, + hasProjectFilter: false, + selectedProjects: [], + startDate: createdAfter, + }); + }); + it('does not render the loading icon', () => { expect(findLoadingIcon().exists()).toBe(false); }); diff --git a/spec/frontend/cycle_analytics/store/actions_spec.js b/spec/frontend/cycle_analytics/store/actions_spec.js index 915a828ff19..a4b638ddd42 100644 --- a/spec/frontend/cycle_analytics/store/actions_spec.js +++ b/spec/frontend/cycle_analytics/store/actions_spec.js @@ -4,21 +4,41 @@ import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/cycle_analytics/store/actions'; import * as getters from '~/cycle_analytics/store/getters'; import httpStatusCodes from '~/lib/utils/http_status'; -import { allowedStages, selectedStage, selectedValueStream } from '../mock_data'; - +import { + allowedStages, + selectedStage, + selectedValueStream, + currentGroup, + createdAfter, + createdBefore, +} from '../mock_data'; + +const { id: groupId, path: groupPath } = currentGroup; +const mockMilestonesPath = 'mock-milestones'; +const mockLabelsPath = 'mock-labels'; const mockRequestPath = 'some/cool/path'; const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams'; -const mockStartDate = 30; -const mockEndpoints = { fullPath: mockFullPath, requestPath: mockRequestPath }; -const mockSetDateActionCommit = { payload: { startDate: mockStartDate }, type: 'SET_DATE_RANGE' }; - -const defaultState = { ...getters, selectedValueStream }; +const mockEndpoints = { + fullPath: mockFullPath, + requestPath: mockRequestPath, + labelsPath: mockLabelsPath, + milestonesPath: mockMilestonesPath, + groupId, + groupPath, +}; +const mockSetDateActionCommit = { + payload: { createdAfter, createdBefore }, + type: 'SET_DATE_RANGE', +}; + +const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore }; describe('Project Value Stream Analytics actions', () => { let state; let mock; beforeEach(() => { + state = { ...defaultState }; mock = new MockAdapter(axios); }); @@ -34,16 +54,17 @@ describe('Project Value Stream Analytics actions', () => { { type: 'fetchCycleAnalyticsData' }, { type: 'fetchStageData' }, { type: 'fetchStageMedians' }, + { type: 'fetchStageCountValues' }, { type: 'setLoading', payload: false }, ]; describe.each` - action | payload | expectedActions | expectedMutations - ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]} - ${'setDateRange'} | ${{ startDate: mockStartDate }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]} - ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]} - ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]} - ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]} + action | payload | expectedActions | expectedMutations + ${'setLoading'} | ${true} | ${[]} | ${[{ type: 'SET_LOADING', payload: true }]} + ${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${mockFetchStageDataActions} | ${[mockSetDateActionCommit]} + ${'setFilters'} | ${[]} | ${mockFetchStageDataActions} | ${[]} + ${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'fetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]} + ${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }, { type: 'fetchCycleAnalyticsData' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]} `('$action', ({ action, payload, expectedActions, expectedMutations }) => { const types = mutationTypes(expectedMutations); it(`will dispatch ${expectedActions} and commit ${types}`, () => @@ -60,6 +81,12 @@ describe('Project Value Stream Analytics actions', () => { let mockDispatch; let mockCommit; const payload = { endpoints: mockEndpoints }; + const mockFilterEndpoints = { + groupEndpoint: 'foo', + labelsEndpoint: 'mock-labels.json', + milestonesEndpoint: 'mock-milestones.json', + projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams', + }; beforeEach(() => { mockDispatch = jest.fn(() => Promise.resolve()); @@ -76,6 +103,9 @@ describe('Project Value Stream Analytics actions', () => { payload, ); expect(mockCommit).toHaveBeenCalledWith('INITIALIZE_VSA', { endpoints: mockEndpoints }); + + expect(mockDispatch).toHaveBeenCalledTimes(4); + expect(mockDispatch).toHaveBeenCalledWith('filters/setEndpoints', mockFilterEndpoints); expect(mockDispatch).toHaveBeenCalledWith('setLoading', true); expect(mockDispatch).toHaveBeenCalledWith('fetchValueStreams'); expect(mockDispatch).toHaveBeenCalledWith('setLoading', false); @@ -84,7 +114,7 @@ describe('Project Value Stream Analytics actions', () => { describe('fetchCycleAnalyticsData', () => { beforeEach(() => { - state = { endpoints: mockEndpoints }; + state = { ...defaultState, endpoints: mockEndpoints }; mock = new MockAdapter(axios); mock.onGet(mockRequestPath).reply(httpStatusCodes.OK); }); @@ -129,7 +159,6 @@ describe('Project Value Stream Analytics actions', () => { state = { ...defaultState, endpoints: mockEndpoints, - startDate: mockStartDate, selectedStage, }; mock = new MockAdapter(axios); @@ -152,7 +181,6 @@ describe('Project Value Stream Analytics actions', () => { state = { ...defaultState, endpoints: mockEndpoints, - startDate: mockStartDate, selectedStage, }; mock = new MockAdapter(axios); @@ -177,7 +205,6 @@ describe('Project Value Stream Analytics actions', () => { state = { ...defaultState, endpoints: mockEndpoints, - startDate: mockStartDate, selectedStage, }; mock = new MockAdapter(axios); diff --git a/spec/frontend/cycle_analytics/store/mutations_spec.js b/spec/frontend/cycle_analytics/store/mutations_spec.js index 7fcfef98547..628e2a4e7ae 100644 --- a/spec/frontend/cycle_analytics/store/mutations_spec.js +++ b/spec/frontend/cycle_analytics/store/mutations_spec.js @@ -1,5 +1,4 @@ import { useFakeDate } from 'helpers/fake_date'; -import { DEFAULT_DAYS_TO_DISPLAY } from '~/cycle_analytics/constants'; import * as types from '~/cycle_analytics/store/mutation_types'; import mutations from '~/cycle_analytics/store/mutations'; import { @@ -65,15 +64,16 @@ describe('Project Value Stream Analytics mutations', () => { expect(state).toMatchObject({ [stateKey]: value }); }); + const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore }; const mockInitialPayload = { endpoints: { requestPath: mockRequestPath }, currentGroup: { title: 'cool-group' }, id: 1337, + ...mockSetDatePayload, }; const mockInitializedObj = { endpoints: { requestPath: mockRequestPath }, - createdAfter: mockCreatedAfter, - createdBefore: mockCreatedBefore, + ...mockSetDatePayload, }; it.each` @@ -89,9 +89,8 @@ describe('Project Value Stream Analytics mutations', () => { it.each` mutation | payload | stateKey | value - ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'daysInPast'} | ${DEFAULT_DAYS_TO_DISPLAY} - ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdAfter'} | ${mockCreatedAfter} - ${types.SET_DATE_RANGE} | ${DEFAULT_DAYS_TO_DISPLAY} | ${'createdBefore'} | ${mockCreatedBefore} + ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter} + ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore} ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false} ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index b0aa11ee5ad..559f347810b 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Issue'] do it 'has specific fields' do fields = %i[id iid title description state reference author assignees updated_by participants labels milestone due_date - confidential discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position + confidential hidden discussion_locked upvotes downvotes merge_requests_count user_notes_count user_discussions_count web_path web_url relative_position emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status design_collection alert_management_alert severity current_user_todos moved moved_to create_note_email timelogs project_id] @@ -201,4 +201,54 @@ RSpec.describe GitlabSchema.types['Issue'] do end end end + + describe 'hidden', :enable_admin_mode do + let_it_be(:admin) { create(:user, :admin)} + let_it_be(:banned_user) { create(:user, :banned) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:hidden_issue) { create(:issue, project: project, author: banned_user) } + let_it_be(:visible_issue) { create(:issue, project: project, author: user) } + + let(:issue) { hidden_issue } + let(:query) do + %( + query { + project(fullPath: "#{project.full_path}") { + issue(iid: "#{issue.iid}") { + hidden + } + } + } + ) + end + + subject { GitlabSchema.execute(query, context: { current_user: admin }).as_json } + + context 'when `ban_user_feature_flag` is enabled' do + context 'when issue is hidden' do + it 'returns `true`' do + expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(true) + end + end + + context 'when issue is visible' do + let(:issue) { visible_issue } + + it 'returns `false`' do + expect(subject.dig('data', 'project', 'issue', 'hidden')).to eq(false) + end + end + end + + context 'when `ban_user_feature_flag` is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it 'returns `nil`' do + expect(subject.dig('data', 'project', 'issue', 'hidden')).to be_nil + end + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index fc4fc02ccf9..9ecd281cce0 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -109,6 +109,32 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names end + describe '#tags' do + subject { repository.tags } + + it 'gets tags from GitalyClient' do + expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| + expect(service).to receive(:tags) + end + + subject + end + + context 'with sorting option' do + subject { repository.tags(sort_by: 'name_asc') } + + it 'gets tags from GitalyClient' do + expect_next_instance_of(Gitlab::GitalyClient::RefService) do |service| + expect(service).to receive(:tags).with(sort_by: 'name_asc') + end + + subject + end + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tags + end + describe '#archive_metadata' do let(:storage_path) { '/tmp' } let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) } diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index cc18ce573b5..09e3692626c 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -154,6 +154,22 @@ RSpec.describe Gitlab::GitalyClient::RefService do client.tags end + + context 'with sorting option' do + it 'sends a correct find_all_tags message' do + expected_sort_by = Gitaly::FindAllTagsRequest::SortBy.new( + key: :REFNAME, + direction: :ASCENDING + ) + + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_all_tags) + .with(gitaly_request_with_params(sort_by: expected_sort_by), kind_of(Hash)) + .and_return([]) + + client.tags(sort_by: 'name_asc') + end + end end describe '#branch_names_contains_sha' do diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb index 7049e64c2ce..9c3ff7aa35b 100644 --- a/spec/models/integrations/datadog_spec.rb +++ b/spec/models/integrations/datadog_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Integrations::Datadog do let(:active) { true } let(:dd_site) { 'datadoghq.com' } - let(:default_url) { 'https://webhooks-http-intake.logs.datadoghq.com/api/v2/webhook' } + let(:default_url) { 'https://webhook-intake.datadoghq.com/api/v2/webhook' } let(:api_url) { '' } let(:api_key) { SecureRandom.hex(32) } let(:dd_env) { 'ci' } @@ -66,7 +66,7 @@ RSpec.describe Integrations::Datadog do context 'with custom api_url' do let(:dd_site) { '' } - let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/api/v2/webhook' } + let(:api_url) { 'https://webhook-intake.datad0g.com/api/v2/webhook' } it { is_expected.not_to validate_presence_of(:datadog_site) } it { is_expected.to validate_presence_of(:api_url) } @@ -108,7 +108,7 @@ RSpec.describe Integrations::Datadog do end context 'with custom URL' do - let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/api/v2/webhook' } + let(:api_url) { 'https://webhook-intake.datad0g.com/api/v2/webhook' } it { is_expected.to eq(api_url + "?dd-api-key=#{api_key}&env=#{dd_env}&service=#{dd_service}") } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 949892beed5..907919c7b82 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -68,51 +68,69 @@ RSpec.describe Repository do describe 'tags_sorted_by' do let(:tags_to_compare) { %w[v1.0.0 v1.1.0] } + let(:feature_flag) { true } + + before do + stub_feature_flags(gitaly_tags_finder: feature_flag) + end context 'name_desc' do subject { repository.tags_sorted_by('name_desc').map(&:name) & tags_to_compare } it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } + + context 'when feature flag is disabled' do + let(:feature_flag) { false } + + it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } + end end context 'name_asc' do subject { repository.tags_sorted_by('name_asc').map(&:name) & tags_to_compare } it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + + context 'when feature flag is disabled' do + let(:feature_flag) { false } + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + end end context 'updated' do - let(:tag_a) { repository.find_tag('v1.0.0') } - let(:tag_b) { repository.find_tag('v1.1.0') } + let(:latest_tag) { 'v0.0.0' } + + before do + rugged_repo(repository).tags.create(latest_tag, repository.commit.id) + end + + after do + rugged_repo(repository).tags.delete(latest_tag) + end context 'desc' do - subject { repository.tags_sorted_by('updated_desc').map(&:name) } + subject { repository.tags_sorted_by('updated_desc').map(&:name) & (tags_to_compare + [latest_tag]) } - before do - double_first = double(committed_date: Time.current) - double_last = double(committed_date: Time.current - 1.second) + it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) } - allow(tag_a).to receive(:dereferenced_target).and_return(double_first) - allow(tag_b).to receive(:dereferenced_target).and_return(double_last) - allow(repository).to receive(:tags).and_return([tag_a, tag_b]) - end + context 'when feature flag is disabled' do + let(:feature_flag) { false } - it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + it { is_expected.to eq([latest_tag, 'v1.1.0', 'v1.0.0']) } + end end context 'asc' do - subject { repository.tags_sorted_by('updated_asc').map(&:name) } + subject { repository.tags_sorted_by('updated_asc').map(&:name) & (tags_to_compare + [latest_tag]) } - before do - double_first = double(committed_date: Time.current - 1.second) - double_last = double(committed_date: Time.current) + it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) } - allow(tag_a).to receive(:dereferenced_target).and_return(double_last) - allow(tag_b).to receive(:dereferenced_target).and_return(double_first) - allow(repository).to receive(:tags).and_return([tag_a, tag_b]) - end + context 'when feature flag is disabled' do + let(:feature_flag) { false } - it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } + it { is_expected.to eq(['v1.0.0', 'v1.1.0', latest_tag]) } + end end context 'annotated tag pointing to a blob' do @@ -125,21 +143,33 @@ RSpec.describe Repository do tagger: { name: 'John Smith', email: 'john@gmail.com' } } rugged_repo(repository).tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', **options) + end - double_first = double(committed_date: Time.current - 1.second) - double_last = double(committed_date: Time.current) + it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) } - allow(tag_a).to receive(:dereferenced_target).and_return(double_last) - allow(tag_b).to receive(:dereferenced_target).and_return(double_first) - end + context 'when feature flag is disabled' do + let(:feature_flag) { false } - it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) } + it { is_expected.to eq(['v1.0.0', 'v1.1.0', annotated_tag_name]) } + end after do rugged_repo(repository).tags.delete(annotated_tag_name) end end end + + context 'unknown option' do + subject { repository.tags_sorted_by('unknown_desc').map(&:name) & tags_to_compare } + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + + context 'when feature flag is disabled' do + let(:feature_flag) { false } + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + end + end end describe '#ref_exists?' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 26f0b252bbc..50520f9bce4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -109,7 +109,6 @@ RSpec.describe User do it { is_expected.to have_many(:spam_logs).dependent(:destroy) } it { is_expected.to have_many(:todos) } it { is_expected.to have_many(:award_emoji).dependent(:destroy) } - it { is_expected.to have_many(:triggers).dependent(:destroy) } it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } it { is_expected.to have_many(:chat_names).dependent(:destroy) } |