Commit 255e8376 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'show-alert-on-environments' into 'master'

RUN-AS-IF-FOSS Show alert on environments

See merge request gitlab-org/gitlab!39743
parents 9065fa7d db6e1bfc
......@@ -14,6 +14,7 @@ export default {
DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
},
props: {
environments: {
......@@ -111,6 +112,9 @@ export default {
shouldShowCanaryCallout(env) {
return env.showCanaryCallout && this.showCanaryDeploymentCallout;
},
shouldRenderAlert(env) {
return env?.has_opened_alert;
},
sortEnvironments(environments) {
/*
* The sorting algorithm should sort in the following priorities:
......@@ -185,6 +189,11 @@ export default {
/>
</div>
</div>
<environment-alert
v-if="shouldRenderAlert(model)"
:key="`alert-row-${i}`"
:environment="model"
/>
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import canaryCalloutMixin from '../mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
Vue.use(VueApollo);
export default () =>
new Vue({
el: '#environments-folder-list-view',
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('environments-folder-list-view');
return new Vue({
el,
components: {
environmentsFolderApp,
},
mixins: [canaryCalloutMixin],
apolloProvider,
provide: {
projectPath: el.dataset.projectPath,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
......@@ -35,3 +48,4 @@ export default () =>
});
},
});
};
......@@ -23,7 +23,8 @@ export default {
},
cssContainerClass: {
type: String,
required: true,
required: false,
default: '',
},
canReadEnvironment: {
type: Boolean,
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import canaryCalloutMixin from './mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
Vue.use(VueApollo);
export default () =>
new Vue({
el: '#environments-list-view',
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('environments-list-view');
return new Vue({
el,
components: {
environmentsComponent,
},
mixins: [canaryCalloutMixin],
apolloProvider,
provide: {
projectPath: el.dataset.projectPath,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
const environmentsData = el.dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
......@@ -39,3 +51,4 @@ export default () =>
});
},
});
};
......@@ -2,7 +2,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default {
data() {
const data = document.querySelector(this.$options.el).dataset;
const data = this.$options.el.dataset;
return {
canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
......
......@@ -2,4 +2,4 @@
- breadcrumb_title _("Folder/%{name}") % { name: @folder }
- page_title _("Environments in %{name}") % { name: @folder }
#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data } }
#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, project_path: @project.full_path } }
......@@ -5,4 +5,5 @@
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments/index.md"),
"deploy-boards-help-path" => help_page_path("user/project/deploy_boards", anchor: "enabling-deploy-boards") } }
"deploy-boards-help-path" => help_page_path("user/project/deploy_boards", anchor: "enabling-deploy-boards"),
"project-path" => @project.full_path } }
<script>
import { GlLink, GlSprintf, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { s__ } from '~/locale';
import TimeagoMixin from '~/vue_shared/mixins/timeago';
import alertQuery from '../graphql/queries/environment.query.graphql';
export default {
components: {
GlLink,
GlSprintf,
SeverityBadge,
},
directives: {
GlTooltip,
},
mixins: [TimeagoMixin],
props: {
environment: {
required: true,
type: Object,
},
},
inject: {
projectPath: {
type: String,
default: '',
},
},
data() {
return { alert: null };
},
apollo: {
alert: {
query: alertQuery,
variables() {
return {
fullPath: this.projectPath,
environmentName: this.environment.name,
};
},
update(data) {
return data?.project?.environment?.latestOpenedMostSevereAlert;
},
},
},
translations: {
alertText: s__(
'EnvironmentsAlert|%{severity} • %{title} %{text}. %{linkStart}View Details%{linkEnd} · %{startedAt} ',
),
},
computed: {
humanizedText() {
return this.alert?.prometheusAlert?.humanizedText;
},
severity() {
return this.alert?.severity || '';
},
},
classes: [
'gl-py-2',
'gl-pl-3',
'gl-text-gray-900',
'gl-bg-gray-10',
'gl-border-t-solid',
'gl-border-gray-100',
'gl-border-1',
],
};
</script>
<template>
<div v-if="alert" :class="$options.classes" data-testid="alert">
<gl-sprintf :message="$options.translations.alertText">
<template #severity>
<severity-badge :severity="severity" class="gl-display-inline" />
</template>
<template #startedAt>
<span v-gl-tooltip :title="tooltipTitle(alert.startedAt)">
{{ timeFormatted(alert.startedAt) }}
</span>
</template>
<template #title>
<span>{{ alert.title }}</span>
</template>
<template #text>
<span>{{ humanizedText }}</span>
</template>
<template #link="{ content }">
<gl-link :href="alert.detailsUrl" data-testid="alert-link">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</template>
query environment($fullPath: ID!, $environmentName: String) {
project(fullPath: $fullPath) {
environment(name: $environmentName) {
latestOpenedMostSevereAlert {
title
severity
detailsUrl
startedAt
prometheusAlert {
humanizedText
}
}
}
}
}
---
title: Show Latest Most Severe Alert on Environment
merge_request: 39743
author:
type: added
import { mount } from '@vue/test-utils';
import EnvironmentAlert from 'ee/environments/components/environment_alert.vue';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { useFakeDate } from 'helpers/fake_date';
describe('Environment Alert', () => {
let wrapper;
const DEFAULT_PROVIDE = { projectPath: 'test-org/test' };
const DEFAULT_PROPS = { environment: { name: 'staging' } };
useFakeDate();
const factory = (props = {}, provide = {}) => {
wrapper = mount(EnvironmentAlert, {
propsData: {
...DEFAULT_PROPS,
...props,
},
provide: {
...DEFAULT_PROVIDE,
...provide,
},
});
};
beforeEach(() => {
factory();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('has alert', () => {
beforeEach(() => {
wrapper.setData({
alert: {
severity: 'CRITICAL',
title: 'alert title',
prometheusAlert: { humanizedText: '>0.1% jest' },
detailsUrl: '/alert/details',
startedAt: new Date(),
},
});
});
it('should display the alert details', () => {
const text = wrapper.text();
expect(text).toContain('Critical');
expect(text).toContain('alert title >0.1% jest.');
expect(text).toContain('View Details');
expect(text).toContain('just now');
});
it('should link to the details of the alert', () => {
const link = wrapper.find('[data-testid="alert-link"]');
expect(link.text()).toBe('View Details');
expect(link.attributes('href')).toBe('/alert/details');
});
it('should show a severity badge', () => {
expect(wrapper.find(SeverityBadge).props('severity')).toBe('CRITICAL');
});
});
describe('has no alert', () => {
it('should display nothing', () => {
expect(wrapper.find('[data-testid="alert"]').exists()).toBe(false);
});
});
});
import { mount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import EnvironmentAlert from 'ee/environments/components/environment_alert.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub';
import { deployBoardMockData } from './mock_data';
......@@ -6,21 +7,23 @@ import { deployBoardMockData } from './mock_data';
describe('Environment table', () => {
let wrapper;
const factory = (options = {}) => {
const factory = async (options = {}, m = mount) => {
// This destroys any wrappers created before a nested call to factory reassigns it
if (wrapper && wrapper.destroy) {
wrapper.destroy();
}
wrapper = mount(EnvironmentTable, {
wrapper = m(EnvironmentTable, {
...options,
});
await wrapper.vm.$nextTick();
await jest.runOnlyPendingTimers();
};
afterEach(() => {
wrapper.destroy();
});
it('Should render a table', () => {
it('Should render a table', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
......@@ -29,7 +32,7 @@ describe('Environment table', () => {
environment_path: 'url',
};
factory({
await factory({
propsData: {
environments: [mockItem],
canReadEnvironment: true,
......@@ -44,7 +47,7 @@ describe('Environment table', () => {
expect(wrapper.classes()).toContain('ci-table');
});
it('should render deploy board container when data is provided', () => {
it('should render deploy board container when data is provided', async () => {
const mockItem = {
name: 'review',
size: 1,
......@@ -58,7 +61,7 @@ describe('Environment table', () => {
isEmptyDeployBoard: false,
};
factory({
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
......@@ -71,8 +74,8 @@ describe('Environment table', () => {
},
});
expect(wrapper.find('.js-deploy-board-row')).toBeDefined();
expect(wrapper.find('.deploy-board-icon')).not.toBeNull();
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 => {
......@@ -112,7 +115,7 @@ describe('Environment table', () => {
wrapper.find('.deploy-board-icon').trigger('click');
});
it('should render canary callout', () => {
it('should render canary callout', async () => {
const mockItem = {
name: 'review',
folderName: 'review',
......@@ -122,7 +125,7 @@ describe('Environment table', () => {
showCanaryCallout: true,
};
factory({
await factory({
propsData: {
environments: [mockItem],
canCreateDeployment: false,
......@@ -135,6 +138,35 @@ describe('Environment table', () => {
},
});
expect(wrapper.find('.canary-deployment-callout')).not.toBeNull();
expect(wrapper.find('.canary-deployment-callout').exists()).toBe(true);
});
it('should render the alert if there is one', async () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
logs_path: 'url',
id: 1,
hasDeployBoard: false,
has_opened_alert: true,
};
await factory(
{
propsData: {
environments: [mockItem],
canReadEnvironment: true,
canaryDeploymentFeatureId: 'canary_deployment',
showCanaryDeploymentCallout: true,
userCalloutsPath: '/callouts',
lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
helpCanaryDeploymentsPath: 'help/canary-deployments',
},
},
shallowMount,
);
expect(wrapper.find(EnvironmentAlert).exists()).toBe(true);
});
});
......@@ -9541,6 +9541,9 @@ msgstr ""
msgid "Environments in %{name}"
msgstr ""
msgid "EnvironmentsAlert|%{severity} • %{title} %{text}. %{linkStart}View Details%{linkEnd} · %{startedAt} "
msgstr ""
msgid "EnvironmentsDashboard|Add a project to the dashboard"
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