Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-30 12:23:27 +00:00
parent 1aa447601c
commit 3bba41a8c5
138 changed files with 1283 additions and 2375 deletions

View File

@ -86,7 +86,6 @@ Style/ExplicitBlockArgument:
- 'spec/support/database/gitlab_schemas_validate_connection.rb'
- 'spec/support/helpers/feature_flag_helpers.rb'
- 'spec/support/helpers/features/runners_helpers.rb'
- 'spec/support/helpers/features/top_nav_spec_helpers.rb'
- 'spec/support/helpers/modal_helpers.rb'
- 'spec/support/helpers/next_found_instance_of.rb'
- 'spec/support/helpers/usage_data_helpers.rb'

View File

@ -555,8 +555,6 @@ Style/GuardClause:
- 'lib/tasks/config_lint.rake'
- 'lib/tasks/gettext.rake'
- 'qa/qa/ee/page/file/show.rb'
- 'qa/qa/mobile/page/main/menu.rb'
- 'qa/qa/mobile/page/sub_menus/common.rb'
- 'qa/qa/page/component/snippet.rb'
- 'qa/qa/page/mattermost/login.rb'
- 'qa/qa/page/page_concern.rb'

View File

@ -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>

View File

@ -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>

View File

@ -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;
},
},

View File

@ -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',

View File

@ -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: {

View File

@ -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)"
/>

View File

@ -4,7 +4,8 @@
"AlertManagementPrometheusIntegration"
],
"AmazonS3ConfigurationInterface": [
"AmazonS3ConfigurationType"
"AmazonS3ConfigurationType",
"InstanceAmazonS3ConfigurationType"
],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()]);

View File

@ -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);

View File

@ -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());

View File

@ -1 +0,0 @@
export * from './reset_menu_items_active';

View File

@ -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),
};
};

View File

@ -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),
};
}

View File

@ -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 });

View File

@ -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;

View File

@ -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() {

View File

@ -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
>

View File

@ -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"

View File

@ -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} users 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"
/>

View File

