Commit 0be9d5f0 authored by Scott Hampton's avatar Scott Hampton

Add a contact sales banner to the environments table

The banner will promote Canary Deployments

The banner is dismissable, and won't show again
parent 8b8a86c0
......@@ -30,6 +30,28 @@ export default {
type: Boolean,
required: true,
},
// ee-only start
canaryDeploymentFeatureId: {
type: String,
required: true,
},
showCanaryDeploymentCallout: {
type: Boolean,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
lockPromotionSvgPath: {
type: String,
required: true,
},
helpCanaryDeploymentsPath: {
type: String,
required: true,
},
// ee-only end
},
methods: {
onChangePage(page) {
......@@ -55,6 +77,11 @@ export default {
:environments="environments"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
/>
<table-pagination
......
......@@ -44,6 +44,28 @@ export default {
type: String,
required: true,
},
// ee-only start
canaryDeploymentFeatureId: {
type: String,
required: true,
},
showCanaryDeploymentCallout: {
type: Boolean,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
lockPromotionSvgPath: {
type: String,
required: true,
},
helpCanaryDeploymentsPath: {
type: String,
required: true,
},
// ee-only end
},
created() {
......@@ -118,6 +140,11 @@ export default {
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
>
<empty-state
......
......@@ -3,15 +3,21 @@
* Render environments table.
*/
import { GlLoadingIcon } from '@gitlab/ui';
import environmentItem from './environment_item.vue';
import environmentItem from './environment_item.vue'; // eslint-disable-line import/order
import deployBoard from 'ee/environments/components/deploy_board_component.vue'; // eslint-disable-line import/order
// ee-only start
import deployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryDeploymentCallout from 'ee/environments/components/canary_deployment_callout.vue';
// ee-only end
export default {
components: {
environmentItem,
deployBoard,
GlLoadingIcon,
// ee-only start
CanaryDeploymentCallout,
// ee-only end
},
props: {
......@@ -32,6 +38,33 @@ export default {
required: false,
default: false,
},
// ee-only start
canaryDeploymentFeatureId: {
type: String,
required: true,
},
showCanaryDeploymentCallout: {
type: Boolean,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
lockPromotionSvgPath: {
type: String,
required: true,
},
helpCanaryDeploymentsPath: {
type: String,
required: true,
},
// ee-only end
},
methods: {
folderUrl(model) {
......@@ -40,6 +73,11 @@ export default {
shouldRenderFolderContent(env) {
return env.isFolder && env.isOpen && env.children && env.children.length > 0;
},
// ee-only start
shouldShowCanaryCallout(env) {
return env.showCanaryCallout && this.showCanaryDeploymentCallout;
},
// ee-only end
},
};
</script>
......@@ -110,6 +148,17 @@ export default {
</div>
</template>
</template>
<template v-if="shouldShowCanaryCallout(model)">
<canary-deployment-callout
:key="`canary-promo-${i}`"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
:data-js-canary-promo-key="i"
/>
</template>
</template>
</div>
</template>
......@@ -3,6 +3,10 @@ import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
// ee-only start
import CanaryCalloutMixin from 'ee/environments/mixins/canary_callout_mixin'; // eslint-disable-line import/order
// ee-only end
Vue.use(Translate);
export default () =>
......@@ -11,6 +15,9 @@ export default () =>
components: {
environmentsFolderApp,
},
// ee-only start
mixins: [CanaryCalloutMixin],
// ee-only end
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
......@@ -30,6 +37,13 @@ export default () =>
cssContainerClass: this.cssContainerClass,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
// ee-only start
canaryDeploymentFeatureId: this.canaryDeploymentFeatureId,
showCanaryDeploymentCallout: this.showCanaryDeploymentCallout,
userCalloutsPath: this.userCalloutsPath,
lockPromotionSvgPath: this.lockPromotionSvgPath,
helpCanaryDeploymentsPath: this.helpCanaryDeploymentsPath,
// ee-only end
},
});
},
......
......@@ -31,6 +31,28 @@ export default {
type: Boolean,
required: true,
},
// ee-only start
canaryDeploymentFeatureId: {
type: String,
required: true,
},
showCanaryDeploymentCallout: {
type: Boolean,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
lockPromotionSvgPath: {
type: String,
required: true,
},
helpCanaryDeploymentsPath: {
type: String,
required: true,
},
// ee-only end
},
methods: {
successCallback(resp) {
......@@ -57,6 +79,11 @@ export default {
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:canary-deployment-feature-id="canaryDeploymentFeatureId"
:show-canary-deployment-callout="showCanaryDeploymentCallout"
:user-callouts-path="userCalloutsPath"
:lock-promotion-svg-path="lockPromotionSvgPath"
:help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
/>
</div>
......
......@@ -3,6 +3,10 @@ import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
// ee-only start
import CanaryCalloutMixin from 'ee/environments/mixins/canary_callout_mixin'; // eslint-disable-line import/order
// ee-only end
Vue.use(Translate);
export default () =>
......@@ -11,6 +15,9 @@ export default () =>
components: {
environmentsComponent,
},
// ee-only start
mixins: [CanaryCalloutMixin],
// ee-only end
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
......@@ -34,6 +41,13 @@ export default () =>
canCreateEnvironment: this.canCreateEnvironment,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
// ee-only start
canaryDeploymentFeatureId: this.canaryDeploymentFeatureId,
showCanaryDeploymentCallout: this.showCanaryDeploymentCallout,
userCalloutsPath: this.userCalloutsPath,
lockPromotionSvgPath: this.lockPromotionSvgPath,
helpCanaryDeploymentsPath: this.helpCanaryDeploymentsPath,
// ee-only end
},
});
},
......
......@@ -2,7 +2,6 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
// ee-only
import { CLUSTER_TYPE } from '~/clusters/constants';
/**
* Environments Store.
*
......@@ -96,6 +95,19 @@ export default class EnvironmentsStore {
this.state.environments = filteredEnvironments;
// ee-only start
/**
* Add the canary callout banner underneath the second environment listed.
*
* If there is only one environment, then add to it underneath the first.
*/
if (this.state.environments.length >= 2) {
this.state.environments[1].showCanaryCallout = true;
} else if (this.state.environments.length === 1) {
this.state.environments[0].showCanaryCallout = true;
}
// ee-only end
return filteredEnvironments;
}
......@@ -222,7 +234,9 @@ export default class EnvironmentsStore {
let updated = Object.assign({}, env);
if (env.id === environmentID) {
updated = Object.assign({}, updated, { isDeployBoardVisible: !env.isDeployBoardVisible });
updated = Object.assign({}, updated, {
isDeployBoardVisible: !env.isDeployBoardVisible,
});
}
return updated;
});
......
......@@ -721,3 +721,59 @@
margin-left: $gl-padding-8;
}
}
// ee-only start
.canary-deployment-callout {
border-bottom: 1px solid $border-white-normal;
display: flex;
@include media-breakpoint-down(sm) {
display: none;
}
&-lock {
height: 82px;
width: 92px;
}
&-message {
max-width: 600px;
color: $gray-700;
}
&-close {
color: $gray-700;
cursor: pointer;
}
&-button {
border-color: $blue-500;
color: $blue-500;
&:not(:disabled):not(.disabled):active {
background-color: $blue-200;
border: 2px solid $blue-600;
color: $blue-700;
height: 34px;
padding: 5px 9px;
}
&:focus {
background-color: $blue-100;
border: 2px solid $blue-500;
box-shadow: 0 0 4px 1px $blue-200;
color: $blue-600;
height: 34px;
padding: 5px 9px;
}
&:hover {
background-color: $blue-100;
border: 2px solid $blue-500;
color: $blue-600;
height: 34px;
padding: 5px 9px;
}
}
}
// ee-only end
<script>
import { GlButton, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import PersistentUserCallout from '~/persistent_user_callout';
export default {
components: {
GlButton,
GlLink,
Icon,
},
props: {
canaryDeploymentFeatureId: {
type: String,
required: true,
},
userCalloutsPath: {
type: String,
required: true,
},
lockPromotionSvgPath: {
type: String,
required: true,
},
helpCanaryDeploymentsPath: {
type: String,
required: true,
},
},
mounted() {
const callout = this.$refs['canary-deployment-callout'];
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
},
};
</script>
<template>
<div
ref="canary-deployment-callout"
class="p-3 canary-deployment-callout"
:data-dismiss-endpoint="userCalloutsPath"
:data-feature-id="canaryDeploymentFeatureId"
>
<img class="canary-deployment-callout-lock pr-3" :src="lockPromotionSvgPath" />
<div class="pl-3">
<p class="font-weight-bold mb-1">
{{ __('Upgrade plan to unlock Canary Deployments feature') }}
</p>
<p class="canary-deployment-callout-message">
{{
__(
'Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application.',
)
}}
<gl-link :href="helpCanaryDeploymentsPath">{{ __('Read more') }}</gl-link>
</p>
<gl-button
href="https://about.gitlab.com/sales/"
variant="outline-primary"
class="canary-deployment-callout-button"
>{{ __('Contact sales to upgrade') }}</gl-button
>
</div>
<div class="ml-auto pr-2 canary-deployment-callout-close js-close"><icon name="close" /></div>
</div>
</template>
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
data() {
const data = document.querySelector(this.$options.el).dataset;
return {
canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
showCanaryDeploymentCallout: parseBoolean(data.environmentsDataShowCanaryDeploymentCallout),
userCalloutsPath: data.environmentsDataUserCalloutsPath,
lockPromotionSvgPath: data.environmentsDataLockPromotionSvgPath,
helpCanaryDeploymentsPath: data.environmentsDataHelpCanaryDeploymentsPath,
};
},
};
......@@ -2,6 +2,30 @@
module EE
module EnvironmentsHelper
def environments_list_data
ee_environments_list_data = {
"canary_deployment_feature_id" => UserCalloutsHelper::CANARY_DEPLOYMENT,
"show-canary-deployment-callout" => show_canary_deployment_callout?(@project).to_s,
"user-callouts-path" => user_callouts_path,
"lock-promotion-svg-path" => image_path('illustrations/lock_promotion.svg'),
"help-canary-deployments-path" => help_page_path('user/project/canary_deployments')
}
super.merge(ee_environments_list_data)
end
def environments_folder_list_view_data
ee_environments_folder_list_view_data = {
"canary_deployment_feature_id" => UserCalloutsHelper::CANARY_DEPLOYMENT,
"show-canary-deployment-callout" => show_canary_deployment_callout?(@project).to_s,
"user-callouts-path" => user_callouts_path,
"lock-promotion-svg-path" => image_path('illustrations/lock_promotion.svg'),
"help-canary-deployments-path" => help_page_path('user/project/canary_deployments')
}
super.merge(ee_environments_folder_list_view_data)
end
def metrics_data(project, environment)
ee_metrics_data = {
"alerts-endpoint" => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
......
......@@ -5,6 +5,7 @@ module EE
GOLD_TRIAL = 'gold_trial'
GEO_ENABLE_HASHED_STORAGE = 'geo_enable_hashed_storage'
GEO_MIGRATE_HASHED_STORAGE = 'geo_migrate_hashed_storage'
CANARY_DEPLOYMENT = 'canary_deployment'
def show_gold_trial?(user = current_user)
return false unless user
......@@ -14,6 +15,13 @@ module EE
users_namespaces_clean?(user)
end
def show_canary_deployment_callout?(project)
!user_dismissed?(CANARY_DEPLOYMENT) &&
show_promotions? &&
# use :canary_deployments if we create a feature flag for it in the future
!project.feature_available?(:deploy_board)
end
def show_gold_trial_suitable_env?
(::Gitlab.com? || Rails.env.development?) &&
!::Gitlab::Database.read_only?
......
......@@ -10,10 +10,10 @@ module EE
override :feature_names
def feature_names
super.merge(
cluster_security_warning: 3,
gold_trial: 4,
geo_enable_hashed_storage: 5,
geo_migrate_hashed_storage: 6)
geo_migrate_hashed_storage: 6,
canary_deployment: 7)
end
end
end
......
---
title: Canary deployment callout on the environments page
merge_request: 8457
author:
type: added
......@@ -148,4 +148,45 @@ describe EE::UserCalloutsHelper do
end
end
end
describe '.show_canary_deployment_callout?' do
let(:project) { build(:project) }
subject { helper.show_canary_deployment_callout?(project) }
before do
allow(helper).to receive(:show_promotions?).and_return(true)
end
context 'when user needs to upgrade to canary deployments' do
before do
allow(project).to receive(:feature_available?).with(:deploy_board).and_return(false)
end
context 'when user has dismissed' do
before do
allow(helper).to receive(:user_dismissed?).and_return(true)
end
it { is_expected.to be_falsey }
end
context 'when user has not dismissed' do
before do
allow(helper).to receive(:user_dismissed?).and_return(false)
end
it { is_expected.to be_truthy }
end
end
context 'when user already has access to canary deployments' do
before do
allow(project).to receive(:feature_available?).with(:deploy_board).and_return(true)
allow(helper).to receive(:user_dismissed?).and_return(false)
end
it { is_expected.to be_falsey }
end
end
end
......@@ -1567,6 +1567,9 @@ msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
msgstr ""
msgid "Cancel"
msgstr ""
......@@ -2478,6 +2481,9 @@ msgstr ""
msgid "Connecting..."
msgstr ""
msgid "Contact sales to upgrade"
msgstr ""
msgid "Container Registry"
msgstr ""
......@@ -9804,6 +9810,9 @@ msgstr ""
msgid "Updating"
msgstr ""
msgid "Upgrade plan to unlock Canary Deployments feature"
msgstr ""
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
......
......@@ -29,6 +29,13 @@ describe('Environment table', () => {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
// ee-only start
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
// ee-only end
});
expect(vm.$el.getAttribute('class')).toContain('ci-table');
......@@ -51,6 +58,13 @@ describe('Environment table', () => {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
// ee-only start
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
// ee-only end
});
expect(vm.$el.querySelector('.js-deploy-board-row')).toBeDefined();
......@@ -83,8 +97,41 @@ describe('Environment table', () => {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
// ee-only start
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
// ee-only end
});
vm.$el.querySelector('.deploy-board-icon').click();
});
// ee-only start
it('should render canary callout', () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
showCanaryCallout: true,
};
vm = mountComponent(Component, {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
});
expect(vm.$el.querySelector('.canary-deployment-callout')).not.toBeNull();
});
// ee-only end
});
......@@ -14,6 +14,13 @@ describe('Environment', () => {
cssContainerClass: 'container',
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
// ee-only start
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
// ee-only end
};
let EnvironmentsComponent;
......@@ -124,6 +131,70 @@ describe('Environment', () => {
});
});
});
// ee-only start
describe('canary callout', () => {
it('should render banner underneath second environment', done => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [environment, environment],
stopped_count: 1,
available_count: 0,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
expect(
component.$el
.querySelector('.canary-deployment-callout')
.getAttribute('data-js-canary-promo-key'),
).toBe('1');
done();
}, 0);
});
it('should render banner underneath first environment', done => {
mock.onGet(mockData.endpoint).reply(
200,
{
environments: [environment],
stopped_count: 1,
available_count: 0,
},
{
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
);
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
expect(
component.$el
.querySelector('.canary-deployment-callout')
.getAttribute('data-js-canary-promo-key'),
).toBe('0');
done();
}, 0);
});
});
// ee-only end
});
describe('unsuccessfull request', () => {
......
......@@ -245,4 +245,20 @@ describe('Store', () => {
expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
});
});
// ee-only start
describe('canaryCallout', () => {
it('should add banner underneath the second environment', () => {
store.storeEnvironments(serverData);
expect(store.state.environments[1].showCanaryCallout).toEqual(true);
});
it('should add banner underneath first environment when only one environment', () => {
store.storeEnvironments(serverData.slice(0, 1));
expect(store.state.environments[0].showCanaryCallout).toEqual(true);
});
});
// ee-only end
});
......@@ -16,6 +16,13 @@ describe('Environments Folder View', () => {
canCreateDeployment: true,
canReadEnvironment: true,
cssContainerClass: 'container',
// ee-only start
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
// ee-only end
};
beforeEach(() => {
......
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