diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-30 15:23:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-30 15:23:27 +0300 |
commit | 3bba41a8c5dfcca0d086eaef10ef36a705dd4f7a (patch) | |
tree | 81954681947aaa85592fa7f3c9beed23a7b6bb01 /app | |
parent | 1aa447601c6be1e964acbb674887649dab23b804 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |