diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js index 478e3f6aed9..c0f2153ce5b 100644 --- a/app/assets/javascripts/emoji/index.js +++ b/app/assets/javascripts/emoji/index.js @@ -10,7 +10,7 @@ let validEmojiNames = null; export const FALLBACK_EMOJI_KEY = 'grey_question'; // Keep the version in sync with `lib/gitlab/emoji.rb` -export const EMOJI_VERSION = '1'; +export const EMOJI_VERSION = '2'; const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage(); diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index b987adc8bae..0fc7337ad26 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -29,14 +29,20 @@ export default { }, }, watch: { - showLoading(newVal) { - if (!newVal) { - this.$emit('tree-ready'); - } + showLoading() { + this.notifyTreeReady(); }, }, + mounted() { + this.notifyTreeReady(); + }, methods: { ...mapActions(['toggleTreeOpen']), + notifyTreeReady() { + if (!this.showLoading) { + this.$emit('tree-ready'); + } + }, clickedFile() { performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_CLICKED }); }, diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 27cedd80347..1fc447886bb 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -1,8 +1,6 @@ import Vue from 'vue'; -import createFlash from '~/flash'; import IdeRouter from '~/ide/ide_router_extension'; import { joinPaths } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; import { WEBIDE_MARK_FETCH_PROJECT_DATA_START, WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, @@ -75,49 +73,34 @@ export const createRouter = (store, defaultBranch) => { router.beforeEach((to, from, next) => { if (to.params.namespace && to.params.project) { - performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START }); - store - .dispatch('getProjectData', { - namespace: to.params.namespace, - projectId: to.params.project, - }) - .then(() => { - const basePath = to.params.pathMatch || ''; - const projectId = `${to.params.namespace}/${to.params.project}`; - const branchId = to.params.branchid; - const mergeRequestId = to.params.mrid; + const basePath = to.params.pathMatch || ''; + const projectId = `${to.params.namespace}/${to.params.project}`; + const branchId = to.params.branchid; + const mergeRequestId = to.params.mrid; - if (branchId) { - performanceMarkAndMeasure({ - mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, - measures: [ - { - name: WEBIDE_MEASURE_FETCH_PROJECT_DATA, - start: WEBIDE_MARK_FETCH_PROJECT_DATA_START, - }, - ], - }); - store.dispatch('openBranch', { - projectId, - branchId, - basePath, - }); - } else if (mergeRequestId) { - store.dispatch('openMergeRequest', { - projectId, - mergeRequestId, - targetProjectId: to.query.target_project, - }); - } - }) - .catch((e) => { - createFlash({ - message: __('Error while loading the project data. Please try again.'), - fadeTransition: false, - addBodyClass: true, - }); - throw e; + performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START }); + if (branchId) { + performanceMarkAndMeasure({ + mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, + measures: [ + { + name: WEBIDE_MEASURE_FETCH_PROJECT_DATA, + start: WEBIDE_MARK_FETCH_PROJECT_DATA_START, + }, + ], }); + store.dispatch('openBranch', { + projectId, + branchId, + basePath, + }); + } else if (mergeRequestId) { + store.dispatch('openMergeRequest', { + projectId, + mergeRequestId, + targetProjectId: to.query.target_project, + }); + } } next(); diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index bdffed70882..df643675357 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -34,11 +34,18 @@ Vue.use(PerformancePlugin, { * @param {extendStoreCallback} options.extendStore - * Function that receives the default store and returns an extended one. */ -export function initIde(el, options = {}) { +export const initIde = (el, options = {}) => { if (!el) return null; const { rootComponent = ide, extendStore = identity } = options; + const store = createStore(); + const project = JSON.parse(el.dataset.project); + store.dispatch('setProject', { project }); + + // fire and forget fetching non-critical project info + store.dispatch('fetchProjectPermissions'); + const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH); return new Vue({ @@ -77,7 +84,7 @@ export function initIde(el, options = {}) { return createElement(rootComponent); }, }); -} +}; /** * Start the IDE. diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 37a08bc4feb..805476c71bc 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,23 +1,12 @@ -import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { query, mutate } from './gql'; -const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data); - -const fetchGqlProjectData = (projectPath) => - query({ - query: getIdeProject, - variables: { projectPath }, - }).then(({ data }) => ({ - ...data.project, - id: getIdFromGraphQLId(data.project.id), - })); - export default { getFileData(endpoint) { return axios.get(endpoint, { @@ -65,18 +54,6 @@ export default { ) .then(({ data }) => data); }, - getProjectData(namespace, project) { - const projectPath = `${namespace}/${project}`; - - return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then( - ([apiProjectData, gqlProjectData]) => ({ - data: { - ...apiProjectData, - ...gqlProjectData, - }, - }), - ); - }, getProjectMergeRequests(projectId, params = {}) { return Api.projectMergeRequests(projectId, params); }, @@ -119,4 +96,13 @@ export default { variables: { input: { featureName: name } }, }).then(({ data }) => data); }, + getProjectPermissionsData(projectPath) { + return query({ + query: getIdeProject, + variables: { projectPath }, + }).then(({ data }) => ({ + ...data.project, + id: getIdFromGraphQLId(data.project.id), + })); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 93ad19ba81e..0ec808339fb 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -1,35 +1,44 @@ import { escape } from 'lodash'; import createFlash from '~/flash'; import { __, sprintf } from '~/locale'; +import { logError } from '~/lib/logger'; import api from '../../../api'; import service from '../../services'; import * as types from '../mutation_types'; -export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) => - new Promise((resolve, reject) => { - if (!state.projects[`${namespace}/${projectId}`] || force) { - commit(types.TOGGLE_LOADING, { entry: state }); - service - .getProjectData(namespace, projectId) - .then((res) => res.data) - .then((data) => { - commit(types.TOGGLE_LOADING, { entry: state }); - commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); - resolve(data); - }) - .catch(() => { - createFlash({ - message: __('Error loading project data. Please try again.'), - fadeTransition: false, - addBodyClass: true, - }); - reject(new Error(`Project not loaded ${namespace}/${projectId}`)); - }); - } else { - resolve(state.projects[`${namespace}/${projectId}`]); - } +const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.'); + +const errorFetchingData = (e) => { + logError(ERROR_LOADING_PROJECT, e); + + createFlash({ + message: ERROR_LOADING_PROJECT, + fadeTransition: false, + addBodyClass: true, }); +}; + +export const setProject = ({ commit }, { project } = {}) => { + if (!project) { + return; + } + const projectPath = project.path_with_namespace; + commit(types.SET_PROJECT, { projectPath, project }); + commit(types.SET_CURRENT_PROJECT, projectPath); +}; + +export const fetchProjectPermissions = ({ commit, state }) => { + const projectPath = state.currentProjectId; + if (!projectPath) { + return undefined; + } + return service + .getProjectPermissionsData(projectPath) + .then((permissions) => { + commit(types.UPDATE_PROJECT, { projectPath, props: permissions }); + }) + .catch(errorFetchingData); +}; export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => service diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 77755b179ef..13f338c4a48 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -8,6 +8,7 @@ export const SET_LINKS = 'SET_LINKS'; // Project Mutation Types export const SET_PROJECT = 'SET_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; +export const UPDATE_PROJECT = 'UPDATE_PROJECT'; export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; // Merge request mutation types diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 034fdad4305..9f65d3a543e 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import * as types from '../mutation_types'; export default { @@ -24,4 +25,15 @@ export default { empty_repo: value, }); }, + [types.UPDATE_PROJECT](state, { projectPath, props }) { + const project = state.projects[projectPath]; + + if (!project || !props) { + return; + } + + Object.keys(props).forEach((key) => { + Vue.set(project, key, props[key]); + }); + }, }; diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue deleted file mode 100644 index 99461475af0..00000000000 --- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - diff --git a/app/assets/javascripts/pages/dashboard/projects/index/index.js b/app/assets/javascripts/pages/dashboard/projects/index/index.js index c34d15b869a..6c9378b7231 100644 --- a/app/assets/javascripts/pages/dashboard/projects/index/index.js +++ b/app/assets/javascripts/pages/dashboard/projects/index/index.js @@ -1,5 +1,3 @@ import ProjectsList from '~/projects_list'; -import initCustomizeHomepageBanner from './init_customize_homepage_banner'; new ProjectsList(); // eslint-disable-line no-new -initCustomizeHomepageBanner(); diff --git a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js deleted file mode 100644 index 8cdcd3134ee..00000000000 --- a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vue from 'vue'; -import CustomizeHomepageBanner from './components/customize_homepage_banner.vue'; - -export default () => { - const el = document.querySelector('.js-customize-homepage-banner'); - - if (!el) { - return false; - } - - return new Vue({ - el, - provide: { ...el.dataset }, - render: (createElement) => createElement(CustomizeHomepageBanner), - }); -}; diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 09ff57e2baf..4d81aeca37a 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -29,7 +29,7 @@ module IdeHelper def convert_to_project_entity_json(project) return unless project - API::Entities::Project.represent(project).to_json + API::Entities::Project.represent(project, current_user: current_user).to_json end def enable_environments_guidance? diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index 5ed17357e9b..32b0d7b3fe3 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -6,7 +6,6 @@ module Users GCP_SIGNUP_OFFER = 'gcp_signup_offer' SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed' TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight' - CUSTOMIZE_HOMEPAGE = 'customize_homepage' FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version' REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout' UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' @@ -36,10 +35,6 @@ module Users !user_dismissed?(SUGGEST_POPOVER_DISMISSED) end - def show_customize_homepage_banner? - current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE) - end - def show_feature_flags_new_version? !user_dismissed?(FEATURE_FLAGS_NEW_VERSION) end diff --git a/app/models/user.rb b/app/models/user.rb index 98d2ceb6dbe..942b37c3cae 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -844,10 +844,6 @@ class User < ApplicationRecord # Instance methods # - def default_dashboard? - dashboard == self.class.column_defaults['dashboard'] - end - def full_path username end diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 9a729072051..9ce0beed3b3 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -24,7 +24,6 @@ module Users buy_pipeline_minutes_notification_dot: 19, # EE-only personal_access_token_expiry: 21, # EE-only suggest_pipeline: 22, - customize_homepage: 23, feature_flags_new_version: 24, registration_enabled_callout: 25, new_user_signups_cap_reached: 26, # EE-only diff --git a/app/services/events/destroy_service.rb b/app/services/events/destroy_service.rb new file mode 100644 index 00000000000..fdb718f0fcb --- /dev/null +++ b/app/services/events/destroy_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Events + class DestroyService + def initialize(project) + @project = project + end + + def execute + project.events.all.delete_all + + ServiceResponse.success(message: 'Events were deleted.') + rescue StandardError + ServiceResponse.error(message: 'Failed to remove events.') + end + + private + + attr_reader :project + end +end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 3a9418f1dfa..0939ced38dd 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -75,6 +75,18 @@ module Projects response.success? end + def destroy_events! + unless remove_events + raise_error(s_('DeleteProject|Failed to remove events. Please try again or contact administrator.')) + end + end + + def remove_events + response = ::Events::DestroyService.new(project).execute + + response.success? + end + def remove_repository(repository) return true unless repository @@ -117,6 +129,7 @@ module Projects log_destroy_event trash_relation_repositories! trash_project_repositories! + destroy_events! destroy_web_hooks! destroy_project_bots! diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml index 8d6e043ebf7..0644910dd3e 100644 --- a/app/views/groups/_import_group_from_file_panel.html.haml +++ b/app/views/groups/_import_group_from_file_panel.html.haml @@ -6,9 +6,17 @@ .gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5 %h4 = _('Import group from file') - %p - = s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.') - .form-group.gl-display-flex.gl-flex-direction-column + .gl-alert.gl-alert-warning{ role: 'alert' } + = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + - docs_link_start = ''.html_safe % { url: help_page_path('user/group/import/index.md') } + - link_end = ''.html_safe + = s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end } + - if Feature.enabled?(:bulk_import, default_enabled: :yaml) + - enable_link_start = ''.html_safe % { url: help_page_path('user/group/import/index.md', anchor: 'enable-or-disable-gitlab-group-migration') } + = s_('GroupsNew|Ask your administrator to %{enable_link_start}enable%{enable_link_end} Group Migration.').html_safe % { enable_link_start: enable_link_start, enable_link_end: link_end } + + .form-group.gl-display-flex.gl-flex-direction-column.gl-mt-5 = f.label :name, _('New group name'), for: 'import_group_name' = f.text_field :name, placeholder: s_('GroupsNew|My Awesome Group'), class: 'js-autofill-group-name gl-form-input col-xs-12 col-sm-8', required: true, diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml index 3e3646df665..ff00ff1f6e8 100644 --- a/app/views/groups/settings/_export.html.haml +++ b/app/views/groups/settings/_export.html.haml @@ -5,6 +5,12 @@ .sub-section %h4= s_('GroupSettings|Export group') %p= _('Export this group with all related data.') + .gl-alert.gl-alert-warning.gl-mb-4{ role: 'alert' } + = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + - docs_link_start = ''.html_safe % { url: help_page_path('user/group/import/index.md') } + - docs_link_end = ''.html_safe + = s_('GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end } %p - export_information = _('After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance.') % { strong_text_start: ''.html_safe, strong_text_end: ''.html_safe} = export_information.html_safe diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index be4beea78e1..3e875a0eb24 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -21,7 +21,6 @@ = render_if_exists "shared/namespace_user_cap_reached_alert" = render_if_exists "shared/new_user_signups_cap_reached_alert" = yield :page_level_alert - = yield :customize_homepage_banner - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" %div{ class: "#{(container_class unless @no_container)} #{@content_class}" } diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index e8ea4ad90dc..3358025b237 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -1,7 +1,21 @@ - @content_class = 'limit-container-width' unless fluid_layout -- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path +- add_to_breadcrumbs _('Webhook Settings'), project_hooks_path(@project) - page_title _('Webhook') +- if @hook.rate_limited? + - placeholders = { strong_start: ''.html_safe, + strong_end: ''.html_safe, + limit: @hook.rate_limit, + support_link_start: ''.html_safe, + support_link_end: ''.html_safe } + = render 'shared/global_alert', + title: s_('Webhooks|Webhook was automatically disabled'), + variant: :danger, + is_contained: true, + close_button_class: 'js-close' do + .gl-alert-body + = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders + .row.gl-mt-3 .col-lg-3 = render 'shared/web_hooks/title_and_docs', hook: @hook diff --git a/app/views/root/index.html.haml b/app/views/root/index.html.haml deleted file mode 100644 index 4b1ac213d68..00000000000 --- a/app/views/root/index.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if show_customize_homepage_banner? - = content_for :customize_homepage_banner do - .gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" } - .js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'), - preferences_behavior_path: profile_preferences_path(anchor: 'behavior'), - callouts_path: callouts_path, - callouts_feature_id: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE, - track_label: 'home_page' } } - -= render template: 'dashboard/projects/index' diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml index fd124c2967d..ef1eb6b4e45 100644 --- a/app/views/shared/web_hooks/_hook.html.haml +++ b/app/views/shared/web_hooks/_hook.html.haml @@ -1,7 +1,11 @@ %li .row .col-md-8.col-lg-7 - %strong.light-header= hook.url + %strong.light-header + = hook.url + - if hook.rate_limited? + %span.gl-badge.badge-danger.badge-pill.sm= _('Disabled') + %div - hook.class.triggers.each_value do |trigger| - if hook.public_send(trigger) diff --git a/config/feature_flags/development/scim_token_vue.yml b/config/feature_flags/development/scim_token_vue.yml index 8bf7f2c4da5..8cc82bafd66 100644 --- a/config/feature_flags/development/scim_token_vue.yml +++ b/config/feature_flags/development/scim_token_vue.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347270 milestone: '14.6' type: development group: group::access -default_enabled: false +default_enabled: true diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5269b3b65d1..18f57965f21 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -17190,7 +17190,6 @@ Name of the feature that the callout is for. | `CANARY_DEPLOYMENT` | Callout feature name for canary_deployment. | | `CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. | | `CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. | -| `CUSTOMIZE_HOMEPAGE` | Callout feature name for customize_homepage. | | `EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. | | `FEATURE_FLAGS_NEW_VERSION` | Callout feature name for feature_flags_new_version. | | `GCP_SIGNUP_OFFER` | Callout feature name for gcp_signup_offer. | diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index 5a497da0ea9..1b607cdac1b 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -5,9 +5,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w description: 'Writing styles, markup, formatting, and other standards for GitLab Documentation.' --- -# A-Z word list +# Recommended word list -To help ensure consistency in the documentation, follow this guidance. +To help ensure consistency in the documentation, the Technical Writing team +recommends these wording choices. The GitLab handbook also maintains a list of +[top misused terms](https://about.gitlab.com/handbook/communication/top-misused-terms/). For guidance not on this page, we defer to these style guides: @@ -769,7 +771,7 @@ Use **you**, **your**, and **yours** instead of [**the user** and **the user's** Documentation should be from the [point of view](https://design.gitlab.com/content/voice-tone#point-of-view) of the reader. - Do: You can configure a pipeline. -- Do not: Users can configure a pipeline. +- Do not: Users can configure a pipeline. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 0e81151bd46..66f6d94a602 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -1037,3 +1037,19 @@ scan occurs. Because the cache is downloaded before the analyzer run occurs, the file in the `CI_BUILDS_DIR` directory triggers the dependency scanning job. We recommend committing the lock files, which prevents this warning. + +### I no longer get the latest Docker image after setting `DS_MAJOR_VERSION` or `DS_ANALYZER_IMAGE` + +If you have manually set `DS_MAJOR_VERSION` or `DS_ANALYZER_IMAGE` for specific reasons, +and now must update your configuration to again get the latest patched versions of our +analyzers, edit your `gitlab-ci.yml` file and either: + +- Set your `DS_MAJOR_VERSION` to match the latest version as seen in + [our current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml#L18). +- If you hardcoded the `DS_ANALYZER_IMAGE` variable directly, change it to match the latest + line as found in our [current Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml). + The line number will vary depending on which scanning job you edited. + + For example, currently the `gemnasium-maven-dependency_scanning` job pulls the latest + `gemnasium-maven` Docker image because `DS_ANALYZER_IMAGE` is set to + `"$SECURE_ANALYZERS_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"`. diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index 519b1d94bf5..3c5d223b106 100644 --- a/lib/gitlab/emoji.rb +++ b/lib/gitlab/emoji.rb @@ -6,7 +6,7 @@ module Gitlab # When updating emoji assets increase the version below # and update the version number in `app/assets/javascripts/emoji/index.js` - EMOJI_VERSION = 1 + EMOJI_VERSION = 2 # Return a Pathname to emoji's current versioned folder # diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index 8376417502d..ef146359da9 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -413,7 +413,6 @@ included_attributes: - :b_mode - :too_large - :binary - - :diff metrics: - :created_at - :updated_at @@ -797,6 +796,7 @@ excluded_attributes: - :verification_checksum - :verification_failure merge_request_diff_files: + - :diff - :external_diff_offset - :external_diff_size - :merge_request_diff_id diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb index d84db92fe69..c391f86b47b 100644 --- a/lib/gitlab/import_export/project/relation_factory.rb +++ b/lib/gitlab/import_export/project/relation_factory.rb @@ -131,7 +131,9 @@ module Gitlab end def setup_diff - @relation_hash['diff'] = @relation_hash.delete('utf8_diff') + diff = @relation_hash.delete('utf8_diff') + + parsed_relation_hash['diff'] = diff end def setup_pipeline diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 180c3e85499..3e1ebdf0fa2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10444,15 +10444,6 @@ msgstr "" msgid "Customize your pipeline configuration." msgstr "" -msgid "CustomizeHomepageBanner|Do you want to customize this page?" -msgstr "" - -msgid "CustomizeHomepageBanner|Go to preferences" -msgstr "" - -msgid "CustomizeHomepageBanner|This page shows a list of your projects by default but it can be changed to show projects' activity, groups, your to-do list, assigned issues, assigned merge requests, and more. You can change this under \"Homepage content\" in your preferences" -msgstr "" - msgid "Cycle Time" msgstr "" @@ -11419,6 +11410,9 @@ msgstr "" msgid "Delete variable" msgstr "" +msgid "DeleteProject|Failed to remove events. Please try again or contact administrator." +msgstr "" + msgid "DeleteProject|Failed to remove project repository. Please try again or contact administrator." msgstr "" @@ -13902,9 +13896,6 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" -msgid "Error while loading the project data. Please try again." -msgstr "" - msgid "Error while migrating %{upload_id}: %{error_message}" msgstr "" @@ -17183,6 +17174,9 @@ msgstr "" msgid "GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects." msgstr "" +msgid "GroupsNew|Ask your administrator to %{enable_link_start}enable%{enable_link_end} Group Migration." +msgstr "" + msgid "GroupsNew|Assemble related projects together and grant members access to several projects at once." msgstr "" @@ -17237,6 +17231,9 @@ msgstr "" msgid "GroupsNew|Provide credentials for another instance of GitLab to import your groups directly." msgstr "" +msgid "GroupsNew|This feature is deprecated and replaced by %{docs_link_start}Group Migration%{docs_link_end}." +msgstr "" + msgid "GroupsNew|To import a group, navigate to the group settings for the GitLab source instance, %{link_start}generate an export file%{link_end}, and upload it here." msgstr "" @@ -28336,9 +28333,6 @@ msgstr "" msgid "Promotions|This feature is locked." msgstr "" -msgid "Promotions|Track activity with Contribution Analytics." -msgstr "" - msgid "Promotions|Try it for free" msgstr "" @@ -28351,9 +28345,6 @@ msgstr "" msgid "Promotions|Upgrade your plan to activate Audit Events." msgstr "" -msgid "Promotions|Upgrade your plan to activate Contribution Analytics." -msgstr "" - msgid "Promotions|Upgrade your plan to activate Group Webhooks." msgstr "" @@ -28378,9 +28369,6 @@ msgstr "" msgid "Promotions|When you have a lot of issues, it can be hard to get an overview. By adding a weight to your issues, you can get a better idea of the effort, cost, required time, or value of each, and so better manage them." msgstr "" -msgid "Promotions|With Contribution Analytics you can have an overview for the activity of issues, merge requests, and push events of your organization and its members." -msgstr "" - msgid "Promotions|You can restrict access to protected branches by choosing a role (Maintainers, Developers) as well as certain users." msgstr "" @@ -39427,6 +39415,9 @@ msgstr "" msgid "Webhooks|Tag push events" msgstr "" +msgid "Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook." +msgstr "" + msgid "Webhooks|Trigger" msgstr "" @@ -39484,6 +39475,9 @@ msgstr "" msgid "Webhooks|Use this token to validate received payloads. It is sent with the request in the X-Gitlab-Token HTTP header." msgstr "" +msgid "Webhooks|Webhook was automatically disabled" +msgstr "" + msgid "Webhooks|Wiki page events" msgstr "" diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index 38f8d267a2c..c6a8cee2f70 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -131,28 +131,10 @@ RSpec.describe RootController do context 'who uses the default dashboard setting', :aggregate_failures do render_views - context 'with customize homepage banner' do - it 'renders the default dashboard' do - get :index + it 'renders the default dashboard' do + get :index - expect(response).to render_template 'root/index' - expect(response.body).to have_css('.js-customize-homepage-banner') - end - end - - context 'without customize homepage banner' do - before do - Users::DismissCalloutService.new( - container: nil, current_user: user, params: { feature_name: Users::CalloutsHelper::CUSTOMIZE_HOMEPAGE } - ).execute - end - - it 'renders the default dashboard' do - get :index - - expect(response).to render_template 'root/index' - expect(response.body).not_to have_css('.js-customize-homepage-banner') - end + expect(response).to render_template 'dashboard/projects/index' end end end diff --git a/spec/features/dashboard/root_spec.rb b/spec/features/dashboard/root_spec.rb deleted file mode 100644 index 55bb43c6fcf..00000000000 --- a/spec/features/dashboard/root_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Root path' do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - - before do - project.add_developer(user) - sign_in(user) - end - - it 'shows the customize banner', :js do - visit root_path - - expect(page).to have_content('Do you want to customize this page?') - end -end diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index 85d9feb0c09..ace51204374 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -38,9 +38,16 @@ describe('IDE tree list', () => { beforeEach(() => { bootstrapWithTree(); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$mount(); }); + it('emits tree-ready event', () => { + expect(vm.$emit).toHaveBeenCalledTimes(1); + expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + }); + it('renders loading indicator', (done) => { store.state.trees['abcproject/main'].loading = true; @@ -61,9 +68,15 @@ describe('IDE tree list', () => { beforeEach(() => { bootstrapWithTree(emptyBranchTree); + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$mount(); }); + it('still emits tree-ready event', () => { + expect(vm.$emit).toHaveBeenCalledWith('tree-ready'); + }); + it('does not load files if the branch is empty', () => { expect(vm.$el.textContent).not.toContain('fileName'); expect(vm.$el.textContent).toContain('No files'); diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js index 3fb7781b176..cd10812f8ea 100644 --- a/spec/frontend/ide/ide_router_spec.js +++ b/spec/frontend/ide/ide_router_spec.js @@ -6,6 +6,7 @@ describe('IDE router', () => { const PROJECT_NAMESPACE = 'my-group/sub-group'; const PROJECT_NAME = 'my-project'; const TEST_PATH = `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`; + const DEFAULT_BRANCH = 'default-main'; let store; let router; @@ -13,36 +14,48 @@ describe('IDE router', () => { beforeEach(() => { window.history.replaceState({}, '', '/'); store = createStore(); - router = createRouter(store); + router = createRouter(store, DEFAULT_BRANCH); jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {})); }); - [ - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`, - `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`, - ].forEach((route) => { - it(`finds project path when route is "${route}"`, () => { - router.push(route); + it.each` + route | expectedBranchId | expectedBasePath + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`} | ${'main'} | ${'src/blob/'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`} | ${'main'} | ${'src/blob'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`} | ${'main'} | ${'src/tree/'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`} | ${'weird:branch/name-123'} | ${'src/tree/'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`} | ${'main'} | ${'src/blob'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`} | ${'main'} | ${'src/edit'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`} | ${'main'} | ${'src/merge_requests/2'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`} | ${'blob'} | ${''} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`} | ${DEFAULT_BRANCH} | ${''} + ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`} | ${DEFAULT_BRANCH} | ${''} + `('correctly opens Web IDE for $route', ({ route, expectedBranchId, expectedBasePath } = {}) => { + router.push(route); - expect(store.dispatch).toHaveBeenCalledWith('getProjectData', { - namespace: PROJECT_NAMESPACE, - projectId: PROJECT_NAME, - }); + expect(store.dispatch).toHaveBeenCalledWith('openBranch', { + projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`, + branchId: expectedBranchId, + basePath: expectedBasePath, }); }); + it('correctly opens an MR', () => { + const expectedId = '2'; + + router.push(`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/${expectedId}`); + + expect(store.dispatch).toHaveBeenCalledWith('openMergeRequest', { + projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`, + mergeRequestId: expectedId, + targetProjectId: undefined, + }); + expect(store.dispatch).not.toHaveBeenCalledWith('openBranch'); + }); + it('keeps router in sync when store changes', async () => { expect(router.currentRoute.fullPath).toBe('/'); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 3cf6240c2c5..0fab828dfb3 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -216,35 +216,6 @@ describe('IDE services', () => { ); }); - describe('getProjectData', () => { - it('combines gql and API requests', () => { - const gqlProjectData = { - id: 'gid://gitlab/Project/1', - userPermissions: { - bogus: true, - }, - }; - const expectedResponse = { - ...projectData, - ...gqlProjectData, - id: 1, - }; - Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } })); - query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } })); - - return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then((response) => { - expect(response).toEqual({ data: expectedResponse }); - expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID); - expect(query).toHaveBeenCalledWith({ - query: getIdeProject, - variables: { - projectPath: TEST_PROJECT_ID, - }, - }); - }); - }); - }); - describe('getFiles', () => { let mock; let relativeUrlRoot; @@ -336,4 +307,38 @@ describe('IDE services', () => { }); }); }); + + describe('getProjectPermissionsData', () => { + const TEST_PROJECT_PATH = 'foo/bar'; + + it('queries for the project permissions', () => { + const result = { data: { project: projectData } }; + query.mockResolvedValue(result); + + return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => { + expect(data).toEqual(result.data.project); + expect(query).toHaveBeenCalledWith( + expect.objectContaining({ + query: getIdeProject, + variables: { projectPath: TEST_PROJECT_PATH }, + }), + ); + }); + }); + + it('converts the returned GraphQL id to the regular ID number', () => { + const projectId = 2; + const gqlProjectData = { + id: `gid://gitlab/Project/${projectId}`, + userPermissions: { + bogus: true, + }, + }; + + query.mockResolvedValue({ data: { project: gqlProjectData } }); + return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => { + expect(data.id).toBe(projectId); + }); + }); + }); }); diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js index ca6f7169059..e07dcf22860 100644 --- a/spec/frontend/ide/stores/actions/project_spec.js +++ b/spec/frontend/ide/stores/actions/project_spec.js @@ -2,9 +2,12 @@ import MockAdapter from 'axios-mock-adapter'; import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; import testAction from 'helpers/vuex_action_helper'; import api from '~/api'; +import createFlash from '~/flash'; import service from '~/ide/services'; import { createStore } from '~/ide/stores'; import { + setProject, + fetchProjectPermissions, refreshLastCommitData, showBranchNotFoundError, createNewBranchFromDefault, @@ -13,8 +16,12 @@ import { loadFile, loadBranch, } from '~/ide/stores/actions'; +import { logError } from '~/lib/logger'; import axios from '~/lib/utils/axios_utils'; +jest.mock('~/flash'); +jest.mock('~/lib/logger'); + const TEST_PROJECT_ID = 'abc/def'; describe('IDE store project actions', () => { @@ -34,6 +41,92 @@ describe('IDE store project actions', () => { mock.restore(); }); + describe('setProject', () => { + const project = { id: 'foo', path_with_namespace: TEST_PROJECT_ID }; + const baseMutations = [ + { + type: 'SET_PROJECT', + payload: { + projectPath: TEST_PROJECT_ID, + project, + }, + }, + { + type: 'SET_CURRENT_PROJECT', + payload: TEST_PROJECT_ID, + }, + ]; + + it.each` + desc | payload | expectedMutations + ${'does not commit any action if project is not passed'} | ${undefined} | ${[]} + ${'commits correct actions in the correct order by default'} | ${{ project }} | ${[...baseMutations]} + `('$desc', async ({ payload, expectedMutations } = {}) => { + await testAction({ + action: setProject, + payload, + state: store.state, + expectedMutations, + expectedActions: [], + }); + }); + }); + + describe('fetchProjectPermissions', () => { + const permissionsData = { + userPermissions: { + bogus: true, + }, + }; + const permissionsMutations = [ + { + type: 'UPDATE_PROJECT', + payload: { + projectPath: TEST_PROJECT_ID, + props: { + ...permissionsData, + }, + }, + }, + ]; + + let spy; + + beforeEach(() => { + spy = jest.spyOn(service, 'getProjectPermissionsData'); + }); + + afterEach(() => { + createFlash.mockRestore(); + }); + + it.each` + desc | projectPath | responseSuccess | expectedMutations + ${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]} + ${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]} + ${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]} + `('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => { + store.state.currentProjectId = projectPath; + if (responseSuccess) { + spy.mockResolvedValue(permissionsData); + } else { + spy.mockRejectedValue(); + } + + await testAction({ + action: fetchProjectPermissions, + state: store.state, + expectedMutations, + expectedActions: [], + }); + + if (!responseSuccess) { + expect(logError).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalled(); + } + }); + }); + describe('refreshLastCommitData', () => { beforeEach(() => { store.state.currentProjectId = 'abc/def'; diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js index b3ce39c33d2..0fdd7798f00 100644 --- a/spec/frontend/ide/stores/mutations/project_spec.js +++ b/spec/frontend/ide/stores/mutations/project_spec.js @@ -3,21 +3,48 @@ import state from '~/ide/stores/state'; describe('Multi-file store branch mutations', () => { let localState; + const nonExistentProj = 'nonexistent'; + const existingProj = 'abcproject'; beforeEach(() => { localState = state(); - localState.projects = { abcproject: { empty_repo: true } }; + localState.projects = { [existingProj]: { empty_repo: true } }; }); describe('TOGGLE_EMPTY_STATE', () => { it('sets empty_repo for project to passed value', () => { - mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false }); + mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: false }); - expect(localState.projects.abcproject.empty_repo).toBe(false); + expect(localState.projects[existingProj].empty_repo).toBe(false); - mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true }); + mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: true }); - expect(localState.projects.abcproject.empty_repo).toBe(true); + expect(localState.projects[existingProj].empty_repo).toBe(true); + }); + }); + + describe('UPDATE_PROJECT', () => { + it.each` + desc | projectPath | props | expectedProps + ${'extends existing project with the passed props'} | ${existingProj} | ${{ foo1: 'bar' }} | ${{ foo1: 'bar' }} + ${'overrides existing props on the exsiting project'} | ${existingProj} | ${{ empty_repo: false }} | ${{ empty_repo: false }} + ${'does nothing if the project does not exist'} | ${nonExistentProj} | ${{ foo2: 'bar' }} | ${undefined} + ${'does nothing if project is not passed'} | ${undefined} | ${{ foo3: 'bar' }} | ${undefined} + ${'does nothing if the props are not passed'} | ${existingProj} | ${undefined} | ${{}} + ${'does nothing if the props are empty'} | ${existingProj} | ${{}} | ${{}} + `('$desc', ({ projectPath, props, expectedProps } = {}) => { + const origProject = localState.projects[projectPath]; + + mutations.UPDATE_PROJECT(localState, { projectPath, props }); + + if (!expectedProps) { + expect(localState.projects[projectPath]).toBeUndefined(); + } else { + expect(localState.projects[projectPath]).toEqual({ + ...origProject, + ...expectedProps, + }); + } }); }); }); diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js deleted file mode 100644 index f84800d8266..00000000000 --- a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import { GlBanner } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper'; -import axios from '~/lib/utils/axios_utils'; -import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue'; - -const svgPath = '/illustrations/background'; -const provide = { - svgPath, - preferencesBehaviorPath: 'some/behavior/path', - calloutsPath: 'call/out/path', - calloutsFeatureId: 'some-feature-id', - trackLabel: 'home_page', -}; - -const createComponent = () => { - return shallowMount(CustomizeHomepageBanner, { provide, stubs: { GlBanner } }); -}; - -describe('CustomizeHomepageBanner', () => { - let trackingSpy; - let mockAxios; - let wrapper; - - beforeEach(() => { - mockAxios = new MockAdapter(axios); - document.body.dataset.page = 'some:page'; - trackingSpy = mockTracking('_category_', undefined, jest.spyOn); - wrapper = createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - mockAxios.restore(); - unmockTracking(); - }); - - it('should render the banner when not dismissed', () => { - expect(wrapper.find(GlBanner).exists()).toBe(true); - }); - - it('should close the banner when dismiss is clicked', async () => { - mockAxios.onPost(provide.calloutsPath).replyOnce(200); - expect(wrapper.find(GlBanner).exists()).toBe(true); - wrapper.find(GlBanner).vm.$emit('close'); - - await wrapper.vm.$nextTick(); - expect(wrapper.find(GlBanner).exists()).toBe(false); - }); - - it('includes the body text from options', () => { - expect(wrapper.html()).toContain(wrapper.vm.$options.i18n.body); - }); - - describe('tracking', () => { - const preferencesTrackingEvent = 'click_go_to_preferences'; - const mockTrackingOnWrapper = () => { - unmockTracking(); - trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn); - }; - - it('sets the needed data attributes for tracking button', async () => { - await wrapper.vm.$nextTick(); - const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`); - - expect(button.attributes('data-track-action')).toEqual(preferencesTrackingEvent); - expect(button.attributes('data-track-label')).toEqual(provide.trackLabel); - }); - - it('sends a tracking event when the banner is shown', () => { - const trackCategory = undefined; - const trackEvent = 'show_home_page_banner'; - - expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, { - label: provide.trackLabel, - }); - }); - - it('sends a tracking event when the banner is dismissed', async () => { - mockTrackingOnWrapper(); - mockAxios.onPost(provide.calloutsPath).replyOnce(200); - const trackCategory = undefined; - const trackEvent = 'click_dismiss'; - - wrapper.find(GlBanner).vm.$emit('close'); - - await wrapper.vm.$nextTick(); - expect(trackingSpy).toHaveBeenCalledWith(trackCategory, trackEvent, { - label: provide.trackLabel, - }); - }); - - it('sends a tracking event when the button is clicked', async () => { - mockTrackingOnWrapper(); - mockAxios.onPost(provide.calloutsPath).replyOnce(200); - const button = wrapper.find(`[href='${wrapper.vm.preferencesBehaviorPath}']`); - - triggerEvent(button.element); - - await wrapper.vm.$nextTick(); - expect(trackingSpy).toHaveBeenCalledWith('_category_', preferencesTrackingEvent, { - label: provide.trackLabel, - }); - }); - }); -}); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js index 1faa3b0af1d..884bc4684ba 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js @@ -75,7 +75,7 @@ export const mockSuggestedColors = { '#013220': 'Dark green', '#6699cc': 'Blue-gray', '#0000ff': 'Blue', - '#e6e6fa': 'Lavendar', + '#e6e6fa': 'Lavender', '#9400d3': 'Dark violet', '#330066': 'Deep violet', '#808080': 'Gray', diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js index 56b2e298aa3..54a522324f5 100644 --- a/spec/frontend_integration/ide/helpers/ide_helper.js +++ b/spec/frontend_integration/ide/helpers/ide_helper.js @@ -192,6 +192,13 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName = switchLeftSidebarTab('Commit'); screen.getByTestId('begin-commit-button').click(); + await waitForMonacoEditor(); + + const mrCheck = await screen.findByLabelText('Start a new merge request'); + if (Boolean(mrCheck.checked) !== newMR) { + mrCheck.click(); + } + if (!newBranch) { const option = await screen.findByLabelText(/Commit to .+ branch/); option.click(); @@ -201,12 +208,9 @@ export const commit = async ({ newBranch = false, newMR = false, newBranchName = const branchNameInput = await screen.findByTestId('ide-new-branch-name'); fireEvent.input(branchNameInput, { target: { value: newBranchName } }); - - const mrCheck = await screen.findByLabelText('Start a new merge request'); - if (Boolean(mrCheck.checked) !== newMR) { - mrCheck.click(); - } } screen.getByText('Commit').click(); + + await waitForMonacoEditor(); }; diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js index 4451c1ee946..3c5ed9dfe20 100644 --- a/spec/frontend_integration/ide/helpers/start.js +++ b/spec/frontend_integration/ide/helpers/start.js @@ -4,16 +4,18 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; import { initIde } from '~/ide'; import extendStore from '~/ide/stores/extend'; +import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures'; import { IDE_DATASET } from './mock_data'; export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => { const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum'; const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`; + const project = isRepoEmpty ? getEmptyProject() : getProject(); setWindowLocation(`${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`); const el = document.createElement('div'); - Object.assign(el.dataset, IDE_DATASET); + Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) }); container.appendChild(el); const vm = initIde(el, { extendStore }); diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js index f56cd008d1c..c9d78d1de8f 100644 --- a/spec/frontend_integration/ide/user_opens_ide_spec.js +++ b/spec/frontend_integration/ide/user_opens_ide_spec.js @@ -34,10 +34,10 @@ describe('IDE: User opens IDE', () => { expect(await screen.findByText('No files')).toBeDefined(); }); - it('shows a "New file" button', async () => { - const button = await screen.findByTitle('New file'); + it('shows a "New file" button', () => { + const buttons = screen.queryAllByTitle('New file'); - expect(button.tagName).toEqual('BUTTON'); + expect(buttons.map((x) => x.tagName)).toContain('BUTTON'); }); }); diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index a06c9ec6699..dc0a234f981 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -34,7 +34,7 @@ RSpec.describe IdeHelper do self.instance_variable_set(:@fork_info, fork_info) self.instance_variable_set(:@project, project) - serialized_project = API::Entities::Project.represent(project).to_json + serialized_project = API::Entities::Project.represent(project, current_user: project.creator).to_json expect(helper.ide_data) .to include( diff --git a/spec/helpers/users/callouts_helper_spec.rb b/spec/helpers/users/callouts_helper_spec.rb index ba4d8797a24..85e11c2ed3b 100644 --- a/spec/helpers/users/callouts_helper_spec.rb +++ b/spec/helpers/users/callouts_helper_spec.rb @@ -61,36 +61,6 @@ RSpec.describe Users::CalloutsHelper do end end - describe '.show_customize_homepage_banner?' do - subject { helper.show_customize_homepage_banner? } - - context 'when user has not dismissed' do - before do - allow(helper).to receive(:user_dismissed?).with(described_class::CUSTOMIZE_HOMEPAGE) { false } - end - - context 'when user is on the default dashboard' do - it { is_expected.to be true } - end - - context 'when user is not on the default dashboard' do - before do - user.dashboard = 'stars' - end - - it { is_expected.to be false } - end - end - - context 'when user dismissed' do - before do - allow(helper).to receive(:user_dismissed?).with(described_class::CUSTOMIZE_HOMEPAGE) { true } - end - - it { is_expected.to be false } - end - end - describe '.render_flash_user_callout' do it 'renders the flash_user_callout partial' do expect(helper).to receive(:render) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0fa3756ff3a..6ffe2187466 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -253,7 +253,6 @@ MergeRequestDiffFile: - b_mode - too_large - binary -- diff MergeRequestContextCommit: - id - authored_date diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3f9c3bc6858..19e09c99a15 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -6138,20 +6138,6 @@ RSpec.describe User do end end - describe '#default_dashboard?' do - it 'is the default dashboard' do - user = build(:user) - - expect(user.default_dashboard?).to be true - end - - it 'is not the default dashboard' do - user = build(:user, dashboard: 'stars') - - expect(user.default_dashboard?).to be false - end - end - describe '.dormant' do it 'returns dormant users' do freeze_time do diff --git a/spec/services/events/destroy_service_spec.rb b/spec/services/events/destroy_service_spec.rb new file mode 100644 index 00000000000..8dcbb83eb1d --- /dev/null +++ b/spec/services/events/destroy_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Events::DestroyService do + subject(:service) { described_class.new(project) } + + let_it_be(:project, reload: true) { create(:project, :repository) } + let_it_be(:another_project) { create(:project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user) } + + let!(:unrelated_event) { create(:event, :merged, project: another_project, target: another_project, author: user) } + + before do + create(:event, :created, project: project, target: project, author: user) + create(:event, :created, project: project, target: merge_request, author: user) + create(:event, :merged, project: project, target: merge_request, author: user) + end + + let(:events) { project.events } + + describe '#execute', :aggregate_failures do + it 'deletes the events' do + response = nil + + expect { response = subject.execute }.to change(Event, :count).by(-3) + + expect(response).to be_success + expect(unrelated_event.reload).to be_present + end + + context 'when an error is raised while deleting the records' do + before do + allow(project).to receive_message_chain(:events, :all, :delete_all).and_raise(ActiveRecord::ActiveRecordError) + end + + it 'returns error' do + response = subject.execute + + expect(response).to be_error + expect(response.message).to eq 'Failed to remove events.' + end + + it 'does not delete events' do + expect { subject.execute }.not_to change(Event, :count) + end + end + end +end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index ac84614121a..18bcfe3f3b4 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -545,6 +545,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do end end + context 'when project has events' do + let!(:event) { create(:event, :created, project: project, target: project, author: user) } + + it 'deletes events from the project' do + expect do + destroy_project(project, user) + end.to change(Event, :count).by(-1) + end + + context 'when an error is returned while deleting events' do + it 'does not delete project' do + allow_next_instance_of(Events::DestroyService) do |instance| + allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo')) + end + + expect(destroy_project(project, user)).to be_falsey + expect(project.delete_error).to include('Failed to remove events') + end + end + end + context 'error while destroying', :sidekiq_inline do let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) } diff --git a/spec/views/projects/hooks/edit.html.haml_spec.rb b/spec/views/projects/hooks/edit.html.haml_spec.rb new file mode 100644 index 00000000000..1265334a572 --- /dev/null +++ b/spec/views/projects/hooks/edit.html.haml_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/hooks/edit' do + let(:hook) { create(:project_hook, project: project) } + + let_it_be_with_refind(:project) { create(:project) } + + before do + assign :project, project + assign :hook, hook + end + + it 'renders webhook page with "Recent events"' do + render + + expect(rendered).to have_css('h4', text: _('Webhook')) + expect(rendered).to have_text(_('Recent events')) + end + + context 'webhook is rate limited' do + before do + allow(hook).to receive(:rate_limited?).and_return(true) + end + + it 'renders alert' do + render + + expect(rendered).to have_text(s_('Webhooks|Webhook was automatically disabled')) + end + end +end diff --git a/spec/views/projects/hooks/index.html.haml_spec.rb b/spec/views/projects/hooks/index.html.haml_spec.rb new file mode 100644 index 00000000000..eb2b7334b98 --- /dev/null +++ b/spec/views/projects/hooks/index.html.haml_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/hooks/index' do + let(:existing_hook) { create(:project_hook, project: project) } + let(:new_hook) { ProjectHook.new } + + let_it_be_with_refind(:project) { create(:project) } + + before do + assign :project, project + assign :hooks, [existing_hook] + assign :hook, new_hook + end + + it 'renders webhooks page with "Project Hooks"' do + render + + expect(rendered).to have_css('h4', text: _('Webhooks')) + expect(rendered).to have_text('Project Hooks') + expect(rendered).not_to have_css('.gl-badge', text: _('Disabled')) + end + + context 'webhook is rate limited' do + before do + allow(existing_hook).to receive(:rate_limited?).and_return(true) + end + + it 'renders "Disabled" badge' do + render + + expect(rendered).to have_css('.gl-badge', text: _('Disabled')) + end + end +end