Commit aa621050 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 058e5f4b ef48555e
...@@ -11,6 +11,12 @@ export default { ...@@ -11,6 +11,12 @@ export default {
GlIcon, GlIcon,
}, },
props: { props: {
showHeader: {
type: Boolean,
required: false,
default: true,
},
sectionTitle: { sectionTitle: {
type: String, type: String,
required: true, required: true,
...@@ -84,7 +90,7 @@ export default { ...@@ -84,7 +90,7 @@ export default {
<template> <template>
<div> <div>
<gl-dropdown-section-header> <gl-dropdown-section-header v-if="showHeader">
<div class="gl-display-flex align-items-center" data-testid="section-header"> <div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span> <span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge> <gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
......
...@@ -8,9 +8,16 @@ import { ...@@ -8,9 +8,16 @@ import {
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { debounce } from 'lodash'; import { debounce, isArray } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants'; import {
ALL_REF_TYPES,
SEARCH_DEBOUNCE_MS,
DEFAULT_I18N,
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
} from '../constants';
import createStore from '../stores'; import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue'; import RefResultsSection from './ref_results_section.vue';
...@@ -28,6 +35,20 @@ export default { ...@@ -28,6 +35,20 @@ export default {
RefResultsSection, RefResultsSection,
}, },
props: { props: {
enabledRefTypes: {
type: Array,
required: false,
default: () => ALL_REF_TYPES,
validator: (val) =>
// It has to be an arrray
isArray(val) &&
// with at least one item
val.length > 0 &&
// and only "REF_TYPE_BRANCHES", "REF_TYPE_TAGS", and "REF_TYPE_COMMITS" are allowed
val.every((item) => ALL_REF_TYPES.includes(item)) &&
// and no duplicates are allowed
val.length === new Set(val).size,
},
value: { value: {
type: String, type: String,
required: false, required: false,
...@@ -62,17 +83,29 @@ export default { ...@@ -62,17 +83,29 @@ export default {
}; };
}, },
showBranchesSection() { showBranchesSection() {
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error); return (
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
);
}, },
showTagsSection() { showTagsSection() {
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error); return (
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
);
}, },
showCommitsSection() { showCommitsSection() {
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error); return (
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
);
}, },
showNoResults() { showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection; return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
}, },
showSectionHeaders() {
return this.enabledRefTypes.length > 1;
},
}, },
watch: { watch: {
// Keep the Vuex store synchronized if the parent // Keep the Vuex store synchronized if the parent
...@@ -97,10 +130,18 @@ export default { ...@@ -97,10 +130,18 @@ export default {
}, SEARCH_DEBOUNCE_MS); }, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId); this.setProjectId(this.projectId);
this.$watch(
'enabledRefTypes',
() => {
this.setEnabledRefTypes(this.enabledRefTypes);
this.search(this.query); this.search(this.query);
}, },
{ immediate: true },
);
},
methods: { methods: {
...mapActions(['setProjectId', 'setSelectedRef', 'search']), ...mapActions(['setEnabledRefTypes', 'setProjectId', 'setSelectedRef', 'search']),
focusSearchBox() { focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus(); this.$refs.searchBox.$el.querySelector('input').focus();
}, },
...@@ -170,6 +211,7 @@ export default { ...@@ -170,6 +211,7 @@ export default {
:selected-ref="selectedRef" :selected-ref="selectedRef"
:error="matches.branches.error" :error="matches.branches.error"
:error-message="i18n.branchesErrorMessage" :error-message="i18n.branchesErrorMessage"
:show-header="showSectionHeaders"
data-testid="branches-section" data-testid="branches-section"
@selected="selectRef($event)" @selected="selectRef($event)"
/> />
...@@ -185,6 +227,7 @@ export default { ...@@ -185,6 +227,7 @@ export default {
:selected-ref="selectedRef" :selected-ref="selectedRef"
:error="matches.tags.error" :error="matches.tags.error"
:error-message="i18n.tagsErrorMessage" :error-message="i18n.tagsErrorMessage"
:show-header="showSectionHeaders"
data-testid="tags-section" data-testid="tags-section"
@selected="selectRef($event)" @selected="selectRef($event)"
/> />
...@@ -200,6 +243,7 @@ export default { ...@@ -200,6 +243,7 @@ export default {
:selected-ref="selectedRef" :selected-ref="selectedRef"
:error="matches.commits.error" :error="matches.commits.error"
:error-message="i18n.commitsErrorMessage" :error-message="i18n.commitsErrorMessage"
:show-header="showSectionHeaders"
data-testid="commits-section" data-testid="commits-section"
@selected="selectRef($event)" @selected="selectRef($event)"
/> />
......
import { __ } from '~/locale'; import { __ } from '~/locale';
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
export const X_TOTAL_HEADER = 'x-total'; export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = 250; export const SEARCH_DEBOUNCE_MS = 250;
......
import Api from '~/api'; import Api from '~/api';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setEnabledRefTypes = ({ commit }, refTypes) =>
commit(types.SET_ENABLED_REF_TYPES, refTypes);
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId); export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setSelectedRef = ({ commit }, selectedRef) => export const setSelectedRef = ({ commit }, selectedRef) =>
commit(types.SET_SELECTED_REF, selectedRef); commit(types.SET_SELECTED_REF, selectedRef);
export const search = ({ dispatch, commit }, query) => { export const search = ({ state, dispatch, commit }, query) => {
commit(types.SET_QUERY, query); commit(types.SET_QUERY, query);
dispatch('searchBranches'); const dispatchIfRefTypeEnabled = (refType, action) => {
dispatch('searchTags'); if (state.enabledRefTypes.includes(refType)) {
dispatch('searchCommits'); dispatch(action);
}
};
dispatchIfRefTypeEnabled(REF_TYPE_BRANCHES, 'searchBranches');
dispatchIfRefTypeEnabled(REF_TYPE_TAGS, 'searchTags');
dispatchIfRefTypeEnabled(REF_TYPE_COMMITS, 'searchCommits');
}; };
export const searchBranches = ({ commit, state }) => { export const searchBranches = ({ commit, state }) => {
......
export const SET_ENABLED_REF_TYPES = 'SET_ENABLED_REF_TYPES';
export const SET_PROJECT_ID = 'SET_PROJECT_ID'; export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_SELECTED_REF = 'SET_SELECTED_REF'; export const SET_SELECTED_REF = 'SET_SELECTED_REF';
export const SET_QUERY = 'SET_QUERY'; export const SET_QUERY = 'SET_QUERY';
......
...@@ -4,6 +4,9 @@ import { X_TOTAL_HEADER } from '../constants'; ...@@ -4,6 +4,9 @@ import { X_TOTAL_HEADER } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_ENABLED_REF_TYPES](state, refTypes) {
state.enabledRefTypes = refTypes;
},
[types.SET_PROJECT_ID](state, projectId) { [types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId; state.projectId = projectId;
}, },
......
const createRefTypeState = () => ({
list: [],
totalCount: 0,
error: null,
});
export default () => ({ export default () => ({
enabledRefTypes: [],
projectId: null, projectId: null,
query: '', query: '',
matches: { matches: {
branches: { branches: createRefTypeState(),
list: [], tags: createRefTypeState(),
totalCount: 0, commits: createRefTypeState(),
error: null,
},
tags: {
list: [],
totalCount: 0,
error: null,
},
commits: {
list: [],
totalCount: 0,
error: null,
},
}, },
selectedRef: null, selectedRef: null,
requestCount: 0, requestCount: 0,
......
...@@ -46,6 +46,8 @@ class RootController < Dashboard::ProjectsController ...@@ -46,6 +46,8 @@ class RootController < Dashboard::ProjectsController
redirect_to(activity_dashboard_path) redirect_to(activity_dashboard_path)
when 'starred_project_activity' when 'starred_project_activity'
redirect_to(activity_dashboard_path(filter: 'starred')) redirect_to(activity_dashboard_path(filter: 'starred'))
when 'followed_user_activity'
redirect_to(activity_dashboard_path(filter: 'followed'))
when 'groups' when 'groups'
redirect_to(dashboard_groups_path) redirect_to(dashboard_groups_path)
when 'todos' when 'todos'
......
...@@ -29,6 +29,7 @@ module PreferencesHelper ...@@ -29,6 +29,7 @@ module PreferencesHelper
stars: _("Starred Projects"), stars: _("Starred Projects"),
project_activity: _("Your Projects' Activity"), project_activity: _("Your Projects' Activity"),
starred_project_activity: _("Starred Projects' Activity"), starred_project_activity: _("Starred Projects' Activity"),
followed_user_activity: _("Followed Users' Activity"),
groups: _("Your Groups"), groups: _("Your Groups"),
todos: _("Your To-Do List"), todos: _("Your To-Do List"),
issues: _("Assigned Issues"), issues: _("Assigned Issues"),
......
...@@ -272,7 +272,7 @@ class User < ApplicationRecord ...@@ -272,7 +272,7 @@ class User < ApplicationRecord
enum layout: { fixed: 0, fluid: 1 } enum layout: { fixed: 0, fluid: 1 }
# User's Dashboard preference # User's Dashboard preference
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8 } enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 }
# User's Project preference # User's Project preference
enum project_view: { readme: 0, activity: 1, files: 2 } enum project_view: { readme: 0, activity: 1, files: 2 }
......
...@@ -25,7 +25,11 @@ ...@@ -25,7 +25,11 @@
&bull; &bull;
- if total_count > recent_releases.count - if total_count > recent_releases.count
&bull; &bull;
= link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project) - more_text = n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }
- if milestone.project_milestone?
= link_to more_text, project_releases_path(milestone.project)
- else
= more_text
%div %div
= render('shared/milestone_expired', milestone: milestone) = render('shared/milestone_expired', milestone: milestone)
- if milestone.group_milestone? - if milestone.group_milestone?
......
---
title: "Add 'Followed User Activity' as dashboard user choices"
merge_request: 55165
author: Benj Fassbind @randombenj
type: added
---
title: Add runners api context metadata
merge_request: 55089
author:
type: changed
...@@ -171,8 +171,7 @@ certain arguments may also increase the complexity of a query. ...@@ -171,8 +171,7 @@ certain arguments may also increase the complexity of a query.
NOTE: NOTE:
The complexity limits may be revised in future, and additionally, the complexity The complexity limits may be revised in future, and additionally, the complexity
of a query may be altered. Changes to complexity can happen on `X.0` or `X.6` of a query may be altered.
releases without a deprecation period.
### Request timeout ### Request timeout
......
...@@ -108,12 +108,13 @@ select few, the amount of activity on the default Dashboard page can be ...@@ -108,12 +108,13 @@ select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine your default overwhelming. Changing this setting allows you to redefine your default
dashboard. dashboard.
You have 8 options here that you can use for your default dashboard view: You can include the following options for your default dashboard view:
- Your projects (default) - Your projects (default)
- Starred projects - Starred projects
- Your projects' activity - Your projects' activity
- Starred projects' activity - Starred projects' activity
- Followed Users' Activity
- Your groups - Your groups
- Your [To-Do List](../todos.md) - Your [To-Do List](../todos.md)
- Assigned Issues - Assigned Issues
......
...@@ -9,7 +9,11 @@ module Ci ...@@ -9,7 +9,11 @@ module Ci
belongs_to :namespace belongs_to :namespace
scope :current_month, -> { where(date: Time.current.utc.beginning_of_month) } scope :current_month, -> { where(date: beginning_of_month) }
def self.beginning_of_month(time = Time.current)
time.utc.beginning_of_month
end
# We should pretty much always use this method to access data for the current month # We should pretty much always use this method to access data for the current month
# since this will lazily create an entry if it doesn't exist. # since this will lazily create an entry if it doesn't exist.
......
...@@ -9,7 +9,11 @@ module Ci ...@@ -9,7 +9,11 @@ module Ci
belongs_to :project belongs_to :project
scope :current_month, -> { where(date: Time.current.beginning_of_month) } scope :current_month, -> { where(date: beginning_of_month) }
def self.beginning_of_month(time = Time.current)
time.utc.beginning_of_month
end
# We should pretty much always use this method to access data for the current month # We should pretty much always use this method to access data for the current month
# since this will lazily create an entry if it doesn't exist. # since this will lazily create an entry if it doesn't exist.
......
---
title: Fix 500 error on group milestones page when milestones are associated to more
than 3 releases
merge_request: 55540
author:
type: fixed
...@@ -16,7 +16,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -16,7 +16,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
end end
it 'does not raise exception if unique index is not violated' do it 'does not raise exception if unique index is not violated' do
expect { create(:ci_namespace_monthly_usage, namespace: namespace, date: 1.month.ago.utc.beginning_of_month) } expect { create(:ci_namespace_monthly_usage, namespace: namespace, date: described_class.beginning_of_month(1.month.ago)) }
.to change { described_class.count }.by(1) .to change { described_class.count }.by(1)
end end
end end
...@@ -31,7 +31,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -31,7 +31,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
expect(subject.amount_used).to eq(0) expect(subject.amount_used).to eq(0)
expect(subject.namespace).to eq(namespace) expect(subject.namespace).to eq(namespace)
expect(subject.date).to eq(Time.current.beginning_of_month) expect(subject.date).to eq(described_class.beginning_of_month)
end end
end end
end end
...@@ -42,7 +42,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do ...@@ -42,7 +42,7 @@ RSpec.describe Ci::Minutes::NamespaceMonthlyUsage do
context 'when namespace usage exists for previous months' do context 'when namespace usage exists for previous months' do
before do before do
create(:ci_namespace_monthly_usage, namespace: namespace, date: 2.months.ago.utc.beginning_of_month) create(:ci_namespace_monthly_usage, namespace: namespace, date: described_class.beginning_of_month(2.months.ago))
end end
it_behaves_like 'creates usage record' it_behaves_like 'creates usage record'
......
...@@ -16,7 +16,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -16,7 +16,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
end end
it 'does not raise exception if unique index is not violated' do it 'does not raise exception if unique index is not violated' do
expect { create(:ci_project_monthly_usage, project: project, date: 1.month.ago.utc.beginning_of_month) } expect { create(:ci_project_monthly_usage, project: project, date: described_class.beginning_of_month(1.month.ago)) }
.to change { described_class.count }.by(1) .to change { described_class.count }.by(1)
end end
end end
...@@ -31,7 +31,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -31,7 +31,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
expect(subject.amount_used).to eq(0) expect(subject.amount_used).to eq(0)
expect(subject.project).to eq(project) expect(subject.project).to eq(project)
expect(subject.date).to eq(Time.current.beginning_of_month) expect(subject.date).to eq(described_class.beginning_of_month)
end end
end end
end end
...@@ -42,7 +42,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do ...@@ -42,7 +42,7 @@ RSpec.describe Ci::Minutes::ProjectMonthlyUsage do
context 'when project usage exists for previous months' do context 'when project usage exists for previous months' do
before do before do
create(:ci_project_monthly_usage, project: project, date: 2.months.ago.utc.beginning_of_month) create(:ci_project_monthly_usage, project: project, date: described_class.beginning_of_month(2.months.ago))
end end
it_behaves_like 'creates usage record' it_behaves_like 'creates usage record'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'shared/milestones/_milestone.html.haml' do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user).tap { |user| project.add_maintainer(user) } }
let_it_be(:releases) { create_list(:release, 4, project: project) }
let_it_be(:milestone) { nil }
let(:more_text) { '1 more release' }
let(:link_href) { project_releases_path(project) }
before do
stub_licensed_features(group_milestone_project_releases: true)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:milestone).and_return(milestone)
allow(view).to receive(:issues_path).and_return('path/to/issues')
allow(view).to receive(:merge_requests_path).and_return('path/to/merge_requests')
end
context 'when a milestone is associated to a lot of releases' do
context 'when viewing a project milestone' do
let(:milestone) { create(:milestone, project: project, releases: releases) }
before do
assign(:project, project)
end
it 'renders "1 more release" as a link to the project\'s Releases page' do
render
expect(rendered).to have_link(more_text, href: link_href)
end
end
context 'when viewing a group milestone' do
let(:milestone) { create(:milestone, group: group, releases: releases) }
before do
assign(:group, group)
end
it 'renders "1 more release" as plain text instead of as a link', :aggregate_failures do
render
expect(rendered).not_to have_link(more_text, href: link_href)
expect(rendered).to have_content(more_text)
end
end
end
end
...@@ -34,12 +34,12 @@ module API ...@@ -34,12 +34,12 @@ module API
if runner_registration_token_valid? if runner_registration_token_valid?
# Create shared runner. Requires admin access # Create shared runner. Requires admin access
attributes.merge(runner_type: :instance_type) attributes.merge(runner_type: :instance_type)
elsif project = Project.find_by_runners_token(params[:token]) elsif @project = Project.find_by_runners_token(params[:token])
# Create a specific runner for the project # Create a specific runner for the project
attributes.merge(runner_type: :project_type, projects: [project]) attributes.merge(runner_type: :project_type, projects: [@project])
elsif group = Group.find_by_runners_token(params[:token]) elsif @group = Group.find_by_runners_token(params[:token])
# Create a specific runner for the group # Create a specific runner for the group
attributes.merge(runner_type: :group_type, groups: [group]) attributes.merge(runner_type: :group_type, groups: [@group])
else else
forbidden! forbidden!
end end
...@@ -81,12 +81,7 @@ module API ...@@ -81,12 +81,7 @@ module API
end end
resource :jobs do resource :jobs do
before do before { set_application_context }
Gitlab::ApplicationContext.push(
user: -> { current_job&.user },
project: -> { current_job&.project }
)
end
desc 'Request a job' do desc 'Request a job' do
success Entities::JobRequest::Response success Entities::JobRequest::Response
......
...@@ -71,6 +71,26 @@ module API ...@@ -71,6 +71,26 @@ module API
header 'Job-Status', job.status header 'Job-Status', job.status
forbidden!(reason) forbidden!(reason)
end end
def set_application_context
if current_job
Gitlab::ApplicationContext.push(
user: -> { current_job.user },
project: -> { current_job.project }
)
elsif current_runner&.project_type?
Gitlab::ApplicationContext.push(
project: -> do
projects = current_runner.projects.limit(2) # rubocop: disable CodeReuse/ActiveRecord
projects.first if projects.length == 1
end
)
elsif current_runner&.group_type?
Gitlab::ApplicationContext.push(
namespace: -> { current_runner.groups.first }
)
end
end
end end
end end
end end
...@@ -13201,6 +13201,9 @@ msgstr "" ...@@ -13201,6 +13201,9 @@ msgstr ""
msgid "Follow" msgid "Follow"
msgstr "" msgstr ""
msgid "Followed Users' Activity"
msgstr ""
msgid "Followed users" msgid "Followed users"
msgstr "" msgstr ""
......
...@@ -68,6 +68,18 @@ RSpec.describe RootController do ...@@ -68,6 +68,18 @@ RSpec.describe RootController do
end end
end end
context 'who has customized their dashboard setting for followed user activities' do
before do
user.dashboard = 'followed_user_activity'
end
it 'redirects to the activity list' do
get :index
expect(response).to redirect_to activity_dashboard_path(filter: 'followed')
end
end
context 'who has customized their dashboard setting for groups' do context 'who has customized their dashboard setting for groups' do
before do before do
user.dashboard = 'groups' user.dashboard = 'groups'
......
...@@ -7,7 +7,13 @@ import { trimText } from 'helpers/text_helper'; ...@@ -7,7 +7,13 @@ import { trimText } from 'helpers/text_helper';
import { ENTER_KEY } from '~/lib/utils/keys'; import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue'; import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants'; import {
X_TOTAL_HEADER,
DEFAULT_I18N,
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
} from '~/ref/constants';
import createStore from '~/ref/stores/'; import createStore from '~/ref/stores/';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -26,6 +32,7 @@ describe('Ref selector component', () => { ...@@ -26,6 +32,7 @@ describe('Ref selector component', () => {
let branchesApiCallSpy; let branchesApiCallSpy;
let tagsApiCallSpy; let tagsApiCallSpy;
let commitApiCallSpy; let commitApiCallSpy;
let requestSpies;
const createComponent = (props = {}, attrs = {}) => { const createComponent = (props = {}, attrs = {}) => {
wrapper = mount(RefSelector, { wrapper = mount(RefSelector, {
...@@ -58,6 +65,7 @@ describe('Ref selector component', () => { ...@@ -58,6 +65,7 @@ describe('Ref selector component', () => {
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]); .mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]); tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]); commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
mock mock
.onGet(`/api/v4/projects/${projectId}/repository/branches`) .onGet(`/api/v4/projects/${projectId}/repository/branches`)
...@@ -592,4 +600,86 @@ describe('Ref selector component', () => { ...@@ -592,4 +600,86 @@ describe('Ref selector component', () => {
}); });
}); });
}); });
describe('with non-default ref types', () => {
it.each`
enabledRefTypes | reqsCalled | reqsNotCalled
${[REF_TYPE_BRANCHES]} | ${['branchesApiCallSpy']} | ${['tagsApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_TAGS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_COMMITS]} | ${[]} | ${['branchesApiCallSpy', 'tagsApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
`(
'only calls $reqsCalled requests when $enabledRefTypes are enabled',
async ({ enabledRefTypes, reqsCalled, reqsNotCalled }) => {
createComponent({ enabledRefTypes });
await waitForRequests();
reqsCalled.forEach((req) => expect(requestSpies[req]).toHaveBeenCalledTimes(1));
reqsNotCalled.forEach((req) => expect(requestSpies[req]).not.toHaveBeenCalled());
},
);
it('only calls commitApiCallSpy when REF_TYPE_COMMITS is enabled', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_COMMITS] });
updateQuery('abcd1234');
await waitForRequests();
expect(commitApiCallSpy).toHaveBeenCalledTimes(1);
expect(branchesApiCallSpy).not.toHaveBeenCalled();
expect(tagsApiCallSpy).not.toHaveBeenCalled();
});
it('triggers another search if enabled ref types change', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES] });
await waitForRequests();
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).not.toHaveBeenCalled();
wrapper.setProps({
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
});
await waitForRequests();
expect(branchesApiCallSpy).toHaveBeenCalledTimes(2);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] });
updateQuery('abcd1234');
await waitForRequests();
expect(findBranchesSection().exists()).toBe(true);
expect(findCommitsSection().exists()).toBe(true);
wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
await waitForRequests();
expect(findBranchesSection().exists()).toBe(false);
expect(findCommitsSection().exists()).toBe(true);
});
it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
${REF_TYPE_TAGS} | ${findTagsSection} | ${[findBranchesSection, findCommitsSection]}
${REF_TYPE_COMMITS} | ${findCommitsSection} | ${[findBranchesSection, findTagsSection]}
`(
'hides section headers if a single ref type is enabled',
async ({ enabledRefType, findVisibleSection, findHiddenSections }) => {
createComponent({ enabledRefTypes: [enabledRefType] });
updateQuery('abcd1234');
await waitForRequests();
expect(findVisibleSection().exists()).toBe(true);
expect(findVisibleSection().find('[data-testid="section-header"]').exists()).toBe(false);
findHiddenSections.forEach((findHiddenSection) =>
expect(findHiddenSection().exists()).toBe(false),
);
},
);
});
}); });
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { ALL_REF_TYPES, REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '~/ref/constants';
import * as actions from '~/ref/stores/actions'; import * as actions from '~/ref/stores/actions';
import * as types from '~/ref/stores/mutation_types'; import * as types from '~/ref/stores/mutation_types';
import createState from '~/ref/stores/state'; import createState from '~/ref/stores/state';
...@@ -25,6 +26,14 @@ describe('Ref selector Vuex store actions', () => { ...@@ -25,6 +26,14 @@ describe('Ref selector Vuex store actions', () => {
state = createState(); state = createState();
}); });
describe('setEnabledRefTypes', () => {
it(`commits ${types.SET_ENABLED_REF_TYPES} with the enabled ref types`, () => {
testAction(actions.setProjectId, ALL_REF_TYPES, state, [
{ type: types.SET_PROJECT_ID, payload: ALL_REF_TYPES },
]);
});
});
describe('setProjectId', () => { describe('setProjectId', () => {
it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => { it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => {
const projectId = '4'; const projectId = '4';
...@@ -46,12 +55,23 @@ describe('Ref selector Vuex store actions', () => { ...@@ -46,12 +55,23 @@ describe('Ref selector Vuex store actions', () => {
describe('search', () => { describe('search', () => {
it(`commits ${types.SET_QUERY} with the new search query`, () => { it(`commits ${types.SET_QUERY} with the new search query`, () => {
const query = 'hello'; const query = 'hello';
testAction(actions.search, query, state, [{ type: types.SET_QUERY, payload: query }]);
});
it.each`
enabledRefTypes | expectedActions
${[REF_TYPE_BRANCHES]} | ${['searchBranches']}
${[REF_TYPE_COMMITS]} | ${['searchCommits']}
${[REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['searchBranches', 'searchTags', 'searchCommits']}
`(`dispatches fetch actions for enabled ref types`, ({ enabledRefTypes, expectedActions }) => {
const query = 'hello';
state.enabledRefTypes = enabledRefTypes;
testAction( testAction(
actions.search, actions.search,
query, query,
state, state,
[{ type: types.SET_QUERY, payload: query }], [{ type: types.SET_QUERY, payload: query }],
[{ type: 'searchBranches' }, { type: 'searchTags' }, { type: 'searchCommits' }], expectedActions.map((type) => ({ type })),
); );
}); });
}); });
......
import { X_TOTAL_HEADER } from '~/ref/constants'; import { X_TOTAL_HEADER, ALL_REF_TYPES } from '~/ref/constants';
import * as types from '~/ref/stores/mutation_types'; import * as types from '~/ref/stores/mutation_types';
import mutations from '~/ref/stores/mutations'; import mutations from '~/ref/stores/mutations';
import createState from '~/ref/stores/state'; import createState from '~/ref/stores/state';
...@@ -13,6 +13,7 @@ describe('Ref selector Vuex store mutations', () => { ...@@ -13,6 +13,7 @@ describe('Ref selector Vuex store mutations', () => {
describe('initial state', () => { describe('initial state', () => {
it('is created with the correct structure and initial values', () => { it('is created with the correct structure and initial values', () => {
expect(state).toEqual({ expect(state).toEqual({
enabledRefTypes: [],
projectId: null, projectId: null,
query: '', query: '',
...@@ -39,6 +40,14 @@ describe('Ref selector Vuex store mutations', () => { ...@@ -39,6 +40,14 @@ describe('Ref selector Vuex store mutations', () => {
}); });
}); });
describe(`${types.SET_ENABLED_REF_TYPES}`, () => {
it('sets the enabled ref types', () => {
mutations[types.SET_ENABLED_REF_TYPES](state, ALL_REF_TYPES);
expect(state.enabledRefTypes).toBe(ALL_REF_TYPES);
});
});
describe(`${types.SET_PROJECT_ID}`, () => { describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => { it('updates the project ID', () => {
const newProjectId = '4'; const newProjectId = '4';
......
...@@ -29,6 +29,7 @@ RSpec.describe PreferencesHelper do ...@@ -29,6 +29,7 @@ RSpec.describe PreferencesHelper do
['Starred Projects', 'stars'], ['Starred Projects', 'stars'],
["Your Projects' Activity", 'project_activity'], ["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'], ["Starred Projects' Activity", 'starred_project_activity'],
["Followed Users' Activity", 'followed_user_activity'],
["Your Groups", 'groups'], ["Your Groups", 'groups'],
["Your To-Do List", 'todos'], ["Your To-Do List", 'todos'],
["Assigned Issues", 'issues'], ["Assigned Issues", 'issues'],
......
...@@ -797,6 +797,50 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do ...@@ -797,6 +797,50 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
end end
describe 'setting the application context' do
subject { request_job }
context 'when triggered by a user' do
let(:job) { create(:ci_build, user: user, project: project) }
subject { request_job(id: job.id) }
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: user.username, project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context', 3 do
# Extra queries: User, Project, Route
let(:subject_proc) { proc { request_job(id: job.id) } }
end
end
context 'when the runner is of project type' do
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context', 2 do
# Extra queries: Project, Route
let(:subject_proc) { proc { request_job } }
end
end
context 'when the runner is of group type' do
let(:group) { create(:group) }
let(:runner) { create(:ci_runner, :group, groups: [group]) }
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { root_namespace: group.full_path_components.first } }
end
it_behaves_like 'not executing any extra queries for the application context', 2 do
# Extra queries: Group, Route
let(:subject_proc) { proc { request_job } }
end
end
end
def request_job(token = runner.token, **params) def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update) new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' } post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' }
......
...@@ -35,6 +35,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do ...@@ -35,6 +35,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end end
context 'when valid token is provided' do context 'when valid token is provided' do
def request
post api('/runners'), params: { token: token }
end
it 'creates runner with default values' do it 'creates runner with default values' do
post api('/runners'), params: { token: registration_token } post api('/runners'), params: { token: registration_token }
...@@ -51,9 +55,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do ...@@ -51,9 +55,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when project token is used' do context 'when project token is used' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:token) { project.runners_token }
it 'creates project runner' do it 'creates project runner' do
post api('/runners'), params: { token: project.runners_token } request
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(project.runners.size).to eq(1) expect(project.runners.size).to eq(1)
...@@ -62,13 +67,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do ...@@ -62,13 +67,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(runner.token).not_to eq(project.runners_token) expect(runner.token).not_to eq(project.runners_token)
expect(runner).to be_project_type expect(runner).to be_project_type
end end
it_behaves_like 'storing arguments in the application context' do
subject { request }
let(:expected_params) { { project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
end end
context 'when group token is used' do context 'when group token is used' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:token) { group.runners_token }
it 'creates a group runner' do it 'creates a group runner' do
post api('/runners'), params: { token: group.runners_token } request
expect(response).to have_gitlab_http_status(:created) expect(response).to have_gitlab_http_status(:created)
expect(group.runners.reload.size).to eq(1) expect(group.runners.reload.size).to eq(1)
...@@ -77,6 +93,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do ...@@ -77,6 +93,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(runner.token).not_to eq(group.runners_token) expect(runner.token).not_to eq(group.runners_token)
expect(runner).to be_group_type expect(runner).to be_group_type
end end
it_behaves_like 'storing arguments in the application context' do
subject { request }
let(:expected_params) { { root_namespace: group.full_path_components.first } }
end
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
end end
end end
......
...@@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do ...@@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do
hash.transform_keys! { |key| "meta.#{key}" } hash.transform_keys! { |key| "meta.#{key}" }
end end
end end
RSpec.shared_examples 'not executing any extra queries for the application context' do |expected_extra_queries = 0|
it 'does not execute more queries than without adding anything to the application context' do
# Call the subject once to memoize all factories being used for the spec, so they won't
# add any queries to the expectation.
subject_proc.call
expect do
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
subject_proc.call
end.to issue_same_number_of_queries_as {
allow(Gitlab::ApplicationContext).to receive(:push)
subject_proc.call
}.with_threshold(expected_extra_queries).ignoring_cached_queries
end
end
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