Commit 256cb958 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 118a9467 645e82d0
......@@ -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
}
......@@ -39,7 +39,7 @@ module HooksHelper
def hook_log_path(hook, hook_log)
case hook
when ProjectHook
when ProjectHook, ServiceHook
hook_log.present.details_path
when SystemHook
admin_hook_hook_log_path(hook, hook_log)
......
......@@ -4,9 +4,14 @@ group: Monitor
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Tracing **(FREE)**
# Tracing (DEPRECATED) **(FREE)**
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) from GitLab Ultimate to GitLab Free in 13.5.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/42645) from GitLab Ultimate to GitLab Free in 13.5.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346540) in GitLab 14.7.
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/346540)
for use in GitLab 14.7, and is planned for removal in GitLab 15.0.
Tracing provides insight into the performance and health of a deployed application, tracking each
function or microservice that handles a given request. Tracing makes it easy to understand the
......
......@@ -5,10 +5,10 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: howto, reference
---
# GitLab and SSH keys **(FREE)**
# Use SSH keys to communicate with GitLab **(FREE)**
Git is a distributed version control system, which means you can work locally,
then share or "push" your changes to a server. In this case, the server is GitLab.
then share or *push* your changes to a server. In this case, the server you push to is GitLab.
GitLab uses the SSH protocol to securely communicate with Git.
When you use SSH keys to authenticate to the GitLab remote server,
......
......@@ -357,6 +357,10 @@ Ensure your SAML identity provider sends an attribute statement named `Groups` o
</saml:AttributeStatement>
```
WARNING:
Setting up Group Sync can disconnect users from SAML IDP if there is any mismatch in the configuration. Ensure the
`Groups` attribute is included in the SAML response, and the **SAML Group Name** matches the `AttributeValue` attribute.
Other attribute names such as `http://schemas.microsoft.com/ws/2008/06/identity/claims/groups`
are not accepted as a source of groups.
See the [SAML troubleshooting page](../../../administration/troubleshooting/group_saml_scim.md)
......
......@@ -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,
......
......@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe HooksHelper do
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook, project: project) }
let(:service_hook) { create(:service_hook, integration: create(:drone_ci_integration)) }
let(:system_hook) { create(:system_hook) }
describe '#link_to_test_hook' do
......@@ -31,6 +32,15 @@ RSpec.describe HooksHelper do
end
end
context 'with a service hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
it 'returns project-namespaced link' do
expect(helper.hook_log_path(project_hook, web_hook_log))
.to eq(web_hook_log.present.details_path)
end
end
context 'with a system hook' do
let(:web_hook_log) { create(:web_hook_log, web_hook: system_hook) }
......
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