Commit 4b83234b authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'afontaine/new-environments-app-tabs' into 'master'

Add tabs to new environments page

See merge request gitlab-org/gitlab!76160
parents 8626c30d 930b9b15
<script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue';
......@@ -17,6 +17,11 @@ export default {
apollo: {
environmentApp: {
query: environmentAppQuery,
variables() {
return {
scope: this.scope,
};
},
pollInterval() {
return this.interval;
},
......@@ -29,10 +34,13 @@ export default {
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
available: __('Available'),
stopped: __('Stopped'),
},
modalId: 'enable-review-app-info',
data() {
return { interval: undefined, isReviewAppModalVisible: false };
const scope = new URLSearchParams(window.location.search).get('scope') || 'available';
return { interval: undefined, scope, isReviewAppModalVisible: false };
},
computed: {
canSetupReviewApp() {
......@@ -71,11 +79,25 @@ export default {
},
};
},
stoppedCount() {
return this.environmentApp?.stoppedCount;
},
},
methods: {
showReviewAppModal() {
this.isReviewAppModalVisible = true;
},
setScope(scope) {
this.scope = scope;
this.$apollo.queries.environmentApp.stopPolling();
this.$nextTick(() => {
if (this.interval) {
this.$apollo.queries.environmentApp.startPolling(this.interval);
} else {
this.$apollo.queries.environmentApp.refetch({ scope });
}
});
},
},
};
</script>
......@@ -90,22 +112,32 @@ export default {
<gl-tabs
:action-secondary="addEnvironment"
:action-primary="openReviewAppModal"
sync-active-tab-with-query-params
query-param-name="scope"
@primary="showReviewAppModal"
>
<gl-tab>
<gl-tab query-param-value="available" @click="setScope('available')">
<template #title>
<span>{{ __('Available') }}</span>
<span>{{ $options.i18n.available }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
{{ availableCount }}
</gl-badge>
</template>
<environment-folder
v-for="folder in folders"
:key="folder.name"
class="gl-mb-3"
:nested-environment="folder"
/>
</gl-tab>
<gl-tab query-param-value="stopped" @click="setScope('stopped')">
<template #title>
<span>{{ $options.i18n.stopped }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
{{ stoppedCount }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
<environment-folder
v-for="folder in folders"
:key="folder.name"
class="gl-mb-3"
:nested-environment="folder"
/>
</div>
</template>
query getEnvironmentApp {
environmentApp @client {
query getEnvironmentApp($scope: String) {
environmentApp(scope: $scope) @client {
availableCount
stoppedCount
environments
reviewApp
stoppedCount
......
......@@ -19,12 +19,12 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({
Query: {
environmentApp(_context, _variables, { cache }) {
return axios.get(endpoint, { params: { nested: true } }).then((res) => {
environmentApp(_context, { scope }, { cache }) {
return axios.get(endpoint, { params: { nested: true, scope } }).then((res) => {
const interval = res.headers['poll-interval'];
if (interval) {
cache.writeQuery({ query: pollIntervalQuery, data: { interval } });
cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(interval) } });
} else {
cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } });
}
......
......@@ -23,9 +23,10 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('environmentApp', () => {
it('should fetch environments and map them to frontend data', async () => {
const cache = { writeQuery: jest.fn() };
mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp, {});
const scope = 'available';
mock.onGet(ENDPOINT, { params: { nested: true, scope } }).reply(200, environmentsApp, {});
const app = await mockResolvers.Query.environmentApp(null, null, { cache });
const app = await mockResolvers.Query.environmentApp(null, { scope }, { cache });
expect(app).toEqual(resolvedEnvironmentsApp);
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
......@@ -34,11 +35,12 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
it('should set the poll interval when there is one', async () => {
const cache = { writeQuery: jest.fn() };
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true } })
.onGet(ENDPOINT, { params: { nested: true, scope } })
.reply(200, environmentsApp, { 'poll-interval': 3000 });
await mockResolvers.Query.environmentApp(null, null, { cache });
await mockResolvers.Query.environmentApp(null, { scope }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery,
data: { interval: 3000 },
......
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { s__ } from '~/locale';
import { __, s__ } from '~/locale';
import EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
......@@ -17,7 +17,10 @@ describe('~/environments/components/new_environments_app.vue', () => {
const createApolloProvider = () => {
const mockResolvers = {
Query: { environmentApp: environmentAppMock, folder: environmentFolderMock },
Query: {
environmentApp: environmentAppMock,
folder: environmentFolderMock,
},
};
return createMockApollo([], mockResolvers);
......@@ -34,6 +37,16 @@ describe('~/environments/components/new_environments_app.vue', () => {
apolloProvider,
});
const createWrapperWithMocked = async ({ provide = {}, environmentsApp, folder }) => {
environmentAppMock.mockReturnValue(environmentsApp);
environmentFolderMock.mockReturnValue(folder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide });
await waitForPromises();
await nextTick();
};
beforeEach(() => {
environmentAppMock = jest.fn();
environmentFolderMock = jest.fn();
......@@ -44,13 +57,10 @@ describe('~/environments/components/new_environments_app.vue', () => {
});
it('should show all the folders that are fetched', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
const text = wrapper.findAllComponents(EnvironmentsFolder).wrappers.map((w) => w.text());
......@@ -59,64 +69,91 @@ describe('~/environments/components/new_environments_app.vue', () => {
});
it('should show a button to create a new environment', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
const button = wrapper.findByRole('link', { name: s__('Environments|New environment') });
expect(button.attributes('href')).toBe('/environments/new');
});
it('should not show a button to create a new environment if the user has no permissions', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({
apolloProvider,
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
provide: { canCreateEnvironment: false, newEnvironmentPath: '' },
});
await waitForPromises();
await Vue.nextTick();
const button = wrapper.findByRole('link', { name: s__('Environments|New environment') });
expect(button.exists()).toBe(false);
});
it('should show a button to open the review app modal', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
button.trigger('click');
await Vue.nextTick();
await nextTick();
expect(wrapper.findByText(s__('ReviewApp|Enable Review App')).exists()).toBe(true);
});
it('should not show a button to open the review app modal if review apps are configured', async () => {
environmentAppMock.mockReturnValue({
...resolvedEnvironmentsApp,
reviewApp: { canSetupReviewApp: false },
await createWrapperWithMocked({
environmentsApp: {
...resolvedEnvironmentsApp,
reviewApp: { canSetupReviewApp: false },
},
folder: resolvedFolder,
});
await waitForPromises();
await nextTick();
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
expect(button.exists()).toBe(false);
});
it('should show tabs for available and stopped environmets', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
folder: resolvedFolder,
});
const [available, stopped] = wrapper.findAllByRole('tab').wrappers;
expect(available.text()).toContain(__('Available'));
expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount);
expect(stopped.text()).toContain(__('Stopped'));
expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount);
});
it('should change the requested scope on tab change', async () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp);
environmentFolderMock.mockReturnValue(resolvedFolder);
const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider });
await waitForPromises();
await Vue.nextTick();
await nextTick();
const stopped = wrapper.findByRole('tab', {
name: `${__('Stopped')} ${resolvedEnvironmentsApp.stoppedCount}`,
});
const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
expect(button.exists()).toBe(false);
stopped.trigger('click');
await nextTick();
await waitForPromises();
expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(),
{ scope: 'stopped' },
expect.anything(),
expect.anything(),
);
});
});
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