Commit bf4e0ec0 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'afontaine/new-environment-item' into 'master'

New Environment Item

See merge request gitlab-org/gitlab!74418
parents 4a13abe6 83d26d45
......@@ -99,8 +99,7 @@ export default {
};
},
isLastDeployment() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return this.environment?.isLastDeployment || this.environment?.lastDeployment?.['last?'];
return this.environment?.isLastDeployment || this.environment?.lastDeployment?.isLast;
},
},
methods: {
......
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
import actionMutation from '../graphql/mutations/action.mutation.graphql';
export default {
directives: {
......@@ -12,7 +13,6 @@ export default {
GlDropdown,
GlDropdownItem,
GlIcon,
GlLoadingIcon,
},
props: {
actions: {
......@@ -20,6 +20,11 @@ export default {
required: false,
default: () => [],
},
graphql: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -49,7 +54,11 @@ export default {
this.isLoading = true;
eventHub.$emit('postAction', { endpoint: action.playPath });
if (this.graphql) {
this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
} else {
eventHub.$emit('postAction', { endpoint: action.playPath });
}
},
isActionDisabled(action) {
......@@ -70,18 +79,16 @@ export default {
<template>
<gl-dropdown
v-gl-tooltip
:text="title"
:title="title"
:loading="isLoading"
:aria-label="title"
:disabled="isLoading"
icon="play"
text-sr-only
right
data-container="body"
data-testid="environment-actions-button"
>
<template #button-content>
<gl-icon name="play" />
<gl-icon name="chevron-down" />
<gl-loading-icon v-if="isLoading" size="sm" />
</template>
<gl-dropdown-item
v-for="(action, i) in actions"
:key="i"
......
......@@ -2,9 +2,11 @@
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import folderQuery from '../graphql/queries/folder.query.graphql';
import EnvironmentItem from './new_environment_item.vue';
export default {
components: {
EnvironmentItem,
GlButton,
GlCollapse,
GlIcon,
......@@ -51,16 +53,25 @@ export default {
folderPath() {
return this.nestedEnvironment.latest.folderPath;
},
environments() {
return this.folder?.environments;
},
},
methods: {
toggleCollapse() {
this.visible = !this.visible;
},
isFirstEnvironment(index) {
return index === 0;
},
},
};
</script>
<template>
<div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
<div
:class="{ 'gl-pb-5': !visible }"
class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3"
>
<div class="gl-w-full gl-display-flex gl-align-items-center">
<gl-button
class="gl-mr-4 gl-fill-current-color gl-text-gray-500"
......@@ -77,6 +88,15 @@ export default {
<gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
<gl-link v-if="visible" :href="folderPath">{{ $options.i18n.link }}</gl-link>
</div>
<gl-collapse :visible="visible" />
<gl-collapse :visible="visible">
<environment-item
v-for="(environment, index) in environments"
:key="environment.name"
:environment="environment"
:class="{ 'gl-mt-5': isFirstEnvironment(index) }"
class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pl-7 gl-pt-3"
in-folder
/>
</gl-collapse>
</div>
</template>
<script>
import {
GlCollapse,
GlDropdown,
GlButton,
GlLink,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { __ } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
import Rollback from './environment_rollback.vue';
import Pin from './environment_pin.vue';
import Monitoring from './environment_monitoring.vue';
import Terminal from './environment_terminal_button.vue';
import Delete from './environment_delete.vue';
export default {
components: {
GlCollapse,
GlDropdown,
GlButton,
GlLink,
Actions,
ExternalUrl,
StopComponent,
Rollback,
Monitoring,
Pin,
Terminal,
Delete,
},
directives: {
GlTooltip,
},
props: {
environment: {
required: true,
type: Object,
},
inFolder: {
required: false,
default: false,
type: Boolean,
},
},
apollo: {
isLastDeployment: {
query: isLastDeployment,
variables() {
return { environment: this.environment };
},
},
},
i18n: {
collapse: __('Collapse'),
expand: __('Expand'),
},
data() {
return { visible: false };
},
computed: {
icon() {
return this.visible ? 'angle-down' : 'angle-right';
},
externalUrl() {
return this.environment.externalUrl;
},
name() {
return this.inFolder ? this.environment.nameWithoutType : this.environment.name;
},
label() {
return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
actions() {
if (!this.environment?.lastDeployment) {
return [];
}
const { manualActions = [], scheduledActions = [] } = this.environment.lastDeployment;
const combinedActions = [...manualActions, ...scheduledActions];
return combinedActions.map((action) => ({
...action,
}));
},
canStop() {
return this.environment?.canStop;
},
retryPath() {
return this.environment?.lastDeployment?.deployable?.retryPath;
},
hasExtraActions() {
return Boolean(
this.retryPath ||
this.canShowAutoStopDate ||
this.metricsPath ||
this.terminalPath ||
this.canDeleteEnvironment,
);
},
canShowAutoStopDate() {
if (!this.environment?.autoStopAt) {
return false;
}
const autoStopDate = new Date(this.environment?.autoStopAt);
const now = new Date();
return now < autoStopDate;
},
autoStopPath() {
return this.environment?.cancelAutoStopPath ?? '';
},
metricsPath() {
return this.environment?.metricsPath ?? '';
},
terminalPath() {
return this.environment?.terminalPath ?? '';
},
canDeleteEnvironment() {
return Boolean(this.environment?.canDelete && this.environment?.deletePath);
},
displayName() {
return truncate(this.name, 80);
},
},
methods: {
toggleCollapse() {
this.visible = !this.visible;
},
},
};
</script>
<template>
<div>
<div
class="gl-px-3 gl-pt-3 gl-pb-5 gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<div class="gl-min-w-0 gl-mr-4 gl-display-flex gl-align-items-center">
<gl-button
class="gl-mr-4 gl-min-w-fit-content"
:icon="icon"
:aria-label="label"
size="small"
category="tertiary"
@click="toggleCollapse"
/>
<gl-link
v-gl-tooltip
:href="environment.environmentPath"
class="gl-text-blue-500 gl-text-truncate"
:class="{ 'gl-font-weight-bold': visible }"
:title="name"
>
{{ displayName }}
</gl-link>
</div>
<div>
<div class="btn-group table-action-buttons" role="group">
<external-url
v-if="externalUrl"
:external-url="externalUrl"
data-track-action="click_button"
data-track-label="environment_url"
/>
<actions
v-if="actions.length > 0"
:actions="actions"
data-track-action="click_dropdown"
data-track-label="environment_actions"
graphql
/>
<stop-component
v-if="canStop"
:environment="environment"
class="gl-z-index-2"
data-track-action="click_button"
data-track-label="environment_stop"
graphql
/>
<gl-dropdown
v-if="hasExtraActions"
icon="ellipsis_v"
text-sr-only
:text="__('More actions')"
category="secondary"
no-caret
right
>
<rollback
v-if="retryPath"
:environment="environment"
:is-last-deployment="isLastDeployment"
:retry-url="retryPath"
graphql
data-track-action="click_button"
data-track-label="environment_rollback"
/>
<pin
v-if="canShowAutoStopDate"
:auto-stop-url="autoStopPath"
data-track-action="click_button"
data-track-label="environment_pin"
/>
<monitoring
v-if="metricsPath"
:monitoring-url="metricsPath"
data-track-action="click_button"
data-track-label="environment_monitoring"
/>
<terminal
v-if="terminalPath"
:terminal-path="terminalPath"
data-track-action="click_button"
data-track-label="environment_terminal"
/>
<delete
v-if="canDeleteEnvironment"
:environment="environment"
data-track-action="click_button"
data-track-label="environment_delete"
graphql
/>
</gl-dropdown>
</div>
</div>
</div>
<gl-collapse :visible="visible" />
</div>
</template>
......@@ -5,20 +5,28 @@ import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_util
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import EnvironmentItem from './new_environment_item.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
export default {
components: {
DeleteEnvironmentModal,
ConfirmRollbackModal,
EnvironmentFolder,
EnableReviewAppModal,
EnvironmentItem,
StopEnvironmentModal,
GlBadge,
GlPagination,
GlTab,
GlTabs,
StopEnvironmentModal,
},
apollo: {
environmentApp: {
......@@ -39,6 +47,12 @@ export default {
pageInfo: {
query: pageInfoQuery,
},
environmentToDelete: {
query: environmentToDeleteQuery,
},
environmentToRollback: {
query: environmentToRollbackQuery,
},
environmentToStop: {
query: environmentToStopQuery,
},
......@@ -63,6 +77,8 @@ export default {
isReviewAppModalVisible: false,
page: parseInt(page, 10),
scope,
environmentToDelete: {},
environmentToRollback: {},
environmentToStop: {},
};
},
......@@ -71,7 +87,10 @@ export default {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
folders() {
return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
},
environments() {
return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
},
availableCount() {
return this.environmentApp?.availableCount;
......@@ -164,7 +183,9 @@ export default {
:modal-id="$options.modalId"
data-testid="enable-review-app-modal"
/>
<delete-environment-modal :environment="environmentToDelete" graphql />
<stop-environment-modal :environment="environmentToStop" graphql />
<confirm-rollback-modal :environment="environmentToRollback" graphql />
<gl-tabs
:action-secondary="addEnvironment"
:action-primary="openReviewAppModal"
......@@ -195,6 +216,12 @@ export default {
class="gl-mb-3"
:nested-environment="folder"
/>
<environment-item
v-for="environment in environments"
:key="environment.name"
class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
:environment="environment.latest"
/>
<gl-pagination
align="center"
:total-items="totalItems"
......
mutation action($action: LocalAction) {
action(action: $action) @client {
errors
}
}
query isLastDeployment($environment: LocalEnvironment) {
isLastDeployment(environment: $environment) @client
}
......@@ -66,8 +66,7 @@ export const resolvers = (endpoint) => ({
}));
},
isLastDeployment(_, { environment }) {
// eslint-disable-next-line @gitlab/require-i18n-strings
return environment?.lastDeployment?.['last?'];
return environment?.lastDeployment?.isLast;
},
},
Mutation: {
......@@ -115,6 +114,14 @@ export const resolvers = (endpoint) => ({
data: { environmentToStop: environment },
});
},
action(_, { action: { playPath } }) {
return axios
.post(playPath)
.then(() => buildErrors())
.catch(() =>
buildErrors([s__('Environments|An error occurred while making the request.')]),
);
},
setEnvironmentToDelete(_, { environment }, { client }) {
client.writeQuery({
query: environmentToDeleteQuery,
......
......@@ -70,7 +70,7 @@ extend type Query {
environmentToRollback: LocalEnvironment
environmentToStop: LocalEnvironment
isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean
isLastDeployment: Boolean
isLastDeployment(environment: LocalEnvironmentInput): Boolean
}
extend type Mutation {
......@@ -81,4 +81,5 @@ extend type Mutation {
setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
action(environment: LocalEnvironmentInput): LocalErrors
}
......@@ -26,7 +26,7 @@ describe('Confirm Rollback Modal Component', () => {
commit: {
shortId: 'abc0123',
},
'last?': true,
isLast: true,
},
modalId: 'test',
};
......@@ -145,7 +145,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': false,
isLast: false,
},
},
hasMultipleCommits,
......@@ -167,7 +167,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': false,
isLast: false,
},
},
hasMultipleCommits,
......@@ -191,7 +191,7 @@ describe('Confirm Rollback Modal Component', () => {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': true,
isLast: true,
},
},
hasMultipleCommits,
......
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { TEST_HOST } from 'helpers/test_constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
import eventHub from '~/environments/event_hub';
import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
const scheduledJobAction = {
name: 'scheduled action',
......@@ -25,12 +29,13 @@ describe('EnvironmentActions Component', () => {
const findEnvironmentActionsButton = () =>
wrapper.find('[data-testid="environment-actions-button"]');
function createComponent(props, { mountFn = shallowMount } = {}) {
function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
wrapper = mountFn(EnvironmentActions, {
propsData: { actions: [], ...props },
directives: {
GlTooltip: createMockDirective(),
},
...options,
});
}
......@@ -150,4 +155,32 @@ describe('EnvironmentActions Component', () => {
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
describe('graphql', () => {
Vue.use(VueApollo);
const action = {
name: 'bar',
play_path: 'https://gitlab.com/play',
};
let mockApollo;
beforeEach(() => {
mockApollo = createMockApollo();
createComponent(
{ actions: [action], graphql: true },
{ options: { apolloProvider: mockApollo } },
);
});
it('should trigger a graphql mutation on click', () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
findDropdownItem(action).vm.$emit('click');
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
mutation: actionMutation,
variables: { action },
});
});
});
});
......@@ -477,7 +477,141 @@ export const resolvedEnvironment = {
externalUrl: 'https://example.org',
environmentType: 'review',
nameWithoutType: 'hello',
lastDeployment: null,
lastDeployment: {
id: 78,
iid: 24,
sha: 'f3ba6dd84f8f891373e9b869135622b954852db1',
ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' },
status: 'success',
createdAt: '2022-01-07T15:47:27.415Z',
deployedAt: '2022-01-07T15:47:32.450Z',
tag: false,
isLast: true,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
webUrl: 'http://gck.test:3000/root',
showStatus: false,
path: '/root',
},
deployable: {
id: 1014,
name: 'deploy-prod',
started: '2022-01-07T15:47:31.037Z',
complete: true,
archived: false,
buildPath: '/h5bp/html5-boilerplate/-/jobs/1014',
retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
playable: false,
scheduled: false,
createdAt: '2022-01-07T15:47:27.404Z',
updatedAt: '2022-01-07T15:47:32.341Z',
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
hasDetails: true,
detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014',
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_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
action: {
icon: 'retry',
title: 'Retry',
path: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
method: 'post',
buttonTitle: 'Retry this job',
},
},
},
commit: {
id: 'f3ba6dd84f8f891373e9b869135622b954852db1',
shortId: 'f3ba6dd8',
createdAt: '2022-01-07T15:47:26.000+00:00',
parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'],
title: 'Update .gitlab-ci.yml file',
message: 'Update .gitlab-ci.yml file',
authorName: 'Administrator',
authorEmail: 'admin@example.com',
authoredDate: '2022-01-07T15:47:26.000+00:00',
committerName: 'Administrator',
committerEmail: 'admin@example.com',
committedDate: '2022-01-07T15:47:26.000+00:00',
trailers: {},
webUrl:
'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
author: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
webUrl: 'http://gck.test:3000/root',
showStatus: false,
path: '/root',
},
authorGravatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
commitUrl:
'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
},
manualActions: [
{
id: 1015,
name: 'deploy-staging',
started: null,
complete: false,
archived: false,
buildPath: '/h5bp/html5-boilerplate/-/jobs/1015',
playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play',
playable: true,
scheduled: false,
createdAt: '2022-01-07T15:47:27.422Z',
updatedAt: '2022-01-07T15:47:28.557Z',
status: {
icon: 'status_manual',
text: 'manual',
label: 'manual play action',
group: 'manual',
tooltip: 'manual action',
hasDetails: true,
detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015',
illustration: {
image:
'/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
size: 'svg-394',
title: 'This job requires a manual action',
content:
'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
},
favicon:
'/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
action: {
icon: 'play',
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
method: 'post',
buttonTitle: 'Trigger this manual action',
},
},
},
],
scheduledActions: [],
cluster: null,
},
hasStopAction: false,
rolloutStatus: null,
environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
......
import MockAdapter from 'axios-mock-adapter';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
......@@ -226,4 +227,21 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
});
describe('action', () => {
it('should POST to the given path', async () => {
mock.onPost(ENDPOINT).reply(200);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
});
it('should return a nice error message on fail', async () => {
mock.onPost(ENDPOINT).reply(500);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({
__typename: 'LocalEnvironmentErrors',
errors: [s__('Environments|An error occurred while making the request.')],
});
});
});
});
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
import { __, s__ } from '~/locale';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
Vue.use(VueApollo);
......@@ -25,13 +28,20 @@ describe('~/environments/components/new_environments_folder.vue', () => {
};
const createWrapper = (propsData, apolloProvider) =>
mountExtended(EnvironmentsFolder, { apolloProvider, propsData });
mountExtended(EnvironmentsFolder, {
apolloProvider,
propsData,
stubs: { transition: stubTransition() },
});
beforeEach(() => {
beforeEach(async () => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
wrapper = createWrapper({ nestedEnvironment }, createApolloProvider());
await nextTick();
await waitForPromises();
folderName = wrapper.findByText(nestedEnvironment.name);
button = wrapper.findByRole('button', { name: __('Expand') });
});
......@@ -57,7 +67,8 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(collapse.attributes('visible')).toBeUndefined();
expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-right', 'folder-o']);
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['angle-right', 'folder-o']);
expect(folderName.classes('gl-font-weight-bold')).toBe(false);
expect(link.exists()).toBe(false);
});
......@@ -68,10 +79,21 @@ describe('~/environments/components/new_environments_folder.vue', () => {
const link = findLink();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('true');
expect(icons.wrappers.map((i) => i.props('name'))).toEqual(['angle-down', 'folder-open']);
expect(collapse.attributes('visible')).toBe('visible');
const iconNames = icons.wrappers.map((i) => i.props('name')).slice(0, 2);
expect(iconNames).toEqual(['angle-down', 'folder-open']);
expect(folderName.classes('gl-font-weight-bold')).toBe(true);
expect(link.attributes('href')).toBe(nestedEnvironment.latest.folderPath);
});
it('displays all environments when opened', async () => {
await button.trigger('click');
const names = resolvedFolder.environments.map((e) =>
expect.stringMatching(e.nameWithoutType),
);
const environments = wrapper.findAllComponents(EnvironmentItem).wrappers.map((w) => w.text());
expect(environments).toEqual(expect.arrayContaining(names));
});
});
});
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { GlCollapse, GlIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubTransition } from 'helpers/stub_transition';
import { __, s__ } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import { resolvedEnvironment } from './graphql/mock_data';
Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
const createApolloProvider = () => {
return createMockApollo();
};
const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
stubs: { transition: stubTransition() },
});
afterEach(() => {
wrapper?.destroy();
});
it('displays the name when not in a folder', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const name = wrapper.findByRole('link', { name: resolvedEnvironment.name });
expect(name.exists()).toBe(true);
});
it('displays the name minus the folder prefix when in a folder', () => {
wrapper = createWrapper({
propsData: { inFolder: true },
apolloProvider: createApolloProvider(),
});
const name = wrapper.findByRole('link', { name: resolvedEnvironment.nameWithoutType });
expect(name.exists()).toBe(true);
});
it('truncates the name if it is very long', () => {
const environment = {
...resolvedEnvironment,
name:
'this is a really long name that should be truncated because otherwise it would look strange in the UI',
};
wrapper = createWrapper({ propsData: { environment }, apolloProvider: createApolloProvider() });
const name = wrapper.findByRole('link', {
name: (text) => environment.name.startsWith(text.slice(0, -1)),
});
expect(name.exists()).toBe(true);
expect(name.text()).toHaveLength(80);
});
describe('url', () => {
it('shows a link for the url if one is present', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
expect(url.attributes('href')).toEqual(resolvedEnvironment.externalUrl);
});
it('does not show a link for the url if one is missing', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, externalUrl: '' } },
apolloProvider: createApolloProvider(),
});
const url = wrapper.findByRole('link', { name: s__('Environments|Open live environment') });
expect(url.exists()).toBe(false);
});
});
describe('actions', () => {
it('shows a dropdown if there are actions to perform', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
expect(actions.exists()).toBe(true);
});
it('does not show a dropdown if there are no actions to perform', () => {
wrapper = createWrapper({
propsData: {
environment: {
...resolvedEnvironment,
lastDeployment: null,
},
apolloProvider: createApolloProvider(),
},
});
const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
expect(actions.exists()).toBe(false);
});
it('passes all the actions down to the action component', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
expect(action.exists()).toBe(true);
});
});
describe('stop', () => {
it('shows a buton to stop the environment if the environment is available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
expect(stop.exists()).toBe(true);
});
it('does not show a buton to stop the environment if the environment is stopped', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
apolloProvider: createApolloProvider(),
});
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
expect(stop.exists()).toBe(false);
});
});
describe('rollback', () => {
it('shows the option to rollback/re-deploy if available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', {
name: s__('Environments|Re-deploy to environment'),
});
expect(rollback.exists()).toBe(true);
});
it('does not show the option to rollback/re-deploy if not available', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, lastDeployment: null } },
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', {
name: s__('Environments|Re-deploy to environment'),
});
expect(rollback.exists()).toBe(false);
});
});
describe('pin', () => {
it('shows the option to pin the environment if there is an autostop date', () => {
wrapper = createWrapper({
propsData: {
environment: { ...resolvedEnvironment, autoStopAt: new Date(Date.now() + 100000) },
},
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
expect(rollback.exists()).toBe(true);
});
it('does not show the option to pin the environment if there is no autostop date', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
expect(rollback.exists()).toBe(false);
});
});
describe('monitoring', () => {
it('shows the link to monitoring if metrics are set up', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
expect(rollback.exists()).toBe(true);
});
it('does not show the link to monitoring if metrics are not set up', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', { name: __('Monitoring') });
expect(rollback.exists()).toBe(false);
});
});
describe('terminal', () => {
it('shows the link to the terminal if set up', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, terminalPath: '/terminal' } },
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
expect(rollback.exists()).toBe(true);
});
it('does not show the link to the terminal if not set up', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
expect(rollback.exists()).toBe(false);
});
});
describe('delete', () => {
it('shows the button to delete the environment if possible', () => {
wrapper = createWrapper({
propsData: {
environment: { ...resolvedEnvironment, canDelete: true, deletePath: '/terminal' },
},
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', {
name: s__('Environments|Delete environment'),
});
expect(rollback.exists()).toBe(true);
});
it('does not show the button to delete the environment if not possible', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', {
name: s__('Environments|Delete environment'),
});
expect(rollback.exists()).toBe(false);
});
});
describe('collapse', () => {
let icon;
let collapse;
let button;
let environmentName;
beforeEach(() => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
collapse = wrapper.findComponent(GlCollapse);
icon = wrapper.findComponent(GlIcon);
button = wrapper.findByRole('button', { name: __('Expand') });
environmentName = wrapper.findByText(resolvedEnvironment.name);
});
it('is collapsed by default', () => {
expect(collapse.attributes('visible')).toBeUndefined();
expect(icon.props('name')).toEqual('angle-right');
expect(environmentName.classes('gl-font-weight-bold')).toBe(false);
});
it('opens on click', async () => {
await button.trigger('click');
expect(button.attributes('aria-label')).toBe(__('Collapse'));
expect(collapse.attributes('visible')).toBe('visible');
expect(icon.props('name')).toEqual('angle-down');
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
});
});
});
......@@ -8,6 +8,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { sprintf, __, s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import EnvironmentsItem from '~/environments/components/new_environment_item.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import { resolvedEnvironmentsApp, resolvedFolder, resolvedEnvironment } from './graphql/mock_data';
......@@ -93,6 +94,18 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(text).not.toContainEqual(expect.stringMatching('production'));
});
it('should show all the environments that are fetched', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
const text = wrapper.findAllComponents(EnvironmentsItem).wrappers.map((w) => w.text());
expect(text).not.toContainEqual(expect.stringMatching('review'));
expect(text).toContainEqual(expect.stringMatching('production'));
});
it('should show a button to create a new environment', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
......
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