diff options
44 files changed, 835 insertions, 254 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue index 279fcfe736f..b16d960402b 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue @@ -4,7 +4,11 @@ import { GlAlert } from '@gitlab/ui'; import { mapKeys, mapValues, pick, some, sum } from 'lodash'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; import { s__ } from '~/locale'; -import { formatDateAsMonth, getDayDifference } from '~/lib/utils/datetime_utility'; +import { + differenceInMonths, + formatDateAsMonth, + getDayDifference, +} from '~/lib/utils/datetime_utility'; import { getAverageByMonth, sortByDate, extractValues } from '../utils'; import pipelineStatsQuery from '../graphql/queries/pipeline_stats.query.graphql'; import { TODAY, START_DATE } from '../constants'; @@ -150,19 +154,14 @@ export default { max: this.$options.endDate, }; }, - differenceInMonths() { - const yearDiff = this.$options.endDate.getYear() - this.$options.startDate.getYear(); - const monthDiff = this.$options.endDate.getMonth() - this.$options.startDate.getMonth(); - - return monthDiff + 12 * yearDiff; - }, chartOptions() { + const { endDate, startDate, i18n } = this.$options; return { xAxis: { ...this.range, - name: this.$options.i18n.xAxisTitle, + name: i18n.xAxisTitle, type: 'time', - splitNumber: this.differenceInMonths + 1, + splitNumber: differenceInMonths(startDate, endDate) + 1, axisLabel: { interval: 0, showMinLabel: false, @@ -172,7 +171,7 @@ export default { }, }, yAxis: { - name: this.$options.i18n.yAxisTitle, + name: i18n.yAxisTitle, }, }; }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue index bbcb866c758..53fac09ab66 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlButton } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; @@ -8,6 +8,7 @@ import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; export default { components: { GlModal, + GlButton, FileIcon, ChangedFileIcon, }, @@ -52,15 +53,16 @@ export default { </strong> <changed-file-icon :file="activeFile" :is-centered="false" /> <div class="ml-auto"> - <button + <gl-button v-if="canDiscard" ref="discardButton" - type="button" - class="btn btn-remove btn-inverted gl-mr-3" + category="secondary" + variant="danger" + class="gl-mr-3" @click="showDiscardModal" > {{ __('Discard changes') }} - </button> + </gl-button> </div> <gl-modal ref="discardModal" diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue index 97b96cb5839..f7c0bd5ae13 100644 --- a/app/assets/javascripts/logs/components/environment_logs.vue +++ b/app/assets/javascripts/logs/components/environment_logs.vue @@ -3,12 +3,11 @@ import { throttle } from 'lodash'; import { mapActions, mapState, mapGetters } from 'vuex'; import { GlSprintf, - GlIcon, GlAlert, - GlDeprecatedDropdown, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, - GlDeprecatedDropdownDivider, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, + GlDropdownDivider, GlInfiniteScroll, } from '@gitlab/ui'; @@ -23,12 +22,11 @@ import { formatDate } from '../utils'; export default { components: { GlSprintf, - GlIcon, GlAlert, - GlDeprecatedDropdown, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, - GlDeprecatedDropdownDivider, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, + GlDropdownDivider, GlInfiniteScroll, LogSimpleFilters, LogAdvancedFilters, @@ -174,46 +172,38 @@ export default { <div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2"> <div class="flex-grow-0"> - <gl-deprecated-dropdown + <gl-dropdown id="environments-dropdown" :text="environments.current || managedApps.current" :disabled="environments.isLoading" - class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown" + class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block js-environments-dropdown" > - <gl-deprecated-dropdown-header class="gl-text-center"> + <gl-dropdown-section-header> {{ s__('Environments|Environments') }} - </gl-deprecated-dropdown-header> - <gl-deprecated-dropdown-item + </gl-dropdown-section-header> + <gl-dropdown-item v-for="env in environments.options" :key="env.id" + :is-check-item="true" + :is-checked="isCurrentEnvironment(env.name)" @click="showEnvironment(env.name)" > - <div class="d-flex"> - <gl-icon - :class="{ invisible: !isCurrentEnvironment(env.name) }" - name="status_success_borderless" - /> - <div class="gl-flex-grow-1">{{ env.name }}</div> - </div> - </gl-deprecated-dropdown-item> - <gl-deprecated-dropdown-divider /> - <gl-deprecated-dropdown-header class="gl-text-center"> + {{ env.name }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-section-header> {{ s__('Environments|Managed apps') }} - </gl-deprecated-dropdown-header> - <gl-deprecated-dropdown-item + </gl-dropdown-section-header> + <gl-dropdown-item v-for="app in managedApps.options" :key="app.id" + :is-check-item="true" + :is-checked="isCurrentManagedApp(app.name)" @click="showManagedApp(app.name)" > - <div class="gl-display-flex"> - <gl-icon - :class="{ invisible: !isCurrentManagedApp(app.name) }" - name="status_success_borderless" - /> - <div class="gl-flex-grow-1">{{ app.name }}</div> - </div> - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> + {{ app.name }} + </gl-dropdown-item> + </gl-dropdown> </div> <log-advanced-filters diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue index 2e1270b5428..ba30d4628c9 100644 --- a/app/assets/javascripts/logs/components/log_simple_filters.vue +++ b/app/assets/javascripts/logs/components/log_simple_filters.vue @@ -1,19 +1,13 @@ <script> import { mapActions, mapState } from 'vuex'; -import { - GlIcon, - GlDeprecatedDropdown, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, -} from '@gitlab/ui'; +import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; export default { components: { - GlIcon, - GlDeprecatedDropdown, - GlDeprecatedDropdownHeader, - GlDeprecatedDropdownItem, + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, }, props: { disabled: { @@ -44,35 +38,31 @@ export default { </script> <template> <div> - <gl-deprecated-dropdown + <gl-dropdown ref="podsDropdown" :text="podDropdownText" :disabled="disabled" - class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown" + class="gl-mr-3 gl-mb-3 gl-display-flex gl-display-md-block qa-pods-dropdown" > - <gl-deprecated-dropdown-header class="text-center"> + <gl-dropdown-section-header> {{ s__('Environments|Select pod') }} - </gl-deprecated-dropdown-header> + </gl-dropdown-section-header> - <gl-deprecated-dropdown-item v-if="!pods.options.length" disabled> + <gl-dropdown-item v-if="!pods.options.length" disabled> <span ref="noPodsMsg" class="text-muted"> {{ s__('Environments|No pods to display') }} </span> - </gl-deprecated-dropdown-item> - <gl-deprecated-dropdown-item + </gl-dropdown-item> + <gl-dropdown-item v-for="podName in pods.options" :key="podName" + :is-check-item="true" + :is-checked="isCurrentPod(podName)" class="text-nowrap" @click="showPodLogs(podName)" > - <div class="d-flex"> - <gl-icon - :class="{ invisible: !isCurrentPod(podName) }" - name="status_success_borderless" - /> - <div class="flex-grow-1">{{ podName }}</div> - </div> - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> + {{ podName }} + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/milestones/stores/actions.js b/app/assets/javascripts/milestones/stores/actions.js new file mode 100644 index 00000000000..3859771aeba --- /dev/null +++ b/app/assets/javascripts/milestones/stores/actions.js @@ -0,0 +1,58 @@ +import Api from '~/api'; +import * as types from './mutation_types'; + +export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId); + +export const setSelectedMilestones = ({ commit }, selectedMilestones) => + commit(types.SET_SELECTED_MILESTONES, selectedMilestones); + +export const toggleMilestones = ({ commit, state }, selectedMilestone) => { + const removeMilestone = state.selectedMilestones.includes(selectedMilestone); + + if (removeMilestone) { + commit(types.REMOVE_SELECTED_MILESTONE, selectedMilestone); + } else { + commit(types.ADD_SELECTED_MILESTONE, selectedMilestone); + } +}; + +export const search = ({ dispatch, commit }, query) => { + commit(types.SET_QUERY, query); + + dispatch('searchMilestones'); +}; + +export const fetchMilestones = ({ commit, state }) => { + commit(types.REQUEST_START); + + Api.projectMilestones(state.projectId) + .then(response => { + commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); + }) + .catch(error => { + commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error); + }) + .finally(() => { + commit(types.REQUEST_FINISH); + }); +}; + +export const searchMilestones = ({ commit, state }) => { + commit(types.REQUEST_START); + + const options = { + search: state.query, + scope: 'milestones', + }; + + Api.projectSearch(state.projectId, options) + .then(response => { + commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); + }) + .catch(error => { + commit(types.RECEIVE_PROJECT_MILESTONES_ERROR, error); + }) + .finally(() => { + commit(types.REQUEST_FINISH); + }); +}; diff --git a/app/assets/javascripts/milestones/stores/getters.js b/app/assets/javascripts/milestones/stores/getters.js new file mode 100644 index 00000000000..d8a283403ec --- /dev/null +++ b/app/assets/javascripts/milestones/stores/getters.js @@ -0,0 +1,2 @@ +/** Returns `true` if there is at least one in-progress request */ +export const isLoading = ({ requestCount }) => requestCount > 0; diff --git a/app/assets/javascripts/milestones/stores/index.js b/app/assets/javascripts/milestones/stores/index.js new file mode 100644 index 00000000000..2bebffc19ab --- /dev/null +++ b/app/assets/javascripts/milestones/stores/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import createState from './state'; + +Vue.use(Vuex); + +export default () => + new Vuex.Store({ + actions, + getters, + mutations, + state: createState(), + }); diff --git a/app/assets/javascripts/milestones/stores/mutation_types.js b/app/assets/javascripts/milestones/stores/mutation_types.js new file mode 100644 index 00000000000..370d386dba2 --- /dev/null +++ b/app/assets/javascripts/milestones/stores/mutation_types.js @@ -0,0 +1,13 @@ +export const SET_PROJECT_ID = 'SET_PROJECT_ID'; + +export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES'; +export const ADD_SELECTED_MILESTONE = 'ADD_SELECTED_MILESTONE'; +export const REMOVE_SELECTED_MILESTONE = 'REMOVE_SELECTED_MILESTONE'; + +export const SET_QUERY = 'SET_QUERY'; + +export const REQUEST_START = 'REQUEST_START'; +export const REQUEST_FINISH = 'REQUEST_FINISH'; + +export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS'; +export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR'; diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js new file mode 100644 index 00000000000..7c75d09766c --- /dev/null +++ b/app/assets/javascripts/milestones/stores/mutations.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import * as types from './mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +export default { + [types.SET_PROJECT_ID](state, projectId) { + state.projectId = projectId; + }, + [types.SET_SELECTED_MILESTONES](state, selectedMilestones) { + Vue.set(state, 'selectedMilestones', selectedMilestones); + }, + [types.ADD_SELECTED_MILESTONE](state, selectedMilestone) { + state.selectedMilestones.push(selectedMilestone); + }, + [types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone) { + const filteredMilestones = state.selectedMilestones.filter( + milestone => milestone !== selectedMilestone, + ); + Vue.set(state, 'selectedMilestones', filteredMilestones); + }, + [types.SET_QUERY](state, query) { + state.query = query; + }, + [types.REQUEST_START](state) { + state.requestCount += 1; + }, + [types.REQUEST_FINISH](state) { + state.requestCount -= 1; + }, + [types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) { + state.matches.projectMilestones = { + list: convertObjectPropsToCamelCase(response.data).map(({ title }) => ({ title })), + totalCount: parseInt(response.headers['x-total'], 10), + error: null, + }; + }, + [types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error) { + state.matches.projectMilestones = { + list: [], + totalCount: 0, + error, + }; + }, +}; diff --git a/app/assets/javascripts/milestones/stores/state.js b/app/assets/javascripts/milestones/stores/state.js new file mode 100644 index 00000000000..0944539f367 --- /dev/null +++ b/app/assets/javascripts/milestones/stores/state.js @@ -0,0 +1,14 @@ +export default () => ({ + projectId: null, + groupId: null, + query: '', + matches: { + projectMilestones: { + list: [], + totalCount: 0, + error: null, + }, + }, + selectedMilestones: [], + requestCount: 0, +}); diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue index 5d59880d497..a9079f91f50 100644 --- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -1,11 +1,5 @@ <script> -import { - GlAlert, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - GlIcon, - GlSprintf, -} from '@gitlab/ui'; +import { GlAlert, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { get } from 'lodash'; @@ -17,9 +11,8 @@ export default { components: { GlAlert, GlAreaChart, - GlDeprecatedDropdown, - GlDeprecatedDropdownItem, - GlIcon, + GlDropdown, + GlDropdownItem, GlSprintf, }, props: { @@ -140,25 +133,18 @@ export default { {{ __('It seems that there is currently no available data for code coverage') }} </span> </gl-alert> - <gl-deprecated-dropdown v-if="canShowData" :text="selectedDailyCoverageName"> - <gl-deprecated-dropdown-item + <gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName"> + <gl-dropdown-item v-for="({ group_name }, index) in dailyCoverageData" :key="index" :value="group_name" + :is-check-item="true" + :is-checked="index === selectedCoverageIndex" @click="setSelectedCoverage(index)" > - <div class="gl-display-flex"> - <gl-icon - v-if="index === selectedCoverageIndex" - name="mobile-issue-close" - class="gl-absolute" - /> - <span class="gl-display-flex align-items-center ml-4"> - {{ group_name }} - </span> - </div> - </gl-deprecated-dropdown-item> - </gl-deprecated-dropdown> + {{ group_name }} + </gl-dropdown-item> + </gl-dropdown> </div> <gl-area-chart v-if="!isLoading" diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue index bc3a9ee45f8..cfe3ce0a11c 100644 --- a/app/assets/javascripts/vue_shared/components/editor_lite.vue +++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue @@ -58,7 +58,9 @@ export default { this.editor.updateModelLanguage(newVal); }, value(newVal) { - this.editor.setValue(newVal); + if (this.editor.getValue() !== newVal) { + this.editor.setValue(newVal); + } }, }, mounted() { diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb index b1dd720d908..641d244b665 100644 --- a/app/models/container_expiration_policy.rb +++ b/app/models/container_expiration_policy.rb @@ -3,6 +3,7 @@ class ContainerExpirationPolicy < ApplicationRecord include Schedulable include UsageStatistics + include EachBatch belongs_to :project, inverse_of: :container_expiration_policy @@ -19,6 +20,16 @@ class ContainerExpirationPolicy < ApplicationRecord scope :active, -> { where(enabled: true) } scope :preloaded, -> { preload(project: [:route]) } + def self.executable + runnable_schedules.where( + 'EXISTS (?)', + ContainerRepository.select(1) + .where( + 'container_repositories.project_id = container_expiration_policies.project_id' + ) + ) + end + def self.keep_n_options { 1 => _('%{tags} tag per image name') % { tags: 1 }, diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb index 0f03f5f09b4..d003124a112 100644 --- a/app/services/merge_requests/cleanup_refs_service.rb +++ b/app/services/merge_requests/cleanup_refs_service.rb @@ -17,7 +17,7 @@ module MergeRequests @repository = merge_request.project.repository @ref_path = merge_request.ref_path @merge_ref_path = merge_request.merge_ref_path - @ref_head_sha = @repository.commit(merge_request.ref_path).id + @ref_head_sha = @repository.commit(merge_request.ref_path)&.id @merge_ref_sha = merge_request.merge_ref_head&.id end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index c5d7b148e69..f6dc808aa55 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -92,7 +92,7 @@ %li.nav-item %div - sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in') - = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-sign-in' + = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only= _('Toggle navigation') diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb index 96590e165ae..61ba27f00d2 100644 --- a/app/workers/container_expiration_policy_worker.rb +++ b/app/workers/container_expiration_policy_worker.rb @@ -7,13 +7,15 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo feature_category :container_registry def perform - ContainerExpirationPolicy.runnable_schedules.preloaded.find_each do |container_expiration_policy| - with_context(project: container_expiration_policy.project, - user: container_expiration_policy.project.owner) do |project:, user:| - ContainerExpirationPolicyService.new(project, user) - .execute(container_expiration_policy) - rescue ContainerExpirationPolicyService::InvalidPolicyError => e - Gitlab::ErrorTracking.log_exception(e, container_expiration_policy_id: container_expiration_policy.id) + ContainerExpirationPolicy.executable.preloaded.each_batch do |relation| + relation.each do |container_expiration_policy| + with_context(project: container_expiration_policy.project, + user: container_expiration_policy.project.owner) do |project:, user:| + ContainerExpirationPolicyService.new(project, user) + .execute(container_expiration_policy) + rescue ContainerExpirationPolicyService::InvalidPolicyError => e + Gitlab::ErrorTracking.log_exception(e, container_expiration_policy_id: container_expiration_policy.id) + end end end end diff --git a/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml b/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml new file mode 100644 index 00000000000..cf13a49aa84 --- /dev/null +++ b/changelogs/unreleased/229023-migrate-sidebar-epic-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Replace tooltip with GLTooltip in epic sidebar datepicker +merge_request: 45392 +author: +type: other diff --git a/changelogs/unreleased/229330-update-discard-changes-button.yml b/changelogs/unreleased/229330-update-discard-changes-button.yml new file mode 100644 index 00000000000..58e60d4a12c --- /dev/null +++ b/changelogs/unreleased/229330-update-discard-changes-button.yml @@ -0,0 +1,5 @@ +--- +title: Updated Discard Changes button in WebIDE +merge_request: 41899 +author: +type: changed diff --git a/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml b/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml new file mode 100644 index 00000000000..bff321cd5b9 --- /dev/null +++ b/changelogs/unreleased/263110-improve-cleanup-policies-selection-during-their-execution.yml @@ -0,0 +1,5 @@ +--- +title: Exclude policies with no container repositories when executing them +merge_request: 44748 +author: +type: fixed diff --git a/changelogs/unreleased/270054-fix-no-method-error.yml b/changelogs/unreleased/270054-fix-no-method-error.yml new file mode 100644 index 00000000000..ae3706062f2 --- /dev/null +++ b/changelogs/unreleased/270054-fix-no-method-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix error when cleaning up MR with no head ref +merge_request: 45504 +author: +type: fixed diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml new file mode 100644 index 00000000000..ccac03b006f --- /dev/null +++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-lo.yml @@ -0,0 +1,5 @@ +--- +title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/logs +merge_request: 41421 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml new file mode 100644 index 00000000000..8638d578605 --- /dev/null +++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-pa.yml @@ -0,0 +1,5 @@ +--- +title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +merge_request: 41423 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml b/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml new file mode 100644 index 00000000000..88610c6ec29 --- /dev/null +++ b/changelogs/unreleased/add-vuex-store-for-milestone-combobox.yml @@ -0,0 +1,5 @@ +--- +title: Add vuex stores for milestone comboxbox +merge_request: 45287 +author: +type: added diff --git a/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb b/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb new file mode 100644 index 00000000000..ec44d5ddcef --- /dev/null +++ b/db/migrate/20201009090954_add_index_with_project_id_to_container_expiration_policies.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexWithProjectIdToContainerExpirationPolicies < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + INDEX_NAME = 'idx_container_exp_policies_on_project_id_next_run_at_enabled' + + disable_ddl_transaction! + + def up + add_concurrent_index :container_expiration_policies, [:project_id, :next_run_at, :enabled], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :container_expiration_policies, INDEX_NAME + end +end diff --git a/db/schema_migrations/20201009090954 b/db/schema_migrations/20201009090954 new file mode 100644 index 00000000000..5d5ca8ff29b --- /dev/null +++ b/db/schema_migrations/20201009090954 @@ -0,0 +1 @@ +d0944a864a1a89e9339eb1f8ffab683df1a5bb90f7b7a16cabd4871f34d1cd48
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c8b3b8fb587..4e6fc7e9260 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19622,6 +19622,8 @@ CREATE INDEX idx_audit_events_on_entity_id_desc_author_id_created_at ON audit_ev CREATE INDEX idx_ci_pipelines_artifacts_locked ON ci_pipelines USING btree (ci_ref_id, id) WHERE (locked = 1); +CREATE INDEX idx_container_exp_policies_on_project_id_next_run_at_enabled ON container_expiration_policies USING btree (project_id, next_run_at, enabled); + CREATE INDEX idx_deployment_clusters_on_cluster_id_and_kubernetes_namespace ON deployment_clusters USING btree (cluster_id, kubernetes_namespace); CREATE UNIQUE INDEX idx_deployment_merge_requests_unique_index ON deployment_merge_requests USING btree (deployment_id, merge_request_id); diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 61d9261650a..9f72293a730 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -597,7 +597,7 @@ database encryption. Proceed with caution. ```ruby pages_external_url "http://<pages_server_URL>" gitlab_pages['enable'] = false - gitlab_rails['pages_enabled']=false + pages_nginx['enable'] = false gitlab_rails['pages_path'] = "/mnt/pages" ``` @@ -788,3 +788,9 @@ This problem most likely results from an [out-dated operating system](https://do The [Pages daemon uses the `securecookie` library](https://gitlab.com/search?group_id=9970&project_id=734943&repository_ref=master&scope=blobs&search=securecookie&snippets=false) to get random strings via [crypto/rand in Go](https://golang.org/pkg/crypto/rand/#pkg-variables). This requires the `getrandom` syscall or `/dev/urandom` to be available on the host OS. Upgrading to an [officially supported operating system](https://about.gitlab.com/install/) is recommended. + +### The requested scope is invalid, malformed, or unknown + +This problem comes from the permissions of the GitLab Pages OAuth application. To fix it, go to +**Admin > Applications > GitLab Pages** and edit the application. Under **Scopes**, ensure that the +`api` scope is selected and save your changes. diff --git a/doc/development/img/architecture_simplified.png b/doc/development/img/architecture_simplified.png Binary files differindex 46ae2b3c055..72d00b91129 100644 --- a/doc/development/img/architecture_simplified.png +++ b/doc/development/img/architecture_simplified.png diff --git a/doc/install/README.md b/doc/install/README.md index 518b94d1694..6b08bb28bbb 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -9,33 +9,42 @@ type: index # Installation **(CORE ONLY)** -GitLab can be installed in most GNU/Linux distributions and in a number -of cloud providers. To get the best experience from GitLab, you need to balance -performance, reliability, ease of administration (backups, upgrades and troubleshooting), -and cost of hosting. - -There are many ways you can install GitLab depending on your platform: - -1. [**Omnibus GitLab**](#installing-gitlab-using-the-omnibus-gitlab-package-recommended): The official deb/rpm packages that contain a bundle of GitLab - and the various components it depends on, like PostgreSQL, Redis, Sidekiq, etc. -1. [**GitLab Helm chart**](#installing-gitlab-on-kubernetes-via-the-gitlab-helm-charts): The cloud native Helm chart for installing GitLab and all its components on Kubernetes. -1. [**Docker**](#installing-gitlab-with-docker): The Omnibus GitLab packages dockerized. -1. [**Source**](#installing-gitlab-from-source): Install GitLab and all its components from scratch. -1. [**Cloud provider**](#installing-gitlab-on-cloud-providers): Install directly from platforms like AWS, Azure, GCP. - -TIP: **If in doubt, choose Omnibus:** -The Omnibus GitLab packages are mature, -[scalable](../administration/reference_architectures/index.md) and are used +GitLab can be installed in most GNU/Linux distributions and with several +cloud providers. To get the best experience from GitLab, you must balance +performance, reliability, ease of administration (backups, upgrades, and +troubleshooting), and the cost of hosting. + +Depending on your platform, select from the following available methods to +install GitLab: + +- [_Omnibus GitLab_](#installing-gitlab-using-the-omnibus-gitlab-package-recommended): + The official deb/rpm packages that contain a bundle of GitLab and the + components it depends on, including PostgreSQL, Redis, and Sidekiq. +- [_GitLab Helm chart_](#installing-gitlab-on-kubernetes-via-the-gitlab-helm-charts): + The cloud native Helm chart for installing GitLab and all of its components + on Kubernetes. +- [_Docker_](#installing-gitlab-with-docker): The Omnibus GitLab packages, + dockerized. +- [_Source_](#installing-gitlab-from-source): Install GitLab and all of its + components from scratch. +- [_Cloud provider_](#installing-gitlab-on-cloud-providers): Install directly + from platforms like AWS, Azure, and GCP. + +If you're not sure which installation method to use, we recommend you use +Omnibus GitLab. The Omnibus GitLab packages are mature, +[scalable](../administration/reference_architectures/index.md), and are used today on GitLab.com. The Helm charts are recommended for those who are familiar with Kubernetes. ## Requirements -Before installing GitLab, it is of critical importance to review the system [requirements](requirements.md). The system requirements include details on the minimum hardware, software, database, and additional requirements to support GitLab. +Before you install GitLab, be sure to review the [system requirements](requirements.md). +The system requirements include details about the minimum hardware, software, +database, and additional requirements to support GitLab. ## Installing GitLab using the Omnibus GitLab package (recommended) -The Omnibus GitLab package uses our official deb/rpm repositories. This is +The Omnibus GitLab package uses our official deb/rpm repositories, and is recommended for most users. If you need additional flexibility and resilience, we recommend deploying @@ -45,11 +54,6 @@ GitLab as described in our [reference architecture documentation](../administrat ## Installing GitLab on Kubernetes via the GitLab Helm charts -NOTE: **Kubernetes experience required:** -We recommend being familiar with Kubernetes before using it to deploy GitLab in -production. The methods for management, observability, and some concepts are -different than traditional deployments. - When installing GitLab on Kubernetes, there are some trade-offs that you need to be aware of: @@ -59,11 +63,17 @@ need to be aware of: are deployed in a redundant fashion. - There are some feature [limitations to be aware of](https://docs.gitlab.com/charts/#limitations). +Due to these trade-offs, having Kubernetes experience is a requirement for +using this method. We recommend being familiar with Kubernetes before using it +to deploy GitLab in production. The methods for management, observability, and +some concepts are different than traditional deployments. + [**> Install GitLab on Kubernetes using the GitLab Helm charts.**](https://docs.gitlab.com/charts/) ## Installing GitLab with Docker -GitLab maintains a set of official Docker images based on the Omnibus GitLab package. +GitLab maintains a set of official Docker images based on the Omnibus GitLab +package. [**> Install GitLab using the official GitLab Docker images.**](docker.md) diff --git a/doc/user/application_security/coverage_fuzzing/index.md b/doc/user/application_security/coverage_fuzzing/index.md index dff71cb9445..9508407ccae 100644 --- a/doc/user/application_security/coverage_fuzzing/index.md +++ b/doc/user/application_security/coverage_fuzzing/index.md @@ -175,6 +175,52 @@ To use coverage fuzzing in an offline environment, follow these steps: `NEW_URL_GITLAB_COV_FUZ` is the URL of the private `gitlab-cov-fuzz` clone that you set up in the first step. +### Continuous fuzzing (long-running async fuzzing jobs) + +It's also possible to run the fuzzing jobs longer and without blocking your main pipeline. This +configuration uses the GitLab [parent-child pipelines](../../../ci/parent_child_pipelines.md). +The full example is available in the [repository](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example/-/tree/continuous_fuzzing#running-go-fuzz-from-ci). +This example uses Go, but is applicable for any other supported languages. + +The suggested workflow in this scenario is to have long-running, async fuzzing jobs on a +main/development branch, and short, blocking sync fuzzing jobs on all other branches and MRs. This +is a good way to balance the needs of letting a developer's per-commit pipeline complete quickly, +and also giving the fuzzer a large amount of time to fully explore and test the app. + +Long-running fuzzing jobs are usually necessary for the coverage guided fuzzer to find deeper bugs +in your latest code base. THe following is an example of what `.gitlab-ci.yml` looks like in this +workflow (for the full example, see the [repository](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example/-/tree/continuous_fuzzing)): + +```yaml + +sync_fuzzing: + variables: + COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=300' + trigger: + include: .covfuzz-ci.yml + strategy: depend + rules: + - if: $CI_COMMIT_BRANCH != 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event' + +async_fuzzing: + variables: + COVFUZZ_ADDITIONAL_ARGS: '-max_total_time=3600' + trigger: + include: .covfuzz-ci.yml + rules: + - if: $CI_COMMIT_BRANCH == 'continuous_fuzzing' && $CI_PIPELINE_SOURCE != 'merge_request_event' +``` + +This essentially creates two steps: + +1. `sync_fuzzing`: Runs all your fuzz targets for a short period of time in a blocking + configuration. This finds simple bugs and allows you to be confident that your MRs aren't + introducing new bugs or causing old bugs to reappear. +1. `async_fuzzing`: Runs on your branch and finds deep bugs in your code without blocking your + development cycle and MRs. + +The `covfuzz-ci.yml` is the same as that in the [original synchronous example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example#running-go-fuzz-from-ci). + ### Glossary - Seed corpus: The set of test cases given as initial input to the fuzz target. This usually speeds diff --git a/locale/gitlab.pot b/locale/gitlab.pot index def2098d7e8..53dc8ed68e1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18490,9 +18490,6 @@ msgstr "" msgid "Open issues" msgstr "" -msgid "Open projects" -msgstr "" - msgid "Open raw" msgstr "" @@ -22103,6 +22100,9 @@ msgstr "" msgid "Replication" msgstr "" +msgid "Replication details" +msgstr "" + msgid "Replication enabled" msgstr "" diff --git a/package.json b/package.json index 1511c095d82..a182da72b28 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,7 @@ "vue": "^2.6.12", "vue-apollo": "^3.0.3", "vue-loader": "^15.9.3", - "vue-router": "^3.4.6", + "vue-router": "^3.4.7", "vue-template-compiler": "^2.6.12", "vue-virtual-scroll-list": "^1.4.4", "vuedraggable": "^2.23.0", diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js index 559ce4f9414..e32deaea993 100644 --- a/spec/frontend/logs/components/environment_logs_spec.js +++ b/spec/frontend/logs/components/environment_logs_spec.js @@ -1,4 +1,4 @@ -import { GlSprintf, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlSprintf, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import EnvironmentLogs from '~/logs/components/environment_logs.vue'; @@ -121,7 +121,7 @@ describe('EnvironmentLogs', () => { it('displays UI elements', () => { initWrapper(); - expect(findEnvironmentsDropdown().is(GlDeprecatedDropdown)).toBe(true); + expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true); expect(findSimpleFilters().exists()).toBe(true); expect(findLogControlButtons().exists()).toBe(true); @@ -164,7 +164,7 @@ describe('EnvironmentLogs', () => { it('displays a disabled environments dropdown', () => { expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true'); - expect(findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem).length).toBe(0); + expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0); }); it('does not update buttons state', () => { @@ -241,7 +241,7 @@ describe('EnvironmentLogs', () => { }); it('populates environments dropdown', () => { - const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem); + const items = findEnvironmentsDropdown().findAll(GlDropdownItem); expect(findEnvironmentsDropdown().props('text')).toBe(mockEnvName); expect(items.length).toBe(mockEnvironments.length); mockEnvironments.forEach((env, i) => { @@ -251,14 +251,14 @@ describe('EnvironmentLogs', () => { }); it('dropdown has one environment selected', () => { - const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem); + const items = findEnvironmentsDropdown().findAll(GlDropdownItem); mockEnvironments.forEach((env, i) => { const item = items.at(i); if (item.text() !== mockEnvName) { - expect(item.find(GlIcon).classes('invisible')).toBe(true); + expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy(); } else { - expect(item.find(GlIcon).classes('invisible')).toBe(false); + expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy(); } }); }); @@ -286,7 +286,7 @@ describe('EnvironmentLogs', () => { describe('when user clicks', () => { it('environment name, trace is refreshed', () => { - const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem); + const items = findEnvironmentsDropdown().findAll(GlDropdownItem); const index = 1; // any env expect(dispatch).not.toHaveBeenCalledWith(`${module}/showEnvironment`, expect.anything()); diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js index 1e30a7df559..b819f0d25a8 100644 --- a/spec/frontend/logs/components/log_simple_filters_spec.js +++ b/spec/frontend/logs/components/log_simple_filters_spec.js @@ -1,4 +1,4 @@ -import { GlIcon, GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { createStore } from '~/logs/stores'; import { mockPods, mockPodName } from '../mock_data'; @@ -17,7 +17,7 @@ describe('LogSimpleFilters', () => { const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' }); const findPodsDropdownItems = () => findPodsDropdown() - .findAll(GlDeprecatedDropdownItem) + .findAll(GlDropdownItem) .filter(item => !('disabled' in item.attributes())); const mockPodsLoading = () => { @@ -114,9 +114,9 @@ describe('LogSimpleFilters', () => { mockPods.forEach((pod, i) => { const item = items.at(i); if (item.text() !== mockPodName) { - expect(item.find(GlIcon).classes('invisible')).toBe(true); + expect(item.find(GlDropdownItem).attributes('ischecked')).toBeFalsy(); } else { - expect(item.find(GlIcon).classes('invisible')).toBe(false); + expect(item.find(GlDropdownItem).attributes('ischecked')).toBeTruthy(); } }); }); diff --git a/spec/frontend/milestones/stores/actions_spec.js b/spec/frontend/milestones/stores/actions_spec.js new file mode 100644 index 00000000000..ad73d0e4238 --- /dev/null +++ b/spec/frontend/milestones/stores/actions_spec.js @@ -0,0 +1,140 @@ +import testAction from 'helpers/vuex_action_helper'; +import createState from '~/milestones/stores/state'; +import * as actions from '~/milestones/stores/actions'; +import * as types from '~/milestones/stores/mutation_types'; + +let mockProjectMilestonesReturnValue; +let mockProjectSearchReturnValue; + +jest.mock('~/api', () => ({ + // `__esModule: true` is required when mocking modules with default exports: + // https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options + __esModule: true, + default: { + projectMilestones: () => mockProjectMilestonesReturnValue, + projectSearch: () => mockProjectSearchReturnValue, + }, +})); + +describe('Milestone combobox Vuex store actions', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe('setProjectId', () => { + it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => { + const projectId = '4'; + testAction(actions.setProjectId, projectId, state, [ + { type: types.SET_PROJECT_ID, payload: projectId }, + ]); + }); + }); + + describe('setSelectedMilestones', () => { + it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => { + const selectedMilestones = ['v1.2.3']; + testAction(actions.setSelectedMilestones, selectedMilestones, state, [ + { type: types.SET_SELECTED_MILESTONES, payload: selectedMilestones }, + ]); + }); + }); + + describe('toggleMilestones', () => { + const selectedMilestone = 'v1.2.3'; + it(`commits ${types.ADD_SELECTED_MILESTONE} with the new selected milestone name`, () => { + testAction(actions.toggleMilestones, selectedMilestone, state, [ + { type: types.ADD_SELECTED_MILESTONE, payload: selectedMilestone }, + ]); + }); + + it(`commits ${types.REMOVE_SELECTED_MILESTONE} with the new selected milestone name`, () => { + state.selectedMilestones = [selectedMilestone]; + testAction(actions.toggleMilestones, selectedMilestone, state, [ + { type: types.REMOVE_SELECTED_MILESTONE, payload: selectedMilestone }, + ]); + }); + }); + + describe('search', () => { + it(`commits ${types.SET_QUERY} with the new search query`, () => { + const query = 'v1.0'; + testAction( + actions.search, + query, + state, + [{ type: types.SET_QUERY, payload: query }], + [{ type: 'searchMilestones' }], + ); + }); + }); + + describe('searchMilestones', () => { + describe('when the search is successful', () => { + const projectSearchApiResponse = { data: [{ title: 'v1.0' }] }; + + beforeEach(() => { + mockProjectSearchReturnValue = Promise.resolve(projectSearchApiResponse); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.searchMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + + describe('when the search fails', () => { + const error = new Error('Something went wrong!'); + + beforeEach(() => { + mockProjectSearchReturnValue = Promise.reject(error); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.searchMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + }); + + describe('fetchMilestones', () => { + describe('when the fetch is successful', () => { + const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] }; + + beforeEach(() => { + mockProjectMilestonesReturnValue = Promise.resolve(projectMilestonesApiResponse); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.fetchMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + + describe('when the fetch fails', () => { + const error = new Error('Something went wrong!'); + + beforeEach(() => { + mockProjectMilestonesReturnValue = Promise.reject(error); + }); + + it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { + return testAction(actions.fetchMilestones, undefined, state, [ + { type: types.REQUEST_START }, + { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, + { type: types.REQUEST_FINISH }, + ]); + }); + }); + }); +}); diff --git a/spec/frontend/milestones/stores/getter_spec.js b/spec/frontend/milestones/stores/getter_spec.js new file mode 100644 index 00000000000..df7c3d28e67 --- /dev/null +++ b/spec/frontend/milestones/stores/getter_spec.js @@ -0,0 +1,15 @@ +import * as getters from '~/milestones/stores/getters'; + +describe('Milestone comboxbox Vuex store getters', () => { + describe('isLoading', () => { + it.each` + requestCount | isLoading + ${2} | ${true} + ${1} | ${true} + ${0} | ${false} + ${-1} | ${false} + `('returns true when at least one request is in progress', ({ requestCount, isLoading }) => { + expect(getters.isLoading({ requestCount })).toBe(isLoading); + }); + }); +}); diff --git a/spec/frontend/milestones/stores/mutations_spec.js b/spec/frontend/milestones/stores/mutations_spec.js new file mode 100644 index 00000000000..8f8ce3c87ad --- /dev/null +++ b/spec/frontend/milestones/stores/mutations_spec.js @@ -0,0 +1,159 @@ +import createState from '~/milestones/stores/state'; +import mutations from '~/milestones/stores/mutations'; +import * as types from '~/milestones/stores/mutation_types'; + +describe('Milestones combobox Vuex store mutations', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe('initial state', () => { + it('is created with the correct structure and initial values', () => { + expect(state).toEqual({ + projectId: null, + groupId: null, + query: '', + matches: { + projectMilestones: { + list: [], + totalCount: 0, + error: null, + }, + }, + selectedMilestones: [], + requestCount: 0, + }); + }); + }); + + describe(`${types.SET_PROJECT_ID}`, () => { + it('updates the project ID', () => { + const newProjectId = '4'; + mutations[types.SET_PROJECT_ID](state, newProjectId); + + expect(state.projectId).toBe(newProjectId); + }); + }); + + describe(`${types.SET_SELECTED_MILESTONES}`, () => { + it('sets the selected milestones', () => { + const selectedMilestones = ['v1.2.3']; + mutations[types.SET_SELECTED_MILESTONES](state, selectedMilestones); + + expect(state.selectedMilestones).toEqual(['v1.2.3']); + }); + }); + + describe(`${types.ADD_SELECTED_MILESTONESs}`, () => { + it('adds the selected milestones', () => { + const selectedMilestone = 'v1.2.3'; + mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone); + + expect(state.selectedMilestones).toEqual(['v1.2.3']); + }); + }); + + describe(`${types.REMOVE_SELECTED_MILESTONES}`, () => { + it('removes the selected milestones', () => { + const selectedMilestone = 'v1.2.3'; + + mutations[types.SET_SELECTED_MILESTONES](state, [selectedMilestone]); + expect(state.selectedMilestones).toEqual(['v1.2.3']); + + mutations[types.REMOVE_SELECTED_MILESTONE](state, selectedMilestone); + expect(state.selectedMilestones).toEqual([]); + }); + }); + + describe(`${types.SET_QUERY}`, () => { + it('updates the search query', () => { + const newQuery = 'hello'; + mutations[types.SET_QUERY](state, newQuery); + + expect(state.query).toBe(newQuery); + }); + }); + + describe(`${types.REQUEST_START}`, () => { + it('increments requestCount by 1', () => { + mutations[types.REQUEST_START](state); + expect(state.requestCount).toBe(1); + + mutations[types.REQUEST_START](state); + expect(state.requestCount).toBe(2); + + mutations[types.REQUEST_START](state); + expect(state.requestCount).toBe(3); + }); + }); + + describe(`${types.REQUEST_FINISH}`, () => { + it('decrements requestCount by 1', () => { + state.requestCount = 3; + + mutations[types.REQUEST_FINISH](state); + expect(state.requestCount).toBe(2); + + mutations[types.REQUEST_FINISH](state); + expect(state.requestCount).toBe(1); + + mutations[types.REQUEST_FINISH](state); + expect(state.requestCount).toBe(0); + }); + }); + + describe(`${types.RECEIVE_PROJECT_MILESTONES_SUCCESS}`, () => { + it('updates state.matches.projectMilestones based on the provided API response', () => { + const response = { + data: [ + { + title: 'v0.1', + }, + { + title: 'v0.2', + }, + ], + headers: { + 'x-total': 2, + }, + }; + + mutations[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response); + + expect(state.matches.projectMilestones).toEqual({ + list: [ + { + title: 'v0.1', + }, + { + title: 'v0.2', + }, + ], + error: null, + totalCount: 2, + }); + }); + + describe(`${types.RECEIVE_PROJECT_MILESTONES_ERROR}`, () => { + it('updates state.matches.projectMilestones to an empty state with the error object', () => { + const error = new Error('Something went wrong!'); + + state.matches.projectMilestones = { + list: [{ title: 'v0.1' }], + totalCount: 1, + error: null, + }; + + mutations[types.RECEIVE_PROJECT_MILESTONES_ERROR](state, error); + + expect(state.matches.projectMilestones).toEqual({ + list: [], + totalCount: 0, + error, + }); + }); + }); + }); +}); diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap index 211f4ea20f5..8ccad7d5c22 100644 --- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap +++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap @@ -9,65 +9,54 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`] <!----> - <gl-deprecated-dropdown-stub + <gl-dropdown-stub + category="tertiary" + headertext="" + size="medium" text="rspec" + variant="default" > - <gl-deprecated-dropdown-item-stub + <gl-dropdown-item-stub + avatarurl="" + iconcolor="" + iconname="" + iconrightname="" + ischecked="true" + ischeckitem="true" + secondarytext="" value="rspec" > - <div - class="gl-display-flex" - > - <gl-icon-stub - class="gl-absolute" - name="mobile-issue-close" - size="16" - /> - - <span - class="gl-display-flex align-items-center ml-4" - > - - rspec - - </span> - </div> - </gl-deprecated-dropdown-item-stub> - <gl-deprecated-dropdown-item-stub + + rspec + + </gl-dropdown-item-stub> + <gl-dropdown-item-stub + avatarurl="" + iconcolor="" + iconname="" + iconrightname="" + ischeckitem="true" + secondarytext="" value="cypress" > - <div - class="gl-display-flex" - > - <!----> - - <span - class="gl-display-flex align-items-center ml-4" - > - - cypress - - </span> - </div> - </gl-deprecated-dropdown-item-stub> - <gl-deprecated-dropdown-item-stub + + cypress + + </gl-dropdown-item-stub> + <gl-dropdown-item-stub + avatarurl="" + iconcolor="" + iconname="" + iconrightname="" + ischeckitem="true" + secondarytext="" value="karma" > - <div - class="gl-display-flex" - > - <!----> - - <span - class="gl-display-flex align-items-center ml-4" - > - - karma - - </span> - </div> - </gl-deprecated-dropdown-item-stub> - </gl-deprecated-dropdown-stub> + + karma + + </gl-dropdown-item-stub> + </gl-dropdown-stub> </div> <gl-area-chart-stub diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js index 8884f7815ab..4a60c7fd509 100644 --- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js +++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import { shallowMount } from '@vue/test-utils'; -import { GlAlert, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui'; +import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import waitForPromises from 'helpers/wait_for_promises'; @@ -17,7 +17,7 @@ describe('Code Coverage', () => { const findAlert = () => wrapper.find(GlAlert); const findAreaChart = () => wrapper.find(GlAreaChart); - const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem); + const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); const findFirstDropdownItem = () => findAllDropdownItems().at(0); const findSecondDropdownItem = () => findAllDropdownItems().at(1); @@ -124,7 +124,7 @@ describe('Code Coverage', () => { }); it('renders the dropdown with all custom names as options', () => { - expect(wrapper.find(GlDeprecatedDropdown).exists()).toBeDefined(); + expect(wrapper.find(GlDropdown).exists()).toBeDefined(); expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length); expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name); }); @@ -145,16 +145,8 @@ describe('Code Coverage', () => { await wrapper.vm.$nextTick(); - expect( - findFirstDropdownItem() - .find(GlIcon) - .exists(), - ).toBe(false); - expect( - findSecondDropdownItem() - .find(GlIcon) - .exists(), - ).toBe(true); + expect(findFirstDropdownItem().attributes('ischecked')).toBeFalsy(); + expect(findSecondDropdownItem().attributes('ischecked')).toBeTruthy(); }); it('updates the graph data when selecting a different option in dropdown', async () => { diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/editor_lite_spec.js index 48005484b91..52502fcf64f 100644 --- a/spec/frontend/vue_shared/components/editor_lite_spec.js +++ b/spec/frontend/vue_shared/components/editor_lite_spec.js @@ -96,17 +96,6 @@ describe('Editor Lite component', () => { }); }); - it('reacts to the changes in the pased value', async () => { - const newValue = 'New Value'; - - wrapper.setProps({ - value: newValue, - }); - - await nextTick(); - expect(setValue).toHaveBeenCalledWith(newValue); - }); - it('registers callback with editor onChangeContent', () => { expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function)); }); @@ -127,5 +116,29 @@ describe('Editor Lite component', () => { expect(wrapper.emitted()['editor-ready']).toBeDefined(); }); + + describe('reaction to the value update', () => { + it('reacts to the changes in the passed value', async () => { + const newValue = 'New Value'; + + wrapper.setProps({ + value: newValue, + }); + + await nextTick(); + expect(setValue).toHaveBeenCalledWith(newValue); + }); + + it("does not update value if the passed one is exactly the same as the editor's content", async () => { + const newValue = `${value}`; // to make sure we're creating a new String with the same content and not just a reference + + wrapper.setProps({ + value: newValue, + }); + + await nextTick(); + expect(setValue).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/spec/models/container_expiration_policy_spec.rb b/spec/models/container_expiration_policy_spec.rb index 588685b04bf..1d9dbe8a867 100644 --- a/spec/models/container_expiration_policy_spec.rb +++ b/spec/models/container_expiration_policy_spec.rb @@ -104,6 +104,18 @@ RSpec.describe ContainerExpirationPolicy, type: :model do end end + describe '.executable' do + subject { described_class.executable } + + let_it_be(:policy1) { create(:container_expiration_policy, :runnable) } + let_it_be(:container_repository1) { create(:container_repository, project: policy1.project) } + let_it_be(:policy2) { create(:container_expiration_policy, :runnable) } + let_it_be(:container_repository2) { create(:container_repository, project: policy2.project) } + let_it_be(:policy3) { create(:container_expiration_policy, :runnable) } + + it { is_expected.to contain_exactly(policy1, policy2) } + end + describe '#disable!' do let_it_be(:container_expiration_policy) { create(:container_expiration_policy) } diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb index b38ccee4aa0..a051b3c9355 100644 --- a/spec/services/merge_requests/cleanup_refs_service_spec.rb +++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb @@ -35,6 +35,17 @@ RSpec.describe MergeRequests::CleanupRefsService do end end + context 'when merge request has no head ref' do + before do + # Simulate a merge request with no head ref + merge_request.project.repository.delete_refs(merge_request.ref_path) + end + + it 'does not fail' do + expect(result[:status]).to eq(:success) + end + end + context 'when merge request has merge ref' do before do MergeRequests::MergeToRefService diff --git a/spec/workers/container_expiration_policy_worker_spec.rb b/spec/workers/container_expiration_policy_worker_spec.rb index 868eb6b192e..6b185c30670 100644 --- a/spec/workers/container_expiration_policy_worker_spec.rb +++ b/spec/workers/container_expiration_policy_worker_spec.rb @@ -7,19 +7,24 @@ RSpec.describe ContainerExpirationPolicyWorker do subject { described_class.new.perform } - context 'With no container expiration policies' do - it 'Does not execute any policies' do + RSpec.shared_examples 'not executing any policy' do + it 'does not run any policy' do expect(ContainerExpirationPolicyService).not_to receive(:new) subject end end + context 'With no container expiration policies' do + it_behaves_like 'not executing any policy' + end + context 'With container expiration policies' do - context 'a valid policy' do - let!(:container_expiration_policy) { create(:container_expiration_policy, :runnable) } - let(:user) { container_expiration_policy.project.owner } + let_it_be(:container_expiration_policy, reload: true) { create(:container_expiration_policy, :runnable) } + let_it_be(:container_repository) { create(:container_repository, project: container_expiration_policy.project) } + let_it_be(:user) { container_expiration_policy.project.owner } + context 'a valid policy' do it 'runs the policy' do service = instance_double(ContainerExpirationPolicyService, execute: true) @@ -31,33 +36,30 @@ RSpec.describe ContainerExpirationPolicyWorker do end context 'a disabled policy' do - let!(:container_expiration_policy) { create(:container_expiration_policy, :runnable, :disabled) } - let(:user) {container_expiration_policy.project.owner } - - it 'does not run the policy' do - expect(ContainerExpirationPolicyService) - .not_to receive(:new).with(container_expiration_policy, user) - - subject + before do + container_expiration_policy.disable! end + + it_behaves_like 'not executing any policy' end context 'a policy that is not due for a run' do - let!(:container_expiration_policy) { create(:container_expiration_policy) } - let(:user) {container_expiration_policy.project.owner } + before do + container_expiration_policy.update_column(:next_run_at, 2.minutes.from_now) + end - it 'does not run the policy' do - expect(ContainerExpirationPolicyService) - .not_to receive(:new).with(container_expiration_policy, user) + it_behaves_like 'not executing any policy' + end - subject + context 'a policy linked to no container repository' do + before do + container_expiration_policy.container_repositories.delete_all end + + it_behaves_like 'not executing any policy' end context 'an invalid policy' do - let_it_be(:container_expiration_policy) { create(:container_expiration_policy, :runnable) } - let_it_be(:user) {container_expiration_policy.project.owner } - before do container_expiration_policy.update_column(:name_regex, '*production') end diff --git a/yarn.lock b/yarn.lock index 8c4777f2797..83a9264bfb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12368,10 +12368,10 @@ vue-loader@^15.9.3: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-router@^3.4.6: - version "3.4.6" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.6.tgz#f7bda2c9a43d39837621c9a02ba7789f5daa24b2" - integrity sha512-kaXnB3pfFxhAJl/Mp+XG1HJMyFqrL/xPqV7oXlpXn4AwMmm6VNgf0nllW8ksflmZANfI4kdo0bVn/FYSsAolPQ== +vue-router@^3.4.7: + version "3.4.7" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.4.7.tgz#bf189bafd16f4e4ef783c4a6250a3090f2c1fa1b" + integrity sha512-CbHXue5BLrDivOk5O4eZ0WT4Yj8XwdXa4kCnsEIOzYUPF/07ZukayA2jGxDCJxLc9SgVQX9QX0OuGOwGlVB4Qg== vue-runtime-helpers@^1.1.2: version "1.1.2" |