Skip to content
Snippets Groups Projects
Commit 40ccf5c4 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Miguel Rincon
Browse files

Show alert about any configured deploy freezes

So users may know that their deployments might be skipped or fail during
a configured deploy freeze.

Changelog: added
parent 0c8d726f
No related branches found
No related tags found
3 merge requests!122597doc/gitaly: Remove references to removed metrics,!120936Draft: Debugging commit to trigger pipeline (DO NOT MERGE),!120351Show alert about any configured deploy freezes
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import deployFreezesQuery from '../graphql/queries/deploy_freezes.query.graphql';
export default {
components: {
GlAlert,
GlLink,
GlSprintf,
},
inject: ['projectFullPath'],
props: {
name: {
type: String,
required: true,
},
},
data() {
return { deployFreezes: [] };
},
apollo: {
deployFreezes: {
query: deployFreezesQuery,
update(data) {
const freezes = data?.project?.environment?.deployFreezes;
return sortBy(freezes, [(freeze) => freeze.startTime]);
},
variables() {
return {
projectFullPath: this.projectFullPath,
environmentName: this.name,
};
},
},
},
computed: {
shouldShowDeployFreezeAlert() {
return this.deployFreezes.length > 0;
},
nextDeployFreeze() {
return this.deployFreezes[0];
},
deployFreezeStartTime() {
return formatDate(this.nextDeployFreeze.startTime);
},
deployFreezeEndTime() {
return formatDate(this.nextDeployFreeze.endTime);
},
},
i18n: {
deployFreezeAlert: s__(
'Environments|A freeze period is in effect from %{startTime} to %{endTime}. Deployments might fail during this time. For more information, see the %{docsLinkStart}deploy freeze documentation%{docsLinkEnd}.',
),
},
deployFreezeDocsPath: helpPagePath('user/project/releases/index', {
anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze',
}),
};
</script>
<template>
<gl-alert v-if="shouldShowDeployFreezeAlert" :dismissible="false" class="gl-mt-4">
<gl-sprintf :message="$options.i18n.deployFreezeAlert">
<template #startTime
><span class="gl-font-weight-bold">{{ deployFreezeStartTime }}</span></template
>
<template #endTime
><span class="gl-font-weight-bold">{{ deployFreezeEndTime }}</span></template
>
<template #docsLink="{ content }"
><gl-link :href="$options.deployFreezeDocsPath">{{ content }}</gl-link></template
>
</gl-sprintf>
</gl-alert>
</template>
......@@ -7,6 +7,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import DeployFreezeAlert from './deploy_freeze_alert.vue';
export default {
name: 'EnvironmentsDetailHeader',
......@@ -15,6 +16,7 @@ export default {
GlButton,
GlSprintf,
TimeAgo,
DeployFreezeAlert,
DeleteEnvironmentModal,
StopEnvironmentModal,
},
......@@ -96,81 +98,88 @@ export default {
};
</script>
<template>
<header class="top-area gl-justify-content-between">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
<h1 class="page-title gl-font-size-h-display">
{{ environment.name }}
</h1>
<p v-if="shouldShowCancelAutoStopButton" class="gl-mb-0 gl-ml-3" data-testid="auto-stops-at">
<gl-sprintf :message="$options.i18n.autoStopAtText">
<template #autoStopAt>
<time-ago :time="environment.autoStopAt" />
</template>
</gl-sprintf>
</p>
</div>
<div class="nav-controls gl-my-1">
<form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-button
<div>
<deploy-freeze-alert :name="environment.name" />
<header class="top-area gl-justify-content-between">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-center">
<h1 class="page-title gl-font-size-h-display">
{{ environment.name }}
</h1>
<p
v-if="shouldShowCancelAutoStopButton"
v-gl-tooltip.hover
data-testid="cancel-auto-stop-button"
:title="$options.i18n.cancelAutoStopButtonTitle"
type="submit"
icon="thumbtack"
class="gl-mb-0 gl-ml-3"
data-testid="auto-stops-at"
>
<gl-sprintf :message="$options.i18n.autoStopAtText">
<template #autoStopAt>
<time-ago :time="environment.autoStopAt" />
</template>
</gl-sprintf>
</p>
</div>
<div class="nav-controls gl-my-1">
<form method="POST" :action="cancelAutoStopPath" data-testid="cancel-auto-stop-form">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<gl-button
v-if="shouldShowCancelAutoStopButton"
v-gl-tooltip.hover
data-testid="cancel-auto-stop-button"
:title="$options.i18n.cancelAutoStopButtonTitle"
type="submit"
icon="thumbtack"
/>
</form>
<gl-button
v-if="shouldShowTerminalButton"
data-testid="terminal-button"
:href="terminalPath"
icon="terminal"
/>
</form>
<gl-button
v-if="shouldShowTerminalButton"
data-testid="terminal-button"
:href="terminalPath"
icon="terminal"
/>
<gl-button
v-if="shouldShowExternalUrlButton"
v-gl-tooltip.hover
data-testid="external-url-button"
:title="$options.i18n.externalButtonTitle"
:href="environment.externalUrl"
is-unsafe-link
icon="external-link"
target="_blank"
>{{ $options.i18n.externalButtonText }}</gl-button
>
<gl-button
v-if="shouldShowMetricsButton"
v-gl-tooltip.hover
data-testid="metrics-button"
:href="metricsPath"
:title="$options.i18n.metricsButtonTitle"
icon="chart"
class="gl-mr-2"
>
{{ $options.i18n.metricsButtonText }}
</gl-button>
<gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
{{ $options.i18n.editButtonText }}
</gl-button>
<gl-button
v-if="shouldShowStopButton"
v-gl-modal-directive="'stop-environment-modal'"
data-testid="stop-button"
icon="stop"
variant="danger"
>
{{ $options.i18n.stopButtonText }}
</gl-button>
<gl-button
v-if="canDestroyEnvironment"
v-gl-modal-directive="'delete-environment-modal'"
data-testid="destroy-button"
variant="danger"
>
{{ $options.i18n.deleteButtonText }}
</gl-button>
</div>
<delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
<stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
</header>
<gl-button
v-if="shouldShowExternalUrlButton"
v-gl-tooltip.hover
data-testid="external-url-button"
:title="$options.i18n.externalButtonTitle"
:href="environment.externalUrl"
is-unsafe-link
icon="external-link"
target="_blank"
>{{ $options.i18n.externalButtonText }}</gl-button
>
<gl-button
v-if="shouldShowMetricsButton"
v-gl-tooltip.hover
data-testid="metrics-button"
:href="metricsPath"
:title="$options.i18n.metricsButtonTitle"
icon="chart"
class="gl-mr-2"
>
{{ $options.i18n.metricsButtonText }}
</gl-button>
<gl-button v-if="canUpdateEnvironment" data-testid="edit-button" :href="updatePath">
{{ $options.i18n.editButtonText }}
</gl-button>
<gl-button
v-if="shouldShowStopButton"
v-gl-modal-directive="'stop-environment-modal'"
data-testid="stop-button"
icon="stop"
variant="danger"
>
{{ $options.i18n.stopButtonText }}
</gl-button>
<gl-button
v-if="canDestroyEnvironment"
v-gl-modal-directive="'delete-environment-modal'"
data-testid="destroy-button"
variant="danger"
>
{{ $options.i18n.deleteButtonText }}
</gl-button>
</div>
<delete-environment-modal v-if="canDestroyEnvironment" :environment="environment" />
<stop-environment-modal v-if="shouldShowStopButton" :environment="environment" />
</header>
</div>
</template>
query getEnvironmentFreezes($projectFullPath: ID!, $environmentName: String) {
project(fullPath: $projectFullPath) {
id
environment(name: $environmentName) {
id
deployFreezes {
startTime
endTime
}
}
}
}
......@@ -3,9 +3,13 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
import { apolloProvider } from './graphql/client';
import { apolloProvider as createApolloProvider } from './graphql/client';
import environmentsMixin from './mixins/environments_mixin';
Vue.use(VueApollo);
const apolloProvider = createApolloProvider();
export const initHeader = () => {
const el = document.getElementById('environments-detail-view-header');
const container = document.getElementById('environments-detail-view');
......@@ -13,7 +17,11 @@ export const initHeader = () => {
return new Vue({
el,
apolloProvider,
mixins: [environmentsMixin],
provide: {
projectFullPath: dataset.projectFullPath,
},
data() {
const environment = {
name: dataset.name,
......@@ -60,7 +68,6 @@ export const initPage = async () => {
const dataElement = document.getElementById('environments-detail-view');
const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
Vue.use(VueApollo);
Vue.use(VueRouter);
const el = document.getElementById('environment_details_page');
......@@ -90,7 +97,7 @@ export const initPage = async () => {
return new Vue({
el,
apolloProvider: apolloProvider(),
apolloProvider,
router,
provide: {
projectPath: dataSet.projectFullPath,
......
......@@ -126,6 +126,10 @@ vacation period when most employees are out, you can set up a [Deploy Freeze](..
During a deploy freeze period, no deployment can be executed. This is helpful to
ensure that deployments do not happen unexpectedly.
The next configured deploy freeze is displayed at the top of the
[environment deployments list](index.md#view-environments-and-deployments)
page.
## Protect production secrets
Production secrets are needed to deploy successfully. For example, when deploying to the cloud,
......
......@@ -16869,6 +16869,9 @@ msgstr ""
msgid "EnvironmentsDashboard|This dashboard displays 3 environments per project, and is linked to the Operations Dashboard. When you add or remove a project from one dashboard, GitLab adds or removes the project from the other. %{linkStart}More information%{linkEnd}"
msgstr ""
 
msgid "Environments|A freeze period is in effect from %{startTime} to %{endTime}. Deployments might fail during this time. For more information, see the %{docsLinkStart}deploy freeze documentation%{docsLinkEnd}."
msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr ""
 
import { GlAlert, GlLink } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
import deployFreezesQuery from '~/environments/graphql/queries/deploy_freezes.query.graphql';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
const ENVIRONMENT_NAME = 'staging';
Vue.use(VueApollo);
describe('~/environments/components/deploy_freeze_alert.vue', () => {
let wrapper;
const createWrapper = (deployFreezes = []) => {
const mockApollo = createMockApollo([
[
deployFreezesQuery,
jest.fn().mockResolvedValue({
data: {
project: {
id: '1',
__typename: 'Project',
environment: {
id: '1',
__typename: 'Environment',
deployFreezes,
},
},
},
}),
],
]);
wrapper = mountExtended(DeployFreezeAlert, {
apolloProvider: mockApollo,
provide: {
projectFullPath: 'gitlab-org/gitlab',
},
propsData: {
name: ENVIRONMENT_NAME,
},
});
};
describe('with deploy freezes', () => {
let deployFreezes;
let alert;
beforeEach(async () => {
deployFreezes = [
{
__typename: 'CiFreezePeriod',
startTime: new Date('2020-02-01'),
endTime: new Date('2020-02-02'),
},
{
__typename: 'CiFreezePeriod',
startTime: new Date('2020-01-01'),
endTime: new Date('2020-01-02'),
},
];
createWrapper(deployFreezes);
await waitForPromises();
alert = wrapper.findComponent(GlAlert);
});
it('shows an alert', () => {
expect(alert.exists()).toBe(true);
});
it('shows the start time of the most recent freeze period', () => {
expect(alert.text()).toContain(`from ${formatDate(deployFreezes[1].startTime)}`);
});
it('shows the end time of the most recent freeze period', () => {
expect(alert.text()).toContain(`to ${formatDate(deployFreezes[1].endTime)}`);
});
it('shows a link to the docs', () => {
const link = alert.findComponent(GlLink);
expect(link.attributes('href')).toBe(
'/help/user/project/releases/index#prevent-unintentional-releases-by-setting-a-deploy-freeze',
);
expect(link.text()).toBe('deploy freeze documentation');
});
});
describe('without deploy freezes', () => {
let deployFreezes;
let alert;
beforeEach(async () => {
deployFreezes = [];
createWrapper(deployFreezes);
await waitForPromises();
alert = wrapper.findComponent(GlAlert);
});
it('does not show an alert', () => {
expect(alert.exists()).toBe(false);
});
});
});
......@@ -5,6 +5,7 @@ import DeleteEnvironmentModal from '~/environments/components/delete_environment
import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
import { createEnvironment } from './mock_data';
describe('Environments detail header component', () => {
......@@ -27,6 +28,7 @@ describe('Environments detail header component', () => {
const findDestroyButton = () => wrapper.findByTestId('destroy-button');
const findStopEnvironmentModal = () => wrapper.findComponent(StopEnvironmentModal);
const findDeleteEnvironmentModal = () => wrapper.findComponent(DeleteEnvironmentModal);
const findDeployFreezeAlert = () => wrapper.findComponent(DeployFreezeAlert);
const buttons = [
['Cancel Auto Stop At', findCancelAutoStopAtButton],
......@@ -44,6 +46,9 @@ describe('Environments detail header component', () => {
GlSprintf,
TimeAgo,
},
provide: {
glFeatures,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
......@@ -54,9 +59,6 @@ describe('Environments detail header component', () => {
canDestroyEnvironment: false,
...props,
},
provide: {
glFeatures,
},
});
};
......@@ -262,4 +264,13 @@ describe('Environments detail header component', () => {
expect(findDeleteEnvironmentModal().exists()).toBe(true);
});
});
describe('deploy freeze alert', () => {
it('passes the environment name to the alert', () => {
const environment = createEnvironment();
createWrapper({ props: { environment } });
expect(findDeployFreezeAlert().props('name')).toBe(environment.name);
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment