Commit f9a41b17 authored by Simon Knox's avatar Simon Knox Committed by Douglas Barbosa Alexandre

Hide info for unlicensed projects on Ops Dashboard

Check for project group license
If needed, check if user can upgrade group
parent 526494dc
......@@ -2,7 +2,7 @@
import { mapActions } from 'vuex';
import _ from 'underscore';
import { GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
......@@ -36,6 +36,28 @@ export default {
triggerer: __('Triggerer'),
},
computed: {
unlicensedMessage() {
if (this.project.upgrade_path) {
return sprintf(
__(
"To see this project's operational details, %{linkStart}upgrade its group plan to Silver%{linkEnd}. You can also remove the project from the dashboard.",
),
{
linkStart: `<a href="${this.project.upgrade_path}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
},
false,
);
}
return sprintf(
__(
"To see this project's operational details, contact an owner of group %{groupName} to upgrade the plan. You can also remove the project from the dashboard.",
),
{
groupName: this.project.namespace.name,
},
);
},
hasPipelineFailed() {
return (
this.lastPipeline &&
......@@ -105,7 +127,13 @@ export default {
@remove="removeProject"
/>
<div :class="cardClasses" class="dashboard-card-body card-body">
<div
v-if="project.upgrade_required"
class="dashboard-card-body card-body bg-secondary"
v-html="unlicensedMessage"
></div>
<div v-else :class="cardClasses" class="dashboard-card-body card-body">
<div v-if="lastPipeline" class="row">
<div class="col-1 align-self-center">
<user-avatar-link
......
# frozen_string_literal: true
class DashboardOperationsProjectEntity < Grape::Entity
include Gitlab::Utils::StrongMemoize
include RequestAwareEntity
expose :project, merge: true, using: API::Entities::BasicProjectDetails
expose :remove_path do |dashboard_project_object|
remove_operations_project_path(project_id: dashboard_project_object.project.id)
expose :remove_path do |_|
remove_operations_project_path(project_id: project.id)
end
expose :last_pipeline, if: -> (*) { last_pipeline } do |_, options|
PipelineDetailsEntity.represent(last_pipeline, options.merge(request: new_request))
expose :upgrade_required
expose :upgrade_path, if: -> (*) { upgrade_required && user_can_upgrade? } do |_|
project&.group ? group_billings_path(project.group) : profile_billings_path
end
expose :last_deployment, if: -> (*) { last_deployment? } do |dashboard_project_object, options|
DeploymentEntity.represent(dashboard_project_object.last_deployment,
options.merge(request: new_request))
expose :last_pipeline, if: -> (*) { !upgrade_required && last_pipeline } do |_, options|
PipelineDetailsEntity.represent(last_pipeline, options.merge(request: new_request))
end
expose :alert_count
expose :alert_path, if: -> (*) { last_deployment? } do |dashboard_project_object|
project = dashboard_project_object.project
environment = dashboard_project_object.last_deployment.environment
expose :last_deployment, if: -> (*) { !upgrade_required && last_deployment } do |_, options|
DeploymentEntity.represent(last_deployment, options.merge(request: new_request))
end
metrics_project_environment_path(project, environment)
expose :alert_count, if: -> (*) { !upgrade_required }
expose :alert_path, if: -> (*) { !upgrade_required && last_deployment } do |_|
metrics_project_environment_path(project, last_deployment.environment)
end
expose :last_alert, using: PrometheusAlertEntity, if: -> (*) { last_alert? }
expose :last_alert, using: PrometheusAlertEntity, if: -> (*) { !upgrade_required && last_alert? }
private
......@@ -33,20 +35,38 @@ class DashboardOperationsProjectEntity < Grape::Entity
def new_request
EntityRequest.new(
current_user: request.current_user,
project: dashboard_project.project
current_user: current_user,
project: project
)
end
def upgrade_required
strong_memoize(:upgrade_required) do
!project.feature_available?(:operations_dashboard)
end
end
def user_can_upgrade?
can?(current_user, :admin_namespace, project&.namespace)
end
def last_pipeline
dashboard_project.project.last_pipeline
project.last_pipeline
end
def last_deployment?
def last_deployment
dashboard_project.last_deployment
end
def last_alert?
dashboard_project.last_alert
end
def current_user
request.current_user
end
def project
dashboard_project.project
end
end
......@@ -27,7 +27,7 @@ module Dashboard
ProjectsService
.new(user)
.execute(projects)
.execute(projects, include_unavailable: true)
.to_a # 1 query
end
......
......@@ -7,18 +7,23 @@ module Dashboard
@user = user
end
def execute(project_ids)
def execute(project_ids, include_unavailable: false)
return [] unless License.feature_available?(:operations_dashboard)
find_projects(user, project_ids)
.to_a
.select { |project| project.feature_available?(:operations_dashboard) }
projects = find_projects(user, project_ids).to_a
projects = available_projects(projects) unless include_unavailable
projects
end
private
attr_reader :user, :project_ids
def available_projects(projects)
projects.select { |project| project.feature_available?(:operations_dashboard) }
end
def find_projects(user, project_ids)
ProjectsFinder.new(
current_user: user,
......
---
title: Hide info for unlicensed projects on Ops Dashboard
merge_request: 15099
author:
type: fixed
......@@ -20,4 +20,66 @@ describe 'Dashboard operations', :js do
expect(page).to have_text('Alerts')
expect(page).to have_text(pipeline.status)
end
context 'when opened on gitlab.com' do
before do
stub_application_setting(check_namespace_plan: true)
stub_licensed_features(operations_dashboard: true)
end
it 'masks projects without valid license' do
user = create(:user)
gold_group = create(:group)
bronze_group = create(:group)
create(:gitlab_subscription, :gold, namespace: gold_group)
create(:gitlab_subscription, :bronze, namespace: bronze_group)
gold_project = create(:project, :repository, namespace: gold_group, name: 'Gold Project')
bronze_project = create(:project, :repository, namespace: bronze_group, name: 'Bronze Project')
public_project = create(:project, :repository, :public, namespace: bronze_group, name: 'Public Bronze Project')
gold_pipeline = create(:ci_pipeline, project: gold_project, sha: gold_project.commit.sha, status: :running)
bronze_pipeline = create(:ci_pipeline, project: bronze_project, sha: bronze_project.commit.sha, status: :running)
public_pipeline = create(:ci_pipeline, project: public_project, sha: public_project.commit.sha, status: :running)
gold_project.add_developer(user)
bronze_group.add_developer(user)
user.update(ops_dashboard_projects: [gold_project, bronze_project, public_project])
sign_in(user)
visit operations_path
bronze_card = project_card(bronze_project)
gold_card = project_card(gold_project)
public_card = project_card(public_project)
assert_masked(bronze_card, bronze_project, bronze_pipeline, bronze_group)
assert_available(gold_card, gold_project, gold_pipeline)
assert_available(public_card, public_project, public_pipeline)
end
def project_card(project)
page.find('.js-dashboard-project', text: "#{project.namespace.name} / #{project.name}")
end
def assert_available(card, project, pipeline)
expect(card).to have_text(project.name)
expect(card).to have_text(pipeline.ref)
expect(card).to have_text(pipeline.short_sha)
expect(card).to have_text('Alerts')
expect(card).to have_text(pipeline.status)
end
def assert_masked(card, project, pipeline, group)
expect(card).to have_text(project.name)
expect(card).to have_text("To see this project's operational details, contact an owner of group #{group.path} to upgrade the plan. You can also remove the project from the dashboard.")
expect(card).not_to have_text(pipeline.ref)
expect(card).not_to have_text(pipeline.short_sha)
expect(card).not_to have_text('Alerts')
expect(card).not_to have_text(pipeline.status)
end
end
end
......@@ -27,6 +27,69 @@ describe('project component', () => {
wrapper.destroy();
});
describe('with unlicensed project', () => {
let project;
describe('can upgrade project group', () => {
beforeEach(() => {
project = {
...mockOneProject,
upgrade_required: true,
upgrade_path: '/upgrade',
};
wrapper = shallowMount(ProjectComponent, {
sync: false,
store,
localVue,
propsData: { project },
});
});
it('shows project title', () => {
const header = wrapper.find(ProjectHeader);
expect(header.props('project')).toEqual(project);
});
it('styles card with gray background', () => {
expect(wrapper.find('.dashboard-card-body.bg-secondary').exists()).toBe(true);
});
it('shows upgrade license text', () => {
expect(wrapper.find('.dashboard-card-body').html()).toContain(wrapper.vm.unlicensedMessage);
expect(wrapper.vm.unlicensedMessage).toContain('upgrade its group plan to Silver');
});
it('hides commit info', () => {
expect(wrapper).not.toContain(Commit);
});
});
describe('cannot upgrade project group', () => {
beforeEach(() => {
project = {
...mockOneProject,
upgrade_required: true,
upgrade_path: '',
};
wrapper = shallowMount(ProjectComponent, {
sync: false,
store,
localVue,
propsData: { project },
});
});
it('shows upgrade license text', () => {
expect(wrapper.find('.dashboard-card-body').html()).toContain(wrapper.vm.unlicensedMessage);
expect(wrapper.vm.unlicensedMessage).not.toContain('upgrade its group plan to Silver');
expect(wrapper.vm.unlicensedMessage).toContain(
`contact an owner of group ${project.namespace.name}`,
);
});
});
});
describe('wrapped components', () => {
describe('project header', () => {
it('binds project', () => {
......
......@@ -107,5 +107,7 @@ export function mockProjectData(
upstream_pipeline: mockPipelineData(upstreamStatus),
downstream_pipelines: [],
alert_count: alertCount,
upgrade_required: false,
upgrade_path: '/groups/test/-/billings',
}));
}
......@@ -10,13 +10,18 @@ describe DashboardOperationsProjectEntity do
subject { described_class.new(resource, request: request).as_json }
context 'the project supports the ops dashboard' do
before do
stub_licensed_features(operations_dashboard: true)
end
it 'has all required fields' do
expect(subject).to include(:remove_path, :alert_count)
expect(subject).to include(:remove_path, :alert_count, :upgrade_required)
expect(subject.first).to include(:id)
end
it 'does not have optional fields' do
expect(subject).not_to include(:last_pipeline, :upstream_pipeline, :downstream_pipelines)
expect(subject).not_to include(:last_pipeline, :upstream_pipeline, :downstream_pipelines, :upgrade_path)
end
context 'when there is a pipeline' do
......@@ -80,4 +85,31 @@ describe DashboardOperationsProjectEntity do
end
end
end
end
context 'the project does not support the ops dashboard' do
it 'has the expected fields' do
expect(subject).to include(:remove_path, :upgrade_required)
expect(subject.first).to include(:id)
expect(subject).not_to include(:alert_count, :upgrade_path)
end
context 'the user has permission to upgrade plan' do
let(:user) { build(:user, :admin) }
it 'shows the profile upgrade path' do
expect(subject[:upgrade_path]).to eq '/profile/billings'
end
end
context 'the user has permission to upgrade group' do
let(:project) { build(:project, namespace: create(:group)) }
let(:user) { build(:user, :admin) }
it 'shows the group upgrade path' do
expect(subject[:upgrade_path]).to eq "/groups/#{project.namespace.path}/-/billings"
end
end
end
end
......@@ -60,7 +60,9 @@ describe Dashboard::Operations::ListService do
user.ops_dashboard_projects << project
allow(projects_service)
.to receive(:execute).with([project]).and_return([project])
.to receive(:execute)
.with([project], include_unavailable: true)
.and_return([project])
end
it 'returns a list of projects' do
......@@ -120,7 +122,7 @@ describe Dashboard::Operations::ListService do
allow(projects_service)
.to receive(:execute)
.with([project, project2])
.with([project, project2], include_unavailable: true)
.and_return([project, project2])
end
......@@ -145,7 +147,9 @@ describe Dashboard::Operations::ListService do
context 'without added projects' do
before do
allow(projects_service)
.to receive(:execute).with([]).and_return([])
.to receive(:execute)
.with([], include_unavailable: true)
.and_return([])
end
it_behaves_like 'no projects'
......
......@@ -116,6 +116,12 @@ describe Dashboard::Operations::ProjectsService do
else
it_behaves_like 'project not found'
end
context 'if :include_unavailable option is provided' do
let(:result) { service.execute(projects, include_unavailable: true) }
it_behaves_like 'project found'
end
end
end
......
......@@ -16217,6 +16217,12 @@ msgstr ""
msgid "To see all the user's personal access tokens you must impersonate them first."
msgstr ""
msgid "To see this project's operational details, %{linkStart}upgrade its group plan to Silver%{linkEnd}. You can also remove the project from the dashboard."
msgstr ""
msgid "To see this project's operational details, contact an owner of group %{groupName} to upgrade the plan. You can also remove the project from the dashboard."
msgstr ""
msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
msgstr ""
......
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