@ -26,6 +26,22 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
return 0;
});
/**
* 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

View File

@ -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'),
};

View File

@ -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;
}
}
}
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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)}"

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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' -%>

View File

@ -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'

View File

@ -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)

View File

@ -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' } }

View File

@ -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

View File

@ -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

View File

@ -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}"
= _('You can get started by cloning the repository or start adding files to it with one of the following options.')
%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 }
.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"
%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 }
.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
%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
- 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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,8 @@
---
name: key_contacts_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432400
rollout_issue_url: https://gitlab.com/gitlab-org/customers-gitlab-com/-/issues/5873
milestone: '16.7'
type: development
group: group::subscription management
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: project_overview_reorg
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137025
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432034
milestone: '16.7'
type: development
group: group::ux paper cuts
default_enabled: false

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class FixBrokenUserAchievementsAwarded < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '16.7'
class User < MigrationRecord
self.table_name = 'users'
end
def up
User.reset_column_information
ghost_id = User.where(user_type: 5).first&.id
return unless ghost_id
update_column_in_batches(:user_achievements, :awarded_by_user_id, ghost_id) do |table, query|
query.where(table[:awarded_by_user_id].eq(nil))
end
end
def down
# noop -- this is a data migration and can't be reversed
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class FixBrokenUserAchievementsRevoked < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main
milestone '16.7'
class User < MigrationRecord
self.table_name = 'users'
end
def up
User.reset_column_information
ghost_id = User.where(user_type: 5).first&.id
return unless ghost_id
update_column_in_batches(:user_achievements, :revoked_by_user_id, ghost_id) do |table, query|
query.where(table[:revoked_at].not_eq(nil)).where(table[:revoked_by_user_id].eq(nil))
end
end
def down
# noop -- this is a data migration and can't be reversed
end
end

View File

@ -0,0 +1 @@
2fd167741f25de79d9aa561e0b48f3b1c3c40bce45df762d82841ac0e52109aa

View File

@ -0,0 +1 @@
8bea5995e63f29947b408a871615b3838d586af4baac3eca79aaa39c8334a379

View File

@ -55,6 +55,7 @@ Audit event types belong to the following product categories.
| [`google_cloud_logging_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
| [`google_cloud_logging_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
| [`google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122025) | Triggered when Google Cloud Logging configuration is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) |
| [`instance_amazon_s3_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137651) | Triggered when instance Amazon S3 configuration for audit events streaming is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/423235) |
| [`instance_google_cloud_logging_configuration_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130663) | Triggered when Instance level Google Cloud Logging configuration is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/423038) |
| [`instance_google_cloud_logging_configuration_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131752) | Triggered when instance level Google Cloud Logging configuration is deleted.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423040) |
| [`instance_google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790) | Triggered when instance level Google Cloud Logging configuration is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423039) |

View File

@ -682,6 +682,19 @@ In GitLab 13.4, a seed project is added when GitLab is first installed. This mak
on a new Geo secondary site. There is an [issue to account for seed projects](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5618)
when checking the database.
### Message: `FATAL: could not map anonymous shared memory: Cannot allocate memory`
If you see this message, it means that the secondary site's PostgreSQL tries to request memory that is higher than the available memory. There is an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/381585) that tracks this problem.
Example error message in Patroni logs (located at `/var/log/gitlab/patroni/current` for Linux package installations):
```plaintext
2023-11-21_23:55:18.63727 FATAL: could not map anonymous shared memory: Cannot allocate memory
2023-11-21_23:55:18.63729 HINT: This error usually means that PostgreSQL's request for a shared memory segment exceeded available memory, swap space, or huge pages. To reduce the request size (currently 17035526144 bytes), reduce PostgreSQL's shared memory usage, perhaps by reducing shared_buffers or max_connections.
```
The workaround is to increase the memory available to the secondary site's PostgreSQL nodes to match the memory requirements of the primary site's PostgreSQL nodes.
## Synchronization errors
### Reverify all uploads (or any SSF data type which is verified)

View File

@ -1472,6 +1472,29 @@ Input type: `AuditEventsAmazonS3ConfigurationUpdateInput`
| <a id="mutationauditeventsamazons3configurationupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationauditeventsamazons3configurationupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.auditEventsInstanceAmazonS3ConfigurationCreate`
Input type: `AuditEventsInstanceAmazonS3ConfigurationCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationauditeventsinstanceamazons3configurationcreateaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
| <a id="mutationauditeventsinstanceamazons3configurationcreateawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
| <a id="mutationauditeventsinstanceamazons3configurationcreatebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="mutationauditeventsinstanceamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationauditeventsinstanceamazons3configurationcreatename"></a>`name` | [`String`](#string) | Destination name. |
| <a id="mutationauditeventsinstanceamazons3configurationcreatesecretaccesskey"></a>`secretAccessKey` | [`String!`](#string) | Secret access key of the Amazon S3 account. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationauditeventsinstanceamazons3configurationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationauditeventsinstanceamazons3configurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationauditeventsinstanceamazons3configurationcreateinstanceamazons3configuration"></a>`instanceAmazonS3Configuration` | [`InstanceAmazonS3ConfigurationType`](#instanceamazons3configurationtype) | Created instance Amazon S3 configuration. |
### `Mutation.auditEventsStreamingDestinationEventsAdd`
Input type: `AuditEventsStreamingDestinationEventsAddInput`
@ -20317,6 +20340,20 @@ CI/CD variables a project inherites from its parent group and ancestors.
| <a id="inheritedcivariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
| <a id="inheritedcivariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
### `InstanceAmazonS3ConfigurationType`
Stores instance level Amazon S3 configurations for audit event streaming.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="instanceamazons3configurationtypeaccesskeyxid"></a>`accessKeyXid` | [`String!`](#string) | Access key ID of the Amazon S3 account. |
| <a id="instanceamazons3configurationtypeawsregion"></a>`awsRegion` | [`String!`](#string) | AWS region where the bucket is created. |
| <a id="instanceamazons3configurationtypebucketname"></a>`bucketName` | [`String!`](#string) | Name of the bucket where the audit events would be logged. |
| <a id="instanceamazons3configurationtypeid"></a>`id` | [`ID!`](#id) | ID of the configuration. |
| <a id="instanceamazons3configurationtypename"></a>`name` | [`String!`](#string) | Name of the external destination to send audit events to. |
### `InstanceExternalAuditEventDestination`
Represents an external resource to send instance audit events to.
@ -32377,6 +32414,7 @@ Implementations:
Implementations:
- [`AmazonS3ConfigurationType`](#amazons3configurationtype)
- [`InstanceAmazonS3ConfigurationType`](#instanceamazons3configurationtype)
##### Fields

View File

@ -189,9 +189,10 @@ a patch fix like `1.5.1`, then `~latest` returns the `1.5.1` release.
[Issue #427286](https://gitlab.com/gitlab-org/gitlab/-/issues/427286) proposes to
change this behavior.
## CI/CD Catalog **(FREE ALL EXPERIMENT)**
## CI/CD Catalog **(FREE ALL BETA)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407249) in GitLab 16.1.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/407249) in GitLab 16.1 as an [experiment](../../policy/experiment-beta-support.md#experiment).
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/432045) to [beta](../../policy/experiment-beta-support.md#beta) in GitLab 16.7.
The CI/CD Catalog is a list of projects with published CI/CD components you can use to extend your CI/CD workflow.

View File

@ -19,10 +19,8 @@ When selecting a single link to navigate, use `click_`.
For example:
```ruby
def click_ci_cd_pipelines
within_sidebar do
click_element(:link_pipelines)
end
def click_add_badge_button
click_element :add_badge_button
end
```
@ -38,12 +36,8 @@ When interacting with multiple elements to go to a page, use `go_to_`.
For example:
```ruby
def go_to_operations_environments
hover_operations do
within_submenu do
click_element(:operations_environments_link)
end
end
def go_to_applications
click_element('nav-item-link', submenu_item: 'Applications')
end
```

View File

@ -359,6 +359,7 @@ Below is a list of Mattermost version changes for GitLab 14.0 and later:
| GitLab version | Mattermost version | Notes |
| :------------- | :----------------- | ---------------------------------------------------------------------------------------- |
| 16.7 | 9.2 | |
| 16.6 | 9.1 | |
| 16.5 | 9.0 | |
| 16.4 | 8.1 | |

View File

@ -63,9 +63,9 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**.
1. Set the variable `AZURE_CLIENT_ID` to your Azure client ID.
1. Set the variable `AZURE_CLIENT_SECRET` to your Azure client secret.
1. Set the variable `AZURE_TENANT_ID` to your service principal.
1. Set the variable `ARM_CLIENT_ID` to your Azure client ID.
1. Set the variable `ARM_CLIENT_SECRET` to your Azure client secret.
1. Set the variable `ARM_TENANT_ID` to your service principal.
1. Set the variable `TF_VAR_agent_token` to the agent token displayed in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address displayed in the previous task.

View File

@ -36,9 +36,11 @@ You can upload a file from the GitLab UI.
<!-- For why we duplicated the info, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111072#note_1267429478 -->
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name, select the plus icon (**{plus}**).
1. From the dropdown list, select **Upload file**.
1. Complete the fields. To create a merge request with the uploaded file, ensure the **Start a new merge request with these changes** toggle is turned on.
1. Go to the directory where you want to upload the file.
1. Next to the directory name, select the plus icon (**{plus}**) > **Upload file**.
1. Complete the fields.
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Upload file**.
## Commit changes to a repository

View File

@ -6,70 +6,70 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Web Editor **(FREE ALL)**
You can use the Web Editor to make changes to a single file directly from the
GitLab UI. To make changes to multiple files, see [Web IDE](../web_ide/index.md).
You can use the Web Editor to make changes to a single file directly from the GitLab UI.
To make changes to multiple files, see [Web IDE](../web_ide/index.md).
Your [primary email address](../../../user/profile/index.md#change-the-email-displayed-on-your-commits)
is used by default for any change you commit through the Web Editor.
Your [primary email address](../../profile/index.md#change-the-email-displayed-on-your-commits)
is used by default for any change you commit with the Web Editor.
When you create or edit a file in the Web Editor, you can use the same
[keyboard shortcuts](../../shortcuts.md#web-ide) for the Web IDE.
## Create a file
To create a text file in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name,
select the plus icon (**{plus}**).
1. From the dropdown list, select **New file**.
1. Go to the directory where you want to create the new file.
1. Next to the directory name, select the plus icon (**{plus}**) > **New file**.
1. Complete the fields.
1. To create a merge request with the new file, ensure the **Start a new merge request with these changes** checkbox is selected, if you had chosen a **Target branch** other than the [default branch (such as `main`)](../../../user/project/repository/branches/default.md).
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Commit changes**.
### Create a file from a template
### From a template
To create a text file from a template in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Repository**.
1. Next to the project name, select the plus icon (**{plus}**) to display a
dropdown list, then select **New file** from the list.
1. For **Filename**, provide one of the filenames that GitLab provides a template for:
1. Go to the directory where you want to create the new file.
1. Next to the directory name, select the plus icon (**{plus}**) > **New file**.
1. In **Filename**, enter a filename that GitLab provides a template for:
- `.gitignore`
- `.gitlab-ci.yml`
- `LICENSE`
- `Dockerfile`
1. Select **Apply a template**, then select the template you want to apply.
1. Make your changes to the file.
1. Provide a **Commit message**.
1. Enter a **Target branch** to merge into. To create a new merge request with
your changes, enter a branch name that is not your repository's
[default branch](../../../user/project/repository/branches/default.md),
1. Select **Commit changes** to add the commit to your branch.
1. From the **Apply a template** dropdown list, select a template.
1. Complete the fields.
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Commit changes**.
## Edit a file
To edit a text file in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. Go to your file.
1. In the upper right, select **Edit > Edit single file**.
Here you can use the same [keyboard shortcuts](../../shortcuts.md#web-ide) for the Web IDE.
1. Go to the file you want to edit.
1. Select **Edit > Edit single file**.
1. Complete the fields.
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Commit changes**.
### Preview Markdown
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378966) in GitLab 15.6.
To preview Markdown content in the Web Editor:
To preview a Markdown file in the Web Editor:
1. [Edit a file](#edit-a-file).
1. Do one of the following:
- Select the **Preview** tab.
- From the context menu, select **Preview Markdown**.
1. On the left sidebar, select **Search or go to** and find your project.
1. Go to the file you want to preview.
1. Select **Edit > Edit single file**.
1. Select the **Preview** tab.
In the **Preview** tab, you can see a live Markdown preview alongside your content.
You can see a live Markdown preview alongside your content.
To close the preview panel, do one of the following:
- Select the **Write** tab.
- From the context menu, select **Hide Live Preview**.
To close the preview panel, select the **Write** tab.
### Link to specific lines
@ -79,22 +79,21 @@ information to the filename segment of the URL. For example:
- `MY_FILE.js#L3` highlights line 3 in `MY_FILE.js`.
- `MY_FILE.js#L3-10` highlights lines 3 to 10 in `MY_FILE.js`.
To link to a single line, you can also:
1. [Edit a file](#edit-a-file).
1. Select a line number.
When you edit a file, you can also link to a single line by selecting a line number.
## Upload a file
To upload a binary file in the Web Editor:
To upload a file in the Web Editor:
<!-- This list is duplicated at doc/gitlab-basics/add-file.md#from-the-ui -->
<!-- This list is duplicated at doc/user/project/repository/index.md#add-a-file-from-the-ui -->
<!-- For why we duplicated the info, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111072#note_1267429478 -->
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name, select the plus icon (**{plus}**).
1. From the dropdown list, select **Upload file**.
1. Complete the fields. To create a merge request with the uploaded file, ensure the **Start a new merge request with these changes** toggle is turned on.
1. Go to the directory where you want to upload the file.
1. Next to the directory name, select the plus icon (**{plus}**) > **Upload file**.
1. Complete the fields.
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Upload file**.
## Create a directory
@ -102,9 +101,11 @@ To upload a binary file in the Web Editor:
To create a directory in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name, select the plus icon (**{plus}**).
1. From the dropdown list, select **New directory**.
1. Complete the fields. To create a merge request with the new directory, ensure the **Start a new merge request with these changes** toggle is turned on.
1. Go to the directory where you want to create the new directory.
1. Next to the directory name, select the plus icon (**{plus}**) > **New directory**.
1. Complete the fields.
- To create a merge request with your changes, enter a branch name
that's not your repository's [default branch](branches/default.md).
1. Select **Create directory**.
## Create a branch
@ -112,8 +113,7 @@ To create a directory in the Web Editor:
To create a [branch](branches/index.md) in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name, select the plus icon (**{plus}**).
1. From the dropdown list, select **New branch**.
1. Next to the repository name, select the plus icon (**{plus}**) > **New branch**.
1. Complete the fields.
1. Select **Create branch**.
@ -123,7 +123,6 @@ You can create [tags](tags/index.md) to mark milestones such as
production releases and release candidates. To create a tag in the Web Editor:
1. On the left sidebar, select **Search or go to** and find your project.
1. From the project dashboard or repository, next to the branch name, select the plus icon (**{plus}**).
1. From the dropdown list, select **New tag**.
1. Next to the repository name, select the plus icon (**{plus}**) > **New tag**.
1. Complete the fields.
1. Select **Create tag**.

View File

@ -122,7 +122,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def project_id
@project_id ||= Project.where_full_path_in([full_path]).pluck(:id)
@project_id ||= Project.where_full_path_in([full_path], use_includes: false).pluck(:id)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View File

@ -264,6 +264,13 @@ module Gitlab
100 * migrated_tuple_count / total_tuple_count
end
def finalize_command
<<~SCRIPT.delete("\n").squeeze(' ').strip
sudo gitlab-rake gitlab:background_migrations:finalize
[#{job_class_name},#{table_name},#{column_name},'#{job_arguments.to_json.gsub(',', '\,')}']
SCRIPT
end
private
def validate_batched_jobs_status

View File

@ -231,7 +231,7 @@ module Gitlab
"\n\n" \
"Finalize it manually by running the following command in a `bash` or `sh` shell:" \
"\n\n" \
"\tsudo gitlab-rake gitlab:background_migrations:finalize[#{job_class_name},#{table_name},#{column_name},'#{job_arguments.to_json.gsub(',', '\,')}']" \
"\t#{migration.finalize_command}" \
"\n\n" \
"For more information, check the documentation" \
"\n\n" \

View File

@ -77,6 +77,7 @@ module Gitlab
push_frontend_feature_flag(:security_auto_fix)
push_frontend_feature_flag(:source_editor_toolbar)
push_frontend_feature_flag(:vscode_web_ide, current_user)
push_frontend_feature_flag(:key_contacts_management, current_user)
# To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:custom_emoji)

View File

@ -20,8 +20,7 @@ module Gitlab
# this logic cannot be placed in the NamespaceResolver due to N+1
scope = scope.without_project_namespaces if scope == Namespace
# `with_route` avoids an N+1 calculating full_path
scope = scope.where_full_path_in(full_paths).with_route
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
scope = scope.where_full_path_in(full_paths)
scope.each do |model_instance|
loader.call(model_instance.full_path.downcase, model_instance)

View File

@ -13840,6 +13840,9 @@ msgstr ""
msgid "Copy command"
msgstr ""
msgid "Copy command to finalize manually"
msgstr ""
msgid "Copy commands"
msgstr ""
@ -18985,7 +18988,7 @@ msgstr ""
msgid "Environment|Deployments"
msgstr ""
msgid "Environment|Environment health"
msgid "Environment|Environment status"
msgstr ""
msgid "Environment|External IP"
@ -30337,15 +30340,9 @@ msgstr ""
msgid "Metrics|Delete metric?"
msgstr ""
msgid "Metrics|Description"
msgstr ""
msgid "Metrics|Edit metric"
msgstr ""
msgid "Metrics|Failed to load metrics."
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
@ -30355,15 +30352,9 @@ msgstr ""
msgid "Metrics|Legend label (optional)"
msgstr ""
msgid "Metrics|Metrics"
msgstr ""
msgid "Metrics|Must be a valid PromQL query."
msgstr ""
msgid "Metrics|Name"
msgstr ""
msgid "Metrics|New metric"
msgstr ""
@ -30376,9 +30367,6 @@ msgstr ""
msgid "Metrics|There was an error trying to validate your query"
msgstr ""
msgid "Metrics|Type"
msgstr ""
msgid "Metrics|Unit label"
msgstr ""
@ -32891,15 +32879,30 @@ msgstr ""
msgid "Objective"
msgstr ""
msgid "ObservabilityMetrics|Description"
msgstr ""
msgid "ObservabilityMetrics|Error: Failed to load metrics details. Try reloading the page."
msgstr ""
msgid "ObservabilityMetrics|Failed to load metrics."
msgstr ""
msgid "ObservabilityMetrics|Metric Details"
msgstr ""
msgid "ObservabilityMetrics|Metrics"
msgstr ""
msgid "ObservabilityMetrics|Name"
msgstr ""
msgid "ObservabilityMetrics|Search metrics starting with..."
msgstr ""
msgid "ObservabilityMetrics|Type"
msgstr ""
msgid "Observability|Enable"
msgstr ""
@ -37438,6 +37441,12 @@ msgstr ""
msgid "ProjectPage|Project ID: %{project_id}"
msgstr ""
msgid "ProjectPage|Project information"
msgstr ""
msgid "ProjectPage|Project settings"
msgstr ""
msgid "ProjectQualitySummary|An error occurred while trying to fetch project quality statistics"
msgstr ""
@ -50719,9 +50728,6 @@ msgstr ""
msgid "TopNav|Explore"
msgstr ""
msgid "TopNav|Go back"
msgstr ""
msgid "TopNav|Switch to"
msgstr ""

View File

@ -60,7 +60,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.71.0",
"@gitlab/ui": "^70.0.1",
"@gitlab/ui": "^71.1.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "^0.0.1-dev-20231129035648",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
@ -264,9 +264,6 @@
"gettext-extractor": "^3.7.0",
"gettext-extractor-vue": "^5.1.0",
"glob": "^7.1.6",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-reports": "^3.0.0",
"jest": "^28.1.3",
"jest-canvas-mock": "^2.4.0",
"jest-diff": "^28.1.3",

View File

@ -1,60 +0,0 @@
# frozen_string_literal: true
module QA
module Mobile
module Page
module Main
module Menu
extend QA::Page::PageConcern
def self.prepended(base)
super
base.class_eval do
view 'app/views/layouts/header/_default.html.haml' do
element :mobile_navbar_button, required: true
end
view 'app/assets/javascripts/nav/components/responsive_home.vue' do
element :mobile_new_dropdown
end
end
end
def open_mobile_menu
if has_no_element?('user-avatar-content')
Support::Retrier.retry_until do
click_element(:mobile_navbar_button)
has_element?('user-avatar-content')
end
end
end
def open_mobile_new_dropdown
open_mobile_menu
Support::Retrier.retry_until do
find('[data-qa-selector="mobile_new_dropdown"] > button').click
has_css?('.dropdown-menu-right.show')
end
end
def has_personal_area?(wait: Capybara.default_max_wait_time)
open_mobile_menu
super
end
def has_no_personal_area?(wait: Capybara.default_max_wait_time)
open_mobile_menu
super
end
def within_user_menu
open_mobile_menu
super
end
end
end
end
end
end

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
module QA
module Mobile
module Page
module Project
module Show
extend QA::Page::PageConcern
def self.prepended(base)
super
base.class_eval do
prepend QA::Mobile::Page::Main::Menu
view 'app/assets/javascripts/nav/components/top_nav_new_dropdown.vue' do
element :new_issue_mobile_button
end
end
end
def go_to_new_issue
open_mobile_new_dropdown
click_element(:new_issue_mobile_button)
end
end
end
end
end
end

View File

@ -1,28 +0,0 @@
# frozen_string_literal: true
module QA
module Mobile
module Page
module SubMenus
module Common
def open_mobile_nav_sidebar
unless has_css?('.sidebar-expanded-mobile')
Support::Retrier.retry_until do
click_element('toggle-mobile-nav-button')
has_css?('.sidebar-expanded-mobile')
end
end
end
def within_sidebar
wait_for_requests
open_mobile_nav_sidebar
super
end
end
end
end
end
end

View File

@ -23,16 +23,6 @@ module QA
def go_to_applications
click_element('nav-item-link', submenu_item: 'Applications')
end
private
def within_sidebar(&block)
page.within('.sidebar-top-level-items', &block)
end
def within_submenu(element, &block)
within_element(element, &block)
end
end
end
end

View File

@ -6,7 +6,6 @@ module QA
class Menu < Page::Base
# We need to check phone_layout? instead of mobile_layout? here
# since tablets have the regular top navigation bar
prepend Mobile::Page::Main::Menu if Runtime::Env.phone_layout?
include SubMenus::CreateNewMenu
include SubMenus::SuperSidebar::GlobalSearchModal
@ -40,14 +39,6 @@ module QA
element 'global-search-input'
end
view 'app/assets/javascripts/nav/components/top_nav_app.vue' do
element :navbar_dropdown
end
view 'app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue' do
element 'menu-subview'
end
view 'lib/gitlab/nav/top_nav_menu_item.rb' do
element :menu_item_link
end

View File

@ -9,9 +9,6 @@ module QA
include Page::Component::Breadcrumbs
include Page::File::Shared::CommitMessage
include Page::Component::Dropdown
# We need to check phone_layout? instead of mobile_layout? here
# since tablets have the regular top navigation bar
prepend Mobile::Page::Project::Show if Runtime::Env.phone_layout?
view 'app/assets/javascripts/repository/components/preview/index.vue' do
element 'blob-viewer-content'
@ -36,6 +33,9 @@ module QA
view 'app/views/projects/_home_panel.html.haml' do
element 'project-name-content'
element 'project-id-content'
end
view 'app/views/projects/_sidebar.html.haml' do
element 'project-badges-content'
element 'badge-image-link'
end

View File

@ -8,20 +8,12 @@ module QA
super
base.class_eval do
prepend Mobile::Page::SubMenus::Common if QA::Runtime::Env.mobile_layout?
view 'app/assets/javascripts/super_sidebar/components/super_sidebar.vue' do
element :navbar
end
end
end
def within_sidebar(&block)
wait_for_requests
within_element(:navbar, &block)
end
private
# Opens the new item menu and yields to the block

View File

@ -57,6 +57,6 @@ else
end
puts missing_issue_message % missing_issues unless missing_issues.empty?
puts "See https://about.gitlab.com/handbook/engineering/quality/quality-engineering/debugging-qa-test-failures/#quarantining-tests"
puts "See https://about.gitlab.com/handbook/engineering/infrastructure/test-platform/debugging-qa-test-failures/#quarantining-tests"
exit 1
end

View File

@ -25,7 +25,10 @@ RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_cat
'path' => "/#{project.full_path}/-/ml/models/#{model1.id}",
'description' => 'This is a placeholder for the short description',
'latestVersion' => {
'version' => model1.latest_version.version
'version' => model1.latest_version.version,
'description' => model1.latest_version.description,
'projectPath' => "/#{project.full_path}",
'packageId' => model1.latest_version.package_id
},
'versionCount' => 1
}

View File

@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:model) { build_stubbed(:ml_models, project: project) }
let_it_be(:version) { build_stubbed(:ml_model_versions, model: model) }
let_it_be(:version) { build_stubbed(:ml_model_versions, :with_package, model: model, description: 'abc') }
subject(:component) do
described_class.new(model_version: version)
@ -23,7 +23,10 @@ RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feat
'modelVersion' => {
'id' => version.id,
'version' => version.version,
'description' => 'abc',
'projectPath' => "/#{project.full_path}",
'path' => "/#{project.full_path}/-/ml/models/#{model.id}/versions/#{version.id}",
'packageId' => version.package_id,
'model' => {
'name' => model.name,
'path' => "/#{project.full_path}/-/ml/models/#{model.id}"

View File

@ -6,6 +6,7 @@ FactoryBot.define do
model { association :ml_models }
project { model.project }
description { 'Some description' }
trait :with_package do
package do

View File

@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'Frequently visited items', :js, feature_category: :shared do
include Features::TopNavSpecHelpers
let_it_be(:user) { create(:user) }
before do

View File

@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
include Features::TopNavSpecHelpers
before do
stub_application_setting(import_sources: Gitlab::ImportSources.values)
end

View File

@ -10,6 +10,7 @@ RSpec.describe 'Projects > Show > User sees Git instructions', feature_category:
# validation failure on NotificationSetting.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/299822#note_492817174
user.notification_settings.reset
stub_feature_flags(project_overview_reorg: false)
end
shared_examples_for 'redirects to the sign in page' do

View File

@ -17,8 +17,8 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons', feature_cat
describe 'as a normal user' do
before do
stub_feature_flags(project_overview_reorg: false)
sign_in(user)
visit project_path(project)
end
@ -40,6 +40,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons', feature_cat
describe 'as a maintainer' do
before do
stub_feature_flags(project_overview_reorg: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -12,6 +12,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in user
visit new_project_path
stub_feature_flags(project_overview_reorg: false)
end
shared_examples 'creates from template' do |template, sub_template_tab = nil|
@ -99,6 +100,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
stub_feature_flags(project_overview_reorg: false)
end
it 'parses Markdown' do
@ -164,6 +166,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
visit path
stub_feature_flags(project_overview_reorg: false)
end
it 'shows project topics' do
@ -195,6 +198,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
visit path
stub_feature_flags(project_overview_reorg: false)
end
context 'desktop component' do
@ -427,6 +431,10 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
stub_feature_flags(project_overview_reorg: false)
end
it 'does not contain default branch information in its content', :js do
default_branch = 'merge-commit-analyze-side-branch'

View File

@ -18,8 +18,10 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
wait_for_all_requests
data_uri = find('.avatar-image .gl-avatar')['src']
within_testid('user-dropdown') { expect(find('.gl-avatar')['src']).to eq data_uri }
within_testid('user-dropdown') do
# We are setting a blob URL
expect(find('.gl-avatar')['src']).to start_with 'blob:'
end
visit profile_path

View File

@ -149,14 +149,14 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
it('sets `clusterHealthStatus` as error when pods emitted a failure', async () => {
findKubernetesPods().vm.$emit('failed');
findKubernetesPods().vm.$emit('update-failed-state', { pods: true });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
});
it('sets `clusterHealthStatus` as error when workload types emitted a failure', async () => {
findKubernetesTabs().vm.$emit('failed');
findKubernetesTabs().vm.$emit('update-failed-state', { summary: true });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
@ -165,6 +165,21 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
it('sets `clusterHealthStatus` as success when data is loaded and no failures where emitted', () => {
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('success');
});
it('sets `clusterHealthStatus` as success after state update if there are no failures', async () => {
findKubernetesTabs().vm.$emit('update-failed-state', { summary: true });
findKubernetesTabs().vm.$emit('update-failed-state', { pods: true });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
findKubernetesTabs().vm.$emit('update-failed-state', { summary: false });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
findKubernetesTabs().vm.$emit('update-failed-state', { pods: false });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('success');
});
});
describe('on cluster error', () => {

View File

@ -90,11 +90,17 @@ describe('~/environments/components/kubernetes_pods.vue', () => {
]);
});
it('emits a failed event when there are failed pods', async () => {
it('emits a update-failed-state event for each pod', async () => {
createWrapper();
await waitForPromises();
expect(wrapper.emitted('failed')).toHaveLength(1);
expect(wrapper.emitted('update-failed-state')).toHaveLength(4);
expect(wrapper.emitted('update-failed-state')).toEqual([
[{ pods: false }],
[{ pods: false }],
[{ pods: false }],
[{ pods: true }],
]);
});
});

View File

@ -107,8 +107,8 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
);
});
it('emits a failed event when there are failed workload types', () => {
expect(wrapper.emitted('failed')).toHaveLength(1);
it('emits a update-failed-state event when there are failed workload types', () => {
expect(wrapper.emitted('update-failed-state')).toEqual([[{ summary: true }]]);
});
it('emits an error message when gets an error from the cluster_client API', async () => {

View File

@ -179,9 +179,10 @@ describe('~/environments/components/kubernetes_tabs.vue', () => {
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
it('emits a failed event when gets it from the component', () => {
findKubernetesSummary().vm.$emit('failed');
expect(wrapper.emitted('failed')).toHaveLength(1);
it('emits a state update event when gets it from the component', () => {
const eventData = { summary: true };
findKubernetesSummary().vm.$emit('update-failed-state', eventData);
expect(wrapper.emitted('update-failed-state')).toEqual([[eventData]]);
});
});
});

View File

@ -2,6 +2,7 @@ import { GlBadge, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { ShowMlModel } from '~/ml/model_registry/apps';
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
@ -16,6 +17,7 @@ const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
const findVersionsTab = () => wrapper.findAllComponents(GlTab).at(1);
const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
const findModelVersionList = () => findVersionsTab().findComponent(ModelVersionList);
const findModelVersionDetail = () => findDetailTab().findComponent(ModelVersionDetail);
const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
const findTitleArea = () => wrapper.findComponent(TitleArea);
@ -47,7 +49,11 @@ describe('ShowMlModel', () => {
describe('when it has latest version', () => {
it('displays the version', () => {
expect(findDetailTab().text()).toContain(MODEL.latestVersion.version);
expect(findModelVersionDetail().props('modelVersion')).toBe(MODEL.latestVersion);
});
it('displays the title', () => {
expect(findDetailTab().text()).toContain('Latest version: 1.2.3');
});
});
@ -59,6 +65,10 @@ describe('ShowMlModel', () => {
it('shows no version message', () => {
expect(findDetailTab().text()).toContain(NO_VERSIONS_LABEL);
});
it('does not render model version detail', () => {
expect(findModelVersionDetail().exists()).toBe(false);
});
});
});

Some files were not shown because too many files have changed in this diff Show More