Commit bd8d4d2e authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge branch '22539-display-folders' of gitlab.com:gitlab-org/gitlab-ce into 22539-display-folders

parents 8d001844 25f99d84
......@@ -6,15 +6,36 @@
/* globals Vue, EnvironmentsService */
/* eslint-disable no-param-reassign */
$(() => {
(() => { // eslint-disable-line
window.gl = window.gl || {};
const filterState = state => environment => environment.state === state && environment;
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmnetsByState`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*/
// recursiveMap :: (Function, Array) -> Array
const recursiveMap = (fn, arr) => arr.map((item) => {
const filterState = state => environment => environment.state === state && environment;
/**
* Given the filter function and the array of environments will return only
* the environments that match the state provided to the filter function.
*
* @param {Function} fn
* @param {Array} array
* @return {Array}
*/
const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
if (item.children) {
const filteredChildren = recursiveMap(fn, item.children).filter(Boolean);
const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
......@@ -37,26 +58,27 @@ $(() => {
},
data() {
const environmentsListApp = document.querySelector('#environments-list-view');
const environmentsData = document.querySelector('#environments-list-view').dataset;
return {
state: this.store.state,
endpoint: environmentsListApp.dataset.environmentsDataEndpoint,
canCreateDeployment: environmentsListApp.dataset.canCreateDeployment,
canReadEnvironment: environmentsListApp.dataset.canReadEnvironment,
canCreateEnvironment: environmentsListApp.dataset.canCreateEnvironment,
projectEnvironmentsPath: environmentsListApp.dataset.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsListApp.dataset.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsListApp.dataset.newEnvironmentPath,
helpPagePath: environmentsListApp.dataset.helpPagePath,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
canCreateEnvironment: environmentsData.canCreateEnvironment,
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
};
},
computed: {
filteredEnvironments() {
return recursiveMap(filterState(this.visibility), this.state.environments);
return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
},
scope() {
......@@ -81,7 +103,7 @@ $(() => {
* Toggles loading property.
*/
created() {
window.gl.environmentsService = new EnvironmentsService(this.endpoint);
gl.environmentsService = new EnvironmentsService(this.endpoint);
const scope = this.$options.getQueryParameter('scope');
if (scope) {
......@@ -90,7 +112,7 @@ $(() => {
this.isLoading = true;
return window.gl.environmentsService.all()
return gl.environmentsService.all()
.then(resp => resp.json())
.then((json) => {
this.store.storeEnvironments(json);
......@@ -119,10 +141,7 @@ $(() => {
* @returns {Boolean}
*/
convertPermissionToBoolean(string) {
if (string === 'true') {
return true;
}
return false;
return string === 'true';
},
methods: {
......@@ -132,10 +151,10 @@ $(() => {
},
template: `
<div class="container-fluid container-limited">
<div :class="cssContainerClass">
<div class="top-area">
<ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === undefined}">
<li v-bind:class="{ 'active': scope === undefined }">
<a :href="projectEnvironmentsPath">
Available
<span
......@@ -143,7 +162,7 @@ $(() => {
v-html="state.availableCounter"></span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped'}">
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span
......@@ -172,19 +191,18 @@ $(() => {
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create">
New Environment
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create">
New Environment
</a>
</div>
<div
......@@ -227,4 +245,4 @@ $(() => {
</div>
`,
});
});
})();
......@@ -44,18 +44,19 @@
<div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container">
<!-- svg goes here -->
</span>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<a :href="action.play_url" data-method="post" data-rel="nofollow" class="js-manual-action-link">
<span class="action-play-icon-container">
<!-- svg goes here -->
</span>
<span v-html="action.name"></span>
<a :href="action.play_path"
data-method="post"
rel="nofollow"
class="js-manual-action-link">
<span class="action-play-icon-container">
</span>
<span v-html="action.name"></span>
</a>
</li>
</ul>
......
......@@ -76,8 +76,7 @@
* @returns {Boolean|Undefined}
*/
isFolder() {
return this.model.children &&
this.model.children.length > 0;
return this.model.children && this.model.children.length > 0;
},
/**
......@@ -97,8 +96,7 @@
* @returns {Number|Undefined} The number of environments for the current folder.
*/
childrenCounter() {
return this.model.children &&
this.model.children.length;
return this.model.children && this.model.children.length;
},
/**
......@@ -109,7 +107,8 @@
* @returns {Boolean}
*/
hasLastDeploymentKey() {
if (this.model.last_deployment && this.model.last_deployment !== {}) {
if (this.model.last_deployment &&
!this.$options.isObjectEmpty(this.model.last_deployment)) {
return true;
}
return false;
......@@ -168,7 +167,7 @@
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: gl.text.humanize(action.name),
play_url: action.play_url,
play_path: action.play_path,
};
return parsedAction;
});
......@@ -209,8 +208,7 @@
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model.last_deployment &&
this.model.last_deployment.ref) {
if (this.model.last_deployment && this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
......@@ -224,8 +222,8 @@
commitUrl() {
if (this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_url) {
return this.model.last_deployment.commit.commit_url;
this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
......@@ -274,15 +272,15 @@
},
/**
* Verifies if the `retry_url` key is present and returns its value.
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_url) {
return this.model.last_deployment.deployable.retry_url;
this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
......@@ -328,11 +326,8 @@
* @returns {Boolean}
*/
deploymentHasUser() {
if (this.model.last_deployment &&
this.model.last_deployment.user) {
return true;
}
return false;
return !this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user);
},
/**
......@@ -342,11 +337,53 @@
* @returns {Object}
*/
deploymentUser() {
if (this.model.last_deployment && this.model.last_deployment.user) {
if (!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return !this.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.deployable);
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return !this.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
},
/**
* Helper to verify if certain given object are empty.
* Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
* @param {Object} object
* @returns {Bollean}
*/
isObjectEmpty(object) {
for (const key in object) { // eslint-disable-line
if (hasOwnProperty.call(object, key)) {
return false;
}
}
return true;
},
template: `
......@@ -355,7 +392,7 @@
<a
v-if="!isFolder"
class="environment-name"
:href="model.environment_url"
:href="model.environment_path"
v-html="model.name">
</a>
<span v-else v-on:click="toggleRow(model)" class="folder-name">
......@@ -372,7 +409,7 @@
<td class="deployment-column">
<span
v-if="!isFolder && model.last_deployment && model.last_deployment.iid"
v-if="shouldRenderDeploymentID"
v-html="deploymentInternalId">
</span>
......@@ -388,9 +425,9 @@
</td>
<td>
<a v-if="!isFolder && model.last_deployment && model.last_deployment.deployable"
<a v-if="shouldRenderBuildName"
class="build-link"
:href="model.last_deployment.deployable.build_url"
:href="model.last_deployment.deployable.build_path"
v-html="buildName">
</a>
</td>
......@@ -421,25 +458,29 @@
<td class="hidden-xs">
<div v-if="!isFolder">
<div v-if="hasManualActions && canCreateDeployment" class="inline js-manual-actions-container">
<div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container">
<actions-component
:actions="manualActions">
</actions-component>
</div>
<div v-if="model.external_url && canReadEnvironment" class="inline js-external-url-container">
<div v-if="model.external_url && canReadEnvironment"
class="inline js-external-url-container">
<external-url-component
:external_url="model.external_url">
</external_url-component>
</div>
<div v-if="isStoppable && canCreateDeployment" class="inline js-stop-component-container">
<div v-if="isStoppable && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop_url="model.environment_url">
:stop_url="model.environment_path">
</stop-component>
</div>
<div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container">
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
:is_last_deployment="isLastDeployment"
:retry_url="retryUrl">
......
......@@ -30,7 +30,7 @@
class="btn stop-env-link"
:href="stopUrl"
data-method="post"
data-rel="nofollow">
rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i>
</a>
`,
......
......@@ -89,17 +89,17 @@
toggleFolder(envType) {
const environments = this.state.environments;
const environmnetsCopy = environments.map((env) => {
if (env['vue-isChildren'] === true && env.name === envType) {
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
}
return env;
});
this.state.environments = environmnetsCopy;
this.state.environments = environmentsCopy;
return environmnetsCopy;
return environmentsCopy;
},
/**
......@@ -125,15 +125,7 @@
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
},
};
})();
......@@ -143,7 +143,6 @@
</a>
<div class="icon-container commit-icon commit-icon-container">
<!-- svg goes here -->
</div>
<a class="commit-id monospace"
......@@ -153,7 +152,6 @@
<p class="commit-title">
<span v-if="title">
<!-- commit author info-->
<a v-if="hasAuthor"
class="avatar-image-container"
:href="author.web_url">
......
......@@ -16,6 +16,8 @@
}
.environments {
table-layout: fixed;
.deployment-column {
.avatar {
float: none;
......
......@@ -4,21 +4,21 @@ class BuildEntity < Grape::Entity
expose :id
expose :name
expose :build_url do |build|
url_to(:namespace_project_build, build)
expose :build_path do |build|
path_to(:namespace_project_build, build)
end
expose :retry_url do |build|
url_to(:retry_namespace_project_build, build)
expose :retry_path do |build|
path_to(:retry_namespace_project_build, build)
end
expose :play_url, if: ->(build, _) { build.manual? } do |build|
url_to(:play_namespace_project_build, build)
expose :play_path, if: ->(build, _) { build.manual? } do |build|
path_to(:play_namespace_project_build, build)
end
private
def url_to(route, build)
send("#{route}_url", build.project.namespace, build.project, build)
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
end
......@@ -3,8 +3,8 @@ class CommitEntity < API::Entities::RepoCommit
expose :author, using: UserEntity
expose :commit_url do |commit|
namespace_project_tree_url(
expose :commit_path do |commit|
namespace_project_tree_path(
request.project.namespace,
request.project,
id: commit.id)
......
......@@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity
deployment.ref
end
expose :ref_url do |deployment|
namespace_project_tree_url(
expose :ref_path do |deployment|
namespace_project_tree_path(
deployment.project.namespace,
deployment.project,
id: deployment.ref)
......
......@@ -9,8 +9,8 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
expose :environment_url do |environment|
namespace_project_environment_url(
expose :environment_path do |environment|
namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
......
......@@ -16,4 +16,5 @@
"project-environments-path" => project_environments_path(@project),
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
"help-page-path" => help_page_path("ci/environments")}, class: container_class }
"help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class}}
require 'spec_helper'
feature 'Environments', feature: true do
feature 'Environment', :feature do
given(:project) { create(:empty_project) }
given(:user) { create(:user) }
given(:role) { :developer }
......@@ -10,13 +10,13 @@ feature 'Environments', feature: true do
project.team << [user, role]
end
describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) }
feature 'environment details page' do
given!(:environment) { create(:environment, project: project) }
given!(:deployment) { }
given!(:manual) { }
before do
visit namespace_project_environment_path(project.namespace, project, environment)
visit_environment(environment)
end
context 'without deployments' do
......@@ -26,20 +26,27 @@ feature 'Environments', feature: true do
end
context 'with deployments' do
given(:deployment) { create(:deployment, environment: environment) }
context 'when there is no related deployable' do
given(:deployment) do
create(:deployment, environment: environment, deployable: nil)
end
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
scenario 'does not show a re-deploy button for deployment without build' do
expect(page).not_to have_link('Re-deploy')
scenario 'does not show a re-deploy button for deployment without build' do
expect(page).not_to have_link('Re-deploy')
end
end
context 'with build' do
context 'with related deployable present' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
given(:deployment) do
create(:deployment, environment: environment, deployable: build)
end
scenario 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
......@@ -57,7 +64,6 @@ feature 'Environments', feature: true do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
scenario 'does show a play button' do
expect(page).to have_link(manual.name.humanize)
end
......@@ -104,4 +110,52 @@ feature 'Environments', feature: true do
end
end
end
feature 'auto-close environment when branch is deleted' do
given(:project) { create(:project) }
given!(:environment) do
create(:environment, :with_review_app, project: project,
ref: 'feature')
end
scenario 'user visits environment page' do
visit_environment(environment)
expect(page).to have_link('Stop')
end
scenario 'user deletes the branch with running environment' do
visit namespace_project_branches_path(project.namespace, project)
remove_branch_with_hooks(project, user, 'feature') do
page.within('.js-branch-feature') { find('a.btn-remove').click }
end
visit_environment(environment)
expect(page).to have_no_link('Stop')
end
##
# This is a workaround for problem described in #24543
#
def remove_branch_with_hooks(project, user, branch)
params = {
oldrev: project.commit(branch).id,
newrev: Gitlab::Git::BLANK_SHA,
ref: "refs/heads/#{branch}"
}
yield
GitPushService.new(project, user, params).execute
end
end
def visit_environment(environment)
visit namespace_project_environment_path(environment.project.namespace,
environment.project,
environment)
end
end
This diff is collapsed.
......@@ -40,18 +40,18 @@ const environmentsList = [
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_url: 'http://localhost:3000/root/ci-folders/builds/1278',
retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stoppable?': true,
environment_url: 'http://localhost:3000/root/ci-folders/environments/31',
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
......@@ -95,18 +95,18 @@ const environmentsList = [
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_url: 'http://localhost:3000/root/ci-folders/builds/1278',
retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stoppable?': false,
environment_url: 'http://localhost:3000/root/ci-folders/environments/31',
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
......@@ -117,7 +117,7 @@ const environmentsList = [
environment_type: 'review',
last_deployment: null,
'stoppable?': true,
environment_url: 'http://localhost:3000/root/ci-folders/environments/31',
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
......@@ -128,9 +128,8 @@ const environmentsList = [
environment_type: 'review',
last_deployment: null,
'stoppable?': true,
environment_url: 'http://localhost:3000/root/ci-folders/environments/31',
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
];
......@@ -10,9 +10,9 @@ describe BuildEntity do
context 'when build is a regular job' do
let(:build) { create(:ci_build) }
it 'contains url to build page and retry action' do
expect(subject).to include(:build_url, :retry_url)
expect(subject).not_to include(:play_url)
it 'contains paths to build page and retry action' do
expect(subject).to include(:build_path, :retry_path)
expect(subject).not_to include(:play_path)
end
it 'does not contain sensitive information' do
......@@ -24,8 +24,8 @@ describe BuildEntity do
context 'when build is a manual action' do
let(:build) { create(:ci_build, :manual) }
it 'contains url to play action' do
expect(subject).to include(:play_url)
it 'contains path to play action' do
expect(subject).to include(:play_path)
end
end
end
......@@ -31,8 +31,8 @@ describe CommitEntity do
end
end
it 'contains commit URL' do
expect(subject).to include(:commit_url)
it 'contains path to commit' do
expect(subject).to include(:commit_path)
end
it 'needs to receive project in the request' do
......
......@@ -15,6 +15,6 @@ describe DeploymentEntity do
it 'exposes nested information about branch' do
expect(subject[:ref][:name]).to eq 'master'
expect(subject[:ref][:ref_url]).not_to be_empty
expect(subject[:ref][:ref_path]).not_to be_empty
end
end
......@@ -13,6 +13,6 @@ describe EnvironmentEntity do
end
it 'exposes core elements of environment' do
expect(subject).to include(:id, :name, :state, :environment_url)
expect(subject).to include(:id, :name, :state, :environment_path)
end
end
......@@ -33,7 +33,7 @@ describe EnvironmentSerializer do
it 'contains important elements of environment' do
expect(json)
.to include(:name, :external_url, :environment_url, :last_deployment)
.to include(:name, :external_url, :environment_path, :last_deployment)
end
it 'contains relevant information about last deployment' do
......
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