Commit d90b6ee0 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-09-14' into 'master'

CE upstream - 2018-09-14 09:21 UTC

See merge request gitlab-org/gitlab-ee!7374
parents c6d2ac22 d2abdaaf
......@@ -11,8 +11,10 @@ export default () => ({
endpoint: '',
basePath: '',
commit: null,
startVersion: null,
diffFiles: [],
mergeRequestDiffs: [],
mergeRequestDiff: null,
diffLineCommentForms: {},
diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
});
......@@ -3,10 +3,10 @@ import * as getters from '../getters';
import mutations from '../mutations';
import createState from './diff_state';
export default {
export default () => ({
namespaced: true,
state: createState(),
getters,
actions,
mutations,
};
});
......@@ -3,7 +3,7 @@ import {
getParameterByName,
getUrlParamsArray,
} from '~/lib/utils/common_utils';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import FilteredSearchContainer from './container';
......@@ -23,7 +23,7 @@ export default class FilteredSearchManager {
isGroup = false,
isGroupAncestor = true,
isGroupDecendent = false,
filteredSearchTokenKeys = IssuesFilteredSearchTokenKeys,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
this.isGroup = isGroup;
......
......@@ -71,7 +71,7 @@ export const conditions = [{
value: 'none',
}];
const IssuesFilteredSearchTokenKeys =
const IssuableFilteredSearchTokenKeys =
new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions);
export default IssuesFilteredSearchTokenKeys;
export default IssuableFilteredSearchTokenKeys;
......@@ -9,7 +9,7 @@ Vue.use(Vuex);
export default new Vuex.Store({
modules: {
page: mrPageModule,
notes: notesModule,
diffs: diffsModule,
notes: notesModule(),
diffs: diffsModule(),
},
});
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import module from './modules';
import notesModule from './modules';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
state: module.state,
actions,
getters,
mutations,
});
new Vuex.Store(notesModule());
......@@ -2,7 +2,7 @@ import * as actions from '../actions';
import * as getters from '../getters';
import mutations from '../mutations';
export default {
export default () => ({
state: {
discussions: [],
targetNoteHash: null,
......@@ -24,4 +24,4 @@ export default {
actions,
getters,
mutations,
};
});
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
filteredSearchTokenKeys: IssuesFilteredSearchTokenKeys,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
});
......@@ -2,14 +2,14 @@ import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
filteredSearchTokenKeys: IssuesFilteredSearchTokenKeys,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
......
class Admin::RunnersController < Admin::ApplicationController
before_action :runner, except: :index
# rubocop: disable CodeReuse/ActiveRecord
def index
finder = Admin::RunnersFinder.new(params: params)
@runners = finder.execute
@active_runners_cnt = Ci::Runner.online.count
@active_runners_count = Ci::Runner.online.count
@sort = finder.sort_key
end
# rubocop: enable CodeReuse/ActiveRecord
def show
assign_builds_and_projects
......
......@@ -22,7 +22,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
private
def group_milestones
groups = GroupsFinder.new(current_user, all_available: true).execute
groups = GroupsFinder.new(current_user, all_available: false).execute
DashboardGroupMilestone.build_collection(groups)
end
......
......@@ -43,8 +43,7 @@ class Admin::RunnersFinder < UnionFinder
end
def sort!
sort = sort_key == 'contacted_asc' ? { contacted_at: :asc } : { created_at: :desc }
@runners = @runners.order(sort)
@runners = @runners.order_by(sort_key)
end
def paginate!
......
......@@ -32,6 +32,12 @@ module Ci
scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
# The following query using negation is cheaper than using `contacted_at <= ?`
# because there are less runners online than have been created. The
# resulting query is quickly finding online ones and then uses the regular
# indexed search and rejects the ones that are in the previous set. If we
# did `contacted_at <= ?` the query would effectively have to do a seq
# scan.
scope :offline, -> { where.not(id: online) }
scope :ordered, -> { order(id: :desc) }
......@@ -67,6 +73,9 @@ module Ci
.project_type
end
scope :order_contacted_at_asc, -> { order(contacted_at: :asc) }
scope :order_created_at_desc, -> { order(created_at: :desc) }
validate :tag_constraints
validates :access_level, presence: true
validates :runner_type, presence: true
......@@ -119,6 +128,14 @@ module Ci
ONLINE_CONTACT_TIMEOUT.ago
end
def self.order_by(order)
if order == 'contacted_asc'
order_contacted_at_asc
else
order_created_at_desc
end
end
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
......
......@@ -73,19 +73,10 @@ module Clusters
"clientSecret" => oauth_application.secret,
"callbackUrl" => callback_url
}
},
"singleuser" => {
"extraEnv" => {
"GITLAB_PROJECT_ID" => project_id
}
}
}
end
def project_id
cluster&.project&.id
end
def gitlab_url
Gitlab.config.gitlab.url
end
......
......@@ -13,7 +13,7 @@ class DashboardGroupMilestone < GlobalMilestone
end
def self.build_collection(groups)
MilestonesFinder.new(group_ids: groups.pluck(:id)).execute.map { |m| new(m) } # rubocop: disable CodeReuse/Finder
MilestonesFinder.new(group_ids: groups.select(:id)).execute.map { |m| new(m) } # rubocop: disable CodeReuse/Finder
end
override :group_milestone?
......
......@@ -47,7 +47,7 @@
.bs-callout
%p
= _('Runners currently online: %{active_runners_cnt}') % { active_runners_cnt: @active_runners_cnt }
= _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count }
.row-content-block.second-block
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
......@@ -68,13 +68,13 @@
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link
= button_tag class: %w[btn btn-link] do
= icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
= button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue
%i.fa{ class: "#{'{{icon}}'}" }
......@@ -86,9 +86,9 @@
%ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_STATUSES.each do |status|
%li.filter-dropdown-item{ data: { value: status } }
%button.btn.btn-link
= button_tag class: %w[btn btn-link] do
= status.titleize
%button.clear-search.hidden{ type: 'button' }
= button_tag class: %w[clear-search hidden] do
= icon('times')
.filter-dropdown-container
= render 'sort_dropdown'
......
---
title: Filter group milestones based on user membership.
merge_request: 21660
author:
type: fixed
......@@ -3,7 +3,7 @@ import {
tokenKeys,
alternativeTokenKeys,
conditions,
} from '~/filtered_search/issues_filtered_search_token_keys';
} from '~/filtered_search/issuable_filtered_search_token_keys';
const weightTokenKey = {
key: 'weight',
......@@ -27,6 +27,11 @@ const weightConditions = [
},
];
/**
* Filter tokens for issues in EE.
*
* @type {FilteredSearchTokenKeys}
*/
const IssuesFilteredSearchTokenKeysEE = new FilteredSearchTokenKeys(
[...tokenKeys, weightTokenKey],
alternativeTokenKeys,
......
......@@ -14,7 +14,7 @@ module API
use :pagination
end
get do
runners = filter_runners(current_user.ci_owned_runners, params[:scope], only: Ci::Runner::AVAILABLE_STATUSES)
runners = filter_runners(current_user.ci_owned_runners, params[:scope], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
present paginate(runners), with: Entities::Runner
end
......@@ -160,12 +160,10 @@ module API
end
helpers do
def filter_runners(runners, scope, only: nil)
def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
return runners unless scope.present?
available_scopes = only || ::Ci::Runner::AVAILABLE_SCOPES
unless available_scopes.include?(scope)
unless allowed_scopes.include?(scope)
render_api_error!('Scope contains invalid value', 400)
end
......
......@@ -6480,7 +6480,7 @@ msgstr ""
msgid "Runners can be placed on separate users, servers, even on your local machine."
msgstr ""
msgid "Runners currently online: %{active_runners_cnt}"
msgid "Runners currently online: %{active_runners_count}"
msgstr ""
msgid "Runners page"
......
......@@ -3,9 +3,11 @@ require 'spec_helper'
describe Dashboard::MilestonesController do
let(:project) { create(:project) }
let(:group) { create(:group) }
let(:public_group) { create(:group, :public) }
let(:user) { create(:user) }
let(:project_milestone) { create(:milestone, project: project) }
let(:group_milestone) { create(:milestone, group: group) }
let!(:public_milestone) { create(:milestone, group: public_group) }
let(:milestone) do
DashboardMilestone.build(
[project],
......@@ -43,13 +45,13 @@ describe Dashboard::MilestonesController do
end
describe "#index" do
it 'should contain group and project milestones' do
it 'returns group and project milestones to which the user belongs' do
get :index, format: :json
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
expect(json_response.map { |i| i["first_milestone"]["id"] }).to include(group_milestone.id, project_milestone.id)
expect(json_response.map { |i| i["group_name"] }).to include(group.name)
expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id])
expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name)
end
end
end
......@@ -17,8 +17,9 @@ describe 'Dashboard > Milestones' do
let(:project) { create(:project, namespace: user.namespace) }
let!(:milestone) { create(:milestone, project: project) }
let!(:milestone2) { create(:milestone, group: group) }
before do
project.add_maintainer(user)
group.add_developer(user)
sign_in(user)
visit dashboard_milestones_path
end
......
// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import createDiffsStore from '../create_diffs_store';
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
const Component = Vue.extend(App);
let vm;
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
// setup component
const store = createDiffsStore();
store.state.diffs.isLoading = false;
vm = mountComponentWithStore(Component, {
store,
props: {
endpoint: `${TEST_HOST}/diff/endpoint`,
projectPath: 'namespace/project',
currentUser: {},
},
});
});
afterEach(() => {
// reset globals
window.mrTabs = oldMrTabs;
// reset component
vm.$destroy();
});
it('shows comments message, with commit', done => {
vm.$store.state.diffs.commit = {};
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainText('Only comments from the following commit are shown below');
})
.then(done)
.catch(done.fail);
});
it('shows comments message, with old mergeRequestDiff', done => {
vm.$store.state.diffs.mergeRequestDiff = { latest: false };
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainText("Not all comments are displayed because you're viewing an old version of the diff.");
})
.then(done)
.catch(done.fail);
});
it('shows comments message, with startVersion', done => {
vm.$store.state.diffs.startVersion = 'test';
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainText("Not all comments are displayed because you're comparing two versions of the diff.");
})
.then(done)
.catch(done.fail);
});
});
......@@ -8,7 +8,7 @@ describe('ChangedFiles', () => {
const Component = Vue.extend(changedFiles);
const store = new Vuex.Store({
modules: {
diffs: diffsModule,
diffs: diffsModule(),
},
});
......
......@@ -16,8 +16,8 @@ describe('diff_file_header', () => {
const store = new Vuex.Store({
modules: {
diffs: diffsModule,
notes: notesModule,
diffs: diffsModule(),
notes: notesModule(),
},
});
......@@ -450,13 +450,14 @@ describe('diff_file_header', () => {
propsCopy.diffFile.deletedFile = true;
const discussionGetter = () => [diffDiscussionMock];
notesModule.getters.discussions = discussionGetter;
const notesModuleMock = notesModule();
notesModuleMock.getters.discussions = discussionGetter;
vm = mountComponentWithStore(Component, {
props: propsCopy,
store: new Vuex.Store({
modules: {
diffs: diffsModule,
notes: notesModule,
diffs: diffsModule(),
notes: notesModuleMock,
},
}),
});
......
import Vue from 'vue';
import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
Vue.use(Vuex);
export default function createDiffsStore() {
return new Vuex.Store({
modules: {
diffs: diffsModule(),
notes: notesModule(),
},
});
}
import Vue from 'vue';
import eventHub from '~/filtered_search/event_hub';
import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
const createComponent = (propsData) => {
const Component = Vue.extend(RecentSearchesDropdownContent);
......@@ -18,14 +18,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim();
describe('RecentSearchesDropdownContent', () => {
const propsDataWithoutItems = {
items: [],
allowedKeys: IssuesFilteredSearchTokenKeys.getKeys(),
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
};
const propsDataWithItems = {
items: [
'foo',
'author:@root label:~foo bar',
],
allowedKeys: IssuesFilteredSearchTokenKeys.getKeys(),
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
};
let vm;
......
import DropdownUtils from '~/filtered_search/dropdown_utils';
import DropdownUser from '~/filtered_search/dropdown_user';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown User', () => {
describe('getSearchInput', () => {
......@@ -14,7 +14,7 @@ describe('Dropdown User', () => {
spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new DropdownUser({
tokenKeys: IssuesFilteredSearchTokenKeys,
tokenKeys: IssuableFilteredTokenKeys,
});
});
......
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
......@@ -137,7 +137,7 @@ describe('Dropdown Utils', () => {
`);
input = document.getElementById('test');
allowedKeys = IssuesFilteredSearchTokenKeys.getKeys();
allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
});
function config() {
......
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import '~/lib/utils/common_utils';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
......@@ -86,7 +86,7 @@ describe('Filtered Search Manager', function () {
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
isLocalStorageAvailable,
allowedKeys: IssuesFilteredSearchTokenKeys.getKeys(),
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
});
});
});
......
import IssuesFilteredSearchTokenKeys from '~/filtered_search/issues_filtered_search_token_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
describe('Filtered Search Tokenizer', () => {
const allowedKeys = IssuesFilteredSearchTokenKeys.getKeys();
const allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
describe('processTokens', () => {
it('returns for input containing only search value', () => {
......
......@@ -797,4 +797,22 @@ describe Ci::Runner do
expect { subject.destroy }.to change { described_class.count }.by(-1)
end
end
describe '.order_by' do
it 'supports ordering by the contact date' do
runner1 = create(:ci_runner, contacted_at: 1.year.ago)
runner2 = create(:ci_runner, contacted_at: 1.month.ago)
runners = described_class.order_by('contacted_asc')
expect(runners).to eq([runner1, runner2])
end
it 'supports ordering by the creation date' do
runner1 = create(:ci_runner, created_at: 1.year.ago)
runner2 = create(:ci_runner, created_at: 1.month.ago)
runners = described_class.order_by('created_asc')
expect(runners).to eq([runner2, runner1])
end
end
end
......@@ -108,17 +108,8 @@ describe Clusters::Applications::Jupyter do
expect(values).to include('rbac')
expect(values).to include('proxy')
expect(values).to include('auth')
expect(values).to include('singleuser')
expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
end
context 'when cluster belongs to a project' do
let(:project) { application.cluster.first_project }
it 'sets GitLab project id' do
expect(values).to match(/GITLAB_PROJECT_ID: '?#{project.id}/)
end
end
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