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-30 15:23:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-30 15:23:27 +0300
commit3bba41a8c5dfcca0d086eaef10ef36a705dd4f7a (patch)
tree81954681947aaa85592fa7f3c9beed23a7b6bb01 /app
parent1aa447601c6be1e964acbb674887649dab23b804 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue15
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_pods.vue7
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_summary.vue4
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json3
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue9
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue18
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_version_detail.vue44
-rw-r--r--app/assets/javascripts/nav/components/responsive_app.vue95
-rw-r--r--app/assets/javascripts/nav/components/responsive_header.vue37
-rw-r--r--app/assets/javascripts/nav/components/responsive_home.vue63
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue61
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue81
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue107
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue52
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_sections.vue82
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue73
-rw-r--r--app/assets/javascripts/nav/index.js31
-rw-r--r--app/assets/javascripts/nav/mount.js30
-rw-r--r--app/assets/javascripts/nav/stores/index.js5
-rw-r--r--app/assets/javascripts/nav/utils/index.js1
-rw-r--r--app/assets/javascripts/nav/utils/reset_menu_items_active.js14
-rw-r--r--app/assets/javascripts/observability/client.js20
-rw-r--r--app/assets/javascripts/pages/projects/ml/model_versions/show/index.js2
-rw-r--r--app/assets/javascripts/profile/edit/components/profile_edit_app.vue21
-rw-r--r--app/assets/javascripts/profile/profile.js9
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue3
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue15
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js2
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss67
-rw-r--r--app/assets/stylesheets/page_bundles/projects.scss3
-rw-r--r--app/components/projects/ml/show_ml_model_component.rb7
-rw-r--r--app/components/projects/ml/show_ml_model_version_component.rb3
-rw-r--r--app/helpers/stat_anchors_helper.rb12
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/members/project_member.rb11
-rw-r--r--app/presenters/project_presenter.rb53
-rw-r--r--app/services/clusters/agents/authorizations/user_access/refresh_service.rb4
-rw-r--r--app/services/users/migrate_records_to_ghost_user_service.rb7
-rw-r--r--app/views/admin/background_migrations/_migration.html.haml14
-rw-r--r--app/views/notify/request_review_merge_request_email.text.erb2
-rw-r--r--app/views/projects/_files.html.haml12
-rw-r--r--app/views/projects/_home_panel.html.haml42
-rw-r--r--app/views/projects/_invite_members_empty_project.html.haml29
-rw-r--r--app/views/projects/_sidebar.html.haml61
-rw-r--r--app/views/projects/_stat_anchor_list.html.haml5
-rw-r--r--app/views/projects/empty.html.haml192
-rw-r--r--app/views/projects/show.html.haml38
-rw-r--r--app/views/shared/projects/_topics.html.haml5
54 files changed, 583 insertions, 918 deletions
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
index 634fc2ad447..a45387ca676 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
@@ -75,6 +75,6 @@ export default {
/* TODO: Use max-height prop when gitlab-ui got updated.
See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
::v-deep .gl-new-dropdown-inner {
- max-height: 310px;
+ max-height: 310px !important;
}
</style>
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 36cce29d624..d5a7b43c953 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -43,7 +43,7 @@ export default {
return {
isVisible: false,
error: '',
- hasFailedState: false,
+ failedState: {},
podsLoading: false,
workloadTypesLoading: false,
};
@@ -78,6 +78,9 @@ export default {
return this.hasFailedState ? 'error' : 'success';
},
+ hasFailedState() {
+ return Object.values(this.failedState).some((item) => item);
+ },
},
methods: {
toggleCollapse() {
@@ -86,6 +89,12 @@ export default {
onClusterError(message) {
this.error = message;
},
+ onUpdateFailedState(event) {
+ this.failedState = {
+ ...this.failedState,
+ ...event,
+ };
+ },
},
i18n: {
collapse: __('Collapse'),
@@ -126,14 +135,14 @@ export default {
class="gl-mb-5"
@cluster-error="onClusterError"
@loading="podsLoading = $event"
- @failed="hasFailedState = true" />
+ @update-failed-state="onUpdateFailedState" />
<kubernetes-tabs
:configuration="k8sAccessConfiguration"
:namespace="namespace"
class="gl-mb-5"
@cluster-error="onClusterError"
@loading="workloadTypesLoading = $event"
- @failed="hasFailedState = true"
+ @update-failed-state="onUpdateFailedState"
/></template>
</gl-collapse>
</div>
diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue
index 743159d6256..2015355f794 100644
--- a/app/assets/javascripts/environments/components/kubernetes_pods.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue
@@ -82,9 +82,10 @@ export default {
methods: {
countPodsByPhase(phase) {
const filteredPods = this.k8sPods.filter((item) => item.status.phase === phase);
- if (phase === PHASE_FAILED && filteredPods.length) {
- this.$emit('failed');
- }
+
+ const hasFailedState = Boolean(phase === PHASE_FAILED && filteredPods.length);
+ this.$emit('update-failed-state', { pods: hasFailedState });
+
return filteredPods.length;
},
},
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index 8ecb61711ce..20ed67f6bd9 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -153,7 +153,7 @@ export default {
},
},
i18n: {
- healthLabel: s__('Environment|Environment health'),
+ healthLabel: s__('Environment|Environment status'),
syncStatusLabel: s__('Environment|Sync status'),
},
badgeContainerClasses: 'gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-mr-3 gl-mb-2',
diff --git a/app/assets/javascripts/environments/components/kubernetes_summary.vue b/app/assets/javascripts/environments/components/kubernetes_summary.vue
index 1f4e91afe35..2912fd8f4d8 100644
--- a/app/assets/javascripts/environments/components/kubernetes_summary.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_summary.vue
@@ -140,9 +140,7 @@ export default {
return workloadType.items?.failed?.length > 0;
});
- if (failed) {
- this.$emit('failed');
- }
+ this.$emit('update-failed-state', { summary: failed });
},
},
i18n: {
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 60b36596ef3..0d80b1fd797 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -140,7 +140,7 @@ export default {
:namespace="namespace"
:configuration="configuration"
@loading="$emit('loading', $event)"
- @failed="$emit('failed')"
+ @update-failed-state="$emit('update-failed-state', $event)"
@cluster-error="$emit('cluster-error', $event)"
/>
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4ef0d067030..dbdadf371ca 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -4,7 +4,8 @@
"AlertManagementPrometheusIntegration"
],
"AmazonS3ConfigurationInterface": [
- "AmazonS3ConfigurationType"
+ "AmazonS3ConfigurationType",
+ "InstanceAmazonS3ConfigurationType"
],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index a3cd7a627ee..e9a4b936b9c 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -30,7 +30,6 @@ import { initUserTracking, initDefaultTrackers } from './tracking';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
-import { initTopNav } from './nav';
import { initCopyCodeButton } from './behaviors/copy_code';
import initGitlabVersionCheck from './gitlab_version_check';
@@ -82,9 +81,6 @@ initRails();
function deferredInitialisation() {
const $body = $('body');
- if (!gon.use_new_navigation) {
- initTopNav();
- }
initBreadcrumbs();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
index 2c60e05dc57..a3182d9e622 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -3,6 +3,7 @@ import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
+import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import * as i18n from '../translations';
export default {
@@ -14,6 +15,7 @@ export default {
GlTab,
GlBadge,
MetadataItem,
+ ModelVersionDetail,
},
props: {
model: {
@@ -28,6 +30,9 @@ export default {
candidateCount() {
return this.model.candidateCount || 0;
},
+ latestVersionTitle() {
+ return `${i18n.LATEST_VERSION_LABEL}: ${this.model.latestVersion.version}`;
+ },
},
i18n,
};
@@ -50,9 +55,9 @@ export default {
<gl-tabs class="gl-mt-4">
<gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
- <h3 class="gl-font-lg">{{ $options.i18n.LATEST_VERSION_LABEL }}</h3>
<template v-if="model.latestVersion">
- {{ model.latestVersion.version }}
+ <h3 class="gl-font-lg">{{ latestVersionTitle }}</h3>
+ <model-version-detail :model-version="model.latestVersion" />
</template>
<div v-else class="gl-text-secondary">{{ $options.i18n.NO_VERSIONS_LABEL }}</div>
</gl-tab>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
index a9440aff1ce..6608f44ecf7 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
@@ -1,16 +1,30 @@
<script>
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import ModelVersionDetail from '../components/model_version_detail.vue';
+
export default {
name: 'ShowMlModelVersionApp',
- components: {},
+ components: {
+ ModelVersionDetail,
+ TitleArea,
+ },
props: {
modelVersion: {
type: Object,
required: true,
},
},
+ computed: {
+ title() {
+ return `${this.modelVersion.model.name} / ${this.modelVersion.version}`;
+ },
+ },
};
</script>
<template>
- <div>{{ modelVersion.model.name }} - {{ modelVersion.version }}</div>
+ <div>
+ <title-area :title="title" />
+ <model-version-detail :model-version="modelVersion" />
+ </div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue
new file mode 100644
index 00000000000..19d91df43b2
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue
@@ -0,0 +1,44 @@
+<script>
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
+
+export default {
+ name: 'ModelVersionDetail',
+ components: {
+ PackageFiles: () =>
+ import('~/packages_and_registries/package_registry/components/details/package_files.vue'),
+ },
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ packageId() {
+ return convertToGraphQLId(TYPENAME_PACKAGES_PACKAGE, this.modelVersion.packageId);
+ },
+ projectPath() {
+ return this.modelVersion.projectPath;
+ },
+ packageType() {
+ return 'ml_model';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <p>
+ {{ modelVersion.description }}
+ </p>
+ <template v-if="modelVersion.packageId">
+ <package-files
+ :package-id="packageId"
+ :project-path="projectPath"
+ :package-type="packageType"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/nav/components/responsive_app.vue b/app/assets/javascripts/nav/components/responsive_app.vue
deleted file mode 100644
index 68a39f862fc..00000000000
--- a/app/assets/javascripts/nav/components/responsive_app.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
-import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
-import { BV_DROPDOWN_SHOW, BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import { resetMenuItemsActive } from '../utils';
-import ResponsiveHeader from './responsive_header.vue';
-import ResponsiveHome from './responsive_home.vue';
-import TopNavContainerView from './top_nav_container_view.vue';
-
-export default {
- components: {
- KeepAliveSlots,
- ResponsiveHeader,
- ResponsiveHome,
- TopNavContainerView,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- activeView: 'home',
- hasMobileOverlay: false,
- };
- },
- computed: {
- nav() {
- return resetMenuItemsActive(this.navData);
- },
- },
- created() {
- this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay);
- this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
- },
- beforeDestroy() {
- this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay);
- this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
- },
- methods: {
- onMenuItemClick({ view }) {
- if (view) {
- this.activeView = view;
- }
- },
- showMobileOverlay() {
- this.hasMobileOverlay = true;
- },
- hideMobileOverlay() {
- this.hasMobileOverlay = false;
- },
- },
- FREQUENT_ITEMS_PROJECTS,
- FREQUENT_ITEMS_GROUPS,
-};
-</script>
-
-<template>
- <div>
- <div
- class="mobile-overlay"
- :class="{ 'mobile-nav-open': hasMobileOverlay }"
- data-testid="mobile-overlay"
- ></div>
- <keep-alive-slots :slot-key="activeView">
- <template #home>
- <responsive-home :nav-data="nav" @menu-item-click="onMenuItemClick" />
- </template>
- <template #projects>
- <responsive-header @menu-item-click="onMenuItemClick">
- {{ __('Projects') }}
- </responsive-header>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
- container-class="gl-px-3"
- v-bind="nav.views.projects"
- />
- </template>
- <template #groups>
- <responsive-header @menu-item-click="onMenuItemClick">
- {{ __('Groups') }}
- </responsive-header>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
- container-class="gl-px-3"
- v-bind="nav.views.groups"
- />
- </template>
- </keep-alive-slots>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/responsive_header.vue b/app/assets/javascripts/nav/components/responsive_header.vue
deleted file mode 100644
index e29b4a67383..00000000000
--- a/app/assets/javascripts/nav/components/responsive_header.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import TopNavMenuItem from './top_nav_menu_item.vue';
-
-export default {
- components: {
- TopNavMenuItem,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- computed: {
- menuItem() {
- return {
- id: 'home',
- view: 'home',
- icon: 'chevron-lg-left',
- };
- },
- },
-};
-</script>
-
-<template>
- <header class="gl-py-4 gl-display-flex gl-align-items-center">
- <top-nav-menu-item
- v-gl-tooltip="{ title: s__('TopNav|Go back') }"
- class="gl-p-3!"
- :menu-item="menuItem"
- icon-only
- @click="$emit('menu-item-click', menuItem)"
- />
- <span class="gl-font-size-h2 gl-font-weight-bold gl-ml-2">
- <slot></slot>
- </span>
- </header>
-</template>
diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue
deleted file mode 100644
index 371b252a6ba..00000000000
--- a/app/assets/javascripts/nav/components/responsive_home.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import TopNavMenuItem from './top_nav_menu_item.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-import TopNavNewDropdown from './top_nav_new_dropdown.vue';
-
-const NEW_VIEW = 'new';
-const SEARCH_VIEW = 'search';
-
-export default {
- components: {
- TopNavMenuItem,
- TopNavMenuSections,
- TopNavNewDropdown,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- computed: {
- menuSections() {
- return [
- { id: 'primary', menuItems: this.navData.primary },
- { id: 'secondary', menuItems: this.navData.secondary },
- ].filter((x) => x.menuItems?.length);
- },
- newDropdownViewModel() {
- return this.navData.views[NEW_VIEW];
- },
- searchMenuItem() {
- return this.navData.views[SEARCH_VIEW];
- },
- },
-};
-</script>
-
-<template>
- <div>
- <header class="gl-display-flex gl-align-items-center gl-py-4 gl-pl-4">
- <h1 class="gl-m-0 gl-font-size-h2 gl-reset-color gl-mr-auto">{{ __('Menu') }}</h1>
- <top-nav-menu-item
- v-if="searchMenuItem"
- v-gl-tooltip="{ title: searchMenuItem.title }"
- class="gl-ml-3"
- :menu-item="searchMenuItem"
- icon-only
- />
- <top-nav-new-dropdown
- v-if="newDropdownViewModel"
- v-gl-tooltip="{ title: newDropdownViewModel.title }"
- :view-model="newDropdownViewModel"
- class="gl-ml-3"
- data-testid="mobile_new_dropdown"
- />
- </header>
- <top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
deleted file mode 100644
index 22c77e9ae32..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
-import Tracking from '~/tracking';
-import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
-
-export default {
- components: {
- GlIcon,
- GlNav,
- GlNavItemDropdown,
- GlDropdownForm,
- TopNavDropdownMenu,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- methods: {
- trackToggleEvent() {
- Tracking.event(undefined, 'click_nav', {
- label: 'hamburger_menu',
- property: 'navigation_top',
- });
- },
- },
-};
-</script>
-
-<template>
- <gl-nav class="navbar-sub-nav">
- <gl-nav-item-dropdown
- v-gl-tooltip.bottom="navData.menuTooltip"
- data-testid="navbar_dropdown"
- data-qa-title="Menu"
- menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto! js-top-nav-dropdown-menu"
- toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
- no-flip
- no-caret
- @toggle="trackToggleEvent"
- >
- <template #button-content>
- <gl-icon name="hamburger" />
- <span v-if="navData.menuTitle" class="gl-ml-3">
- {{ navData.menuTitle }}
- </span>
- </template>
- <gl-dropdown-form>
- <top-nav-dropdown-menu
- :primary="navData.primary"
- :secondary="navData.secondary"
- :views="navData.views"
- />
- </gl-dropdown-form>
- </gl-nav-item-dropdown>
- </gl-nav>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
deleted file mode 100644
index 36e4a278da9..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_container_view.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import FrequentItemsApp from '~/frequent_items/components/app.vue';
-import eventHub from '~/frequent_items/event_hub';
-import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-
-export default {
- components: {
- FrequentItemsApp,
- TopNavMenuSections,
- VuexModuleProvider,
- },
- inheritAttrs: false,
- props: {
- frequentItemsVuexModule: {
- type: String,
- required: true,
- },
- frequentItemsDropdownType: {
- type: String,
- required: true,
- },
- currentItem: {
- type: Object,
- required: true,
- },
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- linksPrimary: {
- type: Array,
- required: false,
- default: () => [],
- },
- linksSecondary: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- menuSections() {
- return [
- { id: 'primary', menuItems: this.linksPrimary },
- { id: 'secondary', menuItems: this.linksSecondary },
- ].filter((x) => x.menuItems?.length);
- },
- currentItemTimestamped() {
- return {
- ...this.currentItem,
- lastAccessedOn: Date.now(),
- };
- },
- },
- mounted() {
- // For historic reasons, the frequent-items-app component requires this too start up.
- this.$nextTick(() => {
- eventHub.$emit(`${this.frequentItemsDropdownType}-dropdownOpen`);
- });
- },
-};
-</script>
-
-<template>
- <div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
- <div
- class="frequent-items-dropdown-container gl-w-auto"
- :class="containerClass"
- data-testid="frequent-items-container"
- >
- <div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
- <vuex-module-provider :vuex-module="frequentItemsVuexModule">
- <frequent-items-app :current-item="currentItemTimestamped" v-bind="$attrs" />
- </vuex-module-provider>
- </div>
- </div>
- <top-nav-menu-sections class="gl-mt-auto" :sections="menuSections" with-top-border />
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
deleted file mode 100644
index fa202a0574d..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<script>
-import { cloneDeep } from 'lodash';
-import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import TopNavContainerView from './top_nav_container_view.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-
-export default {
- components: {
- KeepAliveSlots,
- TopNavContainerView,
- TopNavMenuSections,
- },
- props: {
- primary: {
- type: Array,
- required: false,
- default: () => [],
- },
- secondary: {
- type: Array,
- required: false,
- default: () => [],
- },
- views: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- },
- data() {
- // It's expected that primary & secondary never change, so these are treated as "init" props.
- // We need to clone so that we can mutate the data without mutating the props
- const menuSections = [
- { id: 'primary', menuItems: cloneDeep(this.primary) },
- { id: 'secondary', menuItems: cloneDeep(this.secondary) },
- ].filter((x) => x.menuItems?.length);
-
- return {
- menuSections,
- };
- },
- computed: {
- allMenuItems() {
- return this.menuSections.flatMap((x) => x.menuItems);
- },
- activeView() {
- const active = this.allMenuItems.find((x) => x.active);
-
- return active?.view;
- },
- menuClass() {
- if (!this.activeView) {
- return 'gl-w-full';
- }
-
- return '';
- },
- },
- methods: {
- onMenuItemClick({ id }) {
- this.allMenuItems.forEach((menuItem) => {
- this.$set(menuItem, 'active', id === menuItem.id);
- });
- },
- },
- FREQUENT_ITEMS_PROJECTS,
- FREQUENT_ITEMS_GROUPS,
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-align-items-stretch">
- <div
- class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-p-3"
- :class="menuClass"
- data-testid="menu-sidebar"
- >
- <top-nav-menu-sections
- :sections="menuSections"
- :is-primary-section="true"
- @menu-item-click="onMenuItemClick"
- />
- </div>
- <keep-alive-slots
- v-show="activeView"
- :slot-key="activeView"
- class="gl-w-grid-size-40 gl-overflow-hidden gl-p-3"
- data-testid="menu-subview"
- >
- <template #projects>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
- v-bind="views.projects"
- />
- </template>
- <template #groups>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
- v-bind="views.groups"
- />
- </template>
- </keep-alive-slots>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
deleted file mode 100644
index bf1fd691ca8..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_menu_item.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { kebabCase, mapKeys } from 'lodash';
-
-const getDataKey = (key) => `data-${kebabCase(key)}`;
-
-const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
-
-export default {
- components: {
- GlButton,
- GlIcon,
- },
- props: {
- menuItem: {
- type: Object,
- required: true,
- },
- iconOnly: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- dataAttrs() {
- return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key));
- },
- },
- ACTIVE_CLASS,
-};
-</script>
-
-<template>
- <gl-button
- category="tertiary"
- :href="menuItem.href"
- class="top-nav-menu-item gl-display-block gl-pr-3!"
- :class="[menuItem.css_class, { [$options.ACTIVE_CLASS]: menuItem.active }]"
- :aria-label="menuItem.title"
- v-bind="dataAttrs"
- v-on="$listeners"
- >
- <span class="gl-display-flex">
- <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-3!': !iconOnly }" />
- <template v-if="!iconOnly">
- {{ menuItem.title }}
- <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
- </template>
- </span>
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
deleted file mode 100644
index 1f3f11dc624..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import TopNavMenuItem from './top_nav_menu_item.vue';
-
-const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid';
-
-export default {
- components: {
- TopNavMenuItem,
- },
- props: {
- sections: {
- type: Array,
- required: true,
- },
- withTopBorder: {
- type: Boolean,
- required: false,
- default: false,
- },
- isPrimarySection: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- onClick(menuItem) {
- // If we're a link, let's just do the default behavior so the view won't change
- if (menuItem.href) {
- return;
- }
-
- this.$emit('menu-item-click', menuItem);
- },
- getMenuSectionClasses(index) {
- // This is a method instead of a computed so we don't have to incur the cost of
- // creating a whole new array/object.
- const hasBorder = this.withTopBorder || index > 0;
- return {
- [BORDER_CLASSES]: hasBorder,
- 'gl-border-gray-100': hasBorder && this.isPrimarySection,
- 'gl-border-gray-50': hasBorder && !this.isPrimarySection,
- 'gl-mt-3': index > 0,
- };
- },
- },
- // Expose for unit tests
- BORDER_CLASSES,
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-align-items-stretch gl-flex-direction-column">
- <div
- v-for="({ id, menuItems }, sectionIndex) in sections"
- :key="id"
- :class="getMenuSectionClasses(sectionIndex)"
- data-testid="menu-section"
- >
- <template v-for="(menuItem, menuItemIndex) in menuItems">
- <strong
- v-if="menuItem.type == 'header'"
- :key="menuItem.title"
- class="gl-px-4 gl-py-2 gl-text-gray-900 gl-display-block"
- :class="{ 'gl-pt-3!': menuItemIndex > 0 }"
- data-testid="menu-header"
- >
- {{ menuItem.title }}
- </strong>
- <top-nav-menu-item
- v-else
- :key="menuItem.id"
- :menu-item="menuItem"
- data-testid="menu-item"
- class="gl-w-full"
- :class="{ 'gl-mt-1': menuItemIndex > 0 }"
- @click="onClick(menuItem)"
- />
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
deleted file mode 100644
index 2dfd77bc02e..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- InviteMembersTrigger,
- },
- props: {
- viewModel: {
- type: Object,
- required: true,
- },
- },
- computed: {
- sections() {
- return this.viewModel.menu_sections || [];
- },
- showHeaders() {
- return this.sections.length > 1;
- },
- },
- methods: {
- isInvitedMembers(menuItem) {
- return menuItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT;
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- toggle-class="top-nav-menu-item"
- icon="plus"
- :text="viewModel.title"
- category="tertiary"
- text-sr-only
- no-caret
- right
- >
- <template v-for="({ title, menu_items }, index) in sections">
- <gl-dropdown-divider v-if="index > 0" :key="`${index}_divider`" data-testid="divider" />
- <gl-dropdown-section-header v-if="showHeaders" :key="`${index}_header`" data-testid="header">
- {{ title }}
- </gl-dropdown-section-header>
- <template v-for="menuItem in menu_items">
- <invite-members-trigger
- v-if="isInvitedMembers(menuItem)"
- :key="`${index}_item_${menuItem.id}`"
- :trigger-element="`dropdown-${menuItem.data.trigger_element}`"
- :display-text="menuItem.title"
- :icon="menuItem.icon"
- :trigger-source="menuItem.data.trigger_source"
- />
- <gl-dropdown-item
- v-else
- :key="`${index}_item_${menuItem.id}`"
- link-class="top-nav-menu-item"
- :href="menuItem.href"
- data-testid="item"
- :data-qa-selector="`${menuItem.title.toLowerCase().replace(' ', '_')}_mobile_button`"
- >
- {{ menuItem.title }}
- </gl-dropdown-item>
- </template>
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js
deleted file mode 100644
index abd537d2c9a..00000000000
--- a/app/assets/javascripts/nav/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// TODO: With the combined_menu feature flag removed, there's likely a better
-// way to slice up the async import (i.e., include trigger in main bundle, but
-// async import subviews. Don't do this at the cost of UX).
-// See https://gitlab.com/gitlab-org/gitlab/-/issues/336042
-const importModule = () => import(/* webpackChunkName: 'top_nav' */ './mount');
-
-const tryMountTopNav = async () => {
- const el = document.getElementById('js-top-nav');
-
- if (!el) {
- return;
- }
-
- const { mountTopNav } = await importModule();
-
- mountTopNav(el);
-};
-
-const tryMountTopNavResponsive = async () => {
- const el = document.getElementById('js-top-nav-responsive');
-
- if (!el) {
- return;
- }
-
- const { mountTopNavResponsive } = await importModule();
-
- mountTopNavResponsive(el);
-};
-
-export const initTopNav = async () => Promise.all([tryMountTopNav(), tryMountTopNavResponsive()]);
diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js
deleted file mode 100644
index 0fc946bea76..00000000000
--- a/app/assets/javascripts/nav/mount.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import ResponsiveApp from './components/responsive_app.vue';
-import App from './components/top_nav_app.vue';
-import { createStore } from './stores';
-
-Vue.use(Vuex);
-
-const mount = (el, Component) => {
- const viewModel = JSON.parse(el.dataset.viewModel);
- const store = createStore();
-
- return new Vue({
- el,
- name: 'TopNavRoot',
- store,
- render(h) {
- return h(Component, {
- props: {
- navData: viewModel,
- },
- });
- },
- });
-};
-
-export const mountTopNav = (el) => mount(el, App);
-
-export const mountTopNavResponsive = (el) => mount(el, ResponsiveApp);
diff --git a/app/assets/javascripts/nav/stores/index.js b/app/assets/javascripts/nav/stores/index.js
deleted file mode 100644
index 7c8f93f042c..00000000000
--- a/app/assets/javascripts/nav/stores/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import { createStoreOptions } from '~/frequent_items/store';
-
-export const createStore = () => new Vuex.Store(createStoreOptions());
diff --git a/app/assets/javascripts/nav/utils/index.js b/app/assets/javascripts/nav/utils/index.js
deleted file mode 100644
index 6d93818f0d3..00000000000
--- a/app/assets/javascripts/nav/utils/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './reset_menu_items_active';
diff --git a/app/assets/javascripts/nav/utils/reset_menu_items_active.js b/app/assets/javascripts/nav/utils/reset_menu_items_active.js
deleted file mode 100644
index 9b5d8e97c9c..00000000000
--- a/app/assets/javascripts/nav/utils/reset_menu_items_active.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const resetActiveInArray = (arr) => arr?.map((menuItem) => ({ ...menuItem, active: false }));
-
-/**
- * This method sets `active: false` for the menu items within the given nav data.
- *
- * @returns navData with the menu items updated with `active: false`
- */
-export const resetMenuItemsActive = ({ primary, secondary, ...navData }) => {
- return {
- ...navData,
- primary: resetActiveInArray(primary),
- secondary: resetActiveInArray(secondary),
- };
-};
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index be13e69f225..9c3f9dda4d0 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -251,10 +251,26 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-async function fetchMetrics(metricsUrl) {
+async function fetchMetrics(metricsUrl, { filters = {}, limit } = {}) {
try {
+ const params = new URLSearchParams();
+
+ if (Array.isArray(filters.search)) {
+ const searchPrefix = filters.search
+ .map((f) => f.value)
+ .join(' ')
+ .trim();
+
+ if (searchPrefix) {
+ params.append('starts_with', searchPrefix);
+ if (limit) {
+ params.append('limit', limit);
+ }
+ }
+ }
const { data } = await axios.get(metricsUrl, {
withCredentials: true,
+ params,
});
if (!Array.isArray(data.metrics)) {
throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
@@ -299,6 +315,6 @@ export function buildClient(config) {
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
- fetchMetrics: () => fetchMetrics(metricsUrl),
+ fetchMetrics: (options) => fetchMetrics(metricsUrl, options),
};
}
diff --git a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
index 1a2b85d7e16..7202dcccd31 100644
--- a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModelVersion } from '~/ml/model_registry/apps';
-initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion);
+initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion, { withApolloProvider: true });
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 815b8742500..eedb5d7764e 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -3,7 +3,6 @@ import { nextTick } from 'vue';
import { GlForm, GlButton } from '@gitlab/ui';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { readFileAsDataURL } from '~/lib/utils/file_utility';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils';
@@ -106,20 +105,12 @@ export default {
this.updateProfileSettings = false;
}
},
- async syncHeaderAvatars() {
- const dataURL = await readFileAsDataURL(this.avatarBlob);
-
- const elements = gon?.use_new_navigation
- ? ['[data-testid="user-dropdown"] .gl-avatar']
- : ['.header-user-avatar', '.js-sidebar-user-avatar'];
-
- elements.forEach((selector) => {
- const node = document.querySelector(selector);
- if (!node) return;
-
- node.setAttribute('src', dataURL);
- node.setAttribute('srcset', dataURL);
- });
+ syncHeaderAvatars() {
+ document.dispatchEvent(
+ new CustomEvent('userAvatar:update', {
+ detail: { url: URL.createObjectURL(this.avatarBlob) },
+ }),
+ );
},
onBlobChange(blob) {
this.avatarBlob = blob;
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 4d3824f910c..16f0110a1af 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -89,12 +89,9 @@ export default class Profile {
}
updateHeaderAvatar() {
- if (gon?.use_new_navigation) {
- $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL);
- } else {
- $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- }
+ const url = URL.createObjectURL(this.avatarGlCrop.getBlob());
+
+ document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } }));
}
setRepoRadio() {
diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
index e1c8c66a214..d19ec4bcab6 100644
--- a/app/assets/javascripts/projects/details/upload_button.vue
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -36,7 +36,10 @@ export default {
<span>
<gl-button
v-gl-modal="$options.uploadBlobModalId"
+ variant="link"
icon="upload"
+ class="stat-link gl-px-0!"
+ button-text-classes="gl-ml-2"
data-testid="upload-file-button"
>{{ __('Upload File') }}</gl-button
>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 079d4c522a8..8d4c4384e1d 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -269,6 +269,7 @@ export default {
:invalid-feedback="form.fields['commit_message'].feedback"
>
<gl-form-textarea
+ id="commit_message"
ref="message"
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
@@ -289,6 +290,7 @@ export default {
:invalid-feedback="form.fields['branch_name'].feedback"
>
<gl-form-input
+ id="branch_name"
v-model="form.fields['branch_name'].value"
v-validation:[form.showValidation]
:state="form.fields['branch_name'].state"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index db769df873f..5dab74374df 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -59,9 +59,13 @@ export default {
data() {
return {
setStatusModalReady: false,
+ updatedAvatarUrl: null,
};
},
computed: {
+ avatarUrl() {
+ return this.updatedAvatarUrl || this.data.avatar_url;
+ },
toggleText() {
return sprintf(__('%{user} user’s menu'), { user: this.data.name });
},
@@ -190,7 +194,16 @@ export default {
};
},
},
+ mounted() {
+ document.addEventListener('userAvatar:update', this.updateAvatar);
+ },
+ unmounted() {
+ document.removeEventListener('userAvatar:update', this.updateAvatar);
+ },
methods: {
+ updateAvatar(event) {
+ this.updatedAvatarUrl = event.detail?.url;
+ },
onShow() {
this.initBuyCIMinsCallout();
},
@@ -240,7 +253,7 @@ export default {
<gl-avatar
:size="24"
:entity-name="data.name"
- :src="data.avatar_url"
+ :src="avatarUrl"
aria-hidden="true"
data-testid="user-avatar-content"
/>
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index dbea306eced..2f24eb84f92 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -27,6 +27,22 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
});
/**
+ * Returns the most frequently visited items.
+ *
+ * @param {Array} items - A list of items retrieved from the local storage
+ * @param {Number} maxCount - The maximum number of items to be returned
+ * @returns {Array}
+ */
+export const getTopFrequentItems = (items, maxCount) => {
+ if (!Array.isArray(items)) return [];
+
+ const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
+ sortItemsByFrequencyAndLastAccess(frequentItems);
+
+ return frequentItems.slice(0, maxCount);
+};
+
+/**
* This tracks projects' and groups' visits in order to suggest a list of frequently visited
* entities to the user. The suggestion logic is implemented server-side and computed items can be
* retrieved through the GraphQL API.
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index 431348e1d57..a8d2736531a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -1,6 +1,6 @@
export const COMPONENTS = {
conflict: () => import('./conflicts.vue'),
- unresolved_discussions: () => import('./unresolved_discussions.vue'),
+ discussions_not_resolved: () => import('./unresolved_discussions.vue'),
need_rebase: () => import('./rebase.vue'),
default: () => import('./message.vue'),
};
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index 504f1405148..b3477b5d4c5 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -134,7 +134,7 @@
.stat-text,
.stat-link {
- padding: $gl-btn-vert-padding 0;
+ padding: $gl-btn-vert-padding;
background-color: transparent;
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
@@ -149,7 +149,6 @@
&:hover,
&:focus {
text-decoration: underline;
- border-bottom: 0;
}
.project-stat-value {
@@ -159,13 +158,6 @@
.icon {
color: var(--gray-500, $gl-text-color-secondary);
}
-
- .add-license-link {
- &,
- .icon {
- color: var(--blue-600, $blue-600);
- }
- }
}
.btn {
@@ -186,3 +178,60 @@
color: var(--gl-text-color, $gl-text-color);
}
}
+
+// FF :project_overview_reorg enabled
+.project-page-indicator:not(.hidden) + .project-page-layout {
+ --project-overview-sidebar-width: 290px;
+
+ @include media-breakpoint-up(lg) {
+ display: grid;
+ grid-template-columns: auto var(--project-overview-sidebar-width);
+ gap: 2rem;
+
+ .project-page-layout-content,
+ .project-page-layout-sidebar {
+ min-width: 1px;
+ }
+
+ .project-page-layout-sidebar {
+ order: 2;
+ overflow-x: clip;
+ margin-right: -$gl-padding-8;
+ }
+
+ .project-page-sidebar {
+ position: sticky;
+ top: calc(#{$calc-application-header-height} + #{$gl-spacing-scale-4});
+ width: calc(100% + 100px);
+ height: calc(
+ #{$calc-application-viewport-height} - #{$gl-spacing-scale-4}
+ );
+ padding-inline: $gl-padding-4;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+
+ .project-page-sidebar-block {
+ width: calc(var(--project-overview-sidebar-width) - 1px);
+
+ &:first-of-type {
+ padding-top: $gl-spacing-scale-1;
+ }
+ }
+
+ .nav {
+ > li {
+ width: 100%;
+ }
+
+ .btn {
+ justify-content: flex-start;
+
+ &:not(.btn-dashed) {
+ box-shadow: none;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index d252afd0b29..1afc456a16a 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -235,8 +235,7 @@
}
.repository-languages-bar {
- height: 8px;
- margin-bottom: $gl-padding;
+ height: 0.5rem;
background-color: var(--white, $white);
border-radius: $border-radius-default;
diff --git a/app/components/projects/ml/show_ml_model_component.rb b/app/components/projects/ml/show_ml_model_component.rb
index d349c0a22e9..38a81a5837d 100644
--- a/app/components/projects/ml/show_ml_model_component.rb
+++ b/app/components/projects/ml/show_ml_model_component.rb
@@ -29,8 +29,13 @@ module Projects
def latest_version_view_model
return unless model.latest_version
+ model_version = model.latest_version
+
{
- version: model.latest_version.version
+ version: model_version.version,
+ description: model_version.description,
+ project_path: project_path(model_version.project),
+ package_id: model_version.package_id
}
end
end
diff --git a/app/components/projects/ml/show_ml_model_version_component.rb b/app/components/projects/ml/show_ml_model_version_component.rb
index ae81642a891..0e39a1cbcc6 100644
--- a/app/components/projects/ml/show_ml_model_version_component.rb
+++ b/app/components/projects/ml/show_ml_model_version_component.rb
@@ -18,6 +18,9 @@ module Projects
id: model_version.id,
version: model_version.version,
path: model_version.path,
+ description: model_version.description,
+ project_path: project_path(model_version.project),
+ package_id: model_version.package_id,
model: {
name: model.name,
path: model.path
diff --git a/app/helpers/stat_anchors_helper.rb b/app/helpers/stat_anchors_helper.rb
index 957985d6953..31f5d73d020 100644
--- a/app/helpers/stat_anchors_helper.rb
+++ b/app/helpers/stat_anchors_helper.rb
@@ -11,12 +11,22 @@ module StatAnchorsHelper
private
+ def new_button_attribute(anchor)
+ anchor.class_modifier || 'btn-link gl-text-blue-500!'
+ end
+
def button_attribute(anchor)
anchor.class_modifier || 'btn-dashed'
end
def extra_classes(anchor)
- if anchor.is_link
+ if Feature.enabled?(:project_overview_reorg)
+ if anchor.is_link
+ 'stat-link gl-px-0! gl-pb-2!'
+ else
+ "stat-link gl-px-0! gl-pb-2! #{new_button_attribute(anchor)}"
+ end
+ elsif anchor.is_link
'stat-link'
else
"gl-button btn #{button_attribute(anchor)}"
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 21980a5e1b7..f1aeb7e528f 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -86,7 +86,7 @@ class CommitStatus < Ci::ApplicationRecord
scope :for_project_paths, -> (paths) do
# Pluck is used to split this query. Splitting the query is required for database decomposition for `ci_*` tables.
# https://docs.gitlab.com/ee/development/database/transaction_guidelines.html#database-decomposition-and-sharding
- project_ids = Project.where_full_path_in(Array(paths)).pluck(:id)
+ project_ids = Project.where_full_path_in(Array(paths), use_includes: false).pluck(:id)
for_project(project_ids)
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 7ae6e138aca..f52fef9e247 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -51,8 +51,15 @@ class ProjectMember < Member
end
def permissible_access_level_roles_for_project_access_token(current_user, project)
- permissible_access_level_roles(current_user, project).filter do |_, value|
- value <= project.project_authorizations.find_by(user: current_user).access_level
+ if Ability.allowed?(current_user, :manage_owners, project)
+ Gitlab::Access.options_with_owner
+ else
+ max_access_level = project.team.max_member_access(current_user.id)
+ return {} unless max_access_level.present?
+
+ ProjectMember.access_level_roles.filter do |_, value|
+ value <= max_access_level
+ end
end
end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index c983d8623d2..a7c8f9a469d 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -21,8 +21,16 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data)
MAX_TOPICS_TO_SHOW = 3
- def statistic_icon(icon_name = 'plus-square-o')
- sprite_icon(icon_name, css_class: 'icon gl-mr-2 gl-text-gray-500')
+ def statistic_default_class_list
+ Feature.enabled?(:project_overview_reorg) ? 'icon gl-mr-3 gl-text-gray-500' : 'icon gl-mr-2 gl-text-gray-500'
+ end
+
+ def statistic_default_icon
+ Feature.enabled?(:project_overview_reorg) ? 'plus' : 'plus-square-o'
+ end
+
+ def statistic_icon(icon_name = statistic_default_icon, class_list = statistic_default_class_list)
+ sprite_icon(icon_name, css_class: class_list)
end
def statistics_anchors(show_auto_devops_callout:)
@@ -288,13 +296,19 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if can_current_user_push_to_default_branch?
new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_main) : project_new_blob_path(project, default_branch_or_main)
- AnchorData.new(false, statistic_icon + _('New file'), new_file_path, 'btn-dashed')
+ if Feature.enabled?(:project_overview_reorg)
+ AnchorData.new(false, statistic_icon('plus', 'gl-text-blue-500! gl-mr-3') + _('New file'), new_file_path)
+ else
+ AnchorData.new(false, statistic_icon + _('New file'), new_file_path, 'btn-dashed')
+ end
end
end
def readme_anchor_data
if can_current_user_push_to_default_branch? && readme_path.nil?
- AnchorData.new(false, statistic_icon + _('Add README'), empty_repo? ? add_readme_ide_path : add_readme_path)
+ icon = Feature.enabled?(:project_overview_reorg) ? statistic_icon('plus', 'gl-text-blue-500! gl-mr-3') : statistic_icon
+ label = icon + _('Add README')
+ AnchorData.new(false, label, empty_repo? ? add_readme_ide_path : add_readme_path)
elsif readme_path
AnchorData.new(
false,
@@ -308,9 +322,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def changelog_anchor_data
if can_current_user_push_to_default_branch? && repository.changelog.blank?
+ icon = Feature.enabled?(:project_overview_reorg) ? statistic_icon('plus', 'gl-mr-3') : statistic_icon
+ label = icon + _('Add CHANGELOG')
AnchorData.new(
false,
- statistic_icon + _('Add CHANGELOG'),
+ label,
empty_repo? ? add_changelog_ide_path : add_changelog_path
)
elsif repository.changelog.present?
@@ -336,9 +352,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
'license'
)
elsif can_current_user_push_to_default_branch?
+ icon = Feature.enabled?(:project_overview_reorg) ? statistic_icon('plus', 'gl-text-blue-500! gl-mr-3') : statistic_icon
+ label = icon + _('Add LICENSE')
AnchorData.new(
false,
- content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'),
+ content_tag(:span, label, class: 'add-license-link d-flex'),
empty_repo? ? add_license_ide_path : add_license_path
)
end
@@ -346,9 +364,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def contribution_guide_anchor_data
if can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
+ icon = Feature.enabled?(:project_overview_reorg) ? statistic_icon('plus', 'gl-text-blue-500! gl-mr-3') : statistic_icon
+ label = icon + _('Add CONTRIBUTING')
AnchorData.new(
false,
- statistic_icon + _('Add CONTRIBUTING'),
+ label,
empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path
)
elsif repository.contribution_guide.present?
@@ -387,7 +407,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def kubernetes_cluster_anchor_data
if can_instantiate_cluster?
if clusters.empty?
- AnchorData.new(false, statistic_icon + _('Add Kubernetes cluster'), project_clusters_path(project))
+ if Feature.enabled?(:project_overview_reorg)
+ AnchorData.new(false, content_tag(:span, statistic_icon('plus', 'gl-mr-3') + _('Add Kubernetes cluster'), class: 'btn-link'), project_clusters_path(project))
+ else
+ AnchorData.new(false, content_tag(:span, statistic_icon + _('Add Kubernetes cluster')), project_clusters_path(project))
+ end
else
cluster_link = clusters.count == 1 ? project_cluster_path(project, clusters.first) : project_clusters_path(project)
@@ -402,7 +426,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if cicd_missing?
AnchorData.new(false, statistic_icon + _('Set up CI/CD'), project_ci_pipeline_editor_path(project))
elsif repository.gitlab_ci_yml.present?
- AnchorData.new(false, statistic_icon('doc-text') + _('CI/CD configuration'), project_ci_pipeline_editor_path(project), 'btn-default')
+ AnchorData.new(false, statistic_icon('rocket') + _('CI/CD configuration'), project_ci_pipeline_editor_path(project), 'btn-default')
end
end
@@ -412,7 +436,9 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if project.wiki.has_home_page?
AnchorData.new(false, statistic_icon('book') + _('Wiki'), project_wiki_path, 'btn-default', nil, nil)
elsif can_create_wiki?
- AnchorData.new(false, statistic_icon + _('Add Wiki'), project_create_wiki_path, nil, nil, nil)
+ icon = Feature.enabled?(:project_overview_reorg) ? statistic_icon('plus', 'gl-mr-3') : statistic_icon
+ label = icon + _('Add Wiki')
+ AnchorData.new(false, label, project_create_wiki_path, nil, nil, nil)
end
end
@@ -457,8 +483,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def integrations_anchor_data
return unless can?(current_user, :admin_project, project)
- label = statistic_icon('settings') + _('Configure Integrations')
- AnchorData.new(false, label, project_settings_integrations_path(project), nil, nil, nil)
+ if Feature.enabled?(:project_overview_reorg)
+ AnchorData.new(false, content_tag(:span, statistic_icon('plus', 'gl-blue-500! gl-mr-3') + _('Configure Integrations'), class: 'btn-link'), project_settings_integrations_path(project), nil, nil, nil)
+ else
+ AnchorData.new(false, content_tag(:span, statistic_icon('settings') + _('Configure Integrations')), project_settings_integrations_path(project), nil, nil, nil)
+ end
end
def cicd_missing?
diff --git a/app/services/clusters/agents/authorizations/user_access/refresh_service.rb b/app/services/clusters/agents/authorizations/user_access/refresh_service.rb
index 04d6e04c54d..7efa95739fb 100644
--- a/app/services/clusters/agents/authorizations/user_access/refresh_service.rb
+++ b/app/services/clusters/agents/authorizations/user_access/refresh_service.rb
@@ -59,7 +59,7 @@ module Clusters
return unless project_entries
- allowed_projects.where_full_path_in(project_entries.keys).map do |project|
+ allowed_projects.where_full_path_in(project_entries.keys, use_includes: false).map do |project|
{ project_id: project.id, config: user_access_as }
end
end
@@ -70,7 +70,7 @@ module Clusters
return unless group_entries
- allowed_groups.where_full_path_in(group_entries.keys).map do |group|
+ allowed_groups.where_full_path_in(group_entries.keys, use_includes: false).map do |group|
{ group_id: group.id, config: user_access_as }
end
end
diff --git a/app/services/users/migrate_records_to_ghost_user_service.rb b/app/services/users/migrate_records_to_ghost_user_service.rb
index 06950292fea..bd651a3c45e 100644
--- a/app/services/users/migrate_records_to_ghost_user_service.rb
+++ b/app/services/users/migrate_records_to_ghost_user_service.rb
@@ -33,6 +33,8 @@ module Users
attr_reader :execution_tracker
def migrate_records
+ migrate_user_achievements
+
return if hard_delete
migrate_issues
@@ -101,6 +103,11 @@ module Users
batched_migrate(Release, :author_id)
end
+ def migrate_user_achievements
+ batched_migrate(Achievements::UserAchievement, :awarded_by_user_id)
+ batched_migrate(Achievements::UserAchievement, :revoked_by_user_id)
+ end
+
# rubocop:disable CodeReuse/ActiveRecord
def batched_migrate(base_scope, column, batch_size: 50)
loop do
diff --git a/app/views/admin/background_migrations/_migration.html.haml b/app/views/admin/background_migrations/_migration.html.haml
index 99cb63709f5..423eb4b7eb7 100644
--- a/app/views/admin/background_migrations/_migration.html.haml
+++ b/app/views/admin/background_migrations/_migration.html.haml
@@ -22,7 +22,13 @@
href: resume_admin_background_migration_path(migration, database: params[:database]),
button_options: { class: 'has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') })
- elsif migration.failed?
- = render Pajamas::ButtonComponent.new(icon: 'retry',
- method: :post,
- href: retry_admin_background_migration_path(migration, database: params[:database]),
- button_options: { class: 'has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') })
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ size: :small,
+ icon: 'ellipsis_v',
+ button_options: { class: 'js-label-options-dropdown gl-ml-3', 'aria_label': _('Label actions dropdown'), title: _('Label actions dropdown'), data: { toggle: 'dropdown' } })
+ .dropdown-menu.dropdown-menu-right
+ %ul
+ %li
+ = link_button_to _('Retry'), retry_admin_background_migration_path(migration, database: params[:database]), method: :post, icon: 'retry', category: :tertiary, title: _('Retry')
+ %li
+ = clipboard_button text: migration.finalize_command, variant: :default, size: :medium, title: _('Copy command to finalize manually'), category: :tertiary, button_text: _('Copy command to finalize manually')
diff --git a/app/views/notify/request_review_merge_request_email.text.erb b/app/views/notify/request_review_merge_request_email.text.erb
index dc1746d3a8c..14ca59767a8 100644
--- a/app/views/notify/request_review_merge_request_email.text.erb
+++ b/app/views/notify/request_review_merge_request_email.text.erb
@@ -1,2 +1,2 @@
-<%= sanitize_name(@updated_by.name) %> requested a new review on <%= merge_request_reference_link(@merge_request) %>.
+<%= sanitize_name(@updated_by.name) %> requested a new review on <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %>
<%= render_if_exists 'notify/diff_summary' -%>
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index fded6fe8abb..b958c8f8035 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -8,23 +8,27 @@
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
#tree-holder.tree-holder.clearfix.js-per-page{ data: { blame_per_page: Gitlab::Git::BlamePagination::PAGINATION_PER_PAGE } }
+ - if Feature.enabled?(:project_overview_reorg)
+ .nav-block.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-align-items-stretch
+ = render 'projects/tree/tree_header', tree: @tree
+
.info-well.gl-display-none.gl-sm-display-flex.project-last-commit.gl-flex-direction-column.gl-mt-5
#js-last-commit.gl-m-auto{ data: {ref_type: @ref_type.to_s} }
= gl_loading_icon(size: 'md')
- if project.licensed_feature_available?(:code_owners)
#js-code-owners{ data: { branch: @ref, can_view_branch_rules: can_view_branch_rules?, branch_rules_path: branch_rules_path } }
- .nav-block.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-align-items-stretch
- = render 'projects/tree/tree_header', tree: @tree
+ - if Feature.disabled?(:project_overview_reorg)
+ .nav-block.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-align-items-stretch
+ = render 'projects/tree/tree_header', tree: @tree
- if project.forked?
#js-fork-info{ data: vue_fork_divergence_data(project, ref) }
- - if is_project_overview && has_project_shortcut_buttons
+ - if Feature.disabled?(:project_overview_reorg) && is_project_overview && has_project_shortcut_buttons
.project-buttons.gl-mb-5.js-show-on-project-root{ data: { testid: 'project-buttons' } }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true
#js-tree-list{ data: vue_file_list_data(project, ref) }
- if can_edit_tree?
= render 'projects/blob/new_dir'
-
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index af48bbd262f..4ade8ccf348 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,9 +1,9 @@
- empty_repo = @project.empty_repo?
-- show_auto_devops_callout = show_auto_devops_callout?(@project)
- emails_disabled = @project.emails_disabled?
+- ff_reorg_disabled = Feature.disabled?(:project_overview_reorg)
-.project-home-panel.js-show-on-project-root.gl-mt-4.gl-mb-5{ class: [("empty-project" if empty_repo)] }
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-mb-3.gl-gap-5
+%header.project-home-panel.js-show-on-project-root.gl-mt-5{ class: [("empty-project" if empty_repo)] }
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-flex-direction-column.gl-md-flex-direction-row.gl-gap-5
.home-panel-title-row.gl-display-flex.gl-align-items-center
%div{ class: 'avatar-container rect-avatar s64 home-panel-avatar gl-flex-shrink-0 gl-w-11 gl-h-11 gl-mr-3! float-none' }
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
@@ -35,25 +35,29 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- - if can?(current_user, :read_code, @project)
- %nav.project-stats
- - if @project.empty_repo?
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- - else
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
- .gl-my-3
- = render "shared/projects/topics", project: @project
- .home-panel-home-desc.mt-1
- - if @project.description.present?
- .home-panel-description.text-break
- .home-panel-description-markdown.read-more-container{ itemprop: 'description' }
- = markdown_field(@project, :description)
- = render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link, button_options: { class: 'js-read-more-trigger gl-lg-display-none' }) do
- = _("Read more")
+ - if ff_reorg_disabled
+ - if can?(current_user, :read_code, @project)
+ - show_auto_devops_callout = show_auto_devops_callout?(@project)
+
+ %nav.project-stats.gl-mt-3
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ .gl-my-3
+ = render "shared/projects/topics", project: @project
+
+ .home-panel-home-desc.mt-1
+ - if @project.description.present?
+ .home-panel-description.text-break
+ .home-panel-description-markdown.read-more-container{ itemprop: 'description' }
+ = markdown_field(@project, :description)
+ = render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link, button_options: { class: 'js-read-more-trigger gl-lg-display-none' }) do
+ = _("Read more")
= render_if_exists "projects/home_mirror"
- - if @project.badges.present?
+ - if ff_reorg_disabled && @project.badges.present?
.project-badges.mb-2{ data: { testid: 'project-badges-content' } }
- @project.badges.each do |badge|
- badge_link_url = badge.rendered_link_url(@project)
diff --git a/app/views/projects/_invite_members_empty_project.html.haml b/app/views/projects/_invite_members_empty_project.html.haml
index 14b0e82e021..730021f345a 100644
--- a/app/views/projects/_invite_members_empty_project.html.haml
+++ b/app/views/projects/_invite_members_empty_project.html.haml
@@ -1,9 +1,20 @@
-%h4.gl-mt-0.gl-mb-3{ data: { testid: 'invite-member-section',
- track_label: 'invite_members_empty_project',
- track_action: 'render' } }
- = s_('InviteMember|Invite your team')
-%p= s_('InviteMember|Add members to this project and start collaborating with your team.')
-.js-invite-members-trigger{ data: { variant: 'confirm',
- classes: 'gl-mb-8 gl-w-full gl-sm-w-auto',
- display_text: s_('InviteMember|Invite members'),
- trigger_source: 'project_empty_page' } }
+- if Feature.enabled?(:project_overview_reorg)
+ %p.gl-font-weight-bold.gl-text-gray-900.gl-mt-0.gl-mt-n1.gl-mb-3{ data: { testid: 'invite-member-section',
+ track_label: 'invite_members_empty_project',
+ track_action: 'render' } }
+ = s_('InviteMember|Invite your team')
+ %p.gl-mb-3= s_('InviteMember|Add members to this project and start collaborating with your team.')
+ .js-invite-members-trigger{ data: { variant: 'confirm',
+ classes: 'gl-mb-3 gl-w-full gl-sm-w-auto',
+ display_text: s_('InviteMember|Invite members'),
+ trigger_source: 'project_empty_page' } }
+- else
+ %h4.gl-mt-0.gl-mb-3{ data: { testid: 'invite-member-section',
+ track_label: 'invite_members_empty_project',
+ track_action: 'render' } }
+ = s_('InviteMember|Invite your team')
+ %p= s_('InviteMember|Add members to this project and start collaborating with your team.')
+ .js-invite-members-trigger{ data: { variant: 'confirm',
+ classes: 'gl-mb-8 gl-w-full gl-sm-w-auto',
+ display_text: s_('InviteMember|Invite members'),
+ trigger_source: 'project_empty_page' } }
diff --git a/app/views/projects/_sidebar.html.haml b/app/views/projects/_sidebar.html.haml
new file mode 100644
index 00000000000..565f14d01d9
--- /dev/null
+++ b/app/views/projects/_sidebar.html.haml
@@ -0,0 +1,61 @@
+- has_project_shortcut_buttons = !current_user || current_user.project_shortcut_buttons
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
+
+%aside.project-page-sidebar
+ - if @project.description.present? || @project.badges.present?
+ .project-page-sidebar-block.home-panel-home-desc.gl-py-4.gl-border-b.gl-border-gray-50
+ -# Project description
+ - if @project.description.present?
+ .gl-display-flex.gl-justify-content-space-between.gl-mt-1.gl-pr-2
+ %p.gl-font-weight-bold.gl-text-gray-900.gl-m-0= s_('ProjectPage|Project information')
+ = render Pajamas::ButtonComponent.new(href: edit_project_path(@project),
+ category: :tertiary,
+ icon: 'settings',
+ size: :small,
+ button_options: { class: 'has-tooltip', title: s_('ProjectPage|Project settings'), 'aria-label' => s_('ProjectPage|Project settings') })
+ .home-panel-description.text-break
+ .home-panel-description-markdown{ itemprop: 'description' }
+ = markdown_field(@project, :description)
+
+ -# Topics
+ - if @project.topics.present?
+ .gl-mb-5
+ = render "shared/projects/topics", project: @project
+
+ -# Programming languages
+ - if can?(current_user, :read_code, @project) && @project.repository_languages.present?
+ .gl-mb-2{ class: ('gl-mb-4!' if @project.badges.present?) }
+ = repository_languages_bar(@project.repository_languages)
+
+ -# Badges
+ - if @project.badges.present?
+ .project-badges.gl-mb-2{ data: { testid: 'project-badges-content' } }
+ - @project.badges.each do |badge|
+ - badge_link_url = badge.rendered_link_url(@project)
+ %a.gl-mr-3{ href: badge_link_url,
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ data: { testid: 'badge-image-link', qa_link_url: badge_link_url } }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
+
+ -# Invite members
+ - if @project.empty_repo?
+ .project-page-sidebar-block.gl-py-4.gl-border-b.gl-border-gray-50
+ = render "invite_members_empty_project" if can_admin_project_member?(@project)
+
+ -# Buttons
+ - if can?(current_user, :read_code, @project) && !@project.empty_repo?
+ .project-page-sidebar-block.gl-py-4.gl-border-b.gl-border-gray-50
+ %nav.project-stats
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+
+ -# Buttons
+ - if has_project_shortcut_buttons
+ .project-page-sidebar-block.gl-py-4
+ .project-buttons.gl-mb-2.js-show-on-project-root{ data: { testid: 'project-buttons' } }
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true
diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml
index 1409b28e735..8cad1974ffa 100644
--- a/app/views/projects/_stat_anchor_list.html.haml
+++ b/app/views/projects/_stat_anchor_list.html.haml
@@ -2,8 +2,9 @@
- project_buttons = local_assigns.fetch(:project_buttons, false)
- return unless anchors.any?
-%ul.nav{ class: (project_buttons ? 'gl-gap-3' : 'gl-gap-5') }
+
+%ul.nav.gl-gap-2
- anchors.each do |anchor|
%li.nav-item
= link_to_if(anchor.link, anchor.label, anchor.link, stat_anchor_attrs(anchor)) do
- .stat-text.d-flex.align-items-center{ class: ('btn gl-button btn-default disabled' if project_buttons) }= anchor.label
+ .stat-text.d-flex.align-items-center{ class: ('btn gl-button btn-default gl-px-0! disabled' if project_buttons) }= anchor.label
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index ca9900a646e..a260dd2bf28 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -9,68 +9,142 @@
= render "home_panel"
= render "archived_notice", project: @project
-= render "invite_members_empty_project" if can_admin_project_member?(@project)
+- if Feature.enabled?(:project_overview_reorg)
+ - add_page_specific_style 'page_bundles/project'
-%h4.gl-mt-0.gl-mb-3
- = _('The repository for this project is empty')
+ .project-page-indicator.js-show-on-project-root
-- if @project.can_current_user_push_code?
- %p
- = _('You can get started by cloning the repository or start adding files to it with one of the following options.')
+ .project-page-layout
+ .project-page-layout-content.gl-mt-5
+ .project-buttons.gl-mb-5{ data: { testid: 'quick-actions-container' } }
+ .project-clone-holder.d-block.d-md-none
+ = render "shared/mobile_clone_panel"
-.project-buttons{ data: { testid: 'quick-actions-container' } }
- .project-code-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
- = render "shared/mobile_clone_panel"
+ .project-clone-holder.gl-display-none.gl-md-display-flex.gl-justify-content-end.gl-w-full.gl-mt-2
+ = render "projects/buttons/code", ref: @ref
- .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
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-new-card-body gl-bg-gray-10 gl-p-5' }) do |c|
+ - c.with_body do
+ %h4.gl-font-lg.gl-mt-0.gl-mb-2= _('The repository for this project is empty')
+ - if @project.can_current_user_push_code?
+ %p.gl-m-0.gl-text-secondary= _('You can get started by cloning the repository or start adding files to it with one of the following options.')
-- if can?(current_user, :push_code, @project)
- .empty-wrapper.gl-mt-4
- %h3#repo-command-line-instructions.page-title-empty
- = _('Command line instructions')
+ - if can?(current_user, :push_code, @project)
+ = render Pajamas::CardComponent.new(header_options: { class: 'gl-py-4' }) do |c|
+ - c.with_header do
+ %h5.gl-font-lg.gl-m-0= _('Command line instructions')
+ - c.with_body do
+ %p
+ = _('You can also upload existing files from your computer using the instructions below.')
+ .git-empty.js-git-empty
+ %h5= _('Git global setup')
+ %pre.gl-bg-gray-10
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
+
+ %h5= _('Create a new repository')
+ %pre.gl-bg-gray-10
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ git switch --create #{h escaped_default_branch_name}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin #{h escaped_default_branch_name }
+
+ %h5= _('Push an existing folder')
+ %pre.gl-bg-gray-10
+ :preserve
+ cd existing_folder
+ git init --initial-branch=#{h escaped_default_branch_name}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin #{h escaped_default_branch_name }
+
+ %h5= _('Push an existing Git repository')
+ %pre.gl-bg-gray-10
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin --all
+ git push --set-upstream origin --tags
+
+ .project-page-layout-sidebar.js-show-on-project-root.gl-mt-5
+ = render "sidebar"
+
+- else
+ = render "invite_members_empty_project" if can_admin_project_member?(@project)
+
+ %h4.gl-mt-0.gl-mb-3
+ = _('The repository for this project is empty')
+
+ - if @project.can_current_user_push_code?
%p
- = _('You can also upload existing files from your computer using the instructions below.')
- .git-empty.js-git-empty
- %h5= _('Git global setup')
- %pre.bg-light
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
-
- %h5= _('Create a new repository')
- %pre.bg-light
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- cd #{h @project.path}
- git switch --create #{h escaped_default_branch_name}
- touch README.md
- git add README.md
- git commit -m "add README"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push --set-upstream origin #{h escaped_default_branch_name }
-
- %h5= _('Push an existing folder')
- %pre.bg-light
- :preserve
- cd existing_folder
- git init --initial-branch=#{h escaped_default_branch_name}
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- git add .
- git commit -m "Initial commit"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push --set-upstream origin #{h escaped_default_branch_name }
-
- %h5= _('Push an existing Git repository')
- %pre.bg-light
- :preserve
- cd existing_repo
- git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push --set-upstream origin --all
- git push --set-upstream origin --tags
+ = _('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
+ = 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/code", ref: @ref
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true
+
+ - if can?(current_user, :push_code, @project)
+ .empty-wrapper.gl-mt-4
+ %h3#repo-command-line-instructions.page-title-empty
+ = _('Command line instructions')
+ %p
+ = _('You can also upload existing files from your computer using the instructions below.')
+ .git-empty.js-git-empty
+ %h5= _('Git global setup')
+ %pre.bg-light
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
+
+ %h5= _('Create a new repository')
+ %pre.bg-light
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ git switch --create #{h escaped_default_branch_name}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin #{h escaped_default_branch_name }
+
+ %h5= _('Push an existing folder')
+ %pre.bg-light
+ :preserve
+ cd existing_folder
+ git init --initial-branch=#{h escaped_default_branch_name}
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin #{h escaped_default_branch_name }
+
+ %h5= _('Push an existing Git repository')
+ %pre.bg-light
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push --set-upstream origin --all
+ git push --set-upstream origin --tags
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index c76fa5e2220..9c0badf4090 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -15,15 +15,35 @@
= render "home_panel"
-- if can?(current_user, :read_code, @project) && @project.repository_languages.present?
- - add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- = repository_languages_bar(@project.repository_languages)
+- if Feature.enabled?(:project_overview_reorg)
+ .project-page-indicator.js-show-on-project-root
-= render "archived_notice", project: @project
-= render_if_exists "projects/marked_for_deletion_notice", project: @project
-= render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
+ .project-page-layout
+ .project-page-layout-sidebar.js-show-on-project-root.gl-mt-5
+ = render "sidebar"
-- view_path = @project.default_view
+ .project-page-layout-content.gl-mt-5
+ - if can?(current_user, :read_code, @project) && @project.repository_languages.present?
+ - add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
-%div{ class: project_child_container_class(view_path) }
- = render view_path, is_project_overview: true
+ = render "archived_notice", project: @project
+ = render_if_exists "projects/marked_for_deletion_notice", project: @project
+ = render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
+
+ - view_path = @project.default_view
+
+ %div{ class: project_child_container_class(view_path) }
+ = render view_path, is_project_overview: true
+- else
+ - if can?(current_user, :read_code, @project) && @project.repository_languages.present?
+ - add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
+ = repository_languages_bar(@project.repository_languages)
+
+ = render "archived_notice", project: @project
+ = render_if_exists "projects/marked_for_deletion_notice", project: @project
+ = render_if_exists "projects/ancestor_group_marked_for_deletion_notice", project: @project
+
+ - view_path = @project.default_view
+
+ %div{ class: project_child_container_class(view_path) }
+ = render view_path, is_project_overview: true
diff --git a/app/views/shared/projects/_topics.html.haml b/app/views/shared/projects/_topics.html.haml
index c4524125a21..33e4ac58fa5 100644
--- a/app/views/shared/projects/_topics.html.haml
+++ b/app/views/shared/projects/_topics.html.haml
@@ -2,8 +2,9 @@
- if project.topics.present?
.gl-w-full.gl-display-inline-flex.gl-flex-wrap.gl-font-base.gl-font-weight-normal.gl-align-items-center.gl-mx-n2.gl-my-n2{ 'data-testid': 'project_topic_list' }
- %span.gl-p-2.gl-text-gray-500
- = _('Topics') + ':'
+ - if Feature.disabled?(:project_overview_reorg)
+ %span.gl-p-2.gl-text-gray-500
+ = _('Topics') + ':'
- project.topics_to_show.each do |topic|
- explore_project_topic_path = topic_explore_projects_cleaned_path(topic_name: topic[:name])
- if topic[:title].length > max_project_topic_length