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