Commit 930b9b15 authored by Andrew Fontaine's avatar Andrew Fontaine

Add tabs to new environments page

The tabs select the scope of environments to show, and are synced to the
URL query parameters.

Testing the tab selection is a little tricky due to the reactive poll
interval, so if there is no interval, just skip polling.

This is also good in the event polling is disabled.
parent 4f163766
<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