Merge branch '28732-expandable-folders' into 'master'
Resolve "Expandable folders for environments" Closes #28732 See merge request !10290
This commit is contained in:
commit
30253183c6
|
@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
|
|||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
isLoadingFolderContent: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
|
@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
|
|||
this.fetchEnvironments();
|
||||
|
||||
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
|
||||
eventHub.$on('toggleFolder', this.toggleFolder);
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('refreshEnvironments');
|
||||
eventHub.$off('toggleFolder');
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRow(model) {
|
||||
return this.store.toggleFolder(model.name);
|
||||
toggleFolder(folder, folderUrl) {
|
||||
this.store.toggleFolder(folder);
|
||||
|
||||
if (!folder.isOpen) {
|
||||
this.fetchChildEnvironments(folder, folderUrl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
|
|||
new Flash('An error occurred while fetching the environments.');
|
||||
});
|
||||
},
|
||||
|
||||
fetchChildEnvironments(folder, folderUrl) {
|
||||
this.isLoadingFolderContent = true;
|
||||
|
||||
this.service.getFolderContent(folderUrl)
|
||||
.then(resp => resp.json())
|
||||
.then((response) => {
|
||||
this.store.setfolderContent(folder, response.environments);
|
||||
this.isLoadingFolderContent = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoadingFolderContent = false;
|
||||
new Flash('An error occurred while fetching the environments.');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
|
@ -179,7 +201,8 @@ export default Vue.component('environment-component', {
|
|||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"/>
|
||||
:service="service"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
</div>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
|
|
|
@ -7,6 +7,7 @@ import RollbackComponent from './environment_rollback';
|
|||
import TerminalButtonComponent from './environment_terminal_button';
|
||||
import MonitoringButtonComponent from './environment_monitoring';
|
||||
import CommitComponent from '../../vue_shared/components/commit';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
/**
|
||||
* Envrionment Item Component
|
||||
|
@ -410,7 +411,6 @@ export default {
|
|||
folderUrl() {
|
||||
return `${window.location.pathname}/folders/${this.model.folderName}`;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -428,15 +428,37 @@ export default {
|
|||
return true;
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickFolder() {
|
||||
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<tr>
|
||||
<tr :class="{ 'js-child-row': model.isChildren }">
|
||||
<td>
|
||||
<a v-if="!model.isFolder"
|
||||
class="environment-name"
|
||||
:class="{ 'prepend-left-default': model.isChildren }"
|
||||
:href="environmentPath">
|
||||
{{model.name}}
|
||||
</a>
|
||||
<a v-else class="folder-name" :href="folderUrl">
|
||||
<span v-else
|
||||
class="folder-name"
|
||||
@click="onClickFolder"
|
||||
role="button">
|
||||
|
||||
<span class="folder-icon">
|
||||
<i
|
||||
v-show="model.isOpen"
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true" />
|
||||
<i
|
||||
v-show="!model.isOpen"
|
||||
class="fa fa-caret-right"
|
||||
aria-hidden="true"/>
|
||||
</span>
|
||||
|
||||
<span class="folder-icon">
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
@ -448,7 +470,7 @@ export default {
|
|||
<span class="badge">
|
||||
{{model.size}}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="deployment-column">
|
||||
|
|
|
@ -31,6 +31,18 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isLoadingFolderContent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
folderUrl(model) {
|
||||
return `${window.location.pathname}/folders/${model.folderName}`;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
|
@ -53,6 +65,31 @@ export default {
|
|||
:can-create-deployment="canCreateDeployment"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:service="service"></tr>
|
||||
|
||||
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
|
||||
<tr v-if="isLoadingFolderContent">
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<template v-else>
|
||||
<tr is="environment-item"
|
||||
v-for="children in model.children"
|
||||
:model="children"
|
||||
:can-create-deployment="canCreateDeployment"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:service="service"></tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<a :href="folderUrl(model)" class="btn btn-default">
|
||||
Show all
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -7,6 +7,7 @@ Vue.use(VueResource);
|
|||
export default class EnvironmentsService {
|
||||
constructor(endpoint) {
|
||||
this.environments = Vue.resource(endpoint);
|
||||
this.folderResults = 3;
|
||||
}
|
||||
|
||||
get(scope, page) {
|
||||
|
@ -16,4 +17,8 @@ export default class EnvironmentsService {
|
|||
postAction(endpoint) {
|
||||
return Vue.http.post(endpoint, {}, { emulateJSON: true });
|
||||
}
|
||||
|
||||
getFolderContent(folderUrl) {
|
||||
return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,12 @@ export default class EnvironmentsStore {
|
|||
let filtered = {};
|
||||
|
||||
if (env.size > 1) {
|
||||
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
|
||||
filtered = Object.assign({}, env, {
|
||||
isFolder: true,
|
||||
folderName: env.name,
|
||||
isOpen: false,
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (env.latest) {
|
||||
|
@ -85,4 +90,67 @@ export default class EnvironmentsStore {
|
|||
this.state.stoppedCounter = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles folder open property for the given folder.
|
||||
*
|
||||
* @param {Object} folder
|
||||
* @return {Array}
|
||||
*/
|
||||
toggleFolder(folder) {
|
||||
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the folder with the received environments.
|
||||
*
|
||||
*
|
||||
* @param {Object} folder Folder to update
|
||||
* @param {Array} environments Received environments
|
||||
* @return {Object}
|
||||
*/
|
||||
setfolderContent(folder, environments) {
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
let updated = env;
|
||||
|
||||
if (env.latest) {
|
||||
updated = Object.assign({}, env, env.latest);
|
||||
delete updated.latest;
|
||||
} else {
|
||||
updated = env;
|
||||
}
|
||||
|
||||
updated.isChildren = true;
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
return this.updateFolder(folder, 'children', updatedEnvironments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a folder a prop and a new value updates the correct folder.
|
||||
*
|
||||
* @param {Object} folder
|
||||
* @param {String} prop
|
||||
* @param {String|Boolean|Object|Array} newValue
|
||||
* @return {Array}
|
||||
*/
|
||||
updateFolder(folder, prop, newValue) {
|
||||
const environments = this.state.environments;
|
||||
|
||||
const updatedEnvironments = environments.map((env) => {
|
||||
const updateEnv = Object.assign({}, env);
|
||||
if (env.isFolder && env.id === folder.id) {
|
||||
updateEnv[prop] = newValue;
|
||||
}
|
||||
|
||||
return updateEnv;
|
||||
});
|
||||
|
||||
this.state.environments = updatedEnvironments;
|
||||
|
||||
return updatedEnvironments;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add back expandable folder behavior
|
||||
merge_request:
|
||||
author:
|
|
@ -23,6 +23,42 @@ feature 'Environments page', :feature, :js do
|
|||
expect(page).to have_link('Available')
|
||||
expect(page).to have_link('Stopped')
|
||||
end
|
||||
|
||||
describe 'with one available environment' do
|
||||
given(:environment) { create(:environment, project: project, state: :available) }
|
||||
|
||||
describe 'in available tab page' do
|
||||
it 'should show one environment' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
|
||||
expect(page.all('tbody > tr').length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'in stopped tab page' do
|
||||
it 'should show no environments' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
|
||||
expect(page).to have_content('You don\'t have any environments right now')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with one stopped environment' do
|
||||
given(:environment) { create(:environment, project: project, state: :stopped) }
|
||||
|
||||
describe 'in available tab page' do
|
||||
it 'should show no environments' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
|
||||
expect(page).to have_content('You don\'t have any environments right now')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'in stopped tab page' do
|
||||
it 'should show one environment' do
|
||||
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
|
||||
expect(page.all('tbody > tr').length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without environments' do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import '~/flash';
|
||||
import EnvironmentsComponent from '~/environments/components/environment';
|
||||
import { environment } from './mock_data';
|
||||
import { environment, folder } from './mock_data';
|
||||
|
||||
describe('Environment', () => {
|
||||
preloadFixtures('static/environments/environments.html.raw');
|
||||
|
@ -179,4 +179,101 @@ describe('Environment', () => {
|
|||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandable folders', () => {
|
||||
const environmentsResponseInterceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify({
|
||||
environments: [folder],
|
||||
stopped_count: 0,
|
||||
available_count: 1,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'X-nExt-pAge': '2',
|
||||
'x-page': '1',
|
||||
'X-Per-Page': '1',
|
||||
'X-Prev-Page': '',
|
||||
'X-TOTAL': '37',
|
||||
'X-Total-Pages': '2',
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(environmentsResponseInterceptor);
|
||||
component = new EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(
|
||||
Vue.http.interceptors, environmentsResponseInterceptor,
|
||||
);
|
||||
});
|
||||
|
||||
it('should open a closed folder', (done) => {
|
||||
setTimeout(() => {
|
||||
component.$el.querySelector('.folder-name').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
|
||||
).toContain('display: none');
|
||||
expect(
|
||||
component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
|
||||
).not.toContain('display: none');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should close an opened folder', (done) => {
|
||||
setTimeout(() => {
|
||||
// open folder
|
||||
component.$el.querySelector('.folder-name').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
// close folder
|
||||
component.$el.querySelector('.folder-name').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
|
||||
).toContain('display: none');
|
||||
expect(
|
||||
component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
|
||||
).not.toContain('display: none');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show children environments and a button to show all environments', (done) => {
|
||||
setTimeout(() => {
|
||||
// open folder
|
||||
component.$el.querySelector('.folder-name').click();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
const folderInterceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify({
|
||||
environments: [environment],
|
||||
}), { status: 200 }));
|
||||
};
|
||||
|
||||
Vue.http.interceptors.push(folderInterceptor);
|
||||
|
||||
// wait for next async request
|
||||
setTimeout(() => {
|
||||
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
|
||||
expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
|
||||
|
||||
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import Store from '~/environments/stores/environments_store';
|
||||
import { environmentsList, serverData } from './mock_data';
|
||||
|
||||
(() => {
|
||||
describe('Store', () => {
|
||||
let store;
|
||||
|
||||
|
@ -32,7 +31,76 @@ import { environmentsList, serverData } from './mock_data';
|
|||
expect(store.state.stoppedCounter).toEqual(2);
|
||||
});
|
||||
|
||||
it('should store pagination information', () => {
|
||||
describe('store environments', () => {
|
||||
it('should store environments', () => {
|
||||
store.storeEnvironments(serverData);
|
||||
expect(store.state.environments.length).toEqual(serverData.length);
|
||||
});
|
||||
|
||||
it('should add folder keys when environment is a folder', () => {
|
||||
const environment = {
|
||||
name: 'bar',
|
||||
size: 3,
|
||||
id: 2,
|
||||
};
|
||||
|
||||
store.storeEnvironments([environment]);
|
||||
expect(store.state.environments[0].isFolder).toEqual(true);
|
||||
expect(store.state.environments[0].folderName).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should extract content of `latest` key when provided', () => {
|
||||
const environment = {
|
||||
name: 'bar',
|
||||
size: 3,
|
||||
id: 2,
|
||||
latest: {
|
||||
last_deployment: {},
|
||||
isStoppable: true,
|
||||
},
|
||||
};
|
||||
|
||||
store.storeEnvironments([environment]);
|
||||
expect(store.state.environments[0].last_deployment).toEqual({});
|
||||
expect(store.state.environments[0].isStoppable).toEqual(true);
|
||||
});
|
||||
|
||||
it('should store latest.name when the environment is not a folder', () => {
|
||||
store.storeEnvironments(serverData);
|
||||
expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
|
||||
});
|
||||
|
||||
it('should store root level name when environment is a folder', () => {
|
||||
store.storeEnvironments(serverData);
|
||||
expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFolder', () => {
|
||||
it('should toggle folder', () => {
|
||||
store.storeEnvironments(serverData);
|
||||
|
||||
store.toggleFolder(store.state.environments[1]);
|
||||
expect(store.state.environments[1].isOpen).toEqual(true);
|
||||
|
||||
store.toggleFolder(store.state.environments[1]);
|
||||
expect(store.state.environments[1].isOpen).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setfolderContent', () => {
|
||||
it('should store folder content', () => {
|
||||
store.storeEnvironments(serverData);
|
||||
|
||||
store.setfolderContent(store.state.environments[1], serverData);
|
||||
|
||||
expect(store.state.environments[1].children.length).toEqual(serverData.length);
|
||||
expect(store.state.environments[1].children[0].isChildren).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store pagination', () => {
|
||||
it('should store normalized and integer pagination information', () => {
|
||||
const pagination = {
|
||||
'X-nExt-pAge': '2',
|
||||
'X-page': '1',
|
||||
|
@ -55,4 +123,4 @@ import { environmentsList, serverData } from './mock_data';
|
|||
expect(store.state.paginationInformation).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
})();
|
||||
});
|
||||
|
|
|
@ -84,3 +84,19 @@ export const environment = {
|
|||
updated_at: '2017-01-31T10:53:46.894Z',
|
||||
},
|
||||
};
|
||||
|
||||
export const folder = {
|
||||
folderName: 'build',
|
||||
size: 5,
|
||||
id: 12,
|
||||
name: 'build/update-README',
|
||||
state: 'available',
|
||||
external_url: null,
|
||||
environment_type: 'build',
|
||||
last_deployment: null,
|
||||
'stop_action?': false,
|
||||
environment_path: '/root/review-app/environments/12',
|
||||
stop_path: '/root/review-app/environments/12/stop',
|
||||
created_at: '2017-02-01T19:42:18.400Z',
|
||||
updated_at: '2017-02-01T19:42:18.400Z',
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue