Commit 23230163 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'afontaine/graphql-environment-rollback' into 'master'

Add GraphQL to the Environment Rollback Components

See merge request gitlab-org/gitlab!75560
parents 20c148a7 d99aa3c8
...@@ -7,6 +7,7 @@ import { escape } from 'lodash'; ...@@ -7,6 +7,7 @@ import { escape } from 'lodash';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
import rollbackEnvironment from '../graphql/mutations/rollback_environment.mutation.graphql';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
...@@ -40,10 +41,15 @@ export default { ...@@ -40,10 +41,15 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
graphql: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
modalTitle() { modalTitle() {
const title = this.environment.isLastDeployment const title = this.isLastDeployment
? s__('Environments|Re-deploy environment %{name}?') ? s__('Environments|Re-deploy environment %{name}?')
: s__('Environments|Rollback environment %{name}?'); : s__('Environments|Rollback environment %{name}?');
...@@ -53,6 +59,11 @@ export default { ...@@ -53,6 +59,11 @@ export default {
}, },
commitShortSha() { commitShortSha() {
if (this.hasMultipleCommits) { if (this.hasMultipleCommits) {
if (this.graphql) {
const { lastDeployment } = this.environment;
return this.commitData(lastDeployment, 'shortId');
}
const { last_deployment } = this.environment; const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'short_id'); return this.commitData(last_deployment, 'short_id');
} }
...@@ -61,6 +72,11 @@ export default { ...@@ -61,6 +72,11 @@ export default {
}, },
commitUrl() { commitUrl() {
if (this.hasMultipleCommits) { if (this.hasMultipleCommits) {
if (this.graphql) {
const { lastDeployment } = this.environment;
return this.commitData(lastDeployment, 'commitPath');
}
const { last_deployment } = this.environment; const { last_deployment } = this.environment;
return this.commitData(last_deployment, 'commit_path'); return this.commitData(last_deployment, 'commit_path');
} }
...@@ -68,9 +84,7 @@ export default { ...@@ -68,9 +84,7 @@ export default {
return this.environment.commitUrl; return this.environment.commitUrl;
}, },
modalActionText() { modalActionText() {
return this.environment.isLastDeployment return this.isLastDeployment ? s__('Environments|Re-deploy') : s__('Environments|Rollback');
? s__('Environments|Re-deploy')
: s__('Environments|Rollback');
}, },
primaryProps() { primaryProps() {
let attributes = [{ variant: 'danger' }]; let attributes = [{ variant: 'danger' }];
...@@ -84,20 +98,27 @@ export default { ...@@ -84,20 +98,27 @@ export default {
attributes, attributes,
}; };
}, },
isLastDeployment() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return this.environment?.isLastDeployment || this.environment?.lastDeployment?.['last?'];
},
}, },
methods: { methods: {
handleChange(event) { handleChange(event) {
this.$emit('change', event); this.$emit('change', event);
}, },
onOk() { onOk() {
if (this.graphql) {
this.$apollo.mutate({
mutation: rollbackEnvironment,
variables: { environment: this.environment },
});
} else {
eventHub.$emit('rollbackEnvironment', this.environment); eventHub.$emit('rollbackEnvironment', this.environment);
}
}, },
commitData(lastDeployment, key) { commitData(lastDeployment, key) {
if (lastDeployment && lastDeployment.commit) { return lastDeployment?.commit?.[key] ?? '';
return lastDeployment.commit[key];
}
return '';
}, },
}, },
csrf, csrf,
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import { GlModalDirective, GlDropdownItem } from '@gitlab/ui'; import { GlModalDirective, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import setEnvironmentToRollback from '../graphql/mutations/set_environment_to_rollback.mutation.graphql';
export default { export default {
components: { components: {
...@@ -32,11 +33,12 @@ export default { ...@@ -32,11 +33,12 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
graphql: {
type: Boolean,
required: false,
default: false,
}, },
data() {
return {
isLoading: false,
};
}, },
computed: { computed: {
...@@ -49,16 +51,18 @@ export default { ...@@ -49,16 +51,18 @@ export default {
methods: { methods: {
onClick() { onClick() {
if (this.graphql) {
this.$apollo.mutate({
mutation: setEnvironmentToRollback,
variables: { environment: this.environment },
});
} else {
eventHub.$emit('requestRollbackEnvironment', { eventHub.$emit('requestRollbackEnvironment', {
...this.environment, ...this.environment,
retryUrl: this.retryUrl, retryUrl: this.retryUrl,
isLastDeployment: this.isLastDeployment, isLastDeployment: this.isLastDeployment,
}); });
eventHub.$on('rollbackEnvironment', (environment) => {
if (environment.id === this.environment.id) {
this.isLoading = true;
} }
});
}, },
}, },
}; };
......
mutation SetEnvironmentToRollback($environment: Environment) {
setEnvironmentToRollback(environment: $environment) @client
}
query environmentToRollback {
environmentToRollback @client {
id
name
lastDeployment
}
}
...@@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import pollIntervalQuery from './queries/poll_interval.query.graphql'; import pollIntervalQuery from './queries/poll_interval.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
const buildErrors = (errors = []) => ({ const buildErrors = (errors = []) => ({
errors, errors,
...@@ -84,6 +85,12 @@ export const resolvers = (endpoint) => ({ ...@@ -84,6 +85,12 @@ export const resolvers = (endpoint) => ({
]); ]);
}); });
}, },
setEnvironmentToRollback(_, { environment }, { client }) {
client.writeQuery({
query: environmentToRollbackQuery,
data: { environmentToRollback: environment },
});
},
cancelAutoStop(_, { environment: { autoStopPath } }) { cancelAutoStop(_, { environment: { autoStopPath } }) {
return axios return axios
.post(autoStopPath) .post(autoStopPath)
......
...@@ -58,6 +58,7 @@ type LocalErrors { ...@@ -58,6 +58,7 @@ type LocalErrors {
extend type Query { extend type Query {
environmentApp: LocalEnvironmentApp environmentApp: LocalEnvironmentApp
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
environmentToRollback: LocalEnvironment
isLastDeployment: Boolean isLastDeployment: Boolean
} }
...@@ -66,4 +67,5 @@ extend type Mutation { ...@@ -66,4 +67,5 @@ extend type Mutation {
deleteEnvironment(environment: LocalEnvironmentInput): LocalErrors deleteEnvironment(environment: LocalEnvironmentInput): LocalErrors
rollbackEnvironment(environment: LocalEnvironmentInput): LocalErrors rollbackEnvironment(environment: LocalEnvironmentInput): LocalErrors
cancelAutoStop(environment: LocalEnvironmentInput): LocalErrors cancelAutoStop(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
} }
import { GlModal, GlSprintf } from '@gitlab/ui'; import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue'; import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import eventHub from '~/environments/event_hub'; import eventHub from '~/environments/event_hub';
describe('Confirm Rollback Modal Component', () => { describe('Confirm Rollback Modal Component', () => {
...@@ -17,6 +20,17 @@ describe('Confirm Rollback Modal Component', () => { ...@@ -17,6 +20,17 @@ describe('Confirm Rollback Modal Component', () => {
modalId: 'test', modalId: 'test',
}; };
const envWithLastDeploymentGraphql = {
name: 'test',
lastDeployment: {
commit: {
shortId: 'abc0123',
},
'last?': true,
},
modalId: 'test',
};
const envWithoutLastDeployment = { const envWithoutLastDeployment = {
name: 'test', name: 'test',
modalId: 'test', modalId: 'test',
...@@ -26,7 +40,7 @@ describe('Confirm Rollback Modal Component', () => { ...@@ -26,7 +40,7 @@ describe('Confirm Rollback Modal Component', () => {
const retryPath = 'test/-/jobs/123/retry'; const retryPath = 'test/-/jobs/123/retry';
const createComponent = (props = {}) => { const createComponent = (props = {}, options = {}) => {
component = shallowMount(ConfirmRollbackModal, { component = shallowMount(ConfirmRollbackModal, {
propsData: { propsData: {
...props, ...props,
...@@ -34,6 +48,7 @@ describe('Confirm Rollback Modal Component', () => { ...@@ -34,6 +48,7 @@ describe('Confirm Rollback Modal Component', () => {
stubs: { stubs: {
GlSprintf, GlSprintf,
}, },
...options,
}); });
}; };
...@@ -101,4 +116,121 @@ describe('Confirm Rollback Modal Component', () => { ...@@ -101,4 +116,121 @@ describe('Confirm Rollback Modal Component', () => {
}); });
}, },
); );
describe('graphql', () => {
describe.each`
hasMultipleCommits | environmentData | retryUrl | primaryPropsAttrs
${true} | ${envWithLastDeploymentGraphql} | ${null} | ${[{ variant: 'danger' }]}
${false} | ${envWithoutLastDeployment} | ${retryPath} | ${[{ variant: 'danger' }, { 'data-method': 'post' }, { href: retryPath }]}
`(
'when hasMultipleCommits=$hasMultipleCommits',
({ hasMultipleCommits, environmentData, retryUrl, primaryPropsAttrs }) => {
Vue.use(VueApollo);
let apolloProvider;
let rollbackResolver;
beforeEach(() => {
rollbackResolver = jest.fn();
apolloProvider = createMockApollo([], {
Mutation: { rollbackEnvironment: rollbackResolver },
});
environment = environmentData;
});
it('should set contain the commit hash and ask for confirmation', () => {
createComponent(
{
environment: {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': false,
},
},
hasMultipleCommits,
retryUrl,
graphql: true,
},
{ apolloProvider },
);
const modal = component.find(GlModal);
expect(modal.text()).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
});
it('should show "Rollback" when isLastDeployment is false', () => {
createComponent(
{
environment: {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': false,
},
},
hasMultipleCommits,
retryUrl,
graphql: true,
},
{ apolloProvider },
);
const modal = component.find(GlModal);
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
expect(modal.props('actionPrimary').text).toBe('Rollback');
expect(modal.props('actionPrimary').attributes).toEqual(primaryPropsAttrs);
});
it('should show "Re-deploy" when isLastDeployment is true', () => {
createComponent(
{
environment: {
...environment,
lastDeployment: {
...environment.lastDeployment,
'last?': true,
},
},
hasMultipleCommits,
graphql: true,
},
{ apolloProvider },
);
const modal = component.find(GlModal);
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
expect(modal.props('actionPrimary').text).toBe('Re-deploy');
});
it('should commit the "rollback" mutation when "ok" is clicked', async () => {
const env = { ...environmentData, isLastDeployment: true };
createComponent(
{
environment: env,
hasMultipleCommits,
graphql: true,
},
{ apolloProvider },
);
const modal = component.find(GlModal);
modal.vm.$emit('ok');
await nextTick();
expect(rollbackResolver).toHaveBeenCalledWith(
expect.anything(),
{ environment: env },
expect.anything(),
expect.anything(),
);
});
},
);
});
}); });
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui'; import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue'; import RollbackComponent from '~/environments/components/environment_rollback.vue';
import eventHub from '~/environments/event_hub'; import eventHub from '~/environments/event_hub';
import setEnvironmentToRollback from '~/environments/graphql/mutations/set_environment_to_rollback.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
describe('Rollback Component', () => { describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry'; const retryUrl = 'https://gitlab.com/retry';
...@@ -50,4 +54,29 @@ describe('Rollback Component', () => { ...@@ -50,4 +54,29 @@ describe('Rollback Component', () => {
name: 'test', name: 'test',
}); });
}); });
it('should trigger a graphql mutation when graphql is enabled', () => {
Vue.use(VueApollo);
const apolloProvider = createMockApollo();
jest.spyOn(apolloProvider.defaultClient, 'mutate');
const environment = {
name: 'test',
};
const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
graphql: true,
environment,
},
apolloProvider,
});
const button = wrapper.find(GlDropdownItem);
button.vm.$emit('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToRollback,
variables: { environment },
});
});
}); });
...@@ -469,6 +469,33 @@ export const folder = { ...@@ -469,6 +469,33 @@ export const folder = {
stopped_count: 0, stopped_count: 0,
}; };
export const resolvedEnvironment = {
id: 41,
globalId: 'gid://gitlab/Environment/41',
name: 'review/hello',
state: 'available',
externalUrl: 'https://example.org',
environmentType: 'review',
nameWithoutType: 'hello',
lastDeployment: null,
hasStopAction: false,
rolloutStatus: null,
environmentPath: '/h5bp/html5-boilerplate/-/environments/41',
stopPath: '/h5bp/html5-boilerplate/-/environments/41/stop',
cancelAutoStopPath: '/h5bp/html5-boilerplate/-/environments/41/cancel_auto_stop',
deletePath: '/api/v4/projects/8/environments/41',
folderPath: '/h5bp/html5-boilerplate/-/environments/folders/review',
createdAt: '2021-10-04T19:27:00.527Z',
updatedAt: '2021-10-04T19:27:00.527Z',
canStop: true,
logsPath: '/h5bp/html5-boilerplate/-/logs?environment_name=review%2Fhello',
logsApiPath: '/h5bp/html5-boilerplate/-/logs/k8s.json?environment_name=review%2Fhello',
enableAdvancedLogsQuerying: false,
canDelete: false,
hasOpenedAlert: false,
__typename: 'LocalEnvironment',
};
export const resolvedFolder = { export const resolvedFolder = {
availableCount: 2, availableCount: 2,
environments: [ environments: [
......
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers'; import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data'; import {
environmentsApp,
resolvedEnvironmentsApp,
resolvedEnvironment,
folder,
resolvedFolder,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`; const ENDPOINT = `${TEST_HOST}/environments`;
describe('~/frontend/environments/graphql/resolvers', () => { describe('~/frontend/environments/graphql/resolvers', () => {
let mockResolvers; let mockResolvers;
let mock; let mock;
let mockApollo;
let localState;
beforeEach(() => { beforeEach(() => {
mockResolvers = resolvers(ENDPOINT); mockResolvers = resolvers(ENDPOINT);
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockApollo = createMockApollo();
localState = mockApollo.defaultClient.localState;
}); });
afterEach(() => { afterEach(() => {
...@@ -108,4 +120,19 @@ describe('~/frontend/environments/graphql/resolvers', () => { ...@@ -108,4 +120,19 @@ describe('~/frontend/environments/graphql/resolvers', () => {
); );
}); });
}); });
describe('setEnvironmentToRollback', () => {
it('should write the given environment to the cache', () => {
localState.client.writeQuery = jest.fn();
mockResolvers.Mutation.setEnvironmentToRollback(
null,
{ environment: resolvedEnvironment },
localState,
);
expect(localState.client.writeQuery).toHaveBeenCalledWith({
query: environmentToRollback,
data: { environmentToRollback: resolvedEnvironment },
});
});
});
}); });
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