Commit d615c310 authored by Nathan Friend's avatar Nathan Friend Committed by Enrique Alcántara

Add upcoming deployment to Environments page

This commit updates the Environments table with a new column: upcoming
deployment.
parent 5a0b7fee
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { isEmpty } from 'lodash';
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
......@@ -30,6 +31,7 @@ export default {
CommitComponent,
ExternalUrlComponent,
GlIcon,
GlLink,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
......@@ -38,6 +40,7 @@ export default {
TerminalButtonComponent,
TooltipOnTruncate,
UserAvatarLink,
CiIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -80,6 +83,24 @@ export default {
return false;
},
/**
* @returns {Object|Undefined} The `upcoming_deployment` object if it exists.
* Otherwise, `undefined`.
*/
upcomingDeployment() {
return this.model?.upcoming_deployment;
},
/**
* @returns {String} Text that will be shown in the tooltip when
* the user hovers over the upcoming deployment's status icon.
*/
upcomingDeploymentTooltipText() {
return sprintf(s__('Environments|Deployment %{status}'), {
status: this.upcomingDeployment.deployable.status.text,
});
},
/**
* Checkes whether the row displayed is a folder.
*
......@@ -234,6 +255,18 @@ export default {
return '';
},
/**
* Same as `userImageAltDescription`, but for the
* upcoming deployment's user
*
* @returns {String}
*/
upcomingDeploymentUserImageAltDescription() {
return sprintf(__("%{username}'s avatar"), {
username: this.upcomingDeployment.user.username,
});
},
/**
* If provided, returns the commit tag.
*
......@@ -381,6 +414,15 @@ export default {
return '';
},
/**
* Same as `deploymentInternalId`, but for the upcoming deployment
*
* @returns {String}
*/
upcomingDeploymentInternalId() {
return `#${this.upcomingDeployment.iid}`;
},
/**
* Verifies if the user object is present under last_deployment object.
*
......@@ -503,6 +545,13 @@ export default {
folderIconName() {
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
},
upcomingDeploymentCellClasses() {
return [
this.tableData.upcoming.spacing,
{ 'gl-display-none gl-display-md-block': !this.upcomingDeployment },
];
},
},
methods: {
......@@ -512,6 +561,19 @@ export default {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
/**
* Returns the field title that will be shown in the field's row
* in the mobile view.
*
* @returns `field.mobileTitle` if present;
* if not, falls back to `field.title`.
*/
getMobileViewTitleForField(fieldName) {
const field = this.tableData[fieldName];
return field.mobileTitle || field.title;
},
},
};
</script>
......@@ -530,7 +592,7 @@ export default {
role="gridcell"
>
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
{{ tableData.name.title }}
{{ getMobileViewTitleForField('name') }}
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
......@@ -609,7 +671,9 @@ export default {
</div>
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
<div role="rowheader" class="table-mobile-header">
{{ getMobileViewTitleForField('commit') }}
</div>
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
......@@ -623,7 +687,9 @@ export default {
</div>
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
<div role="rowheader" class="table-mobile-header">
{{ getMobileViewTitleForField('date') }}
</div>
<span
v-if="canShowDeploymentDate"
v-gl-tooltip
......@@ -636,8 +702,51 @@ export default {
</span>
</div>
<div
v-if="!isFolder"
class="table-section"
:class="upcomingDeploymentCellClasses"
role="gridcell"
data-testid="upcoming-deployment"
>
<div role="rowheader" class="table-mobile-header">
{{ getMobileViewTitleForField('upcoming') }}
</div>
<div
v-if="upcomingDeployment"
class="gl-w-full gl-display-flex gl-flex-direction-row gl-md-flex-direction-column! gl-justify-content-end"
data-testid="upcoming-deployment-content"
>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-mr-2">{{ upcomingDeploymentInternalId }}</span>
<gl-link
v-if="upcomingDeployment.deployable"
v-gl-tooltip
:href="upcomingDeployment.deployable.build_path"
:title="upcomingDeploymentTooltipText"
data-testid="upcoming-deployment-status-link"
>
<ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
</gl-link>
</div>
<div class="gl-display-flex">
<span v-if="upcomingDeployment.user" class="text-break-word">
by
<user-avatar-link
:link-href="upcomingDeployment.user.web_url"
:img-src="upcomingDeployment.user.avatar_url"
:img-alt="upcomingDeploymentUserImageAltDescription"
:tooltip-text="upcomingDeployment.user.username"
/>
</span>
</div>
</div>
</div>
<div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
<div role="rowheader" class="table-mobile-header">
{{ getMobileViewTitleForField('autoStop') }}
</div>
<span
v-if="canShowAutoStopDate"
v-gl-tooltip
......
......@@ -71,7 +71,7 @@ export default {
// percent spacing for cols, should add up to 100
name: {
title: s__('Environments|Environment'),
spacing: 'section-15',
spacing: 'section-10',
},
deploy: {
title: s__('Environments|Deployment'),
......@@ -83,18 +83,23 @@ export default {
},
commit: {
title: s__('Environments|Commit'),
spacing: 'section-20',
spacing: 'section-15',
},
date: {
title: s__('Environments|Updated'),
spacing: 'section-10',
},
upcoming: {
title: s__('Environments|Upcoming'),
mobileTitle: s__('Environments|Upcoming deployment'),
spacing: 'section-10',
},
autoStop: {
title: s__('Environments|Auto stop in'),
spacing: 'section-5',
spacing: 'section-10',
},
actions: {
spacing: 'section-25',
spacing: 'section-20',
},
};
},
......@@ -160,6 +165,9 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }}
</div>
<div class="table-section" :class="tableData.upcoming.spacing" role="columnheader">
{{ tableData.upcoming.title }}
</div>
<div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
{{ tableData.autoStop.title }}
</div>
......
......@@ -129,3 +129,17 @@
content: '';
display: flex;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1085
.gl-md-flex-direction-column {
@media (min-width: $breakpoint-md) {
flex-direction: column;
}
}
// Same as above
.gl-md-flex-direction-column\! {
@media (min-width: $breakpoint-md) {
flex-direction: column !important;
}
}
---
title: Add upcoming deployment column to Environments page
merge_request: 48062
author:
type: added
......@@ -10668,6 +10668,9 @@ msgstr ""
msgid "Environments|Deployment"
msgstr ""
msgid "Environments|Deployment %{status}"
msgstr ""
msgid "Environments|Enable review app"
msgstr ""
......@@ -10785,6 +10788,12 @@ msgstr ""
msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
msgid "Environments|Upcoming"
msgstr ""
msgid "Environments|Upcoming deployment"
msgstr ""
msgid "Environments|Updated"
msgstr ""
......
......@@ -24,6 +24,10 @@ RSpec.describe 'Environments page', :js do
'button[title="Stop environment"]'
end
def upcoming_deployment_content_selector
'[data-testid="upcoming-deployment-content"]'
end
describe 'page tabs' do
it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project)
......@@ -362,6 +366,26 @@ RSpec.describe 'Environments page', :js do
expect(page).to have_content('No deployments yet')
end
end
context 'when there is an upcoming deployment' do
let_it_be(:project) { create(:project, :repository) }
let!(:deployment) do
create(:deployment, :running,
environment: environment,
sha: project.commit.id)
end
it "renders the upcoming deployment", :aggregate_failures do
visit_environments(project)
within(upcoming_deployment_content_selector) do
expect(page).to have_content("##{deployment.iid}")
expect(page).to have_selector("a[href=\"#{project_job_path(project, deployment.deployable)}\"]")
expect(page).to have_link(href: /#{deployment.user.username}/)
end
end
end
end
it 'does have a new environment button' do
......
import { cloneDeep } from 'lodash';
import { mount } from '@vue/test-utils';
import { format } from 'timeago.js';
import EnvironmentItem from '~/environments/components/environment_item.vue';
......@@ -30,6 +31,11 @@ describe('Environment item', () => {
});
const findAutoStop = () => wrapper.find('.js-auto-stop');
const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]');
const findUpcomingDeploymentContent = () =>
wrapper.find('[data-testid="upcoming-deployment-content"]');
const findUpcomingDeploymentStatusLink = () =>
wrapper.find('[data-testid="upcoming-deployment-status-link"]');
afterEach(() => {
wrapper.destroy();
......@@ -87,6 +93,72 @@ describe('Environment item', () => {
});
});
describe('When the envionment has an upcoming deployment', () => {
describe('When the upcoming deployment has a deployable', () => {
it('should render the build ID and user', () => {
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
'#27 by upcoming-username',
);
});
it('should render a status icon with a link and tooltip', () => {
expect(findUpcomingDeploymentStatusLink().exists()).toBe(true);
expect(findUpcomingDeploymentStatusLink().attributes().href).toBe(
'/root/environment-test/-/jobs/892',
);
expect(findUpcomingDeploymentStatusLink().attributes().title).toBe(
'Deployment running',
);
});
});
describe('When the deployment does not have a deployable', () => {
beforeEach(() => {
const environmentWithoutDeployable = cloneDeep(environment);
delete environmentWithoutDeployable.upcoming_deployment.deployable;
factory({
propsData: {
model: environmentWithoutDeployable,
canReadEnvironment: true,
tableData,
},
});
});
it('should still renders the build ID and user', () => {
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
'#27 by upcoming-username',
);
});
it('should not render the status icon', () => {
expect(findUpcomingDeploymentStatusLink().exists()).toBe(false);
});
});
});
describe('Without upcoming deployment', () => {
beforeEach(() => {
const environmentWithoutUpcomingDeployment = cloneDeep(environment);
delete environmentWithoutUpcomingDeployment.upcoming_deployment;
factory({
propsData: {
model: environmentWithoutUpcomingDeployment,
canReadEnvironment: true,
tableData,
},
});
});
it('should not render anything in the upcoming deployment column', () => {
expect(findUpcomingDeploymentContent().exists()).toBe(false);
});
});
describe('Without auto-stop date', () => {
beforeEach(() => {
factory({
......@@ -209,6 +281,10 @@ describe('Environment item', () => {
it('should render the number of children in a badge', () => {
expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size);
});
it('should not render the "Upcoming deployment" column', () => {
expect(findUpcomingDeployment().exists()).toBe(false);
});
});
describe('When environment can be deleted', () => {
......
......@@ -86,6 +86,98 @@ const environment = {
],
deployed_at: '2016-11-29T18:11:58.430Z',
},
upcoming_deployment: {
id: 82,
iid: 27,
sha: '1132df044b73943943c949e7ac2c2f120a89bf59',
ref: {
name: 'master',
ref_path: '/root/environment-test/-/tree/master',
},
status: 'running',
created_at: '2020-12-04T19:57:49.514Z',
deployed_at: null,
tag: false,
'last?': false,
user: {
id: 1,
name: 'Upcoming Name',
username: 'upcoming-username',
state: 'active',
avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png',
web_url: 'http://0.0.0.0:3000/upcoming-username',
show_status: false,
path: '/upcoming-username',
},
deployable: {
id: 1310,
name: 'deploy_to_development',
started: '2020-12-04T19:58:10.806Z',
archived: false,
build_path: '/root/environment-test/-/jobs/892',
cancel_path:
'/root/environment-test/-/jobs/892/cancel?continue%5Bto%5D=%2Froot%2Fenvironment-test%2F-%2Fjobs%2F892',
playable: false,
scheduled: false,
created_at: '2020-12-04T19:57:49.455Z',
updated_at: '2020-12-04T19:58:10.809Z',
status: {
icon: 'status_running',
text: 'running',
label: 'running',
group: 'running',
tooltip: 'running',
has_details: true,
details_path: '/root/environment-test/-/jobs/892',
illustration: {
image:
'/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
size: 'svg-430',
title: 'This job does not have a trace.',
},
favicon:
'/assets/ci_favicons/favicon_status_running-9c635b2419a8e1ec991c993061b89cc5aefc0743bb238ecd0c381e7741a70e8c.png',
action: {
icon: 'cancel',
title: 'Cancel',
path: '/root/environment-test/-/jobs/892/cancel',
method: 'post',
button_title: 'Cancel this job',
},
},
},
commit: {
id: '1132df044b73943943c949e7ac2c2f120a89bf59',
short_id: '1132df04',
created_at: '2020-12-01T15:46:26.000-05:00',
parent_ids: ['e0808dee2a5877563ec140e65d8b41908f90098c'],
title: 'Update .gitlab-ci.yml',
message: 'Update .gitlab-ci.yml',
author_name: 'Upcoming Name',
author_email: 'admin@example.com',
authored_date: '2020-12-01T15:46:26.000-05:00',
committer_name: 'Upcoming Name',
committer_email: 'admin@example.com',
committed_date: '2020-12-01T15:46:26.000-05:00',
web_url:
'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
author: {
id: 1,
name: 'Upcoming Name',
username: 'upcoming-username',
state: 'active',
avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png',
web_url: 'http://0.0.0.0:3000/upcoming-username',
show_status: false,
path: '/upcoming-username',
},
author_gravatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
commit_url:
'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
commit_path: '/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
},
},
has_stop_action: true,
environment_path: 'root/ci-folders/environments/31',
log_path: 'root/ci-folders/environments/31/logs',
......@@ -156,6 +248,11 @@ const tableData = {
title: 'Updated',
spacing: 'section-10',
},
upcoming: {
title: 'Upcoming',
mobileTitle: 'Upcoming deployment',
spacing: 'section-10',
},
autoStop: {
title: 'Auto stop in',
spacing: 'section-5',
......
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