diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-13 15:15:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-13 15:15:20 +0300 |
commit | 944a3a7b7e19354abdfcaa79129d0736c4b8565f (patch) | |
tree | 627802e84525946f11fdd6976ab5f04fb69e702c /app | |
parent | 62798ed33c878f936009da05fdddb707e1c7d58d (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
44 files changed, 504 insertions, 405 deletions
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 0251ffe28f9..2f2b2ed1a90 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -332,7 +332,7 @@ export default { <template> <div - class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row" + class="design-detail js-design-detail fixed-top gl-w-full gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row" > <div class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative" diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/components/experiment_header.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/components/experiment_header.vue new file mode 100644 index 00000000000..5b840a5ed43 --- /dev/null +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/components/experiment_header.vue @@ -0,0 +1,47 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue'; +import { __ } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; + +export default { + name: 'ExperimentHeader', + components: { + DeleteButton, + GlButton, + }, + props: { + title: { + type: String, + required: true, + }, + deleteInfo: { + type: Object, + required: true, + }, + }, + methods: { + downloadCsv() { + const currentPath = window.location.pathname; + const currentSearch = window.location.search; + + visitUrl(`${currentPath}.csv${currentSearch}`); + }, + }, + i18n: { + downloadAsCsvLabel: __('Download as CSV'), + }, +}; +</script> + +<template> + <div class="detail-page-header gl-flex-wrap-wrap"> + <div class="detail-page-header-body"> + <h1 class="page-title gl-font-size-h-display flex-fill">{{ title }}</h1> + + <gl-button @click="downloadCsv">{{ $options.i18n.downloadAsCsvLabel }}</gl-button> + + <delete-button v-bind="deleteInfo" /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue index 40b9e0723e9..acb5fc7cad2 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue @@ -12,7 +12,7 @@ import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue'; import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue'; -import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue'; +import ExperimentHeader from './components/experiment_header.vue'; import { LIST_KEY_CREATED_AT, BASE_SORT_FIELDS, @@ -31,7 +31,7 @@ export default { IncubationAlert, RegistrySearch, KeysetPagination, - DeleteButton, + ExperimentHeader, }, props: { experiment: { @@ -130,6 +130,14 @@ export default { hasItems() { return this.candidates.length > 0; }, + deleteButtonInfo() { + return { + deletePath: this.experiment.path, + deleteConfirmationText: translations.DELETE_EXPERIMENT_CONFIRMATION_MESSAGE, + actionPrimaryText: translations.DELETE_EXPERIMENT_PRIMARY_ACTION_LABEL, + modalTitle: translations.DELETE_EXPERIMENT_MODAL_TITLE, + }; + }, }, methods: { submitFilters() { @@ -163,20 +171,7 @@ export default { :link-to-feedback-issue="$options.constants.FEATURE_FEEDBACK_ISSUE" /> - <div class="detail-page-header gl-flex-wrap-wrap"> - <div class="detail-page-header-body"> - <h1 class="page-title gl-font-size-h-display flex-fill"> - {{ experiment.name }} - </h1> - - <delete-button - :delete-path="experiment.path" - :delete-confirmation-text="$options.i18n.DELETE_EXPERIMENT_CONFIRMATION_MESSAGE" - :action-primary-text="$options.i18n.DELETE_EXPERIMENT_PRIMARY_ACTION_LABEL" - :modal-title="$options.i18n.DELETE_EXPERIMENT_MODAL_TITLE" - /> - </div> - </div> + <experiment-header :title="experiment.name" :delete-info="deleteButtonInfo" /> <registry-search :filters="filters" diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue index 02a3870f499..1c81f652387 100644 --- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue +++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue @@ -22,7 +22,9 @@ export default { ...mapState(['navigation', 'urlQuery']), }, created() { - this.fetchSidebarCount(); + if (this.urlQuery?.search) { + this.fetchSidebarCount(); + } }, methods: { ...mapActions(['fetchSidebarCount']), diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js index 395629dc70c..f123494fba2 100644 --- a/app/assets/javascripts/search/sidebar/constants/index.js +++ b/app/assets/javascripts/search/sidebar/constants/index.js @@ -12,7 +12,7 @@ export const NAV_LINK_DEFAULT_CLASSES = [ 'gl-justify-content-space-between', ]; export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal']; -export const HR_DEFAULT_CLASSES = ['gl-my-5', 'gl-mx-5', 'gl-border-gray-100']; +export const HR_DEFAULT_CLASSES = ['gl-m-5', 'gl-border-gray-100']; export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block']; export const TRACKING_LABEL_CHECKBOX = 'Checkbox'; diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue index 5c1cebe0195..fa9da6cef9d 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue @@ -150,7 +150,12 @@ export default { {{ $options.i18n.switchTo }} </div> <ul :aria-label="$options.i18n.switchTo" class="gl-p-0"> - <nav-item v-for="item in persistentLinks" :key="item.link" :item="item" /> + <nav-item + v-for="item in persistentLinks" + :key="item.link" + :item="item" + :link-classes="{ [item.link_classes]: item.link_classes }" + /> </ul> </li> <projects-list diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue index 451e12df697..e56ef9e410b 100644 --- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue +++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue @@ -38,7 +38,7 @@ export default { <button v-collapse-toggle.context-switcher type="button" - class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-border-0 border-top border-bottom gl-border-gray-a-08 gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-h-8" + class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 border-top border-bottom gl-border-gray-a-08 gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-h-8" > <span v-if="context.icon" diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue index eb256e4971b..4fa15f1cd76 100644 --- a/app/assets/javascripts/super_sidebar/components/groups_list.vue +++ b/app/assets/javascripts/super_sidebar/components/groups_list.vue @@ -36,11 +36,14 @@ export default { storageKey() { return `${this.username}/frequent-groups`; }, - viewAllItem() { + viewAllProps() { return { - link: this.viewAllLink, - title: s__('Navigation|View all your groups'), - icon: 'group', + item: { + link: this.viewAllLink, + title: s__('Navigation|View all your groups'), + icon: 'group', + }, + linkClasses: { 'dashboard-shortcuts-groups': true }, }; }, }, @@ -61,7 +64,7 @@ export default { :search-results="searchResults" > <template #view-all-items> - <nav-item :item="viewAllItem" /> + <nav-item v-bind="viewAllProps" /> </template> </search-results> <frequent-items-list @@ -72,7 +75,7 @@ export default { :pristine-text="$options.i18n.pristineText" > <template #view-all-items> - <nav-item :item="viewAllItem" /> + <nav-item v-bind="viewAllProps" /> </template> </frequent-items-list> </template> diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 3a3eb72a4a8..53698a808a7 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -146,7 +146,7 @@ export default { <component :is="elem" v-bind="linkProps" - 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="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none!" :class="computedLinkClasses" data-qa-selector="nav_item_link" data-testid="nav-item-link" diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue index d1c0e757a91..193de143c2b 100644 --- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue +++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue @@ -60,7 +60,7 @@ export default { <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!" + 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-focus-bg-t-gray-a-08 gl-text-decoration-none!" @click.prevent="expanded = !expanded" > <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3"> diff --git a/app/assets/javascripts/super_sidebar/components/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue index b7a29a78d5f..78860e35eb1 100644 --- a/app/assets/javascripts/super_sidebar/components/projects_list.vue +++ b/app/assets/javascripts/super_sidebar/components/projects_list.vue @@ -36,11 +36,14 @@ export default { storageKey() { return `${this.username}/frequent-projects`; }, - viewAllItem() { + viewAllProps() { return { - link: this.viewAllLink, - title: s__('Navigation|View all your projects'), - icon: 'project', + item: { + link: this.viewAllLink, + title: s__('Navigation|View all your projects'), + icon: 'project', + }, + linkClasses: { 'dashboard-shortcuts-projects': true }, }; }, }, @@ -62,7 +65,7 @@ export default { :search-results="searchResults" > <template #view-all-items> - <nav-item :item="viewAllItem" /> + <nav-item v-bind="viewAllProps" /> </template> </search-results> <frequent-items-list @@ -73,7 +76,7 @@ export default { :pristine-text="$options.i18n.pristineText" > <template #view-all-items> - <nav-item :item="viewAllItem" /> + <nav-item v-bind="viewAllProps" /> </template> </frequent-items-list> </template> diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index 302b6a9d4b0..2a95d2c37c4 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -108,5 +108,14 @@ export default { </div> </div> </aside> + <a + v-for="shortcutLink in sidebarData.shortcut_links" + :key="shortcutLink.href" + :href="shortcutLink.href" + :class="shortcutLink.css_class" + class="gl-display-none" + > + {{ shortcutLink.title }} + </a> </div> </template> diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index 498c082ddb2..b69ebc6be17 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -137,7 +137,7 @@ export default { <div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"> <counter v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues" - class="gl-flex-basis-third" + class="gl-flex-basis-third dashboard-shortcuts-issues" icon="issues" :count="sidebarData.assigned_open_issues_count" :href="sidebarData.issues_dashboard_path" @@ -165,7 +165,7 @@ export default { </merge-request-menu> <counter v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList" - class="gl-flex-basis-third" + class="gl-flex-basis-third shortcuts-todos" icon="todo-done" :count="sidebarData.todos_pending_count" href="/dashboard/todos" diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 0d18835ac0f..22c6fb04d3b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -8,7 +8,7 @@ $search-input-field-x-min-width: 200px; min-height: $header-height; border: 0; position: fixed; - top: 0; + top: $calc-application-bars-height; left: 0; right: 0; border-radius: 0; @@ -479,11 +479,6 @@ $search-input-field-x-min-width: 200px; visibility: visible; } -.with-performance-bar .navbar-gitlab, -.with-performance-bar .fixed-top { - top: $performance-bar-height; -} - .navbar-empty { justify-content: center; height: $header-height; diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss index db59a482c64..946a241e6dd 100644 --- a/app/assets/stylesheets/framework/system_messages.scss +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -36,27 +36,6 @@ } } -// System Header -.with-system-header { - // main navigation - // login page - .navbar-gitlab, - .fixed-top { - top: $system-header-height; - } - - // Performance Bar - // System Header - &.with-performance-bar { - // main navigation - header.navbar-gitlab, - .fixed-top { - top: $performance-bar-height + $system-header-height; - } - - } -} - // System Footer .with-system-footer { // navless pages' footer eg: login page diff --git a/app/assets/stylesheets/page_bundles/design_management.scss b/app/assets/stylesheets/page_bundles/design_management.scss index 143682e1cd7..f56eb4ae6fb 100644 --- a/app/assets/stylesheets/page_bundles/design_management.scss +++ b/app/assets/stylesheets/page_bundles/design_management.scss @@ -20,10 +20,7 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); .design-detail { background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity); - - .with-performance-bar & { - top: 35px; - } + bottom: $calc-application-footer-height; .comment-indicator { border-radius: 50%; diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss index cde570cfb0f..d37e87b5cd5 100644 --- a/app/assets/stylesheets/page_bundles/search.scss +++ b/app/assets/stylesheets/page_bundles/search.scss @@ -21,18 +21,18 @@ $border-radius-medium: 3px; } } +.language-filter-checkbox { + .custom-control-label { + flex-grow: 1; + } +} + .search-sidebar { @include media-breakpoint-up(md) { min-width: $search-sidebar-min-width; max-width: $search-sidebar-max-width; } - .language-filter-checkbox { - .custom-control-label { - flex-grow: 1; - } - } - .language-filter-max-height { max-height: $language-filter-max-height; } diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index de2a2ddcf32..5219cd62775 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -750,7 +750,7 @@ kbd { min-height: var(--header-height, 48px); border: 0; position: fixed; - top: 0; + top: calc(var(--system-header-height) + var(--performance-bar-height)); left: 0; right: 0; border-radius: 0; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index e0a71a1c306..16bf2c20bbc 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -750,7 +750,7 @@ kbd { min-height: var(--header-height, 48px); border: 0; position: fixed; - top: 0; + top: calc(var(--system-header-height) + var(--performance-bar-height)); left: 0; right: 0; border-radius: 0; diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss index 3663c97f41a..fcdcab5caeb 100644 --- a/app/assets/stylesheets/startup/startup-signin.scss +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -769,6 +769,9 @@ svg { fill: currentColor; } +.fixed-top { + top: calc(var(--system-header-height) + var(--performance-bar-height)); +} .gl-display-flex { display: flex; } diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 66c543aa654..e5f99879166 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -39,6 +39,12 @@ .border-radius-small { border-radius: $border-radius-small; } .box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; } +// Override Bootstrap class with offset for system-header and +// performance bar when present +.fixed-top { + top: $calc-application-bars-height; +} + .gl-children-ml-sm-3 > * { @include media-breakpoint-up(sm) { @include gl-ml-3; diff --git a/app/finders/clusters/agent_authorizations_finder.rb b/app/finders/clusters/agent_authorizations_finder.rb deleted file mode 100644 index 70c0868cc7f..00000000000 --- a/app/finders/clusters/agent_authorizations_finder.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module Clusters - class AgentAuthorizationsFinder - def initialize(project) - @project = project - end - - def execute - # closest, most-specific authorization for a given agent wins - (project_authorizations + implicit_authorizations + group_authorizations) - .uniq(&:agent_id) - end - - private - - attr_reader :project - - def implicit_authorizations - project.cluster_agents.map do |agent| - Clusters::Agents::ImplicitAuthorization.new(agent: agent) - end - end - - # rubocop: disable CodeReuse/ActiveRecord - def project_authorizations - namespace_ids = project.group ? all_namespace_ids : project.namespace_id - - Clusters::Agents::ProjectAuthorization - .where(project_id: project.id) - .joins(agent: :project) - .preload(agent: :project) - .where(cluster_agents: { projects: { namespace_id: namespace_ids } }) - .with_available_ci_access_fields(project) - .to_a - end - - def group_authorizations - return [] unless project.group - - authorizations = Clusters::Agents::GroupAuthorization.arel_table - - ordered_ancestors_cte = Gitlab::SQL::CTE.new( - :ordered_ancestors, - project.group.self_and_ancestors(hierarchy_order: :asc).reselect(:id) - ) - - cte_join_sources = authorizations.join(ordered_ancestors_cte.table).on( - authorizations[:group_id].eq(ordered_ancestors_cte.table[:id]) - ).join_sources - - Clusters::Agents::GroupAuthorization - .with(ordered_ancestors_cte.to_arel) - .joins(cte_join_sources) - .joins(agent: :project) - .with_available_ci_access_fields(project) - .where(projects: { namespace_id: all_namespace_ids }) - .order(Arel.sql('agent_id, array_position(ARRAY(SELECT id FROM ordered_ancestors)::bigint[], agent_group_authorizations.group_id)')) - .select('DISTINCT ON (agent_id) agent_group_authorizations.*') - .preload(agent: :project) - .to_a - end - # rubocop: enable CodeReuse/ActiveRecord - - def all_namespace_ids - project.root_ancestor.self_and_descendants.select(:id) - end - end -end diff --git a/app/finders/clusters/agents/authorizations/ci_access/finder.rb b/app/finders/clusters/agents/authorizations/ci_access/finder.rb new file mode 100644 index 00000000000..97d378669a4 --- /dev/null +++ b/app/finders/clusters/agents/authorizations/ci_access/finder.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class Finder + def initialize(project) + @project = project + end + + def execute + # closest, most-specific authorization for a given agent wins + (project_authorizations + implicit_authorizations + group_authorizations) + .uniq(&:agent_id) + end + + private + + attr_reader :project + + def implicit_authorizations + project.cluster_agents.map do |agent| + Clusters::Agents::Authorizations::CiAccess::ImplicitAuthorization.new(agent: agent) + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def project_authorizations + namespace_ids = project.group ? all_namespace_ids : project.namespace_id + + Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization + .where(project_id: project.id) + .joins(agent: :project) + .preload(agent: :project) + .where(cluster_agents: { projects: { namespace_id: namespace_ids } }) + .with_available_ci_access_fields(project) + .to_a + end + + def group_authorizations + return [] unless project.group + + authorizations = Clusters::Agents::Authorizations::CiAccess::GroupAuthorization.arel_table + + ordered_ancestors_cte = Gitlab::SQL::CTE.new( + :ordered_ancestors, + project.group.self_and_ancestors(hierarchy_order: :asc).reselect(:id) + ) + + cte_join_sources = authorizations.join(ordered_ancestors_cte.table).on( + authorizations[:group_id].eq(ordered_ancestors_cte.table[:id]) + ).join_sources + + Clusters::Agents::Authorizations::CiAccess::GroupAuthorization + .with(ordered_ancestors_cte.to_arel) + .joins(cte_join_sources) + .joins(agent: :project) + .with_available_ci_access_fields(project) + .where(projects: { namespace_id: all_namespace_ids }) + .order(Arel.sql('agent_id, array_position(ARRAY(SELECT id FROM ordered_ancestors)::bigint[], agent_group_authorizations.group_id)')) + .select('DISTINCT ON (agent_id) agent_group_authorizations.*') + .preload(agent: :project) + .to_a + end + # rubocop: enable CodeReuse/ActiveRecord + + def all_namespace_ids + project.root_ancestor.self_and_descendants.select(:id) + end + end + end + end + end +end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index a6eddb4d529..8af4204e5e1 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -89,7 +89,8 @@ module SidebarsHelper panel_type: panel_type, update_pins_url: pins_url, is_impersonating: impersonating?, - stop_impersonation_path: admin_impersonation_path + stop_impersonation_path: admin_impersonation_path, + shortcut_links: shortcut_links } end @@ -180,7 +181,8 @@ module SidebarsHelper extraAttrs: { 'data-track-action': 'click_link', 'data-track-label': 'merge_requests_assigned', - 'data-track-property': 'nav_core_menu' + 'data-track-property': 'nav_core_menu', + class: 'dashboard-shortcuts-merge_requests' } }, { @@ -190,7 +192,8 @@ module SidebarsHelper extraAttrs: { 'data-track-action': 'click_link', 'data-track-label': 'merge_requests_to_review', - 'data-track-property': 'nav_core_menu' + 'data-track-property': 'nav_core_menu', + class: 'dashboard-shortcuts-review_requests' } } ] @@ -334,6 +337,26 @@ module SidebarsHelper def impersonating? !!session[:impersonator_id] end + + def shortcut_links + [ + { + title: _('Milestones'), + href: dashboard_milestones_path, + css_class: 'dashboard-shortcuts-milestones' + }, + { + title: _('Snippets'), + href: dashboard_snippets_path, + css_class: 'dashboard-shortcuts-snippets' + }, + { + title: _('Activity'), + href: activity_dashboard_path, + css_class: 'dashboard-shortcuts-activity' + } + ] + end end SidebarsHelper.prepend_mod_with('SidebarsHelper') diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 4b2be446fe3..b98fdba44ec 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -11,9 +11,11 @@ module Ci include ChronicDurationAttribute include Gitlab::Utils::StrongMemoize include IgnorableColumns + include SafelyChangeColumnDefault self.table_name = 'p_ci_builds_metadata' self.primary_key = 'id' + columns_changing_default :partition_id partitionable scope: :build diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 8c7b0193199..f5a2911fc59 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1334,7 +1334,7 @@ module Ci def cluster_agent_authorizations strong_memoize(:cluster_agent_authorizations) do - ::Clusters::AgentAuthorizationsFinder.new(project).execute + ::Clusters::Agents::Authorizations::CiAccess::Finder.new(project).execute end end diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb index 3478bb69707..4e2de06577d 100644 --- a/app/models/clusters/agent.rb +++ b/app/models/clusters/agent.rb @@ -12,11 +12,11 @@ module Clusters has_many :agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent - has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization' - has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group + has_many :ci_access_group_authorizations, class_name: 'Clusters::Agents::Authorizations::CiAccess::GroupAuthorization' + has_many :ci_access_authorized_groups, class_name: '::Group', through: :ci_access_group_authorizations, source: :group - has_many :project_authorizations, class_name: 'Clusters::Agents::ProjectAuthorization' - has_many :authorized_projects, class_name: '::Project', through: :project_authorizations, source: :project + has_many :ci_access_project_authorizations, class_name: 'Clusters::Agents::Authorizations::CiAccess::ProjectAuthorization' + has_many :ci_access_authorized_projects, class_name: '::Project', through: :ci_access_project_authorizations, source: :project has_many :activity_events, -> { in_timeline_order }, class_name: 'Clusters::Agents::ActivityEvent', inverse_of: :agent diff --git a/app/models/clusters/agents/authorizations/ci_access/group_authorization.rb b/app/models/clusters/agents/authorizations/ci_access/group_authorization.rb new file mode 100644 index 00000000000..4261fd6570f --- /dev/null +++ b/app/models/clusters/agents/authorizations/ci_access/group_authorization.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class GroupAuthorization < ApplicationRecord + include ConfigScopes + + self.table_name = 'agent_group_authorizations' + + belongs_to :agent, class_name: 'Clusters::Agent', optional: false + belongs_to :group, class_name: '::Group', optional: false + + validates :config, json_schema: { filename: 'clusters_agents_authorizations_ci_access_config' } + + def config_project + agent.project + end + end + end + end + end +end diff --git a/app/models/clusters/agents/authorizations/ci_access/implicit_authorization.rb b/app/models/clusters/agents/authorizations/ci_access/implicit_authorization.rb new file mode 100644 index 00000000000..b996ae3f92b --- /dev/null +++ b/app/models/clusters/agents/authorizations/ci_access/implicit_authorization.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class ImplicitAuthorization + attr_reader :agent + + delegate :id, to: :agent, prefix: true + + def initialize(agent:) + @agent = agent + end + + def config_project + agent.project + end + + def config + {} + end + end + end + end + end +end diff --git a/app/models/clusters/agents/authorizations/ci_access/project_authorization.rb b/app/models/clusters/agents/authorizations/ci_access/project_authorization.rb new file mode 100644 index 00000000000..7742d109cdb --- /dev/null +++ b/app/models/clusters/agents/authorizations/ci_access/project_authorization.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class ProjectAuthorization < ApplicationRecord + include ConfigScopes + + self.table_name = 'agent_project_authorizations' + + belongs_to :agent, class_name: 'Clusters::Agent', optional: false + belongs_to :project, class_name: '::Project', optional: false + + validates :config, json_schema: { filename: 'clusters_agents_authorizations_ci_access_config' } + + def config_project + agent.project + end + end + end + end + end +end diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb deleted file mode 100644 index 58ba874ab53..00000000000 --- a/app/models/clusters/agents/group_authorization.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - class GroupAuthorization < ApplicationRecord - include ::Clusters::Agents::AuthorizationConfigScopes - - self.table_name = 'agent_group_authorizations' - - belongs_to :agent, class_name: 'Clusters::Agent', optional: false - belongs_to :group, class_name: '::Group', optional: false - - validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' } - - def config_project - agent.project - end - end - end -end diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb deleted file mode 100644 index a365ccdc568..00000000000 --- a/app/models/clusters/agents/implicit_authorization.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - class ImplicitAuthorization - attr_reader :agent - - delegate :id, to: :agent, prefix: true - - def initialize(agent:) - @agent = agent - end - - def config_project - agent.project - end - - def config - {} - end - end - end -end diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb deleted file mode 100644 index b9b44741936..00000000000 --- a/app/models/clusters/agents/project_authorization.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - class ProjectAuthorization < ApplicationRecord - include ::Clusters::Agents::AuthorizationConfigScopes - - self.table_name = 'agent_project_authorizations' - - belongs_to :agent, class_name: 'Clusters::Agent', optional: false - belongs_to :project, class_name: '::Project', optional: false - - validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' } - - def config_project - agent.project - end - end - end -end diff --git a/app/models/concerns/clusters/agents/authorization_config_scopes.rb b/app/models/concerns/clusters/agents/authorization_config_scopes.rb deleted file mode 100644 index 0a0406c3389..00000000000 --- a/app/models/concerns/clusters/agents/authorization_config_scopes.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - module AuthorizationConfigScopes - extend ActiveSupport::Concern - - included do - scope :with_available_ci_access_fields, ->(project) { - where("config->'access_as' IS NULL") - .or(where("config->'access_as' = '{}'")) - .or(where("config->'access_as' ?| array[:fields]", fields: available_ci_access_fields(project))) - } - end - - class_methods do - def available_ci_access_fields(_project) - %w(agent) - end - end - end - end -end - -Clusters::Agents::AuthorizationConfigScopes.prepend_mod diff --git a/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb new file mode 100644 index 00000000000..eef68bfd349 --- /dev/null +++ b/app/models/concerns/clusters/agents/authorizations/ci_access/config_scopes.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + module ConfigScopes + extend ActiveSupport::Concern + + included do + scope :with_available_ci_access_fields, ->(project) { + where("config->'access_as' IS NULL") + .or(where("config->'access_as' = '{}'")) + .or(where("config->'access_as' ?| array[:fields]", fields: available_ci_access_fields(project))) + } + end + + class_methods do + def available_ci_access_fields(_project) + %w(agent) + end + end + end + end + end + end +end + +Clusters::Agents::Authorizations::CiAccess::ConfigScopes.prepend_mod diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb index 1c6aaa9d1ff..56e22a64529 100644 --- a/app/services/ci/generate_kubeconfig_service.rb +++ b/app/services/ci/generate_kubeconfig_service.rb @@ -41,7 +41,7 @@ module Ci attr_reader :pipeline, :token, :environment, :template def agent_authorizations - ::Clusters::Agents::FilterAuthorizationsService.new( + ::Clusters::Agents::Authorizations::CiAccess::FilterService.new( pipeline.cluster_agent_authorizations, environment: environment ).execute diff --git a/app/services/clusters/agents/authorizations/ci_access/filter_service.rb b/app/services/clusters/agents/authorizations/ci_access/filter_service.rb new file mode 100644 index 00000000000..cd08aaa12d4 --- /dev/null +++ b/app/services/clusters/agents/authorizations/ci_access/filter_service.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class FilterService + def initialize(authorizations, filter_params) + @authorizations = authorizations + @filter_params = filter_params + + @environments_matcher = {} + end + + def execute + filter_by_environment(authorizations) + end + + private + + attr_reader :authorizations, :filter_params + + def filter_by_environment(auths) + return auths unless filter_by_environment? + + auths.select do |auth| + next true if auth.config['environments'].blank? + + auth.config['environments'].any? { |environment_pattern| matches_environment?(environment_pattern) } + end + end + + def filter_by_environment? + filter_params.has_key?(:environment) + end + + def environment_filter + @environment_filter ||= filter_params[:environment] + end + + def matches_environment?(environment_pattern) + return false if environment_filter.nil? + + environments_matcher(environment_pattern).match?(environment_filter) + end + + def environments_matcher(environment_pattern) + @environments_matcher[environment_pattern] ||= ::Gitlab::Ci::EnvironmentMatcher.new(environment_pattern) + end + end + end + end + end +end diff --git a/app/services/clusters/agents/authorizations/ci_access/refresh_service.rb b/app/services/clusters/agents/authorizations/ci_access/refresh_service.rb new file mode 100644 index 00000000000..047a0725a2c --- /dev/null +++ b/app/services/clusters/agents/authorizations/ci_access/refresh_service.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Clusters + module Agents + module Authorizations + module CiAccess + class RefreshService + include Gitlab::Utils::StrongMemoize + + AUTHORIZED_ENTITY_LIMIT = 100 + + delegate :project, to: :agent, private: true + delegate :root_ancestor, to: :project, private: true + + def initialize(agent, config:) + @agent = agent + @config = config + end + + def execute + refresh_projects! + refresh_groups! + + true + end + + private + + attr_reader :agent, :config + + def refresh_projects! + if allowed_project_configurations.present? + project_ids = allowed_project_configurations.map { |config| config.fetch(:project_id) } + + agent.with_lock do + agent.ci_access_project_authorizations.upsert_all(allowed_project_configurations, unique_by: [:agent_id, :project_id]) + agent.ci_access_project_authorizations.where.not(project_id: project_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord + end + else + agent.ci_access_project_authorizations.delete_all(:delete_all) + end + end + + def refresh_groups! + if allowed_group_configurations.present? + group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) } + + agent.with_lock do + agent.ci_access_group_authorizations.upsert_all(allowed_group_configurations, unique_by: [:agent_id, :group_id]) + agent.ci_access_group_authorizations.where.not(group_id: group_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord + end + else + agent.ci_access_group_authorizations.delete_all(:delete_all) + end + end + + def allowed_project_configurations + strong_memoize(:allowed_project_configurations) do + project_entries = extract_config_entries(entity: 'projects') + + if project_entries + allowed_projects.where_full_path_in(project_entries.keys).map do |project| + { project_id: project.id, config: project_entries[project.full_path.downcase] } + end + end + end + end + + def allowed_group_configurations + strong_memoize(:allowed_group_configurations) do + group_entries = extract_config_entries(entity: 'groups') + + if group_entries + allowed_groups.where_full_path_in(group_entries.keys).map do |group| + { group_id: group.id, config: group_entries[group.full_path.downcase] } + end + end + end + end + + def extract_config_entries(entity:) + config.dig('ci_access', entity) + &.first(AUTHORIZED_ENTITY_LIMIT) + &.index_by { |config| config.delete('id').downcase } + end + + def allowed_projects + root_ancestor.all_projects + end + + def allowed_groups + if group_root_ancestor? + root_ancestor.self_and_descendants + else + ::Group.none + end + end + + def group_root_ancestor? + root_ancestor.group_namespace? + end + end + end + end + end +end diff --git a/app/services/clusters/agents/authorize_proxy_user_service.rb b/app/services/clusters/agents/authorize_proxy_user_service.rb index ec6645b2db4..ba90d61a7ef 100644 --- a/app/services/clusters/agents/authorize_proxy_user_service.rb +++ b/app/services/clusters/agents/authorize_proxy_user_service.rb @@ -57,7 +57,7 @@ module Clusters def authorized_projects(user_access) strong_memoize_with(:authorized_projects, user_access) do user_access.fetch(:projects, []) - .first(::Clusters::Agents::RefreshAuthorizationService::AUTHORIZED_ENTITY_LIMIT) + .first(::Clusters::Agents::Authorizations::CiAccess::RefreshService::AUTHORIZED_ENTITY_LIMIT) .map { |project| ::Project.find_by_full_path(project[:id]) } .select { |project| current_user.can?(:use_k8s_proxies, project) } end @@ -66,7 +66,7 @@ module Clusters def authorized_groups(user_access) strong_memoize_with(:authorized_groups, user_access) do user_access.fetch(:groups, []) - .first(::Clusters::Agents::RefreshAuthorizationService::AUTHORIZED_ENTITY_LIMIT) + .first(::Clusters::Agents::Authorizations::CiAccess::RefreshService::AUTHORIZED_ENTITY_LIMIT) .map { |group| ::Group.find_by_full_path(group[:id]) } .select { |group| current_user.can?(:use_k8s_proxies, group) } end diff --git a/app/services/clusters/agents/filter_authorizations_service.rb b/app/services/clusters/agents/filter_authorizations_service.rb deleted file mode 100644 index 68517ceec04..00000000000 --- a/app/services/clusters/agents/filter_authorizations_service.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - class FilterAuthorizationsService - def initialize(authorizations, filter_params) - @authorizations = authorizations - @filter_params = filter_params - - @environments_matcher = {} - end - - def execute - filter_by_environment(authorizations) - end - - private - - attr_reader :authorizations, :filter_params - - def filter_by_environment(auths) - return auths unless filter_by_environment? - - auths.select do |auth| - next true if auth.config['environments'].blank? - - auth.config['environments'].any? { |environment_pattern| matches_environment?(environment_pattern) } - end - end - - def filter_by_environment? - filter_params.has_key?(:environment) - end - - def environment_filter - @environment_filter ||= filter_params[:environment] - end - - def matches_environment?(environment_pattern) - return false if environment_filter.nil? - - environments_matcher(environment_pattern).match?(environment_filter) - end - - def environments_matcher(environment_pattern) - @environments_matcher[environment_pattern] ||= ::Gitlab::Ci::EnvironmentMatcher.new(environment_pattern) - end - end - end -end diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb deleted file mode 100644 index 23ececef6a1..00000000000 --- a/app/services/clusters/agents/refresh_authorization_service.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -module Clusters - module Agents - class RefreshAuthorizationService - include Gitlab::Utils::StrongMemoize - - AUTHORIZED_ENTITY_LIMIT = 100 - - delegate :project, to: :agent, private: true - delegate :root_ancestor, to: :project, private: true - - def initialize(agent, config:) - @agent = agent - @config = config - end - - def execute - refresh_projects! - refresh_groups! - - true - end - - private - - attr_reader :agent, :config - - def refresh_projects! - if allowed_project_configurations.present? - project_ids = allowed_project_configurations.map { |config| config.fetch(:project_id) } - - agent.with_lock do - agent.project_authorizations.upsert_all(allowed_project_configurations, unique_by: [:agent_id, :project_id]) - agent.project_authorizations.where.not(project_id: project_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord - end - else - agent.project_authorizations.delete_all(:delete_all) - end - end - - def refresh_groups! - if allowed_group_configurations.present? - group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) } - - agent.with_lock do - agent.group_authorizations.upsert_all(allowed_group_configurations, unique_by: [:agent_id, :group_id]) - agent.group_authorizations.where.not(group_id: group_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord - end - else - agent.group_authorizations.delete_all(:delete_all) - end - end - - def allowed_project_configurations - strong_memoize(:allowed_project_configurations) do - project_entries = extract_config_entries(entity: 'projects') - - if project_entries - allowed_projects.where_full_path_in(project_entries.keys).map do |project| - { project_id: project.id, config: project_entries[project.full_path.downcase] } - end - end - end - end - - def allowed_group_configurations - strong_memoize(:allowed_group_configurations) do - group_entries = extract_config_entries(entity: 'groups') - - if group_entries - allowed_groups.where_full_path_in(group_entries.keys).map do |group| - { group_id: group.id, config: group_entries[group.full_path.downcase] } - end - end - end - end - - def extract_config_entries(entity:) - config.dig('ci_access', entity) - &.first(AUTHORIZED_ENTITY_LIMIT) - &.index_by { |config| config.delete('id').downcase } - end - - def allowed_projects - root_ancestor.all_projects - end - - def allowed_groups - if group_root_ancestor? - root_ancestor.self_and_descendants - else - ::Group.none - end - end - - def group_root_ancestor? - root_ancestor.group_namespace? - end - end - end -end diff --git a/app/validators/json_schemas/cluster_agent_authorization_configuration.json b/app/validators/json_schemas/clusters_agents_authorizations_ci_access_config.json index f3de0b7043b..f3de0b7043b 100644 --- a/app/validators/json_schemas/cluster_agent_authorization_configuration.json +++ b/app/validators/json_schemas/clusters_agents_authorizations_ci_access_config.json diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 3280dcf2cd4..99558f61b25 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,9 +1,5 @@ -- search_bar_classes = 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4' - = render_if_exists 'shared/promotions/promote_advanced_search' -.results.gl-md-display-flex.gl-mt-0 - #js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json } } - .gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden - = render partial: 'search/results_status' if @search_objects.present? - = render partial: 'search/results_list' +.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden + = render partial: 'search/results_status' unless @search_objects.to_a.empty? + = render partial: 'search/results_list' diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 826d78c470d..934f59ea586 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,12 +1,14 @@ - @hide_top_links = true - breadcrumb_title _('Search') - page_title @search_term +- nav 'search' - if params[:group_id].present? = hidden_field_tag :group_id, params[:group_id] - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] - group_attributes = @group&.attributes&.slice('id', 'name')&.merge(full_name: @group&.full_name) - project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace) +- search_bar_classes = 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4' - if @search_results && !(@search_results.respond_to?(:failed?) && @search_results.failed?) - if @search_service_presenter.without_count? @@ -20,5 +22,7 @@ = render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' } #js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @search_service_presenter.advanced_search_enabled?.to_s, "default-branch-name": @project&.default_branch } } -- if @search_term - = render 'search/results' +.results.gl-md-display-flex.gl-mt-0 + #js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json } } + - if @search_term + = render 'search/results' |