Commit a4fe62f3 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0364c112 86847033
<script> <script>
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlLink, GlButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; 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 ArtifactsBlock from './artifacts_block.vue';
import TriggerBlock from './trigger_block.vue'; import TriggerBlock from './trigger_block.vue';
import CommitBlock from './commit_block.vue'; import CommitBlock from './commit_block.vue';
import StagesDropdown from './stages_dropdown.vue'; import StagesDropdown from './stages_dropdown.vue';
import JobsContainer from './jobs_container.vue'; import JobsContainer from './jobs_container.vue';
import SidebarJobDetailsContainer from './sidebar_job_details_container.vue';
export default { export default {
name: 'JobSidebar', name: 'JobSidebar',
components: { components: {
ArtifactsBlock, ArtifactsBlock,
CommitBlock, CommitBlock,
DetailRow,
GlIcon, GlIcon,
TriggerBlock, TriggerBlock,
StagesDropdown, StagesDropdown,
JobsContainer, JobsContainer,
GlLink, GlLink,
GlButton, GlButton,
SidebarJobDetailsContainer,
TooltipOnTruncate, TooltipOnTruncate,
}, },
mixins: [timeagoMixin],
props: { props: {
artifactHelpUrl: { artifactHelpUrl: {
type: String, type: String,
...@@ -42,53 +38,12 @@ export default { ...@@ -42,53 +38,12 @@ export default {
}, },
computed: { computed: {
...mapState(['job', 'stages', 'jobs', 'selectedStage']), ...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() { retryButtonClass() {
let className = 'js-retry-button btn btn-retry'; let className = 'js-retry-button btn btn-retry';
className += className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className; 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() { hasArtifact() {
return !isEmpty(this.job.artifact); return !isEmpty(this.job.artifact);
}, },
...@@ -96,16 +51,10 @@ export default { ...@@ -96,16 +51,10 @@ export default {
return !isEmpty(this.job.trigger); return !isEmpty(this.job.trigger);
}, },
hasStages() { hasStages() {
return ( return this.job?.pipeline?.stages?.length > 0;
(this.job &&
this.job.pipeline &&
this.job.pipeline.stages &&
this.job.pipeline.stages.length > 0) ||
false
);
}, },
commit() { commit() {
return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {}; return this.job?.pipeline?.commit || {};
}, },
}, },
methods: { methods: {
...@@ -131,22 +80,22 @@ export default { ...@@ -131,22 +80,22 @@ export default {
data-method="post" data-method="post"
data-qa-selector="retry_button" data-qa-selector="retry_button"
rel="nofollow" rel="nofollow"
>{{ __('Retry') }}</gl-link >{{ __('Retry') }}
> </gl-link>
<gl-link <gl-link
v-if="job.cancel_path" v-if="job.cancel_path"
:href="job.cancel_path" :href="job.cancel_path"
class="js-cancel-job btn btn-default" class="js-cancel-job btn btn-default"
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
>{{ __('Cancel') }}</gl-link >{{ __('Cancel') }}
> </gl-link>
</div> </div>
<gl-button <gl-button
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
class="d-md-none gl-ml-2 js-sidebar-build-toggle"
category="tertiary" category="tertiary"
class="gl-display-md-none gl-ml-2 js-sidebar-build-toggle"
icon="chevron-double-lg-right" icon="chevron-double-lg-right"
@click="toggleSidebar" @click="toggleSidebar"
/> />
...@@ -158,77 +107,37 @@ export default { ...@@ -158,77 +107,37 @@ export default {
:href="job.new_issue_path" :href="job.new_issue_path"
class="btn btn-success btn-inverted float-left mr-2" class="btn btn-success btn-inverted float-left mr-2"
data-testid="job-new-issue" data-testid="job-new-issue"
>{{ __('New issue') }}</gl-link >{{ __('New issue') }}
> </gl-link>
<gl-link <gl-link
v-if="job.terminal_path" v-if="job.terminal_path"
:href="job.terminal_path" :href="job.terminal_path"
class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left"
target="_blank" target="_blank"
> >
{{ __('Debug') }} <gl-icon name="external-link" :size="14" /> {{ __('Debug') }}
<gl-icon :size="14" name="external-link" />
</gl-link> </gl-link>
</div> </div>
<sidebar-job-details-container :runner-help-url="runnerHelpUrl" />
<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>
<artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" /> <artifacts-block v-if="hasArtifact" :artifact="job.artifact" :help-url="artifactHelpUrl" />
<trigger-block v-if="hasTriggers" :trigger="job.trigger" /> <trigger-block v-if="hasTriggers" :trigger="job.trigger" />
<commit-block <commit-block
:is-last-block="hasStages"
:commit="commit" :commit="commit"
:is-last-block="hasStages"
:merge-request="job.merge_request" :merge-request="job.merge_request"
/> />
<stages-dropdown <stages-dropdown
:stages="stages" v-if="job.pipeline"
:pipeline="job.pipeline" :pipeline="job.pipeline"
:selected-stage="selectedStage" :selected-stage="selectedStage"
:stages="stages"
@requestSidebarStageDropdown="fetchJobsForStage" @requestSidebarStageDropdown="fetchJobsForStage"
/> />
</div> </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> </div>
</aside> </aside>
</template> </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'; ...@@ -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 headerTime = state => (state.job.started ? state.job.started : state.job.created_at);
export const hasUnmetPrerequisitesFailure = state => 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 => export const shouldRenderCalloutMessage = state =>
!isEmpty(state.job.status) && !isEmpty(state.job.callout_message); !isEmpty(state.job.status) && !isEmpty(state.job.callout_message);
......
...@@ -3,5 +3,5 @@ import initSearchApp from '~/search'; ...@@ -3,5 +3,5 @@ import initSearchApp from '~/search';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initSearchApp(); initSearchApp();
return new Search(); return new Search(); // Deprecated Dropdown (Projects)
}); });
...@@ -5,48 +5,22 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; ...@@ -5,48 +5,22 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api'; import Api from '~/api';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Project from '~/pages/projects/project'; 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'; import refreshCounts from './refresh_counts';
export default class Search { export default class Search {
constructor() { constructor() {
setHighlightClass(); setHighlightClass(); // Code Highlighting
const $groupDropdown = $('.js-search-group-dropdown');
const $projectDropdown = $('.js-search-project-dropdown'); const $projectDropdown = $('.js-search-project-dropdown');
this.searchInput = '.js-search-input'; this.searchInput = '.js-search-input';
this.searchClear = '.js-search-clear'; this.searchClear = '.js-search-clear';
this.groupId = $groupDropdown.data('groupId'); const query = queryToObject(window.location.search);
this.groupId = query?.group_id;
this.eventListeners(); this.eventListeners();
refreshCounts(); 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, { initDeprecatedJQueryDropdown($projectDropdown, {
selectable: true, selectable: true,
filterable: 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 { queryToObject } from '~/lib/utils/url_utility';
import createStore from './store'; import createStore from './store';
import initDropdownFilters from './dropdown_filter'; import initDropdownFilters from './dropdown_filter';
import initGroupFilter from './group_filter';
export default () => { export default () => {
const store = createStore({ query: queryToObject(window.location.search) }); const store = createStore({ query: queryToObject(window.location.search) });
initDropdownFilters(store); 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 Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state'; import createState from './state';
Vue.use(Vuex); Vue.use(Vuex);
export const getStoreConfig = ({ query }) => ({ export const getStoreConfig = ({ query }) => ({
actions,
mutations,
state: createState({ query }), 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 }) => ({ const createState = ({ query }) => ({
query, query,
groups: [],
fetchingGroups: false,
}); });
export default createState; export default createState;
...@@ -270,7 +270,8 @@ input[type='checkbox']:hover { ...@@ -270,7 +270,8 @@ input[type='checkbox']:hover {
width: 100%; width: 100%;
} }
.dropdown-menu-toggle { .dropdown-menu-toggle,
.gl-new-dropdown {
@include media-breakpoint-up(lg) { @include media-breakpoint-up(lg) {
width: 240px; 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 ...@@ -23,7 +23,6 @@ module Resolvers
# The namespace could have been loaded in batch by `BatchLoader`. # The namespace could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` or the `full_path` of the namespace # 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. # 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? return Project.none if namespace.nil?
query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route query = include_subgroups ? namespace.all_projects.with_route : namespace.projects.with_route
...@@ -41,6 +40,14 @@ module Resolvers ...@@ -41,6 +40,14 @@ module Resolvers
complexity = super complexity = super
complexity + 10 complexity + 10
end end
private
def namespace
strong_memoize(:namespace) do
object.respond_to?(:sync) ? object.sync : object
end
end
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 ...@@ -7,6 +7,7 @@ module Types
description 'Values for sorting projects' description 'Values for sorting projects'
value 'SIMILARITY', 'Most similar to the search query', value: :similarity value 'SIMILARITY', 'Most similar to the search query', value: :similarity
value 'STORAGE', 'Sort by storage size', value: :storage
end end
end end
end end
...@@ -32,6 +32,10 @@ module Types ...@@ -32,6 +32,10 @@ module Types
field :group_memberships, Types::GroupMemberType.connection_type, null: true, field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user', description: 'Group memberships of the user',
method: :group_members 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, field :status, Types::UserStatusType, null: true,
description: 'User status' description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true, field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
......
...@@ -98,6 +98,17 @@ class Group < Namespace ...@@ -98,6 +98,17 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) } 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 class << self
def sort_by_attribute(method) def sort_by_attribute(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
......
...@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy ...@@ -21,6 +21,7 @@ class UserPolicy < BasePolicy
enable :update_user enable :update_user
enable :update_user_status enable :update_user_status
enable :read_user_personal_access_tokens enable :read_user_personal_access_tokens
enable :read_group_count
end end
rule { default }.enable :read_user_profile rule { default }.enable :read_user_profile
......
...@@ -2,21 +2,10 @@ ...@@ -2,21 +2,10 @@
= hidden_field_tag :group_id, params[:group_id] = hidden_field_tag :group_id, params[:group_id]
- if params[:project_id].present? - if params[:project_id].present?
= hidden_field_tag :project_id, params[:project_id] = 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" } %label.d-block{ for: "dashboard_search_group" }
= _("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] } } %input#js-search-group-dropdown.dropdown-menu-toggle{ value: "Loading...", data: { "initial-group-data": @group.to_json } }
%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
.dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } } .dropdown.form-group.mb-lg-0.mx-lg-1{ data: { testid: "project-filter" } }
%label.d-block{ for: "dashboard_search_project" } %label.d-block{ for: "dashboard_search_project" }
= _("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. ...@@ -462,6 +462,34 @@ are stored.
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure). 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 ## 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 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 { ...@@ -13276,6 +13276,11 @@ enum NamespaceProjectSort {
Most similar to the search query Most similar to the search query
""" """
SIMILARITY SIMILARITY
"""
Sort by storage size
"""
STORAGE
} }
input NegatedBoardIssueInput { input NegatedBoardIssueInput {
...@@ -21541,6 +21546,11 @@ type User { ...@@ -21541,6 +21546,11 @@ type User {
""" """
email: String email: String
"""
Group count for the user. Available only when feature flag `user_group_counts` is enabled
"""
groupCount: Int
""" """
Group memberships of the user Group memberships of the user
""" """
......
...@@ -39133,6 +39133,12 @@ ...@@ -39133,6 +39133,12 @@
"description": "Most similar to the search query", "description": "Most similar to the search query",
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "STORAGE",
"description": "Sort by storage size",
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"possibleTypes": null "possibleTypes": null
...@@ -62400,6 +62406,20 @@ ...@@ -62400,6 +62406,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "groupMemberships",
"description": "Group memberships of the user", "description": "Group memberships of the user",
...@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet. ...@@ -3041,6 +3041,7 @@ Autogenerated return type of UpdateSnippet.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `avatarUrl` | String | URL of the user's avatar | | `avatarUrl` | String | URL of the user's avatar |
| `email` | String | User email | | `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 | | `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user | | `name` | String! | Human-readable name of the user |
| `state` | UserState! | State of the user | | `state` | UserState! | State of the user |
...@@ -3770,6 +3771,7 @@ Values for sorting projects. ...@@ -3770,6 +3771,7 @@ Values for sorting projects.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| `SIMILARITY` | Most similar to the search query | | `SIMILARITY` | Most similar to the search query |
| `STORAGE` | Sort by storage size |
### PackageTypeEnum ### PackageTypeEnum
......
...@@ -282,10 +282,10 @@ When running your project pipeline at this point: ...@@ -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 on the related JSON object's content. The deployment job finishes whenever the deployment to EC2
is done or has failed. 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/) - [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/) - [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 comments: false
type: index 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 # Proxying assets
A possible security concern when managing a public facing GitLab instance is 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 # Updating GitLab
Depending on the installation method and your GitLab version, there are multiple 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 # Migrating from MySQL to PostgreSQL
This guide documents how to take a working GitLab instance that uses MySQL and 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 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 # Restoring from backup after a failed upgrade
Upgrades are usually smooth and restoring from backup is a rare occurrence. 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 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 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 # Upgrading PostgreSQL Using Slony
This guide describes the steps one can take to upgrade their PostgreSQL database 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 # Instance-level Kubernetes clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/39840) in GitLab 11.11. > [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)** # 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. 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 # 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). > - [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 # Importing issues from CSV
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23532) in GitLab 11.7. > [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 # Members of a project
You can manage the groups and users and their access levels in all of your 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 # Share Projects with other Groups
You can share projects with other [groups](../../group/index.md). This makes it 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 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)." 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 # Reserved project and group names
Not all project & group names are allowed because they would conflict with 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 type: reference
disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html' 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 # 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/), 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) ...@@ -19,7 +19,7 @@ query getStorageCounter($fullPath: ID!, $withExcessStorageData: Boolean = false)
wikiSize wikiSize
snippetsSize snippetsSize
} }
projects(includeSubgroups: true) { projects(includeSubgroups: true, sort: STORAGE) {
edges { edges {
node { node {
id id
......
...@@ -14,8 +14,9 @@ module EE ...@@ -14,8 +14,9 @@ module EE
def resolve(include_subgroups:, search:, sort:, has_vulnerabilities: false) def resolve(include_subgroups:, search:, sort:, has_vulnerabilities: false)
projects = super(include_subgroups: include_subgroups, search: search, sort: sort) projects = super(include_subgroups: include_subgroups, search: search, sort: sort)
projects = projects.has_vulnerabilities if has_vulnerabilities
has_vulnerabilities ? projects.has_vulnerabilities : projects projects = projects.order_by_total_repository_size_excess_desc(namespace.actual_size_limit) if sort == :storage
projects
end end
end end
end end
......
...@@ -165,6 +165,16 @@ module EE ...@@ -165,6 +165,16 @@ module EE
scope :without_unlimited_repository_size_limit, -> { where.not(repository_size_limit: 0) } scope :without_unlimited_repository_size_limit, -> { where.not(repository_size_limit: 0) }
scope :without_repository_size_limit, -> { where(repository_size_limit: nil) } 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, delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
to: :statistics, allow_nil: true to: :statistics, allow_nil: true
......
...@@ -8,11 +8,11 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in ...@@ -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) } let(:project) { create(:project, :repository, :wiki_repo, namespace: group) }
def choose_group(group) def choose_group(group)
find('.js-search-group-dropdown').click find('[data-testid="group-filter"]').click
wait_for_requests wait_for_requests
page.within '.js-search-form' do page.within '[data-testid="group-filter"]' do
click_link group.name click_button group.name
end end
end end
...@@ -25,6 +25,9 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in ...@@ -25,6 +25,9 @@ RSpec.describe 'Group elastic search', :js, :elastic, :sidekiq_might_not_need_in
sign_in(user) sign_in(user)
visit(search_path) visit(search_path)
wait_for_requests
choose_group(group) choose_group(group)
end end
......
...@@ -19,27 +19,51 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do ...@@ -19,27 +19,51 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do
end end
describe '#resolve' do 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 context 'when the `has_vulnerabilities` parameter is not truthy' do
let(:has_vulnerabilities) { false } 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 end
context 'when the `has_vulnerabilities` parameter is truthy' do context 'sorting' do
let(:has_vulnerabilities) { true } 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 end
end end
def resolve_projects(has_vulnerabilities) def resolve_projects(has_vulnerabilities: false, sort: :similarity)
args = { args = {
include_subgroups: false, include_subgroups: false,
has_vulnerabilities: has_vulnerabilities, has_vulnerabilities: has_vulnerabilities,
sort: :similarity, sort: sort,
search: nil search: nil
} }
......
...@@ -279,6 +279,17 @@ RSpec.describe Project do ...@@ -279,6 +279,17 @@ RSpec.describe Project do
expect(described_class.not_aimed_for_deletion).to contain_exactly(project) expect(described_class.not_aimed_for_deletion).to contain_exactly(project)
end end
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 end
describe 'validations' do describe 'validations' do
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authorize_read_git_snapshot! } before { authorize_read_git_snapshot! }
feature_category :source_code_management
resource :projects do resource :projects do
desc 'Download a (possibly inconsistent) snapshot of a repository' do desc 'Download a (possibly inconsistent) snapshot of a repository' do
detail 'This feature was introduced in GitLab 10.7' detail 'This feature was introduced in GitLab 10.7'
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { check_snippets_enabled } before { check_snippets_enabled }
feature_category :snippets
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module API module API
class ProjectStatistics < ::API::Base class ProjectStatistics < ::API::Base
feature_category :source_code_management
before do before do
authenticate! authenticate!
authorize! :daily_statistics, user_project authorize! :daily_statistics, user_project
......
...@@ -12,6 +12,8 @@ module API ...@@ -12,6 +12,8 @@ module API
before { authenticate_non_get! } before { authenticate_non_get! }
feature_category :templates
params do params do
requires :id, type: String, desc: 'The ID of a project' 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' 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 ...@@ -8,6 +8,8 @@ module API
before { authorize_admin_project } before { authorize_admin_project }
feature_category :source_code_management
helpers Helpers::ProtectedBranchesHelpers helpers Helpers::ProtectedBranchesHelpers
params do params do
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
before { authorize_admin_project } before { authorize_admin_project }
feature_category :source_code_management
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -10,6 +10,8 @@ module API ...@@ -10,6 +10,8 @@ module API
before { authorize! :read_release, user_project } before { authorize! :read_release, user_project }
feature_category :release_orchestration
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -9,6 +9,8 @@ module API ...@@ -9,6 +9,8 @@ module API
before { authorize_read_releases! } before { authorize_read_releases! }
feature_category :release_orchestration
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class RemoteMirrors < ::API::Base class RemoteMirrors < ::API::Base
include PaginationParams include PaginationParams
feature_category :source_code_management
before do before do
unauthorized! unless can?(current_user, :admin_remote_mirror, user_project) unauthorized! unless can?(current_user, :admin_remote_mirror, user_project)
end end
......
...@@ -12,6 +12,8 @@ module API ...@@ -12,6 +12,8 @@ module API
before { authorize! :download_code, user_project } before { authorize! :download_code, user_project }
feature_category :source_code_management
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :global_search
helpers do helpers do
SCOPE_ENTITY = { SCOPE_ENTITY = {
merge_requests: Entities::MergeRequestBasic, merge_requests: Entities::MergeRequestBasic,
......
# frozen_string_literal: true # frozen_string_literal: true
module API module API
class Services < ::API::Base class Services < ::API::Base
feature_category :integrations
services = Helpers::ServicesHelpers.services services = Helpers::ServicesHelpers.services
service_classes = Helpers::ServicesHelpers.service_classes service_classes = Helpers::ServicesHelpers.service_classes
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Settings < ::API::Base class Settings < ::API::Base
before { authenticated_as_admin! } before { authenticated_as_admin! }
feature_category :not_owned
helpers Helpers::SettingsHelpers helpers Helpers::SettingsHelpers
helpers do helpers do
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
class SidekiqMetrics < ::API::Base class SidekiqMetrics < ::API::Base
before { authenticated_as_admin! } before { authenticated_as_admin! }
feature_category :not_owned
helpers do helpers do
def queue_metrics def queue_metrics
Sidekiq::Queue.all.each_with_object({}) do |queue, hash| Sidekiq::Queue.all.each_with_object({}) do |queue, hash|
......
...@@ -5,6 +5,8 @@ module API ...@@ -5,6 +5,8 @@ module API
class Snippets < ::API::Base class Snippets < ::API::Base
include PaginationParams include PaginationParams
feature_category :snippets
resource :snippets do resource :snippets do
helpers Helpers::SnippetsHelpers helpers Helpers::SnippetsHelpers
helpers do helpers do
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Statistics < ::API::Base class Statistics < ::API::Base
before { authenticated_as_admin! } before { authenticated_as_admin! }
feature_category :instance_statistics
COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue, COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
MergeRequest, Note, Snippet, Key, Milestone].freeze MergeRequest, Note, Snippet, Key, Milestone].freeze
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Submodules < ::API::Base class Submodules < ::API::Base
before { authenticate! } before { authenticate! }
feature_category :source_code_management
helpers do helpers do
def commit_params(attrs) def commit_params(attrs)
{ {
......
...@@ -11,25 +11,29 @@ module API ...@@ -11,25 +11,29 @@ module API
type: 'merge_requests', type: 'merge_requests',
entity: Entities::MergeRequest, entity: Entities::MergeRequest,
source: Project, 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', type: 'issues',
entity: Entities::Issue, entity: Entities::Issue,
source: Project, source: Project,
finder: ->(id) { find_project_issue(id) } finder: ->(id) { find_project_issue(id) },
feature_category: :issue_tracking
}, },
{ {
type: 'labels', type: 'labels',
entity: Entities::ProjectLabel, entity: Entities::ProjectLabel,
source: Project, source: Project,
finder: ->(id) { find_label(user_project, id) } finder: ->(id) { find_label(user_project, id) },
feature_category: :issue_tracking
}, },
{ {
type: 'labels', type: 'labels',
entity: Entities::GroupLabel, entity: Entities::GroupLabel,
source: Group, 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 ...@@ -44,7 +48,7 @@ module API
desc 'Subscribe to a resource' do desc 'Subscribe to a resource' do
success subscribable[:entity] success subscribable[:entity]
end 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) parent = parent_resource(source_type)
resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
...@@ -59,7 +63,7 @@ module API ...@@ -59,7 +63,7 @@ module API
desc 'Unsubscribe from a resource' do desc 'Unsubscribe from a resource' do
success subscribable[:entity] success subscribable[:entity]
end 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) parent = parent_resource(source_type)
resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) resource = instance_exec(params[:subscribable_id], &subscribable[:finder])
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Suggestions < ::API::Base class Suggestions < ::API::Base
before { authenticate! } before { authenticate! }
feature_category :code_review
resource :suggestions do resource :suggestions do
desc 'Apply suggestion patch in the Merge Request it was created' do desc 'Apply suggestion patch in the Merge Request it was created' do
success Entities::Suggestion success Entities::Suggestion
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class SystemHooks < ::API::Base class SystemHooks < ::API::Base
include PaginationParams include PaginationParams
feature_category :integrations
before do before do
authenticate! authenticate!
authenticated_as_admin! authenticated_as_admin!
......
...@@ -23,7 +23,7 @@ module API ...@@ -23,7 +23,7 @@ module API
optional :search, type: String, desc: 'Return list of tags matching the search criteria' optional :search, type: String, desc: 'Return list of tags matching the search criteria'
use :pagination use :pagination
end end
get ':id/repository/tags' do get ':id/repository/tags', feature_category: :source_code_management do
tags = ::TagsFinder.new(user_project.repository, tags = ::TagsFinder.new(user_project.repository,
sort: "#{params[:order_by]}_#{params[:sort]}", sort: "#{params[:order_by]}_#{params[:sort]}",
search: params[:search]).execute search: params[:search]).execute
...@@ -37,7 +37,7 @@ module API ...@@ -37,7 +37,7 @@ module API
params do params do
requires :tag_name, type: String, desc: 'The name of the tag' requires :tag_name, type: String, desc: 'The name of the tag'
end 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]) tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag not_found!('Tag') unless tag
...@@ -54,7 +54,7 @@ module API ...@@ -54,7 +54,7 @@ module API
optional :message, type: String, desc: 'Specifying a message creates an annotated tag' 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)' optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)'
end end
post ':id/repository/tags' do post ':id/repository/tags', :release_orchestration do
authorize_admin_tag authorize_admin_tag
result = ::Tags::CreateService.new(user_project, current_user) result = ::Tags::CreateService.new(user_project, current_user)
...@@ -86,7 +86,7 @@ module API ...@@ -86,7 +86,7 @@ module API
params do params do
requires :tag_name, type: String, desc: 'The name of the tag' requires :tag_name, type: String, desc: 'The name of the tag'
end 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 authorize_admin_tag
tag = user_project.repository.find_tag(params[:tag_name]) tag = user_project.repository.find_tag(params[:tag_name])
...@@ -112,7 +112,7 @@ module API ...@@ -112,7 +112,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
requires :description, type: String, desc: 'Release notes with markdown support' requires :description, type: String, desc: 'Release notes with markdown support'
end 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! authorize_create_release!
## ##
...@@ -144,7 +144,7 @@ module API ...@@ -144,7 +144,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
requires :description, type: String, desc: 'Release notes with markdown support' requires :description, type: String, desc: 'Release notes with markdown support'
end 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! authorize_update_release!
result = ::Releases::UpdateService result = ::Releases::UpdateService
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Templates < ::API::Base class Templates < ::API::Base
include PaginationParams include PaginationParams
feature_category :templates
GLOBAL_TEMPLATE_TYPES = { GLOBAL_TEMPLATE_TYPES = {
gitignores: { gitignores: {
gitlab_version: 8.8 gitlab_version: 8.8
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
class State < ::API::Base class State < ::API::Base
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
feature_category :infrastructure_as_code
default_format :json default_format :json
before do before do
......
...@@ -5,6 +5,8 @@ module API ...@@ -5,6 +5,8 @@ module API
class StateVersion < ::API::Base class StateVersion < ::API::Base
default_format :json default_format :json
feature_category :infrastructure_as_code
before do before do
authenticate! authenticate!
authorize! :read_terraform_state, user_project authorize! :read_terraform_state, user_project
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :issue_tracking
ISSUABLE_TYPES = { ISSUABLE_TYPES = {
'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) },
'issues' => ->(iid) { find_project_issue(iid) } 'issues' => ->(iid) { find_project_issue(iid) }
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
feature_category :continuous_integration
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Unleash < ::API::Base class Unleash < ::API::Base
include PaginationParams include PaginationParams
feature_category :feature_flags
namespace :feature_flags do namespace :feature_flags do
resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do params do
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class UsageData < ::API::Base class UsageData < ::API::Base
before { authenticate! } before { authenticate! }
feature_category :collection
namespace 'usage_data' do namespace 'usage_data' do
before do before do
not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true) not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true)
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module API module API
class UserCounts < ::API::Base class UserCounts < ::API::Base
feature_category :navigation
resource :user_counts do resource :user_counts do
desc 'Return the user specific counts' do desc 'Return the user specific counts' do
detail 'Open MR Count' 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