diff options
Diffstat (limited to 'app')
26 files changed, 432 insertions, 71 deletions
diff --git a/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql index 648cd8b66b5..f93f5ad4f11 100644 --- a/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql +++ b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql @@ -1,7 +1,7 @@ query ciConfigVariables($fullPath: ID!, $ref: String!) { project(fullPath: $fullPath) { id - ciConfigVariables(sha: $ref) { + ciConfigVariables(ref: $ref) { description key value diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js index a44855c14d5..fb201576e85 100644 --- a/app/assets/javascripts/projects/default_project_templates.js +++ b/app/assets/javascripts/projects/default_project_templates.js @@ -121,4 +121,8 @@ export default { text: s__('ProjectTemplates|TYPO3 Distribution'), icon: '.template-option .icon-typo3', }, + laravel: { + text: s__('ProjectTemplates|Laravel Framework'), + icon: '.template-option .icon-laravel', + }, }; diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index c3bd3b9b5d1..8248e11da33 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -1,16 +1,36 @@ <script> import { kebabCase } from 'lodash'; -import { GlCollapse, GlIcon, GlBadge } from '@gitlab/ui'; +import { GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui'; +import { s__ } from '~/locale'; import { CLICK_MENU_ITEM_ACTION, TRACKING_UNKNOWN_ID } from '~/super_sidebar/constants'; export default { + i18n: { + pinItem: s__('Navigation|Pin item'), + unpinItem: s__('Navigation|Unpin item'), + }, name: 'NavItem', components: { + GlButton, GlCollapse, GlIcon, GlBadge, }, + inject: { + pinnedItemIds: { default: { ids: [] } }, + panelSupportsPins: { default: false }, + }, props: { + draggable: { + type: Boolean, + required: false, + default: false, + }, + isStatic: { + type: Boolean, + required: false, + default: false, + }, item: { type: Object, required: true, @@ -54,6 +74,12 @@ export default { } return this.item.is_active; }, + isPinnable() { + return this.panelSupportsPins && !this.isSection && !this.isStatic; + }, + isPinned() { + return this.pinnedItemIds.ids.includes(this.item.id); + }, trackingProps() { if (!this.item.id) { return { @@ -87,7 +113,10 @@ export default { // Reset user agent styles on <button> 'gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left': this.isSection, 'gl-w-full gl-focus--focus': this.isSection, + 'nav-item-link': !this.isSection, 'gl-bg-t-gray-a-08': this.isActive, + 'gl-py-2': this.isPinnable, + 'gl-py-3': !this.isPinnable, ...this.linkClasses, }; }, @@ -108,7 +137,7 @@ export default { <component :is="elem" v-bind="linkProps" - class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-py-3 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-text-decoration-none!" + class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-text-decoration-none!" :class="computedLinkClasses" data-qa-selector="sidebar_menu_link" data-testid="nav-item-link" @@ -124,6 +153,11 @@ export default { <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3"> <slot name="icon"> <gl-icon v-if="item.icon" :name="item.icon" class="gl-ml-2" /> + <gl-icon + v-else-if="draggable" + name="grip" + class="gl-text-gray-400 gl-ml-2 draggable-icon" + /> </slot> </div> <div class="gl-pr-3 gl-text-gray-900 gl-truncate-end"> @@ -133,11 +167,27 @@ export default { </div> </div> <slot name="actions"></slot> - <span v-if="isSection || hasPill" class="gl-flex-grow-1 gl-text-right gl-mr-3"> + <span v-if="isSection || hasPill || isPinnable" class="gl-flex-grow-1 gl-text-right gl-mr-3"> <gl-badge v-if="hasPill" size="sm" variant="info"> {{ pillData }} </gl-badge> <gl-icon v-else-if="isSection" :name="collapseIcon" /> + <gl-button + v-else-if="isPinnable && !isPinned" + size="small" + category="tertiary" + icon="thumbtack" + :aria-label="$options.i18n.pinItem" + @click.prevent="$emit('pin-add', item.id)" + /> + <gl-button + v-else-if="isPinnable && isPinned" + size="small" + category="tertiary" + :aria-label="$options.i18n.unpinItem" + icon="thumbtack-solid" + @click.prevent="$emit('pin-remove', item.id)" + /> </span> </component> <gl-collapse @@ -152,6 +202,8 @@ export default { v-for="subItem of item.items" :key="`${item.title}-${subItem.title}`" :item="subItem" + @pin-add="(itemId) => $emit('pin-add', itemId)" + @pin-remove="(itemId) => $emit('pin-remove', itemId)" /> </gl-collapse> </li> diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue new file mode 100644 index 00000000000..34aeae26d1f --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue @@ -0,0 +1,97 @@ +<script> +import { GlCollapse, GlIcon } from '@gitlab/ui'; +import Draggable from 'vuedraggable'; +import { s__ } from '~/locale'; +import NavItem from './nav_item.vue'; + +export default { + i18n: { + pinned: s__('Navigation|Pinned'), + emptyHint: s__('Navigation|Your pinned items appear here.'), + }, + name: 'PinnedSection', + components: { + Draggable, + GlCollapse, + GlIcon, + NavItem, + }, + props: { + items: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + expanded: true, + draggableItems: this.items, + }; + }, + computed: { + collapseIcon() { + return this.expanded ? 'chevron-up' : 'chevron-down'; + }, + itemIds() { + return this.draggableItems.map((item) => item.id); + }, + }, + watch: { + items(newItems) { + this.draggableItems = newItems; + }, + }, + methods: { + handleDrag(event) { + if (event.oldIndex === event.newIndex) return; + this.$emit( + 'pin-reorder', + this.items[event.oldIndex].id, + this.items[event.newIndex].id, + event.oldIndex < event.newIndex, + ); + }, + }, +}; +</script> + +<template> + <section class="gl-mx-2"> + <a + href="#" + class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-py-3 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-text-decoration-none!" + @click="expanded = !expanded" + > + <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3"> + <gl-icon name="thumbtack" class="gl-ml-2" /> + </div> + + <span class="gl-font-weight-bold gl-font-sm gl-flex-grow-1">{{ $options.i18n.pinned }}</span> + <gl-icon :name="collapseIcon" class="gl-mr-3" /> + </a> + <gl-collapse v-model="expanded"> + <draggable + v-if="items.length > 0" + v-model="draggableItems" + class="gl-p-0 gl-m-0" + data-testid="pinned-nav-items" + handle=".draggable-icon" + tag="ul" + @end="handleDrag" + > + <nav-item + v-for="item of draggableItems" + :key="item.id" + draggable + :item="item" + @pin-remove="(itemId) => $emit('pin-remove', itemId)" + /> + </draggable> + <div v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem"> + {{ $options.i18n.emptyHint }} + </div> + </gl-collapse> + <hr class="gl-my-2" /> + </section> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index fc8968c50ea..843db691816 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -1,24 +1,156 @@ <script> +import * as Sentry from '@sentry/browser'; +import axios from '~/lib/utils/axios_utils'; +import { PANELS_WITH_PINS } from '../constants'; import NavItem from './nav_item.vue'; +import PinnedSection from './pinned_section.vue'; export default { name: 'SidebarMenu', components: { NavItem, + PinnedSection, + }, + + provide() { + return { + pinnedItemIds: this.changedPinnedItemIds, + panelSupportsPins: this.supportsPins, + }; }, props: { items: { type: Array, required: true, }, + pinnedItemIds: { + type: Array, + required: false, + default: () => [], + }, + panelType: { + type: String, + required: false, + default: '', + }, + updatePinsUrl: { + type: String, + required: true, + }, + }, + + data() { + return { + // This is used as a provide and injected into the nav items. + // Note: It has to be an object to be reactive. + changedPinnedItemIds: { ids: this.pinnedItemIds }, + }; + }, + + computed: { + // Returns the list of items that we want to have static at the top. + // Only sidebars that support pins also support a static section. + staticItems() { + if (!this.supportsPins) return []; + return this.items.filter((item) => !item.items || item.items.length === 0); + }, + + // Returns only the items that aren't static at the top and makes sure no + // section shows as active (and expanded) when one of its items is pinned. + nonStaticItems() { + if (!this.supportsPins) return this.items; + + return this.items + .filter((item) => item.items && item.items.length > 0) + .map((item) => { + const hasActivePinnedChild = item.items.some((childItem) => { + return childItem.is_active && this.changedPinnedItemIds.ids.includes(childItem.id); + }); + const showAsActive = item.is_active && !hasActivePinnedChild; + + return { ...item, is_active: showAsActive }; + }); + }, + + // Returns a flat list of all items that are in sections, but not the sections. + // Only items from sections (item.items) can be pinned. + flatPinnableItems() { + return this.nonStaticItems.flatMap((item) => item.items).filter(Boolean); + }, + + pinnedItems() { + return this.changedPinnedItemIds.ids + .map((id) => this.flatPinnableItems.find((item) => item.id === id)) + .filter(Boolean); + }, + supportsPins() { + return PANELS_WITH_PINS.includes(this.panelType); + }, + }, + methods: { + createPin(itemId) { + this.changedPinnedItemIds.ids.push(itemId); + this.updatePins(); + }, + destroyPin(itemId) { + this.changedPinnedItemIds.ids = this.changedPinnedItemIds.ids.filter((id) => id !== itemId); + this.updatePins(); + }, + movePin(fromId, toId, isDownwards) { + const fromIndex = this.changedPinnedItemIds.ids.indexOf(fromId); + this.changedPinnedItemIds.ids.splice(fromIndex, 1); + + let toIndex = this.changedPinnedItemIds.ids.indexOf(toId); + + // If the item was moved downwards, we insert it *after* the item it was dragged on to. + // This matches how vuedraggable previews the change while still dragging. + if (isDownwards) toIndex += 1; + + this.changedPinnedItemIds.ids.splice(toIndex, 0, fromId); + + this.updatePins(); + }, + updatePins() { + axios + .put(this.updatePinsUrl, { + panel: this.panelType, + menu_item_ids: this.changedPinnedItemIds.ids, + }) + .then((response) => { + this.changedPinnedItemIds.ids = response.data; + }) + .catch((e) => { + Sentry.captureException(e); + }); + }, }, }; </script> <template> <nav class="gl-py-2 gl-relative"> + <section v-if="staticItems.length > 0" class="gl-mx-2"> + <ul class="gl-p-0 gl-m-0"> + <nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static /> + </ul> + <hr class="gl-my-2" /> + </section> + + <pinned-section + v-if="supportsPins" + :items="pinnedItems" + @pin-remove="destroyPin" + @pin-reorder="movePin" + /> + <ul class="gl-px-2 gl-list-style-none"> - <nav-item v-for="item in items" :key="`menu-${item.title}`" :item="item" /> + <nav-item + v-for="item in nonStaticItems" + :key="item.id" + :item="item" + @pin-add="createPin" + @pin-remove="destroyPin" + /> </ul> </nav> </template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index b7a9583cae9..6807b4b2c7e 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -92,7 +92,12 @@ export default { /> </gl-collapse> <gl-collapse :visible="!contextSwitcherOpen"> - <sidebar-menu :items="menuItems" /> + <sidebar-menu + :items="menuItems" + :panel-type="sidebarData.panel_type" + :pinned-item-ids="sidebarData.pinned_items" + :update-pins-url="sidebarData.update_pins_url" + /> <sidebar-portal-target /> </gl-collapse> </div> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index ad9d4bc43f2..8290c4f533f 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -15,3 +15,5 @@ export const MAX_FREQUENT_GROUPS_COUNT = 3; export const TRACKING_UNKNOWN_ID = 'item_without_id'; export const CLICK_MENU_ITEM_ACTION = 'click_menu_item'; + +export const PANELS_WITH_PINS = ['group', 'project']; diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index 48c87682897..1431d8ed154 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -144,6 +144,24 @@ @include active-toggle; } } + + .nav-item-link { + button, + .draggable-icon { + opacity: 0; + } + + .draggable-icon { + cursor: grab; + } + + &:hover { + button, + .draggable-icon { + opacity: 1; + } + } + } } .super-sidebar-skip-to { diff --git a/app/assets/stylesheets/page_bundles/escalation_policies.scss b/app/assets/stylesheets/page_bundles/escalation_policies.scss index 84c62ba93dd..5ca3dbcbcef 100644 --- a/app/assets/stylesheets/page_bundles/escalation_policies.scss +++ b/app/assets/stylesheets/page_bundles/escalation_policies.scss @@ -28,7 +28,7 @@ $stroke-size: 1px; .escalation-rule-row { @media (max-width: $breakpoint-lg) { - @include gl-flex-wrap; + @include gl-flex-wrap-wrap; } } diff --git a/app/controllers/users/pins_controller.rb b/app/controllers/users/pins_controller.rb new file mode 100644 index 00000000000..81709dd4a2b --- /dev/null +++ b/app/controllers/users/pins_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Users + class PinsController < ApplicationController + feature_category :navigation + respond_to :json + + def update + panel = pins_params[:panel] + pinned_nav_items = current_user.pinned_nav_items.merge({ panel => pins_params[:menu_item_ids] }) + if current_user.update(pinned_nav_items: pinned_nav_items) + render json: current_user.pinned_nav_items[panel].to_json + else + head :bad_request + end + end + + private + + def pins_params + params.permit(:panel, menu_item_ids: []) + end + end +end diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb index bc1dcb3ad5f..5f03ae77338 100644 --- a/app/finders/ci/runners_finder.rb +++ b/app/finders/ci/runners_finder.rb @@ -74,7 +74,7 @@ module Ci end def project_runners - raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_project, @project) + raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :read_project_runners, @project) @runners = ::Ci::Runner.owned_or_instance_wide(@project.id) end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index a67ead051c2..6530894562b 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -25,8 +25,13 @@ module Types alpha: { milestone: '15.3' }, description: 'CI/CD config variable.' do argument :sha, GraphQL::Types::String, - required: true, - description: 'Sha.' + required: false, + description: 'Sha.', + deprecated: { reason: 'Use `ref`', milestone: '15.11' } + + argument :ref, GraphQL::Types::String, + required: false, # Make required when `sha` argument is removed + description: 'Ref.' end field :full_path, GraphQL::Types::ID, @@ -645,8 +650,10 @@ module Types # Even if the parameter name is `sha`, it is actually a ref name. We always send `ref` to the endpoint. # See: https://gitlab.com/gitlab-org/gitlab/-/issues/389065 - def ci_config_variables(sha:) - result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(sha) + # Remove `sha` argument and make `ref` required in a future release + # See: https://gitlab.com/gitlab-org/gitlab/-/issues/404493 + def ci_config_variables(ref: nil, sha: nil) + result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref || sha) return if result.nil? diff --git a/app/helpers/ci/catalog/resources_helper.rb b/app/helpers/ci/catalog/resources_helper.rb index c40c881aa20..9f70410f17f 100644 --- a/app/helpers/ci/catalog/resources_helper.rb +++ b/app/helpers/ci/catalog/resources_helper.rb @@ -3,7 +3,7 @@ module Ci module Catalog module ResourcesHelper - def can_view_private_catalog?(_project) + def can_view_namespace_catalog?(_project) false end @@ -13,3 +13,5 @@ module Ci end end end + +Ci::Catalog::ResourcesHelper.prepend_mod_with('Ci::Catalog::ResourcesHelper') diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index a37c4e057b3..3769f03feb0 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -42,7 +42,7 @@ module SidebarsHelper Sidebars::Context.new(**context_data, **args) end - def super_sidebar_context(user, group:, project:, panel:) + def super_sidebar_context(user, group:, project:, panel:, panel_type:) # rubocop:disable Metrics/AbcSize { current_menu_items: panel.super_sidebar_menu_items, current_context_header: panel.super_sidebar_context_header, @@ -84,7 +84,10 @@ module SidebarsHelper canary_toggle_com_url: Gitlab::Saas.canary_toggle_com_url, current_context: super_sidebar_current_context(project: project, group: group), context_switcher_links: context_switcher_links, - search: search_data + search: search_data, + pinned_items: user.pinned_nav_items[panel_type] || [], + panel_type: panel_type, + update_pins_url: pins_url } end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index ae2d3758110..d5d1d38784e 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -44,23 +44,18 @@ class BulkImports::Entity < ApplicationRecord validates :source_full_path, presence: true, format: { with: Gitlab::Regex.bulk_import_source_full_path_regex, - message: Gitlab::Regex.bulk_import_destination_namespace_path_regex_message } + message: Gitlab::Regex.bulk_import_source_full_path_regex_message } validates :destination_name, presence: true, - format: { with: Gitlab::Regex.group_path_regex, - message: Gitlab::Regex.group_path_regex_message } + if: -> { group || project } validates :destination_namespace, exclusion: [nil], - format: { with: Gitlab::Regex.bulk_import_destination_namespace_path_regex, - message: Gitlab::Regex.bulk_import_destination_namespace_path_regex_message }, if: :group validates :destination_namespace, presence: true, - format: { with: Gitlab::Regex.bulk_import_destination_namespace_path_regex, - message: Gitlab::Regex.bulk_import_destination_namespace_path_regex_message }, if: :project validate :validate_parent_is_a_group, if: :parent diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb index 92464cb645f..b9e777f27a0 100644 --- a/app/models/ci/catalog/listing.rb +++ b/app/models/ci/catalog/listing.rb @@ -27,7 +27,7 @@ module Ci def projects_in_namespace_visible_to_user Project .in_namespace(namespace.self_and_descendant_ids) - .public_or_visible_to_user(current_user) + .public_or_visible_to_user(current_user, ::Gitlab::Access::DEVELOPER) end end end diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb index ccf182e4b83..864ea04c019 100644 --- a/app/models/pages/lookup_path.rb +++ b/app/models/pages/lookup_path.rb @@ -61,6 +61,13 @@ module Pages end strong_memoize_attr :unique_host + def root_directory + return unless deployment + + deployment.root_directory + end + strong_memoize_attr :root_directory + private attr_reader :project, :trim_prefix, :domain diff --git a/app/models/project.rb b/app/models/project.rb index f49a1a65cba..9190e273151 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2409,6 +2409,8 @@ class Project < ApplicationRecord .append(key: 'CI_SERVER_HOST', value: Gitlab.config.gitlab.host) .append(key: 'CI_SERVER_PORT', value: Gitlab.config.gitlab.port.to_s) .append(key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol) + .append(key: 'CI_SERVER_SHELL_SSH_HOST', value: Gitlab.config.gitlab_shell.ssh_host.to_s) + .append(key: 'CI_SERVER_SHELL_SSH_PORT', value: Gitlab.config.gitlab_shell.ssh_port.to_s) .append(key: 'CI_SERVER_NAME', value: 'GitLab') .append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) .append(key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s) diff --git a/app/models/user.rb b/app/models/user.rb index ff131b9093b..c6afadb482f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -366,6 +366,7 @@ class User < ApplicationRecord :diffs_addition_color, :diffs_addition_color=, :use_legacy_web_ide, :use_legacy_web_ide=, :use_new_navigation, :use_new_navigation=, + :pinned_nav_items, :pinned_nav_items=, :achievements_enabled, :achievements_enabled=, to: :user_preference diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index bc2c6b526b8..ecc64da2098 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -24,6 +24,8 @@ class UserPreference < ApplicationRecord allow_blank: true validates :use_legacy_web_ide, allow_nil: false, inclusion: { in: [true, false] } + validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' } + ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22' attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT } diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8a0bbbba4d9..d6d0eefe3cd 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -242,6 +242,8 @@ class ProjectPolicy < BasePolicy Feature.enabled?(:create_runner_workflow_for_namespace, project.namespace) end + condition(:namespace_catalog_available) { namespace_catalog_available? } + # `:read_project` may be prevented in EE, but `:read_project_for_iids` should # not. rule { guest | admin }.enable :read_project_for_iids @@ -261,7 +263,6 @@ class ProjectPolicy < BasePolicy enable :reporter_access enable :developer_access enable :maintainer_access - enable :add_catalog_resource enable :change_namespace enable :change_visibility_level @@ -279,9 +280,6 @@ class ProjectPolicy < BasePolicy enable :set_show_default_award_emojis enable :set_show_diff_preview_in_email enable :set_warn_about_potentially_unwanted_characters - - enable :register_project_runners - enable :create_project_runners enable :manage_owners end @@ -537,6 +535,8 @@ class ProjectPolicy < BasePolicy enable :admin_feature_flags_client enable :register_project_runners enable :create_project_runners + enable :admin_project_runners + enable :read_project_runners enable :update_runners_registration_token enable :admin_project_google_cloud enable :admin_project_aws @@ -875,6 +875,14 @@ class ProjectPolicy < BasePolicy # Should be matched with GroupPolicy#read_internal_note rule { admin | can?(:reporter_access) }.enable :read_internal_note + rule { can?(:developer_access) & namespace_catalog_available }.policy do + enable :read_namespace_catalog + end + + rule { can?(:owner_access) & namespace_catalog_available }.policy do + enable :add_catalog_resource + end + private def user_is_user? @@ -968,6 +976,10 @@ class ProjectPolicy < BasePolicy def project @subject end + + def namespace_catalog_available? + false + end end ProjectPolicy.prepend_mod_with('ProjectPolicy') diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb index 348280030f4..aec32209b19 100644 --- a/app/services/bulk_imports/create_service.rb +++ b/app/services/bulk_imports/create_service.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Entry point of the BulkImport feature. +# Entry point of the BulkImport/Direct Transfer feature. # This service receives a Gitlab Instance connection params # and a list of groups to be imported. # @@ -84,6 +84,8 @@ module BulkImports Array.wrap(params).each do |entity_params| track_access_level(entity_params) + validate_destination_namespace(entity_params[:destination_namespace]) + validate_destination_slug(entity_params[:destination_slug] || entity_params[:destination_name]) validate_destination_full_path(entity_params) BulkImports::Entity.create!( @@ -135,6 +137,18 @@ module BulkImports credentials[:url].starts_with?(Settings.gitlab.base_url) end + def validate_destination_namespace(destination_namespace) + return if destination_namespace =~ Gitlab::Regex.bulk_import_destination_namespace_path_regex + + raise BulkImports::Error.destination_namespace_validation_failure + end + + def validate_destination_slug(destination_slug) + return if destination_slug =~ Gitlab::Regex.oci_repository_path_regex + + raise BulkImports::Error.destination_slug_validation_failure + end + def validate_destination_full_path(entity_params) source_type = entity_params[:source_type] diff --git a/app/services/ci/catalog/add_resource_service.rb b/app/services/ci/catalog/add_resource_service.rb deleted file mode 100644 index 1f53513b7d1..00000000000 --- a/app/services/ci/catalog/add_resource_service.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -module Ci - module Catalog - class AddResourceService - include Gitlab::Allowable - - attr_reader :project, :current_user - - def initialize(project, user) - @current_user = user - @project = project - end - - def execute - raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project) - - validation_response = Ci::Catalog::ValidateResourceService.new(project, project.default_branch).execute - - if validation_response.success? - create_catalog_resource - else - ServiceResponse.error(message: validation_response.message) - end - end - - private - - def create_catalog_resource - catalog_resource = Ci::Catalog::Resource.new(project: project) - - if catalog_resource.valid? - catalog_resource.save! - ServiceResponse.success(payload: catalog_resource) - else - ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', ')) - end - end - end - end -end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 0fadd75669e..403f645392c 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -90,7 +90,8 @@ module Projects file: file, file_count: deployment_update.entries_count, file_sha256: sha256, - ci_build_id: build.id + ci_build_id: build.id, + root_directory: build.options[:publish] ) break if deployment.size != file.size || deployment.file.size != file.size diff --git a/app/validators/json_schemas/pinned_nav_items.json b/app/validators/json_schemas/pinned_nav_items.json new file mode 100644 index 00000000000..60dee5cc463 --- /dev/null +++ b/app/validators/json_schemas/pinned_nav_items.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Pinned navigation items per panel", + "type": "object", + "properties": { + "group": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "project": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false +} diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 0b9072f6876..7e7f8a6a871 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -6,7 +6,7 @@ - group = @parent_group || @group - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user) - - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel).to_json + - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url } } - if display_whats_new? |