Commit cd8b5987 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-10-29' into 'master'

CE upstream - 2018-10-29 11:21 UTC

Closes #7760

See merge request gitlab-org/gitlab-ee!8131
parents 6a0e1212 b306a0c7
......@@ -58,10 +58,15 @@ export const alternativeTokenKeys = [
export const conditions = [
{
url: 'assignee_id=0',
url: 'assignee_id=None',
tokenKey: 'assignee',
value: 'none',
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
value: 'any',
},
{
url: 'milestone_title=No+Milestone',
tokenKey: 'milestone',
......
......@@ -4,12 +4,13 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
include MilestoneActions
before_action :projects
before_action :groups, only: :index
before_action :milestone, only: [:show, :merge_requests, :participants, :labels]
def index
respond_to do |format|
format.html do
@milestone_states = GlobalMilestone.states_count(@projects)
@milestone_states = Milestone.states_count(@projects.select(:id), @groups.select(:id))
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
......@@ -42,4 +43,8 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
@milestone = DashboardMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
def groups
@groups ||= GroupsFinder.new(current_user, state_all: true).execute
end
end
......@@ -12,7 +12,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def index
respond_to do |format|
format.html do
@milestone_states = GlobalMilestone.states_count(group_projects, group)
@milestone_states = Milestone.states_count(group_projects, [group])
@milestones = Kaminari.paginate_array(milestones).page(params[:page])
end
format.json do
......
......@@ -22,6 +22,12 @@ module Clusters
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group'
has_one :group, through: :cluster_group, class_name: '::Group'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
......@@ -40,8 +46,12 @@ module Clusters
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
validates :cluster_type, presence: true
validate :restrict_modification, on: :update
validate :no_groups, unless: :group_type?
validate :no_projects, unless: :project_type?
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
......@@ -52,6 +62,12 @@ module Clusters
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
enum cluster_type: {
instance_type: 1,
group_type: 2,
project_type: 3
}
enum platform_type: {
kubernetes: 1
}
......@@ -124,5 +140,17 @@ module Clusters
true
end
def no_groups
if groups.any?
errors.add(:cluster, 'cannot have groups assigned')
end
end
def no_projects
if projects.any?
errors.add(:cluster, 'cannot have projects assigned')
end
end
end
end
# frozen_string_literal: true
module Clusters
class Group < ActiveRecord::Base
self.table_name = 'cluster_groups'
belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :group, class_name: '::Group'
end
end
......@@ -42,6 +42,7 @@ module DeploymentPlatform
{
name: 'kubernetes-template',
projects: [self],
cluster_type: :project_type,
provider_type: :user,
platform_type: :kubernetes,
platform_kubernetes_attributes: platform_kubernetes_attributes_from_service_template
......
......@@ -36,50 +36,6 @@ class GlobalMilestone
new(title, child_milestones)
end
def self.states_count(projects, group = nil)
legacy_group_milestones_count = legacy_group_milestone_states_count(projects)
group_milestones_count = group_milestones_states_count(group)
legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count|
legacy_group_milestones_count + group_milestones_count
end
end
def self.group_milestones_states_count(group)
return STATE_COUNT_HASH unless group
params = { group_ids: [group.id], state: 'all' }
relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
grouped_by_state = relation.reorder(nil).group(:state).count
{
opened: grouped_by_state['active'] || 0,
closed: grouped_by_state['closed'] || 0,
all: grouped_by_state.values.sum
}
end
# Counts the legacy group milestones which must be grouped by title
def self.legacy_group_milestone_states_count(projects)
return STATE_COUNT_HASH unless projects
params = { project_ids: projects.map(&:id), state: 'all' }
relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder
project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count
opened = count_by_state(project_milestones_by_state_and_title, 'active')
closed = count_by_state(project_milestones_by_state_and_title, 'closed')
all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count
{
opened: opened,
closed: closed,
all: all
}
end
def self.count_by_state(milestones_by_state_and_title, state)
milestones_by_state_and_title.count do |(milestone_state, _), _|
milestone_state == state
......
......@@ -43,6 +43,9 @@ class Group < Namespace
has_many :boards
has_many :badges, class_name: 'GroupBadge'
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
......
......@@ -174,6 +174,22 @@ class Milestone < ActiveRecord::Base
sorted.with_order_id_desc
end
def self.states_count(projects, groups = nil)
return STATE_COUNT_HASH unless projects || groups
counts = Milestone
.for_projects_and_groups(projects&.map(&:id), groups&.map(&:id))
.reorder(nil)
.group(:state)
.count
{
opened: counts['active'] || 0,
closed: counts['closed'] || 0,
all: counts.values.sum
}
end
##
# Returns the String necessary to reference this Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
......
......@@ -11,9 +11,9 @@ module Clusters
end
def execute(project:, access_token: nil)
raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project)
raise ArgumentError, _('Instance does not support multiple Kubernetes clusters') unless can_create_cluster?(project)
cluster_params = params.merge(user: current_user, projects: [project])
cluster_params = params.merge(user: current_user, cluster_type: :project_type, projects: [project])
cluster_params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token
end
......
......@@ -61,7 +61,10 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
= _('No Assignee')
= _('None')
%li.filter-dropdown-item{ data: { value: 'any' } }
%button.btn.btn-link{ type: 'button' }
= _('Any')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
......@@ -81,7 +84,7 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link{ type: 'button' }
= _('Upcoming')
%li.filter-dropdown-item{ 'data-value' => 'started' }
%li.filter-dropdown-item{ data: { value: 'started' } }
%button.btn.btn-link{ type: 'button' }
= _('Started')
%li.divider.droplab-item-ignore
......
---
title: Adds model and migrations to enable group level clusters
merge_request: 22307
author:
type: other
---
title: Add None/Any option for assignee_id in search bar
merge_request: 22599
author: Heinrich Lee Yu
type: added
---
title: Fixing count on Milestones
merge_request: 21446
author:
type: fixed
......@@ -87,6 +87,7 @@ Rails.application.routes.draw do
get 'ide' => 'ide#index'
get 'ide/*vueroute' => 'ide#index', format: false
draw :operations
draw :instance_statistics
end
......
# frozen_string_literal: true
class CreateClusterGroups < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :cluster_groups do |t|
t.references :cluster, null: false, foreign_key: { on_delete: :cascade }
t.references :group, null: false, index: true
t.index [:cluster_id, :group_id], unique: true
t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
end
end
end
# frozen_string_literal: true
class AddClusterTypeToClusters < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
PROJECT_CLUSTER_TYPE = 3
disable_ddl_transaction!
def up
add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE)
end
def down
remove_column(:clusters, :cluster_type)
end
end
......@@ -717,6 +717,14 @@ ActiveRecord::Schema.define(version: 20181017131623) do
add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
create_table "cluster_groups", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "group_id", null: false
end
add_index "cluster_groups", ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree
add_index "cluster_groups", ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree
create_table "cluster_platforms_kubernetes", force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime "created_at", null: false
......@@ -772,6 +780,7 @@ ActiveRecord::Schema.define(version: 20181017131623) do
t.boolean "enabled", default: true
t.string "name", null: false
t.string "environment_scope", default: "*", null: false
t.integer "cluster_type", limit: 2, default: 3, null: false
end
add_index "clusters", ["enabled"], name: "index_clusters_on_enabled", using: :btree
......@@ -3225,6 +3234,8 @@ ActiveRecord::Schema.define(version: 20181017131623) do
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade
add_foreign_key "cluster_groups", "clusters", on_delete: :cascade
add_foreign_key "cluster_groups", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "cluster_platforms_kubernetes", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "clusters", on_delete: :cascade
add_foreign_key "cluster_projects", "projects", on_delete: :cascade
......
......@@ -134,7 +134,7 @@ Parameters:
"description": "Amazing release. Wow"
},
"name": "v1.0.0",
"target: "2695effb5807a22ff3d138d593fd856244e155e7",
"target": "2695effb5807a22ff3d138d593fd856244e155e7",
"message": null
}
```
......
......@@ -176,8 +176,8 @@ Clicking on the current board name in the upper left corner will reveal a
menu from where you can create another Issue Board and rename or delete the
existing one.
Clicking on the main issue board link will take you to the last board
you visited.
When you're revisiting an issue board in a project or group with multiple boards,
GitLab will automatically load the last board you visited.
NOTE: **Note:**
The Multiple Issue Boards feature is available for
......
......@@ -40,6 +40,16 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
### Filtering by **None** / **Any**
Some filter fields like milestone and assignee, allow you to filter by **None** or **Any**.
![filter by none any](img/issues_filter_none_any.png)
Selecting **None** returns results that have an empty value for that field. E.g.: no milestone, no assignee.
Selecting **Any** does the opposite. It returns results that have a non-empty value for that field.
### Searching for specific terms
You can filter issues and merge requests by specific terms included in titles or descriptions.
......
# frozen_string_literal: true
# Placeholder for https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7341
# Added to resolve https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8131
......@@ -5269,9 +5269,6 @@ msgstr ""
msgid "No"
msgstr ""
msgid "No Assignee"
msgstr ""
msgid "No Label"
msgstr ""
......
......@@ -45,6 +45,8 @@ describe Dashboard::MilestonesController do
end
describe "#index" do
render_views
it 'returns group and project milestones to which the user belongs' do
get :index, format: :json
......@@ -53,5 +55,12 @@ describe Dashboard::MilestonesController do
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
it 'should contain group and project milestones to which the user belongs to' do
get :index
expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>")
expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
end
end
end
......@@ -37,7 +37,7 @@ describe Groups::BoardsController do
allow(visited).to receive(:board_id).and_return(12)
allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
list_boards
list_boards format: :html
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
......
......@@ -2,13 +2,28 @@ FactoryBot.define do
factory :cluster, class: Clusters::Cluster do
user
name 'test-cluster'
cluster_type :project_type
trait :instance do
cluster_type { Clusters::Cluster.cluster_types[:instance_type] }
end
trait :project do
cluster_type { Clusters::Cluster.cluster_types[:project_type] }
before(:create) do |cluster, evaluator|
cluster.projects << create(:project, :repository)
end
end
trait :group do
cluster_type { Clusters::Cluster.cluster_types[:group_type] }
before(:create) do |cluster, evalutor|
cluster.groups << create(:group)
end
end
trait :provided_by_user do
provider_type :user
platform_type :kubernetes
......
......@@ -106,7 +106,7 @@ describe 'Issue Boards add issue modal filtering', :js do
it 'filters by unassigned' do
set_filter('assignee')
click_filter_link('No Assignee')
click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
......
......@@ -95,9 +95,9 @@ describe 'Group milestones' do
end
it 'counts milestones correctly' do
expect(find('.top-area .active .badge').text).to eq("2")
expect(find('.top-area .closed .badge').text).to eq("2")
expect(find('.top-area .all .badge').text).to eq("4")
expect(find('.top-area .active .badge').text).to eq("3")
expect(find('.top-area .closed .badge').text).to eq("3")
expect(find('.top-area .all .badge').text).to eq("6")
end
it 'lists legacy group milestones and group milestones' do
......
......@@ -156,13 +156,21 @@ describe 'Dropdown assignee', :js do
expect_filtered_search_input_empty
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
it 'selects `None`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('none')])
expect_filtered_search_input_empty
end
it 'selects `Any`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect_tokens([assignee_token('any')])
expect_filtered_search_input_empty
end
end
describe 'selecting from dropdown without Ajax call' do
......
......@@ -118,7 +118,7 @@ describe 'Visual tokens', :js do
describe 'selecting static option from dropdown' do
before do
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
end
it 'changes value in visual token' do
......
......@@ -4,7 +4,10 @@ require 'spec_helper'
describe Clusters::Cluster do
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
it { is_expected.to have_many(:cluster_groups) }
it { is_expected.to have_many(:groups) }
it { is_expected.to have_one(:provider_gcp) }
it { is_expected.to have_one(:platform_kubernetes) }
it { is_expected.to have_one(:application_helm) }
......@@ -178,6 +181,53 @@ describe Clusters::Cluster do
it { expect(cluster.update(enabled: false)).to be_truthy }
end
end
describe 'cluster_type validations' do
let(:instance_cluster) { create(:cluster, :instance) }
let(:group_cluster) { create(:cluster, :group) }
let(:project_cluster) { create(:cluster, :project) }
it 'validates presence' do
cluster = build(:cluster, :project, cluster_type: nil)
expect(cluster).not_to be_valid
expect(cluster.errors.full_messages).to include("Cluster type can't be blank")
end
context 'project_type cluster' do
it 'does not allow setting group' do
project_cluster.groups << build(:group)
expect(project_cluster).not_to be_valid
expect(project_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
end
end
context 'group_type cluster' do
it 'does not allow setting project' do
group_cluster.projects << build(:project)
expect(group_cluster).not_to be_valid
expect(group_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
end
end
context 'instance_type cluster' do
it 'does not allow setting group' do
instance_cluster.groups << build(:group)
expect(instance_cluster).not_to be_valid
expect(instance_cluster.errors.full_messages).to include('Cluster cannot have groups assigned')
end
it 'does not allow setting project' do
instance_cluster.projects << build(:project)
expect(instance_cluster).not_to be_valid
expect(instance_cluster.errors.full_messages).to include('Cluster cannot have projects assigned')
end
end
end
end
describe '#provider' do
......@@ -229,6 +279,23 @@ describe Clusters::Cluster do
end
end
describe '#group' do
subject { cluster.group }
context 'when cluster belongs to a group' do
let(:cluster) { create(:cluster, :group) }
let(:group) { cluster.groups.first }
it { is_expected.to eq(group) }
end
context 'when cluster does not belong to any group' do
let(:cluster) { create(:cluster) }
it { is_expected.to be_nil }
end
end
describe '#applications' do
set(:cluster) { create(:cluster) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Group do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to belong_to(:group) }
end
......@@ -92,41 +92,6 @@ describe GlobalMilestone do
end
end
describe '.states_count' do
context 'when the projects have milestones' do
before do
create(:closed_milestone, title: 'Active Group Milestone', project: project3)
create(:active_milestone, title: 'Active Group Milestone', project: project1)
create(:active_milestone, title: 'Active Group Milestone', project: project2)
create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
end
it 'returns the quantity of global milestones in each possible state' do
expected_count = { opened: 1, closed: 2, all: 2 }
count = described_class.states_count(Project.all)
expect(count).to eq(expected_count)
end
end
context 'when the projects do not have milestones' do
before do
project1
end
it 'returns 0 as the quantity of global milestones in each state' do
expected_count = { opened: 0, closed: 0, all: 0 }
count = described_class.states_count(Project.all)
expect(count).to eq(expected_count)
end
end
end
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
......
......@@ -19,6 +19,8 @@ describe Group do
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
it { is_expected.to have_many(:badges).class_name('GroupBadge') }
it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
......
......@@ -350,4 +350,41 @@ describe Milestone do
end
end
end
describe '.states_count' do
context 'when the projects have milestones' do
before do
project_1 = create(:project)
project_2 = create(:project)
group_1 = create(:group)
group_2 = create(:group)
create(:active_milestone, title: 'Active Group Milestone', project: project_1)
create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
create(:active_milestone, title: 'Active Group Milestone', project: project_2)
create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
create(:closed_milestone, title: 'Active Group Milestone', group: group_1)
create(:closed_milestone, title: 'Closed Group Milestone', group: group_1)
create(:closed_milestone, title: 'Active Group Milestone', group: group_2)
create(:closed_milestone, title: 'Closed Group Milestone', group: group_2)
end
it 'returns the quantity of milestones in each possible state' do
expected_count = { opened: 5, closed: 6, all: 11 }
count = described_class.states_count(Project.all, Group.all)
expect(count).to eq(expected_count)
end
end
context 'when the projects do not have milestones' do
it 'returns 0 as the quantity of global milestones in each state' do
expected_count = { opened: 0, closed: 0, all: 0 }
count = described_class.states_count([project])
expect(count).to eq(expected_count)
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