Commit e4167f1a authored by Tiger's avatar Tiger Committed by Jose Vargas
parent eccf0728
......@@ -18,7 +18,8 @@ import {
GlTooltipDirective,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import deployBoardSvg from 'ee_empty_states/icons/_deploy_board.svg';
import deployBoardSvg from 'empty_states/icons/_deploy_board.svg';
import instanceComponent from '~/vue_shared/components/deployment_instance.vue';
import { n__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATUS_MAP, CANARY_STATUS } from '../constants';
......@@ -26,7 +27,7 @@ import CanaryIngress from './canary_ingress.vue';
export default {
components: {
instanceComponent: () => import('ee_component/vue_shared/components/deployment_instance.vue'),
instanceComponent,
CanaryIngress,
GlIcon,
GlLoadingIcon,
......
......@@ -6,14 +6,15 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp';
import { s__ } from '~/locale';
import EnvironmentItem from './environment_item.vue';
import DeployBoard from './deploy_board.vue';
import CanaryDeploymentCallout from './canary_deployment_callout.vue';
export default {
components: {
EnvironmentItem,
GlLoadingIcon,
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
DeployBoard,
CanaryDeploymentCallout,
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
CanaryUpdateModal: () => import('ee_component/environments/components/canary_update_modal.vue'),
},
......
......@@ -3,7 +3,7 @@
*/
import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs';
import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store';
import EnvironmentsStore from '../stores/environments_store';
import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
......
......@@ -81,6 +81,17 @@ export default class EnvironmentsStore {
this.state.environments = filteredEnvironments;
/**
* 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;
}
return filteredEnvironments;
}
......
......@@ -129,3 +129,146 @@
width: 38px;
}
}
/**
* Deploy boards
*/
.deploy-board {
background-color: var(--gray-50, $gray-50);
min-height: 20px;
> .loading-icon,
> .deploy-board-empty,
> .deploy-board-information {
padding: 10px;
}
.deploy-board-information {
display: flex;
justify-content: space-between;
.deploy-board-status {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 0 5px;
}
.deploy-board-instances {
order: 2;
margin-left: 20px;
width: 100%;
}
.deploy-board-canary-ingress {
order: 7;
}
.deploy-board-actions {
order: 3;
align-self: center;
min-width: 150px;
margin-left: 10px;
}
&.deploy-board-error-message {
justify-content: center;
}
.deploy-board-empty-state-svg {
order: 1;
width: 90px;
margin: auto 0 auto 20px;
}
.deploy-board-empty-state-text {
order: 2;
flex-wrap: wrap;
margin: auto auto 15px 0;
}
.deploy-board-empty-state-title {
order: 1;
font-size: 17px;
line-height: 40px;
}
}
.deploy-board-legend .legend-text {
color: var(--gray-900, $gray-900);
font-size: $gl-font-size-small;
font-weight: $gl-font-weight-bold;
line-height: $gl-line-height-14;
}
}
.deploy-board-icon {
display: none;
@include media-breakpoint-up(md) {
float: left;
display: block;
}
i {
cursor: pointer;
color: var(--gray-200, $gray-200);
padding-right: 10px;
}
}
.canary-deployment-callout {
border-bottom: 1px solid var(--gray-500, $gray-500);
display: flex;
@include media-breakpoint-down(sm) {
display: none;
}
&-lock {
height: 82px;
width: 92px;
}
&-message {
max-width: 600px;
color: var(--gray-500, $gray-500);
}
&-close {
color: var(--gray-500, $gray-500);
cursor: pointer;
}
&-button {
border-color: var(--blue-500, $blue-500);
color: var(--blue-500, $blue-500);
&:not(:disabled):not(.disabled):active {
background-color: var(--blue-200, $blue-200);
border: 2px solid var(--blue-600, $blue-600);
color: var(--blue-700, $blue-700);
height: 34px;
padding: 5px 9px;
}
&:focus {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
box-shadow: 0 0 4px 1px var(--blue-200, $blue-200);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
&:hover {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
}
}
......@@ -19,6 +19,7 @@ class EnvironmentEntity < Grape::Entity
expose :name_without_type
expose :last_deployment, using: DeploymentEntity
expose :stop_action_available?, as: :has_stop_action
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: RolloutStatusEntity
expose :upcoming_deployment, expose_nil: false do |environment, ops|
DeploymentEntity.represent(environment.upcoming_deployment,
......@@ -104,6 +105,10 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project)
end
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
......
---
title: Move deploy boards to Core
merge_request: 47147
author:
type: changed
......@@ -2,6 +2,7 @@
import { GlTable, GlEmptyState, GlLoadingIcon, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import deploymentInstance from '~/vue_shared/components/deployment_instance.vue';
export default {
components: {
......@@ -12,7 +13,7 @@ export default {
GlLink,
GlLoadingIcon,
GlSprintf,
deploymentInstance: () => import('ee_component/vue_shared/components/deployment_instance.vue'),
deploymentInstance,
},
props: {
isFetching: {
......
import CeEnvironmentsStore from '~/environments/stores/environments_store';
export default class EnvironmentsStore extends CeEnvironmentsStore {
// TODO: delete when deploy boards moved to Core
storeEnvironments(environments = []) {
super.storeEnvironments(environments);
/**
* 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;
}
}
}
@import 'page_bundles/mixins_and_variables_and_functions';
/**
* Deploy boards
*/
.deploy-board {
background-color: var(--gray-50, $gray-50);
min-height: 20px;
> .loading-icon,
> .deploy-board-empty,
> .deploy-board-information {
padding: 10px;
}
.deploy-board-information {
display: flex;
justify-content: space-between;
.deploy-board-status {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 0 5px;
}
.deploy-board-instances {
order: 2;
margin-left: 20px;
width: 100%;
}
.deploy-board-canary-ingress {
order: 7;
}
.deploy-board-actions {
order: 3;
align-self: center;
min-width: 150px;
margin-left: 10px;
}
&.deploy-board-error-message {
justify-content: center;
}
.deploy-board-empty-state-svg {
order: 1;
width: 90px;
margin: auto 0 auto 20px;
}
.deploy-board-empty-state-text {
order: 2;
flex-wrap: wrap;
margin: auto auto 15px 0;
}
.deploy-board-empty-state-title {
order: 1;
font-size: 17px;
line-height: 40px;
}
}
.deploy-board-legend .legend-text {
color: var(--gray-900, $gray-900);
font-size: $gl-font-size-small;
font-weight: $gl-font-weight-bold;
line-height: $gl-line-height-14;
}
}
.deploy-board-icon {
display: none;
@include media-breakpoint-up(md) {
float: left;
display: block;
}
i {
cursor: pointer;
color: var(--gray-200, $gray-200);
padding-right: 10px;
}
}
@import '../../../../../app/assets/stylesheets/page_bundles/environments';
.alert-dropdown-button {
margin-left: $btn-side-margin;
......@@ -132,57 +43,3 @@
display: flex;
}
}
.canary-deployment-callout {
border-bottom: 1px solid var(--gray-500, $gray-500);
display: flex;
@include media-breakpoint-down(sm) {
display: none;
}
&-lock {
height: 82px;
width: 92px;
}
&-message {
max-width: 600px;
color: var(--gray-500, $gray-500);
}
&-close {
color: var(--gray-500, $gray-500);
cursor: pointer;
}
&-button {
border-color: var(--blue-500, $blue-500);
color: var(--blue-500, $blue-500);
&:not(:disabled):not(.disabled):active {
background-color: var(--blue-200, $blue-200);
border: 2px solid var(--blue-600, $blue-600);
color: var(--blue-700, $blue-700);
height: 34px;
padding: 5px 9px;
}
&:focus {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
box-shadow: 0 0 4px 1px var(--blue-200, $blue-200);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
&:hover {
background-color: var(--blue-500, $blue-500);
border: 2px solid var(--blue-500, $blue-500);
color: var(--blue-600, $blue-600);
height: 34px;
padding: 5px 9px;
}
}
}
......@@ -6,16 +6,11 @@ module EE
extend ::Gitlab::Utils::Override
prepended do
expose :rollout_status, if: -> (*) { can_read_deploy_board? }, using: ::RolloutStatusEntity
expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
end
private
def can_read_deploy_board?
can?(current_user, :read_deploy_board, environment.project)
end
def can_read_alert_management_alert?
can?(current_user, :read_alert_management_alert, environment.project) &&
environment.project.feature_available?(:environment_alerts)
......
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe Projects::EnvironmentsController do
include KubernetesHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
......@@ -18,64 +16,6 @@ RSpec.describe Projects::EnvironmentsController do
sign_in(user)
end
describe 'GET index' do
context 'when requesting JSON response for folders' do
before do
allow_any_instance_of(EE::Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(EE::Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
name: 'staging/review-2',
state: :available)
create(:environment, project: project,
name: 'staging/review-3',
state: :stopped)
end
let(:environments) { json_response['environments'] }
context 'when requesting available environments scope' do
before do
stub_licensed_features(deploy_board: true)
get :index, params: environment_params(format: :json, nested: true, scope: :available)
end
it 'responds with matching schema' do
expect(response).to match_response_schema('environments', dir: 'ee')
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status']).to be_present
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status']).to be_present
end
end
context 'when license does not have the GitLab_DeployBoard add-on' do
before do
stub_licensed_features(deploy_board: false)
get :index, params: environment_params(format: :json, nested: true)
end
it 'does not return the rollout_status_path attribute' do
expect(environments.first['latest']['rollout_status']).not_to be_present
expect(environments.second['latest']['rollout_status']).not_to be_present
end
end
end
end
describe '#GET terminal' do
let(:protected_environment) { create(:protected_environment, name: environment.name, project: project) }
......
......@@ -21,7 +21,7 @@
"rollout_status": {
"oneOf": [
{ "type": "null" },
{ "$ref": "../rollout_status.json" }
{ "$ref": "../../../../../../spec/fixtures/api/schemas/rollout_status.json" }
]
},
"logs_path": { "type": "string" },
......
......@@ -44,7 +44,7 @@
"type": "boolean"
},
"rollout_status": {
"$ref": "rollout_status.json"
"$ref": "../../../../../spec/fixtures/api/schemas/rollout_status.json"
},
"environment_path": {
"type": "string"
......
import Store from 'ee/environments/stores/environments_store';
import { serverDataList, deployBoardMockData } from './mock_data';
describe('Store', () => {
let store;
beforeEach(() => {
store = new Store();
});
it('should store a non folder environment with deploy board if rollout_status key is provided', () => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
rollout_status: deployBoardMockData,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
});
it('should keep deploy board data when updating environments', () => {
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
describe('canaryCallout', () => {
it('should add banner underneath the second environment', () => {
store.storeEnvironments(serverDataList);
expect(store.state.environments[1].showCanaryCallout).toEqual(true);
});
it('should add banner underneath first environment when only one environment', () => {
store.storeEnvironments(serverDataList.slice(0, 1));
expect(store.state.environments[0].showCanaryCallout).toEqual(true);
});
});
});
......@@ -3,8 +3,6 @@ import EnvironmentAlert from 'ee/environments/components/environment_alert.vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryUpdateModal from 'ee/environments/components/canary_update_modal.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub';
import { deployBoardMockData } from './mock_data';
describe('Environment table', () => {
let wrapper;
......@@ -25,125 +23,6 @@ describe('Environment table', () => {
wrapper.destroy();
});
it('Should render a table', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
};
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.classes()).toContain('ci-table');
});
it('should render deploy board container when data is provided', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.js-deploy-board-row').exists()).toBe(true);
expect(wrapper.find('.deploy-board-icon').exists()).toBe(true);
});
it('should toggle deploy board visibility when arrow is clicked', (done) => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: {
instances: [{ status: 'ready', tooltip: 'foo' }],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
canary_ingress: { canary_weight: 60 },
},
isDeployBoardVisible: false,
};
eventHub.$on('toggleDeployBoard', (env) => {
expect(env.id).toEqual(mockItem.id);
done();
});
factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find('.deploy-board-icon').trigger('click');
});
it('should render canary callout', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
showCanaryCallout: true,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.canary-deployment-callout').exists()).toBe(true);
});
it('should render the alert if there is one', async () => {
const mockItem = {
name: 'review',
......
// TODO remove
export const deployBoardMockData = {
instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished', pod_name: 'production-tanuki-1' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying', pod_name: 'production-tanuki-1' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready', pod_name: 'production-tanuki-1' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting', pod_name: 'production-tanuki-1' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting', pod_name: 'production-tanuki-1' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
status: 'found',
canary_ingress: {
canary_weight: 50,
},
};
export const environment = {
name: 'production',
size: 1,
......
......@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe EnvironmentEntity do
include KubernetesHelpers
let(:user) { create(:user) }
let(:environment) { create(:environment) }
let(:project) { create(:project) }
......@@ -52,34 +50,6 @@ RSpec.describe EnvironmentEntity do
end
end
context 'when deploy_boards are available' do
before do
stub_licensed_features(deploy_board: true)
end
context 'with deployment service ready' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
environment.project.add_maintainer(user)
end
it 'exposes rollout_status' do
expect(subject).to include(:rollout_status)
end
end
end
context 'when deploy_boards are not available' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
end
it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status)
end
end
context 'when environment has a review app' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, :with_review_app, ref: 'development', project: project) }
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Projects::EnvironmentsController do
include MetricsDashboardHelpers
include KubernetesHelpers
let_it_be(:project) { create(:project) }
let_it_be(:maintainer) { create(:user, name: 'main-dos').tap { |u| project.add_maintainer(u) } }
......@@ -34,6 +35,9 @@ RSpec.describe Projects::EnvironmentsController do
context 'when requesting JSON response for folders' do
before do
allow_any_instance_of(Environment).to receive(:has_terminals?).and_return(true)
allow_any_instance_of(Environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
create(:environment, project: project,
name: 'staging/review-1',
state: :available)
......@@ -91,9 +95,11 @@ RSpec.describe Projects::EnvironmentsController do
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.first['latest']['rollout_status']).to be_present
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
expect(environments.second['latest']['rollout_status']).to be_present
end
it 'contains values describing environment scopes sizes' do
......
......@@ -17,6 +17,8 @@ RSpec.describe 'Environment > Pod Logs', :js, :kubeclient do
stub_kubeclient_pods(environment.deployment_namespace)
stub_kubeclient_logs(pod_name, environment.deployment_namespace, container: 'container-0')
stub_kubeclient_deployments(environment.deployment_namespace)
stub_kubeclient_ingresses(environment.deployment_namespace)
stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url)
sign_in(project.owner)
......
......@@ -37,6 +37,12 @@
"has_opened_alert": { "type": "boolean" },
"cluster_type": { "type": "types/nullable_string.json" },
"terminal_path": { "type": "types/nullable_string.json" },
"rollout_status": {
"oneOf": [
{ "type": "null" },
{ "$ref": "rollout_status.json" }
]
},
"last_deployment": {
"oneOf": [
{ "type": "null" },
......
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
import CanaryIngress from 'ee/environments/components/canary_ingress.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
import { deployBoardMockData, environment } from './mock_data';
const logsPath = `gitlab-org/gitlab-test/-/logs?environment_name=${environment.name}`;
......
import { mount } from '@vue/test-utils';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import { folder } from './mock_data';
import eventHub from '~/environments/event_hub';
import { folder, deployBoardMockData } from './mock_data';
const eeOnlyProps = {
canaryDeploymentFeatureId: 'canary_deployment',
......@@ -37,10 +38,124 @@ describe('Environment table', () => {
wrapper.destroy();
});
it('Should render a table', () => {
it('Should render a table', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
};
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.classes()).toContain('ci-table');
});
it('should render deploy board container when data is provided', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: deployBoardMockData,
isDeployBoardVisible: true,
isLoadingDeployBoard: false,
isEmptyDeployBoard: false,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.js-deploy-board-row').exists()).toBe(true);
expect(wrapper.find('.deploy-board-icon').exists()).toBe(true);
});
it('should toggle deploy board visibility when arrow is clicked', done => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
hasDeployBoard: true,
deployBoardData: {
instances: [{ status: 'ready', tooltip: 'foo' }],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
},
isDeployBoardVisible: false,
};
eventHub.$on('toggleDeployBoard', env => {
expect(env.id).toEqual(mockItem.id);
done();
});
factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
wrapper.find('.deploy-board-icon').trigger('click');
});
it('should render canary callout', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
environment_path: 'url',
showCanaryCallout: true,
};
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
});
expect(wrapper.find('.canary-deployment-callout').exists()).toBe(true);
});
describe('sortEnvironments', () => {
it('should sort environments by last updated', () => {
const mockItems = [
......
......@@ -5,6 +5,8 @@ import EnableReviewAppModal from '~/environments/components/enable_review_app_mo
import Container from '~/environments/components/container.vue';
import EmptyState from '~/environments/components/empty_state.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import CanaryDeploymentBoard from '~/environments/components/canary_deployment_callout.vue';
import axios from '~/lib/utils/axios_utils';
import { environment, folder } from './mock_data';
......@@ -36,6 +38,9 @@ describe('Environment', () => {
});
};
const canaryPromoKeyValue = () =>
wrapper.find(CanaryDeploymentBoard).attributes('data-js-canary-promo-key');
const createWrapper = (shallow = false, props = {}) => {
const fn = shallow ? shallowMount : mount;
wrapper = extendedWrapper(fn(EnvironmentsApp, { propsData: { ...mockData, ...props } }));
......@@ -114,6 +119,57 @@ describe('Environment', () => {
expect(wrapper.vm.updateContent).toHaveBeenCalledTimes(0);
});
});
describe('deploy boards', () => {
beforeEach(() => {
const deployEnvironment = {
...environment,
rollout_status: {
status: 'found',
},
};
mockRequest(200, {
environments: [deployEnvironment],
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render deploy boards', () => {
expect(wrapper.find(DeployBoard).exists()).toBe(true);
});
it('should render arrow to open deploy boards', () => {
expect(
wrapper.find('.deploy-board-icon [data-testid="chevron-down-icon"]').exists(),
).toBe(true);
});
});
describe('canary callout with one environment', () => {
it('should render banner underneath first environment', () => {
expect(canaryPromoKeyValue()).toBe('0');
});
});
describe('canary callout with multiple environments', () => {
beforeEach(() => {
mockRequest(200, {
environments: [environment, environment],
stopped_count: 1,
available_count: 0,
});
return createWrapper();
});
it('should render banner underneath second environment', () => {
expect(canaryPromoKeyValue()).toBe('1');
});
});
});
});
......
......@@ -76,27 +76,6 @@ describe('Store', () => {
expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
});
});
describe('toggleFolder', () => {
it('should toggle folder', () => {
store.storeEnvironments(serverData);
......@@ -181,4 +160,72 @@ describe('Store', () => {
expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
});
});
it('should store a non folder environment with deploy board if rollout_status key is provided', () => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
rollout_status: deployBoardMockData,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
});
it('should keep deploy board data when updating environments', () => {
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
const environment = {
name: 'foo',
size: 1,
latest: {
id: 1,
},
rollout_status: deployBoardMockData,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
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);
});
});
});
......@@ -60,6 +60,9 @@ const deployBoardMockData = {
rollback_url: 'url',
completion: 100,
status: 'found',
canary_ingress: {
canary_weight: 50,
},
};
const environment = {
......
import { shallowMount } from '@vue/test-utils';
import DeployBoardInstance from 'ee/vue_shared/components/deployment_instance.vue';
import DeployBoardInstance from '~/vue_shared/components/deployment_instance.vue';
import { folder } from './mock_data';
describe('Deploy Board Instance', () => {
......
......@@ -3,9 +3,10 @@
require 'spec_helper'
RSpec.describe EnvironmentEntity do
include KubernetesHelpers
include Gitlab::Routing.url_helpers
let(:request) { double('request') }
let(:request) { double('request', current_user: user, project: project) }
let(:entity) do
described_class.new(environment, request: request)
end
......@@ -167,4 +168,23 @@ RSpec.describe EnvironmentEntity do
end
end
end
context 'with deployment service ready' do
before do
allow(environment).to receive(:has_terminals?).and_return(true)
allow(environment).to receive(:rollout_status).and_return(kube_deployment_rollout_status)
end
it 'exposes rollout_status' do
expect(subject).to include(:rollout_status)
end
end
context 'with deployment service not ready' do
let(:user) { create(:user) }
it 'does not expose rollout_status' do
expect(subject).not_to include(:rollout_status)
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