Commit 6c9e74ac authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'afontaine/new-environments-folder' into 'master'

Set up entry point for new environments table

See merge request gitlab-org/gitlab!73025
parents 6c172a07 cea54a80
<script>
export default {};
</script>
<template>
<div></div>
</template>
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import environmentApp from './queries/environmentApp.query.graphql';
import { resolvers } from './resolvers';
import typedefs from './typedefs.graphql';
export const apolloProvider = (endpoint) => {
const defaultClient = createDefaultClient(
resolvers(endpoint),
{
assumeImmutableResults: true,
},
typedefs,
);
const { cache } = defaultClient;
cache.writeQuery({
query: environmentApp,
data: {
availableCount: 0,
environments: [],
reviewApp: {},
stoppedCount: 0,
},
});
return new VueApollo({
defaultClient,
});
};
mutation($environment: Environment) {
cancelAutoStop(environment: $environment) @client {
errors
}
}
mutation($environment: Environment) {
deleteEnvironment(environment: $environment) @client {
errors
}
}
mutation($environment: Environment) {
rollbackEnvironment(environment: $environment) @client {
errors
}
}
mutation($environment: Environment) {
stopEnvironment(environment: $environment) @client {
errors
}
}
query getEnvironmentApp {
environmentApp @client {
availableCount
environments
reviewApp
stoppedCount
}
}
query getEnvironmentFolder($environment: NestedEnvironment) {
folder(environment: $environment) @client {
availableCount
environments
stoppedCount
}
}
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const mapNestedEnvironment = (env) => ({
...convertObjectPropsToCamelCase(env, { deep: true }),
__typename: 'NestedEnvironment',
});
const mapEnvironment = (env) => ({
...convertObjectPropsToCamelCase(env),
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Environment',
});
export const resolvers = (endpoint) => ({
Query: {
environmentApp() {
return axios.get(endpoint, { params: { nested: true } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
__typename: 'ReviewApp',
},
stoppedCount: res.data.stopped_count,
__typename: 'EnvironmentApp',
}));
},
folder(_, { environment: { folderPath } }) {
return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
__typename: 'EnvironmentFolder',
}));
},
},
Mutations: {
stopEnvironment(_, { environment: { stopPath } }) {
return axios.post(stopPath);
},
deleteEnvironment(_, { environment: { deletePath } }) {
return axios.delete(deletePath);
},
rollbackEnvironment(_, { environment: { retryUrl } }) {
return axios.post(retryUrl);
},
cancelAutoStop(_, { environment: { autoStopPath } }) {
return axios.post(autoStopPath);
},
},
});
type Environment {
id: Int!
globalId: ID!
name: String!
folderPath: String
stopPath: String
deletePath: String
retryUrl: String
autoStopPath: String
}
type NestedEnvironment {
name: String!
size: Int!
latest: Environment!
}
type EnvironmentFolder {
environments: [Environment!]!
availableCount: Int!
stoppedCount: Int!
}
type ReviewApp {
canSetupReviewApp: Boolean!
allClustersEmpty: Boolean!
reviewSnippet: String
}
type EnvironmentApp {
stoppedCount: Int!
availableCount: Int!
environments: [NestedEnvironment!]!
reviewApp: ReviewApp!
}
......@@ -12,8 +12,8 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default () => {
const el = document.getElementById('environments-list-view');
export default (el) => {
if (el) {
return new Vue({
el,
components: {
......@@ -45,4 +45,7 @@ export default () => {
});
},
});
}
return null;
};
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '../lib/utils/common_utils';
import { apolloProvider } from './graphql/client';
import EnvironmentsApp from './components/new_environments_app.vue';
Vue.use(VueApollo);
export default (el) => {
if (el) {
const {
canCreateEnvironment,
endpoint,
newEnvironmentPath,
helpPagePath,
projectPath,
defaultBranchName,
} = el.dataset;
return new Vue({
el,
apolloProvider: apolloProvider(endpoint),
provide: {
projectPath,
defaultBranchName,
endpoint,
newEnvironmentPath,
helpPagePath,
canCreateEnvironment: parseBoolean(canCreateEnvironment),
},
render(h) {
return h(EnvironmentsApp);
},
});
}
return null;
};
import initEnvironments from '~/environments/';
import initNewEnvironments from '~/environments/new_index';
initEnvironments();
let el = document.getElementById('environments-list-view');
if (el) {
initEnvironments(el);
} else {
el = document.getElementById('environments-table');
initNewEnvironments(el);
}
- page_title _("Environments")
- add_page_specific_style 'page_bundles/environments'
#environments-list-view{ data: { environments_data: environments_list_data,
- if Feature.enabled?(:new_environments_table)
#environments-table{ data: { endpoint: project_environments_path(@project, format: :json),
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments/index.md"),
"project-path" => @project.full_path,
"default-branch-name" => @project.default_branch_or_main } }
- else
- add_page_specific_style 'page_bundles/environments'
#environments-list-view{ data: { environments_data: environments_list_data,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"new-environment-path" => new_project_environment_path(@project),
......
---
name: new_environments_table
introduced_by_url:
rollout_issue_url:
milestone: '14.4'
type: development
group: group::release
default_enabled: false
......@@ -8,6 +8,7 @@ RSpec.describe 'Environments page', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(new_environments_table: false)
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:protected_environments).and_return(true)
project.add_maintainer(user)
......
......@@ -8,6 +8,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
before do
stub_feature_flags(new_environments_table: false)
project.add_role(user, role)
sign_in(user)
end
......
This diff is collapsed.
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { resolvers } from '~/environments/graphql/resolvers';
import { TEST_HOST } from 'helpers/test_constants';
import { environmentsApp, resolvedEnvironmentsApp, folder, resolvedFolder } from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
describe('~/frontend/environments/graphql/resolvers', () => {
let mockResolvers;
let mock;
beforeEach(() => {
mockResolvers = resolvers(ENDPOINT);
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.reset();
});
describe('environmentApp', () => {
it('should fetch environments and map them to frontend data', async () => {
mock.onGet(ENDPOINT, { params: { nested: true } }).reply(200, environmentsApp);
const app = await mockResolvers.Query.environmentApp();
expect(app).toEqual(resolvedEnvironmentsApp);
});
});
describe('folder', () => {
it('should fetch the folder url passed to it', async () => {
mock.onGet(ENDPOINT, { params: { per_page: 3 } }).reply(200, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
});
expect(environmentFolder).toEqual(resolvedFolder);
});
});
describe('stopEnvironment', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.stopEnvironment(null, { environment: { stopPath: ENDPOINT } });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
});
});
describe('rollbackEnvironment', () => {
it('should post to the retry environment path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.rollbackEnvironment(null, {
environment: { retryUrl: ENDPOINT },
});
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
});
});
describe('deleteEnvironment', () => {
it('should DELETE to the delete environment path', async () => {
mock.onDelete(ENDPOINT).reply(200);
await mockResolvers.Mutations.deleteEnvironment(null, {
environment: { deletePath: ENDPOINT },
});
expect(mock.history.delete).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'delete' }),
);
});
});
describe('cancelAutoStop', () => {
it('should post to the auto stop path', async () => {
mock.onPost(ENDPOINT).reply(200);
await mockResolvers.Mutations.cancelAutoStop(null, {
environment: { autoStopPath: ENDPOINT },
});
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
});
});
});
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