Commit a4fe62f3 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0364c112 86847033
<script>
import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import DetailRow from './sidebar_detail_row.vue';
import ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue';
import SidebarJobDetailsContainer from './sidebar_job_details_container.vue';
export default {
name: 'JobSidebar',
components: {
ArtifactsBlock,
CommitBlock,
DetailRow,
GlIcon,
TriggerBlock,
StagesDropdown,
JobsContainer,
GlLink,
GlButton,
SidebarJobDetailsContainer,
TooltipOnTruncate,
},
mixins: [timeagoMixin],
props: {
artifactHelpUrl: {
type: String,
......@@ -42,53 +38,12 @@ export default {
},
computed: {
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className = 'js-retry-button btn btn-retry';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
},
timeout() {
if (this.job.metadata == null) {
return '';
}
let t = this.job.metadata.timeout_human_readable;
if (this.job.metadata.timeout_source !== '') {
t += sprintf(__(` (from %{timeoutSource})`), {
timeoutSource: this.job.metadata.timeout_source,
});
}
return t;
},
renderBlock() {
return (
this.job.duration ||
this.job.finished_at ||
this.job.erased_at ||
this.job.queued ||
this.hasTimeout ||
this.job.runner ||
this.job.coverage ||
this.job.tags.length
);
},
hasArtifact() {
return !isEmpty(this.job.artifact);
},
......@@ -96,16 +51,10 @@ export default {
return !isEmpty(this.job.trigger);
},
hasStages() {
return (
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
return this.job?.pipeline?.stages?.length > 0;
},
commit() {
return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {};
return this.job?.pipeline?.commit || {};
},
},
methods: {
......@@ -131,22 +80,22 @@ export default {
data-method="post"
data-qa-selector="retry_button"
rel="nofollow"
>{{ __('Retry') }}</gl-link
>
>{{ __('Retry') }}
</gl-link>
<gl-link
v-if="job.cancel_path"
:href="job.cancel_path"
class="js-cancel-job btn btn-default"
data-method="post"
rel="nofollow"
>{{ __('Cancel') }}</gl-link
>
>{{ __('Cancel') }}
</gl-link>
</div>
<gl-button
:aria-label="__('Toggle Sidebar')"
class="d-md-none gl-ml-2 js-sidebar-build-toggle"
category="tertiary"
class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right"
@click="toggleSidebar"
/>
......@@ -158,77 +107,37 @@ export default {
:href="job.new_issue_path"
class="btn btn-success btn-inverted float-left mr-2"
data-testid="job-new-issue"
>{{ __('New issue') }}</gl-link
>
>{{ __('New issue') }}
</gl-link>
<gl-link
v-if="job.terminal_path"
:href="job.terminal_path"
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank"
>
{{ __('Debug') }} <gl-icon name="external-link" :size="14" />
{{ __('Debug') }}
<gl-icon :size="14" name="external-link" />
</gl-link>
</div>
<div v-if="renderBlock" class="block">
<detail-row
v-if="job.duration"
:value="duration"
class="js-job-duration"
title="Duration"
/>
<detail-row
v-if="job.finished_at"
:value="timeFormatted(job.finished_at)"
class="js-job-finished"
title="Finished"
/>
<detail-row
v-if="job.erased_at"
:value="timeFormatted(job.erased_at)"
class="js-job-erased"
title="Erased"
/>
<detail-row v-if="job.queued" :value="queued" class="js-job-queued" title="Queued" />
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
class="js-job-timeout"
title="Timeout"
/>
<detail-row v-if="job.runner" :value="runnerId" class="js-job-runner" title="Runner" />
<detail-row
v-if="job.coverage"
:value="coverage"
class="js-job-coverage"
title="Coverage"
/>
<p v-if="job.tags.length" class="build-detail-row js-job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
<span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{
tag
}}</span>
</p>
</div>
<sidebar-job-details-container :runner-help-url="runnerHelpUrl" />
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block
:is-last-block="hasStages"
:commit="commit"
:is-last-block="hasStages"
:merge-request="job.merge_request"
/>
<stages-dropdown
:stages="stages"
v-if="job.pipeline"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
</div>
<jobs-container v-if="jobs.length" :jobs="jobs" :job-id="job.id" />
<jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
</div>
</aside>
</template>
<script>
import { mapState } from 'vuex';
import DetailRow from './sidebar_detail_row.vue';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
export default {
name: 'SidebarJobDetailsContainer',
components: {
DetailRow,
},
mixins: [timeagoMixin],
props: {
runnerHelpUrl: {
type: String,
required: false,
default: '',
},
},
computed: {
...mapState(['job']),
coverage() {
return `${this.job.coverage}%`;
},
duration() {
return timeIntervalInWords(this.job.duration);
},
erasedAt() {
return this.timeFormatted(this.job.erased_at);
},
finishedAt() {
return this.timeFormatted(this.job.finished_at);
},
hasTags() {
return this.job?.tags?.length;
},
hasTimeout() {
return this.job?.metadata?.timeout_human_readable ?? false;
},
hasAnyDetail() {
return Boolean(
this.job.duration ||
this.job.finished_at ||
this.job.erased_at ||
this.job.queued ||
this.job.runner ||
this.job.coverage,
);
},
queued() {
return timeIntervalInWords(this.job.queued);
},
runnerId() {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
shouldRenderBlock() {
return Boolean(this.hasAnyDetail || this.hasTimeout || this.hasTags);
},
timeout() {
return `${this.job?.metadata?.timeout_human_readable}${this.timeoutSource}`;
},
timeoutSource() {
if (!this.job?.metadata?.timeout_source) {
return '';
}
return sprintf(__(` (from %{timeoutSource})`), {
timeoutSource: this.job.metadata.timeout_source,
});
},
},
};
</script>
<template>
<div v-if="shouldRenderBlock" class="block">
<detail-row v-if="job.duration" :value="duration" title="Duration" />
<detail-row
v-if="job.finished_at"
:value="finishedAt"
data-testid="job-finished"
title="Finished"
/>
<detail-row v-if="job.erased_at" :value="erasedAt" title="Erased" />
<detail-row v-if="job.queued" :value="queued" title="Queued" />
<detail-row
v-if="hasTimeout"
:help-url="runnerHelpUrl"
:value="timeout"
data-testid="job-timeout"
title="Timeout"
/>
<detail-row v-if="job.runner" :value="runnerId" title="Runner" />
<detail-row v-if="job.coverage" :value="coverage" title="Coverage" />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
<span v-for="(tag, i) in job.tags" :key="i" class="badge badge-primary mr-1">{{ tag }}</span>
</p>
</div>
</template>
......@@ -4,7 +4,7 @@ import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const hasUnmetPrerequisitesFailure = state =>
state.job && state.job.failure_reason && state.job.failure_reason === 'unmet_prerequisites';
state?.job?.failure_reason === 'unmet_prerequisites';
export const shouldRenderCalloutMessage = state =>
!isEmpty(state.job.status) && !isEmpty(state.job.callout_message);
......
......@@ -3,5 +3,5 @@ import initSearchApp from '~/search';
document.addEventListener('DOMContentLoaded', () => {
initSearchApp();
return new Search();
return new Search(); // Deprecated Dropdown (Projects)
});
......@@ -5,48 +5,22 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
import { visitUrl } from '~/lib/utils/url_utility';
import { visitUrl, queryToObject } from '~/lib/utils/url_utility';
import refreshCounts from './refresh_counts';
export default class Search {
constructor() {
setHighlightClass();
const $groupDropdown = $('.js-search-group-dropdown');
setHighlightClass(); // Code Highlighting
const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear';
this.groupId = $groupDropdown.data('groupId');
const query = queryToObject(window.location.search);
this.groupId = query?.group_id;
this.eventListeners();
refreshCounts();
initDeprecatedJQueryDropdown($groupDropdown, {
selectable: true,
filterable: true,
filterRemote: true,
fieldName: 'group_id',
search: {
fields: ['full_name'],
},
data(term, callback) {
return Api.groups(term, {}, data => {
data.unshift({
full_name: __('Any'),
});
data.splice(1, 0, { type: 'divider' });
return callback(data);
});
},
id(obj) {
return obj.id;
},
text(obj) {
return obj.full_name;
},
clicked: () => Search.submitSearch(),
});
initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true,
filterable: true,
......
<script>
import {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { isEmpty } from 'lodash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_GROUP, GROUP_QUERY_PARAM, PROJECT_QUERY_PARAM } from '../constants';
export default {
name: 'GroupFilter',
components: {
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
GlSkeletonLoader,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
initialGroup: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
groupSearch: '',
};
},
computed: {
...mapState(['groups', 'fetchingGroups']),
selectedGroup: {
get() {
return isEmpty(this.initialGroup) ? ANY_GROUP : this.initialGroup;
},
set(group) {
visitUrl(setUrlParams({ [GROUP_QUERY_PARAM]: group.id, [PROJECT_QUERY_PARAM]: null }));
},
},
},
methods: {
...mapActions(['fetchGroups']),
isGroupSelected(group) {
return group.id === this.selectedGroup.id;
},
handleGroupChange(group) {
this.selectedGroup = group;
},
},
ANY_GROUP,
};
</script>
<template>
<gl-dropdown
ref="groupFilter"
class="gl-w-full"
menu-class="gl-w-full!"
toggle-class="gl-text-truncate gl-reset-line-height!"
:header-text="__('Filter results by group')"
@show="fetchGroups(groupSearch)"
>
<template #button-content>
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedGroup.name }}
</span>
<gl-loading-icon v-if="fetchingGroups" inline class="mr-2" />
<gl-icon
v-if="!isGroupSelected($options.ANY_GROUP)"
v-gl-tooltip
name="clear"
:title="__('Clear')"
class="gl-text-gray-200! gl-hover-text-blue-800!"
@click.stop="handleGroupChange($options.ANY_GROUP)"
/>
<gl-icon name="chevron-down" />
</template>
<div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
<gl-search-box-by-type
v-model="groupSearch"
class="m-2"
:debounce="500"
@input="fetchGroups"
/>
<gl-dropdown-item
class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
:is-check-item="true"
:is-checked="isGroupSelected($options.ANY_GROUP)"
@click="handleGroupChange($options.ANY_GROUP)"
>
{{ $options.ANY_GROUP.name }}
</gl-dropdown-item>
</div>
<div v-if="!fetchingGroups">
<gl-dropdown-item
v-for="group in groups"
:key="group.id"
:is-check-item="true"
:is-checked="isGroupSelected(group)"
@click="handleGroupChange(group)"
>
{{ group.full_name }}
</gl-dropdown-item>
</div>
<div v-if="fetchingGroups" class="mx-3 mt-2">
<gl-skeleton-loader :height="100">
<rect y="0" width="90%" height="20" rx="4" />
<rect y="40" width="70%" height="20" rx="4" />
<rect y="80" width="80%" height="20" rx="4" />
</gl-skeleton-loader>
</div>
</gl-dropdown>
</template>
import { __ } from '~/locale';
export const ANY_GROUP = Object.freeze({
id: null,
name: __('Any'),
});
export const GROUP_QUERY_PARAM = 'group_id';
export const PROJECT_QUERY_PARAM = 'project_id';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GroupFilter from './components/group_filter.vue';
Vue.use(Translate);
export default store => {
let initialGroup;
const el = document.getElementById('js-search-group-dropdown');
const { initialGroupData } = el.dataset;
initialGroup = JSON.parse(initialGroupData);
initialGroup = convertObjectPropsToCamelCase(initialGroup, { deep: true });
return new Vue({
el,
store,
render(createElement) {
return createElement(GroupFilter, {
props: {
initialGroup,
},
});
},
});
};
import { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store';
import initDropdownFilters from './dropdown_filter';
import initGroupFilter from './group_filter';
export default () => {
const store = createStore({ query: queryToObject(window.location.search) });
initDropdownFilters(store);
initGroupFilter(store);
};
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
Api.groups(search)
.then(data => {
commit(types.RECEIVE_GROUPS_SUCCESS, data);
})
.catch(() => {
createFlash({ message: __('There was a problem fetching groups.') });
commit(types.RECEIVE_GROUPS_ERROR);
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const getStoreConfig = ({ query }) => ({
actions,
mutations,
state: createState({ query }),
});
......
export const REQUEST_GROUPS = 'REQUEST_GROUPS';
export const RECEIVE_GROUPS_SUCCESS = 'RECEIVE_GROUPS_SUCCESS';
export const RECEIVE_GROUPS_ERROR = 'RECEIVE_GROUPS_ERROR';
import * as types from './mutation_types';
export default {
[types.REQUEST_GROUPS](state) {
state.fetchingGroups = true;
},
[types.RECEIVE_GROUPS_SUCCESS](state, data) {
state.fetchingGroups = false;
state.groups = data;
},
[types.RECEIVE_GROUPS_ERROR](state) {
state.fetchingGroups = false;
state.groups = [];
},
};
const createState = ({ query }) => ({
query,
groups: [],
fetchingGroups: false,
});
export default createState;
......@@ -270,7 +270,8 @@ input[type='checkbox']:hover {
width: 100%;
}
.dropdown-menu-toggle {
.dropdown-menu-toggle,
.gl-new-dropdown {
@include media-breakpoint-up(lg) {
width: 240px;
}
......
# frozen_string_literal: true
class UserGroupsCounter
def initialize(user_ids)
@user_ids = user_ids
end
def execute
Namespace.unscoped do
Namespace.from_union([
groups,
project_groups
]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
end
end
private
attr_reader :user_ids
def groups
Group.for_authorized_group_members(user_ids)
.select('namespaces.*, members.user_id as user_id')
end
def project_groups
Group.for_authorized_project_members(user_ids)
.select('namespaces.*, project_authorizations.user_id as user_id')
end
end
......@@ -23,7 +23,6 @@ module Resolvers
# The namespace could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` or the `full_path` of the namespace
# to query for projects, so make sure it's loaded and not `nil` before continuing.
namespace = object.respond_to?(:sync) ? object.sync : object
return Project.none if namespace.nil?
query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route
......@@ -41,6 +40,14 @@ module Resolvers
complexity = super
complexity + 10
end
private
def namespace
strong_memoize(:namespace) do
object.respond_to?(:sync) ? object.sync : object
end
end
end
end
......
# frozen_string_literal: true
module Resolvers
module Users
class GroupCountResolver < BaseResolver
alias_method :user, :object
def resolve(**args)
return unless can_read_group_count?
BatchLoader::GraphQL.for(user.id).batch do |user_ids, loader|
results = UserGroupsCounter.new(user_ids).execute
results.each do |user_id, count|
loader.call(user_id, count)
end
end
end
def can_read_group_count?
current_user&.can?(:read_group_count, user)
end
end
end
end
......@@ -7,6 +7,7 @@ module Types
description 'Values for sorting projects'
value 'SIMILARITY', 'Most similar to the search query', value: :similarity
value 'STORAGE', 'Sort by storage size', value: :storage
end
end
end
......@@ -32,6 +32,10 @@ module Types
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user',
method: :group_members
field :group_count, GraphQL::INT_TYPE, null: true,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user',
feature_flag: :user_group_counts
field :status, Types::UserStatusType, null: true,
description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
......
......@@ -98,6 +98,17 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) }
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
.where("members.user_id IN (?)", user_ids)
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, -> (user_ids) do
joins(projects: :project_authorizations)
.where("project_authorizations.user_id IN (?)", user_ids)
end
class << self
def sort_by_attribute(method)
if method == 'storage_size_desc'
......
......@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy
enable :update_user
enable :update_user_status
enable :read_user_personal_access_tokens
enable :read_group_count
end
rule { default }.enable :read_user_profile
......
......@@ -2,21 +2,10 @@
= hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id]
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "group-filter" } }
.dropdown.form-group.mb-lg-0.mx-lg-1.gl-p-0{ data: { testid: "group-filter" } }
%label.d-block{ for: "dashboard_search_group" }
= _("Group")
%button.dropdown-menu-toggle.gl-display-inline-flex.js-search-group-dropdown.gl-mt-0{ type: "button", id: "dashboard_search_group", data: { toggle: "dropdown", group_id: params[:group_id] } }
%span.dropdown-toggle-text.gl-flex-grow-1.str-truncated-100
= @group&.name || _("Any")
- if @group.present?
= link_to sprite_icon("clear"), url_for(safe_params.except(:project_id, :group_id)), class: 'search-clear js-search-clear has-tooltip', title: _('Clear')
= icon("chevron-down")
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title(_("Filter results by group"))
= dropdown_filter(_("Search groups"))
= dropdown_content
= dropdown_loading
%input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-group-data": @group.to_json } }
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } }
%label.d-block{ for: "dashboard_search_project" }
= _("Project")
......
---
name: user_group_counts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/
rollout_issue_url:
type: development
group: group::compliance
default_enabled: false
......@@ -462,6 +462,34 @@ are stored.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
Alternatively, if you have existing Pages deployed you can follow
the below steps to do a no downtime transfer to a new storage location.
1. Pause Pages deployments by setting the following in `/etc/gitlab/gitlab.rb`:
```ruby
sidekiq['experimental_queue_selector'] = true
sidekiq['queue_groups'] = [
"feature_category!=pages"
]
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. `rsync` contents from the current storage location to the new storage location: `sudo rsync -avzh --progress /var/opt/gitlab/gitlab-rails/shared/pages/ /mnt/storage/pages`
1. Set the new storage location in `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Verify Pages are still being served up as expected.
1. Unpause Pages deployments by removing from `/etc/gitlab/gitlab.rb` the `sidekiq` setting set above.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
1. Trigger a new Pages deployment and verify it's working as expected.
1. Remove the old Pages storage location: `sudo rm -rf /var/opt/gitlab/gitlab-rails/shared/pages`
1. Verify Pages are still being served up as expected.
## Configure listener for reverse proxy requests
Follow the steps below to configure the proxy listener of GitLab Pages. [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/2533) in
......
......@@ -13276,6 +13276,11 @@ enum NamespaceProjectSort {
Most similar to the search query
"""
SIMILARITY
"""
Sort by storage size
"""
STORAGE
}
input NegatedBoardIssueInput {
......@@ -21541,6 +21546,11 @@ type User {
"""
email: String
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
"""
groupCount: Int
"""
Group memberships of the user
"""
......
......@@ -39133,6 +39133,12 @@
"description": "Most similar to the search query",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "STORAGE",
"description": "Sort by storage size",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
......@@ -62400,6 +62406,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "groupCount",
"description": "Group count for the user. Available only when feature flag `user_group_counts` is enabled",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "groupMemberships",
"description": "Group memberships of the user",
......@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet.
| ----- | ---- | ----------- |
| `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled |
| `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user |
| `state` | UserState! | State of the user |
......@@ -3770,6 +3771,7 @@ Values for sorting projects.
| Value | Description |
| ----- | ----------- |
| `SIMILARITY` | Most similar to the search query |
| `STORAGE` | Sort by storage size |
### PackageTypeEnum
......
......@@ -282,10 +282,10 @@ When running your project pipeline at this point:
on the related JSON object's content. The deployment job finishes whenever the deployment to EC2
is done or has failed.
#### Deploy Amazon EKS
### Deploy to Amazon EKS
- [How to deploy your application to a GitLab-managed Amazon EKS cluster with Auto DevOps](https://about.gitlab.com/blog/2020/05/05/deploying-application-eks/)
#### Deploy to Google Cloud
## Deploy to Google Cloud
- [Deploying with GitLab on Google Cloud](https://about.gitlab.com/solutions/google-cloud-platform/)
---
stage: none
group: unassigned
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/#designated-technical-writers
comments: false
type: index
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Proxying assets
A possible security concern when managing a public facing GitLab instance is
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: concepts
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference, howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference, howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference, howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference, howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: concepts, reference, howto
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: index, reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: index, reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: index, reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: howto, reference
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Updating GitLab
Depending on the installation method and your GitLab version, there are multiple
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Migrating from MySQL to PostgreSQL
This guide documents how to take a working GitLab instance that uses MySQL and
......
---
stage: none
group: unassigned
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/#designated-technical-writers
comments: false
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Restoring from backup after a failed upgrade
Upgrades are usually smooth and restoring from backup is a rare occurrence.
......
---
stage: none
group: unassigned
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/#designated-technical-writers
comments: false
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
comments: false
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Upgrading PostgreSQL Using Slony
This guide describes the steps one can take to upgrade their PostgreSQL database
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Instance-level Kubernetes clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/39840) in GitLab 11.11.
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# IBM Engineering Workflow Management (EWM) Integration **(CORE)**
This service allows you to navigate from GitLab to EWM work items mentioned in merge request descriptions and commit messages. Each work item reference is automatically converted to a link back to the work item.
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Export Issues to CSV
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/releases/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Importing issues from CSV
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23532) in GitLab 11.7.
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Members of a project
You can manage the groups and users and their access levels in all of your
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Share Projects with other Groups
You can share projects with other [groups](../../group/index.md). This makes it
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference, concepts
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
description: "How to secure GitLab Pages websites with Let's Encrypt (manual process, deprecated)."
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Reserved project and group names
Not all project & group names are allowed because they would conflict with
......
---
stage: none
group: unassigned
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/#designated-technical-writers
type: reference
disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html'
---
......
---
stage: none
group: unassigned
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/#designated-technical-writers
---
# Updating to GitLab 13.2: Email confirmation issues
In the [GitLab 13.0.1 security release](https://about.gitlab.com/releases/2020/05/27/security-release-13-0-1-released/),
......
......@@ -19,7 +19,7 @@ query getStorageCounter($fullPath: ID!, $withExcessStorageData: Boolean = false)
wikiSize
snippetsSize
}
projects(includeSubgroups: true) {
projects(includeSubgroups: true, sort: STORAGE) {
edges {
node {
id
......
......@@ -14,8 +14,9 @@ module EE
def resolve(include_subgroups:, search:, sort:, has_vulnerabilities: false)
projects = super(include_subgroups: include_subgroups, search: search, sort: sort)
has_vulnerabilities ? projects.has_vulnerabilities : projects
projects = projects.has_vulnerabilities if has_vulnerabilities
projects = projects.order_by_total_repository_size_excess_desc(namespace.actual_size_limit) if sort == :storage
projects
end
end
end
......
......@@ -165,6 +165,16 @@ module EE
scope :without_unlimited_repository_size_limit, -> { where.not(repository_size_limit: 0) }
scope :without_repository_size_limit, -> { where(repository_size_limit: nil) }
scope :order_by_total_repository_size_excess_desc, -> (limit) do
excess = ::ProjectStatistics.arel_table[:repository_size] +
::ProjectStatistics.arel_table[:lfs_objects_size] -
::Project.arel_table.coalesce(::Project.arel_table[:repository_size_limit], limit, 0)
joins(:statistics).order(
Arel.sql(Arel::Nodes::Descending.new(excess).to_sql)
)
end
delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :statistics, allow_nil: true
......
......@@ -8,11 +8,11 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in
let(:project) { create(:project, :repository, :wiki_repo, namespace: group) }
def choose_group(group)
find('.js-search-group-dropdown').click
find('[data-testid="group-filter"]').click
wait_for_requests
page.within '.js-search-form' do
click_link group.name
page.within '[data-testid="group-filter"]' do
click_button group.name
end
end
......@@ -25,6 +25,9 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in
sign_in(user)
visit(search_path)
wait_for_requests
choose_group(group)
end
......
......@@ -19,27 +19,51 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do
end
describe '#resolve' do
subject(:projects) { resolve_projects(has_vulnerabilities) }
context 'has_vulnerabilities' do
subject(:projects) { resolve_projects(has_vulnerabilities: has_vulnerabilities) }
context 'when the `has_vulnerabilities` parameter is not truthy' do
let(:has_vulnerabilities) { false }
context 'when the `has_vulnerabilities` parameter is not truthy' do
let(:has_vulnerabilities) { false }
it { is_expected.to contain_exactly(project_1, project_2) }
it { is_expected.to contain_exactly(project_1, project_2) }
end
context 'when the `has_vulnerabilities` parameter is truthy' do
let(:has_vulnerabilities) { true }
it { is_expected.to contain_exactly(project_1) }
end
end
context 'when the `has_vulnerabilities` parameter is truthy' do
let(:has_vulnerabilities) { true }
context 'sorting' do
let(:project_3) { create(:project, namespace: group) }
before do
project_1.statistics.update!(lfs_objects_size: 11, repository_size: 10)
project_2.statistics.update!(lfs_objects_size: 10, repository_size: 12)
project_3.statistics.update!(lfs_objects_size: 12, repository_size: 11)
end
context 'when sort equals :storage' do
subject(:projects) { resolve_projects(sort: :storage) }
it { is_expected.to eq([project_3, project_2, project_1]) }
end
context 'when sort does not equal :storage' do
subject(:projects) { resolve_projects }
it { is_expected.to contain_exactly(project_1) }
it { is_expected.to eq([project_1, project_2, project_3]) }
end
end
end
end
def resolve_projects(has_vulnerabilities)
def resolve_projects(has_vulnerabilities: false, sort: :similarity)
args = {
include_subgroups: false,
has_vulnerabilities: has_vulnerabilities,
sort: :similarity,
sort: sort,
search: nil
}
......
......@@ -279,6 +279,17 @@ RSpec.describe Project do
expect(described_class.not_aimed_for_deletion).to contain_exactly(project)
end
end
describe '.order_by_total_repository_size_excess_desc' do
let_it_be(:project_1) { create(:project_statistics, lfs_objects_size: 10, repository_size: 10).project }
let_it_be(:project_2) { create(:project_statistics, lfs_objects_size: 5, repository_size: 55).project }
let_it_be(:project_3) { create(:project, repository_size_limit: 30, statistics: create(:project_statistics, lfs_objects_size: 8, repository_size: 32)) }
let(:limit) { 20 }
subject { described_class.order_by_total_repository_size_excess_desc(limit) }
it { is_expected.to eq([project_2, project_3, project_1]) }
end
end
describe 'validations' do
......
......@@ -6,6 +6,8 @@ module API
before { authorize_read_git_snapshot! }
feature_category :source_code_management
resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7'
......
......@@ -6,6 +6,8 @@ module API
before { check_snippets_enabled }
feature_category :snippets
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -2,6 +2,8 @@
module API
class ProjectStatistics < ::API::Base
feature_category :source_code_management
before do
authenticate!
authorize! :daily_statistics, user_project
......
......@@ -12,6 +12,8 @@ module API
before { authenticate_non_get! }
feature_category :templates
params do
requires :id, type: String, desc: 'The ID of a project'
requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses|metrics_dashboard_ymls|issues|merge_requests) of the template'
......
......@@ -8,6 +8,8 @@ module API
before { authorize_admin_project }
feature_category :source_code_management
helpers Helpers::ProtectedBranchesHelpers
params do
......
......@@ -8,6 +8,8 @@ module API
before { authorize_admin_project }
feature_category :source_code_management
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -10,6 +10,8 @@ module API
before { authorize! :read_release, user_project }
feature_category :release_orchestration
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -9,6 +9,8 @@ module API
before { authorize_read_releases! }
feature_category :release_orchestration
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -4,6 +4,8 @@ module API
class RemoteMirrors < ::API::Base
include PaginationParams
feature_category :source_code_management
before do
unauthorized! unless can?(current_user, :admin_remote_mirror, user_project)
end
......
......@@ -12,6 +12,8 @@ module API
before { authorize! :download_code, user_project }
feature_category :source_code_management
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -6,6 +6,8 @@ module API
before { authenticate! }
feature_category :global_search
helpers do
SCOPE_ENTITY = {
merge_requests: Entities::MergeRequestBasic,
......
# frozen_string_literal: true
module API
class Services < ::API::Base
feature_category :integrations
services = Helpers::ServicesHelpers.services
service_classes = Helpers::ServicesHelpers.service_classes
......
......@@ -4,6 +4,8 @@ module API
class Settings < ::API::Base
before { authenticated_as_admin! }
feature_category :not_owned
helpers Helpers::SettingsHelpers
helpers do
......
......@@ -6,6 +6,8 @@ module API
class SidekiqMetrics < ::API::Base
before { authenticated_as_admin! }
feature_category :not_owned
helpers do
def queue_metrics
Sidekiq::Queue.all.each_with_object({}) do |queue, hash|
......
......@@ -5,6 +5,8 @@ module API
class Snippets < ::API::Base
include PaginationParams
feature_category :snippets
resource :snippets do
helpers Helpers::SnippetsHelpers
helpers do
......
......@@ -4,6 +4,8 @@ module API
class Statistics < ::API::Base
before { authenticated_as_admin! }
feature_category :instance_statistics
COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
MergeRequest, Note, Snippet, Key, Milestone].freeze
......
......@@ -4,6 +4,8 @@ module API
class Submodules < ::API::Base
before { authenticate! }
feature_category :source_code_management
helpers do
def commit_params(attrs)
{
......
......@@ -11,25 +11,29 @@ module API
type: 'merge_requests',
entity: Entities::MergeRequest,
source: Project,
finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) }
finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) },
feature_category: :code_review
},
{
type: 'issues',
entity: Entities::Issue,
source: Project,
finder: ->(id) { find_project_issue(id) }
finder: ->(id) { find_project_issue(id) },
feature_category: :issue_tracking
},
{
type: 'labels',
entity: Entities::ProjectLabel,
source: Project,
finder: ->(id) { find_label(user_project, id) }
finder: ->(id) { find_label(user_project, id) },
feature_category: :issue_tracking
},
{
type: 'labels',
entity: Entities::GroupLabel,
source: Group,
finder: ->(id) { find_label(user_group, id) }
finder: ->(id) { find_label(user_group, id) },
feature_category: :issue_tracking
}
]
......@@ -44,7 +48,7 @@ module API
desc 'Subscribe to a resource' do
success subscribable[:entity]
end
post ":id/#{subscribable[:type]}/:subscribable_id/subscribe" do
post ":id/#{subscribable[:type]}/:subscribable_id/subscribe", subscribable.slice(:feature_category) do
parent = parent_resource(source_type)
resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
......@@ -59,7 +63,7 @@ module API
desc 'Unsubscribe from a resource' do
success subscribable[:entity]
end
post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe" do
post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe", subscribable.slice(:feature_category) do
parent = parent_resource(source_type)
resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
......
......@@ -4,6 +4,8 @@ module API
class Suggestions < ::API::Base
before { authenticate! }
feature_category :code_review
resource :suggestions do
desc 'Apply suggestion patch in the Merge Request it was created' do
success Entities::Suggestion
......
......@@ -4,6 +4,8 @@ module API
class SystemHooks < ::API::Base
include PaginationParams
feature_category :integrations
before do
authenticate!
authenticated_as_admin!
......
......@@ -23,7 +23,7 @@ module API
optional :search, type: String, desc: 'Return list of tags matching the search criteria'
use :pagination
end
get ':id/repository/tags' do
get ':id/repository/tags', feature_category: :source_code_management do
tags = ::TagsFinder.new(user_project.repository,
sort: "#{params[:order_by]}_#{params[:sort]}",
search: params[:search]).execute
......@@ -37,7 +37,7 @@ module API
params do
requires :tag_name, type: String, desc: 'The name of the tag'
end
get ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
get ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
......@@ -54,7 +54,7 @@ module API
optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)'
end
post ':id/repository/tags' do
post ':id/repository/tags', :release_orchestration do
authorize_admin_tag
result = ::Tags::CreateService.new(user_project, current_user)
......@@ -86,7 +86,7 @@ module API
params do
requires :tag_name, type: String, desc: 'The name of the tag'
end
delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do
authorize_admin_tag
tag = user_project.repository.find_tag(params[:tag_name])
......@@ -112,7 +112,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
requires :description, type: String, desc: 'Release notes with markdown support'
end
post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do
post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
authorize_create_release!
##
......@@ -144,7 +144,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
requires :description, type: String, desc: 'Release notes with markdown support'
end
put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do
put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do
authorize_update_release!
result = ::Releases::UpdateService
......
......@@ -4,6 +4,8 @@ module API
class Templates < ::API::Base
include PaginationParams
feature_category :templates
GLOBAL_TEMPLATE_TYPES = {
gitignores: {
gitlab_version: 8.8
......
......@@ -7,6 +7,8 @@ module API
class State < ::API::Base
include ::Gitlab::Utils::StrongMemoize
feature_category :infrastructure_as_code
default_format :json
before do
......
......@@ -5,6 +5,8 @@ module API
class StateVersion < ::API::Base
default_format :json
feature_category :infrastructure_as_code
before do
authenticate!
authorize! :read_terraform_state, user_project
......
......@@ -6,6 +6,8 @@ module API
before { authenticate! }
feature_category :issue_tracking
ISSUABLE_TYPES = {
'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
'issues' => ->(iid) { find_project_issue(iid) }
......
......@@ -6,6 +6,8 @@ module API
HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
feature_category :continuous_integration
params do
requires :id, type: String, desc: 'The ID of a project'
end
......
......@@ -4,6 +4,8 @@ module API
class Unleash < ::API::Base
include PaginationParams
feature_category :feature_flags
namespace :feature_flags do
resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
......
......@@ -4,6 +4,8 @@ module API
class UsageData < ::API::Base
before { authenticate! }
feature_category :collection
namespace 'usage_data' do
before do
not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true)
......
......@@ -2,6 +2,8 @@
module API
class UserCounts < ::API::Base
feature_category :navigation
resource :user_counts do
desc 'Return the user specific counts' do
detail 'Open MR Count'
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment