Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-24 21:09:57 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-24 21:09:57 +0300
commit80b22a4413679216b470c7a4e9fefd0eb928add5 (patch)
tree909b8eb16b9316d5260e609b11d0eb7d8dc03323 /app
parent92849dc177d5e0d11f89b4ca75f4e3e45ad6341b (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue7
-rw-r--r--app/assets/javascripts/clone_panel.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_app.vue21
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js27
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js7
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js35
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js6
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue5
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss2
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/models/product_analytics_event.rb37
-rw-r--r--app/models/project.rb4
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/services/product_analytics/build_activity_graph_service.rb13
-rw-r--r--app/services/product_analytics/build_graph_service.rb33
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/buttons/_code.html.haml (renamed from app/views/projects/buttons/_clone.html.haml)16
-rw-r--r--app/views/projects/buttons/_download.html.haml20
-rw-r--r--app/views/projects/buttons/_download_links.html.haml9
-rw-r--r--app/views/projects/buttons/_download_menu_items.html.haml21
-rw-r--r--app/views/projects/empty.html.haml6
-rw-r--r--app/views/projects/tree/_tree_header.html.haml10
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml4
25 files changed, 154 insertions, 148 deletions
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index 64229f54904..3a9ec341789 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -2,6 +2,7 @@
import { GlBanner, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
const defaultTitle = __('CI/CD Catalog');
@@ -11,6 +12,7 @@ const defaultDescription = s__(
export default {
components: {
+ BetaBadge,
GlBanner,
GlLink,
},
@@ -58,7 +60,10 @@ export default {
{{ $options.i18n.banner.description }}
</p>
</gl-banner>
- <h1 class="page-title gl-font-size-h-display">{{ pageTitle }}</h1>
+ <div class="gl-my-4 gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h-display">{{ pageTitle }}</h1>
+ <beta-badge class="gl-ml-3" />
+ </div>
<p>
<span data-testid="page-description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index 79280c13f0f..d2a5d5a9db7 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -14,7 +14,7 @@ export default function initClonePanel() {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- $('a', $cloneOptions).on('click', (e) => {
+ $('.js-clone-links a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
if (
diff --git a/app/assets/javascripts/environments/folder/environments_folder_app.vue b/app/assets/javascripts/environments/folder/environments_folder_app.vue
new file mode 100644
index 00000000000..a963ca9b144
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_app.vue
@@ -0,0 +1,21 @@
+<script>
+import { s__ } from '~/locale';
+
+export default {
+ props: {
+ folderName: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ pageTitle: s__('Environments|Environments'),
+ },
+};
+</script>
+<template>
+ <h4 class="gl-font-weight-normal" data-testid="folder-name">
+ {{ $options.i18n.pageTitle }} /
+ <b>{{ folderName }}</b>
+ </h4>
+</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 1a32de30de0..beaf8041c39 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -2,7 +2,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
-import EnvironmentsFolderApp from './environments_folder_view.vue';
+import EnvironmentsFolderView from './environments_folder_view.vue';
+import EnvironmentsFolderApp from './environments_folder_app.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -13,19 +14,35 @@ const apolloProvider = new VueApollo({
export default () => {
const el = document.getElementById('environments-folder-list-view');
+ const environmentsData = el.dataset;
+ if (gon.features.environmentsFolderNewLook) {
+ const folderName = environmentsData.environmentsDataFolderName;
+
+ return new Vue({
+ el,
+ components: {
+ EnvironmentsFolderApp,
+ },
+ render(createElement) {
+ return createElement('environments-folder-app', {
+ props: {
+ folderName,
+ },
+ });
+ },
+ });
+ }
return new Vue({
el,
components: {
- EnvironmentsFolderApp,
+ EnvironmentsFolderView,
},
apolloProvider,
provide: {
projectPath: el.dataset.projectPath,
},
data() {
- const environmentsData = el.dataset;
-
return {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
@@ -33,7 +50,7 @@ export default () => {
};
},
render(createElement) {
- return createElement('environments-folder-app', {
+ return createElement('environments-folder-view', {
props: {
endpoint: this.endpoint,
folderName: this.folderName,
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 9111dc9c86c..eab25298c36 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -10,6 +10,7 @@ import produce from 'immer';
import {
getK8sPods,
handleClusterError,
+ buildWatchPath,
} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
@@ -62,9 +63,7 @@ const mapWorkloadItems = (items, kind) => {
const watchWorkloadItems = ({ kind, apiVersion, configuration, namespace, client }) => {
const itemKind = kind.toLowerCase().replace('list', 's');
- const path = namespace
- ? `/apis/${apiVersion}/namespaces/${namespace}/${itemKind}`
- : `/apis/${apiVersion}/${itemKind}`;
+ const path = buildWatchPath({ resource: itemKind, api: `apis/${apiVersion}`, namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
@@ -113,7 +112,7 @@ const mapServicesItems = (items) => {
};
const watchServices = ({ configuration, namespace, client }) => {
- const path = namespace ? `/api/v1/namespaces/${namespace}/services` : '/api/v1/services';
+ const path = buildWatchPath({ resource: 'services', namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 39a8b1d0a9c..1babccff425 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -262,6 +262,38 @@ class GfmAutoComplete {
});
}
+ // eslint-disable-next-line class-methods-use-this
+ setSubmitReviewStates($input) {
+ if (!window.gon.features?.mrRequestChanges) return;
+
+ const REVIEW_STATES = {
+ reviewed: {
+ header: __('Comment'),
+ description: __('Submit general feedback without explicit approval.'),
+ },
+ approve: {
+ header: __('Approve'),
+ description: __('Submit feedback and approve these changes.'),
+ },
+ requested_changes: {
+ header: __('Request changes'),
+ description: __('Submit feedback that should be addressed before merging.'),
+ },
+ };
+
+ $input.filter('[data-supports-quick-actions="true"]').atwho({
+ // Always keep the trailing space otherwise the command won't display correctly
+ at: '/submit_review ',
+ alias: 'submit_review',
+ data: Object.keys(REVIEW_STATES),
+ displayTpl({ name }) {
+ const reviewState = REVIEW_STATES[name];
+
+ return `<li><span class="gl-font-weight-bold gl-display-block">${reviewState.header}</span><small class="description gl-display-block gl-w-full gl-float-left! gl-px-0!">${reviewState.description}</small></li>`;
+ },
+ });
+ }
+
setupEmoji($input) {
const fetchData = this.fetchData.bind(this);
@@ -851,6 +883,9 @@ class GfmAutoComplete {
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then((data) => {
+ if (data.some((c) => c.name === 'submit_review')) {
+ this.setSubmitReviewStates($input);
+ }
this.loadData($input, at, data);
})
.catch(() => {
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
index 22ddd7c6a61..4eec31cfe37 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
@@ -9,8 +9,12 @@ export const handleClusterError = async (err) => {
throw errorData;
};
+export const buildWatchPath = ({ resource, api = 'api/v1', namespace = '' }) => {
+ return namespace ? `/${api}/namespaces/${namespace}/${resource}` : `/${api}/${resource}`;
+};
+
export const watchPods = ({ client, query, configuration, namespace }) => {
- const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const path = buildWatchPath({ resource: 'pods', namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index 23b2ae74e41..be9c768ae60 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -38,9 +38,7 @@ export default {
},
computed: {
showAiActions() {
- return (
- this.resourceGlobalId && this.glFeatures.aiGlobalSwitch && this.glFeatures.summarizeNotes
- );
+ return this.resourceGlobalId && this.glFeatures.summarizeNotes;
},
},
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index e0c49412d56..307be0b0aa0 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -67,10 +67,7 @@ export default {
return this.currentScope === SCOPE_MILESTONES;
},
showWikiBlobsFilters() {
- return (
- this.currentScope === SCOPE_WIKI_BLOBS &&
- this.glFeatures?.searchProjectWikisHideArchivedProjects
- );
+ return this.currentScope === SCOPE_WIKI_BLOBS;
},
},
methods: {
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index 8d8da10268a..504f1405148 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -53,7 +53,7 @@
}
}
- .project-clone-holder {
+ .project-code-holder {
display: inline-block;
margin: $gl-padding 0 0;
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4b2749dc716..8cdd6efa7c5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -14,6 +14,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:k8s_watch_api, project)
end
+ before_action only: [:folder] do
+ push_frontend_feature_flag(:environments_folder_new_look, project)
+ end
+
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb
deleted file mode 100644
index 52baa3be6c4..00000000000
--- a/app/models/product_analytics_event.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-class ProductAnalyticsEvent < ApplicationRecord
- self.table_name = 'product_analytics_events_experimental'
-
- # Ignore that the partition key :project_id is part of the formal primary key
- self.primary_key = :id
-
- belongs_to :project
-
- validates :event_id, :project_id, :v_collector, :v_etl, presence: true
-
- # There is no default Rails timestamps in the table.
- # collector_tstamp is a timestamp when a collector recorded an event.
- scope :order_by_time, -> { order(collector_tstamp: :desc) }
-
- # If we decide to change this scope to use date_trunc('day', collector_tstamp),
- # we should remember that a btree index on collector_tstamp will be no longer effective.
- scope :timerange, ->(duration, today = Time.zone.today) {
- where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
- }
-
- def self.count_by_graph(graph, days)
- group(graph).timerange(days).count
- end
-
- def self.count_collector_tstamp_by_day(days)
- group("DATE_TRUNC('day', collector_tstamp)")
- .reorder('date_trunc_day_collector_tstamp')
- .timerange(days)
- .count
- end
-
- def as_json_wo_empty
- as_json.compact
- end
-end
diff --git a/app/models/project.rb b/app/models/project.rb
index 57c127965f7..5788885498c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -468,10 +468,6 @@ class Project < ApplicationRecord
# rubocop:enable Cop/ActiveRecordDependent
has_many :active_pages_deployments, -> { active }, class_name: 'PagesDeployment', inverse_of: :project
- # Can be too many records. We need to implement delete_all in batches.
- # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
- has_many :product_analytics_events, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 04fbc8467c9..ccab3d9f02d 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -33,6 +33,7 @@ class UserPolicy < BasePolicy
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
+ enable :make_profile_private
end
rule { default }.enable :read_user_profile
diff --git a/app/services/product_analytics/build_activity_graph_service.rb b/app/services/product_analytics/build_activity_graph_service.rb
deleted file mode 100644
index 63108d76afd..00000000000
--- a/app/services/product_analytics/build_activity_graph_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module ProductAnalytics
- class BuildActivityGraphService < BuildGraphService
- def execute
- timerange = @params[:timerange].days
-
- results = product_analytics_events.count_collector_tstamp_by_day(timerange)
-
- format_results('collector_tstamp', results.transform_keys(&:to_date))
- end
- end
-end
diff --git a/app/services/product_analytics/build_graph_service.rb b/app/services/product_analytics/build_graph_service.rb
deleted file mode 100644
index da54ad4de0e..00000000000
--- a/app/services/product_analytics/build_graph_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module ProductAnalytics
- class BuildGraphService
- def initialize(project, params)
- @project = project
- @params = params
- end
-
- def execute
- graph = @params[:graph].to_sym
- timerange = @params[:timerange].days
-
- results = product_analytics_events.count_by_graph(graph, timerange)
-
- format_results(graph, results)
- end
-
- private
-
- def format_results(name, results)
- {
- id: name,
- keys: results.keys,
- values: results.values
- }
- end
-
- def product_analytics_events
- @project.product_analytics_events
- end
- end
-end
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 00da6c73081..68e73a017a7 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -5,8 +5,8 @@
.controls.gl-display-flex
= link_button_to nil, project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'd-none d-sm-inline-flex has-tooltip', icon: 'rss'
- if is_project_overview && can?(current_user, :download_code, @project)
- .project-clone-holder.d-none.d-md-inline-flex.gl-ml-2
- = render "projects/buttons/clone", dropdown_class: 'dropdown-menu-right'
+ .project-code-holder.d-none.d-md-inline-flex.gl-ml-2
+ = render "projects/buttons/code", dropdown_class: 'dropdown-menu-right', ref: @ref
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
.loading
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_code.html.haml
index 0e645eda678..0b2a527025e 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_code.html.haml
@@ -1,15 +1,16 @@
- project = project || @project
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
+- ref = local_assigns.fetch(:ref)
- if can?(current_user, :download_code, @project)
.git-clone-holder.js-git-clone-holder
= render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { id: 'clone-dropdown', class: 'clone-dropdown-btn', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }) do
- %span.gl-mr-2.js-clone-dropdown-label
- = _('Clone')
+ %span.js-clone-dropdown-label
+ = _('Code')
= sprite_icon("chevron-down", css_class: "icon")
- %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
+ %ul.dropdown-menu.dropdown-menu-large.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
- if ssh_enabled?
- %li{ class: 'gl-px-4!' }
+ %li.gl-dropdown-item.js-clone-links{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with SSH')
.input-group.btn-group
@@ -18,7 +19,7 @@
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
= render_if_exists 'projects/buttons/geo'
- if http_enabled?
- %li.pt-2{ class: 'gl-px-4!' }
+ %li.pt-2.gl-dropdown-item.js-clone-links{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group.btn-group
@@ -28,7 +29,7 @@
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
%li.divider.mt-2
- %li.pt-2.gl-dropdown-item
+ %li.pt-2.gl-dropdown-item.js-clone-links
%label.label-bold{ class: 'gl-px-4!' }
= _('Open in your IDE')
- if ssh_enabled?
@@ -53,3 +54,6 @@
%a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
.gl-dropdown-item-text-wrapper
= _("Xcode")
+ - if !project.empty_repo? && can?(current_user, :download_code, project)
+ %li.divider.mt-2
+ = render 'projects/buttons/download_menu_items', project: project, ref: ref
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index b3282742407..946057a2e2f 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,27 +1,13 @@
- project = local_assigns.fetch(:project)
- ref = local_assigns.fetch(:ref)
-- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
+- pipeline = local_assigns.fetch(:pipeline, nil)
- css_class = local_assigns.fetch(:css_class, '')
- if !project.empty_repo? && can?(current_user, :download_code, project)
- - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.gl-dropdown.inline{ class: css_class }>
= render Pajamas::ButtonComponent.new(button_options: { class: 'dropdown-toggle gl-dropdown-toggle dropdown-icon-only has-tooltip', title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { testid: 'download-source-code-button' } }) do
= sprite_icon('download', css_class: 'gl-icon dropdown-icon')
%span.sr-only= _('Select Archive Format')
= sprite_icon('chevron-down', css_class: 'gl-icon dropdown-chevron')
- .dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %section
- %h5.m-0.dropdown-bold-header= _('Download source code')
- .dropdown-menu-content
- = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
- .js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
- - if pipeline && pipeline.latest_builds_with_artifacts.any?
- %section.border-top.pt-1.mt-1
- %h5.m-0.dropdown-bold-header= _('Download artifacts')
- - unless pipeline.latest?
- %span.unclickable= ci_status_for_statuseable(project.latest_pipeline(ref))
- %h6.m-0.dropdown-header= _('Previous Artifacts')
- %ul
- - pipeline.latest_builds_with_artifacts.each do |job|
- %li= link_to job.name, latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: ''
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
+ = render 'projects/buttons/download_menu_items', project: project, ref: ref, pipeline: pipeline
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index 31185fc1532..7035f3b3792 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -1,4 +1,5 @@
-.btn-group.ml-0.w-100
- - Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
- = link_button_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', variant: index == 0 ? :confirm : :default, size: :small
+- Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
+ - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
+
+ %a.dropdown-item.open-with-link{ href: external_storage_url_or_path(archive_path), rel: 'nofollow', download: '' }
+ .gl-dropdown-item-text-wrapper= fmt
diff --git a/app/views/projects/buttons/_download_menu_items.html.haml b/app/views/projects/buttons/_download_menu_items.html.haml
new file mode 100644
index 00000000000..e4fcae1815c
--- /dev/null
+++ b/app/views/projects/buttons/_download_menu_items.html.haml
@@ -0,0 +1,21 @@
+- project = local_assigns.fetch(:project)
+- ref = local_assigns.fetch(:ref)
+- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
+- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
+
+%li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Download source code')
+ = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
+.js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
+- if pipeline && pipeline.latest_builds_with_artifacts.any?
+ %li.divider.mt-2
+ %li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Download artifacts')
+ - unless pipeline.latest?
+ %span.gl-ml-3.unclickable= ci_status_for_statuseable(project.latest_pipeline(ref))
+ %li.divider.mt-2
+ %li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Previous Artifacts')
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), class: 'dropdown-item open-with-link', rel: 'nofollow', download: '' do
+ .gl-dropdown-item-text-wrapper= job.name
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 60b5ab376ec..ca9900a646e 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -19,11 +19,11 @@
= _('You can get started by cloning the repository or start adding files to it with one of the following options.')
.project-buttons{ data: { testid: 'quick-actions-container' } }
- .project-clone-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
+ .project-code-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
= render "shared/mobile_clone_panel"
- .project-clone-holder.d-none.d-md-inline-block.gl-mb-3.gl-mr-3.float-left
- = render "projects/buttons/clone"
+ .project-code-holder.d-none.d-md-inline-block.gl-mb-3.gl-mr-3.float-left
+ = render "projects/buttons/code", ref: @ref
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true
- if can?(current_user, :push_code, @project)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index bed37d9cb63..8eb0bb85e66 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -16,10 +16,10 @@
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
- = render 'projects/buttons/download', project: @project, ref: @ref
- .project-clone-holder.d-none.d-md-inline-block>
- = render "projects/buttons/clone", dropdown_class: 'dropdown-menu-right'
+ .project-code-holder.d-none.d-md-inline-block>
+ = render "projects/buttons/code", dropdown_class: 'dropdown-menu-right', ref: @ref
- .project-clone-holder.d-block.d-md-none.mt-sm-2.mt-md-0.ml-md-2>
- = render "shared/mobile_clone_panel"
+ .project-code-holder.d-block.d-md-none.mt-sm-2.mt-md-0.ml-md-2>
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render "shared/mobile_clone_panel", ref: @ref
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 4b39ec52837..7dce8737eb4 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -10,9 +10,9 @@
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { testid: 'clone-dropdown-content' } }
- %li
+ %li.js-clone-links
= ssh_clone_button(container)
- %li
+ %li.js-clone-links
= http_clone_button(container)
= render_if_exists 'shared/kerberos_clone_button', container: container
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index c594cee326e..2f7fb348c17 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -8,9 +8,9 @@
= sprite_icon("chevron-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
- if ssh_enabled?
- %li
+ %li.js-clone-links
= dropdown_item_with_description(ssh_copy_label, ssh_clone_url_to_repo(project), href: ssh_clone_url_to_repo(project), data: { clone_type: 'ssh' }, default: true)
- if http_enabled?
- %li
+ %li.js-clone-links
= dropdown_item_with_description(http_copy_label, http_clone_url_to_repo(project), href: http_clone_url_to_repo(project), data: { clone_type: 'http' })
= render_if_exists 'shared/mobile_kerberos_clone'