Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-09 15:10:05 +00:00
parent f4d6d3ec77
commit c0f42c6d66
108 changed files with 1857 additions and 768 deletions

View File

@ -1 +1 @@
22f7db01953debe8e7c1c46ff2b1ffb5a143c566
dc956109aa3e352cadc55ebba267b43fdd4fdc27

View File

@ -129,7 +129,7 @@ gem 'fog-local', '~> 0.6'
gem 'fog-openstack', '~> 1.0'
gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.3'
gem 'gitlab-fog-azure-rm', '~> 1.0.1', require: false
gem 'gitlab-fog-azure-rm', '~> 1.1.1', require: false
# for Google storage
gem 'google-api-client', '~> 0.33'

View File

@ -457,7 +457,7 @@ GEM
activesupport (>= 3.0)
request_store (>= 1.0)
scientist (~> 1.6, >= 1.6.0)
gitlab-fog-azure-rm (1.0.1)
gitlab-fog-azure-rm (1.1.1)
azure-storage-blob (~> 2.0)
azure-storage-common (~> 2.0)
fog-core (= 2.1.0)
@ -1474,7 +1474,7 @@ DEPENDENCIES
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 2.0.0)
gitlab-experiment (~> 0.5.4)
gitlab-fog-azure-rm (~> 1.0.1)
gitlab-fog-azure-rm (~> 1.1.1)
gitlab-fog-google (~> 1.13)
gitlab-labkit (~> 0.18.0)
gitlab-license (~> 1.5)

View File

@ -5,7 +5,6 @@ import {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlDropdownDivider,
GlInfiniteScroll,
} from '@gitlab/ui';
import { throttle } from 'lodash';
@ -25,7 +24,6 @@ export default {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
GlDropdownDivider,
GlInfiniteScroll,
LogSimpleFilters,
LogAdvancedFilters,
@ -66,7 +64,7 @@ export default {
};
},
computed: {
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods', 'managedApps']),
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace', 'showAdvancedFilters']),
showLoader() {
@ -88,15 +86,12 @@ export default {
});
this.fetchEnvironments(this.environmentsPath);
this.fetchManagedApps(this.clustersPath);
},
methods: {
...mapActions('environmentLogs', [
'setInitData',
'showEnvironment',
'showManagedApp',
'fetchEnvironments',
'fetchManagedApps',
'refreshPodLogs',
'fetchMoreLogsPrepend',
'dismissRequestEnvironmentsError',
@ -107,9 +102,6 @@ export default {
isCurrentEnvironment(envName) {
return envName === this.environments.current;
},
isCurrentManagedApp(appName) {
return appName === this.managedApps.current;
},
topReached() {
if (!this.logs.isLoading) {
this.fetchMoreLogsPrepend();
@ -173,7 +165,7 @@ export default {
<div class="flex-grow-0">
<gl-dropdown
id="environments-dropdown"
:text="environments.current || managedApps.current"
:text="environments.current"
:disabled="environments.isLoading"
class="gl-mr-3 gl-mb-3 gl-display-flex gl-md-display-block js-environments-dropdown"
>
@ -189,19 +181,6 @@ export default {
>
{{ env.name }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-section-header>
{{ s__('Environments|Managed apps') }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="app in managedApps.options"
:key="app.id"
:is-check-item="true"
:is-checked="isCurrentManagedApp(app.name)"
@click="showManagedApp(app.name)"
>
{{ app.name }}
</gl-dropdown-item>
</gl-dropdown>
</div>

View File

@ -13,5 +13,4 @@ export const tracking = {
export const logExplorerOptions = {
environments: 'environments',
managedApps: 'managedApps',
};

View File

@ -25,15 +25,9 @@ const requestUntilData = (url, params) =>
const requestLogsUntilData = ({ commit, state }) => {
const params = {};
const type = state.environments.current
? logExplorerOptions.environments
: logExplorerOptions.managedApps;
const type = logExplorerOptions.environments;
const selectedObj = state[type].options.find(({ name }) => name === state[type].current);
const path =
type === logExplorerOptions.environments
? selectedObj.logs_api_path
: selectedObj.gitlab_managed_apps_logs_path;
const path = selectedObj.logs_api_path;
if (state.pods.current) {
params.pod_name = state.pods.current;
@ -106,11 +100,6 @@ export const showEnvironment = ({ dispatch, commit }, environmentName) => {
dispatch('fetchLogs', tracking.ENVIRONMENT_SELECTED);
};
export const showManagedApp = ({ dispatch, commit }, managedApp) => {
commit(types.SET_MANAGED_APP, managedApp);
dispatch('fetchLogs', tracking.MANAGED_APP_SELECTED);
};
export const refreshPodLogs = ({ dispatch, commit }) => {
commit(types.REFRESH_POD_LOGS);
dispatch('fetchLogs', tracking.REFRESH_POD_LOGS);
@ -135,23 +124,6 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
});
};
/**
* Fetch managed apps data
* @param {Object} store
* @param {String} clustersPath
*/
export const fetchManagedApps = ({ commit }, clustersPath) => {
return axios
.get(clustersPath)
.then(({ data }) => {
commit(types.RECEIVE_MANAGED_APPS_DATA_SUCCESS, data.clusters);
})
.catch(() => {
commit(types.RECEIVE_MANAGED_APPS_DATA_ERROR);
});
};
export const fetchLogs = ({ commit, state }, trackingLabel) => {
commit(types.REQUEST_LOGS_DATA);

View File

@ -6,16 +6,9 @@ const mapTrace = ({ timestamp = null, pod = '', message = '' }) =>
export const trace = (state) => state.logs.lines.map(mapTrace).join('\n');
export const showAdvancedFilters = (state) => {
if (state.environments.current) {
const environment = state.environments.options.find(
({ name }) => name === state.environments.current,
);
return Boolean(environment?.enable_advanced_logs_querying);
}
const managedApp = state.managedApps.options.find(
({ name }) => name === state.managedApps.current,
const environment = state.environments.options.find(
({ name }) => name === state.environments.current,
);
return Boolean(managedApp?.enable_advanced_logs_querying);
return Boolean(environment?.enable_advanced_logs_querying);
};

View File

@ -13,9 +13,6 @@ export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCC
export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR';
export const HIDE_REQUEST_ENVIRONMENTS_ERROR = 'HIDE_REQUEST_ENVIRONMENTS_ERROR';
export const RECEIVE_MANAGED_APPS_DATA_SUCCESS = 'RECEIVE_MANAGED_APPS_DATA_SUCCESS';
export const RECEIVE_MANAGED_APPS_DATA_ERROR = 'RECEIVE_MANAGED_APPS_DATA_ERROR';
export const REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA';
export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS';
export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR';

View File

@ -32,9 +32,6 @@ export default {
// Clear current pod options
state.pods.current = null;
state.pods.options = [];
// Clear current managedApps options
state.managedApps.current = null;
},
[types.REQUEST_ENVIRONMENTS_DATA](state) {
state.environments.options = [];
@ -110,26 +107,4 @@ export default {
[types.RECEIVE_PODS_DATA_ERROR](state) {
state.pods.options = [];
},
// Managed apps data
[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, apps) {
state.managedApps.options = apps.filter(
({ gitlab_managed_apps_logs_path }) => gitlab_managed_apps_logs_path, // eslint-disable-line babel/camelcase
);
state.managedApps.isLoading = false;
},
[types.RECEIVE_MANAGED_APPS_DATA_ERROR](state) {
state.managedApps.options = [];
state.managedApps.isLoading = false;
state.managedApps.fetchError = true;
},
[types.SET_MANAGED_APP](state, managedApp) {
state.managedApps.current = managedApp;
// Clear current pod options
state.pods.current = null;
state.pods.options = [];
// Clear current environment options
state.environments.current = null;
},
};

View File

@ -30,16 +30,6 @@ export default () => ({
fetchError: false,
},
/**
* Managed apps list information
*/
managedApps: {
options: [],
isLoading: false,
current: null,
fetchError: false,
},
/**
* Logs including trace
*/

View File

@ -1,30 +1,101 @@
<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 eventHub, { EVENT_RESPONSIVE_TOGGLE } from '../event_hub';
const TEMPORARY_PLACEHOLDER = 'Placeholder for responsive top nav';
import { resetMenuItemsActive } from '../utils/reset_menu_items_active';
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() {
eventHub.$on(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay);
this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
},
beforeDestroy() {
eventHub.$off(EVENT_RESPONSIVE_TOGGLE, this.onToggle);
this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay);
this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
},
methods: {
onToggle() {
document.body.classList.toggle('top-nav-responsive-open');
},
onMenuItemClick({ view }) {
if (view) {
this.activeView = view;
}
},
showMobileOverlay() {
this.hasMobileOverlay = true;
},
hideMobileOverlay() {
this.hasMobileOverlay = false;
},
},
TEMPORARY_PLACEHOLDER,
FREQUENT_ITEMS_PROJECTS,
FREQUENT_ITEMS_GROUPS,
};
</script>
<template>
<p>{{ $options.TEMPORARY_PLACEHOLDER }}</p>
<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

@ -0,0 +1,37 @@
<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: 'angle-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

@ -0,0 +1,62 @@
<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"
/>
</header>
<top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
</div>
</template>

View File

@ -20,6 +20,11 @@ export default {
type: String,
required: true,
},
containerClass: {
type: String,
required: false,
default: '',
},
linksPrimary: {
type: Array,
required: false,
@ -50,7 +55,11 @@ export default {
<template>
<div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
<div class="frequent-items-dropdown-container gl-w-auto">
<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 v-bind="$attrs" />

View File

@ -16,6 +16,11 @@ export default {
type: Object,
required: true,
},
iconOnly: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
dataAttrs() {
@ -32,13 +37,16 @@ export default {
:href="menuItem.href"
class="top-nav-menu-item gl-display-block"
: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-2!" />
{{ menuItem.title }}
<gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
<gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-2!': !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

@ -54,6 +54,7 @@ export default {
:key="menuItem.id"
:menu-item="menuItem"
data-testid="menu-item"
class="gl-w-full"
:class="{ 'gl-mt-1': menuItemIndex > 0 }"
@click="onClick(menuItem)"
/>

View File

@ -0,0 +1,55 @@
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
},
props: {
viewModel: {
type: Object,
required: true,
},
},
computed: {
sections() {
return this.viewModel.menu_sections || [];
},
showHeaders() {
return this.sections.length > 1;
},
},
};
</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">
<gl-dropdown-item
:key="`${index}_item_${menuItem.id}`"
link-class="top-nav-menu-item"
:href="menuItem.href"
data-testid="item"
>
{{ menuItem.title }}
</gl-dropdown-item>
</template>
</template>
</gl-dropdown>
</template>

View File

@ -0,0 +1,14 @@
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

@ -1,15 +1,14 @@
<script>
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '../../locale';
export default {
i18n: {
removesBranchText: __('%{strongStart}Deletes%{strongEnd} source branch'),
removesBranchText: __('The source branch will be deleted'),
tooltipTitle: __('A user with write access to the source branch selected this option'),
},
components: {
GlIcon,
GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -20,11 +19,7 @@ export default {
<template>
<p v-once class="mr-info-list gl-ml-7 gl-pb-5 gl-mb-0">
<span class="status-text">
<gl-sprintf :message="$options.i18n.removesBranchText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
{{ $options.i18n.removesBranchText }}
</span>
<gl-icon
v-gl-tooltip.hover

View File

@ -497,6 +497,9 @@ body {
color: #dbdbdb;
vertical-align: baseline;
}
.gl-font-sm {
font-size: 12px;
}
.dropdown {
position: relative;
}
@ -2179,6 +2182,12 @@ body.gl-dark {
margin-left: 0 !important;
margin-right: 0 !important;
}
.gl-font-sm {
font-size: 0.75rem;
}
.gl-font-weight-bold {
font-weight: 600;
}
@import "startup/cloaking";
@include cloak-startup-scss(none);

View File

@ -482,6 +482,9 @@ body {
color: #525252;
vertical-align: baseline;
}
.gl-font-sm {
font-size: 12px;
}
.dropdown {
position: relative;
}
@ -1962,6 +1965,12 @@ body.sidebar-refactoring
margin-left: 0 !important;
margin-right: 0 !important;
}
.gl-font-sm {
font-size: 0.75rem;
}
.gl-font-weight-bold {
font-weight: 600;
}
@import "startup/cloaking";
@include cloak-startup-scss(none);

View File

@ -48,7 +48,7 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def pipelines_using_cte
sha_relation = merge_request.all_commits.select(:sha)
sha_relation = sha_relation.distinct if Feature.enabled?(:use_distinct_in_shas_cte)
sha_relation = sha_relation.distinct if Feature.enabled?(:use_distinct_in_shas_cte, default_enabled: :yaml)
cte = Gitlab::SQL::CTE.new(:shas, sha_relation)

View File

@ -17,6 +17,10 @@ module Resolvers
required: false,
description: 'Filter by tags associated with the runner (comma-separated or array).'
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Filter by full token or partial text in description field.'
argument :sort, ::Types::Ci::RunnerSortEnum,
required: false,
description: 'Sort order of results.'

View File

@ -4,21 +4,58 @@ module Nav
module TopNavHelper
PROJECTS_VIEW = :projects
GROUPS_VIEW = :groups
NEW_VIEW = :new
SEARCH_VIEW = :search
def top_nav_view_model(project:, group:)
builder = ::Gitlab::Nav::TopNavViewModelBuilder.new
if current_user
build_view_model(builder: builder, project: project, group: group)
else
build_anonymous_view_model(builder: builder)
build_base_view_model(builder: builder, project: project, group: group)
builder.build
end
def top_nav_responsive_view_model(project:, group:)
builder = ::Gitlab::Nav::TopNavViewModelBuilder.new
build_base_view_model(builder: builder, project: project, group: group)
new_view_model = new_dropdown_view_model(project: project, group: group)
if new_view_model
builder.add_view(NEW_VIEW, new_view_model)
end
if top_nav_show_search
builder.add_view(SEARCH_VIEW, ::Gitlab::Nav::TopNavMenuItem.build(**top_nav_search_menu_item_attrs))
end
builder.build
end
def top_nav_show_search
header_link?(:search)
end
def top_nav_search_menu_item_attrs
{
id: 'search',
title: _('Search'),
icon: 'search',
href: search_context.search_url
}
end
private
def build_base_view_model(builder:, project:, group:)
if current_user
build_view_model(builder: builder, project: project, group: group)
else
build_anonymous_view_model(builder: builder)
end
end
def build_anonymous_view_model(builder:)
# These come from `app/views/layouts/nav/_explore.html.ham`
if explore_nav_link?(:projects)

View File

@ -168,18 +168,13 @@ module Ci
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL.
#
# This method performs a *partial* match on tokens, thus a query for "a"
# will match any runner where the token contains the letter "a". As a result
# you should *not* use this method for non-admin purposes as otherwise users
# might be able to query a list of all runners.
# This method uses ILIKE on PostgreSQL for the description field and performs a full match on tokens.
#
# query - The search query as a String.
#
# Returns an ActiveRecord::Relation.
def self.search(query)
fuzzy_search(query, [:token, :description])
where(token: query).or(fuzzy_search(query, [:description]))
end
def self.online_contact_time_deadline

View File

@ -147,23 +147,15 @@ class Packages::Package < ApplicationRecord
scope :order_by_package_file, -> { joins(:package_files).order('packages_package_files.created_at ASC') }
scope :order_project_path, -> do
if Feature.enabled?(:arel_package_scopes)
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc)
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :asc)
joins(:project).reorder(keyset_order)
else
joins(:project).reorder('projects.path ASC, id ASC')
end
joins(:project).reorder(keyset_order)
end
scope :order_project_path_desc, -> do
if Feature.enabled?(:arel_package_scopes)
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :desc)
keyset_order = keyset_pagination_order(join_class: Project, column_name: :path, direction: :desc)
joins(:project).reorder(keyset_order)
else
joins(:project).reorder('projects.path DESC, id DESC')
end
joins(:project).reorder(keyset_order)
end
after_commit :update_composer_cache, on: :destroy, if: -> { composer? }

View File

@ -6,7 +6,7 @@
%a.gl-sr-only.gl-accessibility{ href: "#content-body" } Skip to content
.container-fluid
.header-content
.title-container{ class: ('hide-when-menu-expanded' if !use_top_nav_redesign) }
.title-container.hide-when-menu-expanded
%h1.title
%span.gl-sr-only GitLab
= link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
@ -33,12 +33,13 @@
%ul.nav.navbar-nav
- if current_user
= render 'layouts/header/new_dropdown', class: ('gl-display-none gl-sm-display-block' if use_top_nav_redesign)
- if header_link?(:search)
- if top_nav_show_search
- search_menu_item = top_nav_search_menu_item_attrs
%li.nav-item.d-none.d-lg-block.m-auto
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item{ class: use_top_nav_redesign ? "gl-display-none gl-sm-display-inline-block gl-lg-display-none" : "gl-display-inline-block gl-lg-display-none" }
= link_to search_context.search_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('search')
= link_to search_menu_item.fetch(:href), title: search_menu_item.fetch(:title), aria: { label: search_menu_item.fetch(:title) }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon(search_menu_item.fetch(:icon))
- if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') },
@ -120,9 +121,9 @@
%button.navbar-toggler.d-block.d-sm-none{ type: 'button', class: ('gl-border-none!' if use_top_nav_redesign) }
%span.sr-only= _('Toggle navigation')
- if use_top_nav_redesign
%span.more-icon.gl-px-3
%span.more-icon.gl-px-3.gl-font-sm.gl-font-weight-bold
%span.gl-pr-2= _('Menu')
= sprite_icon('dot-grid', size: 16)
= sprite_icon('hamburger', size: 16)
- else
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon')
= sprite_icon('close', size: 12, css_class: 'close-icon')

View File

@ -1,7 +1,7 @@
- return unless Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
- top_class = local_assigns.fetch(:class, nil)
- view_model = top_nav_view_model(project: @project, group: @group)
- view_model = top_nav_responsive_view_model(project: @project, group: @group)
.top-nav-responsive{ class: top_class }
#js-top-nav-responsive{ data: { view_model: view_model.to_json } }

View File

@ -3,10 +3,10 @@
had
= failed.size
failed
#{'build'.pluralize(failed.size)}.
#{'job'.pluralize(failed.size)}.
%tr.table-warning
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" }
Failed builds
Failed jobs
%tr.section
%td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" }
%table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" }

View File

@ -28,7 +28,7 @@ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
<% end -%>
<% failed = @pipeline.latest_statuses.failed -%>
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
had <%= failed.size %> failed <%= 'job'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>

View File

@ -312,11 +312,33 @@ module Gitlab
end
# Cross-origin requests must be enabled for the Authorization code with PKCE OAuth flow when used from a browser.
%w(/oauth/token /oauth/revoke).each do |oauth_path|
allow do
origins '*'
resource oauth_path,
headers: %w(Authorization),
credentials: false,
methods: %i(post)
end
end
# These are routes from doorkeeper-openid_connect:
# https://github.com/doorkeeper-gem/doorkeeper-openid_connect#routes
allow do
origins '*'
resource '/oauth/token',
resource '/oauth/userinfo',
headers: %w(Authorization),
credentials: false,
methods: [:post]
methods: %i(get head post)
end
%w(/oauth/discovery/keys /.well-known/openid-configuration /.well-known/webfinger).each do |openid_path|
allow do
origins '*'
resource openid_path,
credentials: false,
methods: %i(get head)
end
end
end

View File

@ -1,8 +0,0 @@
---
name: arel_package_scopes
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62042
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331306
milestone: '13.12'
type: development
group: group::package
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330586
milestone: '13.12'
type: development
group: group::optimize
default_enabled: false
default_enabled: true

View File

@ -1,16 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_close_issue_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Number of users closing Jira issues by month
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_cross_reference_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Number of users that cross-referenced Jira issues by month
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +0,0 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_list_issues_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ce
tier:
- free
skip_validation: true

View File

@ -1,16 +0,0 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_create_issue_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ce
tier:
- free
skip_validation: true

View File

@ -1,16 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Number of users performing actions on Jira issues by month
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -0,0 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_close_issue_weekly
description: Number of users closing Jira issues by week
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.i_ecosystem_jira_service_cross_reference_weekly
description: Number of users that cross-referenced Jira issues by week
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,18 @@
---
key_path: redis_hll_counters.ecosystem.ecosystem_total_unique_counts_weekly
description: Number of users performing actions on Jira issues by week
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.projects_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: 'Count of projects with active integrations for Datadog'
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.groups_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of groups with active integrations for Datadog
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.templates_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active service templates for Datadog
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.instances_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active instance-level integrations for Datadog
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.projects_inheriting_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active projects inheriting integrations for Datadog
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.groups_inheriting_datadog_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active groups inheriting integrations for Datadog
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.projects_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: 'Count of projects with active integrations for EWM'
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.groups_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of groups with active integrations for EWM
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.templates_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active service templates for EWM
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.instances_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active instance-level integrations for EWM
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.projects_inheriting_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active projects inheriting integrations for EWM
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,18 @@
---
key_path: counts.groups_inheriting_ewm_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active groups inheriting integrations for EWM
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.projects_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of projects with active integrations for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.groups_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of groups with active integrations for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.templates_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active service templates for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.instances_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active instance-level integrations for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.projects_inheriting_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active projects inheriting integrations for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.groups_inheriting_mock_ci_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active groups inheriting integrations for Mock CI
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.projects_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of projects with active integrations for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.groups_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of groups with active integrations for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.templates_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active service templates for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.instances_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active instance-level integrations for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.projects_inheriting_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active projects inheriting integrations for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -1,16 +1,19 @@
---
key_path: counts.groups_inheriting_mock_monitoring_active
description: ''
product_section: ''
product_stage: ''
product_group: ''
product_category: ''
description: Count of active groups inheriting integrations for Mock Monitoring
product_section: dev
product_stage: create
product_group: group::ecosystem
product_category: integrations
value_type: number
status: data_available
status: removed
milestone_removed: '13.12'
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate

View File

@ -44,6 +44,14 @@ Rails.application.routes.draw do
draw :oauth
use_doorkeeper_openid_connect
# Add OPTIONS method for CORS preflight requests
match '/oauth/userinfo' => 'doorkeeper/openid_connect/userinfo#show', via: :options
match '/oauth/discovery/keys' => 'doorkeeper/openid_connect/discovery#keys', via: :options
match '/.well-known/openid-configuration' => 'doorkeeper/openid_connect/discovery#provider', via: :options
match '/.well-known/webfinger' => 'doorkeeper/openid_connect/discovery#webfinger', via: :options
match '/oauth/token' => 'oauth/tokens#create', via: :options
match '/oauth/revoke' => 'oauth/tokens#revoke', via: :options
# Sign up
scope path: '/users/sign_up', module: :registrations, as: :users_sign_up do

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddRunnersDescriptionIndex < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_ci_runners_on_description_trigram'
disable_ddl_transaction!
def up
add_concurrent_index :ci_runners, :description, name: INDEX_NAME, using: :gin, opclass: { description: :gin_trgm_ops }
end
def down
remove_concurrent_index_by_name :ci_runners, INDEX_NAME
end
end

View File

@ -0,0 +1 @@
96c70de2567fc3e816c720ed6e4cef2446c0f0ee288d0959cd1298523913077f

View File

@ -22921,6 +22921,8 @@ CREATE INDEX index_ci_runner_projects_on_runner_id ON ci_runner_projects USING b
CREATE INDEX index_ci_runners_on_contacted_at ON ci_runners USING btree (contacted_at);
CREATE INDEX index_ci_runners_on_description_trigram ON ci_runners USING gin (description gin_trgm_ops);
CREATE INDEX index_ci_runners_on_locked ON ci_runners USING btree (locked);
CREATE INDEX index_ci_runners_on_runner_type ON ci_runners USING btree (runner_type);

View File

@ -7,17 +7,14 @@ type: howto
# Disaster Recovery (Geo) **(PREMIUM SELF)**
Geo replicates your database, your Git repositories, and few other assets.
We will support and replicate more data in the future, that will enable you to
failover with minimal effort, in a disaster situation.
See [Geo limitations](../index.md#limitations) for more information.
Geo replicates your database, your Git repositories, and few other assets,
but there are some [limitations](../index.md#limitations).
WARNING:
Disaster recovery for multi-secondary configurations is in **Alpha**.
For the latest updates, check the [Disaster Recovery epic for complete maturity](https://gitlab.com/groups/gitlab-org/-/epics/3574).
Multi-secondary configurations require the complete re-synchronization and re-configuration of all non-promoted secondaries and
will cause downtime.
causes downtime.
## Promoting a **secondary** Geo node in single-secondary configurations
@ -91,7 +88,7 @@ Note the following when promoting a secondary:
before proceeding. If the secondary node
[has been paused](../../geo/index.md#pausing-and-resuming-replication), the promotion
performs a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
Data that was created on the primary while the secondary was paused is lost.
- A new **secondary** should not be added at this time. If you want to add a new
**secondary**, do this after you have completed the entire process of promoting
the **secondary** to the **primary**.
@ -497,7 +494,7 @@ must disable the **primary** site:
WARNING:
If the secondary site [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost.
Data that was created on the primary while the secondary was paused is lost.
1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
promote to read-write:
@ -513,7 +510,7 @@ Data that was created on the primary while the secondary was paused will be lost
`geo_secondary_role`:
NOTE:
Depending on your architecture these steps will need to be run on any GitLab node that is external to the **secondary** Kubernetes cluster.
Depending on your architecture, these steps need to run on any GitLab node that is external to the **secondary** Kubernetes cluster.
```ruby
## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
@ -547,7 +544,7 @@ Data that was created on the primary while the secondary was paused will be lost
helm --namespace gitlab get values gitlab-geo > gitlab.yaml
```
The existing configuration will contain a section for Geo that should resemble:
The existing configuration contains a section for Geo that should resemble:
```yaml
geo:
@ -564,7 +561,7 @@ Data that was created on the primary while the secondary was paused will be lost
To promote the **secondary** cluster to a **primary** cluster, update `role: secondary` to `role: primary`.
You can remove the entire `psql` section if the cluster will remain as a primary site, this refers to the tracking database and will be ignored whilst the cluster is acting as a primary site.
If the cluster remains as a primary site, you can remove the entire `psql` section; it refers to the tracking database and is ignored whilst the cluster is acting as a primary site.
Update the cluster with the new configuration:

View File

@ -4,12 +4,13 @@ group: Ecosystem
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Getting started with GitLab GraphQL API
# Get started with GitLab GraphQL API **(FREE)**
This guide demonstrates basic usage of the GitLab GraphQL API.
See the [GraphQL API style guide](../../development/api_graphql_styleguide.md) for implementation details
aimed at developers who wish to work on developing the API itself.
Read the [GraphQL API style guide](../../development/api_graphql_styleguide.md)
for implementation details aimed at developers who wish to work on developing
the API itself.
## Running examples
@ -20,10 +21,11 @@ The examples documented here can be run using:
### Command line
You can run GraphQL queries in a `curl` request on the command line on your local machine.
A GraphQL request can be made as a `POST` request to `/api/graphql` with the query as the payload.
You can authorize your request by generating a [personal access token](../../user/profile/personal_access_tokens.md)
to use as a bearer token.
You can run GraphQL queries in a `curl` request on the command line on your
local computer. A GraphQL request can be made as a `POST` request to `/api/graphql`
with the query as the payload. You can authorize your request by generating a
[personal access token](../../user/profile/personal_access_tokens.md) to use as
a bearer token.
Example:
@ -36,24 +38,26 @@ curl "https://gitlab.com/api/graphql" --header "Authorization: Bearer $GRAPHQL_T
### GraphiQL
GraphiQL (pronounced "graphical") allows you to run queries directly against the server endpoint
with syntax highlighting and autocomplete. It also allows you to explore the schema and types.
GraphiQL (pronounced "graphical") allows you to run queries directly against
the server endpoint with syntax highlighting and autocomplete. It also allows
you to explore the schema and types.
The examples below:
- Can be run directly against GitLab 11.0 or later, though some of the types and fields
may not be supported in older versions.
- Works against GitLab.com without any further setup. Make sure you are signed in and
navigate to the [GraphiQL Explorer](https://gitlab.com/-/graphql-explorer).
- Can be run directly against GitLab 11.0 or later, though some of the types
and fields may not be supported in older versions.
- Works against GitLab.com without any further setup. Make sure you are signed
in and navigate to the [GraphiQL Explorer](https://gitlab.com/-/graphql-explorer).
If you want to run the queries locally, or on a self-managed instance,
you must either:
If you want to run the queries locally, or on a self-managed instance, you must
either:
- Create the `gitlab-org` group with a project called `graphql-sandbox` under it. Create
several issues within the project.
- Edit the queries to replace `gitlab-org/graphql-sandbox` with your own group and project.
- Create the `gitlab-org` group with a project called `graphql-sandbox` under
it. Create several issues in the project.
- Edit the queries to replace `gitlab-org/graphql-sandbox` with your own group
and project.
Please refer to [running GraphiQL](index.md#graphiql) for more information.
Refer to [running GraphiQL](index.md#graphiql) for more information.
NOTE:
If you are running GitLab 11.0 to 12.0, enable the `graphql`
@ -74,8 +78,8 @@ which is an object identifier in the format of `"gid://gitlab/Issue/123"`.
[GitLab GraphQL Schema](reference/index.md) outlines which objects and fields are
available for clients to query and their corresponding data types.
Example: Get only the names of all the projects the currently logged in user can access (up to a limit, more on that later)
in the group `gitlab-org`.
Example: Get only the names of all the projects the currently logged in user can
access (up to a limit) in the group `gitlab-org`.
```graphql
query {
@ -108,12 +112,12 @@ query {
When retrieving child nodes use:
- the `edges { node { } }` syntax.
- the short form `nodes { }` syntax.
- The `edges { node { } }` syntax.
- The short form `nodes { }` syntax.
Underneath it all is a graph we are traversing, hence the name GraphQL.
Example: Get a project (only its name) and the titles of all its issues.
Example: Get the name of a project, and the titles of all its issues.
```graphql
query {
@ -130,23 +134,24 @@ query {
```
More about queries:
[GraphQL docs](https://graphql.org/learn/queries/)
[GraphQL documentation](https://graphql.org/learn/queries/)
### Authorization
Authorization uses the same engine as the GitLab application (and GitLab.com). So if you've signed in to GitLab
and use GraphiQL, all queries are performed as you, the signed in user. For more information, see the
Authorization uses the same engine as the GitLab application (and GitLab.com).
If you've signed in to GitLab and use GraphiQL, all queries are performed as
you, the signed in user. For more information, read the
[GitLab API documentation](../README.md#authentication).
### Mutations
Mutations make changes to data. We can update, delete, or create new records. Mutations
generally use InputTypes and variables, neither of which appear here.
Mutations make changes to data. We can update, delete, or create new records.
Mutations generally use InputTypes and variables, neither of which appear here.
Mutations have:
- Inputs. For example, arguments, such as which emoji you'd like to award,
and to which object.
and to which object.
- Return statements. That is, what you'd like to get back when it's successful.
- Errors. Always ask for what went wrong, just in case.
@ -174,8 +179,9 @@ mutation {
}
```
Example: Add a comment to the issue (we're using the ID of the `GitLab.com` issue - but
if you're using a local instance, you must get the ID of an issue you can write to).
Example: Add a comment to the issue. In this example, we use the ID of the
`GitLab.com` issue. If you're using a local instance, you must get the ID of an
issue you can write to.
```graphql
mutation {
@ -196,7 +202,8 @@ mutation {
#### Update mutations
When you see the result `id` of the note you created - take a note of it. Now let's edit it to sip faster!
When you see the result `id` of the note you created, take a note of it. Let's
edit it to sip faster.
```graphql
mutation {
@ -214,7 +221,7 @@ mutation {
#### Deletion mutations
Let's delete the comment, since our tea is all gone.
Let's delete the comment, because our tea is all gone.
```graphql
mutation {
@ -244,16 +251,18 @@ You should get something like the following output:
We've asked for the note details, but it doesn't exist anymore, so we get `null`.
More about mutations:
[GraphQL Docs](https://graphql.org/learn/queries/#mutations).
[GraphQL Documentation](https://graphql.org/learn/queries/#mutations).
### Introspective queries
Clients can query the GraphQL endpoint for information about its own schema.
by making an [introspective query](https://graphql.org/learn/introspection/).
The [GraphiQL Query Explorer](https://gitlab.com/-/graphql-explorer) uses an
introspection query to:
It is through an introspection query that the [GraphiQL Query Explorer](https://gitlab.com/-/graphql-explorer)
gets all of its knowledge about our GraphQL schema to do autocompletion and provide
its interactive `Docs` tab.
- Gain knowledge about our GraphQL schema.
- Do autocompletion.
- Provide its interactive `Docs` tab.
Example: Get all the type names in the schema.
@ -267,8 +276,8 @@ Example: Get all the type names in the schema.
}
```
Example: Get all the fields associated with Issue.
`kind` tells us the enum value for the type, like `OBJECT`, `SCALAR` or `INTERFACE`.
Example: Get all the fields associated with Issue. `kind` tells us the enum
value for the type, like `OBJECT`, `SCALAR` or `INTERFACE`.
```graphql
query IssueTypes {
@ -287,12 +296,12 @@ query IssueTypes {
```
More about introspection:
[GraphQL docs](https://graphql.org/learn/introspection/)
[GraphQL documentation](https://graphql.org/learn/introspection/)
## Sorting
Some of the GitLab GraphQL endpoints allow you to specify how you'd like a collection of
objects to be sorted. You can only sort by what the schema allows you to.
Some of the GitLab GraphQL endpoints allow you to specify how to sort a
collection of objects. You can only sort by what the schema allows you to.
Example: Issues can be sorted by creation date:
@ -312,17 +321,18 @@ query {
## Pagination
Pagination is a way of only asking for a subset of the records (say, the first 10).
If we want more of them, we can make another request for the next 10 from the server
(in the form of something like "please give me the next 10 records").
Pagination is a way of only asking for a subset of the records, such as the
first ten. If we want more of them, we can make another request for the next
ten from the server in the form of something like `please give me the next ten records`.
By default, the GitLab GraphQL API returns 100 records per page.
This can be changed by using `first` or `last` arguments. Both arguments take a value,
so `first: 10` returns the first 10 records, and `last: 10` the last 10 records.
There is a limit on how many records will be returned per page, which is generally `100`.
By default, the GitLab GraphQL API returns 100 records per page. To change this
behavior, use `first` or `last` arguments. Both arguments take a value, so
`first: 10` returns the first ten records, and `last: 10` the last ten records.
There is a limit on how many records are returned per page, which is generally
`100`.
Example: Retrieve only the first 2 issues (slicing). The `cursor` field gives us a position from which
we can retrieve further records relative to that one.
Example: Retrieve only the first two issues (slicing). The `cursor` field gives
us a position from which we can retrieve further records relative to that one.
```graphql
query {
@ -343,9 +353,10 @@ query {
}
```
Example: Retrieve the next 3. (The cursor value
Example: Retrieve the next three. (The cursor value
`eyJpZCI6IjI3MDM4OTMzIiwiY3JlYXRlZF9hdCI6IjIwMTktMTEtMTQgMDU6NTY6NDQgVVRDIn0`
could be different, but it's the `cursor` value returned for the second issue returned above.)
could be different, but it's the `cursor` value returned for the second issue
returned above.)
```graphql
query {
@ -367,5 +378,5 @@ query {
}
```
More on pagination and cursors:
[GraphQL docs](https://graphql.org/learn/pagination/)
More about pagination and cursors:
[GraphQL documentation](https://graphql.org/learn/pagination/)

View File

@ -336,6 +336,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="queryrunnerssearch"></a>`search` | [`String`](#string) | Filter by full token or partial text in description field. |
| <a id="queryrunnerssort"></a>`sort` | [`CiRunnerSort`](#cirunnersort) | Sort order of results. |
| <a id="queryrunnersstatus"></a>`status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. |
| <a id="queryrunnerstaglist"></a>`tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). |
@ -1227,6 +1228,10 @@ Input type: `CreateIssueInput`
### `Mutation.createIteration`
WARNING:
**Deprecated** in 14.0.
Use iterationCreate.
Input type: `CreateIterationInput`
#### Arguments
@ -1237,6 +1242,7 @@ Input type: `CreateIterationInput`
| <a id="mutationcreateiterationdescription"></a>`description` | [`String`](#string) | The description of the iteration. |
| <a id="mutationcreateiterationduedate"></a>`dueDate` | [`String`](#string) | The end date of the iteration. |
| <a id="mutationcreateiterationgrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group with which the resource is associated. |
| <a id="mutationcreateiterationiterationscadenceid"></a>`iterationsCadenceId` | [`IterationsCadenceID`](#iterationscadenceid) | Global ID of the iterations cadence to be assigned to newly created iteration. |
| <a id="mutationcreateiterationprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project with which the resource is associated. |
| <a id="mutationcreateiterationstartdate"></a>`startDate` | [`String`](#string) | The start date of the iteration. |
| <a id="mutationcreateiterationtitle"></a>`title` | [`String`](#string) | The title of the iteration. |
@ -2657,6 +2663,31 @@ Input type: `IterationCadenceUpdateInput`
| <a id="mutationiterationcadenceupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationcadenceupdateiterationcadence"></a>`iterationCadence` | [`IterationCadence`](#iterationcadence) | The updated iteration cadence. |
### `Mutation.iterationCreate`
Input type: `iterationCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationcreatedescription"></a>`description` | [`String`](#string) | The description of the iteration. |
| <a id="mutationiterationcreateduedate"></a>`dueDate` | [`String`](#string) | The end date of the iteration. |
| <a id="mutationiterationcreategrouppath"></a>`groupPath` | [`ID`](#id) | Full path of the group with which the resource is associated. |
| <a id="mutationiterationcreateiterationscadenceid"></a>`iterationsCadenceId` | [`IterationsCadenceID`](#iterationscadenceid) | Global ID of the iterations cadence to be assigned to newly created iteration. |
| <a id="mutationiterationcreateprojectpath"></a>`projectPath` | [`ID`](#id) | Full path of the project with which the resource is associated. |
| <a id="mutationiterationcreatestartdate"></a>`startDate` | [`String`](#string) | The start date of the iteration. |
| <a id="mutationiterationcreatetitle"></a>`title` | [`String`](#string) | The title of the iteration. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationiterationcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationiterationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationiterationcreateiteration"></a>`iteration` | [`Iteration`](#iteration) | The created iteration. |
### `Mutation.iterationDelete`
Input type: `IterationDeleteInput`

View File

@ -725,6 +725,28 @@ Example response:
}
```
### Download a Group avatar
Get a group avatar. This endpoint can be accessed without authentication if the
group is publicly accessible.
```plaintext
GET /groups/:id/avatar
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | --------------------- |
| `id` | integer/string | yes | ID of the group |
Example:
```shell
curl --header "PRIVATE-TOKEN: $GITLAB_LOCAL_TOKEN" \
--remote-header-name \
--remote-name \
"https://gitlab.example.com/api/v4/groups/4/avatar"
```
### Disable the results limit **(FREE SELF)**
The 100 results limit can break integrations developed using GitLab 12.4 and earlier.

View File

@ -41,6 +41,7 @@ GET /runners?scope=active
GET /runners?type=project_type
GET /runners?status=active
GET /runners?tag_list=tag1,tag2
GET /runners?search=gitlab
```
| Attribute | Type | Required | Description |
@ -49,6 +50,7 @@ GET /runners?tag_list=tag1,tag2
| `type` | string | no | The type of runners to show, one of: `instance_type`, `group_type`, `project_type` |
| `status` | string | no | The status of runners to show, one of: `active`, `paused`, `online`, `offline` |
| `tag_list` | string array | no | List of the runner's tags |
| `search` | string | no | The full token or partial description text to match |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners"

View File

@ -1296,15 +1296,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_datadog_active`
Missing description
Count of groups with active integrations for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182549_groups_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_discord_active`
@ -1344,15 +1344,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_ewm_active`
Missing description
Count of groups with active integrations for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182616_groups_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_external_wiki_active`
@ -1512,15 +1512,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_datadog_active`
Missing description
Count of active groups inheriting integrations for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182557_groups_inheriting_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_discord_active`
@ -1560,15 +1560,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_ewm_active`
Missing description
Count of active groups inheriting integrations for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182623_groups_inheriting_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_external_wiki_active`
@ -1704,27 +1704,27 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_mock_ci_active`
Missing description
Count of active groups inheriting integrations for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182732_groups_inheriting_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_mock_monitoring_active`
Missing description
Count of active groups inheriting integrations for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182743_groups_inheriting_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_inheriting_packagist_active`
@ -1944,27 +1944,27 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.groups_mock_ci_active`
Missing description
Count of groups with active integrations for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182724_groups_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_mock_monitoring_active`
Missing description
Count of groups with active integrations for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182736_groups_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.groups_packagist_active`
@ -2652,15 +2652,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.instances_datadog_active`
Missing description
Count of active instance-level integrations for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182553_instances_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.instances_discord_active`
@ -2700,15 +2700,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.instances_ewm_active`
Missing description
Count of active instance-level integrations for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182620_instances_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.instances_external_wiki_active`
@ -2844,27 +2844,27 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.instances_mock_ci_active`
Missing description
Count of active instance-level integrations for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182728_instances_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.instances_mock_monitoring_active`
Missing description
Count of active instance-level integrations for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182739_instances_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.instances_packagist_active`
@ -4308,15 +4308,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.projects_datadog_active`
Missing description
Count of projects with active integrations for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182547_projects_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_discord_active`
@ -4356,15 +4356,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.projects_ewm_active`
Missing description
Count of projects with active integrations for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182614_projects_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_external_wiki_active`
@ -4536,15 +4536,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_datadog_active`
Missing description
Count of active projects inheriting integrations for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182555_projects_inheriting_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_discord_active`
@ -4584,15 +4584,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_ewm_active`
Missing description
Count of active projects inheriting integrations for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182622_projects_inheriting_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_external_wiki_active`
@ -4728,27 +4728,27 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_mock_ci_active`
Missing description
Count of active projects inheriting integrations for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182730_projects_inheriting_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_mock_monitoring_active`
Missing description
Count of active projects inheriting integrations for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182741_projects_inheriting_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_inheriting_packagist_active`
@ -5040,27 +5040,27 @@ Tiers: `premium`, `ultimate`
### `counts.projects_mock_ci_active`
Missing description
Count of projects with active integrations for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182722_projects_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_mock_monitoring_active`
Missing description
Count of projects with active integrations for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182734_projects_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.projects_packagist_active`
@ -5988,15 +5988,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.templates_datadog_active`
Missing description
Count of active service templates for Datadog
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182551_templates_datadog_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.templates_discord_active`
@ -6036,15 +6036,15 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.templates_ewm_active`
Missing description
Count of active service templates for EWM
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182618_templates_ewm_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.templates_external_wiki_active`
@ -6180,27 +6180,27 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.templates_mock_ci_active`
Missing description
Count of active service templates for Mock CI
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182726_templates_mock_ci_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.templates_mock_monitoring_active`
Missing description
Count of active service templates for Mock Monitoring
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216182738_templates_mock_monitoring_active.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Status: `removed`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `counts.templates_packagist_active`
@ -10440,123 +10440,123 @@ Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly`
Missing description
Number of users performing actions on Jira issues by month
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184957_ecosystem_total_unique_counts_monthly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.ecosystem_total_unique_counts_weekly`
Missing description
Number of users performing actions on Jira issues by week
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216184955_ecosystem_total_unique_counts_weekly.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216184955_ecosystem_total_unique_counts_weekly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers:
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_close_issue_monthly`
Missing description
Number of users closing Jira issues by month
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184941_i_ecosystem_jira_service_close_issue_monthly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_close_issue_weekly`
Missing description
Number of users closing Jira issues by week
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216184939_i_ecosystem_jira_service_close_issue_weekly.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216184939_i_ecosystem_jira_service_close_issue_weekly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers:
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_create_issue_monthly`
Missing description
Number of users creating Jira issues by month
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184953_i_ecosystem_jira_service_create_issue_monthly.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210216184953_i_ecosystem_jira_service_create_issue_monthly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_create_issue_weekly`
Missing description
Number of users creating Jira issues by week
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216184951_i_ecosystem_jira_service_create_issue_weekly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers:
Tiers: `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_cross_reference_monthly`
Missing description
Number of users that cross-referenced Jira issues by month
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184945_i_ecosystem_jira_service_cross_reference_monthly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_cross_reference_weekly`
Missing description
Number of users that cross-referenced Jira issues by week
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216184943_i_ecosystem_jira_service_cross_reference_weekly.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_7d/20210216184943_i_ecosystem_jira_service_cross_reference_weekly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers:
Tiers: `free`, `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_list_issues_monthly`
Missing description
Count of Jira Issue List visits by month
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_28d/20210216184949_i_ecosystem_jira_service_list_issues_monthly.yml)
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210216184949_i_ecosystem_jira_service_list_issues_monthly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers: `free`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_jira_service_list_issues_weekly`
Missing description
Count of Jira Issue List visits by week
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210216184947_i_ecosystem_jira_service_list_issues_weekly.yml)
Group: ``
Group: `group::ecosystem`
Status: `data_available`
Tiers:
Tiers: `premium`, `ultimate`
### `redis_hll_counters.ecosystem.i_ecosystem_slack_service_confidential_issue_notification_monthly`
@ -16260,6 +16260,18 @@ Status: `data_available`
Tiers: `premium`, `ultimate`
### `usage_activity_by_stage.enablement.counts.geo_node_usage.git_push_event_count_weekly`
Number of Git push events from Prometheus on the Geo secondary
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210604110603_git_push_event_count_weekly.yml)
Group: `group::geo`
Status: `data_available`
Tiers: `premium`, `ultimate`
### `usage_activity_by_stage.enablement.geo_secondary_web_oauth_users`
Missing description

View File

@ -40,14 +40,17 @@ collected before this feature is available.
The DevOps Adoption tab shows you which groups within your organization are using the most essential features of GitLab:
- Approvals
- Code owners
- Deployments
- Issues
- Merge Requests
- Pipelines
- Runners
- Scans
- Dev
- Issues
- Merge Requests
- Approvals
- Code owners
- Sec
- Scans
- Ops
- Runners
- Pipelines
- Deployments
Buttons to manage your groups appear in the DevOps Adoption section of the page.
@ -57,6 +60,8 @@ DevOps Adoption allows you to:
- Identify specific groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
- Find the groups that have adopted certain features and can provide guidance to other groups on how to use those features.
![DevOps Report](img/admin_devops_adoption_v14_0.png)
### Disable or enable DevOps Adoption
DevOps Adoption is deployed behind a feature flag that is **enabled by default**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -24,14 +24,17 @@ To access Group DevOps Adoption, go to your group and select **Analytics > DevOp
Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features:
- Approvals
- Code owners
- Deployments
- Issues
- Merge Requests
- Pipelines
- Runners
- Scans
- Dev
- Issues
- Merge Requests
- Approvals
- Code owners
- Sec
- Scans
- Ops
- Runners
- Pipelines
- Deployments
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
button, in the top right hand section of your Groups pages.
@ -42,7 +45,7 @@ With DevOps Adoption you can:
- Identify specific sub-groups that are lagging in their adoption of GitLab so you can help them along in their DevOps journey.
- Find the sub-groups that have adopted certain features and can provide guidance to other sub-groups on how to use those features.
![DevOps Report](img/group_devops_adoption_v13_11.png)
![DevOps Report](img/group_devops_adoption_v14_0.png)
## Enable data processing

View File

@ -173,6 +173,7 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::FreezePeriods
mount ::API::GroupAvatar
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport

21
lib/api/group_avatar.rb Normal file
View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module API
class GroupAvatar < ::API::Base
helpers Helpers::GroupsHelpers
feature_category :subgroups
resource :groups do
desc 'Download the group avatar' do
detail 'This feature was introduced in GitLab 14.0'
end
params do
requires :id, type: String, desc: 'The group id'
end
get ':id/avatar' do
present_carrierwave_file!(user_group.avatar)
end
end
end
end

View File

@ -854,9 +854,6 @@ msgstr ""
msgid "%{state} epics"
msgstr ""
msgid "%{strongStart}Deletes%{strongEnd} source branch"
msgstr ""
msgid "%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}"
msgstr ""
@ -12562,9 +12559,6 @@ msgstr ""
msgid "Environments|Logs from %{start} to %{end}."
msgstr ""
msgid "Environments|Managed apps"
msgstr ""
msgid "Environments|More information"
msgstr ""
@ -32784,6 +32778,9 @@ msgstr ""
msgid "The snippet is visible to any logged in user except external users."
msgstr ""
msgid "The source branch will be deleted"
msgstr ""
msgid "The specified tab is invalid, please select another"
msgstr ""
@ -34369,6 +34366,9 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
msgid "TopNav|Go back"
msgstr ""
msgid "Topics (optional)"
msgstr ""

View File

@ -432,7 +432,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
it 'user cannot remove source branch', :sidekiq_might_not_need_inline do
expect(page).not_to have_field('remove-source-branch-input')
expect(page).to have_content('Deletes source branch')
expect(page).to have_content('The source branch will be deleted')
end
end

View File

@ -6,7 +6,6 @@ RSpec.describe 'top nav responsive', :js do
include MobileHelpers
let_it_be(:user) { create(:user) }
let_it_be(:responsive_menu_text) { 'Placeholder for responsive top nav' }
before do
stub_feature_flags(combined_menu: true)
@ -20,7 +19,9 @@ RSpec.describe 'top nav responsive', :js do
context 'before opened' do
it 'has page content and hides responsive menu', :aggregate_failures do
expect(page).to have_css('.page-title', text: 'Projects')
expect(page).to have_no_text(responsive_menu_text)
expect(page).to have_link('Dashboard', id: 'logo')
expect(page).to have_no_css('.top-nav-responsive')
end
end
@ -31,8 +32,22 @@ RSpec.describe 'top nav responsive', :js do
it 'hides everything and shows responsive menu', :aggregate_failures do
expect(page).to have_no_css('.page-title', text: 'Projects')
expect(page).to have_link('Dashboard', id: 'logo')
expect(page).to have_text(responsive_menu_text)
expect(page).to have_no_link('Dashboard', id: 'logo')
within '.top-nav-responsive' do
expect(page).to have_link(nil, href: search_path)
expect(page).to have_button('Projects')
expect(page).to have_button('Groups')
expect(page).to have_link('Snippets', href: dashboard_snippets_path)
end
end
it 'has new dropdown', :aggregate_failures do
click_button('New...')
expect(page).to have_link('New project', href: new_project_path)
expect(page).to have_link('New group', href: new_group_path)
expect(page).to have_link('New snippet', href: new_snippet_path)
end
end
end

View File

@ -40,7 +40,7 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do
dropdown_items = find(".dropdown-menu").all(".dropdown-item")
expect(dropdown_items.first).to have_content(environment.name)
expect(dropdown_items.size).to eq(3)
expect(dropdown_items.size).to eq(2)
end
end

View File

@ -12,7 +12,6 @@ import {
mockTrace,
mockEnvironmentsEndpoint,
mockDocumentationPath,
mockManagedAppsEndpoint,
} from '../mock_data';
jest.mock('~/lib/utils/scroll_utils');
@ -35,7 +34,7 @@ describe('EnvironmentLogs', () => {
environmentName: mockEnvName,
environmentsPath: mockEnvironmentsEndpoint,
clusterApplicationsDocumentationPath: mockDocumentationPath,
clustersPath: mockManagedAppsEndpoint,
clustersPath: '/gitlab-org',
};
const updateControlBtnsMock = jest.fn();

View File

@ -7,8 +7,6 @@ export const mockDocumentationPath = '/documentation.md';
export const mockLogsEndpoint = '/dummy_logs_path.json';
export const mockCursor = 'MOCK_CURSOR';
export const mockNextCursor = 'MOCK_NEXT_CURSOR';
export const mockManagedAppName = 'kubernetes-cluster-1';
export const mockManagedAppsEndpoint = `${mockProjectPath}/clusters.json`;
const makeMockEnvironment = (id, name, advancedQuerying) => ({
id,
@ -25,31 +23,6 @@ export const mockEnvironments = [
makeMockEnvironment(102, 'review/a-feature', false),
];
export const mockManagedApps = [
{
cluster_type: 'project_type',
enabled: true,
environment_scope: '*',
name: 'kubernetes-cluster-1',
provider_type: 'user',
status: 'connected',
path: '/root/autodevops-deploy/-/clusters/15',
gitlab_managed_apps_logs_path: '/root/autodevops-deploy/-/logs?cluster_id=15',
enable_advanced_logs_querying: true,
},
{
cluster_type: 'project_type',
enabled: true,
environment_scope: '*',
name: 'kubernetes-cluster-2',
provider_type: 'user',
status: 'connected',
path: '/root/autodevops-deploy/-/clusters/16',
gitlab_managed_apps_logs_path: null,
enable_advanced_logs_querying: false,
},
];
export const mockPodName = 'production-764c58d697-aaaaa';
export const mockPods = [
mockPodName,

View File

@ -11,7 +11,6 @@ import {
fetchEnvironments,
fetchLogs,
fetchMoreLogsPrepend,
fetchManagedApps,
} from '~/logs/stores/actions';
import * as types from '~/logs/stores/mutation_types';
import logsPageState from '~/logs/stores/state';
@ -31,8 +30,6 @@ import {
mockResponse,
mockCursor,
mockNextCursor,
mockManagedApps,
mockManagedAppsEndpoint,
} from '../mock_data';
jest.mock('~/flash');
@ -219,30 +216,6 @@ describe('Logs Store actions', () => {
});
});
describe('fetchManagedApps', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
it('should commit RECEIVE_MANAGED_APPS_DATA_SUCCESS mutation on succesful fetch', () => {
mock.onGet(mockManagedAppsEndpoint).replyOnce(200, { clusters: mockManagedApps });
return testAction(fetchManagedApps, mockManagedAppsEndpoint, state, [
{ type: types.RECEIVE_MANAGED_APPS_DATA_SUCCESS, payload: mockManagedApps },
]);
});
it('should commit RECEIVE_MANAGED_APPS_DATA_ERROR on wrong data', () => {
mock.onGet(mockManagedAppsEndpoint).replyOnce(500);
return testAction(
fetchManagedApps,
mockManagedAppsEndpoint,
state,
[{ type: types.RECEIVE_MANAGED_APPS_DATA_ERROR }],
[],
);
});
});
describe('when the backend responds succesfully', () => {
let expectedMutations;
let expectedActions;

View File

@ -1,14 +1,7 @@
import { trace, showAdvancedFilters } from '~/logs/stores/getters';
import logsPageState from '~/logs/stores/state';
import {
mockLogsResult,
mockTrace,
mockEnvName,
mockEnvironments,
mockManagedApps,
mockManagedAppName,
} from '../mock_data';
import { mockLogsResult, mockTrace, mockEnvName, mockEnvironments } from '../mock_data';
describe('Logs Store getters', () => {
let state;
@ -79,43 +72,4 @@ describe('Logs Store getters', () => {
});
});
});
describe('when no managedApps are set', () => {
beforeEach(() => {
state.environments.current = null;
state.environments.options = [];
state.managedApps.current = mockManagedAppName;
state.managedApps.options = [];
});
it('returns false', () => {
expect(showAdvancedFilters(state)).toBe(false);
});
});
describe('when the managedApp supports filters', () => {
beforeEach(() => {
state.environments.current = null;
state.environments.options = mockEnvironments;
state.managedApps.current = mockManagedAppName;
state.managedApps.options = mockManagedApps;
});
it('returns true', () => {
expect(showAdvancedFilters(state)).toBe(true);
});
});
describe('when the managedApp does not support filters', () => {
beforeEach(() => {
state.environments.current = null;
state.environments.options = mockEnvironments;
state.managedApps.options = mockManagedApps;
state.managedApps.current = mockManagedApps[1].name;
});
it('returns false', () => {
expect(showAdvancedFilters(state)).toBe(false);
});
});
});

View File

@ -11,8 +11,6 @@ import {
mockSearch,
mockCursor,
mockNextCursor,
mockManagedApps,
mockManagedAppName,
} from '../mock_data';
describe('Logs Store Mutations', () => {
@ -32,15 +30,6 @@ describe('Logs Store Mutations', () => {
it('sets the environment', () => {
mutations[types.SET_PROJECT_ENVIRONMENT](state, mockEnvName);
expect(state.environments.current).toEqual(mockEnvName);
expect(state.managedApps.current).toBe(null);
});
});
describe('SET_MANAGED_APP', () => {
it('sets the managed app', () => {
mutations[types.SET_MANAGED_APP](state, mockManagedAppName);
expect(state.managedApps.current).toBe(mockManagedAppName);
expect(state.environments.current).toBe(null);
});
});
@ -265,29 +254,4 @@ describe('Logs Store Mutations', () => {
);
});
});
describe('RECEIVE_MANAGED_APPS_DATA_SUCCESS', () => {
it('receives managed apps data success', () => {
expect(state.managedApps.options).toEqual([]);
mutations[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, mockManagedApps);
expect(state.managedApps.options.length).toEqual(1);
expect(state.managedApps.options).toEqual([mockManagedApps[0]]);
expect(state.managedApps.isLoading).toBe(false);
});
});
describe('RECEIVE_MANAGED_APPS_DATA_ERROR', () => {
it('received managed apps data error', () => {
mutations[types.RECEIVE_MANAGED_APPS_DATA_ERROR](state);
expect(state.managedApps).toEqual({
options: [],
isLoading: false,
current: null,
fetchError: true,
});
});
});
});

View File

@ -1,7 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { range } from 'lodash';
import ResponsiveApp from '~/nav/components/responsive_app.vue';
import ResponsiveHeader from '~/nav/components/responsive_header.vue';
import ResponsiveHome from '~/nav/components/responsive_home.vue';
import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
import eventHub, { EVENT_RESPONSIVE_TOGGLE } from '~/nav/event_hub';
import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import { TEST_NAV_DATA } from '../mock_data';
describe('~/nav/components/responsive_app.vue', () => {
@ -12,11 +17,19 @@ describe('~/nav/components/responsive_app.vue', () => {
propsData: {
navData: TEST_NAV_DATA,
},
stubs: {
KeepAliveSlots,
},
});
};
const triggerResponsiveToggle = () => eventHub.$emit(EVENT_RESPONSIVE_TOGGLE);
const findHome = () => wrapper.findComponent(ResponsiveHome);
const findMobileOverlay = () => wrapper.find('[data-testid="mobile-overlay"]');
const findSubviewHeader = () => wrapper.findComponent(ResponsiveHeader);
const findSubviewContainer = () => wrapper.findComponent(TopNavContainerView);
const hasBodyResponsiveOpen = () => document.body.classList.contains('top-nav-responsive-open');
const hasMobileOverlayVisible = () => findMobileOverlay().classes('mobile-nav-open');
beforeEach(() => {
// Add test class to reset state + assert that we're adding classes correctly
@ -32,6 +45,13 @@ describe('~/nav/components/responsive_app.vue', () => {
createComponent();
});
it('shows home by default', () => {
expect(findHome().isVisible()).toBe(true);
expect(findHome().props()).toEqual({
navData: resetMenuItemsActive(TEST_NAV_DATA),
});
});
it.each`
times | expectation
${0} | ${false}
@ -45,6 +65,78 @@ describe('~/nav/components/responsive_app.vue', () => {
expect(hasBodyResponsiveOpen()).toBe(expectation);
},
);
it.each`
events | expectation
${[]} | ${false}
${['bv::dropdown::show']} | ${true}
${['bv::dropdown::show', 'bv::dropdown::hide']} | ${false}
`(
'with root events $events, movile overlay visible = $expectation',
async ({ events, expectation }) => {
// `await...reduce(async` is like doing an `forEach(async (...))` excpet it works
await events.reduce(async (acc, evt) => {
await acc;
wrapper.vm.$root.$emit(evt);
await wrapper.vm.$nextTick();
}, Promise.resolve());
expect(hasMobileOverlayVisible()).toBe(expectation);
},
);
});
const projectsContainerProps = {
containerClass: 'gl-px-3',
frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace,
frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule,
linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary,
linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary,
};
const groupsContainerProps = {
containerClass: 'gl-px-3',
frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace,
frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule,
linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary,
linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary,
};
describe.each`
view | header | containerProps
${'projects'} | ${'Projects'} | ${projectsContainerProps}
${'groups'} | ${'Groups'} | ${groupsContainerProps}
`('when menu item with $view is clicked', ({ view, header, containerProps }) => {
beforeEach(async () => {
createComponent();
findHome().vm.$emit('menu-item-click', { view });
await wrapper.vm.$nextTick();
});
it('shows header', () => {
expect(findSubviewHeader().text()).toBe(header);
});
it('shows container subview', () => {
expect(findSubviewContainer().props()).toEqual(containerProps);
});
it('hides home', () => {
expect(findHome().isVisible()).toBe(false);
});
describe('when header back button is clicked', () => {
beforeEach(() => {
findSubviewHeader().vm.$emit('menu-item-click', { view: 'home' });
});
it('shows home', () => {
expect(findHome().isVisible()).toBe(true);
});
});
});
describe('when destroyed', () => {

View File

@ -0,0 +1,67 @@
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResponsiveHeader from '~/nav/components/responsive_header.vue';
import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
const TEST_SLOT_CONTENT = 'Test slot content';
describe('~/nav/components/top_nav_menu_sections.vue', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(ResponsiveHeader, {
slots: {
default: TEST_SLOT_CONTENT,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
const findMenuItem = () => wrapper.findComponent(TopNavMenuItem);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders slot', () => {
expect(wrapper.text()).toBe(TEST_SLOT_CONTENT);
});
it('renders back button', () => {
const button = findMenuItem();
const tooltip = getBinding(button.element, 'gl-tooltip').value.title;
expect(tooltip).toBe('Go back');
expect(button.props()).toEqual({
menuItem: {
id: 'home',
view: 'home',
icon: 'angle-left',
},
iconOnly: true,
});
});
it('emits nothing', () => {
expect(wrapper.emitted()).toEqual({});
});
describe('when back button is clicked', () => {
beforeEach(() => {
findMenuItem().vm.$emit('click');
});
it('emits menu-item-click', () => {
expect(wrapper.emitted()).toEqual({
'menu-item-click': [[{ id: 'home', view: 'home', icon: 'angle-left' }]],
});
});
});
});

View File

@ -0,0 +1,137 @@
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ResponsiveHome from '~/nav/components/responsive_home.vue';
import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
import { TEST_NAV_DATA } from '../mock_data';
const TEST_SEARCH_MENU_ITEM = {
id: 'search',
title: 'search',
icon: 'search',
href: '/search',
};
const TEST_NEW_DROPDOWN_VIEW_MODEL = {
title: 'new',
menu_sections: [],
};
describe('~/nav/components/responsive_home.vue', () => {
let wrapper;
let menuItemClickListener;
const createComponent = (props = {}) => {
wrapper = shallowMount(ResponsiveHome, {
propsData: {
navData: TEST_NAV_DATA,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
listeners: {
'menu-item-click': menuItemClickListener,
},
});
};
const findSearchMenuItem = () => wrapper.findComponent(TopNavMenuItem);
const findNewDropdown = () => wrapper.findComponent(TopNavNewDropdown);
const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
beforeEach(() => {
menuItemClickListener = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it.each`
desc | fn
${'does not show search menu item'} | ${findSearchMenuItem}
${'does not show new dropdown'} | ${findNewDropdown}
`('$desc', ({ fn }) => {
expect(fn().exists()).toBe(false);
});
it('shows menu sections', () => {
expect(findMenuSections().props('sections')).toEqual([
{ id: 'primary', menuItems: TEST_NAV_DATA.primary },
{ id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
]);
});
it('emits when menu sections emits', () => {
expect(menuItemClickListener).not.toHaveBeenCalled();
findMenuSections().vm.$emit('menu-item-click', TEST_NAV_DATA.primary[0]);
expect(menuItemClickListener).toHaveBeenCalledWith(TEST_NAV_DATA.primary[0]);
});
});
describe('without secondary', () => {
beforeEach(() => {
createComponent({ navData: { ...TEST_NAV_DATA, secondary: null } });
});
it('shows menu sections', () => {
expect(findMenuSections().props('sections')).toEqual([
{ id: 'primary', menuItems: TEST_NAV_DATA.primary },
]);
});
});
describe('with search view', () => {
beforeEach(() => {
createComponent({
navData: {
...TEST_NAV_DATA,
views: { search: TEST_SEARCH_MENU_ITEM },
},
});
});
it('shows search menu item', () => {
expect(findSearchMenuItem().props()).toEqual({
menuItem: TEST_SEARCH_MENU_ITEM,
iconOnly: true,
});
});
it('shows tooltip for search', () => {
const tooltip = getBinding(findSearchMenuItem().element, 'gl-tooltip');
expect(tooltip.value).toEqual({ title: TEST_SEARCH_MENU_ITEM.title });
});
});
describe('with new view', () => {
beforeEach(() => {
createComponent({
navData: {
...TEST_NAV_DATA,
views: { new: TEST_NEW_DROPDOWN_VIEW_MODEL },
},
});
});
it('shows new dropdown', () => {
expect(findNewDropdown().props()).toEqual({
viewModel: TEST_NEW_DROPDOWN_VIEW_MODEL,
});
});
it('shows tooltip for new dropdown', () => {
const tooltip = getBinding(findNewDropdown().element, 'gl-tooltip');
expect(tooltip.value).toEqual({ title: TEST_NEW_DROPDOWN_VIEW_MODEL.title });
});
});
});

View File

@ -13,6 +13,7 @@ const DEFAULT_PROPS = {
frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule,
linksPrimary: TEST_NAV_DATA.primary,
linksSecondary: TEST_NAV_DATA.secondary,
containerClass: 'test-frequent-items-container-class',
};
const TEST_OTHER_PROPS = {
namespace: 'projects',
@ -44,6 +45,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
attributes: parent.findComponent(FrequentItemsApp).attributes(),
};
};
const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]');
afterEach(() => {
wrapper.destroy();
@ -85,6 +87,10 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
});
});
it('renders given container class', () => {
expect(findFrequentItemsContainer().classes(DEFAULT_PROPS.containerClass)).toBe(true);
});
it('renders menu sections', () => {
const sections = [
{ id: 'primary', menuItems: TEST_NAV_DATA.primary },

View File

@ -30,7 +30,10 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
const findButtonIcons = () =>
findButton()
.findAllComponents(GlIcon)
.wrappers.map((x) => x.props('name'));
.wrappers.map((x) => ({
name: x.props('name'),
classes: x.classes(),
}));
beforeEach(() => {
listener = jest.fn();
@ -65,11 +68,42 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
expect(listener).toHaveBeenCalledWith('TEST');
});
it('renders expected icons', () => {
expect(findButtonIcons()).toEqual([
{
name: TEST_MENU_ITEM.icon,
classes: ['gl-mr-2!'],
},
{
name: 'chevron-right',
classes: ['gl-ml-auto'],
},
]);
});
});
describe('with icon-only', () => {
beforeEach(() => {
createComponent({ iconOnly: true });
});
it('does not render title or view icon', () => {
expect(wrapper.text()).toBe('');
});
it('only renders menuItem icon', () => {
expect(findButtonIcons()).toEqual([
{
name: TEST_MENU_ITEM.icon,
classes: [],
},
]);
});
});
describe.each`
desc | menuItem | expectedIcons
${'default'} | ${TEST_MENU_ITEM} | ${[TEST_MENU_ITEM.icon, 'chevron-right']}
${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']}
${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]}
${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]}
@ -79,7 +113,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => {
});
it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => {
expect(findButtonIcons()).toEqual(expectedIcons);
expect(findButtonIcons().map((x) => x.name)).toEqual(expectedIcons);
});
});

View File

@ -51,11 +51,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems: [
{
menuItem: TEST_SECTIONS[0].menuItems[0],
classes: [],
classes: ['gl-w-full'],
},
...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({
menuItem,
classes: ['gl-mt-1'],
classes: ['gl-w-full', 'gl-mt-1'],
})),
],
},
@ -64,11 +64,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems: [
{
menuItem: TEST_SECTIONS[1].menuItems[0],
classes: [],
classes: ['gl-w-full'],
},
...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({
menuItem,
classes: ['gl-mt-1'],
classes: ['gl-w-full', 'gl-mt-1'],
})),
],
},

View File

@ -0,0 +1,122 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
const TEST_VIEW_MODEL = {
title: 'Dropdown',
menu_sections: [
{
title: 'Section 1',
menu_items: [
{ id: 'foo-1', title: 'Foo 1', href: '/foo/1' },
{ id: 'foo-2', title: 'Foo 2', href: '/foo/2' },
{ id: 'foo-3', title: 'Foo 3', href: '/foo/3' },
],
},
{
title: 'Section 2',
menu_items: [
{ id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
{ id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
],
},
],
};
describe('~/nav/components/top_nav_menu_sections.vue', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(TopNavNewDropdown, {
propsData: {
viewModel: TEST_VIEW_MODEL,
...props,
},
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownContents = () =>
findDropdown()
.findAll('[data-testid]')
.wrappers.map((child) => {
const type = child.attributes('data-testid');
if (type === 'divider') {
return { type };
} else if (type === 'header') {
return { type, text: child.text() };
}
return {
type,
text: child.text(),
href: child.attributes('href'),
};
});
afterEach(() => {
wrapper.destroy();
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('renders dropdown parent', () => {
expect(findDropdown().props()).toMatchObject({
text: TEST_VIEW_MODEL.title,
textSrOnly: true,
icon: 'plus',
});
});
it('renders dropdown content', () => {
expect(findDropdownContents()).toEqual([
{
type: 'header',
text: TEST_VIEW_MODEL.menu_sections[0].title,
},
...TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
{
type: 'divider',
},
{
type: 'header',
text: TEST_VIEW_MODEL.menu_sections[1].title,
},
...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
]);
});
});
describe('with only 1 section', () => {
beforeEach(() => {
createComponent({
viewModel: {
...TEST_VIEW_MODEL,
menu_sections: TEST_VIEW_MODEL.menu_sections.slice(0, 1),
},
});
});
it('renders dropdown content without headers and dividers', () => {
expect(findDropdownContents()).toEqual(
TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
);
});
});
});

View File

@ -25,11 +25,15 @@ export const TEST_NAV_DATA = {
namespace: 'projects',
currentUserName: '',
currentItem: {},
linksPrimary: [{ id: 'project-link', href: '/path/to/projects', title: 'Project Link' }],
linksSecondary: [],
},
groups: {
namespace: 'groups',
currentUserName: '',
currentItem: {},
linksPrimary: [],
linksSecondary: [{ id: 'group-link', href: '/path/to/groups', title: 'Group Link' }],
},
},
};

View File

@ -532,7 +532,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
const tooltip = wrapper.find('[data-testid="question-o-icon"]');
expect(wrapper.text()).toContain('Deletes source branch');
expect(wrapper.text()).toContain('The source branch will be deleted');
expect(tooltip.attributes('title')).toBe(
'A user with write access to the source branch selected this option',
);
@ -548,7 +548,7 @@ describe('MrWidgetOptions', () => {
nextTick(() => {
expect(wrapper.text()).toContain('The source branch has been deleted');
expect(wrapper.text()).not.toContain('Deletes source branch');
expect(wrapper.text()).not.toContain('The source branch will be deleted');
done();
});

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