Commit 97cc765d authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'environments-list-data-a' into 'master'

Show Environment Dashboard Cards with Headers

See merge request gitlab-org/gitlab!15191
parents 7c0c77c6 fa7625d9
...@@ -59,12 +59,12 @@ export default { ...@@ -59,12 +59,12 @@ export default {
> >
<span class="js-environment-name bold"> {{ environment.name }}</span> <span class="js-environment-name bold"> {{ environment.name }}</span>
</gl-link> </gl-link>
<gl-badge v-if="environment.children" :pill="true" class="dashboard-card-icon">{{ <gl-badge v-if="environment.within_folder" :pill="true" class="dashboard-card-icon">{{
environment.children.length environment.size
}}</gl-badge> }}</gl-badge>
</div> </div>
<icon <icon
v-if="environment.children" v-if="environment.within_folder"
v-gl-tooltip v-gl-tooltip
:title="$options.tooltips.information" :title="$options.tooltips.information"
name="information" name="information"
......
...@@ -82,6 +82,6 @@ class OperationsController < ApplicationController ...@@ -82,6 +82,6 @@ class OperationsController < ApplicationController
end end
def serialize_as_json_for_environments(projects) def serialize_as_json_for_environments(projects)
DashboardEnvironmentsSerializer.new.represent(projects).as_json DashboardEnvironmentsSerializer.new(current_user: current_user).represent(projects).as_json
end end
end end
# frozen_string_literal: true
class EnvironmentFolder
attr_reader :last_environment, :size
delegate :project, to: :last_environment
def self.find_for_projects(projects)
environments = ::Environment.where(project: projects).available
t = ::Environment.arel_table
folder_data = environments
.group('COALESCE(environment_type, name), project_id')
.pluck(t[:id].maximum, t[:id].count)
environments_by_id = environments
.id_in(folder_data.map { |(env_id, _)| env_id })
.includes(:project)
.index_by(&:id)
folders = folder_data.map do |(environment_id, count)|
environment = environments_by_id[environment_id]
next unless environment
new(environments_by_id[environment_id], count)
end
projects_with_folders = folders.compact.group_by(&:project)
projects.map { |p| { p => [] } }.reduce({}, :merge).merge(projects_with_folders)
end
def initialize(last_environment, size)
@last_environment = last_environment
@size = size
end
def within_folder?
last_environment.environment_type.present? || size > 1
end
end
# frozen_string_literal: true
class DashboardEnvironmentEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :name
expose :environment_path do |environment|
project_environment_path(environment.project, environment)
end
expose :external_url
end
# frozen_string_literal: true
class DashboardEnvironmentsFolderEntity < Grape::Entity
expose :last_environment, merge: true, using: DashboardEnvironmentEntity
expose :size
expose :within_folder?, as: :within_folder
end
...@@ -8,9 +8,12 @@ class DashboardEnvironmentsProjectEntity < Grape::Entity ...@@ -8,9 +8,12 @@ class DashboardEnvironmentsProjectEntity < Grape::Entity
expose :avatar_url expose :avatar_url
expose :web_url expose :web_url
expose :remove_path do |project_object| expose :remove_path do |project|
remove_operations_project_path(project_id: project_object.id) remove_operations_project_path(project_id: project.id)
end end
expose :namespace, using: API::Entities::NamespaceBasic expose :namespace, using: API::Entities::NamespaceBasic
expose :environments, using: DashboardEnvironmentsFolderEntity do |_project, options|
options[:folders]
end
end end
...@@ -2,4 +2,10 @@ ...@@ -2,4 +2,10 @@
class DashboardEnvironmentsSerializer < BaseSerializer class DashboardEnvironmentsSerializer < BaseSerializer
entity DashboardEnvironmentsProjectEntity entity DashboardEnvironmentsProjectEntity
def represent(projects_with_folders, opts = {}, entity_class = nil)
projects_with_folders.map do |project, folders|
super(project, opts.merge(folders: folders), entity_class)
end
end
end end
...@@ -16,12 +16,11 @@ module Dashboard ...@@ -16,12 +16,11 @@ module Dashboard
attr_reader :user attr_reader :user
def load_projects(user) def load_projects(user)
projects = user.ops_dashboard_projects projects = ::Dashboard::Operations::ProjectsService
::Dashboard::Operations::ProjectsService
.new(user) .new(user)
.execute(projects) .execute(user.ops_dashboard_projects)
.to_a
EnvironmentFolder.find_for_projects(projects)
end end
end end
end end
......
...@@ -164,7 +164,7 @@ describe OperationsController do ...@@ -164,7 +164,7 @@ describe OperationsController do
end end
end end
describe 'GET #environment_list' do describe 'GET #environments_list' do
it_behaves_like 'unlicensed', :get, :environments_list it_behaves_like 'unlicensed', :get, :environments_list
context 'with an anonymous user' do context 'with an anonymous user' do
...@@ -218,20 +218,177 @@ describe OperationsController do ...@@ -218,20 +218,177 @@ describe OperationsController do
expect(json_response['projects']).to eq([]) expect(json_response['projects']).to eq([])
end end
it 'returns a list with one project when the developer has added that project to the dashboard' do context 'with a project in the dashboard' do
project = create(:project, :with_avatar) let(:project) { create(:project, :with_avatar) }
before do
project.add_developer(user) project.add_developer(user)
user.update!(ops_dashboard_projects: [project]) user.update!(ops_dashboard_projects: [project])
end
it 'returns a project without an environment' do
get :environments_list get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first project_json = json_response['projects'].first
expect(project_json['id']).to eq(project.id)
expect(project_json['name']).to eq(project.name)
expect(project_json['namespace']['id']).to eq(project.namespace.id)
expect(project_json['namespace']['name']).to eq(project.namespace.name)
expect(project_json['environments']).to eq([])
end
it 'returns one project with one environment' do
environment = create(:environment, project: project, name: 'staging')
get :environments_list
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee') expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['id']).to eq(project.id) expect(project_json['id']).to eq(project.id)
expect(project_json['name']).to eq(project.name) expect(project_json['name']).to eq(project.name)
expect(project_json['namespace']['id']).to eq(project.namespace.id) expect(project_json['namespace']['id']).to eq(project.namespace.id)
expect(project_json['namespace']['name']).to eq(project.namespace.name) expect(project_json['namespace']['name']).to eq(project.namespace.name)
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['id']).to eq(environment.id)
expect(project_json['environments'].first['environment_path']).to eq(project_environment_path(project, environment))
end
it 'returns multiple projects and environments' do
project2 = create(:project)
project2.add_developer(user)
user.update!(ops_dashboard_projects: [project, project2])
environment1 = create(:environment, project: project)
environment2 = create(:environment, project: project)
environment3 = create(:environment, project: project2)
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
expect(json_response['projects'].count).to eq(2)
expect(json_response['projects'].map { |p| p['id'] }.sort).to eq([project.id, project2.id])
project_json = json_response['projects'].find { |p| p['id'] == project.id }
project2_json = json_response['projects'].find { |p| p['id'] == project2.id }
expect(project_json['environments'].map { |e| e['id'] }.sort).to eq([environment1.id, environment2.id])
expect(project2_json['environments'].map { |e| e['id'] }).to eq([environment3.id])
end
it 'groups like environments together in a folder' do
create(:environment, project: project, name: 'review/test-feature')
environment = create(:environment, project: project, name: 'review/another-feature')
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['id']).to eq(environment.id)
expect(project_json['environments'].first['size']).to eq(2)
expect(project_json['environments'].first['within_folder']).to eq(true)
end
it 'returns an environment not in a folder' do
environment = create(:environment, project: project, name: 'production')
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['id']).to eq(environment.id)
expect(project_json['environments'].first['size']).to eq(1)
expect(project_json['environments'].first['within_folder']).to eq(false)
end
it 'returns true for within_folder when a folder contains only a single environment' do
environment = create(:environment, project: project, name: 'review/test-feature')
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['id']).to eq(environment.id)
expect(project_json['environments'].first['size']).to eq(1)
expect(project_json['environments'].first['within_folder']).to eq(true)
end
it 'counts only available environments' do
create(:environment, project: project, name: 'review/test-feature', state: :available)
environment = create(:environment, project: project, name: 'review/another-feature', state: :available)
create(:environment, project: project, name: 'review/great-feature', state: :stopped)
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['size']).to eq(2)
expect(project_json['environments'].first['within_folder']).to eq(true)
expect(project_json['environments'].first['id']).to eq(environment.id)
end
it "excludes environments with the same folder name for other projects" do
project2 = create(:project)
create(:environment, project: project, name: 'review/test')
create(:environment, project: project2, name: 'review/test')
environment = create(:environment, project: project, name: 'review/something')
user.update!(ops_dashboard_projects: [project])
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].first
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['size']).to eq(2)
expect(project_json['environments'].first['within_folder']).to eq(true)
expect(project_json['environments'].first['id']).to eq(environment.id)
end
it "groups environments scoped to projects for multiple projects included in the user's ops dashboard" do
project2 = create(:project)
project2.add_developer(user)
environment = create(:environment, project: project, name: 'review/test')
create(:environment, project: project2, name: 'review/test')
create(:environment, project: project2, name: 'review/thing')
user.update!(ops_dashboard_projects: [project, project2])
get :environments_list
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('dashboard/operations/environments_list', dir: 'ee')
project_json = json_response['projects'].find { |p| p['id'] == project.id }
expect(project_json['environments'].count).to eq(1)
expect(project_json['environments'].first['size']).to eq(1)
expect(project_json['environments'].first['within_folder']).to eq(true)
expect(project_json['environments'].first['id']).to eq(environment.id)
end
end end
end end
end end
......
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
"avatar_url", "avatar_url",
"remove_path", "remove_path",
"web_url", "web_url",
"namespace" "namespace",
"environments"
], ],
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
...@@ -30,7 +31,8 @@ ...@@ -30,7 +31,8 @@
"avatar_url": { "type": ["string", "null"] }, "avatar_url": { "type": ["string", "null"] },
"remove_path": { "type": "string" }, "remove_path": { "type": "string" },
"web_url": { "type": "string" }, "web_url": { "type": "string" },
"namespace": { "$ref": "#/definitions/namespace" } "namespace": { "$ref": "#/definitions/namespace" },
"environments": { "type": "array", "items": { "$ref": "#/definitions/environment" } }
} }
}, },
"namespace": { "namespace": {
...@@ -47,6 +49,25 @@ ...@@ -47,6 +49,25 @@
"avatar_url": { "type": ["string", "null"] }, "avatar_url": { "type": ["string", "null"] },
"full_path": { "type": "string" } "full_path": { "type": "string" }
} }
},
"environment": {
"type": "object",
"required": [
"id",
"name",
"size",
"within_folder",
"external_url",
"environment_path"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"size": { "type": "integer" },
"within_folder": { "type": "boolean" },
"external_url": { "type": "string" },
"environment_path": { "type": "string" }
}
} }
} }
} }
...@@ -90,7 +90,7 @@ exports[`Environment Header renders name and link to app matches the snapshot 1` ...@@ -90,7 +90,7 @@ exports[`Environment Header renders name and link to app matches the snapshot 1`
</div> </div>
`; `;
exports[`Environment Header with children matches the snapshot 1`] = ` exports[`Environment Header with environments grouped into a folder matches the snapshot 1`] = `
<div <div
class="card-header border-0 py-2 d-flex align-items-center bg-light" class="card-header border-0 py-2 d-flex align-items-center bg-light"
> >
......
...@@ -17,6 +17,8 @@ describe('Environment Header', () => { ...@@ -17,6 +17,8 @@ describe('Environment Header', () => {
environment_path: '/enivronment/1', environment_path: '/enivronment/1',
name: 'staging', name: 'staging',
external_url: 'http://example.com', external_url: 'http://example.com',
size: 1,
within_folder: false,
}, },
}; };
}); });
...@@ -37,10 +39,14 @@ describe('Environment Header', () => { ...@@ -37,10 +39,14 @@ describe('Environment Header', () => {
expect(wrapper.find('.js-environment-name').text()).toBe(propsData.environment.name); expect(wrapper.find('.js-environment-name').text()).toBe(propsData.environment.name);
}); });
it('renders a link to the enivironment page', () => { it('renders a link to the environment page', () => {
expect(wrapper.find(GlLink).attributes('href')).toBe(propsData.environment.environment_path); expect(wrapper.find(GlLink).attributes('href')).toBe(propsData.environment.environment_path);
}); });
it('does not show a badge with the number of environments in the folder', () => {
expect(wrapper.find(GlBadge).exists()).toBe(false);
});
it('renders a link to the external app', () => { it('renders a link to the external app', () => {
expect(wrapper.find(ReviewAppLink).attributes('link')).toBe( expect(wrapper.find(ReviewAppLink).attributes('link')).toBe(
propsData.environment.external_url, propsData.environment.external_url,
...@@ -52,9 +58,10 @@ describe('Environment Header', () => { ...@@ -52,9 +58,10 @@ describe('Environment Header', () => {
}); });
}); });
describe('with children', () => { describe('with environments grouped into a folder', () => {
beforeEach(() => { beforeEach(() => {
propsData.environment.children = [{}, {}, {}, {}, {}]; propsData.environment.size = 5;
propsData.environment.within_folder = true;
propsData.environment.name = 'review/testing'; propsData.environment.name = 'review/testing';
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
...@@ -64,7 +71,7 @@ describe('Environment Header', () => { ...@@ -64,7 +71,7 @@ describe('Environment Header', () => {
}); });
it('shows a badge with the number of other environments in the folder', () => { it('shows a badge with the number of other environments in the folder', () => {
const expected = propsData.environment.children.length.toString(); const expected = propsData.environment.size.toString();
expect(wrapper.find(GlBadge).text()).toBe(expected); expect(wrapper.find(GlBadge).text()).toBe(expected);
}); });
......
# frozen_string_literal: true
require 'spec_helper'
describe EnvironmentFolder do
describe '.find_for_projects' do
it 'returns an environment within a folder when the last environment does not have an environment_type' do
project = create(:project)
create(:environment, project: project, name: 'production/azure')
last_environment = create(:environment, project: project, name: 'production')
projects_with_environment_folders = described_class.find_for_projects([project])
environment_folder = projects_with_environment_folders[project].first
expect(environment_folder.last_environment.id).to eq(last_environment.id)
expect(environment_folder.within_folder?).to eq(true)
end
it 'returns an environment outside a folder' do
project = create(:project)
create(:environment, project: project, name: 'production')
projects_with_environment_folders = described_class.find_for_projects([project])
environment_folder = projects_with_environment_folders[project].first
expect(environment_folder.within_folder?).to eq(false)
end
it 'returns a project without any environments' do
project = create(:project)
projects_with_environment_folders = described_class.find_for_projects([project])
expect(projects_with_environment_folders).to eq({ project => [] })
end
it 'returns a project without any available environments' do
project = create(:project)
create(:environment, project: project, state: :stopped)
projects_with_environment_folders = described_class.find_for_projects([project])
expect(projects_with_environment_folders).to eq({ project => [] })
end
it 'returns multiple projects' do
project1 = create(:project)
project2 = create(:project)
create(:environment, project: project1, state: :stopped)
environment = create(:environment, project: project2, state: :available)
projects_with_environment_folders = described_class.find_for_projects([project1, project2])
expect(projects_with_environment_folders[project1]).to eq([])
expect(projects_with_environment_folders[project2].count).to eq(1)
environment_folder = projects_with_environment_folders[project2].first
expect(environment_folder.last_environment).to eq(environment)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DashboardEnvironmentEntity do
describe '.as_json' do
it 'includes environment attributes' do
environment = create(:environment)
result = described_class.new(environment).as_json
expect(result.keys.sort).to eq([:environment_path, :external_url, :id, :name])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DashboardEnvironmentsFolderEntity do
describe '.as_json' do
it 'includes folder and environment attributes' do
environment = create(:environment)
size = 1
environment_folder = EnvironmentFolder.new(environment, size)
result = described_class.new(environment_folder).as_json
expect(result.keys.sort).to eq([:environment_path, :external_url, :id, :name, :size, :within_folder])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DashboardEnvironmentsProjectEntity do
describe '.as_json' do
it 'includes project attributes' do
current_user = create(:user)
project = create(:project)
environment = create(:environment)
size = 1
environment_folder = EnvironmentFolder.new(environment, size)
entity_request = EntityRequest.new(current_user: current_user)
result = described_class.new(project, { folders: [environment_folder], request: entity_request }).as_json
expect(result.keys.sort).to eq([:avatar_url, :environments, :id, :name, :namespace, :remove_path, :web_url])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe DashboardEnvironmentsSerializer do
describe '.represent' do
it 'returns an empty array when there are no projects' do
current_user = create(:user)
projects_with_folders = {}
result = described_class.new(current_user: current_user).represent(projects_with_folders)
expect(result).to eq([])
end
it 'includes project attributes' do
current_user = create(:user)
project = create(:project)
environment = create(:environment)
size = 1
environment_folder = EnvironmentFolder.new(environment, size)
projects_with_folders = { project => [environment_folder] }
result = described_class.new(current_user: current_user).represent(projects_with_folders)
expect(result.first.keys.sort).to eq([:avatar_url, :environments, :id, :name, :namespace, :remove_path, :web_url])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Dashboard::Environments::ListService do
describe '#execute' do
before do
stub_licensed_features(operations_dashboard: true)
end
it 'returns a list of projects' do
user = create(:user)
project = create(:project)
project.add_developer(user)
user.update!(ops_dashboard_projects: [project])
projects_with_folders = described_class.new(user).execute
expect(projects_with_folders).to eq({ project => [] })
end
context 'when unlicensed' do
before do
stub_licensed_features(operations_dashboard: false)
end
it 'returns an empty hash' do
user = create(:user)
project = create(:project)
project.add_developer(user)
user.update!(ops_dashboard_projects: [project])
projects_with_folders = described_class.new(user).execute
expect(projects_with_folders).to eq({})
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