summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6365
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es687
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es636
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es61047
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es655
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es645
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es647
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js.es674
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es612
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js.es614
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js.es6181
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es620
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es6280
-rw-r--r--app/assets/javascripts/issue.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es655
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es62
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es611
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es611
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es61
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es64
-rw-r--r--app/assets/stylesheets/pages/environments.scss22
-rw-r--r--app/controllers/projects/environments_controller.rb35
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/serializers/environment_serializer.rb23
-rw-r--r--app/services/merge_requests/merge_service.rb18
-rw-r--r--app/views/projects/environments/folder.html.haml13
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--changelogs/unreleased/27452-update-issue-count.yml4
-rw-r--r--changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml4
-rw-r--r--changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml4
-rw-r--r--changelogs/unreleased/28176_merge_widget_fix.yml4
-rw-r--r--changelogs/unreleased/add-auto-submited-header.yml4
-rw-r--r--changelogs/unreleased/beautiful-karma-output.yml4
-rw-r--r--changelogs/unreleased/fe-paginated-environments-api-add-subview.yml4
-rw-r--r--changelogs/unreleased/snippets-search-performance.yml4
-rw-r--r--config/initializers/additional_headers_interceptor.rb1
-rw-r--r--config/karma.config.js3
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/webpack.config.js1
-rw-r--r--doc/ci/environments.md5
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/gotchas.md8
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/user/project/integrations/img/services_templates_redmine_example.pngbin8776 -> 8608 bytes
-rw-r--r--doc/user/project/integrations/services_templates.md19
-rw-r--r--lib/additional_email_headers_interceptor.rb8
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/gitlab/import_export/command_line_util.rb12
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb21
-rw-r--r--lib/gitlab/shell.rb4
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-rw-r--r--package.json1
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb65
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/features/merge_requests/widget_spec.rb15
-rw-r--r--spec/features/projects/builds_spec.rb2
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es66
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es64
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es636
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es68
-rw-r--r--spec/javascripts/environments/environment_spec.js.es697
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es64
-rw-r--r--spec/javascripts/environments/environment_table_spec.js.es630
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es696
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js.es6202
-rw-r--r--spec/javascripts/environments/mock_data.js.es6211
-rw-r--r--spec/javascripts/fixtures/environments/environments_folder_view.html.haml7
-rw-r--r--spec/javascripts/issue_spec.js4
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es624
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es612
-rw-r--r--spec/lib/additional_email_headers_interceptor_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb40
-rw-r--r--spec/requests/api/commits_spec.rb14
-rw-r--r--spec/serializers/environment_serializer_spec.rb11
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb29
-rw-r--r--yarn.lock193
79 files changed, 2188 insertions, 1549 deletions
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 91553bda4dc..0cbf952ea5c 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -1,223 +1,192 @@
/* eslint-disable no-param-reassign, no-new */
-/* global Vue */
-/* global EnvironmentsService */
/* global Flash */
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../services/environments_service');
-require('./environment_item');
-
-(() => {
- window.gl = window.gl || {};
-
- gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
- props: {
- store: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+const Vue = require('vue');
+Vue.use(require('vue-resource'));
+const EnvironmentsService = require('../services/environments_service');
+const EnvironmentTable = require('./environments_table');
+const EnvironmentsStore = require('../stores/environments_store');
+require('../../vue_shared/components/table_pagination');
+require('../../lib/utils/common_utils');
+
+module.exports = Vue.component('environment-component', {
+
+ components: {
+ 'environment-table': EnvironmentTable,
+ 'table-pagination': gl.VueGlPagination,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-list-view').dataset;
+ const store = new EnvironmentsStore();
+
+ return {
+ store,
+ state: store.state,
+ 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,
+ commitIconSvg: environmentsData.commitIconSvg,
+ playIconSvg: environmentsData.playIconSvg,
+ terminalIconSvg: environmentsData.terminalIconSvg,
+
+ // Pagination Properties,
+ paginationInformation: {},
+ pageNumber: 1,
+ };
+ },
+
+ computed: {
+ scope() {
+ return gl.utils.getParameterByName('scope');
},
- components: {
- 'environment-item': gl.environmentsList.EnvironmentItem,
+ canReadEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
- data() {
- const environmentsData = document.querySelector('#environments-list-view').dataset;
-
- return {
- state: this.store.state,
- 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,
- commitIconSvg: environmentsData.commitIconSvg,
- playIconSvg: environmentsData.playIconSvg,
- terminalIconSvg: environmentsData.terminalIconSvg,
- };
+ canCreateDeploymentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
- computed: {
- scope() {
- return this.$options.getQueryParameter('scope');
- },
-
- canReadEnvironmentParsed() {
- return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
- },
-
- canCreateDeploymentParsed() {
- return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
- },
-
- canCreateEnvironmentParsed() {
- return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
- },
+ canCreateEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
- /**
- * Fetches all the environments and stores them.
- * Toggles loading property.
- */
- created() {
- gl.environmentsService = new EnvironmentsService(this.endpoint);
-
- const scope = this.$options.getQueryParameter('scope');
- if (scope) {
- this.store.storeVisibility(scope);
- }
-
- this.isLoading = true;
-
- return gl.environmentsService.all()
- .then(resp => resp.json())
- .then((json) => {
- this.store.storeEnvironments(json);
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
+
+ const service = new EnvironmentsService(endpoint);
+
+ this.isLoading = true;
+
+ return service.all()
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.', 'alert');
+ });
+ },
+
+ methods: {
+ toggleRow(model) {
+ return this.store.toggleFolder(model.name);
},
/**
- * Transforms the url parameter into an object and
- * returns the one requested.
+ * Will change the page number and update the URL.
*
- * @param {String} param
- * @returns {String} The value of the requested parameter.
+ * @param {Number} pageNumber desired page to go to.
+ * @return {String}
*/
- getQueryParameter(parameter) {
- return window.location.search.substring(1).split('&').reduce((acc, param) => {
- const paramSplited = param.split('=');
- acc[paramSplited[0]] = paramSplited[1];
- return acc;
- }, {})[parameter];
- },
+ changePage(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
- /**
- * Converts permission provided as strings to booleans.
- * @param {String} string
- * @returns {Boolean}
- */
- convertPermissionToBoolean(string) {
- return string === 'true';
+ gl.utils.visitUrl(param);
+ return param;
},
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul v-if="!isLoading" class="nav-links">
+ <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
+ <a :href="newEnvironmentPath" class="btn btn-create">
+ New environment
+ </a>
+ </div>
+ </div>
- methods: {
- toggleRow(model) {
- return this.store.toggleFolder(model.name);
- },
- },
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
- template: `
- <div :class="cssContainerClass">
- <div class="top-area">
- <ul v-if="!isLoading" class="nav-links">
- <li v-bind:class="{ 'active': scope === undefined }">
- <a :href="projectEnvironmentsPath">
- Available
- <span class="badge js-available-environments-count">
- {{state.availableCounter}}
- </span>
- </a>
- </li><li v-bind:class="{ 'active' : scope === 'stopped' }">
- <a :href="projectStoppedEnvironmentsPath">
- Stopped
- <span class="badge js-stopped-environments-count">
- {{state.stoppedCounter}}
- </span>
- </a>
- </li>
- </ul>
- <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-create">
- New environment
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ You don't have any environments right now.
+ </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>
- </div>
+ </p>
+
+ <a v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create js-new-environment-button">
+ New Environment
+ </a>
</div>
- <div class="environments-container">
- <div class="environments-list-loading text-center" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.environments.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- You don't have any environments right now.
- </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>
- </p>
-
- <a
- v-if="canCreateEnvironmentParsed"
- :href="newEnvironmentPath"
- class="btn btn-create js-new-environment-button">
- New Environment
- </a>
- </div>
-
- <div class="table-holder"
- v-if="!isLoading && state.filteredEnvironments.length > 0">
- <table class="table ci-table environments">
- <thead>
- <tr>
- <th class="environments-name">Environment</th>
- <th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Job</th>
- <th class="environments-commit">Commit</th>
- <th class="environments-date">Updated</th>
- <th class="hidden-xs environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in state.filteredEnvironments"
- v-bind:model="model">
-
- <tr
- is="environment-item"
- :model="model"
- :toggleRow="toggleRow.bind(model)"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg"></tr>
-
- <tr v-if="model.isOpen && model.children && model.children.length > 0"
- is="environment-item"
- v-for="children in model.children"
- :model="children"
- :toggleRow="toggleRow.bind(children)"
- :can-create-deployment="canCreateDeploymentParsed"
- :can-read-environment="canReadEnvironmentParsed"
- :play-icon-svg="playIconSvg"
- :terminal-icon-svg="terminalIconSvg"
- :commit-icon-svg="commitIconSvg">
- </tr>
-
- </template>
- </tbody>
- </table>
- </div>
+ <div class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg">
+ </environment-table>
+
+ <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation">
+ </table-pagination>
</div>
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
index ed1c78945db..c5a714d9673 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -1,50 +1,43 @@
-/* global Vue */
-
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
- props: {
- actions: {
- type: Array,
- required: false,
- default: () => [],
- },
-
- playIconSvg: {
- type: String,
- required: false,
- },
+const Vue = require('vue');
+
+module.exports = Vue.component('actions-component', {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
},
- template: `
- <div class="inline">
- <div class="dropdown">
- <a class="dropdown-new btn btn-default" data-toggle="dropdown">
- <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></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_path"
- data-method="post"
- rel="nofollow"
- class="js-manual-action-link">
-
- <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
-
- <span>
- {{action.name}}
- </span>
- </a>
- </li>
- </ul>
- </div>
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ template: `
+ <div class="inline">
+ <div class="dropdown">
+ <a class="dropdown-new btn btn-default" data-toggle="dropdown">
+ <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></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_path"
+ data-method="post"
+ rel="nofollow"
+ class="js-manual-action-link">
+
+ <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
+
+ <span>
+ {{action.name}}
+ </span>
+ </a>
+ </li>
+ </ul>
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
index 28cc0022d17..2599bba3c59 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -1,23 +1,19 @@
-/* global Vue */
+/**
+ * Renders the external url link in environments table.
+ */
+const Vue = require('vue');
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
- props: {
- externalUrl: {
- type: String,
- default: '',
- },
+module.exports = Vue.component('external-url-component', {
+ props: {
+ externalUrl: {
+ type: String,
+ default: '',
},
+ },
- template: `
- <a class="btn external_url" :href="externalUrl" target="_blank">
- <i class="fa fa-external-link"></i>
- </a>
- `,
- });
-})();
+ template: `
+ <a class="btn external_url" :href="externalUrl" target="_blank">
+ <i class="fa fa-external-link"></i>
+ </a>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 39746621c43..24fd58a301a 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -1,538 +1,549 @@
-/* global Vue */
-/* global timeago */
+const Vue = require('vue');
+const Timeago = require('timeago.js');
-window.Vue = require('vue');
-window.timeago = require('timeago.js');
require('../../lib/utils/text_utility');
require('../../vue_shared/components/commit');
-require('./environment_actions');
-require('./environment_external_url');
-require('./environment_stop');
-require('./environment_rollback');
-require('./environment_terminal_button');
+const ActionsComponent = require('./environment_actions');
+const ExternalUrlComponent = require('./environment_external_url');
+const StopComponent = require('./environment_stop');
+const RollbackComponent = require('./environment_rollback');
+const TerminalButtonComponent = require('./environment_terminal_button');
+
+/**
+ * Envrionment Item Component
+ *
+ * Renders a table row for each environment.
+ */
+
+const timeagoInstance = new Timeago();
+
+module.exports = Vue.component('environment-item', {
+
+ components: {
+ 'commit-component': gl.CommitComponent,
+ 'actions-component': ActionsComponent,
+ 'external-url-component': ExternalUrlComponent,
+ 'stop-component': StopComponent,
+ 'rollback-component': RollbackComponent,
+ 'terminal-button-component': TerminalButtonComponent,
+ },
+
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
-(() => {
- /**
- * Envrionment Item Component
- *
- * Used in a hierarchical structure to show folders with children
- * in a table.
- * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html)
- *
- * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539)
- * for more information.15
- */
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
- window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line
-
- gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
-
- components: {
- 'commit-component': gl.CommitComponent,
- 'actions-component': gl.environmentsList.ActionsComponent,
- 'external-url-component': gl.environmentsList.ExternalUrlComponent,
- 'stop-component': gl.environmentsList.StopComponent,
- 'rollback-component': gl.environmentsList.RollbackComponent,
- 'terminal-button-component': gl.environmentsList.TerminalButtonComponent,
- },
-
- props: {
- model: {
- type: Object,
- required: true,
- default: () => ({}),
- },
-
- toggleRow: {
- type: Function,
- required: false,
- },
-
- canCreateDeployment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- commitIconSvg: {
- type: String,
- required: false,
- },
-
- playIconSvg: {
- type: String,
- required: false,
- },
-
- terminalIconSvg: {
- type: String,
- required: false,
- },
-
- },
-
- data() {
- return {
- rowClass: {
- 'children-row': this.model['vue-isChildren'],
- },
- };
- },
-
- computed: {
-
- /**
- * If an item has a `children` entry it means it is a folder.
- * Folder items have different behaviours - it is possible to toggle
- * them and show their children.
- *
- * @returns {Boolean|Undefined}
- */
- isFolder() {
- return this.model.children && this.model.children.length > 0;
- },
-
- /**
- * If an item is inside a folder structure will return true.
- * Used for css purposes.
- *
- * @returns {Boolean|undefined}
- */
- isChildren() {
- return this.model['vue-isChildren'];
- },
-
- /**
- * Counts the number of environments in each folder.
- * Used to show a badge with the counter.
- *
- * @returns {Number|Undefined} The number of environments for the current folder.
- */
- childrenCounter() {
- return this.model.children && this.model.children.length;
- },
-
- /**
- * Verifies if `last_deployment` key exists in the current Envrionment.
- * This key is required to render most of the html - this method works has
- * an helper.
- *
- * @returns {Boolean}
- */
- hasLastDeploymentKey() {
- if (this.model.last_deployment &&
- !this.$options.isObjectEmpty(this.model.last_deployment)) {
- return true;
- }
- return false;
- },
-
- /**
- * Verifies is the given environment has manual actions.
- * Used to verify if we should render them or nor.
- *
- * @returns {Boolean|Undefined}
- */
- hasManualActions() {
- return this.model.last_deployment && this.model.last_deployment.manual_actions &&
- this.model.last_deployment.manual_actions.length > 0;
- },
-
- /**
- * Returns the value of the `stop_action?` key provided in the response.
- *
- * @returns {Boolean}
- */
- hasStopAction() {
- return this.model['stop_action?'];
- },
-
- /**
- * Verifies if the `deployable` key is present in `last_deployment` key.
- * Used to verify whether we should or not render the rollback partial.
- *
- * @returns {Boolean|Undefined}
- */
- canRetry() {
- return this.hasLastDeploymentKey &&
- this.model.last_deployment &&
- this.model.last_deployment.deployable;
- },
-
- /**
- * Verifies if the date to be shown is present.
- *
- * @returns {Boolean|Undefined}
- */
- canShowDate() {
- return this.model.last_deployment &&
- this.model.last_deployment.deployable &&
- this.model.last_deployment.deployable !== undefined;
- },
-
- /**
- * Human readable date.
- *
- * @returns {String}
- */
- createdDate() {
- return gl.environmentsList.timeagoInstance.format(
- this.model.last_deployment.deployable.created_at,
- );
- },
-
- /**
- * Returns the manual actions with the name parsed.
- *
- * @returns {Array.<Object>|Undefined}
- */
- manualActions() {
- if (this.hasManualActions) {
- return this.model.last_deployment.manual_actions.map((action) => {
- const parsedAction = {
- name: gl.text.humanize(action.name),
- play_path: action.play_path,
- };
- return parsedAction;
- });
- }
- return [];
- },
-
- /**
- * Builds the string used in the user image alt attribute.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- if (this.model.last_deployment &&
- this.model.last_deployment.user &&
- this.model.last_deployment.user.username) {
- return `${this.model.last_deployment.user.username}'s avatar'`;
- }
- return '';
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.model.last_deployment &&
- this.model.last_deployment.tag) {
- return this.model.last_deployment.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.model.last_deployment && this.model.last_deployment.ref) {
- return this.model.last_deployment.ref;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.commit_path) {
- return this.model.last_deployment.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.short_id) {
- return this.model.last_deployment.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.title) {
- return this.model.last_deployment.commit.title;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit tag.
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- if (this.model.last_deployment &&
- this.model.last_deployment.commit &&
- this.model.last_deployment.commit.author) {
- return this.model.last_deployment.commit.author;
- }
-
- return undefined;
- },
-
- /**
- * 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_path) {
- return this.model.last_deployment.deployable.retry_path;
- }
- return undefined;
- },
-
- /**
- * Verifies if the `last?` key is present and returns its value.
- *
- * @returns {Boolean|Undefined}
- */
- isLastDeployment() {
- return this.model.last_deployment && this.model.last_deployment['last?'];
- },
-
- /**
- * Builds the name of the builds needed to display both the name and the id.
- *
- * @returns {String}
- */
- buildName() {
- if (this.model.last_deployment &&
- this.model.last_deployment.deployable) {
- return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
- }
- return '';
- },
-
- /**
- * Builds the needed string to show the internal id.
- *
- * @returns {String}
- */
- deploymentInternalId() {
- if (this.model.last_deployment &&
- this.model.last_deployment.iid) {
- return `#${this.model.last_deployment.iid}`;
- }
- return '';
- },
-
- /**
- * Verifies if the user object is present under last_deployment object.
- *
- * @returns {Boolean}
- */
- deploymentHasUser() {
- return !this.$options.isObjectEmpty(this.model.last_deployment) &&
- !this.$options.isObjectEmpty(this.model.last_deployment.user);
- },
-
- /**
- * Returns the user object nested with the last_deployment object.
- * Used to render the template.
- *
- * @returns {Object}
- */
- deploymentUser() {
- 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;
- },
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+
+ playIconSvg: {
+ type: String,
+ required: false,
},
+ terminalIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ computed: {
/**
- * 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}
+ * Verifies if `last_deployment` key exists in the current Envrionment.
+ * This key is required to render most of the html - this method works has
+ * an helper.
+ *
+ * @returns {Boolean}
*/
- isObjectEmpty(object) {
- for (const key in object) { // eslint-disable-line
- if (hasOwnProperty.call(object, key)) {
- return false;
- }
+ hasLastDeploymentKey() {
+ if (this.model &&
+ this.model.last_deployment &&
+ !this.$options.isObjectEmpty(this.model.last_deployment)) {
+ return true;
}
- return true;
+ return false;
},
- template: `
- <tr>
- <td v-bind:class="{ 'children-row': isChildren}">
- <a v-if="!isFolder"
- class="environment-name"
- :href="model.environment_path">
- {{model.name}}
- </a>
- <span v-else v-on:click="toggleRow(model)" class="folder-name">
- <span class="folder-icon">
- <i v-show="model.isOpen" class="fa fa-caret-down"></i>
- <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
- </span>
-
- <span>
- {{model.name}}
- </span>
-
- <span class="badge">
- {{childrenCounter}}
- </span>
- </span>
- </td>
+ /**
+ * Verifies is the given environment has manual actions.
+ * Used to verify if we should render them or nor.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ hasManualActions() {
+ return this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.manual_actions &&
+ this.model.last_deployment.manual_actions.length > 0;
+ },
+
+ /**
+ * Returns the value of the `stop_action?` key provided in the response.
+ *
+ * @returns {Boolean}
+ */
+ hasStopAction() {
+ return this.model && this.model['stop_action?'];
+ },
+
+ /**
+ * Verifies if the `deployable` key is present in `last_deployment` key.
+ * Used to verify whether we should or not render the rollback partial.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canRetry() {
+ return this.model &&
+ this.hasLastDeploymentKey &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable;
+ },
+
+ /**
+ * Verifies if the date to be shown is present.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canShowDate() {
+ return this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable !== undefined;
+ },
+
+ /**
+ * Human readable date.
+ *
+ * @returns {String}
+ */
+ createdDate() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.created_at) {
+ return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
+ }
+ return '';
+ },
+
+ /**
+ * Returns the manual actions with the name parsed.
+ *
+ * @returns {Array.<Object>|Undefined}
+ */
+ manualActions() {
+ if (this.hasManualActions) {
+ return this.model.last_deployment.manual_actions.map((action) => {
+ const parsedAction = {
+ name: gl.text.humanize(action.name),
+ play_path: action.play_path,
+ };
+ return parsedAction;
+ });
+ }
+ return [];
+ },
+
+ /**
+ * Builds the string used in the user image alt attribute.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.user &&
+ this.model.last_deployment.user.username) {
+ return `${this.model.last_deployment.user.username}'s avatar'`;
+ }
+ return '';
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.tag) {
+ return this.model.last_deployment.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.ref) {
+ return this.model.last_deployment.ref;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.commit_path) {
+ return this.model.last_deployment.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.short_id) {
+ return this.model.last_deployment.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.title) {
+ return this.model.last_deployment.commit.title;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.author) {
+ return this.model.last_deployment.commit.author;
+ }
- <td class="deployment-column">
- <span v-if="shouldRenderDeploymentID">
- {{deploymentInternalId}}
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `retry_path` key is present and returns its value.
+ *
+ * @returns {String|Undefined}
+ */
+ retryUrl() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path) {
+ return this.model.last_deployment.deployable.retry_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `last?` key is present and returns its value.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isLastDeployment() {
+ return this.model && this.model.last_deployment &&
+ this.model.last_deployment['last?'];
+ },
+
+ /**
+ * Builds the name of the builds needed to display both the name and the id.
+ *
+ * @returns {String}
+ */
+ buildName() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable) {
+ return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
+ }
+ return '';
+ },
+
+ /**
+ * Builds the needed string to show the internal id.
+ *
+ * @returns {String}
+ */
+ deploymentInternalId() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.iid) {
+ return `#${this.model.last_deployment.iid}`;
+ }
+ return '';
+ },
+
+ /**
+ * Verifies if the user object is present under last_deployment object.
+ *
+ * @returns {Boolean}
+ */
+ deploymentHasUser() {
+ return this.model &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user);
+ },
+
+ /**
+ * Returns the user object nested with the last_deployment object.
+ * Used to render the template.
+ *
+ * @returns {Object}
+ */
+ deploymentUser() {
+ if (this.model &&
+ !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.model.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
+ },
+
+ /**
+ * Verifies the presence of all the keys needed to render the buil_path.
+ *
+ * @return {String}
+ */
+ buildPath() {
+ if (this.model &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.build_path) {
+ return this.model.last_deployment.deployable.build_path;
+ }
+
+ return '';
+ },
+
+ /**
+ * Verifies the presence of all the keys needed to render the external_url.
+ *
+ * @return {String}
+ */
+ externalURL() {
+ if (this.model && this.model.external_url) {
+ return this.model.external_url;
+ }
+
+ return '';
+ },
+
+ /**
+ * 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.model.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ this.model.last_deployment.iid !== undefined;
+ },
+
+ environmentPath() {
+ if (this.model && this.model.environment_path) {
+ return this.model.environment_path;
+ }
+
+ return '';
+ },
+
+ /**
+ * Constructs folder URL based on the current location and the folder id.
+ *
+ * @return {String}
+ */
+ folderUrl() {
+ return `${window.location.pathname}/folders/${this.model.folderName}`;
+ },
+
+ },
+
+ /**
+ * 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: `
+ <tr>
+ <td>
+ <a v-if="!model.isFolder"
+ class="environment-name"
+ :href="environmentPath">
+ {{model.name}}
+ </a>
+ <a v-else class="folder-name" :href="folderUrl">
+ <span class="folder-icon">
+ <i class="fa fa-folder" aria-hidden="true"></i>
</span>
- <span v-if="!isFolder && deploymentHasUser">
- by
- <a :href="deploymentUser.web_url" class="js-deploy-user-container">
- <img class="avatar has-tooltip s20"
- :src="deploymentUser.avatar_url"
- :alt="userImageAltDescription"
- :title="deploymentUser.username" />
- </a>
+ <span>
+ {{model.folderName}}
</span>
- </td>
- <td class="environments-build-cell">
- <a v-if="shouldRenderBuildName"
- class="build-link"
- :href="model.last_deployment.deployable.build_path">
- {{buildName}}
+ <span class="badge">
+ {{model.size}}
+ </span>
+ </a>
+ </td>
+
+ <td class="deployment-column">
+ <span v-if="shouldRenderDeploymentID">
+ {{deploymentInternalId}}
+ </span>
+
+ <span v-if="!model.isFolder && deploymentHasUser">
+ by
+ <a :href="deploymentUser.web_url" class="js-deploy-user-container">
+ <img class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
</a>
- </td>
-
- <td>
- <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"
- :commit-icon-svg="commitIconSvg">
- </commit-component>
+ </span>
+ </td>
+
+ <td class="environments-build-cell">
+ <a v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="buildPath">
+ {{buildName}}
+ </a>
+ </td>
+
+ <td>
+ <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="commitIconSvg">
+ </commit-component>
+ </div>
+ <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span v-if="!model.isFolder && canShowDate"
+ class="environment-created-date-timeago">
+ {{createdDate}}
+ </span>
+ </td>
+
+ <td class="hidden-xs">
+ <div v-if="!model.isFolder">
+ <div v-if="hasManualActions && canCreateDeployment"
+ class="inline js-manual-actions-container">
+ <actions-component
+ :play-icon-svg="playIconSvg"
+ :actions="manualActions">
+ </actions-component>
</div>
- <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
- No deployments yet
- </p>
- </td>
-
- <td>
- <span
- v-if="!isFolder && canShowDate"
- class="environment-created-date-timeago">
- {{createdDate}}
- </span>
- </td>
-
- <td class="hidden-xs">
- <div v-if="!isFolder">
- <div v-if="hasManualActions && canCreateDeployment"
- class="inline js-manual-actions-container">
- <actions-component
- :play-icon-svg="playIconSvg"
- :actions="manualActions">
- </actions-component>
- </div>
-
- <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="hasStopAction && canCreateDeployment"
- class="inline js-stop-component-container">
- <stop-component
- :stop-url="model.stop_path">
- </stop-component>
- </div>
-
- <div v-if="model.terminal_path"
- class="inline js-terminal-button-container">
- <terminal-button-component
- :terminal-icon-svg="terminalIconSvg"
- :terminal-path="model.terminal_path">
- </terminal-button-component>
- </div>
-
- <div v-if="canRetry && canCreateDeployment"
- class="inline js-rollback-component-container">
- <rollback-component
- :is-last-deployment="isLastDeployment"
- :retry-url="retryUrl">
- </rollback-component>
- </div>
+
+ <div v-if="externalURL && canReadEnvironment"
+ class="inline js-external-url-container">
+ <external-url-component
+ :external-url="externalURL">
+ </external-url-component>
+ </div>
+
+ <div v-if="hasStopAction && canCreateDeployment"
+ class="inline js-stop-component-container">
+ <stop-component
+ :stop-url="model.stop_path">
+ </stop-component>
+ </div>
+
+ <div v-if="model && model.terminal_path"
+ class="inline js-terminal-button-container">
+ <terminal-button-component
+ :terminal-icon-svg="terminalIconSvg"
+ :terminal-path="model.terminal_path">
+ </terminal-button-component>
+ </div>
+
+ <div v-if="canRetry && canCreateDeployment"
+ class="inline js-rollback-component-container">
+ <rollback-component
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryUrl">
+ </rollback-component>
</div>
- </td>
- </tr>
- `,
- });
-})();
+ </div>
+ </td>
+ </tr>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
index 5938340a128..daf126eb4e8 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -1,33 +1,30 @@
-/* global Vue */
+/**
+ * Renders Rollback or Re deploy button in environments table depending
+ * of the provided property `isLastDeployment`
+ */
+const Vue = require('vue');
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
- props: {
- retryUrl: {
- type: String,
- default: '',
- },
+module.exports = Vue.component('rollback-component', {
+ props: {
+ retryUrl: {
+ type: String,
+ default: '',
+ },
- isLastDeployment: {
- type: Boolean,
- default: true,
- },
+ isLastDeployment: {
+ type: Boolean,
+ default: true,
},
+ },
- template: `
- <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
- <span v-if="isLastDeployment">
- Re-deploy
- </span>
- <span v-else>
- Rollback
- </span>
- </a>
- `,
- });
-})();
+ template: `
+ <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
+ <span v-if="isLastDeployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
+ </a>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
index be9526989a0..96983a19568 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -1,27 +1,24 @@
-/* global Vue */
+/**
+ * Renders the stop "button" that allows stop an environment.
+ * Used in environments table.
+ */
+const Vue = require('vue');
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.StopComponent = Vue.component('stop-component', {
- props: {
- stopUrl: {
- type: String,
- default: '',
- },
+module.exports = Vue.component('stop-component', {
+ props: {
+ stopUrl: {
+ type: String,
+ default: '',
},
+ },
- template: `
- <a class="btn stop-env-link"
- :href="stopUrl"
- data-confirm="Are you sure you want to stop this environment?"
- data-method="post"
- rel="nofollow">
- <i class="fa fa-stop stop-env-icon"></i>
- </a>
- `,
- });
-})();
+ template: `
+ <a class="btn stop-env-link"
+ :href="stopUrl"
+ data-confirm="Are you sure you want to stop this environment?"
+ data-method="post"
+ rel="nofollow">
+ <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
+ </a>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
index a3ad063f7cb..481e0d15e7a 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
@@ -1,28 +1,25 @@
-/* global Vue */
+/**
+ * Renders a terminal button to open a web terminal.
+ * Used in environments table.
+ */
+const Vue = require('vue');
-window.Vue = require('vue');
-
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
- props: {
- terminalPath: {
- type: String,
- default: '',
- },
- terminalIconSvg: {
- type: String,
- default: '',
- },
+module.exports = Vue.component('terminal-button-component', {
+ props: {
+ terminalPath: {
+ type: String,
+ default: '',
+ },
+ terminalIconSvg: {
+ type: String,
+ default: '',
},
+ },
- template: `
- <a class="btn terminal-button"
- :href="terminalPath">
- <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
- </a>
- `,
- });
-})();
+ template: `
+ <a class="btn terminal-button"
+ :href="terminalPath">
+ <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
+ </a>
+ `,
+});
diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6
new file mode 100644
index 00000000000..fd35d77fd3d
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environments_table.js.es6
@@ -0,0 +1,74 @@
+/**
+ * Render environments table.
+ */
+const Vue = require('vue');
+const EnvironmentItem = require('./environment_item');
+
+module.exports = Vue.component('environment-table-component', {
+
+ components: {
+ 'environment-item': EnvironmentItem,
+ },
+
+ props: {
+ environments: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ commitIconSvg: {
+ type: String,
+ required: false,
+ },
+
+ playIconSvg: {
+ type: String,
+ required: false,
+ },
+
+ terminalIconSvg: {
+ type: String,
+ required: false,
+ },
+ },
+
+ template: `
+ <table class="table ci-table environments">
+ <thead>
+ <tr>
+ <th class="environments-name">Environment</th>
+ <th class="environments-deploy">Last deployment</th>
+ <th class="environments-build">Job</th>
+ <th class="environments-commit">Commit</th>
+ <th class="environments-date">Updated</th>
+ <th class="hidden-xs environments-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in environments"
+ v-bind:model="model">
+ <tr is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 05c59d92fd4..867eba1d384 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -1,6 +1,4 @@
-window.Vue = require('vue');
-require('./stores/environments_store');
-require('./components/environment');
+const EnvironmentsComponent = require('./components/environment');
require('../vue_shared/vue_resource_interceptor');
$(() => {
@@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true);
}
- const Store = gl.environmentsList.EnvironmentsStore;
- gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
+ gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
-
- propsData: {
- store: Store.create(),
- },
-
});
});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
new file mode 100644
index 00000000000..29f704c1a37
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6
@@ -0,0 +1,14 @@
+const EnvironmentsFolderComponent = require('./environments_folder_view');
+require('../../vue_shared/vue_resource_interceptor');
+
+$(() => {
+ window.gl = window.gl || {};
+
+ if (gl.EnvironmentsListFolderApp) {
+ gl.EnvironmentsListFolderApp.$destroy(true);
+ }
+
+ gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+});
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
new file mode 100644
index 00000000000..0b1204559da
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6
@@ -0,0 +1,181 @@
+/* eslint-disable no-param-reassign, no-new */
+/* global Flash */
+
+const Vue = require('vue');
+Vue.use(require('vue-resource'));
+const EnvironmentsService = require('../services/environments_service');
+const EnvironmentTable = require('../components/environments_table');
+const EnvironmentsStore = require('../stores/environments_store');
+require('../../vue_shared/components/table_pagination');
+require('../../lib/utils/common_utils');
+
+module.exports = Vue.component('environment-folder-view', {
+
+ components: {
+ 'environment-table': EnvironmentTable,
+ 'table-pagination': gl.VueGlPagination,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
+ const store = new EnvironmentsStore();
+ const pathname = window.location.pathname;
+ const endpoint = `${pathname}.json`;
+ const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
+
+ return {
+ store,
+ folderName,
+ endpoint,
+ state: store.state,
+ visibility: 'available',
+ isLoading: false,
+ cssContainerClass: environmentsData.cssClass,
+ canCreateDeployment: environmentsData.canCreateDeployment,
+ canReadEnvironment: environmentsData.canReadEnvironment,
+
+ // svgs
+ commitIconSvg: environmentsData.commitIconSvg,
+ playIconSvg: environmentsData.playIconSvg,
+ terminalIconSvg: environmentsData.terminalIconSvg,
+
+ // Pagination Properties,
+ paginationInformation: {},
+ pageNumber: 1,
+ };
+ },
+
+ computed: {
+ scope() {
+ return gl.utils.getParameterByName('scope');
+ },
+
+ canReadEnvironmentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
+ },
+
+ canCreateDeploymentParsed() {
+ return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
+ },
+
+ /**
+ * URL to link in the stopped tab.
+ *
+ * @return {String}
+ */
+ stoppedPath() {
+ return `${window.location.pathname}?scope=stopped`;
+ },
+
+ /**
+ * URL to link in the available tab.
+ *
+ * @return {String}
+ */
+ availablePath() {
+ return window.location.pathname;
+ },
+ },
+
+ /**
+ * Fetches all the environments and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
+
+ const service = new EnvironmentsService(endpoint);
+
+ this.isLoading = true;
+
+ return service.all()
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ })
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the environments.', 'alert');
+ });
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ changePage(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
+ },
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area" v-if="!isLoading">
+
+ <h4 class="js-folder-name environments-folder-name">
+ Environments / <b>{{folderName}}</b>
+ </h4>
+
+ <ul class="nav-links">
+ <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
+ <a :href="availablePath" class="js-available-environments-folder-tab">
+ Available
+ <span class="badge js-available-environments-count">
+ {{state.availableCounter}}
+ </span>
+ </a>
+ </li>
+ <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="stoppedPath" class="js-stopped-environments-folder-tab">
+ Stopped
+ <span class="badge js-stopped-environments-count">
+ {{state.stoppedCounter}}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+
+ <environment-table
+ :environments="state.environments"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"
+ :play-icon-svg="playIconSvg"
+ :terminal-icon-svg="terminalIconSvg"
+ :commit-icon-svg="commitIconSvg">
+ </environment-table>
+
+ <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
+ :change="changePage"
+ :pageInfo="state.paginationInformation">
+ </table-pagination>
+ </div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
index fab8d977f58..9cef335868e 100644
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -1,20 +1,8 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
+const Vue = require('vue');
class EnvironmentsService {
-
- constructor(root) {
- Vue.http.options.root = root;
-
- this.environments = Vue.resource(root);
-
- Vue.http.interceptors.push((request, next) => {
- // needed in order to not break the tests.
- if ($.rails) {
- request.headers['X-CSRF-Token'] = $.rails.csrfToken();
- }
- next();
- });
+ constructor(endpoint) {
+ this.environments = Vue.resource(endpoint);
}
all() {
@@ -22,4 +10,4 @@ class EnvironmentsService {
}
}
-window.EnvironmentsService = EnvironmentsService;
+module.exports = EnvironmentsService;
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
index 9b4090100da..15cd9bde08e 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -1,190 +1,90 @@
-/* eslint-disable no-param-reassign */
-(() => {
- window.gl = window.gl || {};
- window.gl.environmentsList = window.gl.environmentsList || {};
-
- gl.environmentsList.EnvironmentsStore = {
- state: {},
-
- create() {
- this.state.environments = [];
- this.state.stoppedCounter = 0;
- this.state.availableCounter = 0;
- this.state.visibility = 'available';
- this.state.filteredEnvironments = [];
-
- return this;
- },
-
- /**
- * In order to display a tree view we need to modify the received
- * data in to a tree structure based on `environment_type`
- * sorted alphabetically.
- * In each children a `vue-` property will be added. This property will be
- * used to know if an item is a children mostly for css purposes. This is
- * needed because the children row is a fragment instance and therfore does
- * not accept non-prop attributes.
- *
- *
- * @example
- * it will transform this:
- * [
- * { name: "environment", environment_type: "review" },
- * { name: "environment_1", environment_type: null }
- * { name: "environment_2, environment_type: "review" }
- * ]
- * into this:
- * [
- * { name: "review", children:
- * [
- * { name: "environment", environment_type: "review", vue-isChildren: true},
- * { name: "environment_2", environment_type: "review", vue-isChildren: true}
- * ]
- * },
- * {name: "environment_1", environment_type: null}
- * ]
- *
- *
- * @param {Array} environments List of environments.
- * @returns {Array} Tree structured array with the received environments.
- */
- storeEnvironments(environments = []) {
- this.state.stoppedCounter = this.countByState(environments, 'stopped');
- this.state.availableCounter = this.countByState(environments, 'available');
-
- const environmentsTree = environments.reduce((acc, environment) => {
- if (environment.environment_type !== null) {
- const occurs = acc.filter(element => element.children &&
- element.name === environment.environment_type);
-
- environment['vue-isChildren'] = true;
-
- if (occurs.length) {
- acc[acc.indexOf(occurs[0])].children.push(environment);
- acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
- } else {
- acc.push({
- name: environment.environment_type,
- children: [environment],
- isOpen: false,
- 'vue-isChildren': environment['vue-isChildren'],
- });
- }
- } else {
- acc.push(environment);
- }
-
- return acc;
- }, []).slice().sort(this.sortByName);
-
- this.state.environments = environmentsTree;
-
- this.filterEnvironmentsByVisibility(this.state.environments);
-
- return environmentsTree;
- },
-
- storeVisibility(visibility) {
- this.state.visibility = visibility;
- },
- /**
- * 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 `filterEnvironmentsByVisibility`
- * 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.
- *
- * Given array of environments will return only
- * the environments that match the state stored.
- *
- * @param {Array} array
- * @return {Array}
- */
- filterEnvironmentsByVisibility(arr) {
- const filteredEnvironments = arr.map((item) => {
- if (item.children) {
- const filteredChildren = this.filterEnvironmentsByVisibility(
- item.children,
- ).filter(Boolean);
-
- if (filteredChildren.length) {
- item.children = filteredChildren;
- return item;
- }
- }
-
- return this.filterState(this.state.visibility, item);
- }).filter(Boolean);
-
- this.state.filteredEnvironments = filteredEnvironments;
- return filteredEnvironments;
- },
-
- /**
- * Given the state and the environment,
- * returns only if the environment state matches the one provided.
- *
- * @param {String} state
- * @param {Object} environment
- * @return {Object}
- */
- filterState(state, environment) {
- return environment.state === state && environment;
- },
-
- /**
- * Toggles folder open property given the environment type.
- *
- * @param {String} envType
- * @return {Array}
- */
- toggleFolder(envType) {
- const environments = this.state.environments;
-
- const environmentsCopy = environments.map((env) => {
- if (env['vue-isChildren'] && env.name === envType) {
- env.isOpen = !env.isOpen;
- }
-
- return env;
- });
-
- this.state.environments = environmentsCopy;
-
- return environmentsCopy;
- },
-
- /**
- * Given an array of environments, returns the number of environments
- * that have the given state.
- *
- * @param {Array} environments
- * @param {String} state
- * @returns {Number}
- */
- countByState(environments, state) {
- return environments.filter(env => env.state === state).length;
- },
-
- /**
- * Sorts the two objects provided by their name.
- *
- * @param {Object} a
- * @param {Object} b
- * @returns {Number}
- */
- sortByName(a, b) {
- const nameA = a.name.toUpperCase();
- const nameB = b.name.toUpperCase();
-
- return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
- },
- };
-})();
+require('~/lib/utils/common_utils');
+/**
+ * Environments Store.
+ *
+ * Stores received environments, count of stopped environments and count of
+ * available environments.
+ */
+class EnvironmentsStore {
+ constructor() {
+ this.state = {};
+ this.state.environments = [];
+ this.state.stoppedCounter = 0;
+ this.state.availableCounter = 0;
+ this.state.paginationInformation = {};
+
+ return this;
+ }
+
+ /**
+ *
+ * Stores the received environments.
+ *
+ * In the main environments endpoint, each environment has the following schema
+ * { name: String, size: Number, latest: Object }
+ * In the endpoint to retrieve environments from each folder, the environment does
+ * not have the `latest` key and the data is all in the root level.
+ * To avoid doing this check in the view, we store both cases the same by extracting
+ * what is inside the `latest` key.
+ *
+ * If the `size` is bigger than 1, it means it should be rendered as a folder.
+ * In those cases we add `isFolder` key in order to render it properly.
+ *
+ * @param {Array} environments
+ * @returns {Array}
+ */
+ storeEnvironments(environments = []) {
+ const filteredEnvironments = environments.map((env) => {
+ let filtered = {};
+
+ if (env.size > 1) {
+ filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
+ }
+
+ if (env.latest) {
+ filtered = Object.assign(filtered, env, env.latest);
+ delete filtered.latest;
+ } else {
+ filtered = Object.assign(filtered, env);
+ }
+
+ return filtered;
+ });
+
+ this.state.environments = filteredEnvironments;
+
+ return filteredEnvironments;
+ }
+
+ setPagination(pagination = {}) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
+
+ this.state.paginationInformation = paginationInformation;
+ return paginationInformation;
+ }
+
+ /**
+ * Stores the number of available environments.
+ *
+ * @param {Number} count = 0
+ * @return {Number}
+ */
+ storeAvailableCount(count = 0) {
+ this.state.availableCounter = count;
+ return count;
+ }
+
+ /**
+ * Stores the number of closed environments.
+ *
+ * @param {Number} count = 0
+ * @return {Number}
+ */
+ storeStoppedCount(count = 0) {
+ this.state.stoppedCounter = count;
+ return count;
+ }
+}
+
+module.exports = EnvironmentsStore;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 6c08b1b8e61..1776b3d61f6 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -54,16 +54,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
+ const currentTotal = Number($('.issue_counter').text());
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
+ $('.issue_counter').text(currentTotal - 1);
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
+ $('.issue_counter').text(currentTotal + 1);
}
} else {
new Flash(issueFailMessage, 'alert');
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index bcb3a706b51..764aff51fee 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -232,6 +232,21 @@
};
/**
+ * Parses pagination object string values into numbers.
+ *
+ * @param {Object} paginationInformation
+ * @returns {Object}
+ */
+ w.gl.utils.parseIntPagination = paginationInformation => ({
+ perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
+ page: parseInt(paginationInformation['X-PAGE'], 10),
+ total: parseInt(paginationInformation['X-TOTAL'], 10),
+ totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
+ nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
+ previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
+ });
+
+ /**
* Transforms a DOMStringMap into a plain object.
*
* @param {DOMStringMap} DOMStringMapObject
@@ -241,5 +256,45 @@
acc[element] = DOMStringMapObject[element];
return acc;
}, {});
+
+ /**
+ * Updates the search parameter of a URL given the parameter and values provided.
+ *
+ * If no search params are present we'll add it.
+ * If param for page is already present, we'll update it
+ * If there are params but not for the given one, we'll add it at the end.
+ * Returns the new search parameters.
+ *
+ * @param {String} param
+ * @param {Number|String|Undefined|Null} value
+ * @return {String}
+ */
+ w.gl.utils.setParamInURL = (param, value) => {
+ let search;
+ const locationSearch = window.location.search;
+
+ if (locationSearch.length === 0) {
+ search = `?${param}=${value}`;
+ }
+
+ if (locationSearch.indexOf(param) !== -1) {
+ const regex = new RegExp(param + '=\\d');
+ search = locationSearch.replace(regex, `${param}=${value}`);
+ }
+
+ if (locationSearch.length && locationSearch.indexOf(param) === -1) {
+ search = `${locationSearch}&${param}=${value}`;
+ }
+
+ return search;
+ };
+
+ /**
+ * Converts permission provided as strings to booleans.
+ *
+ * @param {String} string
+ * @returns {Boolean}
+ */
+ w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
})(window);
}).call(this);
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 69aed77c83d..4ab33420e59 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -110,7 +110,7 @@ require('./smart_interval');
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
- return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
+ return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
index e47dc6935d6..39935b08dc0 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -35,7 +35,16 @@ require('../vue_shared/components/pipelines_table');
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
},
methods: {
- change(pagenum, apiScope) {
+
+ /**
+ * Changes the URL according to the pagination component.
+ *
+ * If no scope is provided, 'all' is assumed.
+ *
+ * @param {Number} pagenum
+ * @param {String} apiScope = 'all'
+ */
+ change(pagenum, apiScope = 'all') {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
},
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 0ee21f00fdc..572f0493c9f 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -5,16 +5,7 @@ require('../vue_realtime_listener');
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
-
- const paginationInfo = {
- perPage: +normalized['X-PER-PAGE'],
- page: +normalized['X-PAGE'],
- total: +normalized['X-TOTAL'],
- totalPages: +normalized['X-TOTAL-PAGES'],
- nextPage: +normalized['X-NEXT-PAGE'],
- previousPage: +normalized['X-PREV-PAGE'],
- };
-
+ const paginationInfo = gl.utils.normalizeHeaders(normalized);
return paginationInfo;
};
diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
index 7f7c18ddeb1..ff88e236829 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js.es6
+++ b/app/assets/javascripts/vue_shared/components/commit.js.es6
@@ -1,4 +1,5 @@
/* global Vue */
+window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
index 67c6cb73761..d8042a9b7fc 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
@@ -57,9 +57,7 @@ window.Vue = require('vue');
},
methods: {
changePage(e) {
- let apiScope = gl.utils.getParameterByName('scope');
-
- if (!apiScope) apiScope = 'all';
+ const apiScope = gl.utils.getParameterByName('scope');
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 778ef01430e..181dcb7721f 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -10,6 +10,11 @@
font-size: 34px;
}
+.environments-folder-name {
+ font-weight: normal;
+ padding-top: 20px;
+}
+
@media (max-width: $screen-xs-max) {
.environments-container {
width: 100%;
@@ -110,17 +115,20 @@
}
}
- .children-row .environment-name {
- margin-left: 17px;
- margin-right: -17px;
- }
-
.folder-icon {
- padding: 0 5px 0 0;
+ margin-right: 3px;
+ color: $gl-text-color-secondary;
+ display: inline-block;
+
+ .fa:nth-child(1) {
+ margin-right: 3px;
+ }
}
.folder-name {
cursor: pointer;
+ color: $gl-text-color-secondary;
+ display: inline-block;
}
}
@@ -135,4 +143,4 @@
margin-right: 0;
}
}
-} \ No newline at end of file
+}
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 0ec8f5bd64a..fed75396d6e 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
- @scope = params[:scope]
- @environments = project.environments.includes(:last_deployment)
+ @environments = project.environments
+ .with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
- render json: EnvironmentSerializer
- .new(project: @project, user: current_user)
- .represent(@environments)
+ render json: {
+ environments: EnvironmentSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .within_folders
+ .represent(@environments),
+ available_count: project.environments.available.count,
+ stopped_count: project.environments.stopped.count
+ }
+ end
+ end
+ end
+
+ def folder
+ folder_environments = project.environments.where(environment_type: params[:id])
+ @environments = folder_environments.with_state(params[:scope] || :available)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ environments: EnvironmentSerializer
+ .new(project: @project, user: @current_user)
+ .with_pagination(request, response)
+ .represent(@environments),
+ available_count: folder_environments.available.count,
+ stopped_count: folder_environments.stopped.count
+ }
end
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c3e1760f168..63b5bcbb586 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -369,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
- if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
- @status = :success
- elsif merge_request.merge_when_build_succeeds
+ if merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds
+ else
+ # Only MRs that can be merged end in this action
+ # MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
+ # in last case it does not have any special status. Possible error is handled inside widget js function
+ @status = :success
end
render 'merge'
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 9db96347322..d0b991db112 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -160,6 +160,10 @@ class ProjectWiki
}
end
+ def repository_storage_path
+ project.repository_storage_path
+ end
+
private
def init_repo(path_with_namespace)
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index fe16a3784c4..d0a60f134da 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer
end
def represent(resource, opts = {})
- resource = @paginator.paginate(resource) if paginated?
-
if itemized?
itemize(resource).map do |item|
{ name: item.name,
@@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) }
end
else
+ resource = @paginator.paginate(resource) if paginated?
+
super(resource, opts)
end
end
@@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer
private
def itemize(resource)
- items = resource.group(:item_name).order('item_name ASC')
- .pluck('COALESCE(environment_type, name) AS item_name',
- 'COUNT(*) AS environments_count',
- 'MAX(id) AS last_environment_id')
+ items = resource.order('folder_name ASC')
+ .group('COALESCE(environment_type, name)')
+ .select('COALESCE(environment_type, name) AS folder_name',
+ 'COUNT(*) AS size', 'MAX(id) AS last_id')
+
+ # It makes a difference when you call `paginate` method, because
+ # although `page` is effective at the end, it calls counting methods
+ # immediately.
+ items = @paginator.paginate(items) if paginated?
- environments = resource.where(id: items.map(&:last)).index_by(&:id)
+ environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
- items.map do |name, size, id|
- Item.new(name, size, environments[id])
+ items.map do |item|
+ Item.new(item.folder_name, item.size, environments[item.last_id])
end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 5ca6fec962d..177b714b734 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -11,18 +11,20 @@ module MergeRequests
def execute(merge_request)
@merge_request = merge_request
- return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
+ unless @merge_request.mergeable?
+ return log_merge_error('Merge request is not mergeable', save_message_on_model: true)
+ end
@source = find_merge_source
- return log_merge_error('No source for merge', true) unless @source
+ unless @source
+ log_merge_error('No source for merge', save_message_on_model: true)
+ end
merge_request.in_locked_state do
if commit
after_merge
success
- else
- log_merge_error('Can not merge changes', true)
end
end
end
@@ -43,11 +45,11 @@ module MergeRequests
if commit_id
merge_request.update(merge_commit_sha: commit_id)
else
- merge_request.update(merge_error: 'Conflicts detected during merge')
+ log_merge_error('Conflicts detected during merge', save_message_on_model: true)
false
end
rescue GitHooksService::PreReceiveError => e
- merge_request.update(merge_error: e.message)
+ log_merge_error(e.message, save_message_on_model: true)
false
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
@@ -70,10 +72,10 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
- def log_merge_error(message, http_error = false)
+ def log_merge_error(message, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
- error(message) if http_error
+ @merge_request.update(merge_error: message) if save_message_on_model
end
def merge_request_info
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
new file mode 100644
index 00000000000..d9cb7bc0331
--- /dev/null
+++ b/app/views/projects/environments/folder.html.haml
@@ -0,0 +1,13 @@
+- @no_container = true
+- page_title "Environments"
+= render "projects/pipelines/head"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag("environments_folder")
+
+#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "css-class" => container_class,
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "terminal-icon-svg" => custom_icon("icon_terminal"),
+ "play-icon-svg" => custom_icon("icon_play") } }
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index e3062f47788..c676953f729 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -28,7 +28,7 @@
%span
CI job
= ci_label_for_status(status)
- for
+ for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index f7808ea6aff..e977c1f1698 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -7,7 +7,7 @@
= snippet.title
by
= link_to user_snippets_path(snippet.author) do
- = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
+ = image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 704d1d01a81..026f404ce07 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -18,6 +18,6 @@
%span
by
= link_to user_snippets_path(snippet_title.author) do
- = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
+ = image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at)
diff --git a/changelogs/unreleased/27452-update-issue-count.yml b/changelogs/unreleased/27452-update-issue-count.yml
new file mode 100644
index 00000000000..a7417eba63c
--- /dev/null
+++ b/changelogs/unreleased/27452-update-issue-count.yml
@@ -0,0 +1,4 @@
+---
+title: update issue count when closing/reopening an issue
+merge_request:
+author:
diff --git a/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
new file mode 100644
index 00000000000..89e2bdc69bc
--- /dev/null
+++ b/changelogs/unreleased/27873-when-a-commit-appears-in-several-projects-commit-comments-are-shared-across-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Only return target project's comments for a commit
+merge_request:
+author:
diff --git a/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml b/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml
new file mode 100644
index 00000000000..cd61c38e1bc
--- /dev/null
+++ b/changelogs/unreleased/28124-mrs-don-t-show-all-merge-errors.yml
@@ -0,0 +1,4 @@
+---
+title: Show merge errors in merge request widget
+merge_request: 9229
+author:
diff --git a/changelogs/unreleased/28176_merge_widget_fix.yml b/changelogs/unreleased/28176_merge_widget_fix.yml
new file mode 100644
index 00000000000..8e4e75fc237
--- /dev/null
+++ b/changelogs/unreleased/28176_merge_widget_fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix error in MR widget after /merge slash command
+merge_request: 9259
+author:
diff --git a/changelogs/unreleased/add-auto-submited-header.yml b/changelogs/unreleased/add-auto-submited-header.yml
new file mode 100644
index 00000000000..93481613b39
--- /dev/null
+++ b/changelogs/unreleased/add-auto-submited-header.yml
@@ -0,0 +1,4 @@
+---
+title: Set Auto-Submitted header to mails
+merge_request:
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/beautiful-karma-output.yml b/changelogs/unreleased/beautiful-karma-output.yml
new file mode 100644
index 00000000000..6ccddebab68
--- /dev/null
+++ b/changelogs/unreleased/beautiful-karma-output.yml
@@ -0,0 +1,4 @@
+---
+title: Make Karma output look nicer for CI
+merge_request: 9165
+author: winniehell
diff --git a/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml
new file mode 100644
index 00000000000..7e626982de6
--- /dev/null
+++ b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml
@@ -0,0 +1,4 @@
+---
+title: Adds paginationd and folders view to environments table
+merge_request:
+author:
diff --git a/changelogs/unreleased/snippets-search-performance.yml b/changelogs/unreleased/snippets-search-performance.yml
new file mode 100644
index 00000000000..2895478abfd
--- /dev/null
+++ b/changelogs/unreleased/snippets-search-performance.yml
@@ -0,0 +1,4 @@
+---
+title: Reduced query count for snippet search
+merge_request:
+author:
diff --git a/config/initializers/additional_headers_interceptor.rb b/config/initializers/additional_headers_interceptor.rb
new file mode 100644
index 00000000000..b9159e7c06c
--- /dev/null
+++ b/config/initializers/additional_headers_interceptor.rb
@@ -0,0 +1 @@
+ActionMailer::Base.register_interceptor(AdditionalEmailHeadersInterceptor)
diff --git a/config/karma.config.js b/config/karma.config.js
index a1fbeab1f46..5b472780aed 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -4,6 +4,7 @@ var ROOT_PATH = path.resolve(__dirname, '..');
// Karma configuration
module.exports = function(config) {
+ var progressReporter = process.env.CI ? 'mocha' : 'progress';
config.set({
basePath: ROOT_PATH,
browsers: ['PhantomJS'],
@@ -15,7 +16,7 @@ module.exports = function(config) {
preprocessors: {
'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
},
- reporters: ['progress', 'coverage-istanbul'],
+ reporters: [progressReporter, 'coverage-istanbul'],
coverageIstanbulReporter: {
reports: ['html', 'text-summary'],
dir: 'coverage-javascript/',
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 2ac98cf3842..84f123ff717 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do
get :terminal
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end
+
+ collection do
+ get :folder, path: 'folders/:id'
+ end
end
resource :cycle_analytics, only: [:show]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index e63775e43ce..8d981c76c1a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -22,6 +22,7 @@ var config = {
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
+ environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js',
issuable: './issuable/issuable_bundle.js',
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3bba94f8b83..3c31ba45d3d 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -535,6 +535,7 @@ deploy_review:
- master
stop_review:
+ stage: deploy
variables:
GIT_STRATEGY: none
script:
@@ -555,7 +556,9 @@ when their associated branch is deleted.
When you have an environment that has a stop action defined (typically when
the environment describes a review app), GitLab will automatically trigger a
-stop action when the associated branch is deleted.
+stop action when the associated branch is deleted. The `stop_review` job must
+be in the same `stage` as the `deploy_review` one in order for the environment
+to automatically stop.
You can read more in the [`.gitlab-ci.yml` reference][onstop].
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 63be61d1bca..a73598df812 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -690,6 +690,8 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `when` - [reference](#when)
- `environment:name`
- `environment:action`
+- `stage` should be the same as the `review_app` in order for the environment
+ to stop automatically when the branch is deleted
#### dynamic environments
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 0f78e8238af..565d4b33457 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -3,7 +3,7 @@
The purpose of this guide is to document potential "gotchas" that contributors
might encounter or should avoid during development of GitLab CE and EE.
-## Don't `describe` symbols
+## Do not `describe` symbols
Consider the following model spec:
@@ -32,7 +32,7 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth
Except for the top-level `describe` block, always provide a String argument to
`describe`.
-## Don't assert against the absolute value of a sequence-generated attribute
+## Do not assert against the absolute value of a sequence-generated attribute
Consider the following factory:
@@ -121,7 +121,7 @@ describe API::Labels do
end
```
-## Don't `rescue Exception`
+## Do not `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
@@ -130,7 +130,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897
-## Don't use inline JavaScript in views
+## Do not use inline JavaScript in views
Using the inline `:javascript` Haml filters comes with a
performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
diff --git a/doc/university/README.md b/doc/university/README.md
index e9f14703789..8d4e7eff115 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/).
-Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information.
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) for more information.
## Gitlab University Curriculum
diff --git a/doc/user/project/integrations/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png
index 50d20510daf..379cef9888d 100644
--- a/doc/user/project/integrations/img/services_templates_redmine_example.png
+++ b/doc/user/project/integrations/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md
index be6d13b6d2b..5b04d7d88b8 100644
--- a/doc/user/project/integrations/services_templates.md
+++ b/doc/user/project/integrations/services_templates.md
@@ -1,25 +1,26 @@
-# Services Templates
+# Services templates
A GitLab administrator can add a service template that sets a default for each
-project. This makes it much easier to configure individual projects.
+project. After a service template is enabled, it will be applied to new
+projects only and its details will be pre-filled on the project's Service page.
-After the template is created, the template details will be pre-filled on a
-project's Service page.
-
-## Enable a Service template
+## Enable a service template
In GitLab's Admin area, navigate to **Service Templates** and choose the
service template you wish to create.
-For example, in the image below you can see Redmine.
+## Services for external issue trackers
+
+In the image below you can see how a service template for Redmine would look
+like.
![Redmine service template](img/services_templates_redmine_example.png)
---
-**NOTE:** For each project, you will still need to configure the issue tracking
+For each project, you will still need to configure the issue tracking
URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
the project settings, and GitLab would automatically update the URL configured
in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
-must be configured directly within the project's **Services** settings.
+must be configured directly within the project's **Integrations** settings.
diff --git a/lib/additional_email_headers_interceptor.rb b/lib/additional_email_headers_interceptor.rb
new file mode 100644
index 00000000000..2358fa6bbfd
--- /dev/null
+++ b/lib/additional_email_headers_interceptor.rb
@@ -0,0 +1,8 @@
+class AdditionalEmailHeadersInterceptor
+ def self.delivering_email(message)
+ message.headers(
+ 'Auto-Submitted' => 'auto-generated',
+ 'X-Auto-Response-Suppress' => 'All'
+ )
+ end
+end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2fefe760d24..173083d0ade 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -114,7 +114,7 @@ module API
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
- notes = Note.where(commit_id: commit.id).order(:created_at)
+ notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
present paginate(notes), with: Entities::CommitNote
end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index f00c7460e82..90942774a2e 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -15,14 +15,6 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end
- def git_unbundle(repo_path:, bundle_path:)
- execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
- end
-
- def git_restore_hooks
- execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
- end
-
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
@@ -56,10 +48,6 @@ module Gitlab
FileUtils.copy_entry(source, destination)
true
end
-
- def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
- end
end
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 48a9a6fa5e2..c824d3ea9fc 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
+ include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:)
@project = project
@@ -12,29 +13,11 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- mkdir_p(path_to_repo)
-
- git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
+ gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle)
rescue => e
@shared.error(e)
false
end
-
- private
-
- def path_to_repo
- @project.repository.path_to_repo
- end
-
- def repo_restore_hooks
- return true if wiki?
-
- git_restore_hooks
- end
-
- def wiki?
- @project.class.name == 'ProjectWiki'
- end
end
end
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 942cedd6cd4..3faa336f142 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -80,8 +80,10 @@ module Gitlab
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(storage, name, url)
+ # Timeout should be less than 900 ideally, to prevent the memory killer
+ # to silently kill the process without knowing we are timing out here.
output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
- storage, "#{name}.git", url, '900'])
+ storage, "#{name}.git", url, '800'])
raise Error, output unless status.zero?
true
end
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 9e01f02029c..b85f70e450e 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -31,11 +31,11 @@ module Gitlab
private
def snippet_titles
- limit_snippets.search(query).order('updated_at DESC')
+ limit_snippets.search(query).order('updated_at DESC').includes(:author)
end
def snippet_blobs
- limit_snippets.search_code(query).order('updated_at DESC')
+ limit_snippets.search_code(query).order('updated_at DESC').includes(:author)
end
def default_scope
diff --git a/package.json b/package.json
index 0500ba9670e..08bde1bc313 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"jquery-ui": "github:jquery/jquery-ui#1.11.4",
"jquery-ujs": "1.2.1",
"js-cookie": "^2.1.3",
+ "karma-mocha-reporter": "^2.2.2",
"mousetrap": "1.4.6",
"pikaday": "^1.5.1",
"select2": "3.5.2-browserify",
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 7ac1d62d1b1..84d119f1867 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -3,9 +3,12 @@ require 'spec_helper'
describe Projects::EnvironmentsController do
include ApiHelpers
- let(:environment) { create(:environment) }
- let(:project) { environment.project }
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:environment) do
+ create(:environment, name: 'production', project: project)
+ end
before do
project.team << [user, :master]
@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do
end
end
- context 'when requesting JSON response' do
- it 'responds with correct JSON' do
- get :index, environment_params(format: :json)
+ context 'when requesting JSON response for folders' do
+ before do
+ create(:environment, project: project,
+ name: 'staging/review-1',
+ state: :available)
+
+ create(:environment, project: project,
+ name: 'staging/review-2',
+ state: :available)
+
+ create(:environment, project: project,
+ name: 'staging/review-3',
+ state: :stopped)
+ end
+
+ let(:environments) { json_response['environments'] }
+
+ context 'when requesting available environments scope' do
+ before do
+ get :index, environment_params(format: :json, scope: :available)
+ end
+
+ it 'responds with a payload describing available environments' do
+ expect(environments.count).to eq 2
+ expect(environments.first['name']).to eq 'production'
+ expect(environments.second['name']).to eq 'staging'
+ expect(environments.second['size']).to eq 2
+ expect(environments.second['latest']['name']).to eq 'staging/review-2'
+ end
- first_environment = json_response.first
+ it 'contains values describing environment scopes sizes' do
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
+ end
- expect(first_environment).not_to be_empty
- expect(first_environment['name']). to eq environment.name
+ context 'when requesting stopped environments scope' do
+ before do
+ get :index, environment_params(format: :json, scope: :stopped)
+ end
+
+ it 'responds with a payload describing stopped environments' do
+ expect(environments.count).to eq 1
+ expect(environments.first['name']).to eq 'staging'
+ expect(environments.first['size']).to eq 1
+ expect(environments.first['latest']['name']).to eq 'staging/review-3'
+ end
+
+ it 'contains values describing environment scopes sizes' do
+ expect(json_response['available_count']).to eq 3
+ expect(json_response['stopped_count']).to eq 1
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index bfd134e406e..f84f922ba5e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1143,15 +1143,15 @@ describe Projects::MergeRequestsController do
end
end
- context 'when no special status for MR' do
+ context 'when MR does not have special state' do
let(:merge_request) { create(:merge_request, source_project: project) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
- it 'sets status to nil' do
- expect(assigns(:status)).to be_nil
+ it 'sets status to success' do
+ expect(assigns(:status)).to eq(:success)
expect(response).to render_template('merge')
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 957e913bf95..4ad944366c8 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -52,4 +52,19 @@ describe 'Merge request', :feature, :js do
end
end
end
+
+ context 'merge error' do
+ before do
+ allow_any_instance_of(Repository).to receive(:merge).and_return(false)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ click_button 'Accept Merge Request'
+ wait_for_ajax
+ end
+
+ it 'updates the MR widget' do
+ page.within('.mr-widget-body') do
+ expect(page).to have_content('Conflicts detected during merge')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index f1036b275f7..2116721b224 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -275,7 +275,7 @@ feature 'Builds', :feature do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
- it 'shows a link to lastest deployment' do
+ it 'shows a link to latest deployment' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link('latest deployment')
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
index b1838045a06..850586f9f3a 100644
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -1,4 +1,4 @@
-require('~/environments/components/environment_actions');
+const ActionsComponent = require('~/environments/components/environment_actions');
describe('Actions Component', () => {
preloadFixtures('static/environments/element.html.raw');
@@ -19,7 +19,7 @@ describe('Actions Component', () => {
},
];
- const component = new window.gl.environmentsList.ActionsComponent({
+ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
actions: actionsMock,
@@ -47,7 +47,7 @@ describe('Actions Component', () => {
},
];
- const component = new window.gl.environmentsList.ActionsComponent({
+ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
actions: actionsMock,
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
index a6a587e69f5..393dbb5aae0 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -1,4 +1,4 @@
-require('~/environments/components/environment_external_url');
+const ExternalUrlComponent = require('~/environments/components/environment_external_url');
describe('External URL Component', () => {
preloadFixtures('static/environments/element.html.raw');
@@ -8,7 +8,7 @@ describe('External URL Component', () => {
it('should link to the provided externalUrl prop', () => {
const externalURL = 'https://gitlab.com';
- const component = new window.gl.environmentsList.ExternalUrlComponent({
+ const component = new ExternalUrlComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
externalUrl: externalURL,
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
index e6fb0d00290..7fea80ed799 100644
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -1,5 +1,5 @@
window.timeago = require('timeago.js');
-require('~/environments/components/environment_item');
+const EnvironmentItem = require('~/environments/components/environment_item');
describe('Environment item', () => {
preloadFixtures('static/environments/table.html.raw');
@@ -14,33 +14,16 @@ describe('Environment item', () => {
beforeEach(() => {
mockItem = {
name: 'review',
- children: [
- {
- name: 'review-app',
- id: 1,
- state: 'available',
- external_url: '',
- last_deployment: {},
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-10T15:55:58.778Z',
- },
- {
- name: 'production',
- id: 2,
- state: 'available',
- external_url: '',
- last_deployment: {},
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-10T15:55:58.778Z',
- },
- ],
+ folderName: 'review',
+ size: 3,
+ isFolder: true,
+ environment_path: 'url',
};
- component = new window.gl.environmentsList.EnvironmentItem({
+ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'),
propsData: {
model: mockItem,
- toggleRow: () => {},
canCreateDeployment: false,
canReadEnvironment: true,
},
@@ -53,7 +36,7 @@ describe('Environment item', () => {
});
it('Should render the number of children in a badge', () => {
- expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length);
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
});
});
@@ -63,8 +46,8 @@ describe('Environment item', () => {
beforeEach(() => {
environment = {
- id: 31,
name: 'production',
+ size: 1,
state: 'stopped',
external_url: 'http://external.com',
environment_type: null,
@@ -125,11 +108,10 @@ describe('Environment item', () => {
updated_at: '2016-11-10T15:55:58.778Z',
};
- component = new window.gl.environmentsList.EnvironmentItem({
+ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'),
propsData: {
model: environment,
- toggleRow: () => {},
canCreateDeployment: true,
canReadEnvironment: true,
},
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 043b8708a6e..4a596baad09 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -1,4 +1,4 @@
-require('~/environments/components/environment_rollback');
+const RollbackComponent = require('~/environments/components/environment_rollback');
describe('Rollback Component', () => {
preloadFixtures('static/environments/element.html.raw');
@@ -10,7 +10,7 @@ describe('Rollback Component', () => {
});
it('Should link to the provided retryUrl', () => {
- const component = new window.gl.environmentsList.RollbackComponent({
+ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
@@ -22,7 +22,7 @@ describe('Rollback Component', () => {
});
it('Should render Re-deploy label when isLastDeployment is true', () => {
- const component = new window.gl.environmentsList.RollbackComponent({
+ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
@@ -34,7 +34,7 @@ describe('Rollback Component', () => {
});
it('Should render Rollback label when isLastDeployment is false', () => {
- const component = new window.gl.environmentsList.RollbackComponent({
+ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
retryUrl: retryURL,
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
index 87eda136122..edd0cad32d0 100644
--- a/spec/javascripts/environments/environment_spec.js.es6
+++ b/spec/javascripts/environments/environment_spec.js.es6
@@ -1,9 +1,7 @@
-/* global Vue, environment */
-
+const Vue = require('vue');
require('~/flash');
-require('~/environments/stores/environments_store');
-require('~/environments/components/environment');
-require('./mock_data');
+const EnvironmentsComponent = require('~/environments/components/environment');
+const { environment } = require('./mock_data');
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
@@ -33,11 +31,8 @@ describe('Environment', () => {
});
it('should render the empty state', (done) => {
- component = new gl.environmentsList.EnvironmentsComponent({
+ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
});
setTimeout(() => {
@@ -54,15 +49,30 @@ describe('Environment', () => {
});
});
- describe('with environments', () => {
+ describe('with paginated environments', () => {
const environmentsResponseInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([environment]), {
+ next(request.respondWith(JSON.stringify({
+ environments: [environment],
+ stopped_count: 1,
+ available_count: 0,
+ }), {
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(() => {
@@ -72,13 +82,6 @@ describe('Environment', () => {
});
it('should render a table with environments', (done) => {
- component = new gl.environmentsList.EnvironmentsComponent({
- el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
- });
-
setTimeout(() => {
expect(
component.$el.querySelectorAll('table tbody tr').length,
@@ -86,6 +89,59 @@ describe('Environment', () => {
done();
}, 0);
});
+
+ describe('pagination', () => {
+ it('should render pagination', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
+ done();
+ }, 0);
+ });
+
+ it('should update url when no search params are present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page is already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?scope=all&page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present and page is first param', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1&scope=all');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
+ done();
+ }, 0);
+ });
+ });
});
});
@@ -107,11 +163,8 @@ describe('Environment', () => {
});
it('should render empty state', (done) => {
- component = new gl.environmentsList.EnvironmentsComponent({
+ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
- propsData: {
- store: gl.environmentsList.EnvironmentsStore.create(),
- },
});
setTimeout(() => {
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
index 2dfce5ba824..5ca65b1debc 100644
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -1,4 +1,4 @@
-require('~/environments/components/environment_stop');
+const StopComponent = require('~/environments/components/environment_stop');
describe('Stop Component', () => {
preloadFixtures('static/environments/element.html.raw');
@@ -10,7 +10,7 @@ describe('Stop Component', () => {
loadFixtures('static/environments/element.html.raw');
stopURL = '/stop';
- component = new window.gl.environmentsList.StopComponent({
+ component = new StopComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
stopUrl: stopURL,
diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6
new file mode 100644
index 00000000000..be4330b5012
--- /dev/null
+++ b/spec/javascripts/environments/environment_table_spec.js.es6
@@ -0,0 +1,30 @@
+const EnvironmentTable = require('~/environments/components/environments_table');
+
+describe('Environment item', () => {
+ preloadFixtures('static/environments/element.html.raw');
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+ });
+
+ it('Should render a table', () => {
+ const mockItem = {
+ name: 'review',
+ size: 3,
+ isFolder: true,
+ latest: {
+ environment_path: 'url',
+ },
+ };
+
+ const component = new EnvironmentTable({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ environments: [{ mockItem }],
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ },
+ });
+
+ expect(component.$el.tagName).toEqual('TABLE');
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
index 9a8300d3832..77e182b3830 100644
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ b/spec/javascripts/environments/environments_store_spec.js.es6
@@ -1,70 +1,58 @@
-/* global environmentsList */
-
-require('~/environments/stores/environments_store');
-require('./mock_data');
+const Store = require('~/environments/stores/environments_store');
+const { environmentsList, serverData } = require('./mock_data');
(() => {
describe('Store', () => {
+ let store;
+
beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.create();
+ store = new Store();
});
it('should start with a blank state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0);
- expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0);
- expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0);
+ expect(store.state.environments.length).toEqual(0);
+ expect(store.state.stoppedCounter).toEqual(0);
+ expect(store.state.availableCounter).toEqual(0);
+ expect(store.state.paginationInformation).toEqual({});
});
- describe('store environments', () => {
- beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
- });
-
- it('should count stopped environments and save the count in the state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
- });
-
- it('should count available environments and save the count in the state', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
- });
-
- it('should store environments with same environment_type as sibilings', () => {
- expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
-
- const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
- .filter(env => env.children && env.children.length > 0);
-
- expect(parentFolder[0].children.length).toBe(2);
- expect(parentFolder[0].children[0].environment_type).toBe('review');
- expect(parentFolder[0].children[1].environment_type).toBe('review');
- expect(parentFolder[0].children[0].name).toBe('test-environment');
- expect(parentFolder[0].children[1].name).toBe('test-environment-1');
- });
-
- it('should sort the environments alphabetically', () => {
- const { environments } = gl.environmentsList.EnvironmentsStore.state;
-
- expect(environments[0].name).toBe('production');
- expect(environments[1].name).toBe('review');
- expect(environments[1].children[0].name).toBe('test-environment');
- expect(environments[1].children[1].name).toBe('test-environment-1');
- expect(environments[2].name).toBe('review_app');
- });
+ it('should store environments', () => {
+ store.storeEnvironments(serverData);
+ expect(store.state.environments.length).toEqual(serverData.length);
+ expect(store.state.environments[0]).toEqual(environmentsList[0]);
});
- describe('toggleFolder', () => {
- beforeEach(() => {
- gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
- });
-
- it('should toggle the open property for the given environment', () => {
- gl.environmentsList.EnvironmentsStore.toggleFolder('review');
+ it('should store available count', () => {
+ store.storeAvailableCount(2);
+ expect(store.state.availableCounter).toEqual(2);
+ });
- const { environments } = gl.environmentsList.EnvironmentsStore.state;
- const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
+ it('should store stopped count', () => {
+ store.storeStoppedCount(2);
+ expect(store.state.stoppedCounter).toEqual(2);
+ });
- expect(environment[0].isOpen).toBe(true);
- });
+ it('should store pagination information', () => {
+ const pagination = {
+ 'X-nExt-pAge': '2',
+ 'X-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '2',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ };
+
+ const expectedResult = {
+ perPage: 1,
+ page: 1,
+ total: 37,
+ totalPages: 2,
+ nextPage: 2,
+ previousPage: 2,
+ };
+
+ store.setPagination(pagination);
+ expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
})();
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
new file mode 100644
index 00000000000..d1335b5b304
--- /dev/null
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6
@@ -0,0 +1,202 @@
+const Vue = require('vue');
+require('~/flash');
+const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
+const { environmentsList } = require('../mock_data');
+
+describe('Environments Folder View', () => {
+ preloadFixtures('static/environments/environments_folder_view.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/environments_folder_view.html.raw');
+ window.history.pushState({}, null, 'environments/folders/build');
+ });
+
+ let component;
+
+ describe('successfull request', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: environmentsList,
+ stopped_count: 1,
+ available_count: 0,
+ }), {
+ 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 EnvironmentsFolderViewComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should render a table with environments', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('table tbody tr').length,
+ ).toEqual(2);
+ done();
+ }, 0);
+ });
+
+ it('should render available tab with count', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ ).toContain('Available');
+
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+
+ it('should render stopped tab with count', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ ).toContain('Stopped');
+
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ ).toContain('1');
+ done();
+ }, 0);
+ });
+
+ it('should render parent folder name', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-folder-name').textContent,
+ ).toContain('Environments / build');
+ done();
+ }, 0);
+ });
+
+ describe('pagination', () => {
+ it('should render pagination', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelectorAll('.gl-pagination li').length,
+ ).toEqual(5);
+ done();
+ }, 0);
+ });
+
+ it('should update url when no search params are present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page is already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?scope=all&page=1');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
+ done();
+ }, 0);
+ });
+
+ it('should update url when page and scope are already present and page is first param', (done) => {
+ spyOn(gl.utils, 'visitUrl');
+ window.history.pushState({}, null, '?page=1&scope=all');
+
+ setTimeout(() => {
+ component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const environmentsErrorResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsErrorResponseInterceptor,
+ );
+ });
+
+ it('should not render a table', (done) => {
+ component = new EnvironmentsFolderViewComponent({
+ el: document.querySelector('#environments-folder-list-view'),
+ });
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('table'),
+ ).toBe(null);
+ done();
+ }, 0);
+ });
+
+ it('should render available tab with count 0', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab').textContent,
+ ).toContain('Available');
+
+ expect(
+ component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+
+ it('should render stopped tab with count 0', (done) => {
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
+ ).toContain('Stopped');
+
+ expect(
+ component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
+ ).toContain('0');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 80e1cbc6f4d..5c395c6b2d8 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -1,153 +1,92 @@
-
const environmentsList = [
{
- id: 31,
- name: 'production',
+ name: 'DEV',
+ size: 1,
+ id: 7,
state: 'available',
- external_url: 'https://www.gitlab.com',
- environment_type: null,
- last_deployment: {
- id: 64,
- iid: 5,
- sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- ref: {
- name: 'master',
- ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
- },
- tag: false,
- 'last?': true,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit: {
- id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- short_id: '500aabcb',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2016-11-07T18:28:13.000+00:00',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- },
- deployable: {
- id: 1278,
- name: 'build',
- build_path: '/root/ci-folders/builds/1278',
- retry_path: '/root/ci-folders/builds/1278/retry',
- },
- manual_actions: [],
- },
- 'stop_action?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
- },
- {
- id: 32,
- name: 'review_app',
- state: 'stopped',
- external_url: 'https://www.gitlab.com',
+ external_url: null,
environment_type: null,
- last_deployment: {
- id: 64,
- iid: 5,
- sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- ref: {
- name: 'master',
- ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
- },
- tag: false,
- 'last?': true,
- user: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit: {
- id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- short_id: '500aabcb',
- title: 'Update .gitlab-ci.yml',
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- created_at: '2016-11-07T18:28:13.000+00:00',
- message: 'Update .gitlab-ci.yml',
- author: {
- name: 'Administrator',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
- },
- deployable: {
- id: 1278,
- name: 'build',
- build_path: '/root/ci-folders/builds/1278',
- retry_path: '/root/ci-folders/builds/1278/retry',
- },
- manual_actions: [],
- },
+ last_deployment: null,
'stop_action?': false,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
},
{
- id: 33,
- name: 'test-environment',
+ folderName: 'build',
+ size: 5,
+ id: 12,
+ name: 'build/update-README',
state: 'available',
- environment_type: 'review',
+ external_url: null,
+ environment_type: 'build',
last_deployment: null,
- 'stop_action?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
+ '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',
},
+];
+
+const serverData = [
{
- id: 34,
- name: 'test-environment-1',
- state: 'available',
- environment_type: 'review',
- last_deployment: null,
- 'stop_action?': true,
- environment_path: '/root/ci-folders/environments/31',
- created_at: '2016-11-07T11:11:16.525Z',
- updated_at: '2016-11-07T11:11:16.525Z',
+ name: 'DEV',
+ size: 1,
+ latest: {
+ id: 7,
+ name: 'DEV',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
+ },
+ },
+ {
+ name: 'build',
+ size: 5,
+ latest: {
+ 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',
+ },
},
];
-window.environmentsList = environmentsList;
-
const environment = {
- id: 4,
- name: 'production',
- state: 'available',
- external_url: 'http://production.',
- environment_type: null,
- last_deployment: {},
- 'stop_action?': false,
- environment_path: '/root/review-app/environments/4',
- stop_path: '/root/review-app/environments/4/stop',
- created_at: '2016-12-16T11:51:04.690Z',
- updated_at: '2016-12-16T12:04:51.133Z',
+ name: 'DEV',
+ size: 1,
+ latest: {
+ id: 7,
+ name: 'DEV',
+ state: 'available',
+ external_url: null,
+ environment_type: null,
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/7',
+ stop_path: '/root/review-app/environments/7/stop',
+ created_at: '2017-01-31T10:53:46.894Z',
+ updated_at: '2017-01-31T10:53:46.894Z',
+ },
};
-window.environment = environment;
+module.exports = {
+ environmentsList,
+ environment,
+ serverData,
+};
diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
new file mode 100644
index 00000000000..aceec139730
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml
@@ -0,0 +1,7 @@
+%div
+ #environments-folder-list-view{ data: { "can-create-deployment" => "true",
+ "can-read-environment" => "true",
+ "css-class" => "",
+ "commit-icon-svg" => custom_icon("icon_commit"),
+ "terminal-icon-svg" => custom_icon("icon_terminal"),
+ "play-icon-svg" => custom_icon("icon_play") } }
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 5b0b7aa7903..beb544468ef 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -105,6 +105,7 @@ require('~/issue');
expectIssueState(false);
expect($btnClose).toHaveProp('disabled', false);
+ expect($('.issue_counter')).toHaveText(0);
});
it('fails to close an issue with success:false', function() {
@@ -121,6 +122,7 @@ require('~/issue');
expectIssueState(true);
expect($btnClose).toHaveProp('disabled', false);
expectErrorMessage();
+ expect($('.issue_counter')).toHaveText(1);
});
it('fails to closes an issue with HTTP error', function() {
@@ -135,6 +137,7 @@ require('~/issue');
expectIssueState(true);
expect($btnClose).toHaveProp('disabled', true);
expectErrorMessage();
+ expect($('.issue_counter')).toHaveText(1);
});
});
@@ -159,6 +162,7 @@ require('~/issue');
expectIssueState(true);
expect($btnReopen).toHaveProp('disabled', false);
+ expect($('.issue_counter')).toHaveText(1);
});
});
}).call(this);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index 006ede21093..f4d3e77e515 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -108,6 +108,30 @@ require('~/lib/utils/common_utils');
});
});
+ describe('gl.utils.parseIntPagination', () => {
+ it('should parse to integers all string values and return pagination object', () => {
+ const pagination = {
+ 'X-PER-PAGE': 10,
+ 'X-PAGE': 2,
+ 'X-TOTAL': 30,
+ 'X-TOTAL-PAGES': 3,
+ 'X-NEXT-PAGE': 3,
+ 'X-PREV-PAGE': 1,
+ };
+
+ const expectedPagination = {
+ perPage: 10,
+ page: 2,
+ total: 30,
+ totalPages: 3,
+ nextPage: 3,
+ previousPage: 1,
+ };
+
+ expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination);
+ });
+ });
+
describe('gl.utils.isMetaClick', () => {
it('should identify meta click on Windows/Linux', () => {
const e = {
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
index e84f0dcfe67..dd495cb43bc 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
@@ -34,7 +34,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
it('should go to the previous page', () => {
@@ -55,7 +55,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
it('should go to the next page', () => {
@@ -76,7 +76,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
it('should go to the last page', () => {
@@ -97,7 +97,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
it('should go to the first page', () => {
@@ -118,7 +118,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
it('should do nothing', () => {
@@ -139,7 +139,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1);
- expect(changeChanges.two).toEqual('all');
+ expect(changeChanges.two).toEqual(null);
});
});
diff --git a/spec/lib/additional_email_headers_interceptor_spec.rb b/spec/lib/additional_email_headers_interceptor_spec.rb
new file mode 100644
index 00000000000..580450eef1e
--- /dev/null
+++ b/spec/lib/additional_email_headers_interceptor_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe AdditionalEmailHeadersInterceptor do
+ it 'adds Auto-Submitted header' do
+ mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
+
+ expect(mail.header['To'].value).to eq('test@mail.com')
+ expect(mail.header['From'].value).to eq('info@mail.com')
+ expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
+ expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
+ end
+end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
new file mode 100644
index 00000000000..168a59e5139
--- /dev/null
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RepoRestorer, services: true do
+ describe 'bundle a project Git repo' do
+ let(:user) { create(:user) }
+ let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
+ let!(:project) { create(:empty_project) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
+ let(:restorer) do
+ described_class.new(path_to_bundle: bundle_path,
+ shared: shared,
+ project: project)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ bundler.save
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
+
+ it 'restores the repo successfully' do
+ expect(restorer.restore).to be true
+ end
+
+ it 'has the webhooks' do
+ restorer.restore
+
+ expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 3d0d6735359..3eef10c0698 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -474,6 +474,20 @@ describe API::Commits, api: true do
expect(response).to have_http_status(401)
end
end
+
+ context 'when the commit is present on two projects' do
+ let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) }
+ let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') }
+
+ it 'returns the comments for the target project' do
+ get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['note']).to eq('a comment on a commit for fork')
+ expect(json_response.first['author']['id']).to eq(user2.id)
+ end
+ end
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 1b95f1ff198..6a6df377b35 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -181,6 +181,17 @@ describe EnvironmentSerializer do
expect(subject.first[:name]).to eq 'production'
expect(subject.second[:name]).to eq 'staging'
end
+
+ it 'appends correct total page count header' do
+ expect(subject).not_to be_empty
+ expect(response).to have_received(:[]=).with('X-Total', '3')
+ end
+
+ it 'appends correct page count headers' do
+ expect(subject).not_to be_empty
+ expect(response).to have_received(:[]=).with('X-Total-Pages', '2')
+ expect(response).to have_received(:[]=).with('X-Per-Page', '2')
+ end
end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 5a89acc96a4..d96f819e66a 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do
context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
- it 'saves error if there is an exception' do
- allow(service).to receive(:repository).and_raise("error message")
+ before do
+ allow(Rails.logger).to receive(:error)
+ end
+ it 'logs and saves error if there is an exception' do
+ error_message = 'error message'
+
+ allow(service).to receive(:repository).and_raise("error message")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("Something went wrong during merge: error message")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- it 'saves error if there is an PreReceiveError exception' do
- allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error")
+ it 'logs and saves error if there is an PreReceiveError exception' do
+ error_message = 'error message'
+ allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, error_message)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.merge_error).to eq("error")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- it 'aborts if there is a merge conflict' do
+ it 'logs and saves error if there is a merge conflict' do
+ error_message = 'Conflicts detected during merge'
+
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
- expect(merge_request.open?).to be_truthy
+ expect(merge_request).to be_open
expect(merge_request.merge_commit_sha).to be_nil
- expect(merge_request.merge_error).to eq("Conflicts detected during merge")
+ expect(merge_request.merge_error).to include(error_message)
+ expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 42cdc8b3fcd..99db6f61bcd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1,10 +1,12 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
+
+
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
-accepts@~1.3.3, accepts@1.3.3:
+accepts@1.3.3, accepts@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
dependencies:
@@ -23,14 +25,14 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
+acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
+
acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^4.0.3, acorn@^4.0.4, acorn@4.0.4:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
-
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -178,7 +180,11 @@ async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
-async@^1.4.0, async@^1.4.2, async@^1.5.2, async@1.x:
+async@0.2.x, async@~0.2.6:
+ version "0.2.10"
+ resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
+async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -188,10 +194,6 @@ async@^2.1.2, async@^2.1.4:
dependencies:
lodash "^4.14.0"
-async@~0.2.6, async@0.2.x:
- version "0.2.10"
- resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-
async@~0.9.0:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
@@ -995,7 +997,7 @@ center-align@^0.1.1:
align-text "^0.1.3"
lazy-cache "^1.0.3"
-chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies:
@@ -1140,14 +1142,6 @@ concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-concat-stream@^1.4.6:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
- dependencies:
- inherits "^2.0.3"
- readable-stream "^2.2.2"
- typedarray "^0.0.6"
-
concat-stream@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611"
@@ -1156,6 +1150,14 @@ concat-stream@1.5.0:
readable-stream "~2.0.0"
typedarray "~0.0.5"
+concat-stream@^1.4.6:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
connect-history-api-fallback@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
@@ -1263,16 +1265,16 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+d3@3.5.11:
+ version "3.5.11"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
+
d@^0.1.1, d@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
dependencies:
es5-ext "~0.10.2"
-d3@3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
-
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -1283,28 +1285,28 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
-debug@^2.1.1, debug@^2.2.0, debug@2.6.0:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
- dependencies:
- ms "0.7.2"
+debug@0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
-debug@~2.2.0, debug@2.2.0:
+debug@2.2.0, debug@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
dependencies:
ms "0.7.1"
-debug@0.7.4:
- version "0.7.4"
- resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
-
debug@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
dependencies:
ms "0.7.2"
+debug@2.6.0, debug@^2.1.1, debug@^2.2.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
+ dependencies:
+ ms "0.7.2"
+
decamelize@^1.0.0, decamelize@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -1382,7 +1384,7 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
-doctrine@^1.2.2, doctrine@1.5.0:
+doctrine@1.5.0, doctrine@^1.2.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
dependencies:
@@ -1545,7 +1547,7 @@ es6-set@~0.1.3:
es6-symbol "3"
event-emitter "~0.3.4"
-es6-symbol@~3.1, es6-symbol@~3.1.0, es6-symbol@3:
+es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
dependencies:
@@ -1697,7 +1699,7 @@ espree@^3.4.0:
acorn "4.0.4"
acorn-jsx "^3.0.0"
-esprima@^2.7.1, esprima@2.7.x:
+esprima@2.7.x, esprima@^2.7.1:
version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -2293,7 +2295,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@2, inherits@2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -2499,14 +2501,14 @@ is-windows@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
-isarray@^1.0.0, isarray@~1.0.0, isarray@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
-
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
isbinaryfile@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
@@ -2632,7 +2634,7 @@ jquery-ujs@1.2.1:
dependencies:
jquery ">=1.8.0"
-jquery@>=1.8.0, jquery@2.2.1:
+jquery@2.2.1, jquery@>=1.8.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
@@ -2644,7 +2646,7 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
-js-yaml@^3.5.1, js-yaml@^3.7.0, js-yaml@3.x:
+js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
dependencies:
@@ -2681,7 +2683,7 @@ json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
-json3@^3.3.2, json3@3.3.2:
+json3@3.3.2, json3@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
@@ -2721,6 +2723,12 @@ karma-jasmine@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf"
+karma-mocha-reporter@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.2.tgz#876de9a287244e54a608591732a98e66611f6abe"
+ dependencies:
+ chalk "1.1.3"
+
karma-phantomjs-launcher@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1"
@@ -2823,7 +2831,7 @@ loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5, loader-utils@0.2.x:
+loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies:
@@ -2985,7 +2993,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
dependencies:
mime-db "~1.26.0"
-mime@^1.3.4, mime@1.3.4:
+mime@1.3.4, mime@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -2993,25 +3001,19 @@ minimalistic-assert@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
-minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, "minimatch@2 || 3":
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
dependencies:
brace-expansion "^1.0.0"
-minimist@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
-
-minimist@~0.0.1, minimist@0.0.8:
+minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@0.5.x:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
- dependencies:
- minimist "0.0.8"
+minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
mkdirp@0.5.0:
version "0.5.0"
@@ -3019,6 +3021,12 @@ mkdirp@0.5.0:
dependencies:
minimist "0.0.8"
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
moment@2.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
@@ -3130,7 +3138,7 @@ node-zopfli@^2.0.0:
nan "^2.0.0"
node-pre-gyp "^0.6.4"
-nopt@~3.0.6, nopt@3.x:
+nopt@3.x, nopt@~3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
@@ -3166,14 +3174,14 @@ oauth-sign@~0.8.1:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
-object-assign@^4.0.1, object-assign@^4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
-
object-assign@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
@@ -3199,7 +3207,7 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
-once@^1.3.0, once@^1.4.0, once@1.x:
+once@1.x, once@^1.3.0, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
@@ -3480,22 +3488,18 @@ public-encrypt@^4.0.0:
parse-asn1 "^5.0.0"
randombytes "^2.0.1"
-punycode@^1.2.4, punycode@^1.4.1:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
-
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
-qs@~6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
-
qs@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
@@ -3504,6 +3508,10 @@ qs@6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
+qs@~6.3.0:
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -3743,11 +3751,11 @@ resolve-from@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
-resolve@^1.1.6, resolve@1.1.x:
+resolve@1.1.x:
version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
-resolve@^1.2.0:
+resolve@^1.1.6, resolve@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
@@ -3764,7 +3772,7 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4, rimraf@2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
version "2.5.4"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
dependencies:
@@ -3796,7 +3804,7 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
-semver@^5.3.0, semver@~5.3.0, "semver@2 || 3 || 4 || 5":
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -3963,7 +3971,7 @@ source-map-support@^0.4.2:
dependencies:
source-map "^0.5.3"
-source-map@^0.1.41, source-map@0.1.x:
+source-map@0.1.x, source-map@^0.1.41:
version "0.1.43"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
dependencies:
@@ -4063,10 +4071,6 @@ stream-http@^2.3.1:
to-arraybuffer "^1.0.0"
xtend "^4.0.0"
-string_decoder@^0.10.25, string_decoder@~0.10.x:
- version "0.10.31"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -4082,6 +4086,10 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
+string_decoder@^0.10.25, string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -4292,20 +4300,20 @@ underscore@1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
-unpipe@~1.0.0, unpipe@1.0.0:
+unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
-url-parse@^1.1.1:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
+url-parse@1.0.x:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
dependencies:
querystringify "0.0.x"
requires-port "1.0.x"
-url-parse@1.0.x:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
+url-parse@^1.1.1:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
dependencies:
querystringify "0.0.x"
requires-port "1.0.x"
@@ -4334,7 +4342,7 @@ util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
-util@^0.10.3, util@0.10.3:
+util@0.10.3, util@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies:
@@ -4494,6 +4502,10 @@ window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
wordwrap@^1.0.0, wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
@@ -4502,10 +4514,6 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
-wordwrap@0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
-
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
@@ -4588,4 +4596,3 @@ yauzl@2.4.1:
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
-