From 52c06647b58a8c78c29a79a5647962c065d9ef86 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 1 Nov 2016 15:38:44 -0500 Subject: [PATCH 001/302] Start adding environment info --- app/assets/stylesheets/pages/builds.scss | 7 +++++++ app/views/projects/builds/show.html.haml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f1d311cabbe..ebbec89c58c 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -40,6 +40,13 @@ margin-bottom: 10px; } } + + .environment-information { + background-color: $background-color; + border: 1px solid $border-color; + padding: 12px $gl-padding; + border-radius: $border-radius-default; + } } .build-header { diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index ae7a7ecb392..4614d0af906 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -26,6 +26,13 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page + - if @build.stage == 'deploy' + .prepend-top-default + .environment-information + = ci_icon_for_status(@build.status) + This build is the most recent deployment to + = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + .prepend-top-default - if @build.erased? .erased.alert.alert-warning From 73467bd1634898632d1c9c4e5879546ec9f53032 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 2 Nov 2016 17:25:19 -0500 Subject: [PATCH 002/302] Add switch statement & last_deployment method --- app/assets/stylesheets/pages/builds.scss | 6 ++++++ app/models/ci/build.rb | 6 ++++++ app/views/projects/builds/show.html.haml | 22 +++++++++++++++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index ebbec89c58c..dc694d67199 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -46,6 +46,12 @@ border: 1px solid $border-color; padding: 12px $gl-padding; border-radius: $border-radius-default; + + svg { + position: relative; + top: 1px; + margin-right: 5px; + } } } diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index bf5f92f8462..1b37c70ee4f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -125,6 +125,12 @@ module Ci !self.pipeline.statuses.latest.include?(self) end + def last_deployment + return @last_deployment if defined?(@last_deployment) + + @last_deployment = Deployment.where(deployable: self).order(id: :desc).last + end + def depends_on_builds # Get builds of the same type latest_builds = self.pipeline.builds.latest diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 4614d0af906..6741be25ace 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -26,12 +26,28 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page - - if @build.stage == 'deploy' + - if @build.deploy .prepend-top-default .environment-information = ci_icon_for_status(@build.status) - This build is the most recent deployment to - = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + + - if @build.last_deployment + This build is the most recent deployment to + = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + - else + - case @build.status + - when 'failed', 'canceled' + The deployment of this build to + = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + did not complete + - when 'pending', 'running' + This build is creating a deployment to + = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + and will overwrite the latest deployment + - when 'success' + This build is an out-of-date deployment to + = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + View the most recent deployment #24869 .prepend-top-default - if @build.erased? From c70acb57f5adcd4f0a19f8e7d5bcb356464a9c64 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 4 Nov 2016 16:35:22 +0100 Subject: [PATCH 003/302] Expose `last_deployment` on `Ci::Builds` [ci skip] --- app/helpers/environment_helper.rb | 21 +++++++++++++++ app/models/ci/build.rb | 10 ++++--- app/views/projects/builds/show.html.haml | 34 +++++++++++++----------- app/workers/build_success_worker.rb | 4 +-- 4 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 app/helpers/environment_helper.rb diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb new file mode 100644 index 00000000000..1e2a8c7ddbd --- /dev/null +++ b/app/helpers/environment_helper.rb @@ -0,0 +1,21 @@ +module EnvironmentHelper + def environment_for_build(project, build) + return unless build.environment + + environment_name = ExpandVariables.expand(build.environment, build.variables) + project.environments.find_by(name: environment_name) + end + + def environment_link_for_build(project, build) + environment = environment_for_build(project, build) + return unless environment + + link_to environment.name, namespace_project_environment_path(project.namespace, project, environment) + end + + def deployment_link(project, deployment) + return unless deployment + + link_to "##{deployment.id}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 1b37c70ee4f..09bbea1c653 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -7,6 +7,8 @@ module Ci belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' + has_many :deployments, as: :deployable + serialize :options serialize :yaml_variables @@ -125,10 +127,12 @@ module Ci !self.pipeline.statuses.latest.include?(self) end - def last_deployment - return @last_deployment if defined?(@last_deployment) + def deployable? + self.environment.present? + end - @last_deployment = Deployment.where(deployable: self).order(id: :desc).last + def last_deployment + deployments.order(id: :desc).last end def depends_on_builds diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 6741be25ace..18a336c3fa3 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -26,28 +26,30 @@ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do Runners page - - if @build.deploy + - if @build.deployable? .prepend-top-default .environment-information = ci_icon_for_status(@build.status) - - if @build.last_deployment - This build is the most recent deployment to - = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) - - else - - case @build.status - - when 'failed', 'canceled' + - last_deployment = @build.last_deployment + - if @build.complete? + - if @build.success? + - if last_deployment.try(:last?) + This build is the most recent deployment to + = environment_link_for_build(@build.project, @build) + - else + This build is an out-of-date deployment to + = environment_link_for_build(@build.project, @build) + View the most recent deployment + = deployment_link(@project, last_deployment) + - else The deployment of this build to - = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) + = environment_link_for_build(@build.project, @build) did not complete - - when 'pending', 'running' - This build is creating a deployment to - = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) - and will overwrite the latest deployment - - when 'success' - This build is an out-of-date deployment to - = link_to @build.environment, namespace_project_environment_path(@project.namespace, @project, @build.environment) - View the most recent deployment #24869 + - else + This build is creating a deployment to + = environment_link_for_build(@build.project, @build) + and will overwrite the latest deployment .prepend-top-default - if @build.erased? diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index e0ad5268664..1bc9745ecbc 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -4,15 +4,13 @@ class BuildSuccessWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| - create_deployment(build) + create_deployment(build) if build.deployable? end end private def create_deployment(build) - return if build.environment.blank? - service = CreateDeploymentService.new( build.project, build.user, environment: build.environment, From 1a5a3be84080808554568a8c61a80cc6f3f536ed Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 8 Nov 2016 16:29:55 -0600 Subject: [PATCH 004/302] First pass at tests --- app/views/projects/builds/show.html.haml | 15 +++--- .../projects/builds/show.html.haml_spec.rb | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 18a336c3fa3..43c6437f9b1 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -29,7 +29,10 @@ - if @build.deployable? .prepend-top-default .environment-information - = ci_icon_for_status(@build.status) + - if @build.status == 'success' && (!@build.last_deployment.try(:last?)) + = ci_icon_for_status('success_with_warnings') + - else + = ci_icon_for_status(@build.status) - last_deployment = @build.last_deployment - if @build.complete? @@ -38,18 +41,16 @@ This build is the most recent deployment to = environment_link_for_build(@build.project, @build) - else - This build is an out-of-date deployment to - = environment_link_for_build(@build.project, @build) - View the most recent deployment - = deployment_link(@project, last_deployment) + This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. View the most recent deployment #{deployment_link(@project, last_deployment)}. - else The deployment of this build to = environment_link_for_build(@build.project, @build) - did not complete + did not complete. - else This build is creating a deployment to = environment_link_for_build(@build.project, @build) - and will overwrite the latest deployment + and will overwrite the + = link_to "latest deployment.", deployment_link(@project, last_deployment) .prepend-top-default - if @build.erased? diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index da43622d3f9..0e702d80bd3 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -17,6 +17,56 @@ describe 'projects/builds/show' do allow(view).to receive(:can?).and_return(true) end + describe 'environment info in build view' do + context 'build with latest deployment' do + let(:build) { create(:ci_build, :success, environment: 'staging') } + let(:environment) { create(:environment, name: 'staging') } + let!(:deployment) { create(:deployment, deployable: build) } + + it 'shows deployment message' do + expect(rendered).to have_css('.environment-information', text: 'This build is the most recent deployment') + end + end + + context 'build with outdated deployment' do + let(:build) { create(:ci_build, :success, environment: 'staging', pipeline: pipeline) } + let(:environment) { create(:environment, name: 'staging', project: project) } + let!(:deployment) { create(:deployment, environment: environment, deployable: build) } + let!(:newer_deployment) { create(:deployment, environment: environment, deployable: build) } + + before do + assign(:build, build) + assign(:project, project) + + allow(view).to receive(:can?).and_return(true) + render + end + + it 'shows deployment message' do + expect(rendered).to have_css('.environment-information', text: "This build is an out-of-date deployment to #{environment.name}. View the most recent deployment #1") + end + end + + context 'build failed to deploy' do + let(:build) { create(:ci_build, :failed, environment: 'staging') } + let!(:environment) { create(:environment, name: 'staging') } + end + + context 'build will deploy' do + let(:build) { create(:ci_build, :running, environment: 'staging') } + let!(:environment) { create(:environment, name: 'staging') } + end + + context 'build that failed to deploy and environment has not been created' do + let(:build) { create(:ci_build, :failed, environment: 'staging') } + end + + context 'build that will deploy and environment has not been created' do + let(:build) { create(:ci_build, :running, environment: 'staging') } + let!(:environment) { create(:environment, name: 'staging') } + end + end + context 'when build is running' do before do build.run! From d7ba85c7496fb24625f3ebf3e78af42ec23e842e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 9 Nov 2016 19:40:25 +0100 Subject: [PATCH 005/302] Refine specs for build show page with environments --- spec/spec_helper.rb | 7 ++- .../projects/builds/show.html.haml_spec.rb | 57 ++++++++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 73cf4c9a24c..bead1a006d1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -26,10 +26,11 @@ RSpec.configure do |config| config.verbose_retry = true config.display_try_failure_messages = true - config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::ControllerHelpers, type: :view config.include Warden::Test::Helpers, type: :request - config.include LoginHelpers, type: :feature - config.include SearchHelpers, type: :feature + config.include LoginHelpers, type: :feature + config.include SearchHelpers, type: :feature config.include StubConfiguration config.include EmailHelpers config.include TestEnv diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 0e702d80bd3..98b68e730e6 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -1,15 +1,13 @@ require 'spec_helper' -describe 'projects/builds/show' do - include Devise::Test::ControllerHelpers - +describe 'projects/builds/show', :view do let(:project) { create(:project) } - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id) - end let(:build) { create(:ci_build, pipeline: pipeline) } + let(:pipeline) do + create(:ci_pipeline, project: project, sha: project.commit.id) + end + before do assign(:build, build) assign(:project, project) @@ -19,31 +17,48 @@ describe 'projects/builds/show' do describe 'environment info in build view' do context 'build with latest deployment' do - let(:build) { create(:ci_build, :success, environment: 'staging') } - let(:environment) { create(:environment, name: 'staging') } - let!(:deployment) { create(:deployment, deployable: build) } + let(:build) do + create(:ci_build, :success, environment: 'staging') + end + + before do + create(:environment, name: 'staging') + create(:deployment, deployable: build) + end it 'shows deployment message' do - expect(rendered).to have_css('.environment-information', text: 'This build is the most recent deployment') + expected_text = 'This build is the most recent deployment' + + render + + expect(rendered).to have_css( + '.environment-information', text: expected_text) end end context 'build with outdated deployment' do - let(:build) { create(:ci_build, :success, environment: 'staging', pipeline: pipeline) } - let(:environment) { create(:environment, name: 'staging', project: project) } - let!(:deployment) { create(:deployment, environment: environment, deployable: build) } - let!(:newer_deployment) { create(:deployment, environment: environment, deployable: build) } + let(:build) do + create(:ci_build, :success, environment: 'staging', pipeline: pipeline) + end - before do - assign(:build, build) - assign(:project, project) + let(:environment) do + create(:environment, name: 'staging', project: project) + end - allow(view).to receive(:can?).and_return(true) - render + let!(:first_deployment) do + create(:deployment, environment: environment, deployable: build) + end + + let!(:second_deployment) do + create(:deployment, environment: environment, deployable: build) end it 'shows deployment message' do - expect(rendered).to have_css('.environment-information', text: "This build is an out-of-date deployment to #{environment.name}. View the most recent deployment #1") + expected_text = 'This build is an out-of-date deployment ' \ + "to staging. View the most recent deployment ##{first_deployment.id}" + render + + expect(rendered).to have_css('.environment-information', text: expected_text) end end From 2b8292cd49dbc68b02f46f865b7115191bf2de07 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Wed, 9 Nov 2016 15:30:58 -0600 Subject: [PATCH 006/302] Finish specs for environment info --- app/views/projects/builds/show.html.haml | 13 ++-- .../projects/builds/show.html.haml_spec.rb | 68 ++++++++++++++++--- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 43c6437f9b1..488688a7d59 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -38,19 +38,14 @@ - if @build.complete? - if @build.success? - if last_deployment.try(:last?) - This build is the most recent deployment to - = environment_link_for_build(@build.project, @build) + This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - else This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. View the most recent deployment #{deployment_link(@project, last_deployment)}. - else - The deployment of this build to - = environment_link_for_build(@build.project, @build) - did not complete. + The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not complete. - else - This build is creating a deployment to - = environment_link_for_build(@build.project, @build) - and will overwrite the - = link_to "latest deployment.", deployment_link(@project, last_deployment) + This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} and will overwrite the | + = link_to "latest deployment.", deployment_link(@project, last_deployment) | .prepend-top-default - if @build.erased? diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 98b68e730e6..fa4d66bb6cb 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -28,7 +28,6 @@ describe 'projects/builds/show', :view do it 'shows deployment message' do expected_text = 'This build is the most recent deployment' - render expect(rendered).to have_css( @@ -63,22 +62,75 @@ describe 'projects/builds/show', :view do end context 'build failed to deploy' do - let(:build) { create(:ci_build, :failed, environment: 'staging') } - let!(:environment) { create(:environment, name: 'staging') } + let(:build) do + create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) + end + + let!(:environment) do + create(:environment, name: 'staging', project: project) + end + + it 'shows deployment message' do + expected_text = 'The deployment of this build to staging did not complete.' + render + + expect(rendered).to have_css( + '.environment-information', text: expected_text) + end end context 'build will deploy' do - let(:build) { create(:ci_build, :running, environment: 'staging') } - let!(:environment) { create(:environment, name: 'staging') } + let(:build) do + create(:ci_build, :running, environment: 'staging', pipeline: pipeline) + end + + let!(:environment) do + create(:environment, name: 'staging', project: project) + end + + it 'shows deployment message' do + expected_text = 'This build is creating a deployment to staging' + render + + expect(rendered).to have_css( + '.environment-information', text: expected_text) + end end context 'build that failed to deploy and environment has not been created' do - let(:build) { create(:ci_build, :failed, environment: 'staging') } + let(:build) do + create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) + end + + let!(:environment) do + create(:environment, name: 'staging', project: project) + end + + it 'shows deployment message' do + expected_text = 'The deployment of this build to staging did not complete' + render + + expect(rendered).to have_css( + '.environment-information', text: expected_text) + end end context 'build that will deploy and environment has not been created' do - let(:build) { create(:ci_build, :running, environment: 'staging') } - let!(:environment) { create(:environment, name: 'staging') } + let(:build) do + create(:ci_build, :running, environment: 'staging', pipeline: pipeline) + end + + let!(:environment) do + create(:environment, name: 'staging', project: project) + end + + it 'shows deployment message' do + expected_text = 'This build is creating a deployment to staging' + render + + expect(rendered).to have_css( + '.environment-information', text: expected_text) + end end end From ba1c5c058632322bdc7f4d65988001c6a44197f2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 10 Nov 2016 11:56:37 -0600 Subject: [PATCH 007/302] Remove toggle graph JS; put pipeline and builds in separate tabs --- app/assets/javascripts/pipelines.js.es6 | 18 +--- app/assets/stylesheets/pages/pipelines.scss | 3 + app/views/projects/commit/_pipeline.html.haml | 87 ++++++++++--------- 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index e6fada5c84c..a84db9c0233 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -3,26 +3,12 @@ class Pipelines { constructor() { - this.initGraphToggle(); this.addMarginToBuildColumns(); } - initGraphToggle() { - this.pipelineGraph = document.querySelector('.pipeline-graph'); - this.toggleButton = document.querySelector('.toggle-pipeline-btn'); - this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text'); - this.toggleButton.addEventListener('click', this.toggleGraph.bind(this)); - } - - toggleGraph() { - const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed'); - this.toggleButton.classList.toggle('graph-collapsed'); - this.pipelineGraph.classList.toggle('graph-collapsed'); - this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand'; - } - addMarginToBuildColumns() { - const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)'); + this.pipelineGraph = document.querySelector('.pipeline-graph'); + const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)'); for (buildNodeIndex in secondChildBuildNodes) { const buildNode = secondChildBuildNodes[buildNodeIndex]; const firstChildBuildNode = buildNode.previousElementSibling; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index bf3cb6e7ad9..e1fe2c04a76 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -304,6 +304,8 @@ .pipeline-graph { width: 100%; + background-color: $background-color; + padding: $gl-padding; overflow: auto; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; @@ -367,6 +369,7 @@ .build { border: 1px solid $border-color; + background-color: $white-light; position: relative; padding: 7px 10px 8px; border-radius: 30px; diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 062a8905a19..cf3ed5c64f7 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,10 +1,6 @@ .pipeline-graph-container .row-content-block.build-content.middle-block.pipeline-actions .pull-right - %button.btn.btn-grouped.btn-white.toggle-pipeline-btn - %span.toggle-btn-text Hide - %span pipeline graph - %span.caret - if can?(current_user, :update_pipeline, pipeline.project) - if pipeline.builds.latest.failed.any?(&:retryable?) = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post @@ -28,43 +24,54 @@ in = time_interval_in_words pipeline.duration - .row-content-block.build-content.middle-block.pipeline-graph.hidden - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + .tabs-holder + %ul.nav-links.no-top.no-bottom + %li.active + = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' } + %span.badge + %li + = link_to "Builds", "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' } + %span.badge + .tab-content + #js-tab-pipeline.tab-pane.active + .build-content.middle-block.pipeline-graph + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses -- if pipeline.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - pipeline.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + #js-tab-builds.tab-pane + - if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} -- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit + - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit -.table-holder.pipeline-holder - %table.table.ci-table.pipeline - %thead - %tr - %th Status - %th Build ID - %th Name - %th - - if pipeline.project.build_coverage_enabled? - %th Coverage - %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) From eb1b55132deadaed07ea61dcc461625b8db3c9d9 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 10 Nov 2016 13:48:20 -0600 Subject: [PATCH 008/302] Move reusable well styles into their own file --- app/assets/stylesheets/framework.scss | 1 + .../stylesheets/framework/variables.scss | 4 +- app/assets/stylesheets/framework/wells.scss | 45 ++++++++++++++++++ app/assets/stylesheets/pages/commit.scss | 46 ------------------- .../stylesheets/pages/merge_requests.scss | 2 +- .../projects/commit/_commit_box.html.haml | 6 +-- 6 files changed, 52 insertions(+), 52 deletions(-) create mode 100644 app/assets/stylesheets/framework/wells.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index d5cca1b10fb..84b01dd34a6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -39,3 +39,4 @@ @import "framework/typography.scss"; @import "framework/zen.scss"; @import "framework/blank"; +@import "framework/wells.scss"; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e0d00759c9c..831bbb2a1e3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -90,8 +90,8 @@ $table-border-color: #f0f0f0; $background-color: $gray-light; $dark-background-color: #f5f5f5; $table-text-gray: #8f8f8f; -$widget-expand-item: #e8f2f7; -$widget-inner-border: #eef0f2; +$well-expand-item: #e8f2f7; +$well-inner-border: #eef0f2; /* * Text diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss new file mode 100644 index 00000000000..192939f4527 --- /dev/null +++ b/app/assets/stylesheets/framework/wells.scss @@ -0,0 +1,45 @@ +.info-well { + background: $background-color; + color: $gl-gray; + border: 1px solid $border-color; + border-radius: $border-radius-default; + + .well-segment { + padding: $gl-padding; + + &:not(:last-of-type) { + border-bottom: 1px solid $well-inner-border; + } + + &.branch-info { + .monospace, + .commit-info { + margin-left: 4px; + } + } + } + + .icon-container { + display: inline-block; + margin-right: 8px; + + svg { + position: relative; + top: 2px; + height: 16px; + width: 16px; + } + + &.commit-icon { + svg { + path { + fill: $gl-text-color; + } + } + } + } + + .label.label-gray { + background-color: $well-expand-item; + } +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 47d3e72679b..1424ba01eaf 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -117,52 +117,6 @@ } } -.commit-info-widget { - background: $background-color; - color: $gl-gray; - border: 1px solid $border-color; - border-radius: $border-radius-default; - - .widget-row { - padding: $gl-padding; - - &:not(:last-of-type) { - border-bottom: 1px solid $widget-inner-border; - } - - &.branch-info { - .monospace, - .commit-info { - margin-left: 4px; - } - } - } - - .icon-container { - display: inline-block; - margin-right: 8px; - - svg { - position: relative; - top: 2px; - height: 16px; - width: 16px; - } - - &.commit-icon { - svg { - path { - fill: $gl-text-color; - } - } - } - } - - .label.label-gray { - background-color: $widget-expand-item; - } -} - .ci-status-link { svg { overflow: visible; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index f8e31a624ec..3faccc546e5 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -60,7 +60,7 @@ } .ci_widget { - border-bottom: 1px solid $widget-inner-border; + border-bottom: 1px solid $well-inner-border; svg { margin-right: 4px; diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 0ebc38d16cf..bc48f67d140 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -55,8 +55,8 @@ %pre.commit-description = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) -.commit-info-widget - .widget-row.branch-info +.info-well + .well-segment.branch-info .icon-container.commit-icon = custom_icon("icon_commit") %span.cgray= pluralize(@commit.parents.count, "parent") @@ -66,7 +66,7 @@ %i.fa.fa-spinner.fa-spin - if @commit.status - .widget-row.pipeline-info + .well-segment.pipeline-info .icon-container = ci_icon_for_status(@commit.status) Pipeline From 3c04dc2020d261a6db7fcb33800d1f22d9b62098 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 10 Nov 2016 16:53:32 -0600 Subject: [PATCH 009/302] Rearrange and style items on pipeline page --- app/assets/stylesheets/framework/buttons.scss | 4 ++ app/assets/stylesheets/pages/builds.scss | 16 ++--- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/commit/_pipeline.html.haml | 39 +++++------- app/views/projects/pipelines/_info.html.haml | 63 ++++++++++--------- app/views/projects/pipelines/show.html.haml | 6 +- .../unreleased/23637-title-bar-pipelines.yml | 4 ++ 7 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 changelogs/unreleased/23637-title-bar-pipelines.yml diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e7aff2d0cec..f7d681535dc 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -345,6 +345,10 @@ } } +.retry-build-btn { + @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); +} + @media (max-width: $screen-xs-max) { .btn-wide-on-xs { width: 100%; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f1d311cabbe..5320f3aba66 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -49,10 +49,6 @@ min-height: 58px; align-items: center; - .btn-inverted { - @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); - } - @media (max-width: $screen-sm-max) { padding-right: 40px; @@ -63,14 +59,14 @@ .header-content { flex: 1; - } - a { - color: $gl-gray; + a { + color: $gl-gray; - &:hover { - color: $gl-link-color; - text-decoration: none; + &:hover { + color: $gl-link-color; + text-decoration: none; + } } } diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 3f2ce7377fd..fea7b975df5 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -14,6 +14,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post + = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn retry-build-btn pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index cf3ed5c64f7..859909d3600 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,28 +1,19 @@ .pipeline-graph-container - .row-content-block.build-content.middle-block.pipeline-actions - .pull-right - - if can?(current_user, :update_pipeline, pipeline.project) - - if pipeline.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post - - - if pipeline.builds.running_or_pending.any? - = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" - with - = pluralize pipeline.statuses.count(:id), "build" - - if pipeline.ref - for - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" - - if pipeline.duration - in - = time_interval_in_words pipeline.duration + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration .tabs-holder %ul.nav-links.no-top.no-bottom diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index d288efc546f..e44c6351f46 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,39 +1,44 @@ -%p -.commit-info-row - Pipeline - = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace" - with - = pluralize @pipeline.statuses.count(:id), "build" - - if @pipeline.ref - for - = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" - - if @pipeline.duration - in - = time_interval_in_words(@pipeline.duration) - - if @pipeline.queued_duration - = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" - - .pull-right +.commit-info-row.commit-info-row-header + .commit-meta = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do = ci_icon_for_status(@pipeline.status) = ci_label_for_status(@pipeline.status) + %strong Pipeline ##{@commit.pipelines.last.id} + triggered #{time_ago_with_tooltip(@commit.authored_date)} by + = author_avatar(@commit, size: 24) + = commit_author_link(@commit) + .commit-action-buttons + - if can?(current_user, :update_pipeline, @pipeline.project) + - if @pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-grouped retry-build-btn', method: :post + - if @pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - if @commit - .commit-info-row - %span.light Authored by - %strong - = commit_author_link(@commit, avatar: true, size: 24) - #{time_ago_with_tooltip(@commit.authored_date)} - -.commit-info-row - %span.light Commit - = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" - = clipboard_button(clipboard_text: @pipeline.sha) - -- if @commit - .commit-box.content-block + .commit-box %h3.commit-title = markdown(@commit.title, pipeline: :single_line) - if @commit.description.present? %pre.commit-description = preserve(markdown(@commit.description, pipeline: :single_line)) + +.info-well + - if @commit.status + .well-segment.pipeline-info + .icon-container + = ci_icon_for_status(@commit.status) + = pluralize @pipeline.statuses.count(:id), "build" + - if @pipeline.ref + for + = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" + - if @pipeline.duration + in + = time_interval_in_words(@pipeline.duration) + - if @pipeline.queued_duration + = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" + + .well-segment.branch-info + .icon-container.commit-icon + = custom_icon("icon_commit") + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" + = clipboard_button(clipboard_text: @pipeline.sha) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index 688535ad764..c729e18f1e2 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -3,9 +3,7 @@ = render "projects/pipelines/head" %div{ class: container_class } - .prepend-top-default - - if @commit - = render "projects/pipelines/info" - %div.block-connector + - if @commit + = render "projects/pipelines/info" = render "projects/commit/pipeline", pipeline: @pipeline diff --git a/changelogs/unreleased/23637-title-bar-pipelines.yml b/changelogs/unreleased/23637-title-bar-pipelines.yml new file mode 100644 index 00000000000..4dfd9e8cfb9 --- /dev/null +++ b/changelogs/unreleased/23637-title-bar-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: 23637-title-bar-pipelines +merge_request: +author: From 775c5aab757d1f0caa7c80566e123be2e1f20526 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 14 Nov 2016 10:10:07 -0600 Subject: [PATCH 010/302] Remove builds tab from merge requests and commits --- app/views/projects/commit/_ci_menu.html.haml | 4 ---- app/views/projects/commit/builds.html.haml | 9 --------- app/views/projects/merge_requests/_show.html.haml | 6 ------ 3 files changed, 19 deletions(-) delete mode 100644 app/views/projects/commit/builds.html.haml diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index cbfd99ca448..13ab2253733 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -8,7 +8,3 @@ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Pipelines %span.badge= @ci_pipelines.count - = nav_link(path: 'commit#builds') do - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do - Builds - %span.badge= @statuses.count diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml deleted file mode 100644 index 077b2d2725b..00000000000 --- a/app/views/projects/commit/builds.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- @no_container = true -- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" -= render "projects/commits/head" - -%div{ class: container_class } - = render "commit_box" - - = render "ci_menu" - = render "builds" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f57abe73977..1111a5b9fd8 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -64,10 +64,6 @@ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do Pipelines %span.badge= @pipelines.size - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses.size %li.diffs-tab = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do Changes @@ -95,8 +91,6 @@ #commits.commits.tab-pane - # This tab is always loaded via AJAX - #builds.builds.tab-pane - - # This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - # This tab is always loaded via AJAX #diffs.diffs.tab-pane From 144002249e500922f422b2095f91f35d8a3addb5 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Mon, 14 Nov 2016 14:03:44 -0600 Subject: [PATCH 011/302] Make pipeline page header responsive; add build counter; add short sha --- app/assets/stylesheets/framework.scss | 1 + .../stylesheets/framework/page-header.scss | 67 +++++++++++ app/assets/stylesheets/pages/commit.scss | 96 ++------------- .../projects/commit/_commit_box.html.haml | 7 +- app/views/projects/commit/_pipeline.html.haml | 111 ++++++++---------- app/views/projects/commits/_commit.html.haml | 7 +- app/views/projects/pipelines/_info.html.haml | 21 ++-- 7 files changed, 146 insertions(+), 164 deletions(-) create mode 100644 app/assets/stylesheets/framework/page-header.scss diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 84b01dd34a6..7c7f991dd87 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -40,3 +40,4 @@ @import "framework/zen.scss"; @import "framework/blank"; @import "framework/wells.scss"; +@import "framework/page-header.scss"; diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss new file mode 100644 index 00000000000..85c1385d5d9 --- /dev/null +++ b/app/assets/stylesheets/framework/page-header.scss @@ -0,0 +1,67 @@ +.page-content-header { + line-height: 34px; + padding: 10px 0; + margin-bottom: 0; + + @media (min-width: $screen-sm-min) { + display: flex; + align-items: center; + + .header-main-content { + flex: 1; + } + } + + .header-action-buttons { + i { + color: $gl-icon-color; + font-size: 13px; + margin-right: 3px; + } + + @media (max-width: $screen-xs-max) { + .btn { + width: 100%; + margin-top: 10px; + } + + .dropdown { + width: 100%; + } + } + } + + .avatar { + @extend .avatar-inline; + margin-left: 0; + + @media (min-width: $screen-sm-min) { + margin-left: 4px; + } + } + + .commit-committer-link, + .commit-author-link { + color: $gl-gray; + font-weight: bold; + } + + .fa-clipboard { + color: $dropdown-title-btn-color; + } + + .commit-info { + &.branches { + margin-left: 8px; + } + } + + .ci-status-link { + + svg { + position: relative; + top: 2px; + margin: 0 2px 0 3px; + } + } +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 1424ba01eaf..ddc9d0e2b1a 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -26,91 +26,6 @@ white-space: pre-wrap; } -.commit-info-row { - margin-bottom: 10px; - line-height: 24px; - padding-top: 6px; - - &.commit-info-row-header { - line-height: 34px; - padding: 10px 0; - margin-bottom: 0; - - @media (min-width: $screen-sm-min) { - display: flex; - align-items: center; - - .commit-meta { - flex: 1; - } - } - - .commit-hash-full { - @media (max-width: $screen-sm-max) { - width: 80px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: bottom; - } - } - - .commit-action-buttons { - i { - color: $gl-icon-color; - font-size: 13px; - margin-right: 3px; - } - - @media (max-width: $screen-xs-max) { - .dropdown { - width: 100%; - margin-top: 10px; - } - - .dropdown-toggle { - width: 100%; - } - } - } - } - - .avatar { - @extend .avatar-inline; - margin-left: 0; - - @media (min-width: $screen-sm-min) { - margin-left: 4px; - } - } - - .commit-committer-link, - .commit-author-link { - color: $gl-gray; - font-weight: bold; - } - - .fa-clipboard { - color: $dropdown-title-btn-color; - } - - .commit-info { - &.branches { - margin-left: 8px; - } - } - - .ci-status-link { - - svg { - position: relative; - top: 2px; - margin: 0 2px 0 3px; - } - } -} - .js-details-expand { &:hover { text-decoration: none; @@ -138,6 +53,17 @@ } } +.commit-hash-full { + @media (max-width: $screen-sm-max) { + width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } +} + .file-stats { ul { list-style: none; diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index bc48f67d140..503cbd13b5e 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,5 +1,5 @@ -.commit-info-row.commit-info-row-header - .commit-meta +.page-content-header + .header-main-content %strong Commit %strong.monospace.js-details-short= @commit.short_id = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do @@ -19,7 +19,8 @@ %strong = commit_committer_link(@commit, avatar: true, size: 24) #{time_ago_with_tooltip(@commit.committed_date)} - .commit-action-buttons + + .header-action-buttons - if defined?(@notes_count) && @notes_count > 0 %span.btn.disabled.btn-grouped.hidden-xs.append-right-10 = icon('comment') diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 859909d3600..4d809ca54a3 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,68 +1,51 @@ -.pipeline-graph-container - .oneline.clearfix - - if defined?(pipeline_details) && pipeline_details - Pipeline - = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" - with - = pluralize pipeline.statuses.count(:id), "build" - - if pipeline.ref - for - = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" - - if defined?(link_to_commit) && link_to_commit - for commit - = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" - - if pipeline.duration - in - = time_interval_in_words pipeline.duration +.tabs-holder + %ul.nav-links.no-top.no-bottom + %li.active + = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' } + %li + = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' } do + Builds + %span.badge= pipeline.statuses.count - .tabs-holder - %ul.nav-links.no-top.no-bottom - %li.active - = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' } - %span.badge - %li - = link_to "Builds", "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' } - %span.badge +.tab-content + #js-tab-pipeline.tab-pane.active + .build-content.middle-block.pipeline-graph + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses - .tab-content - #js-tab-pipeline.tab-pane.active - .build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + #js-tab-builds.tab-pane + - if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} - #js-tab-builds.tab-pane - - if pipeline.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - pipeline.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit - - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - - .table-holder.pipeline-holder - %table.table.ci-table.pipeline - %thead - %tr - %th Status - %th Build ID - %th Name - %th - - if pipeline.project.build_coverage_enabled? - %th Coverage - %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 34855c54176..12096941209 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -36,7 +36,6 @@ %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) - .commit-row-info - = commit_author_link(commit, avatar: false, size: 24) - authored - #{time_ago_with_tooltip(commit.committed_date)} + = commit_author_link(commit, avatar: false, size: 24) + authored + #{time_ago_with_tooltip(commit.committed_date)} diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index e44c6351f46..decbbd80ac5 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,5 +1,5 @@ -.commit-info-row.commit-info-row-header - .commit-meta +.page-content-header + .header-main-content = link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do = ci_icon_for_status(@pipeline.status) = ci_label_for_status(@pipeline.status) @@ -7,12 +7,12 @@ triggered #{time_ago_with_tooltip(@commit.authored_date)} by = author_avatar(@commit, size: 24) = commit_author_link(@commit) - .commit-action-buttons + .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - if @pipeline.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-grouped retry-build-btn', method: :post + = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn retry-build-btn', method: :post - if @pipeline.builds.running_or_pending.any? - = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - if @commit .commit-box @@ -29,7 +29,7 @@ = ci_icon_for_status(@commit.status) = pluralize @pipeline.statuses.count(:id), "build" - if @pipeline.ref - for + from = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" - if @pipeline.duration in @@ -40,5 +40,10 @@ .well-segment.branch-info .icon-container.commit-icon = custom_icon("icon_commit") - = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace" - = clipboard_button(clipboard_text: @pipeline.sha) + = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short" + = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do + %span.text-expander + \... + %span.js-details-content.hide + = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full" + = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard") From 5ac2c5839ac47d39b962cbfd236a61eadd5eb2c6 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Tue, 15 Nov 2016 13:24:25 -0600 Subject: [PATCH 012/302] Create separate view for pipeline view with tabs --- app/views/projects/commit/_ci_menu.html.haml | 4 + app/views/projects/commit/_pipeline.html.haml | 109 ++++++++++-------- app/views/projects/commit/builds.html.haml | 9 ++ .../projects/merge_requests/_show.html.haml | 6 + .../projects/pipelines/_with_tabs.html.haml | 51 ++++++++ app/views/projects/pipelines/show.html.haml | 2 +- .../unreleased/23637-title-bar-pipelines.yml | 6 +- spec/features/projects/pipelines_spec.rb | 5 +- 8 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 app/views/projects/commit/builds.html.haml create mode 100644 app/views/projects/pipelines/_with_tabs.html.haml diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 13ab2253733..cbfd99ca448 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -8,3 +8,7 @@ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Pipelines %span.badge= @ci_pipelines.count + = nav_link(path: 'commit#builds') do + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + Builds + %span.badge= @statuses.count diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 4d809ca54a3..1174158eb65 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,51 +1,66 @@ -.tabs-holder - %ul.nav-links.no-top.no-bottom - %li.active - = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' } - %li - = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' } do - Builds - %span.badge= pipeline.statuses.count +.pipeline-graph-container + .row-content-block.build-content.middle-block.pipeline-actions + .pull-right + - if can?(current_user, :update_pipeline, pipeline.project) + - if pipeline.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post -.tab-content - #js-tab-pipeline.tab-pane.active - .build-content.middle-block.pipeline-graph - .pipeline-visualization - %ul.stage-column-list - - stages = pipeline.stages_with_latest_statuses - - stages.each do |stage, statuses| - %li.stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - %ul - = render "projects/commit/pipeline_stage", statuses: statuses + - if pipeline.builds.running_or_pending.any? + = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post - #js-tab-builds.tab-pane - - if pipeline.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - pipeline.yaml_errors.split(",").each do |error| - %li= error - You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + .oneline.clearfix + - if defined?(pipeline_details) && pipeline_details + Pipeline + = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" + with + = pluralize pipeline.statuses.count(:id), "build" + - if pipeline.ref + for + = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace" + - if pipeline.duration + in + = time_interval_in_words pipeline.duration - - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit + .row-content-block.build-content.middle-block.pipeline-graph.hidden + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses - .table-holder.pipeline-holder - %table.table.ci-table.pipeline - %thead - %tr - %th Status - %th Build ID - %th Name - %th - - if pipeline.project.build_coverage_enabled? - %th Coverage - %th - - pipeline.statuses.relevant.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) + +- if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + +- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml new file mode 100644 index 00000000000..077b2d2725b --- /dev/null +++ b/app/views/projects/commit/builds.html.haml @@ -0,0 +1,9 @@ +- @no_container = true +- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits" += render "projects/commits/head" + +%div{ class: container_class } + = render "commit_box" + + = render "ci_menu" + = render "builds" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1111a5b9fd8..f57abe73977 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -64,6 +64,10 @@ = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do Pipelines %span.badge= @pipelines.size + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do + Builds + %span.badge= @statuses.size %li.diffs-tab = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do Changes @@ -91,6 +95,8 @@ #commits.commits.tab-pane - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - # This tab is always loaded via AJAX #diffs.diffs.tab-pane diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml new file mode 100644 index 00000000000..4d809ca54a3 --- /dev/null +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -0,0 +1,51 @@ +.tabs-holder + %ul.nav-links.no-top.no-bottom + %li.active + = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' } + %li + = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' } do + Builds + %span.badge= pipeline.statuses.count + +.tab-content + #js-tab-pipeline.tab-pane.active + .build-content.middle-block.pipeline-graph + .pipeline-visualization + %ul.stage-column-list + - stages = pipeline.stages_with_latest_statuses + - stages.each do |stage, statuses| + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + = render "projects/commit/pipeline_stage", statuses: statuses + + #js-tab-builds.tab-pane + - if pipeline.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - pipeline.yaml_errors.split(",").each do |error| + %li= error + You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path} + + - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + + .table-holder.pipeline-holder + %table.table.ci-table.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index c729e18f1e2..8c6652a5f90 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -6,4 +6,4 @@ - if @commit = render "projects/pipelines/info" - = render "projects/commit/pipeline", pipeline: @pipeline + = render "projects/pipelines/with_tabs", pipeline: @pipeline diff --git a/changelogs/unreleased/23637-title-bar-pipelines.yml b/changelogs/unreleased/23637-title-bar-pipelines.yml index 4dfd9e8cfb9..3d4cf88c54c 100644 --- a/changelogs/unreleased/23637-title-bar-pipelines.yml +++ b/changelogs/unreleased/23637-title-bar-pipelines.yml @@ -1,4 +1,4 @@ --- -title: 23637-title-bar-pipelines -merge_request: -author: +title: Redesign pipelines page +merge_request: +author: diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb index db56a50e058..77184ede784 100644 --- a/spec/features/projects/pipelines_spec.rb +++ b/spec/features/projects/pipelines_spec.rb @@ -156,7 +156,10 @@ describe "Pipelines" do @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } + before do + visit namespace_project_pipeline_path(project.namespace, project, pipeline) + find('.builds-tab').click + end it 'shows a list of builds' do expect(page).to have_content('Test') From 4df7c9edef9c51d2ba45f09ad7a1d2d5e1686ba3 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 14 Oct 2016 13:14:18 +0100 Subject: [PATCH 013/302] Starts Vue view for environmnets List --- .../components/environments_tree_view.js.es6 | 0 .../environments/environments_bundle.js.es6 | 49 +++++++++++++++++++ .../services/environments_service.js.es6 | 19 +++++++ .../stores/environmnets_store.js.es6 | 21 ++++++++ 4 files changed, 89 insertions(+) create mode 100644 app/assets/javascripts/environments/components/environments_tree_view.js.es6 create mode 100644 app/assets/javascripts/environments/environments_bundle.js.es6 create mode 100644 app/assets/javascripts/environments/services/environments_service.js.es6 create mode 100644 app/assets/javascripts/environments/stores/environmnets_store.js.es6 diff --git a/app/assets/javascripts/environments/components/environments_tree_view.js.es6 b/app/assets/javascripts/environments/components/environments_tree_view.js.es6 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 new file mode 100644 index 00000000000..1708fd43d27 --- /dev/null +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -0,0 +1,49 @@ +//= require vue +//= require vue-resource +//= require_tree ./stores +//= require_tree ./services + + +$(() => { + + const $environmentsListApp = document.getElementById('environments-list-view'); + const Store = gl.environmentsList.EnvironmentsStore; + + window.gl = window.gl || {}; + + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); + } + + gl.EnvironmentsListApp = new Vue({ + + el: $environmentsListApp, + + components: { + 'tree-view': gl.environmentsList.TreeView + }, + + data: { + endpoint: $environmentsListApp.dataset.endpoint, + loading: true + }, + + init: Store.create.bind(Store), + + created() { + gl.environmentsService = new EnvironmentsService(this.endpoint); + }, + + /** + * Fetches all the environmnets and stores them. + * Toggles loading property. + */ + ready() { + gl.environmentsService.all().then((resp) => { + Store.addEnvironments(resp.json()); + + this.loading = false; + }); + } + }); +}); \ No newline at end of file diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 new file mode 100644 index 00000000000..179c0807cea --- /dev/null +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -0,0 +1,19 @@ +class EnvironmentsService { + + constructor (root) { + Vue.http.options.root = root; + + debugger; + + this.environments = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + next(); + }); + } + + all () { + return this.environments.get(); + } +}; \ No newline at end of file diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 new file mode 100644 index 00000000000..879719997e2 --- /dev/null +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -0,0 +1,21 @@ +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.EnvironmentsStore = { + + create () { + + }, + + /** + * Stores the received environmnets. + * + * @param {Array} environments List of environments + * @return {type} + */ + addEnvironments(environments) { + console.log(environments); + } + } +})(); \ No newline at end of file From 235213dc68b7eeec8527347f7bc304d7765b4ff0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 14 Oct 2016 13:14:41 +0100 Subject: [PATCH 014/302] Modifies controller to format data to json --- app/controllers/projects/environments_controller.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index ea22b2dcc15..9255388eb84 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,6 +15,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController else @all_environments.available end + + respond_to do |format| + format.html + format.json do + render json: @environments + end + end end def show From 2c0f97cd1e99abdf7c80e2c79ed55b321bf5fb7b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 14 Oct 2016 13:15:29 +0100 Subject: [PATCH 015/302] Includes page specific JS --- app/helpers/environments_helper.rb | 7 +++++++ app/views/projects/environments/index.html.haml | 7 +++++-- config/application.rb | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 app/helpers/environments_helper.rb diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb new file mode 100644 index 00000000000..d9a3106f706 --- /dev/null +++ b/app/helpers/environments_helper.rb @@ -0,0 +1,7 @@ +module EnvironmentsHelper + def environments_list_data + { + endpoint: namespace_project_environments_path(@project.namespace, @project) + } + end +end \ No newline at end of file diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8f555afcf11..a2526209df2 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -2,6 +2,9 @@ - page_title "Environments" = render "projects/pipelines/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('environments/environments_bundle.js') + %div{ class: container_class } .top-area %ul.nav-links @@ -22,7 +25,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .environments-container + .environments-container#environments-list-view{ "v-cloak" => true, data: environments_list_data } - if @all_environments.blank? .blank-state.blank-state-no-icon %h2.blank-state-title @@ -45,4 +48,4 @@ %th Commit %th %th.hidden-xs - = render @environments + / = render @environments diff --git a/config/application.rb b/config/application.rb index 946b632b0e8..fb84870dfbd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -94,6 +94,7 @@ module Gitlab config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" + config.assets.precompile << "environments/environments_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "lib/utils/*.js" From 52c4f8ef648db49b1b8260e9239844faf6198972 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 14 Oct 2016 16:45:57 +0100 Subject: [PATCH 016/302] Stores environmnets in tree --- .../environments/environments_bundle.js.es6 | 2 +- .../stores/environmnets_store.js.es6 | 53 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 1708fd43d27..783940c8f63 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -40,7 +40,7 @@ $(() => { */ ready() { gl.environmentsService.all().then((resp) => { - Store.addEnvironments(resp.json()); + Store.storeEnvironments(resp.json()); this.loading = false; }); diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 879719997e2..911dd3e9352 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -3,19 +3,58 @@ window.gl.environmentsList = window.gl.environmentsList || {}; gl.environmentsList.EnvironmentsStore = { + state: {}, create () { - + this.state.environments = []; }, /** - * Stores the received environmnets. + * 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. * + * @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"}, + * { name: "environment_2", environment_type: "review"} + * ] + * }, + * {name: "environment_1", environment_type: null} + * ] * @param {Array} environments List of environments - * @return {type} - */ - addEnvironments(environments) { - console.log(environments); - } + */ + storeEnvironments(environments) { + this.state.environments = environments.reduce((acc, environment) => { + if (environment.environment_type !== null) { + const occurs = acc.find((element, index, array) => { + return element.name === environment.environment_type; + }); + + if (occurs !== undefined) { + acc[acc.indexOf(occurs)].children.push(environment); + acc[acc.indexOf(occurs)].children.sort(); + } else { + acc.push({ + name: environment.environment_type, + children: [environment] + }); + } + } else { + acc.push(environment); + } + + return acc; + }, []).sort(); + } } })(); \ No newline at end of file From de990812d54e0d1915b16d6d74fd912c39c6c1da Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 14 Oct 2016 18:38:13 +0100 Subject: [PATCH 017/302] Creates component --- .../components/environment_item.js.es6 | 34 +++++++++++++ .../components/environments_tree_view.js.es6 | 0 .../environments/environments_bundle.js.es6 | 5 +- .../services/environments_service.js.es6 | 2 - .../stores/environmnets_store.js.es6 | 11 ++++- .../components/_environment.html.haml | 8 +++ .../projects/environments/index.html.haml | 49 ++++++++++--------- 7 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment_item.js.es6 delete mode 100644 app/assets/javascripts/environments/components/environments_tree_view.js.es6 create mode 100644 app/views/projects/environments/components/_environment.html.haml diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 new file mode 100644 index 00000000000..b62d77140fa --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -0,0 +1,34 @@ +(() => { + const Store = gl.environmentsList.EnvironmentsStore; + + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + gl.environmentsList.EnvironmentItem = Vue.extend({ + + props: { + model: Object + }, + + data: function () { + return { + open: false + }; + }, + + computed: { + isFolder: function() { + debugger; + return this.model.children && this.model.children.length + } + }, + + methods: { + toggle: function () { + if (this.isFolder) { + this.open = !this.open; + } + } + } + }) +})(); \ No newline at end of file diff --git a/app/assets/javascripts/environments/components/environments_tree_view.js.es6 b/app/assets/javascripts/environments/components/environments_tree_view.js.es6 deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 783940c8f63..89d2fad1215 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -2,7 +2,7 @@ //= require vue-resource //= require_tree ./stores //= require_tree ./services - +//= require ./components/environment_item $(() => { @@ -20,10 +20,11 @@ $(() => { el: $environmentsListApp, components: { - 'tree-view': gl.environmentsList.TreeView + 'environment-item': gl.environmentsList.EnvironmentItem }, data: { + state: Store.state, endpoint: $environmentsListApp.dataset.endpoint, loading: true }, diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 179c0807cea..5c545fc4778 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -3,8 +3,6 @@ class EnvironmentsService { constructor (root) { Vue.http.options.root = root; - debugger; - this.environments = Vue.resource(root); Vue.http.interceptors.push((request, next) => { diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 911dd3e9352..6aa1b828bcb 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -31,10 +31,13 @@ * }, * {name: "environment_1", environment_type: null} * ] - * @param {Array} environments List of environments + * + * + * @param {Array} environments List of environments. + * @returns {Array} Tree structured array with the received environments. */ storeEnvironments(environments) { - this.state.environments = environments.reduce((acc, environment) => { + const environmentsTree = environments.reduce((acc, environment) => { if (environment.environment_type !== null) { const occurs = acc.find((element, index, array) => { return element.name === environment.environment_type; @@ -55,6 +58,10 @@ return acc; }, []).sort(); + + this.state.environments = environmentsTree; + + return environmentsTree; } } })(); \ No newline at end of file diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml new file mode 100644 index 00000000000..7cfe1029a41 --- /dev/null +++ b/app/views/projects/environments/components/_environment.html.haml @@ -0,0 +1,8 @@ +%environment-item{"inline-template" => true, + "v-for" => "environment in state.environments", + ":model" => "environment"} + %i{"v-if" => "isFolder"} + = icon("plus") + %i{"v-if" => ""} + = icon("less") + {{model.name}} diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a2526209df2..e989ec60611 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,26 +26,29 @@ New environment .environments-container#environments-list-view{ "v-cloak" => true, data: environments_list_data } - - if @all_environments.blank? - .blank-state.blank-state-no-icon - %h2.blank-state-title - You don't have any environments right now. - %p.blank-state-text - Environments are places where code gets deployed, such as staging or production. - %br - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - if can?(current_user, :create_environment, @project) - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do - New environment - - else - .table-holder - %table.table.ci-table.environments - %tbody - %th Environment - %th Last Deployment - %th Build - %th Commit - %th - %th.hidden-xs - / = render @environments + .environments-list-viewtext-center{ "v-if" => "loading" } + = icon("spinner spin") + + .blank-state.blank-state-no-icon{ "v-if" => "state.environments.length === 0" } + %h2.blank-state-title + You don't have any environments right now. + %p.blank-state-text + Environments are places where code gets deployed, such as staging or production. + %br + = succeed "." do + = link_to "Read more about environments", help_page_path("ci/environments") + - if can?(current_user, :create_environment, @project) + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + New environment + + .table-holder{ "v-if" => "state.environments.length" } + %table.table.ci-table.environments + %thead + %th Environment + %th Last Deployment + %th Build + %th Commit + %th + %th.hidden-xs + %tbody + =render "projects/environments/components/environment" From 109dc41fad156b1999023b5e1deb133ef08660ce Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 17 Oct 2016 10:41:45 +0100 Subject: [PATCH 018/302] Adds folder icons --- .../environments/components/environment_item.js.es6 | 7 +++++-- .../environments/components/_environment.html.haml | 11 ++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index b62d77140fa..70fab9b07d4 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -17,9 +17,12 @@ }, computed: { - isFolder: function() { - debugger; + isFolder: function () { return this.model.children && this.model.children.length + }, + + isOpen: function () { + return this.open; } }, diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 7cfe1029a41..01e3cb3a290 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,8 +1,9 @@ %environment-item{"inline-template" => true, "v-for" => "environment in state.environments", ":model" => "environment"} - %i{"v-if" => "isFolder"} - = icon("plus") - %i{"v-if" => ""} - = icon("less") - {{model.name}} + .name{"click" => "toggle"} + %i{"v-if" => "isFolder && isOpen"} + = icon("caret-down") + %i{"v-if" => "isFolder && !isOpen"} + = icon("caret-right") + {{model.name}} From 58f95c2ccd482dddb3e94f21b65790ca00fcf7ff Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 17 Oct 2016 16:09:39 +0100 Subject: [PATCH 019/302] Adds loading while fetching data --- app/assets/stylesheets/pages/environments.scss | 5 +++++ app/views/projects/environments/index.html.haml | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index fc49ff780fc..495284fa2d7 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -73,3 +73,8 @@ } } } + +.environments-list-loading { + width: 100%; + font-size: 34px; +} diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index e989ec60611..3483683bc2f 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,10 +26,10 @@ New environment .environments-container#environments-list-view{ "v-cloak" => true, data: environments_list_data } - .environments-list-viewtext-center{ "v-if" => "loading" } + .environments-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") - .blank-state.blank-state-no-icon{ "v-if" => "state.environments.length === 0" } + .blank-state.blank-state-no-icon{ "v-if" => "!loading && state.environments.length === 0" } %h2.blank-state-title You don't have any environments right now. %p.blank-state-text @@ -41,7 +41,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .table-holder{ "v-if" => "state.environments.length" } + .table-holder{ "v-if" => "!loading && state.environments.length" } %table.table.ci-table.environments %thead %th Environment From f716da652d857b26dd5e424c77ac0898ae85bdb5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 17 Oct 2016 18:03:19 +0100 Subject: [PATCH 020/302] Adds template for table row --- .../components/environment_item.js.es6 | 8 ++--- .../environments/environments_bundle.js.es6 | 6 ++-- .../stylesheets/pages/environments.scss | 4 +++ .../components/_environment.html.haml | 17 +++++---- .../projects/environments/index.html.haml | 36 ++++++++++++------- 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 70fab9b07d4..fd825842747 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -4,7 +4,11 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; + debugger; + gl.environmentsList.EnvironmentItem = Vue.extend({ + + template: '#environment-item-template', props: { model: Object @@ -19,10 +23,6 @@ computed: { isFolder: function () { return this.model.children && this.model.children.length - }, - - isOpen: function () { - return this.open; } }, diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 89d2fad1215..2769f6899fe 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -6,7 +6,7 @@ $(() => { - const $environmentsListApp = document.getElementById('environments-list-view'); + const environmentsListApp = document.getElementById('environments-list-view'); const Store = gl.environmentsList.EnvironmentsStore; window.gl = window.gl || {}; @@ -17,7 +17,7 @@ $(() => { gl.EnvironmentsListApp = new Vue({ - el: $environmentsListApp, + el: '#environments-list-view', components: { 'environment-item': gl.environmentsList.EnvironmentItem @@ -25,7 +25,7 @@ $(() => { data: { state: Store.state, - endpoint: $environmentsListApp.dataset.endpoint, + endpoint: environmentsListApp.dataset.endpoint, loading: true }, diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 495284fa2d7..f0bc2f635aa 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -72,6 +72,10 @@ margin-right: 0; } } + + .environment-folder-name { + cursor: pointer; + } } .environments-list-loading { diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 01e3cb3a290..8a0e7fb4349 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,9 +1,14 @@ %environment-item{"inline-template" => true, "v-for" => "environment in state.environments", ":model" => "environment"} - .name{"click" => "toggle"} - %i{"v-if" => "isFolder && isOpen"} - = icon("caret-down") - %i{"v-if" => "isFolder && !isOpen"} - = icon("caret-right") - {{model.name}} + .environment-folder-name{"@click" => "toggle"} + %span.icon-container{"v-if" => "isFolder"} + %i{"v-show" => "open"} + = icon("caret-down") + + %i{"v-show" => "!open"} + = icon("caret-right") + %span.name-container + {{model.name}} + + // {"name":"review","children":[{"id":8,"project_id":10,"name":"review/app","created_at":"2016-10-13T14:15:33.550Z","updated_at":"2016-10-13T14:15:33.550Z","external_url":"http://gitlab.com","environment_type":"review","state":"opened"}]} \ No newline at end of file diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 3483683bc2f..ec83399f480 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -25,11 +25,11 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .environments-container#environments-list-view{ "v-cloak" => true, data: environments_list_data } + #environments-list-view{ data: environments_list_data, class: "environments-container" } .environments-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") - .blank-state.blank-state-no-icon{ "v-if" => "!loading && state.environments.length === 0" } + .blank-state.blank-state-no-icon{ "v-if" => "!loading && !state.environments.length" } %h2.blank-state-title You don't have any environments right now. %p.blank-state-text @@ -42,13 +42,25 @@ New environment .table-holder{ "v-if" => "!loading && state.environments.length" } - %table.table.ci-table.environments - %thead - %th Environment - %th Last Deployment - %th Build - %th Commit - %th - %th.hidden-xs - %tbody - =render "projects/environments/components/environment" + %table.table + %tr{"is"=>"environment-item", + "inline-template" => true, + "v-for" => "environment in state.environments", + ":model" => "environment"} + + +%script#environment-item-template{"type"=> "text/x-template"} + %tr + %td.environment-folder-name{"@click" => "toggle"} + %span.icon-container{"v-if" => "isFolder"} + %i{"v-show" => "open"} + = icon("caret-down") + + %i{"v-show" => "!open"} + = icon("caret-right") + %span.name-container + {{model.name}} + %tr{"v-show" => "open", "v-if"=>"isFolder"} + %td{"v-for" => "child in model.children", + ":model" => "child"} + {{child.name}} From 37a100c80f81c8948b1a6d36696ba0a588e070db Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 17 Oct 2016 23:18:49 +0100 Subject: [PATCH 021/302] Adds children rows --- .../stylesheets/pages/environments.scss | 6 +++- .../projects/environments/index.html.haml | 35 ++++++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index f0bc2f635aa..c60455e148f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -61,7 +61,11 @@ } .table.ci-table.environments { - + + .environment-children td:first-child { + padding-left: 40px; + } + .icon-container { width: 20px; text-align: center; diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index ec83399f480..f8e264ac6bf 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -42,15 +42,23 @@ New environment .table-holder{ "v-if" => "!loading && state.environments.length" } - %table.table - %tr{"is"=>"environment-item", - "inline-template" => true, - "v-for" => "environment in state.environments", - ":model" => "environment"} + %table.table.ci-table.environments + %thead + %th Environment + %th Last Deployment + %th Build + %th Commit + %th + %th.hidden-xs + %tbody + %tr{"is"=>"environment-item", + "inline-template" => true, + "v-for" => "environment in state.environments", + ":model" => "environment"} %script#environment-item-template{"type"=> "text/x-template"} - %tr + %tr.environment %td.environment-folder-name{"@click" => "toggle"} %span.icon-container{"v-if" => "isFolder"} %i{"v-show" => "open"} @@ -60,7 +68,16 @@ = icon("caret-right") %span.name-container {{model.name}} - %tr{"v-show" => "open", "v-if"=>"isFolder"} - %td{"v-for" => "child in model.children", - ":model" => "child"} + %td + %a{{"v-if" => "model.last_deployment"}} + {{model.last_deployment.id}} + %tr.environment-children{"v-show" => "open", + "v-if"=>"isFolder", + "v-for" => "child in model.children", + ":model" => "child"} + %td {{child.name}} + %td.deployment-column + %span{"v-if" => "child.last_deployment && child.last_deployment.iid"} + {{child.last_deployment.iid}} + \ No newline at end of file From 234c0415e4b1a7584d629d9cc9d8d8d884dc7b6a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 19 Oct 2016 10:06:03 +0100 Subject: [PATCH 022/302] Updates Component with documentation and template with links for details --- .../components/environment_item.js.es6 | 27 ++++++- .../components/_environment.html.haml | 78 ++++++++++++++++--- .../projects/environments/index.html.haml | 34 ++------ 3 files changed, 96 insertions(+), 43 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index fd825842747..a8298150ff5 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,11 +1,22 @@ (() => { + + /** + * Envrionment Item Component + * + * Used in a hierarchical structure to show folders with children + * in a table. + * Based on [Tree View](https://vuejs.org/examples/tree-view.html) + * The template used in this Component is non recursive. + * + * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539) + * for more information. + */ + const Store = gl.environmentsList.EnvironmentsStore; window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - debugger; - gl.environmentsList.EnvironmentItem = Vue.extend({ template: '#environment-item-template', @@ -21,12 +32,24 @@ }, 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 {Number} The length of the children array + */ isFolder: function () { return this.model.children && this.model.children.length } }, methods: { + + /** + * Toggles the visibility of a folders' children. + */ toggle: function () { if (this.isFolder) { this.open = !this.open; diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 8a0e7fb4349..2943e5ca2c6 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,14 +1,68 @@ -%environment-item{"inline-template" => true, - "v-for" => "environment in state.environments", - ":model" => "environment"} - .environment-folder-name{"@click" => "toggle"} - %span.icon-container{"v-if" => "isFolder"} - %i{"v-show" => "open"} - = icon("caret-down") +%script#environment-item-template{ "type"=> "text/x-template" } + %tr.environment + %td.environment-folder-name{ "@click" => "toggle" } + %a{ "v-if" => "!isFolder", + ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } + {{model.name}} + + %span{ "v-if" => "isFolder" } + %i{ "v-show" => "open" } + = icon("caret-down") - %i{"v-show" => "!open"} - = icon("caret-right") - %span.name-container - {{model.name}} + %i{ "v-show" => "!open" } + = icon("caret-right") - // {"name":"review","children":[{"id":8,"project_id":10,"name":"review/app","created_at":"2016-10-13T14:15:33.550Z","updated_at":"2016-10-13T14:15:33.550Z","external_url":"http://gitlab.com","environment_type":"review","state":"opened"}]} \ No newline at end of file + {{model.name}} + + %td.deployment-column + %span{ "v-if" => "model.last_deployment && model.last_deployment.iid" } + {{model.last_deployment.iid}} + by + %span{ "v-if" => "model.last_deployment.user" } + {{model.last_deployment.user}} + + %td + column 3 + + %td + column 4 + + %td + column 5 + + %td.hidden-xs + .pull-right + actions + -# = render 'projects/environments/external_url', environment: "model" + -# = render 'projects/deployments/actions', deployment: "model.last_deployment" + -# = render 'projects/environments/stop', environment: "model" + -# = render 'projects/deployments/rollback', deployment: "model.last_deployment" + + %tr.environment-children{ "v-show" => "open", + "v-if"=>"isFolder", + "v-for" => "child in model.children", + ":model" => "child" } + + %td + %a{ ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + child.id" } + {{child.name}} + + %td.deployment-column + %span{ "v-if" => "child.last_deployment && child.last_deployment.iid" } + {{child.last_deployment.iid}} + %span{ "v-if" => "child.last_deployment && child.last_deployment.user" } + {{child.last_deployment.user}} + /= user_avatar(user: "child.last_deployment.user", size: 20) + + %td + column 3 + + %td + column 4 + + %td + column 5 + + %td.hidden-xs + .pull-right + actions \ No newline at end of file diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index f8e264ac6bf..4391271f5c7 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,7 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('environments/environments_bundle.js') + = page_specific_javascript_tag("environments/environments_bundle.js") %div{ class: container_class } .top-area @@ -22,7 +22,7 @@ - if can?(current_user, :create_environment, @project) && !@all_environments.blank? .nav-controls - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do + = link_to new_namespace_project_environment_path(@project.namespace, @project), class: "btn btn-create" do New environment #environments-list-view{ data: environments_list_data, class: "environments-container" } @@ -51,33 +51,9 @@ %th %th.hidden-xs %tbody - %tr{"is"=>"environment-item", + %tr{ "is"=>"environment-item", "inline-template" => true, "v-for" => "environment in state.environments", - ":model" => "environment"} + ":model" => "environment" } - -%script#environment-item-template{"type"=> "text/x-template"} - %tr.environment - %td.environment-folder-name{"@click" => "toggle"} - %span.icon-container{"v-if" => "isFolder"} - %i{"v-show" => "open"} - = icon("caret-down") - - %i{"v-show" => "!open"} - = icon("caret-right") - %span.name-container - {{model.name}} - %td - %a{{"v-if" => "model.last_deployment"}} - {{model.last_deployment.id}} - %tr.environment-children{"v-show" => "open", - "v-if"=>"isFolder", - "v-for" => "child in model.children", - ":model" => "child"} - %td - {{child.name}} - %td.deployment-column - %span{"v-if" => "child.last_deployment && child.last_deployment.iid"} - {{child.last_deployment.iid}} - \ No newline at end of file +=render "projects/environments/components/environment" \ No newline at end of file From 2a40dec16d4690d11417013cab82cd6e2cbdb235 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 19 Oct 2016 17:03:48 +0100 Subject: [PATCH 023/302] Serialize all needed data --- .../projects/environments_controller.rb | 12 ++++++++++- app/helpers/environments_helper.rb | 2 +- .../components/_environment.html.haml | 20 ++++++++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 9255388eb84..e937e6b950b 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -19,7 +19,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do - render json: @environments + render json: serialize_as_json(@environments) end end end @@ -69,4 +69,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController def environment @environment ||= project.environments.find(params[:id]) end + + def serialize_as_json(resource) + resource.as_json( + include: { + last_deployment: { + include: [:deployable, :user] + } + } + ) + end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index d9a3106f706..954ecb65985 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -1,5 +1,5 @@ module EnvironmentsHelper - def environments_list_data + def environments_list_data() { endpoint: namespace_project_environments_path(@project.namespace, @project) } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 2943e5ca2c6..a64af5fef6f 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -12,23 +12,27 @@ %i{ "v-show" => "!open" } = icon("caret-right") - {{model.name}} + %td.deployment-column - %span{ "v-if" => "model.last_deployment && model.last_deployment.iid" } + %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } {{model.last_deployment.iid}} + by %span{ "v-if" => "model.last_deployment.user" } - {{model.last_deployment.user}} + {{model.last_deployment.user.name}} %td - column 3 + %a{ "v-if" => "!isFolder" } + column 3 %td - column 4 + %a{ "v-if" => "!isFolder" } + column 3 %td - column 5 + %span{ "v-if" => "!isFolder && model.last_deployment" } + {{last_deployment.created_at}} %td.hidden-xs .pull-right @@ -55,7 +59,9 @@ /= user_avatar(user: "child.last_deployment.user", size: 20) %td - column 3 + %a.build-link{ "v-if" => "child.last_deployment && child.last_deployment.deployable", + ":href" => "" } + {{child.last_deployment}} %td column 4 From c02a4f384ad0530e9d48b3ede4238ac4fe2d3e9c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 19 Oct 2016 17:12:21 +0100 Subject: [PATCH 024/302] Adds all first row columns --- .../components/_environment.html.haml | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index a64af5fef6f..48fb7d90ed1 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -11,8 +11,8 @@ %i{ "v-show" => "!open" } = icon("caret-right") - - + + {{model.name}} %td.deployment-column %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } @@ -23,25 +23,25 @@ {{model.last_deployment.user.name}} %td - %a{ "v-if" => "!isFolder" } - column 3 + %a{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", + ":class" => "build-link", + ":href" => "" } + {{model.last_deployment.deployable.name}} (## {{model.last_deployment.deployable.id}}) %td - %a{ "v-if" => "!isFolder" } - column 3 + %a{ "v-if" => "!isFolder && model.last_deployment" } + column 4 + %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} + No deployments yet %td %span{ "v-if" => "!isFolder && model.last_deployment" } - {{last_deployment.created_at}} + {{model.last_deployment.created_at}} %td.hidden-xs - .pull-right + .pull-right{ "v-if" => "!isFolder"} actions - -# = render 'projects/environments/external_url', environment: "model" - -# = render 'projects/deployments/actions', deployment: "model.last_deployment" - -# = render 'projects/environments/stop', environment: "model" - -# = render 'projects/deployments/rollback', deployment: "model.last_deployment" - + %tr.environment-children{ "v-show" => "open", "v-if"=>"isFolder", "v-for" => "child in model.children", @@ -54,21 +54,27 @@ %td.deployment-column %span{ "v-if" => "child.last_deployment && child.last_deployment.iid" } {{child.last_deployment.iid}} - %span{ "v-if" => "child.last_deployment && child.last_deployment.user" } - {{child.last_deployment.user}} - /= user_avatar(user: "child.last_deployment.user", size: 20) + + by + %span{ "v-if" => "model.last_deployment.user" } + {{child.last_deployment.user.name}} %td - %a.build-link{ "v-if" => "child.last_deployment && child.last_deployment.deployable", + %a{ "v-if" => " child.last_deployment && child.last_deployment.deployable", + ":class" => "build-link", ":href" => "" } - {{child.last_deployment}} + {{model.last_deployment.deployable.name}} (## {{model.last_deployment.deployable.id}}) %td - column 4 + %a{ "v-if" => "child.last_deployment" } + column 4 + %p.commit-title{ "v-if" => "!child.last_deployment"} + No deployments yet %td - column 5 + %span{ "v-if" => "child.last_deployment" } + {{child.last_deployment.created_at}} %td.hidden-xs - .pull-right + .pull-right{ "v-if" => "!isFolder"} actions \ No newline at end of file From 4508f85ab7aae29729b8dce10585586ab206ce60 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 10:06:03 +0100 Subject: [PATCH 025/302] Adds user column with avatar url --- .../projects/environments_controller.rb | 7 +++- .../components/_environment.html.haml | 40 ++++++++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index e937e6b950b..d261bfa9b8d 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -73,8 +73,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController def serialize_as_json(resource) resource.as_json( include: { - last_deployment: { - include: [:deployable, :user] + last_deployment: { + include: { + user: { only: [:id, :name, :username], methods: [:avatar_url] }, + deployable: { only: [:id, :name] } + } } } ) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 48fb7d90ed1..8dd477d11ec 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -18,9 +18,15 @@ %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } {{model.last_deployment.iid}} - by %span{ "v-if" => "model.last_deployment.user" } - {{model.last_deployment.user.name}} + by + %a{":href" => "'/' + model.last_deployment.user.username"} + + %img.avatar.has-tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", + ":alt" => "model.last_deployment.user.username + ' avatar'", + ":title" => "model.last_deployment.user.username", + data: { container: 'body'}, + width: 20, height: 20 } %td %a{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", @@ -30,7 +36,7 @@ %td %a{ "v-if" => "!isFolder && model.last_deployment" } - column 4 + / = render "projects/deployments/commit", deployment: "{{model.last_deployment}}" %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} No deployments yet @@ -40,7 +46,10 @@ %td.hidden-xs .pull-right{ "v-if" => "!isFolder"} - actions + -# = render "projects/environments/external_url", environment: "{{model}}" + -# = render "projects/deployments/actions", deployment: "{{model.last_deployment}}" + -# = render "projects/environments/stop", environment: "{{model}}" + -# = render "projects/deployments/rollback", deployment: "{{model.last_deployment}}" %tr.environment-children{ "v-show" => "open", "v-if"=>"isFolder", @@ -55,19 +64,25 @@ %span{ "v-if" => "child.last_deployment && child.last_deployment.iid" } {{child.last_deployment.iid}} - by - %span{ "v-if" => "model.last_deployment.user" } - {{child.last_deployment.user.name}} + %span{ "v-if" => "child.last_deployment.user" } + by + %a{":href" => "'/' + child.last_deployment.user.username"} + + %img.avatar.has-tooltip.s20{ ":src" => "child.last_deployment.user.avatar_url", + ":alt" => "child.last_deployment.user.username + ' avatar'", + ":title" => "child.last_deployment.user.username", + data: { container: 'body'}, + width: 20, height: 20 } %td %a{ "v-if" => " child.last_deployment && child.last_deployment.deployable", ":class" => "build-link", ":href" => "" } - {{model.last_deployment.deployable.name}} (## {{model.last_deployment.deployable.id}}) + {{chil.last_deployment.deployable.name}} (## {{child.last_deployment.deployable.id}}) %td %a{ "v-if" => "child.last_deployment" } - column 4 + / = render "projects/deployments/commit", deployment: "{{child.last_deployment}}" %p.commit-title{ "v-if" => "!child.last_deployment"} No deployments yet @@ -76,5 +91,8 @@ {{child.last_deployment.created_at}} %td.hidden-xs - .pull-right{ "v-if" => "!isFolder"} - actions \ No newline at end of file + .pull-right + -# = render "projects/environments/external_url", environment: "{{child}}" + -# = render "projects/deployments/actions", deployment: "{{child.last_deployment}}" + -# = render "projects/environments/stop", environment: "{{child}}" + -# = render "projects/deployments/rollback", deployment: "{{child.last_deployment}}" \ No newline at end of file From c28df601dcbfa5b1d44d4f351c64e04a69d24d0e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 11:20:37 +0100 Subject: [PATCH 026/302] Adds build column --- .../environments/components/_environment.html.haml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 8dd477d11ec..fe34730440f 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -29,9 +29,8 @@ width: 20, height: 20 } %td - %a{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":class" => "build-link", - ":href" => "" } + %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", + ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id" } {{model.last_deployment.deployable.name}} (## {{model.last_deployment.deployable.id}}) %td @@ -75,10 +74,9 @@ width: 20, height: 20 } %td - %a{ "v-if" => " child.last_deployment && child.last_deployment.deployable", - ":class" => "build-link", - ":href" => "" } - {{chil.last_deployment.deployable.name}} (## {{child.last_deployment.deployable.id}}) + %a.build-link{ "v-if" => "child.last_deployment && child.last_deployment.deployable", + ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + child.last_deployment.deployable.id" } + {{child.last_deployment.deployable.name}} (## {{child.last_deployment.deployable.id}}) %td %a{ "v-if" => "child.last_deployment" } From b31c9f531853172f5704c81f2f5dee5d1ce37afd Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 11:43:22 +0100 Subject: [PATCH 027/302] Commit column --- app/controllers/projects/environments_controller.rb | 2 +- .../projects/environments/components/_environment.html.haml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index d261bfa9b8d..c760f885a4c 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -76,7 +76,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController last_deployment: { include: { user: { only: [:id, :name, :username], methods: [:avatar_url] }, - deployable: { only: [:id, :name] } + deployable: { only: [:id, :name, :ref, :tag, :short_sha] } } } } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index fe34730440f..ef82a70031e 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -21,7 +21,7 @@ %span{ "v-if" => "model.last_deployment.user" } by %a{":href" => "'/' + model.last_deployment.user.username"} - + %img.avatar.has-tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", ":alt" => "model.last_deployment.user.username + ' avatar'", ":title" => "model.last_deployment.user.username", @@ -35,7 +35,7 @@ %td %a{ "v-if" => "!isFolder && model.last_deployment" } - / = render "projects/deployments/commit", deployment: "{{model.last_deployment}}" + -# = render "projects/deployments/commit", deployment: "{{model.last_deployment}}" %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} No deployments yet @@ -80,7 +80,7 @@ %td %a{ "v-if" => "child.last_deployment" } - / = render "projects/deployments/commit", deployment: "{{child.last_deployment}}" + -# = render "projects/deployments/commit", deployment: "{{child.last_deployment}}" %p.commit-title{ "v-if" => "!child.last_deployment"} No deployments yet From c497153ece65b6cdcb2404a2a928b36e1ef9c75a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 12:09:35 +0100 Subject: [PATCH 028/302] Adds newline at end of file --- .../javascripts/environments/components/environment_item.js.es6 | 2 +- app/assets/javascripts/environments/environments_bundle.js.es6 | 2 +- .../environments/services/environments_service.js.es6 | 2 +- .../javascripts/environments/stores/environmnets_store.js.es6 | 2 +- app/helpers/environments_helper.rb | 2 +- .../projects/environments/components/_environment.html.haml | 2 +- app/views/projects/environments/index.html.haml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index a8298150ff5..3e43c64139a 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -57,4 +57,4 @@ } } }) -})(); \ No newline at end of file +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 2769f6899fe..9d4affdb2b6 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -47,4 +47,4 @@ $(() => { }); } }); -}); \ No newline at end of file +}); diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 5c545fc4778..5cc194ac034 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -14,4 +14,4 @@ class EnvironmentsService { all () { return this.environments.get(); } -}; \ No newline at end of file +}; diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 6aa1b828bcb..564b33607d7 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -64,4 +64,4 @@ return environmentsTree; } } -})(); \ No newline at end of file +})(); diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 954ecb65985..7c09c20d118 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -4,4 +4,4 @@ module EnvironmentsHelper endpoint: namespace_project_environments_path(@project.namespace, @project) } end -end \ No newline at end of file +end diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index ef82a70031e..1c91b60b741 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -93,4 +93,4 @@ -# = render "projects/environments/external_url", environment: "{{child}}" -# = render "projects/deployments/actions", deployment: "{{child.last_deployment}}" -# = render "projects/environments/stop", environment: "{{child}}" - -# = render "projects/deployments/rollback", deployment: "{{child.last_deployment}}" \ No newline at end of file + -# = render "projects/deployments/rollback", deployment: "{{child.last_deployment}}" diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 4391271f5c7..34f38a2ee32 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -56,4 +56,4 @@ "v-for" => "environment in state.environments", ":model" => "environment" } -=render "projects/environments/components/environment" \ No newline at end of file +=render "projects/environments/components/environment" From 3265518b39622c9522f49c8cad58185421b2eda6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 13:40:23 +0100 Subject: [PATCH 029/302] Fixes build column --- .../environments/components/_environment.html.haml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 1c91b60b741..8db29444935 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -31,7 +31,9 @@ %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id" } - {{model.last_deployment.deployable.name}} (## {{model.last_deployment.deployable.id}}) + {{model.last_deployment.deployable.name}} + = precede '#' do + ({{model.last_deployment.deployable.id}}) %td %a{ "v-if" => "!isFolder && model.last_deployment" } @@ -76,7 +78,9 @@ %td %a.build-link{ "v-if" => "child.last_deployment && child.last_deployment.deployable", ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + child.last_deployment.deployable.id" } - {{child.last_deployment.deployable.name}} (## {{child.last_deployment.deployable.id}}) + {{child.last_deployment.deployable.name}} + = precede '#' do + ({{child.last_deployment.deployable.id}}) %td %a{ "v-if" => "child.last_deployment" } From 96152f17d115a08726bfd1322d0dba33fe5cd211 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 13:53:28 +0100 Subject: [PATCH 030/302] Adds # back to deployment id --- .../stylesheets/pages/environments.scss | 31 +++++++++++-------- .../components/_environment.html.haml | 16 +++++----- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index c60455e148f..cb9196dde92 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -4,6 +4,11 @@ overflow: auto; } +.environments-list-loading { + width: 100%; + font-size: 34px; +} + .environments { .deployment-column { .avatar { @@ -58,14 +63,23 @@ } } } + + .children-name{ + margin-left: 17px; + margin-right: -17px; + } + + .folder-icon { + padding: 0 5px 0 0; + } + + .folder-name { + cursor: pointer; + } } .table.ci-table.environments { - .environment-children td:first-child { - padding-left: 40px; - } - .icon-container { width: 20px; text-align: center; @@ -76,13 +90,4 @@ margin-right: 0; } } - - .environment-folder-name { - cursor: pointer; - } -} - -.environments-list-loading { - width: 100%; - font-size: 34px; } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 8db29444935..ae080137361 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,22 +1,23 @@ %script#environment-item-template{ "type"=> "text/x-template" } %tr.environment - %td.environment-folder-name{ "@click" => "toggle" } + %td.folder-name{ "@click" => "toggle" } %a{ "v-if" => "!isFolder", ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } {{model.name}} %span{ "v-if" => "isFolder" } - %i{ "v-show" => "open" } + %i.folder-icon{ "v-show" => "open" } = icon("caret-down") - %i{ "v-show" => "!open" } + %i.folder-icon{ "v-show" => "!open" } = icon("caret-right") {{model.name}} %td.deployment-column %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } - {{model.last_deployment.iid}} + = precede '#' do + {{model.last_deployment.iid}} %span{ "v-if" => "model.last_deployment.user" } by @@ -52,18 +53,19 @@ -# = render "projects/environments/stop", environment: "{{model}}" -# = render "projects/deployments/rollback", deployment: "{{model.last_deployment}}" - %tr.environment-children{ "v-show" => "open", + %tr{ "v-show" => "open", "v-if"=>"isFolder", "v-for" => "child in model.children", ":model" => "child" } %td - %a{ ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + child.id" } + %a.children-name{ ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + child.id" } {{child.name}} %td.deployment-column %span{ "v-if" => "child.last_deployment && child.last_deployment.iid" } - {{child.last_deployment.iid}} + = precede '#' do + {{child.last_deployment.iid}} %span{ "v-if" => "child.last_deployment.user" } by From e4ff8dd2f100daaec614881c2b2733a92f4ec4d8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 17:43:52 +0100 Subject: [PATCH 031/302] Recursive component --- .../components/environment_item.js.es6 | 11 +- .../environments/environments_bundle.js.es6 | 2 +- .../components/_environment.html.haml | 116 +++++------------- .../projects/environments/index.html.haml | 7 +- 4 files changed, 41 insertions(+), 95 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 3e43c64139a..71463bd1748 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -5,8 +5,7 @@ * * Used in a hierarchical structure to show folders with children * in a table. - * Based on [Tree View](https://vuejs.org/examples/tree-view.html) - * The template used in this Component is non recursive. + * 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. @@ -17,7 +16,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - gl.environmentsList.EnvironmentItem = Vue.extend({ + gl.environmentsList.EnvironmentItem = Vue.component("environment-item", { template: '#environment-item-template', @@ -25,7 +24,7 @@ model: Object }, - data: function () { + data () { return { open: false }; @@ -40,7 +39,7 @@ * * @returns {Number} The length of the children array */ - isFolder: function () { + isFolder () { return this.model.children && this.model.children.length } }, @@ -50,7 +49,7 @@ /** * Toggles the visibility of a folders' children. */ - toggle: function () { + toggle () { if (this.isFolder) { this.open = !this.open; } diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 9d4affdb2b6..027dc7fb211 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -20,7 +20,7 @@ $(() => { el: '#environments-list-view', components: { - 'environment-item': gl.environmentsList.EnvironmentItem + 'item': gl.environmentsList.EnvironmentItem }, data: { diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index ae080137361..76d921e9fd3 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,102 +1,52 @@ %script#environment-item-template{ "type"=> "text/x-template" } - %tr.environment - %td.folder-name{ "@click" => "toggle" } - %a{ "v-if" => "!isFolder", + %tr + %td.folder-name + %a{ "v-if" => "!isFolder", ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } {{model.name}} + %span{ "v-if" => "isFolder", + "@click" => "toggle" } - %span{ "v-if" => "isFolder" } - %i.folder-icon{ "v-show" => "open" } - = icon("caret-down") - - %i.folder-icon{ "v-show" => "!open" } - = icon("caret-right") + %i.folder-icon{ "v-show" => "open"} + =icon ("caret-down") + %i.folder-icon{ "v-show" => "!open"} + =icon("caret-right") {{model.name}} - + %td.deployment-column - %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } - = precede '#' do + %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid"} + = precede "#" do {{model.last_deployment.iid}} - - %span{ "v-if" => "model.last_deployment.user" } + + %span{ "v-if" => "model.last_deployment.user"} by %a{":href" => "'/' + model.last_deployment.user.username"} - - %img.avatar.has-tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", - ":alt" => "model.last_deployment.user.username + ' avatar'", + %img.avatar.has_tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", + ":alt" => "model.last_deployment.user.username + 'avatar'", ":title" => "model.last_deployment.user.username", - data: { container: 'body'}, - width: 20, height: 20 } - + data: {container: "body"}, + width: 20, height: 20} + %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id" } - {{model.last_deployment.deployable.name}} - = precede '#' do - ({{model.last_deployment.deployable.id}}) - + ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id"} + {{model.last_deployment.deployble.name}} + = precede "#" do + {{model.last_deployment.deployable.id}} + %td - %a{ "v-if" => "!isFolder && model.last_deployment" } - -# = render "projects/deployments/commit", deployment: "{{model.last_deployment}}" + %a{ "v-if" => "!isFolder && model.last_deployment"} + commit message goes here %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} No deployments yet - - %td - %span{ "v-if" => "!isFolder && model.last_deployment" } + + %td + %span{ "v-if" => "!isFolder && model.last_deployment"} {{model.last_deployment.created_at}} - + %td.hidden-xs .pull-right{ "v-if" => "!isFolder"} - -# = render "projects/environments/external_url", environment: "{{model}}" - -# = render "projects/deployments/actions", deployment: "{{model.last_deployment}}" - -# = render "projects/environments/stop", environment: "{{model}}" - -# = render "projects/deployments/rollback", deployment: "{{model.last_deployment}}" - - %tr{ "v-show" => "open", - "v-if"=>"isFolder", - "v-for" => "child in model.children", - ":model" => "child" } - - %td - %a.children-name{ ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + child.id" } - {{child.name}} - - %td.deployment-column - %span{ "v-if" => "child.last_deployment && child.last_deployment.iid" } - = precede '#' do - {{child.last_deployment.iid}} - - %span{ "v-if" => "child.last_deployment.user" } - by - %a{":href" => "'/' + child.last_deployment.user.username"} - - %img.avatar.has-tooltip.s20{ ":src" => "child.last_deployment.user.avatar_url", - ":alt" => "child.last_deployment.user.username + ' avatar'", - ":title" => "child.last_deployment.user.username", - data: { container: 'body'}, - width: 20, height: 20 } - - %td - %a.build-link{ "v-if" => "child.last_deployment && child.last_deployment.deployable", - ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + child.last_deployment.deployable.id" } - {{child.last_deployment.deployable.name}} - = precede '#' do - ({{child.last_deployment.deployable.id}}) - - %td - %a{ "v-if" => "child.last_deployment" } - -# = render "projects/deployments/commit", deployment: "{{child.last_deployment}}" - %p.commit-title{ "v-if" => "!child.last_deployment"} - No deployments yet - - %td - %span{ "v-if" => "child.last_deployment" } - {{child.last_deployment.created_at}} - - %td.hidden-xs - .pull-right - -# = render "projects/environments/external_url", environment: "{{child}}" - -# = render "projects/deployments/actions", deployment: "{{child.last_deployment}}" - -# = render "projects/environments/stop", environment: "{{child}}" - -# = render "projects/deployments/rollback", deployment: "{{child.last_deployment}}" + actions will go here + + %tr{"v-if" => "open && isFolder", "is" => "environment-item", "v-for" => "model in model.children", ":model" => "model"} diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 34f38a2ee32..a8667d88e7b 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -41,7 +41,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .table-holder{ "v-if" => "!loading && state.environments.length" } + .table-holder{ "v-if" => "!loading && state.environments" } %table.table.ci-table.environments %thead %th Environment @@ -51,9 +51,6 @@ %th %th.hidden-xs %tbody - %tr{ "is"=>"environment-item", - "inline-template" => true, - "v-for" => "environment in state.environments", - ":model" => "environment" } + %tr{"is" => "environment-item", "v-for" => "model in state.environments", ":model" => "model"} =render "projects/environments/components/environment" From d873a3bd552bf63d8037392bdf56b86b1c7996d9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 20 Oct 2016 20:37:54 +0100 Subject: [PATCH 032/302] Adds commit partial for vue component --- .../projects/environments_controller.rb | 5 +++-- .../environments/components/_commit.html.haml | 20 +++++++++++++++++++ .../components/_environment.html.haml | 6 +++--- 3 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 app/views/projects/environments/components/_commit.html.haml diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index c760f885a4c..20b4ed46d0a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -76,8 +76,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController last_deployment: { include: { user: { only: [:id, :name, :username], methods: [:avatar_url] }, - deployable: { only: [:id, :name, :ref, :tag, :short_sha] } - } + deployable: { only: [:id, :name, :ref, :tag] } + }, + methods: [:short_sha, :commit_title, :commit] } } ) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml new file mode 100644 index 00000000000..25bd6223841 --- /dev/null +++ b/app/views/projects/environments/components/_commit.html.haml @@ -0,0 +1,20 @@ +.branch-commit{"v-if" => "!isFolder && model.last_deployment"} + %div{ "v-if" => "model.last_deployment.ref" } + .icon-container{ "v-if" => "model.last_deployment.tag" } + =icon("tag") + .icon-container{ "v-if" => "!model.last_deployment.tag" } + =icon("code-fork") + %a.monospace.branch-name{} + {{model.last_deployment.ref}} + + .icon-container.commit-icon + = custom_icon("icon_commit") + %a.commit-id.monospace{":href" => ""} + {{model.last_deployment.short_sha}} + + %p.commit-title + %span{ "v-if" => "model.last_deployment.commit_title"} + %a.commit-row-message{":href" => ""} + {{model.last_deployment.commit_title}} + %span{ "v-if" => "!model.last_deployment.commit_title"} + Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 76d921e9fd3..3e11141d01a 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -31,13 +31,13 @@ %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id"} - {{model.last_deployment.deployble.name}} + {{model.last_deployment.deployable.name}} = precede "#" do {{model.last_deployment.deployable.id}} %td - %a{ "v-if" => "!isFolder && model.last_deployment"} - commit message goes here + =render "projects/environments/components/commit" + %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} No deployments yet From 9c1e5649a59ec21dbd5eb47e2bbc8c903bd8b18a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 12:05:31 +0100 Subject: [PATCH 033/302] Adds CSS for children rows to be indented --- .../environments/components/environment_item.js.es6 | 12 +++++++++++- app/assets/stylesheets/pages/environments.scss | 2 +- .../environments/components/_environment.html.haml | 12 ++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 71463bd1748..961396b46b7 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -16,7 +16,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - gl.environmentsList.EnvironmentItem = Vue.component("environment-item", { + gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { template: '#environment-item-template', @@ -41,6 +41,16 @@ */ isFolder () { return this.model.children && this.model.children.length + }, + + /** + * If an item is inside a folder structure will return true. + * Used for css purposes. + * + * @returns {Boolean|undefined} + */ + isChildren () { + return this.model['vue-isChildren']; } }, diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index cb9196dde92..973820e8740 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -64,7 +64,7 @@ } } - .children-name{ + .children-row .environment-name{ margin-left: 17px; margin-right: -17px; } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 3e11141d01a..677ab9f60bd 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,10 +1,11 @@ %script#environment-item-template{ "type"=> "text/x-template" } %tr - %td.folder-name - %a{ "v-if" => "!isFolder", + %td{"v-bind:class" => "{ 'children-row': isChildren}"} + %a.environment-name{ "v-if" => "!isFolder", ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } {{model.name}} - %span{ "v-if" => "isFolder", + + %span.folder-name{ "v-if" => "isFolder", "@click" => "toggle" } %i.folder-icon{ "v-show" => "open"} @@ -49,4 +50,7 @@ .pull-right{ "v-if" => "!isFolder"} actions will go here - %tr{"v-if" => "open && isFolder", "is" => "environment-item", "v-for" => "model in model.children", ":model" => "model"} + %tr{"v-if" => "open && isFolder", + "is" => "environment-item", + "v-for" => "model in model.children", + ":model" => "model"} From ea91d9cc970883e0de5dfb35894d52a60f258d3b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 12:06:33 +0100 Subject: [PATCH 034/302] Adds attribute in store --- .../stores/environmnets_store.js.es6 | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 564b33607d7..dbf1bee05bb 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -13,6 +13,11 @@ * 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: @@ -25,8 +30,8 @@ * [ * { name: "review", children: * [ - * { name: "environment", environment_type: "review"}, - * { name: "environment_2", environment_type: "review"} + * { name: "environment", environment_type: "review", vue-isChildren: true}, + * { name: "environment_2", environment_type: "review", vue-isChildren: true} * ] * }, * {name: "environment_1", environment_type: null} @@ -44,12 +49,15 @@ }); if (occurs !== undefined) { - acc[acc.indexOf(occurs)].children.push(environment); + acc[acc.indexOf(occurs)].children.push( + Object.assign({}, environment, {"vue-isChildren": true})); acc[acc.indexOf(occurs)].children.sort(); } else { acc.push({ name: environment.environment_type, - children: [environment] + children: [ + Object.assign({}, environment, {"vue-isChildren": true}) + ] }); } } else { From a60e6f14a7c9bbb6cd3d2eeec0ca384d389d501a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 12:06:57 +0100 Subject: [PATCH 035/302] Adds commit partial for vue component --- .../projects/environments/components/_commit.html.haml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index 25bd6223841..3395b2fa240 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -1,20 +1,28 @@ .branch-commit{"v-if" => "!isFolder && model.last_deployment"} + %div{ "v-if" => "model.last_deployment.ref" } + .icon-container{ "v-if" => "model.last_deployment.tag" } =icon("tag") + .icon-container{ "v-if" => "!model.last_deployment.tag" } =icon("code-fork") - %a.monospace.branch-name{} + + %a.monospace.branch-name{":href" => ""} {{model.last_deployment.ref}} .icon-container.commit-icon = custom_icon("icon_commit") + %a.commit-id.monospace{":href" => ""} {{model.last_deployment.short_sha}} + {{model.last_deployment.sha}} %p.commit-title + %span{ "v-if" => "model.last_deployment.commit_title"} %a.commit-row-message{":href" => ""} {{model.last_deployment.commit_title}} + %span{ "v-if" => "!model.last_deployment.commit_title"} Cant find HEAD commit for this branch From dc46dc1d5f873e1a0aeaacca2b02614c9c79f79b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 12:53:08 +0100 Subject: [PATCH 036/302] Adds commit back --- app/controllers/projects/environments_controller.rb | 5 ++--- .../projects/environments/components/_commit.html.haml | 7 ++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 20b4ed46d0a..cb2420f9042 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -75,10 +75,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController include: { last_deployment: { include: { - user: { only: [:id, :name, :username], methods: [:avatar_url] }, - deployable: { only: [:id, :name, :ref, :tag] } + user: { only: [:id, :name, :username], methods: [:avatar_url] } }, - methods: [:short_sha, :commit_title, :commit] + methods: [:short_sha, :commit_title, :deployable, :commit] } } ) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index 3395b2fa240..b10a2e483ba 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -9,19 +9,24 @@ =icon("code-fork") %a.monospace.branch-name{":href" => ""} + -# need commits branch name url {{model.last_deployment.ref}} .icon-container.commit-icon = custom_icon("icon_commit") %a.commit-id.monospace{":href" => ""} + -# need commit url built with commit sha {{model.last_deployment.short_sha}} - {{model.last_deployment.sha}} %p.commit-title %span{ "v-if" => "model.last_deployment.commit_title"} + + -# need commit author username and avatar_url + commit author goes here %a.commit-row-message{":href" => ""} + -# need commit url built with commit sha {{model.last_deployment.commit_title}} %span{ "v-if" => "!model.last_deployment.commit_title"} From 843e5642369e20d138e2053e50de4481dcb3f9ef Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 15:40:16 +0100 Subject: [PATCH 037/302] Adds commit links --- app/controllers/projects/environments_controller.rb | 3 ++- app/views/projects/environments/components/_commit.html.haml | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index cb2420f9042..6858b6da3ea 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -78,7 +78,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController user: { only: [:id, :name, :username], methods: [:avatar_url] } }, methods: [:short_sha, :commit_title, :deployable, :commit] - } + }, + project: { methods: [:namespace]} } ) end diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index b10a2e483ba..cbf76960ac4 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -8,8 +8,7 @@ .icon-container{ "v-if" => "!model.last_deployment.tag" } =icon("code-fork") - %a.monospace.branch-name{":href" => ""} - -# need commits branch name url + %a.monospace.branch-name{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commits/' +model.last_deployment.ref"} {{model.last_deployment.ref}} .icon-container.commit-icon From b20fd5c74ae87bb4187603ef8f535c1c9004abbb Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 15:47:53 +0100 Subject: [PATCH 038/302] Adds commit sha link --- .../projects/environments/components/_commit.html.haml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index cbf76960ac4..1f923a2d0d6 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -14,8 +14,7 @@ .icon-container.commit-icon = custom_icon("icon_commit") - %a.commit-id.monospace{":href" => ""} - -# need commit url built with commit sha + %a.commit-id.monospace{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commit/' +model.last_deployment.sha"} {{model.last_deployment.short_sha}} %p.commit-title @@ -24,8 +23,7 @@ -# need commit author username and avatar_url commit author goes here - %a.commit-row-message{":href" => ""} - -# need commit url built with commit sha + %a.commit-row-message{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commit/' +model.last_deployment.sha"} {{model.last_deployment.commit_title}} %span{ "v-if" => "!model.last_deployment.commit_title"} From 40e25ea866b14c57fd858fe30d8ab8e86f8af04e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 18:24:48 +0100 Subject: [PATCH 039/302] Adds external url partial for vue component --- .../projects/environments/components/_environment.html.haml | 2 +- .../projects/environments/components/_external_url.html.haml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/environments/components/_external_url.html.haml diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 677ab9f60bd..deef1e034ca 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -48,7 +48,7 @@ %td.hidden-xs .pull-right{ "v-if" => "!isFolder"} - actions will go here + =render "projects/environments/components/external_url" %tr{"v-if" => "open && isFolder", "is" => "environment-item", diff --git a/app/views/projects/environments/components/_external_url.html.haml b/app/views/projects/environments/components/_external_url.html.haml new file mode 100644 index 00000000000..6cd405660cf --- /dev/null +++ b/app/views/projects/environments/components/_external_url.html.haml @@ -0,0 +1,4 @@ +%a.btn.external-url{ "v-if" => "!isFolder && model.external_url", + ":target" => "_blank", + ":href" => "model.external_url"} + = icon("external-link") \ No newline at end of file From 6b71ca0c46aab8612308823a5dfa01b193bc5eaf Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 22:16:05 +0100 Subject: [PATCH 040/302] Adds permissions verification to external url button --- .../environments/components/_external_url.html.haml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/projects/environments/components/_external_url.html.haml b/app/views/projects/environments/components/_external_url.html.haml index 6cd405660cf..dc224475b8b 100644 --- a/app/views/projects/environments/components/_external_url.html.haml +++ b/app/views/projects/environments/components/_external_url.html.haml @@ -1,4 +1,5 @@ -%a.btn.external-url{ "v-if" => "!isFolder && model.external_url", - ":target" => "_blank", - ":href" => "model.external_url"} - = icon("external-link") \ No newline at end of file +-if can?(current_user, :read_environment, @project) + %a.btn.external-url{ "v-if" => "!isFolder && model.external_url", + ":target" => "_blank", + ":href" => "model.external_url"} + = icon("external-link") \ No newline at end of file From 9d8de1c97f03b5fbeedcd0d867eecdd2eed4e1bc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 23:33:18 +0100 Subject: [PATCH 041/302] Adds actions --- .../environments/components/_actions.html.haml | 14 ++++++++++++++ .../components/_external_url.html.haml | 2 +- .../environments/components/_rollback.html.haml | 0 .../environments/components/_stop.html.haml | 0 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 app/views/projects/environments/components/_actions.html.haml create mode 100644 app/views/projects/environments/components/_rollback.html.haml create mode 100644 app/views/projects/environments/components/_stop.html.haml diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml new file mode 100644 index 00000000000..35a80c936db --- /dev/null +++ b/app/views/projects/environments/components/_actions.html.haml @@ -0,0 +1,14 @@ +- if can?(current_user, :create_deployment, @project) + .inline{ "v-if" => "model.last_deployment.manual_actions && model.last_deployment.manual_actions.present"} + .dropdown + %a.dropdown-new.btn.btn-default{type: "button", "data-toggle" => "dropdown"} + = custom_icon('icon_play') + = icon('caret-down') + + %ul.dropdown-menu.dropdown-menu-align-right + %li{ "v-for" => "action in model.last_deployment.manual_actions" } + -# transform this = link_to [:play, @project.namespace.becomes(Namespace), @project, action] into href + %a{data: {"method" => ":post", "rel" => "nofollow"}} + = custom_icon('icon_play') + %span + {{action.name}} \ No newline at end of file diff --git a/app/views/projects/environments/components/_external_url.html.haml b/app/views/projects/environments/components/_external_url.html.haml index dc224475b8b..9b789d0ed5f 100644 --- a/app/views/projects/environments/components/_external_url.html.haml +++ b/app/views/projects/environments/components/_external_url.html.haml @@ -2,4 +2,4 @@ %a.btn.external-url{ "v-if" => "!isFolder && model.external_url", ":target" => "_blank", ":href" => "model.external_url"} - = icon("external-link") \ No newline at end of file + = icon("external-link") diff --git a/app/views/projects/environments/components/_rollback.html.haml b/app/views/projects/environments/components/_rollback.html.haml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml new file mode 100644 index 00000000000..e69de29bb2d From d418d4990ee5ee0db3d73b8270e2466d1915ad4f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 23:33:41 +0100 Subject: [PATCH 042/302] Adds humanize function equivalent to ruby --- app/assets/javascripts/lib/utils/text_utility.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 98f9815ff05..efa25958b15 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -112,6 +112,9 @@ gl.text.removeListeners = function(form) { return $('.js-md', form).off(); }; + gl.text.humanize = function(string) { + return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); + } return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; From adb223442f7167cbb5049ce7088614694f902b15 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 23:34:03 +0100 Subject: [PATCH 043/302] Beautifies sent data into human readable formats --- .../stores/environmnets_store.js.es6 | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index dbf1bee05bb..7f0065bca8e 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -43,25 +43,41 @@ */ storeEnvironments(environments) { const environmentsTree = environments.reduce((acc, environment) => { + const data = Object.assign({}, environment); + + if (data.last_deployment) { + + //humanizes actions names if there are any actions + if (data.last_deployment.manual_actions) { + data.last_deployment.manual_actions = data.last_deployment.manual_actions.map((action) => Object.assign({}, action, {name: gl.text.humanize(action.name)})); + } + + //transforms created date for deployment in a human readable format + if (data.last_deployment.created_at) { + // TODO - how to do this without jquery + } + } + if (environment.environment_type !== null) { const occurs = acc.find((element, index, array) => { return element.name === environment.environment_type; }); + + data["vue-isChildren"] = true; if (occurs !== undefined) { - acc[acc.indexOf(occurs)].children.push( - Object.assign({}, environment, {"vue-isChildren": true})); + acc[acc.indexOf(occurs)].children.push(data); acc[acc.indexOf(occurs)].children.sort(); } else { acc.push({ name: environment.environment_type, children: [ - Object.assign({}, environment, {"vue-isChildren": true}) + Object.assign(data) ] }); } } else { - acc.push(environment); + acc.push(data); } return acc; From 070165f09bcbe63d079f67d5d0ccdc0259fa790b Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 21 Oct 2016 23:39:52 +0100 Subject: [PATCH 044/302] Adds stop template --- .../projects/environments/components/_stop.html.haml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml index e69de29bb2d..6e13a6a1ac4 100644 --- a/app/views/projects/environments/components/_stop.html.haml +++ b/app/views/projects/environments/components/_stop.html.haml @@ -0,0 +1,8 @@ +- if can?(current_user, :create_deployment, @project) + .inline{ "v-if" => "environment.stop_action"} + -# TODO: Fix this link + -# link_to stop_namespace_project_environment_path(@project.namespace, @project, environment) + %a{":href" => "", + "method" => ":post", + "rel" => "nofollow", "confirm" => "Are you sure you want to stop this environment?"} + = icon('stop', class: 'stop-env-icon') From 3ea611e73f76fca4e773deb4e953617d5588fab5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 11:03:08 +0100 Subject: [PATCH 045/302] Adds counter for number of children in each folder --- .../components/environment_item.js.es6 | 15 +++++++++++++-- app/assets/stylesheets/pages/environments.scss | 9 ++++++++- .../components/_environment.html.haml | 3 +++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 961396b46b7..5159d5c4dee 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -37,10 +37,10 @@ * Folder items have different behaviours - it is possible to toggle * them and show their children. * - * @returns {Number} The length of the children array + * @returns {Boolean} */ isFolder () { - return this.model.children && this.model.children.length + return this.model.children && this.model.children.length ? true : false; }, /** @@ -51,6 +51,17 @@ */ isChildren () { return this.model['vue-isChildren']; + }, + + + /** + * Counts the number of environments in each folder. + * Used to show a badge with the counter. + * + * @returns {Boolean} The number of environments for the current folder + */ + childrenCounter () { + return this.model.children && this.model.children.length; } }, diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 973820e8740..ed0151bb120 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -64,7 +64,7 @@ } } - .children-row .environment-name{ + .children-row .environment-name { margin-left: 17px; margin-right: -17px; } @@ -75,6 +75,13 @@ .folder-name { cursor: pointer; + + .badge { + font-weight: normal; + background-color: $gray-darker; + color: $gl-placeholder-color; + vertical-align: baseline; + } } } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index deef1e034ca..7e8bcbaa67d 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -14,6 +14,9 @@ =icon("caret-right") {{model.name}} + + %span.badge + {{childrenCounter}} %td.deployment-column %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid"} From 79e9b89069cbde9c76b8876a2e5296cf4356830e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 11:18:34 +0100 Subject: [PATCH 046/302] Sort environments by name --- .../stores/environmnets_store.js.es6 | 21 ++++++++++++++++--- .../components/_environment.html.haml | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 7f0065bca8e..8b5cb67ed37 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -60,14 +60,14 @@ if (environment.environment_type !== null) { const occurs = acc.find((element, index, array) => { - return element.name === environment.environment_type; + return element.environment_type === environment.environment_type; }); data["vue-isChildren"] = true; if (occurs !== undefined) { acc[acc.indexOf(occurs)].children.push(data); - acc[acc.indexOf(occurs)].children.sort(); + acc[acc.indexOf(occurs)].children.push(data).sort(this.sortByName) } else { acc.push({ name: environment.environment_type, @@ -81,11 +81,26 @@ } return acc; - }, []).sort(); + }, []).sort(this.sortByName); this.state.environments = environmentsTree; return environmentsTree; + }, + + sortByName (a,b) { + const nameA = a.name.toUpperCase(); + const nameB = b.name.toUpperCase(); + + if (nameA < nameB) { + return -1; + } + + if (nameA > nameB) { + return 1; + } + + return 0; } } })(); diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 7e8bcbaa67d..6cf0c1f3999 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -52,6 +52,7 @@ %td.hidden-xs .pull-right{ "v-if" => "!isFolder"} =render "projects/environments/components/external_url" + =render "projects/environments/components/actions" %tr{"v-if" => "open && isFolder", "is" => "environment-item", From 667b71a40b158ff3b0db4f34efdd1efc59c729c0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 11:36:35 +0100 Subject: [PATCH 047/302] Close button --- .../projects/environments/components/_actions.html.haml | 2 +- .../environments/components/_environment.html.haml | 2 ++ .../projects/environments/components/_stop.html.haml | 8 +++----- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml index 35a80c936db..50a731475ba 100644 --- a/app/views/projects/environments/components/_actions.html.haml +++ b/app/views/projects/environments/components/_actions.html.haml @@ -1,5 +1,5 @@ - if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "model.last_deployment.manual_actions && model.last_deployment.manual_actions.present"} + .inline{ "v-if" => "model.last_deployment && model.last_deployment.manual_actions && model.last_deployment.manual_actions.present"} .dropdown %a.dropdown-new.btn.btn-default{type: "button", "data-toggle" => "dropdown"} = custom_icon('icon_play') diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 6cf0c1f3999..e479d5ef85b 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -53,6 +53,8 @@ .pull-right{ "v-if" => "!isFolder"} =render "projects/environments/components/external_url" =render "projects/environments/components/actions" + =render "projects/environments/components/stop" + =render "projects/environments/components/rollback" %tr{"v-if" => "open && isFolder", "is" => "environment-item", diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml index 6e13a6a1ac4..741a72704c5 100644 --- a/app/views/projects/environments/components/_stop.html.haml +++ b/app/views/projects/environments/components/_stop.html.haml @@ -1,8 +1,6 @@ - if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "environment.stop_action"} - -# TODO: Fix this link - -# link_to stop_namespace_project_environment_path(@project.namespace, @project, environment) - %a{":href" => "", + .inline{ "v-if" => "model.stop_action" } + %a.btn.stop-env-link{":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id", "method" => ":post", "rel" => "nofollow", "confirm" => "Are you sure you want to stop this environment?"} - = icon('stop', class: 'stop-env-icon') + = icon("stop", class: "stop-env-icon") From a265826e01dec5341521deb029d6d3aa93ffa6d4 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 11:56:54 +0100 Subject: [PATCH 048/302] Adds rollback button --- .../environments/components/_rollback.html.haml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/views/projects/environments/components/_rollback.html.haml b/app/views/projects/environments/components/_rollback.html.haml index e69de29bb2d..bcc54187b6d 100644 --- a/app/views/projects/environments/components/_rollback.html.haml +++ b/app/views/projects/environments/components/_rollback.html.haml @@ -0,0 +1,10 @@ +- if can?(current_user, :create_deployment, @project) + %a.btn.btn-build{ "v-if" => "model.last_deployment && model.last_deployment.deployable", + ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id + '/retry'", + "data-method" => "post", + "rel" => "nofollow" } + + %span{ "v-if" => "model.last_deployment.last"} + Re-deploy + %span{ "v-if" => "!model.last_deployment.last"} + Rollback From d697b9c86d76603a6eda906572e37ccb049e78cd Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 12:42:01 +0100 Subject: [PATCH 049/302] Adds link to actions. Cleans spaces --- .../components/environment_item.js.es6 | 7 ++- .../stores/environmnets_store.js.es6 | 21 ++++++--- .../stylesheets/pages/environments.scss | 1 - .../projects/environments_controller.rb | 7 +-- .../components/_actions.html.haml | 6 ++- .../components/_environment.html.haml | 44 +++++++++---------- .../components/_rollback.html.haml | 4 +- .../environments/components/_stop.html.haml | 3 +- 8 files changed, 51 insertions(+), 42 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 5159d5c4dee..df9f28b1c59 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -29,7 +29,7 @@ open: false }; }, - + computed: { /** @@ -42,7 +42,7 @@ isFolder () { return this.model.children && this.model.children.length ? true : false; }, - + /** * If an item is inside a folder structure will return true. * Used for css purposes. @@ -52,8 +52,7 @@ isChildren () { return this.model['vue-isChildren']; }, - - + /** * Counts the number of environments in each folder. * Used to show a badge with the counter. diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 8b5cb67ed37..35563b0d7fa 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -1,14 +1,14 @@ (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - + gl.environmentsList.EnvironmentsStore = { state: {}, - + create () { this.state.environments = []; }, - + /** * In order to display a tree view we need to modify the received * data in to a tree structure based on `environment_type` @@ -87,15 +87,22 @@ return environmentsTree; }, - - sortByName (a,b) { + + /** + * 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(); - + if (nameA < nameB) { return -1; } - + if (nameA > nameB) { return 1; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index ed0151bb120..2565036eae7 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -86,7 +86,6 @@ } .table.ci-table.environments { - .icon-container { width: 20px; text-align: center; diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 6858b6da3ea..b9e2187bc3b 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -75,11 +75,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController include: { last_deployment: { include: { - user: { only: [:id, :name, :username], methods: [:avatar_url] } + user: { only: [:id, :name, :username], methods: [:avatar_url] }, + deployable: { only: [:id, :name, :ref, :tag] } }, - methods: [:short_sha, :commit_title, :deployable, :commit] + methods: [:short_sha, :commit_title, :commit] }, - project: { methods: [:namespace]} + project: { methods: [:namespace] } } ) end diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml index 50a731475ba..82acda0de9c 100644 --- a/app/views/projects/environments/components/_actions.html.haml +++ b/app/views/projects/environments/components/_actions.html.haml @@ -7,8 +7,10 @@ %ul.dropdown-menu.dropdown-menu-align-right %li{ "v-for" => "action in model.last_deployment.manual_actions" } - -# transform this = link_to [:play, @project.namespace.becomes(Namespace), @project, action] into href - %a{data: {"method" => ":post", "rel" => "nofollow"}} + %a{ ":ref" => "'#{namespace_project_path(@project.namespace, @project)}/' + action.id + '/play'", + "data-method" => "post", + "rel" => "nofollow" } + = custom_icon('icon_play') %span {{action.name}} \ No newline at end of file diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index e479d5ef85b..1aa0d7ac5e2 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -1,62 +1,62 @@ %script#environment-item-template{ "type"=> "text/x-template" } %tr - %td{"v-bind:class" => "{ 'children-row': isChildren}"} + %td{"v-bind:class" => "{ 'children-row': isChildren}" } %a.environment-name{ "v-if" => "!isFolder", ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } {{model.name}} - + %span.folder-name{ "v-if" => "isFolder", "@click" => "toggle" } - - %i.folder-icon{ "v-show" => "open"} + + %i.folder-icon{ "v-show" => "open" } =icon ("caret-down") - %i.folder-icon{ "v-show" => "!open"} + %i.folder-icon{ "v-show" => "!open" } =icon("caret-right") - + {{model.name}} - + %span.badge {{childrenCounter}} - + %td.deployment-column - %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid"} + %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } = precede "#" do {{model.last_deployment.iid}} - %span{ "v-if" => "model.last_deployment.user"} + %span{ "v-if" => "model.last_deployment.user" } by - %a{":href" => "'/' + model.last_deployment.user.username"} + %a{":href" => "'/' + model.last_deployment.user.username" } %img.avatar.has_tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", ":alt" => "model.last_deployment.user.username + 'avatar'", ":title" => "model.last_deployment.user.username", - data: {container: "body"}, + data: {container: "body" }, width: 20, height: 20} - + %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id"} + ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id" } {{model.last_deployment.deployable.name}} = precede "#" do {{model.last_deployment.deployable.id}} - + %td =render "projects/environments/components/commit" - %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment"} + %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment" } No deployments yet - + %td - %span{ "v-if" => "!isFolder && model.last_deployment"} + %span{ "v-if" => "!isFolder && model.last_deployment" } {{model.last_deployment.created_at}} - + %td.hidden-xs - .pull-right{ "v-if" => "!isFolder"} + .pull-right{ "v-if" => "!isFolder" } =render "projects/environments/components/external_url" =render "projects/environments/components/actions" =render "projects/environments/components/stop" =render "projects/environments/components/rollback" - + %tr{"v-if" => "open && isFolder", "is" => "environment-item", "v-for" => "model in model.children", - ":model" => "model"} + ":model" => "model" } diff --git a/app/views/projects/environments/components/_rollback.html.haml b/app/views/projects/environments/components/_rollback.html.haml index bcc54187b6d..2258f6ec32d 100644 --- a/app/views/projects/environments/components/_rollback.html.haml +++ b/app/views/projects/environments/components/_rollback.html.haml @@ -4,7 +4,7 @@ "data-method" => "post", "rel" => "nofollow" } - %span{ "v-if" => "model.last_deployment.last"} + %span{ "v-if" => "model.last_deployment.last" } Re-deploy - %span{ "v-if" => "!model.last_deployment.last"} + %span{ "v-if" => "!model.last_deployment.last" } Rollback diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml index 741a72704c5..55cccf9e72e 100644 --- a/app/views/projects/environments/components/_stop.html.haml +++ b/app/views/projects/environments/components/_stop.html.haml @@ -2,5 +2,6 @@ .inline{ "v-if" => "model.stop_action" } %a.btn.stop-env-link{":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id", "method" => ":post", - "rel" => "nofollow", "confirm" => "Are you sure you want to stop this environment?"} + "rel" => "nofollow", + "confirm" => "Are you sure you want to stop this environment?"} = icon("stop", class: "stop-env-icon") From 9c41191a002c088ecb269b1e69606000be9be005 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 16:10:58 +0100 Subject: [PATCH 050/302] Adds tab behavior to vue component --- .../environments/environments_bundle.js.es6 | 46 ++++++++++++++++++- .../stores/environmnets_store.js.es6 | 2 +- .../projects/environments_controller.rb | 10 +--- .../projects/environments/index.html.haml | 14 +++--- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 027dc7fb211..4dc4ac2c99b 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -15,6 +15,15 @@ $(() => { gl.EnvironmentsListApp.$destroy(true); } + const filters = { + stopped (environments) { + return environments.filter((env) => env.state === 'stopped') + }, + available (environments) { + return environments.filter((env) => env.state === 'available') + } + }; + gl.EnvironmentsListApp = new Vue({ el: '#environments-list-view', @@ -26,13 +35,33 @@ $(() => { data: { state: Store.state, endpoint: environmentsListApp.dataset.endpoint, - loading: true + loading: true, + visibility: 'available' + }, + + computed: { + filteredEnvironments () { + return filters[this.visibility](this.state.environments); + }, + + countStopped () { + return filters['stopped'](this.state.environments).length; + }, + + counAvailable () { + return filters['available'](this.state.environments).length; + } }, init: Store.create.bind(Store), created() { gl.environmentsService = new EnvironmentsService(this.endpoint); + + const scope = this.$options.getQueryParameter('scope'); + if (scope) { + this.visibility = scope; + } }, /** @@ -45,6 +74,21 @@ $(() => { this.loading = false; }); + }, + + /** + * Transforms the url parameter into an object and + * returns the one requested. + * + * @param {String} param + * @returns {String} The value of the requested parameter. + */ + getQueryParameter(param) { + return window.location.search.substring(1).split('&').reduce((acc, param) => { + acc[param.split('=')[0]] = param.split('=')[1]; + return acc; + }, {})[param]; } + }); }); diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 35563b0d7fa..f48e8d4c49a 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -82,7 +82,7 @@ return acc; }, []).sort(this.sortByName); - + this.state.environments = environmentsTree; return environmentsTree; diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index b9e2187bc3b..42f882b55ab 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -8,14 +8,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController def index @scope = params[:scope] - @all_environments = project.environments - @environments = - if @scope == 'stopped' - @all_environments.stopped - else - @all_environments.available - end - + @environments = project.environments + respond_to do |format| format.html format.json do diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a8667d88e7b..7cec3fa281c 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -4,28 +4,28 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") - -%div{ class: container_class } + +#environments-list-view{ data: environments_list_data, class: container_class } .top-area - %ul.nav-links + %ul.nav-links{ "v-if" => "!loading" } %li{class: ('active' if @scope.nil?)} = link_to project_environments_path(@project) do Available %span.badge.js-available-environments-count - = number_with_delimiter(@all_environments.available.count) + {{counAvailable}} %li{class: ('active' if @scope == 'stopped')} = link_to project_environments_path(@project, scope: :stopped) do Stopped %span.badge.js-stopped-environments-count - = number_with_delimiter(@all_environments.stopped.count) + {{countStopped}} - if can?(current_user, :create_environment, @project) && !@all_environments.blank? .nav-controls = link_to new_namespace_project_environment_path(@project.namespace, @project), class: "btn btn-create" do New environment - #environments-list-view{ data: environments_list_data, class: "environments-container" } + .environments-container .environments-list-loading.text-center{ "v-if" => "loading" } = icon("spinner spin") @@ -51,6 +51,6 @@ %th %th.hidden-xs %tbody - %tr{"is" => "environment-item", "v-for" => "model in state.environments", ":model" => "model"} + %tr{"is" => "environment-item", "v-for" => "model in filteredEnvironments", ":model" => "model"} =render "projects/environments/components/environment" From 98a0f72dd3717c0177271583a19973ce463b5191 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 24 Oct 2016 16:56:13 +0100 Subject: [PATCH 051/302] Properly filter all environments --- .../environments/environments_bundle.js.es6 | 27 ++++++++++--------- .../stores/environmnets_store.js.es6 | 1 + 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 4dc4ac2c99b..f8cefabefa4 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -15,19 +15,20 @@ $(() => { gl.EnvironmentsListApp.$destroy(true); } - const filters = { - stopped (environments) { - return environments.filter((env) => env.state === 'stopped') - }, - available (environments) { - return environments.filter((env) => env.state === 'available') - } + const filterEnvironments = (environments = [], filter = "") => { + return environments.filter((env) => { + if (env.children) { + return env.children.filter((child) => child.state === filter).length; + } else { + return env.state === filter; + }; + }); }; gl.EnvironmentsListApp = new Vue({ el: '#environments-list-view', - + components: { 'item': gl.environmentsList.EnvironmentItem }, @@ -41,15 +42,15 @@ $(() => { computed: { filteredEnvironments () { - return filters[this.visibility](this.state.environments); + return filterEnvironments(this.state.environments, this.visibility); }, - + countStopped () { - return filters['stopped'](this.state.environments).length; + return filterEnvironments(this.state.environments, 'stopped').length; }, - + counAvailable () { - return filters['available'](this.state.environments).length; + return filterEnvironments(this.state.environments, 'available').length; } }, diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index f48e8d4c49a..72267b97255 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -63,6 +63,7 @@ return element.environment_type === environment.environment_type; }); + data["vue-isChildren"] = true; if (occurs !== undefined) { From 32c727cb9b8f0d8dc6de349482d49f81d8067656 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Oct 2016 10:08:01 +0100 Subject: [PATCH 052/302] Fix typo --- .../javascripts/environments/environments_bundle.js.es6 | 8 ++++---- .../environments/stores/environmnets_store.js.es6 | 2 +- app/views/projects/environments/index.html.haml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index f8cefabefa4..4c1fea0d6c6 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -24,7 +24,7 @@ $(() => { }; }); }; - + gl.EnvironmentsListApp = new Vue({ el: '#environments-list-view', @@ -49,7 +49,7 @@ $(() => { return filterEnvironments(this.state.environments, 'stopped').length; }, - counAvailable () { + countAvailable () { return filterEnvironments(this.state.environments, 'available').length; } }, @@ -86,10 +86,10 @@ $(() => { */ getQueryParameter(param) { return window.location.search.substring(1).split('&').reduce((acc, param) => { - acc[param.split('=')[0]] = param.split('=')[1]; + const paramSplited = param.split('='); + acc[paramSplited[0]] = paramSplited[1]; return acc; }, {})[param]; } - }); }); diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 72267b97255..9442a1ee1de 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -41,7 +41,7 @@ * @param {Array} environments List of environments. * @returns {Array} Tree structured array with the received environments. */ - storeEnvironments(environments) { + storeEnvironments(environments = []) { const environmentsTree = environments.reduce((acc, environment) => { const data = Object.assign({}, environment); diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 7cec3fa281c..3538777d11c 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -12,7 +12,7 @@ = link_to project_environments_path(@project) do Available %span.badge.js-available-environments-count - {{counAvailable}} + {{countAvailable}} %li{class: ('active' if @scope == 'stopped')} = link_to project_environments_path(@project, scope: :stopped) do @@ -41,7 +41,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .table-holder{ "v-if" => "!loading && state.environments" } + .table-holder{ "v-if" => "!loading && state.environments.length" } %table.table.ci-table.environments %thead %th Environment From cd476336d9c8f07c5a70fb05f646727965139706 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Oct 2016 13:50:27 +0100 Subject: [PATCH 053/302] tests --- .../environments/environments_bundle.js.es6 | 3 + .../projects/environments/index.html.haml | 2 +- spec/features/environments_spec.rb | 486 ++++++++++-------- 3 files changed, 264 insertions(+), 227 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 4c1fea0d6c6..0ab3afb405c 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -3,6 +3,7 @@ //= require_tree ./stores //= require_tree ./services //= require ./components/environment_item +//= require ../boards/vue_resource_interceptor $(() => { @@ -72,6 +73,8 @@ $(() => { ready() { gl.environmentsService.all().then((resp) => { Store.storeEnvironments(resp.json()); + + console.log("HELLLLOOOOOOOOOOOOOO", resp.json()) this.loading = false; }); diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 3538777d11c..5b9fedb956f 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -41,7 +41,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .table-holder{ "v-if" => "!loading && state.environments.length" } + .table-holder{ "v-if" => "!loading && state.environments" } %table.table.ci-table.environments %thead %th Environment diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index b565586ee14..c8ea4bb3945 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -1,7 +1,10 @@ require 'spec_helper' +require 'rails_helper' -feature 'Environments', feature: true do - given(:project) { create(:empty_project) } +feature 'Environments', feature: true, js:true do + include WaitForVueResource + + given(:project) { create(:project) } given(:user) { create(:user) } given(:role) { :developer } @@ -15,18 +18,17 @@ feature 'Environments', feature: true do given!(:deployment) { } given!(:manual) { } - before do - visit namespace_project_environments_path(project.namespace, project) - end - - context 'shows two tabs' do - scenario 'shows "Available" and "Stopped" tab with links' do + context 'without environments' do + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource + end + + scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Available') expect(page).to have_link('Stopped') end - end - context 'without environments' do scenario 'does show no environments' do expect(page).to have_content('You don\'t have any environments right now.') end @@ -36,239 +38,271 @@ feature 'Environments', feature: true do expect(page.find('.js-stopped-environments-count').text).to eq('0') end end - - context 'with environments' do + + context 'loading environments' do given(:environment) { create(:environment, project: project) } - - scenario 'does show environment name' do - expect(page).to have_link(environment.name) + + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource end + scenario 'does show loading spinner' do + expect(page).to have_selector('.environments-list-loading') + end + end + + context 'with environments' do + let(:environment1) { create(:environment, project: project) } + let(:environment2) { create(:environment, project: project) } + + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource + expect(page).to have_selector('.table.ci-table.environments') + end + + scenario 'does show "Available" and "Stopped" tab with links' do + expect(page).to have_link('Stopped') + expect(page).to have_link('Available') + end + + scenario 'does show environment name' do + expect(page).to have_link(environment1.name) + end + scenario 'does show number of available and stopped environments' do expect(page.find('.js-available-environments-count').text).to eq('1') expect(page.find('.js-stopped-environments-count').text).to eq('0') end - + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') end end - context 'with deployments' do - given(:deployment) { create(:deployment, environment: environment) } - - scenario 'does show deployment SHA' do - expect(page).to have_link(deployment.short_sha) - end - - scenario 'does show deployment internal id' do - expect(page).to have_content(deployment.iid) - end - - context 'with build and manual actions' do - given(:pipeline) { create(:ci_pipeline, project: project) } - given(:build) { create(:ci_build, pipeline: pipeline) } - given(:deployment) { create(:deployment, environment: environment, deployable: build) } - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - - scenario 'does show a play button' do - expect(page).to have_link(manual.name.humanize) - end - - scenario 'does allow to play manual action' do - expect(manual).to be_skipped - expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - expect(page).to have_content(manual.name) - expect(manual.reload).to be_pending - end - - scenario 'does show build name and id' do - expect(page).to have_link("#{build.name} (##{build.id})") - end - - scenario 'does not show stop button' do - expect(page).not_to have_selector('.stop-env-link') - end - - scenario 'does not show external link button' do - expect(page).not_to have_css('external-url') - end - - context 'with external_url' do - given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } - given(:build) { create(:ci_build, pipeline: pipeline) } - given(:deployment) { create(:deployment, environment: environment, deployable: build) } - - scenario 'does show an external link button' do - expect(page).to have_link(nil, href: environment.external_url) - end - end - - context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - - scenario 'does show stop button' do - expect(page).to have_selector('.stop-env-link') - end - - scenario 'starts build when stop button clicked' do - first('.stop-env-link').click - - expect(page).to have_content('close_app') - end - - context 'for reporter' do - let(:role) { :reporter } - - scenario 'does not show stop button' do - expect(page).not_to have_selector('.stop-env-link') - end - end - end - end - end + # context 'with deployments' do + # given(:deployment) { create(:deployment, environment: environment) } + # + # scenario 'does show deployment SHA' do + # expect(page).to have_link(deployment.short_sha) + # end + # + # scenario 'does show deployment internal id' do + # expect(page).to have_content(deployment.iid) + # end + # + # context 'with build and manual actions' do + # given(:pipeline) { create(:ci_pipeline, project: project) } + # given(:build) { create(:ci_build, pipeline: pipeline) } + # given(:deployment) { create(:deployment, environment: environment, deployable: build) } + # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + # + # scenario 'does show a play button' do + # expect(page).to have_link(manual.name.humanize) + # end + # + # scenario 'does allow to play manual action' do + # expect(manual).to be_skipped + # expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + # expect(page).to have_content(manual.name) + # expect(manual.reload).to be_pending + # end + # + # scenario 'does show build name and id' do + # expect(page).to have_link("#{build.name} (##{build.id})") + # end + # + # scenario 'does not show stop button' do + # expect(page).not_to have_selector('.stop-env-link') + # end + # + # scenario 'does not show external link button' do + # expect(page).not_to have_css('external-url') + # end + # + # # context 'with external_url' do + # given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } + # given(:build) { create(:ci_build, pipeline: pipeline) } + # given(:deployment) { create(:deployment, environment: environment, deployable: build) } + # + # scenario 'does show an external link button' do + # expect(page).to have_link(nil, href: environment.external_url) + # end + # end + # + # # context 'with stop action' do + # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + # given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + # + # scenario 'does show stop button' do + # expect(page).to have_selector('.stop-env-link') + # end + # + # scenario 'starts build when stop button clicked' do + # first('.stop-env-link').click + # + # expect(page).to have_content('close_app') + # end + # + # context 'for reporter' do + # let(:role) { :reporter } + # + # scenario 'does not show stop button' do + # expect(page).not_to have_selector('.stop-env-link') + # end + # end + # end + # end + # end end - - scenario 'does have a New environment button' do - expect(page).to have_link('New environment') - end - end - - describe 'when showing the environment' do - given(:environment) { create(:environment, project: project) } - given!(:deployment) { } - given!(:manual) { } - - before do - visit namespace_project_environment_path(project.namespace, project, environment) - end - - context 'without deployments' do - scenario 'does show no deployments' do - expect(page).to have_content('You don\'t have any deployments right now.') - end - end - - context 'with deployments' do - given(:deployment) { create(:deployment, environment: environment) } - - scenario 'does show deployment SHA' do - expect(page).to have_link(deployment.short_sha) - end - - scenario 'does not show a re-deploy button for deployment without build' do - expect(page).not_to have_link('Re-deploy') - end - - context 'with build' do - given(:pipeline) { create(:ci_pipeline, project: project) } - given(:build) { create(:ci_build, pipeline: pipeline) } - given(:deployment) { create(:deployment, environment: environment, deployable: build) } - - scenario 'does show build name' do - expect(page).to have_link("#{build.name} (##{build.id})") - end - - scenario 'does show re-deploy button' do - expect(page).to have_link('Re-deploy') - end - - scenario 'does not show stop button' do - expect(page).not_to have_link('Stop') - end - - context 'with manual action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - - scenario 'does show a play button' do - expect(page).to have_link(manual.name.humanize) - end - - scenario 'does allow to play manual action' do - expect(manual).to be_skipped - expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - expect(page).to have_content(manual.name) - expect(manual.reload).to be_pending - end - - context 'with external_url' do - given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } - given(:build) { create(:ci_build, pipeline: pipeline) } - given(:deployment) { create(:deployment, environment: environment, deployable: build) } - - scenario 'does show an external link button' do - expect(page).to have_link(nil, href: environment.external_url) - end - end - - context 'with stop action' do - given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - - scenario 'does show stop button' do - expect(page).to have_link('Stop') - end - - scenario 'does allow to stop environment' do - click_link('Stop') - - expect(page).to have_content('close_app') - end - - context 'for reporter' do - let(:role) { :reporter } - - scenario 'does not show stop button' do - expect(page).not_to have_link('Stop') - end - end - end - end - end - end - end - - describe 'when creating a new environment' do - before do - visit namespace_project_environments_path(project.namespace, project) - end - - context 'when logged as developer' do + + context 'can create new environment' do before do - click_link 'New environment' + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource end - - context 'for valid name' do - before do - fill_in('Name', with: 'production') - click_on 'Save' - end - - scenario 'does create a new pipeline' do - expect(page).to have_content('Production') - end - end - - context 'for invalid name' do - before do - fill_in('Name', with: 'name,with,commas') - click_on 'Save' - end - - scenario 'does show errors' do - expect(page).to have_content('Name can contain only letters') - end - end - end - - context 'when logged as reporter' do - given(:role) { :reporter } - - scenario 'does not have a New environment link' do - expect(page).not_to have_link('New environment') + + scenario 'does have a New environment button' do + expect(page).to have_link('New environment') end end end + + # describe 'when showing the environment' do + # given(:environment) { create(:environment, project: project) } + # given!(:deployment) { } + # given!(:manual) { } + # + # before do + # visit namespace_project_environment_path(project.namespace, project, environment) + # end + # + # context 'without deployments' do + # scenario 'does show no deployments' do + # expect(page).to have_content('You don\'t have any deployments right now.') + # end + # end + # + # context 'with deployments' do + # given(:deployment) { create(:deployment, environment: environment) } + # + # scenario 'does show deployment SHA' do + # expect(page).to have_link(deployment.short_sha) + # end + # + # scenario 'does not show a re-deploy button for deployment without build' do + # expect(page).not_to have_link('Re-deploy') + # end + # + # context 'with build' do + # given(:pipeline) { create(:ci_pipeline, project: project) } + # given(:build) { create(:ci_build, pipeline: pipeline) } + # given(:deployment) { create(:deployment, environment: environment, deployable: build) } + # + # scenario 'does show build name' do + # expect(page).to have_link("#{build.name} (##{build.id})") + # end + # + # scenario 'does show re-deploy button' do + # expect(page).to have_link('Re-deploy') + # end + # + # scenario 'does not show stop button' do + # expect(page).not_to have_link('Stop') + # end + # + # context 'with manual action' do + # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + # + # scenario 'does show a play button' do + # expect(page).to have_link(manual.name.humanize) + # end + # + # scenario 'does allow to play manual action' do + # expect(manual).to be_skipped + # expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + # expect(page).to have_content(manual.name) + # expect(manual.reload).to be_pending + # end + # + # context 'with external_url' do + # given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } + # given(:build) { create(:ci_build, pipeline: pipeline) } + # given(:deployment) { create(:deployment, environment: environment, deployable: build) } + # + # scenario 'does show an external link button' do + # expect(page).to have_link(nil, href: environment.external_url) + # end + # end + # + # context 'with stop action' do + # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + # given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + # + # scenario 'does show stop button' do + # expect(page).to have_link('Stop') + # end + # + # scenario 'does allow to stop environment' do + # click_link('Stop') + # + # expect(page).to have_content('close_app') + # end + # + # context 'for reporter' do + # let(:role) { :reporter } + # + # scenario 'does not show stop button' do + # expect(page).not_to have_link('Stop') + # end + # end + # end + # end + # end + # end + # end + + # describe 'when creating a new environment' do + # before do + # visit namespace_project_environments_path(project.namespace, project) + # end + # + # context 'when logged as developer' do + # before do + # click_link 'New environment' + # end + # + # context 'for valid name' do + # before do + # fill_in('Name', with: 'production') + # click_on 'Save' + # end + # + # scenario 'does create a new pipeline' do + # expect(page).to have_content('Production') + # end + # end + # + # context 'for invalid name' do + # before do + # fill_in('Name', with: 'name,with,commas') + # click_on 'Save' + # end + # + # scenario 'does show errors' do + # expect(page).to have_content('Name can contain only letters') + # end + # end + # end + # + # context 'when logged as reporter' do + # given(:role) { :reporter } + # + # scenario 'does not have a New environment link' do + # expect(page).not_to have_link('New environment') + # end + # end + # end end From 1906a32bc03179aa15777073de0ff20547bc7f6a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Oct 2016 14:55:57 +0100 Subject: [PATCH 054/302] Fix SCSS lint errors Fixes some tests --- .../environments/environments_bundle.js.es6 | 3 - .../stores/environmnets_store.js.es6 | 21 +- .../stylesheets/pages/environments.scss | 8 +- .../projects/environments/index.html.haml | 2 +- spec/features/environments_spec.rb | 473 +++++++++--------- 5 files changed, 252 insertions(+), 255 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 0ab3afb405c..3382e7a4b29 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -73,9 +73,6 @@ $(() => { ready() { gl.environmentsService.all().then((resp) => { Store.storeEnvironments(resp.json()); - - console.log("HELLLLOOOOOOOOOOOOOO", resp.json()) - this.loading = false; }); }, diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 9442a1ee1de..9416b944b4d 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -43,17 +43,16 @@ */ storeEnvironments(environments = []) { const environmentsTree = environments.reduce((acc, environment) => { - const data = Object.assign({}, environment); - - if (data.last_deployment) { + + if (environment.last_deployment) { //humanizes actions names if there are any actions - if (data.last_deployment.manual_actions) { - data.last_deployment.manual_actions = data.last_deployment.manual_actions.map((action) => Object.assign({}, action, {name: gl.text.humanize(action.name)})); + if (environment.last_deployment.manual_actions) { + environment.last_deployment.manual_actions = environment.last_deployment.manual_actions.map((action) => Object.assign({}, action, {name: gl.text.humanize(action.name)})); } //transforms created date for deployment in a human readable format - if (data.last_deployment.created_at) { + if (environment.last_deployment.created_at) { // TODO - how to do this without jquery } } @@ -64,21 +63,21 @@ }); - data["vue-isChildren"] = true; + environment["vue-isChildren"] = true; if (occurs !== undefined) { - acc[acc.indexOf(occurs)].children.push(data); - acc[acc.indexOf(occurs)].children.push(data).sort(this.sortByName) + acc[acc.indexOf(occurs)].children.push(environment); + acc[acc.indexOf(occurs)].children.push(environment).sort(this.sortByName) } else { acc.push({ name: environment.environment_type, children: [ - Object.assign(data) + Object.assign(environment) ] }); } } else { - acc.push(data); + acc.push(environment); } return acc; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 2565036eae7..9d47e96bb69 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -63,19 +63,19 @@ } } } - + .children-row .environment-name { margin-left: 17px; margin-right: -17px; } - + .folder-icon { padding: 0 5px 0 0; } - + .folder-name { cursor: pointer; - + .badge { font-weight: normal; background-color: $gray-darker; diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 5b9fedb956f..3538777d11c 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -41,7 +41,7 @@ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - .table-holder{ "v-if" => "!loading && state.environments" } + .table-holder{ "v-if" => "!loading && state.environments.length" } %table.table.ci-table.environments %thead %th Environment diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index c8ea4bb3945..62fc56f2f46 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -12,18 +12,32 @@ feature 'Environments', feature: true, js:true do login_as(user) project.team << [user, role] end - - describe 'when showing environments' do + + describe 'Loading environments' do given!(:environment) { } given!(:deployment) { } given!(:manual) { } - context 'without environments' do + context 'loading environments' do before do visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource end - + + scenario 'does show loading spinner' do + expect(page).to have_selector('.environments-list-loading') + end + end + end + + describe 'when showing environments' do + + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource + end + + context 'without environments' do + scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Available') expect(page).to have_link('Stopped') @@ -38,37 +52,17 @@ feature 'Environments', feature: true, js:true do expect(page.find('.js-stopped-environments-count').text).to eq('0') end end - - context 'loading environments' do - given(:environment) { create(:environment, project: project) } - - before do - visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource - end - - scenario 'does show loading spinner' do - expect(page).to have_selector('.environments-list-loading') - end - end context 'with environments' do - let(:environment1) { create(:environment, project: project) } - let(:environment2) { create(:environment, project: project) } - - before do - visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource - expect(page).to have_selector('.table.ci-table.environments') - end - + given!(:environment) { create(:environment, project: project) } + scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Stopped') expect(page).to have_link('Available') end scenario 'does show environment name' do - expect(page).to have_link(environment1.name) + expect(page).to have_link(environment.name) end scenario 'does show number of available and stopped environments' do @@ -77,85 +71,92 @@ feature 'Environments', feature: true, js:true do end context 'without deployments' do + + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource + end + scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') end end - # context 'with deployments' do - # given(:deployment) { create(:deployment, environment: environment) } - # - # scenario 'does show deployment SHA' do - # expect(page).to have_link(deployment.short_sha) - # end - # - # scenario 'does show deployment internal id' do - # expect(page).to have_content(deployment.iid) - # end - # - # context 'with build and manual actions' do - # given(:pipeline) { create(:ci_pipeline, project: project) } - # given(:build) { create(:ci_build, pipeline: pipeline) } - # given(:deployment) { create(:deployment, environment: environment, deployable: build) } - # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - # - # scenario 'does show a play button' do - # expect(page).to have_link(manual.name.humanize) - # end - # - # scenario 'does allow to play manual action' do - # expect(manual).to be_skipped - # expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - # expect(page).to have_content(manual.name) - # expect(manual.reload).to be_pending - # end - # - # scenario 'does show build name and id' do - # expect(page).to have_link("#{build.name} (##{build.id})") - # end - # - # scenario 'does not show stop button' do - # expect(page).not_to have_selector('.stop-env-link') - # end - # - # scenario 'does not show external link button' do - # expect(page).not_to have_css('external-url') - # end - # - # # context 'with external_url' do - # given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } - # given(:build) { create(:ci_build, pipeline: pipeline) } - # given(:deployment) { create(:deployment, environment: environment, deployable: build) } - # - # scenario 'does show an external link button' do - # expect(page).to have_link(nil, href: environment.external_url) - # end - # end - # - # # context 'with stop action' do - # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - # given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - # - # scenario 'does show stop button' do - # expect(page).to have_selector('.stop-env-link') - # end - # - # scenario 'starts build when stop button clicked' do - # first('.stop-env-link').click - # - # expect(page).to have_content('close_app') - # end - # - # context 'for reporter' do - # let(:role) { :reporter } - # - # scenario 'does not show stop button' do - # expect(page).not_to have_selector('.stop-env-link') - # end - # end - # end - # end - # end + context 'with deployments' do + let!(:environment) { create(:environment, project: project) } + given(:deployment) { create(:deployment, environment: environment) } + + scenario 'does show deployment SHA' do + expect(page).to have_link(deployment.short_sha) + end + + scenario 'does show deployment internal id' do + expect(page).to have_content(deployment.iid) + end + + context 'with build and manual actions' do + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end + + scenario 'does show build name and id' do + expect(page).to have_link("#{build.name} (##{build.id})") + end + + scenario 'does not show stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + + scenario 'does not show external link button' do + expect(page).not_to have_css('external-url') + end + + context 'with external_url' do + given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + + scenario 'does show an external link button' do + expect(page).to have_link(nil, href: environment.external_url) + end + end + + context 'with stop action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + + scenario 'does show stop button' do + expect(page).to have_selector('.stop-env-link') + end + + scenario 'starts build when stop button clicked' do + first('.stop-env-link').click + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + let(:role) { :reporter } + + scenario 'does not show stop button' do + expect(page).not_to have_selector('.stop-env-link') + end + end + end + end + end end context 'can create new environment' do @@ -170,139 +171,139 @@ feature 'Environments', feature: true, js:true do end end - # describe 'when showing the environment' do - # given(:environment) { create(:environment, project: project) } - # given!(:deployment) { } - # given!(:manual) { } - # - # before do - # visit namespace_project_environment_path(project.namespace, project, environment) - # end - # - # context 'without deployments' do - # scenario 'does show no deployments' do - # expect(page).to have_content('You don\'t have any deployments right now.') - # end - # end - # - # context 'with deployments' do - # given(:deployment) { create(:deployment, environment: environment) } - # - # scenario 'does show deployment SHA' do - # expect(page).to have_link(deployment.short_sha) - # end - # - # scenario 'does not show a re-deploy button for deployment without build' do - # expect(page).not_to have_link('Re-deploy') - # end - # - # context 'with build' do - # given(:pipeline) { create(:ci_pipeline, project: project) } - # given(:build) { create(:ci_build, pipeline: pipeline) } - # given(:deployment) { create(:deployment, environment: environment, deployable: build) } - # - # scenario 'does show build name' do - # expect(page).to have_link("#{build.name} (##{build.id})") - # end - # - # scenario 'does show re-deploy button' do - # expect(page).to have_link('Re-deploy') - # end - # - # scenario 'does not show stop button' do - # expect(page).not_to have_link('Stop') - # end - # - # context 'with manual action' do - # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - # - # scenario 'does show a play button' do - # expect(page).to have_link(manual.name.humanize) - # end - # - # scenario 'does allow to play manual action' do - # expect(manual).to be_skipped - # expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } - # expect(page).to have_content(manual.name) - # expect(manual.reload).to be_pending - # end - # - # context 'with external_url' do - # given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } - # given(:build) { create(:ci_build, pipeline: pipeline) } - # given(:deployment) { create(:deployment, environment: environment, deployable: build) } - # - # scenario 'does show an external link button' do - # expect(page).to have_link(nil, href: environment.external_url) - # end - # end - # - # context 'with stop action' do - # given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } - # given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - # - # scenario 'does show stop button' do - # expect(page).to have_link('Stop') - # end - # - # scenario 'does allow to stop environment' do - # click_link('Stop') - # - # expect(page).to have_content('close_app') - # end - # - # context 'for reporter' do - # let(:role) { :reporter } - # - # scenario 'does not show stop button' do - # expect(page).not_to have_link('Stop') - # end - # end - # end - # end - # end - # end - # end + describe 'when showing the environment' do + given(:environment) { create(:environment, project: project) } + given!(:deployment) { } + given!(:manual) { } + + before do + visit namespace_project_environment_path(project.namespace, project, environment) + end + + context 'without deployments' do + scenario 'does show no deployments' do + expect(page).to have_content('You don\'t have any deployments right now.') + end + end + + context 'with deployments' do + given(:deployment) { create(:deployment, environment: environment) } + + scenario 'does show deployment SHA' do + expect(page).to have_link(deployment.short_sha) + end + + scenario 'does not show a re-deploy button for deployment without build' do + expect(page).not_to have_link('Re-deploy') + end + + context 'with build' do + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + + scenario 'does show build name' do + expect(page).to have_link("#{build.name} (##{build.id})") + end + + scenario 'does show re-deploy button' do + expect(page).to have_link('Re-deploy') + end + + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end + + context 'with manual action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end + + context 'with external_url' do + given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + + scenario 'does show an external link button' do + expect(page).to have_link(nil, href: environment.external_url) + end + end + + context 'with stop action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } + given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } + + scenario 'does show stop button' do + expect(page).to have_link('Stop') + end + + scenario 'does allow to stop environment' do + click_link('Stop') + + expect(page).to have_content('close_app') + end + + context 'for reporter' do + let(:role) { :reporter } + + scenario 'does not show stop button' do + expect(page).not_to have_link('Stop') + end + end + end + end + end + end + end - # describe 'when creating a new environment' do - # before do - # visit namespace_project_environments_path(project.namespace, project) - # end - # - # context 'when logged as developer' do - # before do - # click_link 'New environment' - # end - # - # context 'for valid name' do - # before do - # fill_in('Name', with: 'production') - # click_on 'Save' - # end - # - # scenario 'does create a new pipeline' do - # expect(page).to have_content('Production') - # end - # end - # - # context 'for invalid name' do - # before do - # fill_in('Name', with: 'name,with,commas') - # click_on 'Save' - # end - # - # scenario 'does show errors' do - # expect(page).to have_content('Name can contain only letters') - # end - # end - # end - # - # context 'when logged as reporter' do - # given(:role) { :reporter } - # - # scenario 'does not have a New environment link' do - # expect(page).not_to have_link('New environment') - # end - # end - # end + describe 'when creating a new environment' do + before do + visit namespace_project_environments_path(project.namespace, project) + end + + context 'when logged as developer' do + before do + click_link 'New environment' + end + + context 'for valid name' do + before do + fill_in('Name', with: 'production') + click_on 'Save' + end + + scenario 'does create a new pipeline' do + expect(page).to have_content('Production') + end + end + + context 'for invalid name' do + before do + fill_in('Name', with: 'name,with,commas') + click_on 'Save' + end + + scenario 'does show errors' do + expect(page).to have_content('Name can contain only letters') + end + end + end + + context 'when logged as reporter' do + given(:role) { :reporter } + + scenario 'does not have a New environment link' do + expect(page).not_to have_link('New environment') + end + end + end end From 9a7fa3b6d38f6ac9dbdfef793d66cb6d21c6431e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Oct 2016 15:14:08 +0100 Subject: [PATCH 055/302] Adds tests for environments store --- .../environments_store_spec.js.es6 | 19 +++++ .../javascripts/environments/mock_data.js.es6 | 77 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 spec/javascripts/environments/environments_store_spec.js.es6 create mode 100644 spec/javascripts/environments/mock_data.js.es6 diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 new file mode 100644 index 00000000000..19c71305aa6 --- /dev/null +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -0,0 +1,19 @@ +//= require vue +//= require vue-resource +//= require lib/utils/url_utility +//= require ./mock_data +// + +(() => { + beforeEach(() => { + gl.environmentsService = new EnvironmentsService('test/environments'); + gl.environmentsList.EnvironmentsStore.create(); + }); + + describe('Store', () => { + it('starts with a blank state', () => { + expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); + }); + + }); +})(); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 new file mode 100644 index 00000000000..8d12965332e --- /dev/null +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -0,0 +1,77 @@ +const environmentsList = [ + { + "id": 15, + "project_id": 11, + "name": "production", + "created_at": "2016-10-18T10:47:46.840Z", + "updated_at": "2016-10-19T15:49:24.378Z", + "external_url": "https://test.com", + "environment_type": null, + "state": "available", + "last_deployment": { + "id": 57, + "iid": 5, + "project_id": 11, + "environment_id": 15, + "ref": "master", + "tag": false, + "sha": "edf8704ba6cea79be4634b82927e9ff534068428", + "user_id": 1, + "deployable_id": 1170, + "deployable_type": "CommitStatus", + "on_stop": null, + "short_sha": "edf8704b", + "commit_title": "Update .gitlab-ci.yml", + "commit": { + "id": "edf8704ba6cea79be4634b82927e9ff534068428", + "message": "Update .gitlab-ci.yml", + "parent_ids": ["f215999006bd3d5c89b9b1e8c0873c9aca0f913a"], + "authored_date": "2016-10-19T16:49:09.000+01:00", + "author_name": "Administrator", + "author_email": "admin@example.com", + "committed_date": "2016-10-19T16:49:09.000+01:00", + "committer_name": "Administrator", + "committer_email": "admin@example.com" + }, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "deployable": { + "id": 1170, + "name": "deploy", + "tag": false, + "ref": "master" + } + }, + "project": { + "id": 11, + "name": "review-apps", + "path": "review-apps", + "namespace_id": 1, + "namespace": { + "id": 1, + "name": "root" + } + } + },{ + "id": 18, + "project_id": 11, + "name": "review/test-environment", + "created_at": "2016-10-19T14:59:59.303Z", + "updated_at": "2016-10-19T14:59:59.303Z", + "external_url": "http://test1.com", + "environment_type": "review", + "state": "available", + "project": { + "id": 11, + "name": "review-apps", + "namespace": { + "id": 1, + "name": "root" + } + } + } +]; \ No newline at end of file From a2dbdb88be6fdd363d42f925c2792adb2e4756ab Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 25 Oct 2016 21:00:56 +0100 Subject: [PATCH 056/302] Fix tab filtering and counting --- .../environments/environments_bundle.js.es6 | 29 ++++++++++++------- .../stores/environmnets_store.js.es6 | 25 +++++++++++++--- .../projects/environments/index.html.haml | 4 +-- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 3382e7a4b29..59c0dcfe1c2 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -16,14 +16,21 @@ $(() => { gl.EnvironmentsListApp.$destroy(true); } - const filterEnvironments = (environments = [], filter = "") => { - return environments.filter((env) => { - if (env.children) { - return env.children.filter((child) => child.state === filter).length; - } else { - return env.state === filter; - }; - }); + + const filterState = (state) => (environment) => environment.state === state && environment; + + // recursiveMap :: (Function, Array) -> Array + const recursiveMap = (fn, arr) => { + return arr.map((item) => { + if (!item.children) { return fn(item); } + + const filteredChildren = recursiveMap(fn, item.children).filter(Boolean); + if (filteredChildren.length) { + item.children = filteredChildren; + return item; + } + + }).filter(Boolean); }; gl.EnvironmentsListApp = new Vue({ @@ -43,15 +50,15 @@ $(() => { computed: { filteredEnvironments () { - return filterEnvironments(this.state.environments, this.visibility); + return recursiveMap(filterState(this.visibility), this.state.environments); }, countStopped () { - return filterEnvironments(this.state.environments, 'stopped').length; + }, countAvailable () { - return filterEnvironments(this.state.environments, 'available').length; + // return recursiveMap(filterState('available'), this.state.environments).length; } }, diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 9416b944b4d..de51d94d2b5 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -7,6 +7,8 @@ create () { this.state.environments = []; + this.state.stoppedCounter = 0; + this.state.availableCounter = 0; }, /** @@ -42,6 +44,10 @@ * @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.last_deployment) { @@ -59,15 +65,14 @@ if (environment.environment_type !== null) { const occurs = acc.find((element, index, array) => { - return element.environment_type === environment.environment_type; + return element.children && element.name === environment.environment_type; }); - - + environment["vue-isChildren"] = true; if (occurs !== undefined) { acc[acc.indexOf(occurs)].children.push(environment); - acc[acc.indexOf(occurs)].children.push(environment).sort(this.sortByName) + acc[acc.indexOf(occurs)].children.sort(this.sortByName) } else { acc.push({ name: environment.environment_type, @@ -88,6 +93,18 @@ return environmentsTree; }, + /** + * 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. * diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 3538777d11c..81c8fd9d9db 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -12,13 +12,13 @@ = link_to project_environments_path(@project) do Available %span.badge.js-available-environments-count - {{countAvailable}} + {{state.availableCounter}} %li{class: ('active' if @scope == 'stopped')} = link_to project_environments_path(@project, scope: :stopped) do Stopped %span.badge.js-stopped-environments-count - {{countStopped}} + {{state.stoppedCounter}} - if can?(current_user, :create_environment, @project) && !@all_environments.blank? .nav-controls From 3c383f9f76a8cec6ecf69a012567cb70b8466897 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 26 Oct 2016 09:17:32 +0100 Subject: [PATCH 057/302] Fixes filter --- .../environments/environments_bundle.js.es6 | 8 -------- .../environments_store_spec.js.es6 | 4 +++- .../javascripts/environments/mock_data.js.es6 | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 59c0dcfe1c2..8e13e7e20a7 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -51,14 +51,6 @@ $(() => { computed: { filteredEnvironments () { return recursiveMap(filterState(this.visibility), this.state.environments); - }, - - countStopped () { - - }, - - countAvailable () { - // return recursiveMap(filterState('available'), this.state.environments).length; } }, diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 19c71305aa6..ccf6ae971c3 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,6 +1,8 @@ //= require vue //= require vue-resource //= require lib/utils/url_utility +//= require environments/services/environments_service +//= require environments/stores/environments_store //= require ./mock_data // @@ -14,6 +16,6 @@ it('starts with a blank state', () => { expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); }); - + }); })(); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 8d12965332e..87420a32450 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -73,5 +73,23 @@ const environmentsList = [ "name": "root" } } + }, + { + "id": 18, + "project_id": 11, + "name": "review/test-environment-1", + "created_at": "2016-10-19T14:59:59.303Z", + "updated_at": "2016-10-19T14:59:59.303Z", + "external_url": "http://test-1.com", + "environment_type": "review", + "state": "stopped", + "project": { + "id": 11, + "name": "review-apps", + "namespace": { + "id": 1, + "name": "root" + } + } } -]; \ No newline at end of file +]; From dcafd476dbc11eb6f2e7327d96868bf2c2d165a9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 26 Oct 2016 16:00:16 +0100 Subject: [PATCH 058/302] Removes trailing whitespace --- .../components/environment_item.js.es6 | 20 ++++++------ .../environments/environments_bundle.js.es6 | 31 +++++++++---------- .../services/environments_service.js.es6 | 8 ++--- .../stores/environmnets_store.js.es6 | 26 ++++++++-------- .../environments_store_spec.js.es6 | 4 +-- 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index df9f28b1c59..ab58e1cc4c0 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,23 +1,23 @@ (() => { - + /** * 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) + * 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. */ - + const Store = gl.environmentsList.EnvironmentsStore; window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - + gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { - + template: '#environment-item-template', props: { @@ -31,11 +31,11 @@ }, 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. + * them and show their children. * * @returns {Boolean} */ diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 8e13e7e20a7..286d81bab7f 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -6,30 +6,29 @@ //= require ../boards/vue_resource_interceptor $(() => { - + const environmentsListApp = document.getElementById('environments-list-view'); const Store = gl.environmentsList.EnvironmentsStore; - + window.gl = window.gl || {}; - + if (gl.EnvironmentsListApp) { gl.EnvironmentsListApp.$destroy(true); } - - + const filterState = (state) => (environment) => environment.state === state && environment; - - // recursiveMap :: (Function, Array) -> Array + + // recursiveMap :: (Function, Array) -> Array const recursiveMap = (fn, arr) => { return arr.map((item) => { if (!item.children) { return fn(item); } - + const filteredChildren = recursiveMap(fn, item.children).filter(Boolean); if (filteredChildren.length) { item.children = filteredChildren; return item; } - + }).filter(Boolean); }; @@ -47,18 +46,18 @@ $(() => { loading: true, visibility: 'available' }, - + computed: { filteredEnvironments () { return recursiveMap(filterState(this.visibility), this.state.environments); } }, - + init: Store.create.bind(Store), - + created() { gl.environmentsService = new EnvironmentsService(this.endpoint); - + const scope = this.$options.getQueryParameter('scope'); if (scope) { this.visibility = scope; @@ -67,7 +66,7 @@ $(() => { /** * Fetches all the environmnets and stores them. - * Toggles loading property. + * Toggles loading property. */ ready() { gl.environmentsService.all().then((resp) => { @@ -75,11 +74,11 @@ $(() => { this.loading = false; }); }, - + /** * Transforms the url parameter into an object and * returns the one requested. - * + * * @param {String} param * @returns {String} The value of the requested parameter. */ diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 5cc194ac034..6e0f5b431e3 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -1,16 +1,16 @@ class EnvironmentsService { - + constructor (root) { Vue.http.options.root = root; - + this.environments = Vue.resource(root); - + Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); next(); }); } - + all () { return this.environments.get(); } diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index de51d94d2b5..c0e97413636 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -12,15 +12,15 @@ }, /** - * In order to display a tree view we need to modify the received - * data in to a tree structure based on `environment_type` + * 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 + * 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: * [ @@ -30,7 +30,7 @@ * ] * into this: * [ - * { name: "review", children: + * { name: "review", children: * [ * { name: "environment", environment_type: "review", vue-isChildren: true}, * { name: "environment_2", environment_type: "review", vue-isChildren: true} @@ -38,8 +38,8 @@ * }, * {name: "environment_1", environment_type: null} * ] - * - * + * + * * @param {Array} environments List of environments. * @returns {Array} Tree structured array with the received environments. */ @@ -56,13 +56,13 @@ if (environment.last_deployment.manual_actions) { environment.last_deployment.manual_actions = environment.last_deployment.manual_actions.map((action) => Object.assign({}, action, {name: gl.text.humanize(action.name)})); } - + //transforms created date for deployment in a human readable format if (environment.last_deployment.created_at) { // TODO - how to do this without jquery } } - + if (environment.environment_type !== null) { const occurs = acc.find((element, index, array) => { return element.children && element.name === environment.environment_type; @@ -89,15 +89,15 @@ }, []).sort(this.sortByName); this.state.environments = environmentsTree; - + return environmentsTree; }, /** - * Given an array of environments, returns the number of environments + * Given an array of environments, returns the number of environments * that have the given state. * - * @param {Array} environments + * @param {Array} environments * @param {String} state * @returns {Number} */ diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index ccf6ae971c3..482b562fdce 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -4,14 +4,14 @@ //= require environments/services/environments_service //= require environments/stores/environments_store //= require ./mock_data -// +// (() => { beforeEach(() => { gl.environmentsService = new EnvironmentsService('test/environments'); gl.environmentsList.EnvironmentsStore.create(); }); - + describe('Store', () => { it('starts with a blank state', () => { expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); From b1ec3cb8d2776070c80cba21d08774e563b42f84 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 26 Oct 2016 18:17:00 +0100 Subject: [PATCH 059/302] Adds tests for environment store. --- .../stores/environmnets_store.js.es6 | 14 ++- .../environments_store_spec.js.es6 | 51 +++++++++-- .../javascripts/environments/mock_data.js.es6 | 89 +++---------------- 3 files changed, 60 insertions(+), 94 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index c0e97413636..11c481c22e3 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -54,7 +54,7 @@ //humanizes actions names if there are any actions if (environment.last_deployment.manual_actions) { - environment.last_deployment.manual_actions = environment.last_deployment.manual_actions.map((action) => Object.assign({}, action, {name: gl.text.humanize(action.name)})); + environment.last_deployment.manual_actions = environment.last_deployment.manual_actions.map((action) => action.name = gl.text.humanize(action.name)); } //transforms created date for deployment in a human readable format @@ -64,21 +64,19 @@ } if (environment.environment_type !== null) { - const occurs = acc.find((element, index, array) => { + const occurs = acc.filter((element, index, array) => { return element.children && element.name === environment.environment_type; }); environment["vue-isChildren"] = true; - if (occurs !== undefined) { - acc[acc.indexOf(occurs)].children.push(environment); - acc[acc.indexOf(occurs)].children.sort(this.sortByName) + if (occurs.length) { + acc[acc.indexOf(occurs[0])].children.push(environment); + acc[acc.indexOf(occurs[0])].children.sort(this.sortByName) } else { acc.push({ name: environment.environment_type, - children: [ - Object.assign(environment) - ] + children: [environment] }); } } else { diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 482b562fdce..db4ce41701c 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,21 +1,58 @@ //= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require environments/services/environments_service -//= require environments/stores/environments_store +//= require environments/stores/environmnets_store //= require ./mock_data -// (() => { + beforeEach(() => { - gl.environmentsService = new EnvironmentsService('test/environments'); gl.environmentsList.EnvironmentsStore.create(); }); describe('Store', () => { - it('starts with a blank state', () => { + 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) }); + 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(2); + }); + + it('should store environments with same environment_type as sibilings', () => { + + expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(2); + + const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments.filter((env) => { + return 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('review/test-environment') + expect(parentFolder[0].children[1].name).toBe('review/test-environment-1'); + }); + + it('should sort the environments alphabetically', () => { + const { environments } = gl.environmentsList.EnvironmentsStore.state; + + expect(environments[0].name).toBe('production'); + expect(environments[1].children[0].name).toBe('review/test-environment'); + expect(environments[1].children[1].name).toBe('review/test-environment-1'); + }); + }); }); })(); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 87420a32450..35d94e3ab44 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -3,93 +3,24 @@ const environmentsList = [ "id": 15, "project_id": 11, "name": "production", - "created_at": "2016-10-18T10:47:46.840Z", - "updated_at": "2016-10-19T15:49:24.378Z", "external_url": "https://test.com", "environment_type": null, - "state": "available", - "last_deployment": { - "id": 57, - "iid": 5, - "project_id": 11, - "environment_id": 15, - "ref": "master", - "tag": false, - "sha": "edf8704ba6cea79be4634b82927e9ff534068428", - "user_id": 1, - "deployable_id": 1170, - "deployable_type": "CommitStatus", - "on_stop": null, - "short_sha": "edf8704b", - "commit_title": "Update .gitlab-ci.yml", - "commit": { - "id": "edf8704ba6cea79be4634b82927e9ff534068428", - "message": "Update .gitlab-ci.yml", - "parent_ids": ["f215999006bd3d5c89b9b1e8c0873c9aca0f913a"], - "authored_date": "2016-10-19T16:49:09.000+01:00", - "author_name": "Administrator", - "author_email": "admin@example.com", - "committed_date": "2016-10-19T16:49:09.000+01:00", - "committer_name": "Administrator", - "committer_email": "admin@example.com" - }, - "user": { - "id": 1, - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" - }, - "deployable": { - "id": 1170, - "name": "deploy", - "tag": false, - "ref": "master" - } - }, - "project": { - "id": 11, - "name": "review-apps", - "path": "review-apps", - "namespace_id": 1, - "namespace": { - "id": 1, - "name": "root" - } - } - },{ - "id": 18, - "project_id": 11, - "name": "review/test-environment", - "created_at": "2016-10-19T14:59:59.303Z", - "updated_at": "2016-10-19T14:59:59.303Z", - "external_url": "http://test1.com", - "environment_type": "review", - "state": "available", - "project": { - "id": 11, - "name": "review-apps", - "namespace": { - "id": 1, - "name": "root" - } - } + "state": "available" }, { "id": 18, "project_id": 11, + "name": "review/test-environment", + "external_url": "http://test1.com", + "environment_type": "review", + "state": "available" + }, + { + "id": 19, + "project_id": 11, "name": "review/test-environment-1", - "created_at": "2016-10-19T14:59:59.303Z", - "updated_at": "2016-10-19T14:59:59.303Z", "external_url": "http://test-1.com", "environment_type": "review", - "state": "stopped", - "project": { - "id": 11, - "name": "review-apps", - "namespace": { - "id": 1, - "name": "root" - } - } + "state": "stopped" } ]; From 14345b21509aa874cf8748a2ddcf34d3a632bd6c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 27 Oct 2016 10:51:44 +0100 Subject: [PATCH 060/302] Fix whitespace --- .../environments/components/environment_item.js.es6 | 2 -- .../javascripts/environments/environments_store_spec.js.es6 | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index ab58e1cc4c0..76c815e8b9f 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -11,8 +11,6 @@ * for more information. */ - const Store = gl.environmentsList.EnvironmentsStore; - window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index db4ce41701c..8d0fea934c3 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -21,20 +21,16 @@ }); 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(2); }); it('should store environments with same environment_type as sibilings', () => { - expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(2); - + const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments.filter((env) => { return env.children && env.children.length > 0; }); From bd01a5a47c70face953cb2d0a6eb3066abc975fd Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Nov 2016 09:43:43 +0100 Subject: [PATCH 061/302] Add EnvironmentSerializer to EnvironmentsController --- .../projects/environments_controller.rb | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 42f882b55ab..6bd4cb3f2f5 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -13,7 +13,9 @@ class Projects::EnvironmentsController < Projects::ApplicationController respond_to do |format| format.html format.json do - render json: serialize_as_json(@environments) + render json: EnvironmentSerializer + .new(project: @project) + .represent(@environments) end end end @@ -63,19 +65,4 @@ class Projects::EnvironmentsController < Projects::ApplicationController def environment @environment ||= project.environments.find(params[:id]) end - - def serialize_as_json(resource) - resource.as_json( - include: { - last_deployment: { - include: { - user: { only: [:id, :name, :username], methods: [:avatar_url] }, - deployable: { only: [:id, :name, :ref, :tag] } - }, - methods: [:short_sha, :commit_title, :commit] - }, - project: { methods: [:namespace] } - } - ) - end end From 4388d90370310a570c5450b6c10eba7533ce1827 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 7 Nov 2016 10:03:19 +0100 Subject: [PATCH 062/302] Add controller specs for environments index action --- .../projects/environments_controller_spec.rb | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 768105cae95..bc5e2711125 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::EnvironmentsController do + include ApiHelpers + let(:environment) { create(:environment) } let(:project) { environment.project } let(:user) { create(:user) } @@ -11,6 +13,27 @@ describe Projects::EnvironmentsController do sign_in(user) end + describe 'GET index' do + context 'when standardrequest has been made' do + it 'responds with status code 200' do + get :index, environment_params + + expect(response).to be_ok + end + end + + context 'when requesting JSON response' do + it 'responds with correct JSON' do + get :index, environment_params(format: :json) + + first_environment = json_response.first + + expect(first_environment).not_to be_empty + expect(first_environment['name']). to eq environment.name + end + end + end + describe 'GET show' do context 'with valid id' do it 'responds with a status code 200' do @@ -48,11 +71,9 @@ describe Projects::EnvironmentsController do end end - def environment_params - { - namespace_id: project.namespace, - project_id: project, - id: environment.id - } + def environment_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, + project_id: project, + id: environment.id) end end From 8d171f0a5fbd920b25b5012526e3cdb2c3aee623 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 12:45:35 +0000 Subject: [PATCH 063/302] Fix commit column with correct data --- .../environments/components/_commit.html.haml | 33 +++++++++---------- .../projects/environments/index.html.haml | 4 +-- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index 1f923a2d0d6..17bc3dcb379 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -1,30 +1,27 @@ .branch-commit{"v-if" => "!isFolder && model.last_deployment"} + .icon-container{ "v-if" => "model.last_deployment.ref && model.last_deployment.tag" } + =icon("tag") - %div{ "v-if" => "model.last_deployment.ref" } + .icon-container{ "v-if" => "model.last_deployment.ref && !model.last_deployment.tag" } + =icon("code-fork") - .icon-container{ "v-if" => "model.last_deployment.tag" } - =icon("tag") - - .icon-container{ "v-if" => "!model.last_deployment.tag" } - =icon("code-fork") - - %a.monospace.branch-name{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commits/' +model.last_deployment.ref"} - {{model.last_deployment.ref}} + %a.monospace.branch-name{"v-if" => "model.last_deployment.ref", ":href" => "model.last_deployment.ref.ref_url"} + {{model.last_deployment.ref.name}} .icon-container.commit-icon = custom_icon("icon_commit") - %a.commit-id.monospace{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commit/' +model.last_deployment.sha"} - {{model.last_deployment.short_sha}} + %a.commit-id.monospace{":href" => "model.last_deployment.commit.commit_url"} + {{model.last_deployment.commit.short_id}} %p.commit-title - %span{ "v-if" => "model.last_deployment.commit_title"} - - -# need commit author username and avatar_url - commit author goes here - %a.commit-row-message{":href" => "'/' + model.project.namespace.name + '/' + model.project.name + '/commit/' +model.last_deployment.sha"} - {{model.last_deployment.commit_title}} + %span{ "v-if" => "model.last_deployment.commit && model.last_deployment.commit.title"} + %a{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} + %img.avatar.has-tooltip.s20{":title" => "model.last_deployment.commit.author.username", + ":src" => "model.last_deployment.commit.author.avatar_url"} + %a.commit-row-message{":href" => "model.last_deployment.commit.commit_url"} + {{model.last_deployment.commit.title}} - %span{ "v-if" => "!model.last_deployment.commit_title"} + %span{ "v-if" => "!model.last_deployment.commit.title"} Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 81c8fd9d9db..fa88d59e3ca 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -20,8 +20,8 @@ %span.badge.js-stopped-environments-count {{state.stoppedCounter}} - - if can?(current_user, :create_environment, @project) && !@all_environments.blank? - .nav-controls + - if can?(current_user, :create_environment, @project) + .nav-controls{ "v-if" => "!loading" } = link_to new_namespace_project_environment_path(@project.namespace, @project), class: "btn btn-create" do New environment From 95490ca379572efc8010019e7b0d7b1a535b7497 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 14:06:01 +0000 Subject: [PATCH 064/302] Remove underline styling when hover the user avatar --- app/assets/stylesheets/pages/environments.scss | 4 ++++ app/views/projects/environments/components/_commit.html.haml | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 9d47e96bb69..0ab1066e970 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -20,6 +20,10 @@ margin: 0; } + .avatar-container { + text-decoration: none; + } + .icon-play { height: 13px; width: 12px; diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index 17bc3dcb379..972a4f241f5 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -15,9 +15,8 @@ {{model.last_deployment.commit.short_id}} %p.commit-title - %span{ "v-if" => "model.last_deployment.commit && model.last_deployment.commit.title"} - %a{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} + %a.avatar-container{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} %img.avatar.has-tooltip.s20{":title" => "model.last_deployment.commit.author.username", ":src" => "model.last_deployment.commit.author.avatar_url"} %a.commit-row-message{":href" => "model.last_deployment.commit.commit_url"} From 8dd19c4279eae0266dce2d2dc9057b0ba0f7650f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 16:15:30 +0000 Subject: [PATCH 065/302] Fixes avatar style --- app/assets/stylesheets/pages/environments.scss | 2 +- app/views/projects/environments/components/_commit.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 0ab1066e970..6ce10f1e7ef 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -20,7 +20,7 @@ margin: 0; } - .avatar-container { + .avatar-image-container { text-decoration: none; } diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index 972a4f241f5..c0c54325f7f 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -16,7 +16,7 @@ %p.commit-title %span{ "v-if" => "model.last_deployment.commit && model.last_deployment.commit.title"} - %a.avatar-container{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} + %a.avatar-image-container{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} %img.avatar.has-tooltip.s20{":title" => "model.last_deployment.commit.author.username", ":src" => "model.last_deployment.commit.author.avatar_url"} %a.commit-row-message{":href" => "model.last_deployment.commit.commit_url"} From 1b00f3a5bb556118298a3396437db74dec7b5933 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 16:53:13 +0000 Subject: [PATCH 066/302] Adds alt attribute in image tags --- app/views/projects/environments/components/_commit.html.haml | 1 + .../projects/environments/components/_environment.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml index c0c54325f7f..8233dec7b93 100644 --- a/app/views/projects/environments/components/_commit.html.haml +++ b/app/views/projects/environments/components/_commit.html.haml @@ -18,6 +18,7 @@ %span{ "v-if" => "model.last_deployment.commit && model.last_deployment.commit.title"} %a.avatar-image-container{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} %img.avatar.has-tooltip.s20{":title" => "model.last_deployment.commit.author.username", + ":alt" => "model.last_deployment.commit.author.username + `'s avatar`", ":src" => "model.last_deployment.commit.author.avatar_url"} %a.commit-row-message{":href" => "model.last_deployment.commit.commit_url"} {{model.last_deployment.commit.title}} diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 1aa0d7ac5e2..42c6ae562e0 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -27,7 +27,7 @@ by %a{":href" => "'/' + model.last_deployment.user.username" } %img.avatar.has_tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", - ":alt" => "model.last_deployment.user.username + 'avatar'", + ":alt" => "model.last_deployment.user.username + `'s avatar`", ":title" => "model.last_deployment.user.username", data: {container: "body" }, width: 20, height: 20} From f5d9b994e4a2362140c73f6be5de280f6ec64eaf Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 17:01:40 +0000 Subject: [PATCH 067/302] Use urls sent in JSON response --- .../environments/components/_environment.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 42c6ae562e0..a90517f76a0 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -2,7 +2,7 @@ %tr %td{"v-bind:class" => "{ 'children-row': isChildren}" } %a.environment-name{ "v-if" => "!isFolder", - ":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id" } + ":href" => "model.environment_url" } {{model.name}} %span.folder-name{ "v-if" => "isFolder", @@ -25,7 +25,7 @@ %span{ "v-if" => "model.last_deployment.user" } by - %a{":href" => "'/' + model.last_deployment.user.username" } + %a{":href" => "model.last_deployment.user.web_url" } %img.avatar.has_tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", ":alt" => "model.last_deployment.user.username + `'s avatar`", ":title" => "model.last_deployment.user.username", @@ -34,7 +34,7 @@ %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id" } + ":href" => "model.last_deployment.deployable.build_url" } {{model.last_deployment.deployable.name}} = precede "#" do {{model.last_deployment.deployable.id}} @@ -47,7 +47,7 @@ %td %span{ "v-if" => "!isFolder && model.last_deployment" } - {{model.last_deployment.created_at}} + {{model.created_at}} %td.hidden-xs .pull-right{ "v-if" => "!isFolder" } From 4ea04c4035368efa007abbf560e7ca6882a361b8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 17:21:42 +0000 Subject: [PATCH 068/302] Adds tooltip to user image --- .../environments/components/_environment.html.haml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index a90517f76a0..122cea2c3a4 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -26,15 +26,13 @@ %span{ "v-if" => "model.last_deployment.user" } by %a{":href" => "model.last_deployment.user.web_url" } - %img.avatar.has_tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", + %img.avatar.has-tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", ":alt" => "model.last_deployment.user.username + `'s avatar`", - ":title" => "model.last_deployment.user.username", - data: {container: "body" }, - width: 20, height: 20} + ":title" => "model.last_deployment.user.username"} %td %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":href" => "model.last_deployment.deployable.build_url" } + ":href" => "model.last_deployment.deployable.build" } {{model.last_deployment.deployable.name}} = precede "#" do {{model.last_deployment.deployable.id}} @@ -47,7 +45,7 @@ %td %span{ "v-if" => "!isFolder && model.last_deployment" } - {{model.created_at}} + {{model.last_deployment.created_at}} %td.hidden-xs .pull-right{ "v-if" => "!isFolder" } From 2207528c376f0c64469f077f82dfb49a3f7e7993 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 18:44:16 +0000 Subject: [PATCH 069/302] Uses vue computed data Fixes some eslint errors Adds some documentation Improves data manipulation --- .../components/environment_item.js.es6 | 99 ++++++++++++++++--- .../environments/environments_bundle.js.es6 | 14 +-- .../stores/environmnets_store.js.es6 | 36 ++----- .../components/_actions.html.haml | 6 +- .../components/_environment.html.haml | 4 +- .../components/_rollback.html.haml | 8 +- .../environments/components/_stop.html.haml | 4 +- 7 files changed, 114 insertions(+), 57 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 76c815e8b9f..0d541f5989d 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,5 +1,6 @@ +/* globals Vue */ +/* eslint-disable no-param-reassign, no-return-assign */ (() => { - /** * Envrionment Item Component * @@ -19,12 +20,12 @@ template: '#environment-item-template', props: { - model: Object + model: Object, }, - data () { + data() { return { - open: false + open: false, }; }, @@ -37,8 +38,8 @@ * * @returns {Boolean} */ - isFolder () { - return this.model.children && this.model.children.length ? true : false; + isFolder() { + return this.$options.hasKey(this.model, 'children') && this.model.children.length > 0; }, /** @@ -47,7 +48,7 @@ * * @returns {Boolean|undefined} */ - isChildren () { + isChildren() { return this.model['vue-isChildren']; }, @@ -57,9 +58,79 @@ * * @returns {Boolean} The number of environments for the current folder */ - childrenCounter () { - return this.model.children && this.model.children.length; - } + childrenCounter() { + return this.$options.hasKey(this.model, 'children') && this.model.children.length; + }, + + /** + * Returns the value of the `last?` key sent in the API. + * Used to know wich title to render when the environment can be re-deployed + * + * @returns {Boolean} + */ + isLast() { + return this.$options.hasKey(this.model, 'last_deployment') && this.model.last_deployment['last?']; + }, + + /** + * 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() { + return this.$options.hasKey(this.model, 'last_deployment'); + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean} description + */ + hasManualActions() { + return this.$options.hasKey(this.model, 'manual_actions') && this.model.manual_actions.length; + }, + + /** + * Returns the value of the `stoppable?` key provided in the response. + * + * @returns {Boolean} + */ + isStoppable() { + return this.model['stoppable?']; + }, + + /** + * 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} + */ + canRetry() { + return this.hasLastDeploymentKey && this.model.last_deployment && this.$options.hasKey(this.model.last_deployment, 'deployable'); + }, + + createdDate() { + return $.timeago(this.model.created_at); + }, + + manualActions() { + this.model.manual_actions.map(action => action.name = gl.text.humanize(action.name)); + }, + }, + + /** + * Helper to verify if key is present in an object. + * Can be removed once we start using lodash. + * + * @param {Object} obj + * @param {String} key + * @returns {Boolean} + */ + hasKey(obj, key) { + return {}.hasOwnProperty.call(obj, key); }, methods: { @@ -67,11 +138,11 @@ /** * Toggles the visibility of a folders' children. */ - toggle () { + toggle() { if (this.isFolder) { this.open = !this.open; } - } - } - }) + }, + }, + }); })(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 286d81bab7f..53a8353a0bf 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -5,8 +5,9 @@ //= require ./components/environment_item //= require ../boards/vue_resource_interceptor -$(() => { +/* eslint-disable */ +$(() => { const environmentsListApp = document.getElementById('environments-list-view'); const Store = gl.environmentsList.EnvironmentsStore; @@ -16,10 +17,11 @@ $(() => { gl.EnvironmentsListApp.$destroy(true); } - const filterState = (state) => (environment) => environment.state === state && environment; + const filterState = state => environment => environment.state === state && environment; // recursiveMap :: (Function, Array) -> Array const recursiveMap = (fn, arr) => { + return arr.map((item) => { if (!item.children) { return fn(item); } @@ -37,20 +39,20 @@ $(() => { el: '#environments-list-view', components: { - 'item': gl.environmentsList.EnvironmentItem + item: gl.environmentsList.EnvironmentItem }, data: { state: Store.state, endpoint: environmentsListApp.dataset.endpoint, loading: true, - visibility: 'available' + visibility: 'available', }, computed: { - filteredEnvironments () { + filteredEnvironments (){ return recursiveMap(filterState(this.visibility), this.state.environments); - } + }, }, init: Store.create.bind(Store), diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 11c481c22e3..587e213a17b 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -5,7 +5,7 @@ gl.environmentsList.EnvironmentsStore = { state: {}, - create () { + create() { this.state.environments = []; this.state.stoppedCounter = 0; this.state.availableCounter = 0; @@ -44,39 +44,23 @@ * @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.last_deployment) { - - //humanizes actions names if there are any actions - if (environment.last_deployment.manual_actions) { - environment.last_deployment.manual_actions = environment.last_deployment.manual_actions.map((action) => action.name = gl.text.humanize(action.name)); - } - - //transforms created date for deployment in a human readable format - if (environment.last_deployment.created_at) { - // TODO - how to do this without jquery - } - } - if (environment.environment_type !== null) { - const occurs = acc.filter((element, index, array) => { - return element.children && element.name === environment.environment_type; - }); + const occurs = acc.filter(element => element.children && + element.name === environment.environment_type); - environment["vue-isChildren"] = true; + environment['vue-isChildren'] = true; if (occurs.length) { acc[acc.indexOf(occurs[0])].children.push(environment); - acc[acc.indexOf(occurs[0])].children.sort(this.sortByName) + acc[acc.indexOf(occurs[0])].children.sort(this.sortByName); } else { acc.push({ name: environment.environment_type, - children: [environment] + children: [environment], }); } } else { @@ -100,7 +84,7 @@ * @returns {Number} */ countByState(environments, state) { - return environments.filter((env) => env.state === state).length; + return environments.filter(env => env.state === state).length; }, /** @@ -110,7 +94,7 @@ * @param {Object} b * @returns {Number} */ - sortByName (a, b) { + sortByName(a, b) { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); @@ -123,6 +107,6 @@ } return 0; - } - } + }, + }; })(); diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml index 82acda0de9c..a2c229f1315 100644 --- a/app/views/projects/environments/components/_actions.html.haml +++ b/app/views/projects/environments/components/_actions.html.haml @@ -1,13 +1,13 @@ - if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "model.last_deployment && model.last_deployment.manual_actions && model.last_deployment.manual_actions.present"} + .inline{ "v-if" => "hasManualActions"} .dropdown %a.dropdown-new.btn.btn-default{type: "button", "data-toggle" => "dropdown"} = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right - %li{ "v-for" => "action in model.last_deployment.manual_actions" } - %a{ ":ref" => "'#{namespace_project_path(@project.namespace, @project)}/' + action.id + '/play'", + %li{ "v-for" => "action in manualActions" } + %a{ ":ref" => "action.play_url", "data-method" => "post", "rel" => "nofollow" } diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml index 122cea2c3a4..2fecfd85f61 100644 --- a/app/views/projects/environments/components/_environment.html.haml +++ b/app/views/projects/environments/components/_environment.html.haml @@ -44,8 +44,8 @@ No deployments yet %td - %span{ "v-if" => "!isFolder && model.last_deployment" } - {{model.last_deployment.created_at}} + %span.environment-created-date-timeago{ "v-if" => "!isFolder && model.last_deployment" } + {{createdDate}} %td.hidden-xs .pull-right{ "v-if" => "!isFolder" } diff --git a/app/views/projects/environments/components/_rollback.html.haml b/app/views/projects/environments/components/_rollback.html.haml index 2258f6ec32d..e3449c5515e 100644 --- a/app/views/projects/environments/components/_rollback.html.haml +++ b/app/views/projects/environments/components/_rollback.html.haml @@ -1,10 +1,10 @@ - if can?(current_user, :create_deployment, @project) - %a.btn.btn-build{ "v-if" => "model.last_deployment && model.last_deployment.deployable", - ":href" => "'#{namespace_project_builds_path(@project.namespace, @project)}/' + model.last_deployment.deployable.id + '/retry'", + %a.btn.btn-build{ "v-if" => "canRetry", + ":href" => "model.last_deployment.deployable.retry_url", "data-method" => "post", "rel" => "nofollow" } - %span{ "v-if" => "model.last_deployment.last" } + %span{ "v-if" => "isLastDeployment" } Re-deploy - %span{ "v-if" => "!model.last_deployment.last" } + %span{ "v-if" => "!isLastDeployment" } Rollback diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml index 55cccf9e72e..a7100f15784 100644 --- a/app/views/projects/environments/components/_stop.html.haml +++ b/app/views/projects/environments/components/_stop.html.haml @@ -1,6 +1,6 @@ - if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "model.stop_action" } - %a.btn.stop-env-link{":href" => "'#{namespace_project_environments_path(@project.namespace, @project)}/' + model.id", + .inline{ "v-if" => "isStoppable" } + %a.btn.stop-env-link{":href" => "model.environment_url", "method" => ":post", "rel" => "nofollow", "confirm" => "Are you sure you want to stop this environment?"} From 26664203eed786961c47eb23bea4fefd89ac3503 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 20:52:04 +0000 Subject: [PATCH 070/302] Small fixes --- .../components/environment_item.js.es6 | 19 ++++++++++++-- .../environments/environments_bundle.js.es6 | 26 +++++++++---------- .../services/environments_service.js.es6 | 8 +++--- .../stores/environmnets_store.js.es6 | 1 + .../components/_actions.html.haml | 16 ++++++------ 5 files changed, 43 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0d541f5989d..436ac365788 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,5 +1,4 @@ /* globals Vue */ -/* eslint-disable no-param-reassign, no-return-assign */ (() => { /** * Envrionment Item Component @@ -112,12 +111,28 @@ return this.hasLastDeploymentKey && this.model.last_deployment && this.$options.hasKey(this.model.last_deployment, 'deployable'); }, + /** + * Human readable date. + * + * @returns {String} + */ createdDate() { return $.timeago(this.model.created_at); }, + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.} + */ manualActions() { - this.model.manual_actions.map(action => action.name = gl.text.humanize(action.name)); + return this.model.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_url: action.play_url, + }; + return parsedAction; + }); }, }, diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 53a8353a0bf..1299f64182c 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -4,9 +4,9 @@ //= require_tree ./services //= require ./components/environment_item //= require ../boards/vue_resource_interceptor +/* globals Vue, EnvironmentsService */ +/* eslint-disable no-param-reassign */ - -/* eslint-disable */ $(() => { const environmentsListApp = document.getElementById('environments-list-view'); const Store = gl.environmentsList.EnvironmentsStore; @@ -20,26 +20,24 @@ $(() => { const filterState = state => environment => environment.state === state && environment; // recursiveMap :: (Function, Array) -> Array - const recursiveMap = (fn, arr) => { - - return arr.map((item) => { - if (!item.children) { return fn(item); } - + const recursiveMap = (fn, arr) => arr.map((item) => { + if (item.children) { const filteredChildren = recursiveMap(fn, item.children).filter(Boolean); if (filteredChildren.length) { item.children = filteredChildren; return item; } + } + return fn(item); + }).filter(Boolean); - }).filter(Boolean); - }; gl.EnvironmentsListApp = new Vue({ el: '#environments-list-view', components: { - item: gl.environmentsList.EnvironmentItem + item: gl.environmentsList.EnvironmentItem, }, data: { @@ -50,7 +48,7 @@ $(() => { }, computed: { - filteredEnvironments (){ + filteredEnvironments() { return recursiveMap(filterState(this.visibility), this.state.environments); }, }, @@ -84,12 +82,12 @@ $(() => { * @param {String} param * @returns {String} The value of the requested parameter. */ - getQueryParameter(param) { + getQueryParameter(parameter) { return window.location.search.substring(1).split('&').reduce((acc, param) => { const paramSplited = param.split('='); acc[paramSplited[0]] = paramSplited[1]; return acc; - }, {})[param]; - } + }, {})[parameter]; + }, }); }); diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 6e0f5b431e3..e10dd87f7ce 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -1,6 +1,8 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ class EnvironmentsService { - constructor (root) { + constructor(root) { Vue.http.options.root = root; this.environments = Vue.resource(root); @@ -11,7 +13,7 @@ class EnvironmentsService { }); } - all () { + all() { return this.environments.get(); } -}; +} diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 index 587e213a17b..ef0188c15bf 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environmnets_store.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-param-reassign */ (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml index a2c229f1315..f23fdb72353 100644 --- a/app/views/projects/environments/components/_actions.html.haml +++ b/app/views/projects/environments/components/_actions.html.haml @@ -5,12 +5,12 @@ = custom_icon('icon_play') = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li{ "v-for" => "action in manualActions" } - %a{ ":ref" => "action.play_url", - "data-method" => "post", - "rel" => "nofollow" } + %ul.dropdown-menu.dropdown-menu-align-right + %li{ "v-for" => "action in manualActions" } + %a{ ":href" => "action.play_url", + "data-method" => "post", + "rel" => "nofollow" } - = custom_icon('icon_play') - %span - {{action.name}} \ No newline at end of file + = custom_icon('icon_play') + %span + {{action.name}} \ No newline at end of file From a8508608eaf8440da7e55686c649f7209621494c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 21:11:58 +0000 Subject: [PATCH 071/302] Fixes the tests --- .../environments_store_spec.js.es6 | 26 +-- .../javascripts/environments/mock_data.js.es6 | 148 +++++++++++++++--- 2 files changed, 142 insertions(+), 32 deletions(-) diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 8d0fea934c3..bc16c90e9be 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,9 +1,8 @@ //= require vue //= require environments/stores/environmnets_store //= require ./mock_data - +/* globals environmentsList */ (() => { - beforeEach(() => { gl.environmentsList.EnvironmentsStore.create(); }); @@ -12,12 +11,12 @@ 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(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); }); describe('store environments', () => { beforeEach(() => { - gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); + gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); }); it('should count stopped environments and save the count in the state', () => { @@ -25,29 +24,30 @@ }); it('should count available environments and save the count in the state', () => { - expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(2); + 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(2); + expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3); - const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments.filter((env) => { - return env.children && env.children.length > 0; - }); + 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('review/test-environment') - expect(parentFolder[0].children[1].name).toBe('review/test-environment-1'); + 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].children[0].name).toBe('review/test-environment'); - expect(environments[1].children[1].name).toBe('review/test-environment-1'); + 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') }); }); }); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 35d94e3ab44..1142ace5846 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,26 +1,136 @@ +/* eslint-disable no-unused-vars */ const environmentsList = [ { - "id": 15, - "project_id": 11, - "name": "production", - "external_url": "https://test.com", - "environment_type": null, - "state": "available" + id: 31, + name: 'production', + 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_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + }, + deployable: { + id: 1278, + name: 'build', + build_url: 'http://localhost:3000/root/ci-folders/builds/1278', + retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry', + }, + manual_actions: [], + }, + 'stoppable?': true, + environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + created_at: '2016-11-07T11:11:16.525Z', + updated_at: '2016-11-07T11:11:16.525Z', }, { - "id": 18, - "project_id": 11, - "name": "review/test-environment", - "external_url": "http://test1.com", - "environment_type": "review", - "state": "available" + id: 32, + name: 'review_app', + state: 'stopped', + 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_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + }, + deployable: { + id: 1278, + name: 'build', + build_url: 'http://localhost:3000/root/ci-folders/builds/1278', + retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry', + }, + manual_actions: [], + }, + 'stoppable?': false, + environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + created_at: '2016-11-07T11:11:16.525Z', + updated_at: '2016-11-07T11:11:16.525Z', }, { - "id": 19, - "project_id": 11, - "name": "review/test-environment-1", - "external_url": "http://test-1.com", - "environment_type": "review", - "state": "stopped" - } + id: 33, + name: 'test-environment', + state: 'available', + environment_type: 'review', + last_deployment: null, + 'stoppable?': true, + environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + created_at: '2016-11-07T11:11:16.525Z', + updated_at: '2016-11-07T11:11:16.525Z', + }, + { + id: 34, + name: 'test-environment-1', + state: 'available', + environment_type: 'review', + last_deployment: null, + 'stoppable?': true, + environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + created_at: '2016-11-07T11:11:16.525Z', + updated_at: '2016-11-07T11:11:16.525Z', + }, ]; + From c3db10d3fb32babb608bb191596e3462454ac41f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 7 Nov 2016 21:53:20 +0000 Subject: [PATCH 072/302] Removes loading assertions --- .../components/environment_item.js.es6 | 2 +- .../environments/environments_bundle.js.es6 | 1 - spec/features/environments_spec.rb | 18 ------------------ 3 files changed, 1 insertion(+), 20 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 436ac365788..633a71ea9a1 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -86,7 +86,7 @@ * Verifies is the given environment has manual actions. * Used to verify if we should render them or nor. * - * @returns {Boolean} description + * @returns {Boolean} */ hasManualActions() { return this.$options.hasKey(this.model, 'manual_actions') && this.model.manual_actions.length; diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 1299f64182c..645274a8666 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -31,7 +31,6 @@ $(() => { return fn(item); }).filter(Boolean); - gl.EnvironmentsListApp = new Vue({ el: '#environments-list-view', diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 62fc56f2f46..edabb97e761 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -12,32 +12,14 @@ feature 'Environments', feature: true, js:true do login_as(user) project.team << [user, role] end - - describe 'Loading environments' do - given!(:environment) { } - given!(:deployment) { } - given!(:manual) { } - - context 'loading environments' do - before do - visit namespace_project_environments_path(project.namespace, project) - end - - scenario 'does show loading spinner' do - expect(page).to have_selector('.environments-list-loading') - end - end - end describe 'when showing environments' do - before do visit namespace_project_environments_path(project.namespace, project) wait_for_vue_resource end context 'without environments' do - scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Available') expect(page).to have_link('Stopped') From 9bf6d3abeba3b318741ab7ef0d4513c57f5112be Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 8 Nov 2016 10:33:36 +0000 Subject: [PATCH 073/302] Adds a list of environments --- spec/features/environments_spec.rb | 49 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index edabb97e761..595cf28ee77 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -1,17 +1,28 @@ -require 'spec_helper' require 'rails_helper' feature 'Environments', feature: true, js:true do include WaitForVueResource - given(:project) { create(:project) } - given(:user) { create(:user) } - given(:role) { :developer } - + let(:json) { serializer.as_json } + let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } + let(:role) { :developer } + + let(:serializer) do + described_class + .new(user: user, project: project) + .represent(resource) + end + background do login_as(user) project.team << [user, role] end + + before do + visit namespace_project_environments_path(project.namespace, project) + wait_for_vue_resource + end describe 'when showing environments' do before do @@ -36,15 +47,15 @@ feature 'Environments', feature: true, js:true do end context 'with environments' do - given!(:environment) { create(:environment, project: project) } - + let(:resource) { create_list(:environment, 2) } + scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Stopped') expect(page).to have_link('Available') end - scenario 'does show environment name' do - expect(page).to have_link(environment.name) + scenario 'does show environments table' do + expect(page).to have_selector('.table-holder') end scenario 'does show number of available and stopped environments' do @@ -53,20 +64,13 @@ feature 'Environments', feature: true, js:true do end context 'without deployments' do - - before do - visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource - end - scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') end end - + context 'with deployments' do - let!(:environment) { create(:environment, project: project) } - given(:deployment) { create(:deployment, environment: environment) } + # TODO add environment with deployment scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) @@ -142,17 +146,12 @@ feature 'Environments', feature: true, js:true do end context 'can create new environment' do - before do - visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource - end - scenario 'does have a New environment button' do expect(page).to have_link('New environment') end end end - + describe 'when showing the environment' do given(:environment) { create(:environment, project: project) } given!(:deployment) { } @@ -246,7 +245,7 @@ feature 'Environments', feature: true, js:true do end end end - + describe 'when creating a new environment' do before do visit namespace_project_environments_path(project.namespace, project) From a31578aa505088a347bfc6ae2483d8c563d1fee9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 8 Nov 2016 11:25:14 +0000 Subject: [PATCH 074/302] Remove unused block --- spec/features/environments_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 595cf28ee77..d60a6be397d 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -18,11 +18,6 @@ feature 'Environments', feature: true, js:true do login_as(user) project.team << [user, role] end - - before do - visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource - end describe 'when showing environments' do before do From 3fef5e66dba055e32dda1ccc887ad630d1e61c87 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 10:55:00 +0000 Subject: [PATCH 075/302] Adds template functions Adds commit component --- .../components/environment_item.js.es6 | 185 +++++++++++++++++- .../environments/environments_bundle.js.es6 | 4 +- .../vue_common_component/commit.js.es6 | 173 ++++++++++++++++ .../projects/environments/index.html.haml | 6 +- .../environments_store_spec.js.es6 | 2 +- 5 files changed, 358 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/vue_common_component/commit.js.es6 diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 633a71ea9a1..29c0316228e 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,4 +1,6 @@ +/*= require vue_common_component/commit /* globals Vue */ + (() => { /** * Envrionment Item Component @@ -16,7 +18,9 @@ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { - template: '#environment-item-template', + components: { + 'commit-component': window.gl.commitComponent, + }, props: { model: Object, @@ -25,6 +29,9 @@ data() { return { open: false, + rowClass: { + 'children-row': this.model['vue-isChildren'], + }, }; }, @@ -38,7 +45,8 @@ * @returns {Boolean} */ isFolder() { - return this.$options.hasKey(this.model, 'children') && this.model.children.length > 0; + return this.$options.hasKey(this.model, 'children') && + this.model.children.length > 0; }, /** @@ -58,7 +66,8 @@ * @returns {Boolean} The number of environments for the current folder */ childrenCounter() { - return this.$options.hasKey(this.model, 'children') && this.model.children.length; + return this.$options.hasKey(this.model, 'children') && + this.model.children.length; }, /** @@ -68,7 +77,8 @@ * @returns {Boolean} */ isLast() { - return this.$options.hasKey(this.model, 'last_deployment') && this.model.last_deployment['last?']; + return this.$options.hasKey(this.model, 'last_deployment') && + this.model.last_deployment['last?']; }, /** @@ -89,7 +99,8 @@ * @returns {Boolean} */ hasManualActions() { - return this.$options.hasKey(this.model, 'manual_actions') && this.model.manual_actions.length; + return this.$options.hasKey(this.model, 'manual_actions') && + this.model.manual_actions.length; }, /** @@ -108,7 +119,9 @@ * @returns {Boolean} */ canRetry() { - return this.hasLastDeploymentKey && this.model.last_deployment && this.$options.hasKey(this.model.last_deployment, 'deployable'); + return this.hasLastDeploymentKey && + this.model.last_deployment && + this.$options.hasKey(this.model.last_deployment, 'deployable'); }, /** @@ -134,6 +147,85 @@ return parsedAction; }); }, + + userImageAltDescription() { + return `${this.model.last_deployment.user.username}'s avatar'`; + }, + + + /** + * 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; + } + }, + + /** + * 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 + } + }, + + /** + * 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_url) { + return this.model.last_deployment.commit.commit_url; + } + }, + + /** + * 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 + } + }, + + /** + * 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 + } + }, + + /** + * 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; + } + }, }, /** @@ -149,7 +241,6 @@ }, methods: { - /** * Toggles the visibility of a folders' children. */ @@ -159,5 +250,85 @@ } }, }, + + template: ` + + + + {{model.name}} + + + + + + + + {{model.name}} + + + {{childrenCounter}} + + + + + + + #{{model.last_deployment.iid}} + + + by + + + + + + + + + + {{model.last_deployment.deployable.name}} #{{model.last_deployment.deployable.id}} + + + + +
+ + +
+

+ No deployments yet +

+ + + + + {{createdDate}} + + + + +
+ +
+ + + + + + `, }); })(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 645274a8666..1192f3e5bfb 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -68,8 +68,8 @@ $(() => { * Toggles loading property. */ ready() { - gl.environmentsService.all().then((resp) => { - Store.storeEnvironments(resp.json()); + gl.environmentsService.all().then(resp => resp.json()).then((json) => { + Store.storeEnvironments(json); this.loading = false; }); }, diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 new file mode 100644 index 00000000000..8847d1d0184 --- /dev/null +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -0,0 +1,173 @@ +/*= require vue +/* global Vue */ +(() => { + window.gl = window.gl || {}; + + window.gl.commitComponent = Vue.component('commit-component', { + + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + ref: { + type: Object, + required: false, + default: () => {}, + }, + + /** + * Used to link to the commit sha. + */ + commit_url: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short_sha that links to the commit url. + */ + short_sha: { + type: String, + required: false, + default: '', + }, + + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => {}, + }, + }, + + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasRef() { + return this.ref && this.ref.name && this.ref.ref_url; + }, + + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.web_url && + this.author.username; + }, + + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + + /** + * In order to reuse the svg instead of copy and paste in this template the html_safe + * we need to render it outside this component using =custom_icon partial. + * Make sure it has this structure: + * .commit-icon-svg.hidden + * svg + * + * TODO: Find a better way to include SVG + */ + ready() { + const commitIconContainer = document.querySelector('.branch-commit .commit-icon-container'); + const commitIcon = document.querySelector('.commit-icon-svg.hidden svg'); + + if (commitIconContainer && commitIcon) { + commitIconContainer.appendChild(commitIcon); + } + }, + + template: ` +
+ +
+ + +
+ + + {{ref.name}} + + +
+ +
+ + + {{short_sha}} + + +

+ + + + + + + + {{title}} + + + + Cant find HEAD commit for this branch + +

+
+ `, + }); +})(); diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index fa88d59e3ca..61a3d76274e 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -40,7 +40,10 @@ - if can?(current_user, :create_environment, @project) = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do New environment - + + .commit-icon-svg.hidden + = custom_icon("icon_commit") + .table-holder{ "v-if" => "!loading && state.environments.length" } %table.table.ci-table.environments %thead @@ -53,4 +56,3 @@ %tbody %tr{"is" => "environment-item", "v-for" => "model in filteredEnvironments", ":model" => "model"} -=render "projects/environments/components/environment" diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index bc16c90e9be..5e35949ac9c 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -47,7 +47,7 @@ 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') + expect(environments[2].name).toBe('review_app'); }); }); }); From de8a6c874b86ae5363864855aed24d8d33005c06 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 11:23:04 +0000 Subject: [PATCH 076/302] Removes jQuery timeago --- .../environments/components/environment_item.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 29c0316228e..05f0483c198 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -130,7 +130,9 @@ * @returns {String} */ createdDate() { - return $.timeago(this.model.created_at); + const timeagoInstance = new timeago(); + + return timeagoInstance.format(this.model.created_at); }, /** From 5cfc2d0c1ce3ec170a531e5da39703698c4f8e59 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 11:26:27 +0000 Subject: [PATCH 077/302] Fix eslint --- .../components/environment_item.js.es6 | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 05f0483c198..d029cc8c92e 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,5 +1,6 @@ /*= require vue_common_component/commit -/* globals Vue */ +/*= require timeago +/* globals Vue, timeago */ (() => { /** @@ -153,8 +154,7 @@ userImageAltDescription() { return `${this.model.last_deployment.user.username}'s avatar'`; }, - - + /** * If provided, returns the commit tag. * @@ -164,6 +164,7 @@ if (this.model.last_deployment && this.model.last_deployment.tag) { return this.model.last_deployment.tag; } + return undefined; }, /** @@ -173,10 +174,11 @@ */ commitRef() { if (this.model.last_deployment && this.model.last_deployment.ref) { - return this.model.last_deployment.ref + return this.model.last_deployment.ref; } + return undefined; }, - + /** * If provided, returns the commit url. * @@ -188,6 +190,7 @@ this.model.last_deployment.commit.commit_url) { return this.model.last_deployment.commit.commit_url; } + return undefined; }, /** @@ -199,8 +202,9 @@ 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 this.model.last_deployment.commit.short_id; } + return undefined; }, /** @@ -208,12 +212,13 @@ * * @returns {String|Undefined} */ - commitTitle(){ + 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 this.model.last_deployment.commit.title; } + return undefined; }, /** @@ -221,12 +226,14 @@ * * @returns {Object|Undefined} */ - commitAuthor(){ + 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; }, }, From 60099d9027e3d7816b1fb8de237dfd70a0c5cb3d Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 15:22:11 +0000 Subject: [PATCH 078/302] Adds environments actions component --- .../components/environment_actions.js.es6 | 39 +++++++++++++++++++ .../components/environment_item.js.es6 | 1 - .../environments/environments_bundle.js.es6 | 4 +- .../projects/environments/index.html.haml | 2 +- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment_actions.js.es6 diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 new file mode 100644 index 00000000000..6b0555360b5 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -0,0 +1,39 @@ +/*= require vue +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', { + props: { + actions: { + type: Array, + required: false, + default: () => [] + } + }, + + template: ` + + ` + }); +})(); \ No newline at end of file diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index d029cc8c92e..c51ea8707dd 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,5 +1,4 @@ /*= require vue_common_component/commit -/*= require timeago /* globals Vue, timeago */ (() => { diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 1192f3e5bfb..9766bd1da27 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -41,7 +41,9 @@ $(() => { data: { state: Store.state, - endpoint: environmentsListApp.dataset.endpoint, + endpoint: environmentsListApp.dataset.environmentsDataEndpoint, + canCreateDeployment: environmentsListApp.dataset.canCreateDeployment, + canReadEnvironment: environmentsListApp.dataset.canReadEnvironment, loading: true, visibility: 'available', }, diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 61a3d76274e..532b617b7b8 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -5,7 +5,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -#environments-list-view{ data: environments_list_data, class: container_class } +#environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @environment), "can_read_environment" => can?(current_user, :read_environment, @project)}, class: container_class } .top-area %ul.nav-links{ "v-if" => "!loading" } %li{class: ('active' if @scope.nil?)} From 883f65ec998683ae224ab69eb59ee45569e56f44 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 16:45:45 +0000 Subject: [PATCH 079/302] Fix commit icon --- app/assets/javascripts/vue_common_component/commit.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 8847d1d0184..44f94dffb5c 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -120,11 +120,11 @@ * TODO: Find a better way to include SVG */ ready() { - const commitIconContainer = document.querySelector('.branch-commit .commit-icon-container'); + const commitIconContainer = this.$el.querySelector('.commit-icon-container'); const commitIcon = document.querySelector('.commit-icon-svg.hidden svg'); if (commitIconContainer && commitIcon) { - commitIconContainer.appendChild(commitIcon); + commitIconContainer.appendChild(commitIcon.cloneNode(true)); } }, From 51e791868666cc7c5f196416f97605f968f69874 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 17:36:42 +0000 Subject: [PATCH 080/302] Manual actions --- .../components/environment_actions.js.es6 | 44 +++++++++++++++---- .../components/environment_item.js.es6 | 21 +++++++-- .../vue_common_component/commit.js.es6 | 2 +- .../projects/environments/index.html.haml | 2 + 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 6b0555360b5..6d49a10eb0e 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -4,28 +4,54 @@ (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - + window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', { props: { actions: { type: Array, required: false, - default: () => [] + default: () => [], + }, + }, + + /** + * Appends the svg icon that were render in the index page. + * In order to reuse the svg instead of copy and paste in this template + * we need to render it outside this component using =custom_icon partial. + * + * TODO: Remove this when webpack is merged. + * + */ + ready() { + const playIcon = document.querySelector('.play-icon-svg.hidden svg'); + + const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); + const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); + + if (playIcon) { + dropdownContainer.appendChild(playIcon.cloneNode(true)); + actionContainers.forEach((element) => { + element.appendChild(playIcon.cloneNode(true)); + }); } }, - + template: ` - ` - }); -})(); \ No newline at end of file + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index c51ea8707dd..4476e7689cd 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,4 +1,5 @@ /*= require vue_common_component/commit +/*= require ./environment_actions /* globals Vue, timeago */ (() => { @@ -19,7 +20,8 @@ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { components: { - 'commit-component': window.gl.commitComponent, + 'commit-component': window.gl.CommitComponent, + 'actions-component': window.gl.environmentsList.ActionsComponent, }, props: { @@ -135,13 +137,24 @@ return timeagoInstance.format(this.model.created_at); }, + /** + * Verifies if the environment has any manual actions to be rendered. + * + * @returns {Boolean} + */ + hasManualActions() { + return this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; + }, + /** * Returns the manual actions with the name parsed. * * @returns {Array.} */ manualActions() { - return this.model.manual_actions.map((action) => { + return this.model.last_deployment.manual_actions.map((action) => { const parsedAction = { name: gl.text.humanize(action.name), play_url: action.play_url, @@ -327,7 +340,9 @@
- +
+ +
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 44f94dffb5c..25c1c27bee7 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -3,7 +3,7 @@ (() => { window.gl = window.gl || {}; - window.gl.commitComponent = Vue.component('commit-component', { + window.gl.CommitComponent = Vue.component('commit-component', { props: { /** diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 532b617b7b8..050c87b8ab0 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -43,6 +43,8 @@ .commit-icon-svg.hidden = custom_icon("icon_commit") + .play-icon-svg.hidden + = custom_icon("icon_play") .table-holder{ "v-if" => "!loading && state.environments.length" } %table.table.ci-table.environments From 8068d977fe123c1c9a08bceb515a9e77acb39cec Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 17:53:06 +0000 Subject: [PATCH 081/302] Adds permissions to actions component --- .../components/environment_item.js.es6 | 31 ++++++++++++++++--- .../projects/environments/index.html.haml | 8 +++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 4476e7689cd..7ffc289719e 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -24,9 +24,7 @@ 'actions-component': window.gl.environmentsList.ActionsComponent, }, - props: { - model: Object, - }, + props: ['model', 'can-create-deployment', 'can-create-deployment', 'can-read-environment'], data() { return { @@ -247,6 +245,14 @@ return undefined; }, + + canReadEnvironmentParsed() { + return convertToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return convertToBoolean(this.canCreateDeployment); + }, }, /** @@ -261,6 +267,19 @@ return {}.hasOwnProperty.call(obj, key); }, + /** + * Converts permission provided as strings to booleans. + * @param {String} string + * @returns {Boolean} + */ + convertPermissionToBoolean(string) { + if (string === 'true') { + return true; + } + + return false; + }, + methods: { /** * Toggles the visibility of a folders' children. @@ -272,6 +291,10 @@ }, }, + ready() { + debugger; + }, + template: ` @@ -340,7 +363,7 @@
-
+
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 050c87b8ab0..391dc8cdce2 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -5,7 +5,7 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -#environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @environment), "can_read_environment" => can?(current_user, :read_environment, @project)}, class: container_class } +#environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @environment), "can-read-environment" => can?(current_user, :read_environment, @project)}, class: container_class } .top-area %ul.nav-links{ "v-if" => "!loading" } %li{class: ('active' if @scope.nil?)} @@ -56,5 +56,9 @@ %th %th.hidden-xs %tbody - %tr{"is" => "environment-item", "v-for" => "model in filteredEnvironments", ":model" => "model"} + %tr{"is" => "environment-item", + "v-for" => "model in filteredEnvironments", + ":model" => "model", + "can-create-deployment" => can?(current_user, :create_deployment, @environment), + "can-read-environment" => can?(current_user, :read_environment, @project)} From 32784d21d36a41f2c22f05b23db68a889b8886c0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 18:04:57 +0000 Subject: [PATCH 082/302] Adds external url component for environment --- .../environment_external_url.js.es6 | 22 +++++++++++++++++++ .../components/environment_item.js.es6 | 16 +++++++++----- .../vue_common_component/commit.js.es6 | 2 +- 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment_external_url.js.es6 diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 new file mode 100644 index 00000000000..0fd91afb32a --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -0,0 +1,22 @@ +/*= require vue +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { + props: { + external_url: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 7ffc289719e..06077c601b5 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,5 +1,6 @@ /*= require vue_common_component/commit /*= require ./environment_actions +/*= require ./environment_external_url /* globals Vue, timeago */ (() => { @@ -22,6 +23,7 @@ components: { 'commit-component': window.gl.CommitComponent, 'actions-component': window.gl.environmentsList.ActionsComponent, + 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, }, props: ['model', 'can-create-deployment', 'can-create-deployment', 'can-read-environment'], @@ -247,11 +249,11 @@ }, canReadEnvironmentParsed() { - return convertToBoolean(this.canReadEnvironment); + return this.$options.convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return convertToBoolean(this.canCreateDeployment); + return this.$options.convertPermissionToBoolean(this.canCreateDeployment); }, }, @@ -291,10 +293,6 @@ }, }, - ready() { - debugger; - }, - template: ` @@ -366,6 +364,12 @@
+ +
+ + +
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 25c1c27bee7..053d08bb089 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -111,7 +111,7 @@ }, /** - * In order to reuse the svg instead of copy and paste in this template the html_safe + * In order to reuse the svg instead of copy and paste in this template * we need to render it outside this component using =custom_icon partial. * Make sure it has this structure: * .commit-icon-svg.hidden From 05e57a079eba158594f56443f1754eafc122cba6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 18:52:26 +0000 Subject: [PATCH 083/302] Adds rollback and stop component --- .../environment_external_url.js.es6 | 2 +- .../components/environment_item.js.es6 | 55 +++++++++++++++---- .../components/environment_rollback.js.es6 | 31 +++++++++++ .../components/environment_stop.js.es6 | 26 +++++++++ .../stylesheets/pages/environments.scss | 3 +- 5 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment_rollback.js.es6 create mode 100644 app/assets/javascripts/environments/components/environment_stop.js.es6 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 0fd91afb32a..b5d540ea934 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -14,7 +14,7 @@ }, template: ` - + `, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 06077c601b5..07ed282a16d 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,6 +1,9 @@ /*= require vue_common_component/commit /*= require ./environment_actions /*= require ./environment_external_url +/*= require ./environment_stop +/*= require ./environment_rollback + /* globals Vue, timeago */ (() => { @@ -24,6 +27,8 @@ 'commit-component': window.gl.CommitComponent, 'actions-component': window.gl.environmentsList.ActionsComponent, 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, + 'stop-component': window.gl.environmentsList.StopComponent, + 'rollback-component': window.gl.environmentsList.RollbackComponent, }, props: ['model', 'can-create-deployment', 'can-create-deployment', 'can-read-environment'], @@ -248,11 +253,26 @@ return undefined; }, + retryUrl() { + if (this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_url) { + return this.model.last_deployment.deployable.retry_url; + } + return undefined; + }, + + isLastDeployment() { + return this.model.last_deployment && this.model.last_deployment['last?']; + }, + canReadEnvironmentParsed() { + return true; return this.$options.convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { + return true; return this.$options.convertPermissionToBoolean(this.canCreateDeployment); }, }, @@ -295,7 +315,7 @@ template: ` - + {{model.name}} @@ -313,7 +333,7 @@ - + #{{model.last_deployment.iid}} @@ -329,7 +349,7 @@ - + @@ -337,7 +357,7 @@ - +
- + {{createdDate}} - +
-
- +
+ +
- -
+ +
+ +
+ + +
+ +
+ + +
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 new file mode 100644 index 00000000000..055b0efec56 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -0,0 +1,31 @@ +/*= require vue +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { + props: { + retry_url: { + type: String, + default: '', + }, + is_last_deployment: { + type: Boolean, + default: true, + }, + }, + + template: ` + + + Re-deploy + + + Rollback + + + `, + }); +})(); diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 new file mode 100644 index 00000000000..c15b669c9c1 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -0,0 +1,26 @@ +/*= require vue +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.StopComponent = Vue.component('stop-component', { + props: { + stop_url: { + type: String, + default: '', + }, + }, + + template: ` + + + + `, + }); +})(); diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 6ce10f1e7ef..e42fa63ba00 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -47,7 +47,8 @@ color: $gl-dark-link-color; } - .stop-env-link { + .stop-env-link, + .external-url { color: $table-text-gray; .stop-env-icon { From 9be7aa0f5e200f8338be8e4c8b0c4a7f72517ced Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Wed, 9 Nov 2016 18:54:54 +0000 Subject: [PATCH 084/302] Removes unused partials --- .../environments/_environment.html.haml | 35 ----------- .../components/_actions.html.haml | 16 ----- .../environments/components/_commit.html.haml | 27 --------- .../components/_environment.html.haml | 60 ------------------- .../components/_external_url.html.haml | 5 -- .../components/_rollback.html.haml | 10 ---- .../environments/components/_stop.html.haml | 7 --- 7 files changed, 160 deletions(-) delete mode 100644 app/views/projects/environments/_environment.html.haml delete mode 100644 app/views/projects/environments/components/_actions.html.haml delete mode 100644 app/views/projects/environments/components/_commit.html.haml delete mode 100644 app/views/projects/environments/components/_environment.html.haml delete mode 100644 app/views/projects/environments/components/_external_url.html.haml delete mode 100644 app/views/projects/environments/components/_rollback.html.haml delete mode 100644 app/views/projects/environments/components/_stop.html.haml diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml deleted file mode 100644 index b75d5df4150..00000000000 --- a/app/views/projects/environments/_environment.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -- last_deployment = environment.last_deployment - -%tr.environment - %td - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) - - %td.deployment-column - - if last_deployment - %span ##{last_deployment.iid} - - if last_deployment.user - by - = user_avatar(user: last_deployment.user, size: 20) - - %td - - if last_deployment && last_deployment.deployable - = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do - = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})" - - %td - - if last_deployment - = render 'projects/deployments/commit', deployment: last_deployment - - else - %p.commit-title - No deployments yet - - %td - - if last_deployment - #{time_ago_with_tooltip(last_deployment.created_at)} - - %td.hidden-xs - .pull-right - = render 'projects/environments/external_url', environment: environment - = render 'projects/deployments/actions', deployment: last_deployment - = render 'projects/environments/stop', environment: environment - = render 'projects/deployments/rollback', deployment: last_deployment diff --git a/app/views/projects/environments/components/_actions.html.haml b/app/views/projects/environments/components/_actions.html.haml deleted file mode 100644 index f23fdb72353..00000000000 --- a/app/views/projects/environments/components/_actions.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "hasManualActions"} - .dropdown - %a.dropdown-new.btn.btn-default{type: "button", "data-toggle" => "dropdown"} - = custom_icon('icon_play') - = icon('caret-down') - - %ul.dropdown-menu.dropdown-menu-align-right - %li{ "v-for" => "action in manualActions" } - %a{ ":href" => "action.play_url", - "data-method" => "post", - "rel" => "nofollow" } - - = custom_icon('icon_play') - %span - {{action.name}} \ No newline at end of file diff --git a/app/views/projects/environments/components/_commit.html.haml b/app/views/projects/environments/components/_commit.html.haml deleted file mode 100644 index 8233dec7b93..00000000000 --- a/app/views/projects/environments/components/_commit.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.branch-commit{"v-if" => "!isFolder && model.last_deployment"} - .icon-container{ "v-if" => "model.last_deployment.ref && model.last_deployment.tag" } - =icon("tag") - - .icon-container{ "v-if" => "model.last_deployment.ref && !model.last_deployment.tag" } - =icon("code-fork") - - %a.monospace.branch-name{"v-if" => "model.last_deployment.ref", ":href" => "model.last_deployment.ref.ref_url"} - {{model.last_deployment.ref.name}} - - .icon-container.commit-icon - = custom_icon("icon_commit") - - %a.commit-id.monospace{":href" => "model.last_deployment.commit.commit_url"} - {{model.last_deployment.commit.short_id}} - - %p.commit-title - %span{ "v-if" => "model.last_deployment.commit && model.last_deployment.commit.title"} - %a.avatar-image-container{"v-if" => "model.last_deployment.commit.author", ":href" => "model.last_deployment.commit.author.web_url"} - %img.avatar.has-tooltip.s20{":title" => "model.last_deployment.commit.author.username", - ":alt" => "model.last_deployment.commit.author.username + `'s avatar`", - ":src" => "model.last_deployment.commit.author.avatar_url"} - %a.commit-row-message{":href" => "model.last_deployment.commit.commit_url"} - {{model.last_deployment.commit.title}} - - %span{ "v-if" => "!model.last_deployment.commit.title"} - Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/components/_environment.html.haml b/app/views/projects/environments/components/_environment.html.haml deleted file mode 100644 index 2fecfd85f61..00000000000 --- a/app/views/projects/environments/components/_environment.html.haml +++ /dev/null @@ -1,60 +0,0 @@ -%script#environment-item-template{ "type"=> "text/x-template" } - %tr - %td{"v-bind:class" => "{ 'children-row': isChildren}" } - %a.environment-name{ "v-if" => "!isFolder", - ":href" => "model.environment_url" } - {{model.name}} - - %span.folder-name{ "v-if" => "isFolder", - "@click" => "toggle" } - - %i.folder-icon{ "v-show" => "open" } - =icon ("caret-down") - %i.folder-icon{ "v-show" => "!open" } - =icon("caret-right") - - {{model.name}} - - %span.badge - {{childrenCounter}} - - %td.deployment-column - %span{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.iid" } - = precede "#" do - {{model.last_deployment.iid}} - - %span{ "v-if" => "model.last_deployment.user" } - by - %a{":href" => "model.last_deployment.user.web_url" } - %img.avatar.has-tooltip.s20{ ":src" => "model.last_deployment.user.avatar_url", - ":alt" => "model.last_deployment.user.username + `'s avatar`", - ":title" => "model.last_deployment.user.username"} - - %td - %a.build-link{ "v-if" => "!isFolder && model.last_deployment && model.last_deployment.deployable", - ":href" => "model.last_deployment.deployable.build" } - {{model.last_deployment.deployable.name}} - = precede "#" do - {{model.last_deployment.deployable.id}} - - %td - =render "projects/environments/components/commit" - - %p.commit-title{ "v-if" => "!isFolder && !model.last_deployment" } - No deployments yet - - %td - %span.environment-created-date-timeago{ "v-if" => "!isFolder && model.last_deployment" } - {{createdDate}} - - %td.hidden-xs - .pull-right{ "v-if" => "!isFolder" } - =render "projects/environments/components/external_url" - =render "projects/environments/components/actions" - =render "projects/environments/components/stop" - =render "projects/environments/components/rollback" - - %tr{"v-if" => "open && isFolder", - "is" => "environment-item", - "v-for" => "model in model.children", - ":model" => "model" } diff --git a/app/views/projects/environments/components/_external_url.html.haml b/app/views/projects/environments/components/_external_url.html.haml deleted file mode 100644 index 9b789d0ed5f..00000000000 --- a/app/views/projects/environments/components/_external_url.html.haml +++ /dev/null @@ -1,5 +0,0 @@ --if can?(current_user, :read_environment, @project) - %a.btn.external-url{ "v-if" => "!isFolder && model.external_url", - ":target" => "_blank", - ":href" => "model.external_url"} - = icon("external-link") diff --git a/app/views/projects/environments/components/_rollback.html.haml b/app/views/projects/environments/components/_rollback.html.haml deleted file mode 100644 index e3449c5515e..00000000000 --- a/app/views/projects/environments/components/_rollback.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if can?(current_user, :create_deployment, @project) - %a.btn.btn-build{ "v-if" => "canRetry", - ":href" => "model.last_deployment.deployable.retry_url", - "data-method" => "post", - "rel" => "nofollow" } - - %span{ "v-if" => "isLastDeployment" } - Re-deploy - %span{ "v-if" => "!isLastDeployment" } - Rollback diff --git a/app/views/projects/environments/components/_stop.html.haml b/app/views/projects/environments/components/_stop.html.haml deleted file mode 100644 index a7100f15784..00000000000 --- a/app/views/projects/environments/components/_stop.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- if can?(current_user, :create_deployment, @project) - .inline{ "v-if" => "isStoppable" } - %a.btn.stop-env-link{":href" => "model.environment_url", - "method" => ":post", - "rel" => "nofollow", - "confirm" => "Are you sure you want to stop this environment?"} - = icon("stop", class: "stop-env-icon") From 8cebb71e0a615341b6d3b17214dade0c8c094287 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 10 Nov 2016 15:43:38 +0000 Subject: [PATCH 085/302] Add template to environment vue instance --- .../components/environment_item.js.es6 | 31 +---- .../environments/environments_bundle.js.es6 | 106 +++++++++++++++++- .../projects/environments/index.html.haml | 68 ++--------- 3 files changed, 121 insertions(+), 84 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 07ed282a16d..1451c1f2a56 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -265,16 +265,6 @@ isLastDeployment() { return this.model.last_deployment && this.model.last_deployment['last?']; }, - - canReadEnvironmentParsed() { - return true; - return this.$options.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return true; - return this.$options.convertPermissionToBoolean(this.canCreateDeployment); - }, }, /** @@ -289,19 +279,6 @@ return {}.hasOwnProperty.call(obj, key); }, - /** - * Converts permission provided as strings to booleans. - * @param {String} string - * @returns {Boolean} - */ - convertPermissionToBoolean(string) { - if (string === 'true') { - return true; - } - - return false; - }, - methods: { /** * Toggles the visibility of a folders' children. @@ -381,25 +358,25 @@
-
+
-
+
-
+
-
+
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 9766bd1da27..7dff4d3ac2a 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -3,7 +3,7 @@ //= require_tree ./stores //= require_tree ./services //= require ./components/environment_item -//= require ../boards/vue_resource_interceptor +//= require ./vue_resource_interceptor /* globals Vue, EnvironmentsService */ /* eslint-disable no-param-reassign */ @@ -44,6 +44,11 @@ $(() => { endpoint: environmentsListApp.dataset.environmentsDataEndpoint, canCreateDeployment: environmentsListApp.dataset.canCreateDeployment, canReadEnvironment: environmentsListApp.dataset.canReadEnvironment, + canCreateEnvironment: environmentsListApp.dataset.canCreateEnvironment, + projectEnvironmentsPath: environmentsListApp.dataset.projectEnvironmentsPath, + projectClosedEnvironmentsPath: environmentsListApp.dataset.projectClosedEnvironmentsPath, + newEnvironmentPath: environmentsListApp.dataset.newEnvironmentPath, + helpPagePath: environmentsListApp.dataset.helpPagePath, loading: true, visibility: 'available', }, @@ -52,6 +57,18 @@ $(() => { filteredEnvironments() { return recursiveMap(filterState(this.visibility), this.state.environments); }, + + scope() { + return this.$options.getQueryParameter('scope'); + }, + + canReadEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateDeployment); + }, }, init: Store.create.bind(Store), @@ -90,5 +107,92 @@ $(() => { return acc; }, {})[parameter]; }, + + /** + * Converts permission provided as strings to booleans. + * @param {String} string + * @returns {Boolean} + */ + convertPermissionToBoolean(string) { + if (string === 'true') { + return true; + } + + return false; + }, + + template: ` +
+ + +
+
+ +
+ +
+

+ You don't have any environments right now. +

+

+ Environments are places where code gets deployed, such as staging or production. + +
+ + + Read more about environments + + + New Environment + +

+
+ +
+ + + + + + + + + + + + +
EnvironmentLast deploymentBuildCommit
+
+
+
+ `, }); }); diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 391dc8cdce2..be4fc4dcb1b 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -5,60 +5,16 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -#environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @environment), "can-read-environment" => can?(current_user, :read_environment, @project)}, class: container_class } - .top-area - %ul.nav-links{ "v-if" => "!loading" } - %li{class: ('active' if @scope.nil?)} - = link_to project_environments_path(@project) do - Available - %span.badge.js-available-environments-count - {{state.availableCounter}} - - %li{class: ('active' if @scope == 'stopped')} - = link_to project_environments_path(@project, scope: :stopped) do - Stopped - %span.badge.js-stopped-environments-count - {{state.stoppedCounter}} - - - if can?(current_user, :create_environment, @project) - .nav-controls{ "v-if" => "!loading" } - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: "btn btn-create" do - New environment - - .environments-container - .environments-list-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - - .blank-state.blank-state-no-icon{ "v-if" => "!loading && !state.environments.length" } - %h2.blank-state-title - You don't have any environments right now. - %p.blank-state-text - Environments are places where code gets deployed, such as staging or production. - %br - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - if can?(current_user, :create_environment, @project) - = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do - New environment - - .commit-icon-svg.hidden - = custom_icon("icon_commit") - .play-icon-svg.hidden - = custom_icon("icon_play") - - .table-holder{ "v-if" => "!loading && state.environments.length" } - %table.table.ci-table.environments - %thead - %th Environment - %th Last Deployment - %th Build - %th Commit - %th - %th.hidden-xs - %tbody - %tr{"is" => "environment-item", - "v-for" => "model in filteredEnvironments", - ":model" => "model", - "can-create-deployment" => can?(current_user, :create_deployment, @environment), - "can-read-environment" => can?(current_user, :read_environment, @project)} +.commit-icon-svg.hidden + = custom_icon("icon_commit") +.play-icon-svg.hidden + = custom_icon("icon_play") +#environments-list-view{ data: { environments_data: environments_list_data, + "can-create-deployment" => can?(current_user, :create_deployment, @environment), + "can-read-environment" => can?(current_user, :read_environment, @project), + "can-create-environmnet" => can?(current_user, :create_environment, @project), + "project-environments-path" => project_environments_path(@project), + "project-closed-environments-path" => project_environments_path(@project, scope: :stopped), + "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), + "help-page-path" => help_page_path("ci/environments")}, class: container_class } From 30c6a7d3acf658253af158ff7069081cf4b109ad Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 10 Nov 2016 18:58:35 +0000 Subject: [PATCH 086/302] Adds tests Adds tests. Changes instance into a constructor Adds tests for environments component Adds tests assertations Adds external URL test Adds tests for Rollback component Adds tests for stop component Adds tests for actions component Fix environment item Init environment item tests --- .../components/environment.js.es6 | 190 +++++++++++++++++ .../components/environment_item.js.es6 | 23 +- .../components/environment_stop.js.es6 | 14 +- .../environments/environments_bundle.js.es6 | 197 +----------------- ...store.js.es6 => environments_store.js.es6} | 2 + .../vue_resource_interceptor.js.es6 | 13 ++ .../projects/environments/index.html.haml | 2 +- spec/features/environments_spec.rb | 4 + .../environment_actions_spec.js.es6 | 37 ++++ .../environment_external_url_spec.js.es6 | 22 ++ .../environments/environment_item_spec.js.es6 | 157 ++++++++++++++ .../environment_rollback_spec.js.es6 | 48 +++++ .../environments/environment_spec.js.es6 | 172 +++++++++++++++ .../environments/environment_stop_spec.js.es6 | 34 +++ .../fixtures/environments/element.html.haml | 1 + .../environments/environments.html.haml | 9 + .../environments_no_permission.html.haml | 9 + .../fixtures/environments/table.html.haml | 11 + .../vue_common_components/commit_spec.js.es6 | 89 ++++++++ 19 files changed, 825 insertions(+), 209 deletions(-) create mode 100644 app/assets/javascripts/environments/components/environment.js.es6 rename app/assets/javascripts/environments/stores/{environmnets_store.js.es6 => environments_store.js.es6} (99%) create mode 100644 app/assets/javascripts/environments/vue_resource_interceptor.js.es6 create mode 100644 spec/javascripts/environments/environment_actions_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_external_url_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_item_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_rollback_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_spec.js.es6 create mode 100644 spec/javascripts/environments/environment_stop_spec.js.es6 create mode 100644 spec/javascripts/fixtures/environments/element.html.haml create mode 100644 spec/javascripts/fixtures/environments/environments.html.haml create mode 100644 spec/javascripts/fixtures/environments/environments_no_permission.html.haml create mode 100644 spec/javascripts/fixtures/environments/table.html.haml create mode 100644 spec/javascripts/vue_common_components/commit_spec.js.es6 diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 new file mode 100644 index 00000000000..9cdc17e8589 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -0,0 +1,190 @@ +//= require vue +//= require vue-resource +//= require_tree ../services/ +//= require ./environment_item + +/* globals Vue, EnvironmentsService */ +/* eslint-disable no-param-reassign */ + +$(() => { + window.gl = window.gl || {}; + + const filterState = state => environment => environment.state === state && environment; + + // recursiveMap :: (Function, Array) -> Array + const recursiveMap = (fn, arr) => arr.map((item) => { + if (item.children) { + const filteredChildren = recursiveMap(fn, item.children).filter(Boolean); + if (filteredChildren.length) { + item.children = filteredChildren; + return item; + } + } + return fn(item); + }).filter(Boolean); + + window.gl.environmentsList.EnvironmentsComponent = Vue.extend({ + props: ['store'], + + components: { + 'environment-item': window.gl.environmentsList.EnvironmentItem, + }, + + data() { + const environmentsListApp = document.querySelector('#environments-list-view'); + + return { + state: this.store.state, + endpoint: environmentsListApp.dataset.environmentsDataEndpoint, + canCreateDeployment: environmentsListApp.dataset.canCreateDeployment, + canReadEnvironment: environmentsListApp.dataset.canReadEnvironment, + canCreateEnvironment: environmentsListApp.dataset.canCreateEnvironment, + projectEnvironmentsPath: environmentsListApp.dataset.projectEnvironmentsPath, + projectStoppedEnvironmentsPath: environmentsListApp.dataset.projectStoppedEnvironmentsPath, + newEnvironmentPath: environmentsListApp.dataset.newEnvironmentPath, + helpPagePath: environmentsListApp.dataset.helpPagePath, + loading: true, + visibility: 'available', + }; + }, + + computed: { + filteredEnvironments() { + return recursiveMap(filterState(this.visibility), this.state.environments); + }, + + scope() { + return this.$options.getQueryParameter('scope'); + }, + + canReadEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canReadEnvironment); + }, + + canCreateDeploymentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateDeployment); + }, + }, + + created() { + window.gl.environmentsService = new EnvironmentsService(this.endpoint); + + const scope = this.$options.getQueryParameter('scope'); + if (scope) { + this.visibility = scope; + } + }, + + /** + * Fetches all the environmnets and stores them. + * Toggles loading property. + */ + ready() { + window.gl.environmentsService.all().then(resp => resp.json()).then((json) => { + this.store.storeEnvironments(json); + this.loading = false; + }); + }, + + /** + * Transforms the url parameter into an object and + * returns the one requested. + * + * @param {String} param + * @returns {String} The value of the requested parameter. + */ + getQueryParameter(parameter) { + return window.location.search.substring(1).split('&').reduce((acc, param) => { + const paramSplited = param.split('='); + acc[paramSplited[0]] = paramSplited[1]; + return acc; + }, {})[parameter]; + }, + + /** + * Converts permission provided as strings to booleans. + * @param {String} string + * @returns {Boolean} + */ + convertPermissionToBoolean(string) { + if (string === 'true') { + return true; + } + return false; + }, + + template: ` +
+ + +
+
+ +
+ +
+

+ You don't have any environments right now. +

+

+ Environments are places where code gets deployed, such as staging or production. + +
+ + + Read more about environments + + + New Environment + +

+
+ +
+ + + + + + + + + + + + +
EnvironmentLast deploymentBuildCommit
+
+
+
+ `, + }); +}); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 1451c1f2a56..ebe31cbc26b 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -31,7 +31,7 @@ 'rollback-component': window.gl.environmentsList.RollbackComponent, }, - props: ['model', 'can-create-deployment', 'can-create-deployment', 'can-read-environment'], + props: ['model', 'can-create-deployment', 'can-read-environment'], data() { return { @@ -107,7 +107,7 @@ */ hasManualActions() { return this.$options.hasKey(this.model, 'manual_actions') && - this.model.manual_actions.length; + this.model.manual_actions.length > 0; }, /** @@ -142,17 +142,6 @@ return timeagoInstance.format(this.model.created_at); }, - /** - * Verifies if the environment has any manual actions to be rendered. - * - * @returns {Boolean} - */ - hasManualActions() { - return this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - /** * Returns the manual actions with the name parsed. * @@ -359,25 +348,25 @@
-
-
-
- diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index c15b669c9c1..1ae02d8080c 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -13,12 +13,18 @@ }, }, + methods: { + openConfirmDialog() { + return window.confirm('Are you sure you want to stop this environment?'); + }, + }, + template: ` - + rel="nofollow"> `, diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 7dff4d3ac2a..20eee7976ec 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,198 +1,21 @@ //= require vue -//= require vue-resource -//= require_tree ./stores -//= require_tree ./services -//= require ./components/environment_item +//= require_tree ./stores/ +//= require ./components/environment //= require ./vue_resource_interceptor -/* globals Vue, EnvironmentsService */ -/* eslint-disable no-param-reassign */ + $(() => { - const environmentsListApp = document.getElementById('environments-list-view'); - const Store = gl.environmentsList.EnvironmentsStore; - window.gl = window.gl || {}; - if (gl.EnvironmentsListApp) { - gl.EnvironmentsListApp.$destroy(true); + if (window.gl.EnvironmentsListApp) { + window.gl.EnvironmentsListApp.$destroy(true); } + const Store = window.gl.environmentsList.EnvironmentsStore; - const filterState = state => environment => environment.state === state && environment; - - // recursiveMap :: (Function, Array) -> Array - const recursiveMap = (fn, arr) => arr.map((item) => { - if (item.children) { - const filteredChildren = recursiveMap(fn, item.children).filter(Boolean); - if (filteredChildren.length) { - item.children = filteredChildren; - return item; - } - } - return fn(item); - }).filter(Boolean); - - gl.EnvironmentsListApp = new Vue({ - - el: '#environments-list-view', - - components: { - item: gl.environmentsList.EnvironmentItem, + window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + propsData: { + store: Store.create(), }, - - data: { - state: Store.state, - endpoint: environmentsListApp.dataset.environmentsDataEndpoint, - canCreateDeployment: environmentsListApp.dataset.canCreateDeployment, - canReadEnvironment: environmentsListApp.dataset.canReadEnvironment, - canCreateEnvironment: environmentsListApp.dataset.canCreateEnvironment, - projectEnvironmentsPath: environmentsListApp.dataset.projectEnvironmentsPath, - projectClosedEnvironmentsPath: environmentsListApp.dataset.projectClosedEnvironmentsPath, - newEnvironmentPath: environmentsListApp.dataset.newEnvironmentPath, - helpPagePath: environmentsListApp.dataset.helpPagePath, - loading: true, - visibility: 'available', - }, - - computed: { - filteredEnvironments() { - return recursiveMap(filterState(this.visibility), this.state.environments); - }, - - scope() { - return this.$options.getQueryParameter('scope'); - }, - - canReadEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canReadEnvironment); - }, - - canCreateDeploymentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateDeployment); - }, - }, - - init: Store.create.bind(Store), - - created() { - gl.environmentsService = new EnvironmentsService(this.endpoint); - - const scope = this.$options.getQueryParameter('scope'); - if (scope) { - this.visibility = scope; - } - }, - - /** - * Fetches all the environmnets and stores them. - * Toggles loading property. - */ - ready() { - gl.environmentsService.all().then(resp => resp.json()).then((json) => { - Store.storeEnvironments(json); - this.loading = false; - }); - }, - - /** - * Transforms the url parameter into an object and - * returns the one requested. - * - * @param {String} param - * @returns {String} The value of the requested parameter. - */ - getQueryParameter(parameter) { - return window.location.search.substring(1).split('&').reduce((acc, param) => { - const paramSplited = param.split('='); - acc[paramSplited[0]] = paramSplited[1]; - return acc; - }, {})[parameter]; - }, - - /** - * Converts permission provided as strings to booleans. - * @param {String} string - * @returns {Boolean} - */ - convertPermissionToBoolean(string) { - if (string === 'true') { - return true; - } - - return false; - }, - - template: ` -
- - -
-
- -
- -
-

- You don't have any environments right now. -

-

- Environments are places where code gets deployed, such as staging or production. - -
- - - Read more about environments - - - New Environment - -

-
- -
- - - - - - - - - - - - -
EnvironmentLast deploymentBuildCommit
-
-
-
- `, }); }); diff --git a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 similarity index 99% rename from app/assets/javascripts/environments/stores/environmnets_store.js.es6 rename to app/assets/javascripts/environments/stores/environments_store.js.es6 index ef0188c15bf..b8fe25ef313 100644 --- a/app/assets/javascripts/environments/stores/environmnets_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -10,6 +10,8 @@ this.state.environments = []; this.state.stoppedCounter = 0; this.state.availableCounter = 0; + + return this; }, /** diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..d19a5969f96 --- /dev/null +++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 @@ -0,0 +1,13 @@ +/* eslint-disable */ +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next(function (response) { + console.log("this is the repsponse", JSON.stringify(response, null, ' ')); + if (typeof response.data === "string") { + response.data = JSON.parse(response.data) + } + + Vue.activeResources--; + }); +}); diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index be4fc4dcb1b..c47f1a21efa 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -15,6 +15,6 @@ "can-read-environment" => can?(current_user, :read_environment, @project), "can-create-environmnet" => can?(current_user, :create_environment, @project), "project-environments-path" => project_environments_path(@project), - "project-closed-environments-path" => project_environments_path(@project, scope: :stopped), + "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped), "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project), "help-page-path" => help_page_path("ci/environments")}, class: container_class } diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index d60a6be397d..f4c0b093246 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -43,6 +43,10 @@ feature 'Environments', feature: true, js:true do context 'with environments' do let(:resource) { create_list(:environment, 2) } + before do + endpoint = namespace_project_environments_path(project.namespace, project) + stub_request(:any, endpoint).to_return(body: [{"name": "test"}]) + end scenario 'does show "Available" and "Stopped" tab with links' do expect(page).to have_link('Stopped') diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 new file mode 100644 index 00000000000..1097582a8e9 --- /dev/null +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -0,0 +1,37 @@ +//= require vue +//= require environments/components/environment_actions + +describe('Actions Component', () => { + fixture.preload('environments/element.html'); + + beforeEach(() => { + fixture.load('environments/element.html'); + }); + + it('Should render a dropdown with the provided actions', () => { + const actionsMock = [ + { + name: 'bar', + play_url: 'https://gitlab.com/play', + }, + { + name: 'foo', + play_url: '#', + }, + ]; + + const component = new window.gl.environmentsList.ActionsComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + actions: actionsMock, + }, + }); + + expect( + component.$el.querySelectorAll('.dropdown-menu li').length + ).toEqual(actionsMock.length); + expect( + component.$el.querySelector('.dropdown-menu li a').getAttribute('href') + ).toEqual(actionsMock[0].play_url); + }); +}); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 new file mode 100644 index 00000000000..156506ef28f --- /dev/null +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -0,0 +1,22 @@ +//= require vue +//= require environments/components/environment_external_url + +describe('External URL Component', () => { + fixture.preload('environments/element.html'); + beforeEach(() => { + fixture.load('environments/element.html'); + }); + + it('should link to the provided external_url', () => { + const externalURL = 'https://gitlab.com'; + const component = new window.gl.environmentsList.ExternalUrlComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + external_url: externalURL, + }, + }); + + expect(component.$el.getAttribute('href')).toEqual(externalURL); + expect(component.$el.querySelector('fa-external-link')).toBeDefined(); + }); +}); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 new file mode 100644 index 00000000000..f357e11dc8e --- /dev/null +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -0,0 +1,157 @@ +//= require vue +//= require environments/components/environment_item + +describe('Environment item', () => { + fixture.preload('environments/table.html'); + beforeEach(() => { + fixture.load('environments/table.html'); + }); + + describe('When item is folder', () => { + let mockItem; + let component; + + 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', + }, + ], + }; + + component = new window.gl.environmentsList.EnvironmentItem({ + el: document.querySelector('tr#environment-row'), + propsData: { + model: mockItem, + 'can-create-deployment': false, + 'can-read-environment': true, + }, + }); + }); + + it('Should render clickable folder icon and name', () => { + expect(document.querySelector('.folder-name').textContent).toContain(mockItem.name); + expect(document.querySelector('.folder-icon')).toBeDefined(); + }); + + it('Should render the number of children in a badge', () => { + expect(document.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); + }); + + it('Should not render any information other than the name', () => { + }); + + describe('when clicked', () => { + it('Should render child row', () => { + }); + }); + }); + + describe('when item is not folder', () => { + it('should render environment name', () => { + + }); + + describe('With deployment', () => { + it('should render deployment internal id', () => { + + }); + + it('should link to deployment', () => { + + }); + + describe('With user information', () => { + it('should render user avatar with link to profile', () => { + + }); + }); + + describe('With build url', () => { + it('Should link to build url provided', () => { + + }); + + it('Should render deployable name and id', () => { + + }); + }); + + describe('With commit information', () => { + it('should render commit component', () => {}); + }); + + it('Should render timeago created date', () => { + + }); + }); + + describe('Without deployment', () => { + it('should render no deployments information', () => { + + }); + }); + + describe('With manual actions', () => { + describe('With create deployment permission', () => { + it('Should render actions component', () => { + + }); + }); + describe('Without create deployment permission', () => { + it('should not render actions component', () => { + + }); + }); + }); + + describe('With external URL', () => { + it('should render external url component', () => { + + }); + }); + + describe('With stop action', () => { + describe('With create deployment permission', () => { + it('Should render stop action component', () => { + + }); + }); + describe('Without create deployment permission', () => { + it('should not render stop action component', () => { + + }); + }); + }); + + describe('With retry action', () => { + describe('With create deployment permission', () => { + it('Should render rollback component', () => { + + }); + }); + describe('Without create deployment permission', () => { + it('should not render rollback component', () => { + + }); + }); + }); + }); +}); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 new file mode 100644 index 00000000000..29449bbbd9e --- /dev/null +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -0,0 +1,48 @@ +//= require vue +//= require environments/components/environment_rollback +describe('Rollback Component', () => { + fixture.preload('environments/element.html'); + + const retryURL = 'https://gitlab.com/retry'; + + beforeEach(() => { + fixture.load('environments/element.html'); + }); + + it('Should link to the provided retry_url', () => { + const component = new window.gl.environmentsList.RollbackComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + retry_url: retryURL, + is_last_deployment: true, + }, + }); + + expect(component.$el.getAttribute('href')).toEqual(retryURL); + }); + + it('Should render Re-deploy label when is_last_deployment is true', () => { + const component = new window.gl.environmentsList.RollbackComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + retry_url: retryURL, + is_last_deployment: true, + }, + }); + + expect(component.$el.querySelector('span').textContent).toContain('Re-deploy'); + }); + + + it('Should render Rollback label when is_last_deployment is false', () => { + const component = new window.gl.environmentsList.RollbackComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + retry_url: retryURL, + is_last_deployment: false, + }, + }); + + expect(component.$el.querySelector('span').textContent).toContain('Rollback'); + }); +}); diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 new file mode 100644 index 00000000000..07eb9938c58 --- /dev/null +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -0,0 +1,172 @@ +//= require vue +//= require environments/stores/environments_store +//= require environments/components/environment + +/* globals environmentsList */ +describe('Environments', () => { + fixture.preload('environments/environments.html'); + fixture.preload('environments/environments_no_permission.html'); + let Store; + let component; + + beforeEach(() => { + Store = window.gl.environmentsList.EnvironmentsStore; + }); + + describe('While loading', () => { + beforeEach(() => { + fixture.load('environments/environments.html'); + component = new window.gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + propsData: { + store: Store.create(), + }, + }); + }); + + it('Should render two tabs', () => { + expect(component.$el.querySelectorAll('ul li').length).toEqual(2); + }); + + it('Should render bagdes with zeros in both tabs indicating the number of available environments', () => { + expect( + component.$el.querySelector('.js-available-environments-count').textContent + ).toContain('0'); + expect( + component.$el.querySelector('.js-stopped-environments-count').textContent + ).toContain('0'); + }); + + it('Should render loading icon', () => { + expect( + component.$el.querySelector('environments-list-loading') + ).toBeDefined(); + }); + }); + + describe('Without environments', () => { + beforeEach(() => { + fixture.load('environments/environments.html'); + + spyOn(component, 'ready').and.callFake(() => { + return { + then: callback => callback([]), + json: () => ({ then: cb => cb([]) }), + }; + }); + + component = new window.gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + propsData: { + store: Store.create(), + }, + }); + }); + + it('Should render two tabs', () => { + expect(component.$el.querySelectorAll('ul li').length).toEqual(2); + }); + + it('Should render bagdes with zeros in both tabs indicating the number of available environments', () => { + expect( + component.$el.querySelector('.js-available-environments-count').textContent + ).toContain('0'); + expect( + component.$el.querySelector('.js-stopped-environments-count').textContent + ).toContain('0'); + }); + + it('Should render blank state information', () => { + expect( + component.$el.querySelector('.blank-state-title').textContent + ).toEqual('You don\'t have any environments right now.'); + + expect( + component.$el.querySelector('.blank-state-text').textContent + ).toEqual('Environments are places where code gets deployed, such as staging or production.'); + }); + + it('Should render the provided help url', () => { + expect( + component.$el.querySelector('.blank-state a').getAttribute('href') + ).toEqual(component.$data.helpPagePath); + }); + + describe('With create permission', () => { + it('Should render new environment button', () => { + expect( + component.$el.querySelector('a.btn-create').getAttribute('href') + ).toEqual(component.$data.newEnvironmentPath); + expect( + component.$el.querySelector('a.btn-create').textContent + ).toEqual('New environment'); + }); + }); + + describe('Without create permission', () => { + beforeEach('Load fixture without permission', () => { + fixture.load('environments/environments_no_permission.html'); + component = new window.gl.environmentsList.EnvironmentsComponent({ + el: document.querySelector('#environments-list-view'), + propsData: { + store: Store.create(), + }, + }); + }); + + it('Should not render new environment button', () => { + + }); + }); + }); + + describe('With environments', () => { + describe('Tabs behavior', () => { + it('Should render two tabs', () => { + + }); + + it('Should render badges with the correct count', () => { + + }); + + describe('When clicking in the available tab', () => { + it('Should make Available tab active', () => { + + }); + + it('Should make visible only available environments', () => { + + }); + }); + + describe('When clicking in the stopped tab', () => { + it('Should make Stopped tab active', () => { + + }); + + it('Should make visible only stopped environments', () => { + + }); + }); + }); + + describe('With create permissions', () => { + it('Should render new environment button', () => { + + }); + }); + + describe('Without create permissions', () => { + it('Should not render the new environment button', () => { + }); + }); + + it('Should render a table', () => { + }); + + it('Should render table pagination', () => { + + }); + }); +}); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 new file mode 100644 index 00000000000..07a68bf28fb --- /dev/null +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -0,0 +1,34 @@ +//= require vue +//= require environments/components/environment_stop +describe('Stop Component', () => { + fixture.preload('environments/element.html'); + beforeEach(() => { + fixture.load('environments/element.html'); + }); + + it('should link to the provided URL', () => { + const stopURL = 'https://gitlab.com/stop'; + const component = new window.gl.environmentsList.StopComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + stop_url: stopURL, + }, + }); + expect(component.$el.getAttribute('href')).toEqual(stopURL); + }); + + describe('When clicked', () => { + it('Should open popup with confirmation warning', () => { + const component = new window.gl.environmentsList.StopComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + stop_url: '#', + }, + }); + + const spy = spyOn(window, 'confirm'); + component.$el.click(); + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/fixtures/environments/element.html.haml b/spec/javascripts/fixtures/environments/element.html.haml new file mode 100644 index 00000000000..d709c863137 --- /dev/null +++ b/spec/javascripts/fixtures/environments/element.html.haml @@ -0,0 +1 @@ +.test-dom-element \ No newline at end of file diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml new file mode 100644 index 00000000000..dd1d6855ce2 --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments.html.haml @@ -0,0 +1,9 @@ +%div + #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments", + "can-create-deployment" => "true", + "can-read-environment" => "true", + "can-create-environmnet" => "true", + "project-environments-path" => "https://gitlab.com/foo/environments", + "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped", + "new-environment-path" => "https://gitlab.com/foo/environments/new", + "help-page-path" => "https://gitlab.com/help_page"}} \ No newline at end of file diff --git a/spec/javascripts/fixtures/environments/environments_no_permission.html.haml b/spec/javascripts/fixtures/environments/environments_no_permission.html.haml new file mode 100644 index 00000000000..71cf7db7a34 --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments_no_permission.html.haml @@ -0,0 +1,9 @@ +%div + #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments", + "can-create-deployment" => "false", + "can-read-environment" => "true", + "can-create-environmnet" => "false", + "project-environments-path" => "https://gitlab.com/foo/environments", + "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped", + "new-environment-path" => "https://gitlab.com/foo/environments/new", + "help-page-path" => "https://gitlab.com/help_page"}} \ No newline at end of file diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml new file mode 100644 index 00000000000..1ea1725c561 --- /dev/null +++ b/spec/javascripts/fixtures/environments/table.html.haml @@ -0,0 +1,11 @@ +%table + %thead + %tr + %th Environment + %th Last deployment + %th Build + %th Commit + %th + %th + %tbody + %tr#environment-row diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 new file mode 100644 index 00000000000..4f158e8ffa5 --- /dev/null +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -0,0 +1,89 @@ +/*= require vue_common_components/commit */ +/* eslint-disable */ + +describe('Commit component', () => { + const getRenderedText = (Component, propsData) => { + const Constructor = Vue.extend(Component); + const vm = new Constructor({propsData}).$mount(); + return vm.$el.textContent; + }; + + const MyComponent = window.gl.commitComponent; + + describe('When `ref` is provided', () => { + const props = { + tag: true, + ref: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master' + }, + commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + short_sha: 'b7836edd', + title: 'Commit message', + author: { + avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + web_url: 'https://gitlab.com/jschatz1', + username: 'jschatz1' + } + }; + + it('should render a tag icon if it represents a tag', () => { + const renderedText = getRenderedText(MyComponent, props); + + }); + + it('should render a code-fork icon if it does not represent a tag', () => { + + }); + + it('should render a link to the ref url', () => { + + }); + + it('should render the ref name', () => { + + }); + }); +}); + +it('should render the commit icon as an svg', () => { + +}); + +it('should render the commit short sha with a link to the commit url', () => { + +}); + +describe('Given commit title and author props', () => { + it('Should render a link to the author profile', () => { + + }); + + it('Should render the author avatar with title and alt attributes', () => { + + }); +}); + +describe('When commit title is not provided', () => { + it('Should render default message', () => { + + }); +}); + +describe('Given no ref prop', () => { + it('Should render without errors', () => { + + }); +}); + +describe('Given no title prop', () => { + it('Should render without errors', () => { + + }); +}); + +describe('Given no author prop', () => { + it('Should render without errors', () => { + + }); +}); \ No newline at end of file From a7a13ed3ef281f36dcd0c15540437aaf9aac39dc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 14 Nov 2016 11:17:02 +0000 Subject: [PATCH 087/302] Update to vue 2 --- .../components/environment.js.es6 | 13 +++--- .../components/environment_actions.js.es6 | 6 +-- .../components/environment_item.js.es6 | 40 +++++++++++++------ .../vue_resource_interceptor.js.es6 | 3 +- .../vue_common_component/commit.js.es6 | 21 ++++++---- 5 files changed, 49 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 9cdc17e8589..53fd9891a60 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -45,6 +45,7 @@ $(() => { helpPagePath: environmentsListApp.dataset.helpPagePath, loading: true, visibility: 'available', + isLoading: this.loading, }; }, @@ -79,7 +80,7 @@ $(() => { * Fetches all the environmnets and stores them. * Toggles loading property. */ - ready() { + mounted() { window.gl.environmentsService.all().then(resp => resp.json()).then((json) => { this.store.storeEnvironments(json); this.loading = false; @@ -120,17 +121,13 @@ $(() => {
  • Available - - {{state.availableCounter}} - +
  • Stopped - - {{state.stoppedCounter}} - +
  • @@ -179,7 +176,7 @@ $(() => { v-for="model in filteredEnvironments" :model="model" :can-create-deployment="canCreateDeploymentParsed" - :can-read-environment="canReadEnvironmentParsed"> + :can-read-environment="canReadEnvironmentParsed">
    diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 6d49a10eb0e..1d04b8a4701 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -22,7 +22,7 @@ * TODO: Remove this when webpack is merged. * */ - ready() { + mounted() { const playIcon = document.querySelector('.play-icon-svg.hidden svg'); const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); @@ -52,9 +52,7 @@ - - {{action.name}} - + diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index ebe31cbc26b..414bbd4d623 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -254,6 +254,20 @@ isLastDeployment() { return this.model.last_deployment && this.model.last_deployment['last?']; }, + + 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 undefined; + }, + + deploymentInternalId() { + if (this.model.last_deployment) { + return `#${this.model.last_deployment.iid}`; + } + return ''; + }, }, /** @@ -282,8 +296,11 @@ template: ` - - {{model.name}} + @@ -291,17 +308,14 @@ - {{model.name}} + - - {{childrenCounter}} - + - - #{{model.last_deployment.iid}} + by @@ -318,8 +332,8 @@ - {{model.last_deployment.deployable.name}} #{{model.last_deployment.deployable.id}} + :href="model.last_deployment.deployable.build_url" + v-html="buildName"> @@ -340,8 +354,10 @@ - - {{createdDate}} + diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 index d19a5969f96..6d3e71d0110 100644 --- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 @@ -3,11 +3,10 @@ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; next(function (response) { - console.log("this is the repsponse", JSON.stringify(response, null, ' ')); if (typeof response.data === "string") { response.data = JSON.parse(response.data) } - + Vue.activeResources--; }); }); diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 053d08bb089..8d4949cda69 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -119,7 +119,7 @@ * * TODO: Find a better way to include SVG */ - ready() { + mounted() { const commitIconContainer = this.$el.querySelector('.commit-icon-container'); const commitIcon = document.querySelector('.commit-icon-svg.hidden svg'); @@ -136,22 +136,27 @@
    - - {{ref.name}} +
    - - {{short_sha}} +

    - + - - {{title}} + From 09c4542946fb5ba8f7ac12bd8952f5669156a27e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 14 Nov 2016 15:38:41 +0000 Subject: [PATCH 088/302] Refactor to work with vue 2 with just one root element --- .../components/environment.js.es6 | 72 ++++++++++++++----- .../components/environment_item.js.es6 | 38 +++------- .../stores/environments_store.js.es6 | 24 +++++++ 3 files changed, 87 insertions(+), 47 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 53fd9891a60..296e18ae8fd 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -81,7 +81,9 @@ $(() => { * Toggles loading property. */ mounted() { - window.gl.environmentsService.all().then(resp => resp.json()).then((json) => { + window.gl.environmentsService.all() + .then(resp => resp.json()) + .then((json) => { this.store.storeEnvironments(json); this.loading = false; }); @@ -114,20 +116,30 @@ $(() => { return false; }, + methods: { + toggleRow(model) { + return this.store.toggleFolder(model.name); + }, + }, + template: ` -

    +
    @@ -143,7 +155,9 @@ $(() => {
    -
    +

    You don't have any environments right now.

    @@ -155,28 +169,48 @@ $(() => { Read more about environments - + New Environment

    -
    +
    - - - - - - + + + + + + + + - +
    EnvironmentLast deploymentBuildCommit
    EnvironmentLast deploymentBuildCommit
    diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 414bbd4d623..3f976ca19e8 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -31,11 +31,10 @@ 'rollback-component': window.gl.environmentsList.RollbackComponent, }, - props: ['model', 'can-create-deployment', 'can-read-environment'], + props: ['model', 'toggleRow', 'can-create-deployment', 'can-read-environment'], data() { return { - open: false, rowClass: { 'children-row': this.model['vue-isChildren'], }, @@ -282,30 +281,19 @@ return {}.hasOwnProperty.call(obj, key); }, - methods: { - /** - * Toggles the visibility of a folders' children. - */ - toggle() { - if (this.isFolder) { - this.open = !this.open; - } - }, - }, - template: ` - + - + - - + + @@ -314,7 +302,7 @@ - + @@ -329,7 +317,7 @@ - + - +
    - + - +
    - - - `, }); })(); diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index b8fe25ef313..928786f0741 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -64,6 +64,8 @@ acc.push({ name: environment.environment_type, children: [environment], + isOpen: false, + 'vue-isChildren': environment['vue-isChildren'], }); } } else { @@ -78,6 +80,28 @@ return environmentsTree; }, + /** + * Toggles folder open property given the environment type. + * + * @param {String} envType + * @return {Array} + */ + toggleFolder(envType) { + const environments = this.state.environments; + + const environmnetsCopy = environments.map((env) => { + if (env['vue-isChildren'] === true && env.name === envType) { + env.isOpen = !env.isOpen; + } + + return env; + }); + + this.state.environments = environmnetsCopy; + + return environmnetsCopy; + }, + /** * Given an array of environments, returns the number of environments * that have the given state. From 56aaa3973b5e62ee8527be073246c138ab83fdc6 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 14 Nov 2016 15:58:51 +0000 Subject: [PATCH 089/302] Revert changes made on rspec tests for environment --- spec/features/environments_spec.rb | 161 +++++++++++++---------------- 1 file changed, 73 insertions(+), 88 deletions(-) diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index f4c0b093246..b565586ee14 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -1,36 +1,32 @@ -require 'rails_helper' +require 'spec_helper' + +feature 'Environments', feature: true do + given(:project) { create(:empty_project) } + given(:user) { create(:user) } + given(:role) { :developer } -feature 'Environments', feature: true, js:true do - include WaitForVueResource - - let(:json) { serializer.as_json } - let(:project) { create(:empty_project, :public) } - let(:user) { create(:user) } - let(:role) { :developer } - - let(:serializer) do - described_class - .new(user: user, project: project) - .represent(resource) - end - background do login_as(user) project.team << [user, role] end describe 'when showing environments' do + given!(:environment) { } + given!(:deployment) { } + given!(:manual) { } + before do visit namespace_project_environments_path(project.namespace, project) - wait_for_vue_resource end - context 'without environments' do - scenario 'does show "Available" and "Stopped" tab with links' do + context 'shows two tabs' do + scenario 'shows "Available" and "Stopped" tab with links' do expect(page).to have_link('Available') expect(page).to have_link('Stopped') end + end + context 'without environments' do scenario 'does show no environments' do expect(page).to have_content('You don\'t have any environments right now.') end @@ -42,99 +38,90 @@ feature 'Environments', feature: true, js:true do end context 'with environments' do - let(:resource) { create_list(:environment, 2) } - before do - endpoint = namespace_project_environments_path(project.namespace, project) - stub_request(:any, endpoint).to_return(body: [{"name": "test"}]) + given(:environment) { create(:environment, project: project) } + + scenario 'does show environment name' do + expect(page).to have_link(environment.name) end - - scenario 'does show "Available" and "Stopped" tab with links' do - expect(page).to have_link('Stopped') - expect(page).to have_link('Available') - end - - scenario 'does show environments table' do - expect(page).to have_selector('.table-holder') - end - + scenario 'does show number of available and stopped environments' do expect(page.find('.js-available-environments-count').text).to eq('1') expect(page.find('.js-stopped-environments-count').text).to eq('0') end - + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('No deployments yet') end end - + context 'with deployments' do - # TODO add environment with deployment - + given(:deployment) { create(:deployment, environment: environment) } + scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end - + scenario 'does show deployment internal id' do expect(page).to have_content(deployment.iid) end - + context 'with build and manual actions' do given(:pipeline) { create(:ci_pipeline, project: project) } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - + scenario 'does show a play button' do expect(page).to have_link(manual.name.humanize) end - + scenario 'does allow to play manual action' do expect(manual).to be_skipped expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + scenario 'does show build name and id' do expect(page).to have_link("#{build.name} (##{build.id})") end - + scenario 'does not show stop button' do expect(page).not_to have_selector('.stop-env-link') end - + scenario 'does not show external link button' do expect(page).not_to have_css('external-url') end - - context 'with external_url' do + + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end - - context 'with stop action' do + + context 'with stop action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - + scenario 'does show stop button' do expect(page).to have_selector('.stop-env-link') end - + scenario 'starts build when stop button clicked' do first('.stop-env-link').click - + expect(page).to have_content('close_app') end - + context 'for reporter' do let(:role) { :reporter } - + scenario 'does not show stop button' do expect(page).not_to have_selector('.stop-env-link') end @@ -143,98 +130,96 @@ feature 'Environments', feature: true, js:true do end end end - - context 'can create new environment' do - scenario 'does have a New environment button' do - expect(page).to have_link('New environment') - end + + scenario 'does have a New environment button' do + expect(page).to have_link('New environment') end end - - describe 'when showing the environment' do + + describe 'when showing the environment' do given(:environment) { create(:environment, project: project) } given!(:deployment) { } given!(:manual) { } - + before do visit namespace_project_environment_path(project.namespace, project, environment) end - + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('You don\'t have any deployments right now.') end end - + context 'with deployments' do given(:deployment) { create(:deployment, environment: environment) } - + scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end - + scenario 'does not show a re-deploy button for deployment without build' do expect(page).not_to have_link('Re-deploy') end - + context 'with build' do given(:pipeline) { create(:ci_pipeline, project: project) } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") end - + scenario 'does show re-deploy button' do expect(page).to have_link('Re-deploy') end - + scenario 'does not show stop button' do expect(page).not_to have_link('Stop') end - + context 'with manual action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } - + scenario 'does show a play button' do expect(page).to have_link(manual.name.humanize) end - + scenario 'does allow to play manual action' do expect(manual).to be_skipped expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } expect(page).to have_content(manual.name) expect(manual.reload).to be_pending end - + context 'with external_url' do given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } - + scenario 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) end end - + context 'with stop action' do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') } given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') } - + scenario 'does show stop button' do expect(page).to have_link('Stop') end - + scenario 'does allow to stop environment' do click_link('Stop') - + expect(page).to have_content('close_app') end - + context 'for reporter' do let(:role) { :reporter } - + scenario 'does not show stop button' do expect(page).not_to have_link('Stop') end @@ -244,43 +229,43 @@ feature 'Environments', feature: true, js:true do end end end - + describe 'when creating a new environment' do before do visit namespace_project_environments_path(project.namespace, project) end - + context 'when logged as developer' do before do click_link 'New environment' end - + context 'for valid name' do before do fill_in('Name', with: 'production') click_on 'Save' end - + scenario 'does create a new pipeline' do expect(page).to have_content('Production') end end - + context 'for invalid name' do before do fill_in('Name', with: 'name,with,commas') click_on 'Save' end - + scenario 'does show errors' do expect(page).to have_content('Name can contain only letters') end end end - + context 'when logged as reporter' do given(:role) { :reporter } - + scenario 'does not have a New environment link' do expect(page).not_to have_link('New environment') end From dd80e09a7bcd4da9d4505090499e0da6e8555a9c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 14 Nov 2016 17:22:21 +0000 Subject: [PATCH 090/302] Adds tests --- .../environments/environment_item_spec.js.es6 | 120 ++++++++++++++++-- 1 file changed, 109 insertions(+), 11 deletions(-) diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index f357e11dc8e..1029d632054 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -9,7 +9,6 @@ describe('Environment item', () => { describe('When item is folder', () => { let mockItem; - let component; beforeEach(() => { mockItem = { @@ -35,38 +34,137 @@ describe('Environment item', () => { }, ], }; + }); - component = new window.gl.environmentsList.EnvironmentItem({ + it('Should render clickable folder icon and name', () => { + const component = new window.gl.environmentsList.EnvironmentItem({ el: document.querySelector('tr#environment-row'), propsData: { model: mockItem, + toggleRow: () => {}, 'can-create-deployment': false, 'can-read-environment': true, }, }); - }); - it('Should render clickable folder icon and name', () => { - expect(document.querySelector('.folder-name').textContent).toContain(mockItem.name); - expect(document.querySelector('.folder-icon')).toBeDefined(); + expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name); + expect(component.$el.querySelector('.folder-icon')).toBeDefined(); }); it('Should render the number of children in a badge', () => { - expect(document.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); - }); + const component = new window.gl.environmentsList.EnvironmentItem({ + el: document.querySelector('tr#environment-row'), + propsData: { + model: mockItem, + toggleRow: () => {}, + 'can-create-deployment': false, + 'can-read-environment': true, + }, + }); - it('Should not render any information other than the name', () => { + expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); }); describe('when clicked', () => { - it('Should render child row', () => { + it('Should call the given prop', () => { + const component = new window.gl.environmentsList.EnvironmentItem({ + el: document.querySelector('tr#environment-row'), + propsData: { + model: mockItem, + toggleRow: () => { + console.log('here!'); + }, + 'can-create-deployment': false, + 'can-read-environment': true, + }, + }); + + spyOn(component.$options.propsData, 'toggleRow'); + component.$el.querySelector('.folder-name').click(); + + expect(component.$options.propsData.toggleRow).toHaveBeenCalled(); }); }); }); describe('when item is not folder', () => { - it('should render environment name', () => { + let environment; + beforeEach(() => { + environment = { + id: 31, + name: 'production', + state: 'stopped', + external_url: 'http://external.com', + environment_type: null, + last_deployment: { + id: 66, + iid: 6, + sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + ref: { + name: 'master', + ref_url: 'http://localhost:3000/root/ci-folders/tree/master', + }, + tag: true, + '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_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + }, + deployable: { + id: 1279, + name: 'deploy', + build_url: 'http://localhost:3000/root/ci-folders/builds/1279', + retry_url: 'http://localhost:3000/root/ci-folders/builds/1279/retry', + }, + manual_actions: [ + { + name: 'action', + play_url: 'http://localhost:3000/play', + }, + ], + }, + 'stoppable?': true, + environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + created_at: '2016-11-07T11:11:16.525Z', + updated_at: '2016-11-10T15:55:58.778Z', + }; + }); + + it('should render environment name', () => { + const component = new window.gl.environmentsList.EnvironmentItem({ + el: document.querySelector('tr#environment-row'), + propsData: { + model: environment, + toggleRow: () => {}, + 'can-create-deployment': false, + 'can-read-environment': true, + }, + }); + + debugger; }); describe('With deployment', () => { From bebbf12a085b09caeaea98e7247d460364a6cfef Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 15 Nov 2016 10:12:59 +0000 Subject: [PATCH 091/302] Fix JSON problem --- app/helpers/environments_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 7c09c20d118..515e802e01e 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -1,7 +1,7 @@ module EnvironmentsHelper - def environments_list_data() + def environments_list_data { - endpoint: namespace_project_environments_path(@project.namespace, @project) + endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json) } end end From ed07264532cac11b1f7cc4c68627cb7bfb7e41fc Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 15 Nov 2016 12:07:11 +0000 Subject: [PATCH 092/302] Adds props validation Improves documentation Adds tests Fix prop validation for objects Finish tests for environment item Adds tests for toggle folder function Environment tests Adds tests --- .../boards/services/board_service.js.es6 | 1 + .../components/environment.js.es6 | 45 ++-- .../environment_external_url.js.es6 | 2 +- .../components/environment_item.js.es6 | 196 ++++++++++++------ .../services/environments_service.js.es6 | 5 +- .../vue_common_component/commit.js.es6 | 4 +- db/schema.rb | 3 + spec/features/environments_spec.rb | 3 +- .../environments/environment_item_spec.js.es6 | 136 +++++------- .../environments/environment_spec.js.es6 | 172 --------------- .../environments_store_spec.js.es6 | 17 +- .../environments/environments.html.haml | 8 +- .../environments_no_permission.html.haml | 9 - 13 files changed, 243 insertions(+), 358 deletions(-) delete mode 100644 spec/javascripts/environments/environment_spec.js.es6 delete mode 100644 spec/javascripts/fixtures/environments/environments_no_permission.html.haml diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f59a2ed7937..570944e132d 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -11,6 +11,7 @@ class BoardService { this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); Vue.http.interceptors.push((request, next) => { + debugger; request.headers['X-CSRF-Token'] = $.rails.csrfToken(); next(); }); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 296e18ae8fd..42b32811e4f 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -23,8 +23,14 @@ $(() => { return fn(item); }).filter(Boolean); - window.gl.environmentsList.EnvironmentsComponent = Vue.extend({ - props: ['store'], + window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { + props: { + store: { + type: Object, + required: true, + default: () => ({}), + }, + }, components: { 'environment-item': window.gl.environmentsList.EnvironmentItem, @@ -43,9 +49,8 @@ $(() => { projectStoppedEnvironmentsPath: environmentsListApp.dataset.projectStoppedEnvironmentsPath, newEnvironmentPath: environmentsListApp.dataset.newEnvironmentPath, helpPagePath: environmentsListApp.dataset.helpPagePath, - loading: true, visibility: 'available', - isLoading: this.loading, + isLoading: false, }; }, @@ -65,6 +70,10 @@ $(() => { canCreateDeploymentParsed() { return this.$options.convertPermissionToBoolean(this.canCreateDeployment); }, + + canCreateEnvironmentParsed() { + return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); + }, }, created() { @@ -74,6 +83,15 @@ $(() => { if (scope) { this.visibility = scope; } + + this.isLoading = true; + + return window.gl.environmentsService.all() + .then(resp => resp.json()) + .then((json) => { + this.store.storeEnvironments(json); + this.isLoading = false; + }); }, /** @@ -81,12 +99,7 @@ $(() => { * Toggles loading property. */ mounted() { - window.gl.environmentsService.all() - .then(resp => resp.json()) - .then((json) => { - this.store.storeEnvironments(json); - this.loading = false; - }); + }, /** @@ -143,21 +156,21 @@ $(() => { -
    -
    +
    + v-if="!isLoading && state.environments.length === 0">

    You don't have any environments right now.

    @@ -170,7 +183,7 @@ $(() => { Read more about environments New Environment @@ -180,7 +193,7 @@ $(() => {
    + v-if="!isLoading && state.environments.length > 0"> 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 b5d540ea934..eca0c368622 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -14,7 +14,7 @@ }, template: ` - + `, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 3f976ca19e8..bea6d09aacc 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,3 +1,5 @@ +/*= require lib/utils/timeago +/*= require lib/utils/text_utility /*= require vue_common_component/commit /*= require ./environment_actions /*= require ./environment_external_url @@ -31,7 +33,30 @@ 'rollback-component': window.gl.environmentsList.RollbackComponent, }, - props: ['model', 'toggleRow', 'can-create-deployment', 'can-read-environment'], + 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, + }, + }, data() { return { @@ -48,10 +73,10 @@ * Folder items have different behaviours - it is possible to toggle * them and show their children. * - * @returns {Boolean} + * @returns {Boolean|Undefined} */ isFolder() { - return this.$options.hasKey(this.model, 'children') && + return this.model.children && this.model.children.length > 0; }, @@ -69,24 +94,13 @@ * Counts the number of environments in each folder. * Used to show a badge with the counter. * - * @returns {Boolean} The number of environments for the current folder + * @returns {Number|Undefined} The number of environments for the current folder. */ childrenCounter() { - return this.$options.hasKey(this.model, 'children') && + return this.model.children && this.model.children.length; }, - /** - * Returns the value of the `last?` key sent in the API. - * Used to know wich title to render when the environment can be re-deployed - * - * @returns {Boolean} - */ - isLast() { - return this.$options.hasKey(this.model, 'last_deployment') && - this.model.last_deployment['last?']; - }, - /** * Verifies if `last_deployment` key exists in the current Envrionment. * This key is required to render most of the html - this method works has @@ -95,18 +109,21 @@ * @returns {Boolean} */ hasLastDeploymentKey() { - return this.$options.hasKey(this.model, 'last_deployment'); + if (this.model.last_deployment && 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} + * @returns {Boolean|Undefined} */ hasManualActions() { - return this.$options.hasKey(this.model, 'manual_actions') && - this.model.manual_actions.length > 0; + return this.model.last_deployment && this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; }, /** @@ -122,12 +139,12 @@ * 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} + * @returns {Boolean|Undefined} */ canRetry() { return this.hasLastDeploymentKey && this.model.last_deployment && - this.$options.hasKey(this.model.last_deployment, 'deployable'); + this.model.last_deployment.deployable; }, /** @@ -144,20 +161,33 @@ /** * Returns the manual actions with the name parsed. * - * @returns {Array.} + * @returns {Array.|Undefined} */ manualActions() { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: gl.text.humanize(action.name), - play_url: action.play_url, - }; - return parsedAction; - }); + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map((action) => { + const parsedAction = { + name: gl.text.humanize(action.name), + play_url: action.play_url, + }; + return parsedAction; + }); + } + return []; }, + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ userImageAltDescription() { - return `${this.model.last_deployment.user.username}'s avatar'`; + 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 ''; }, /** @@ -166,7 +196,8 @@ * @returns {String|Undefined} */ commitTag() { - if (this.model.last_deployment && this.model.last_deployment.tag) { + if (this.model.last_deployment && + this.model.last_deployment.tag) { return this.model.last_deployment.tag; } return undefined; @@ -178,7 +209,8 @@ * @returns {Object|Undefined} */ commitRef() { - if (this.model.last_deployment && this.model.last_deployment.ref) { + if (this.model.last_deployment && + this.model.last_deployment.ref) { return this.model.last_deployment.ref; } return undefined; @@ -241,6 +273,11 @@ return undefined; }, + /** + * Verifies if the `retry_url` key is present and returns its value. + * + * @returns {String|Undefined} + */ retryUrl() { if (this.model.last_deployment && this.model.last_deployment.deployable && @@ -250,35 +287,66 @@ 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) { + if (this.model.last_deployment && + this.model.last_deployment.deployable) { return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; } - return undefined; + return ''; }, + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ deploymentInternalId() { - if (this.model.last_deployment) { + if (this.model.last_deployment && + this.model.last_deployment.iid) { return `#${this.model.last_deployment.iid}`; } return ''; }, - }, - /** - * Helper to verify if key is present in an object. - * Can be removed once we start using lodash. - * - * @param {Object} obj - * @param {String} key - * @returns {Boolean} - */ - hasKey(obj, key) { - return {}.hasOwnProperty.call(obj, key); + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + if (this.model.last_deployment && + this.model.last_deployment.user) { + return true; + } + return false; + }, + + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if (this.model.last_deployment && this.model.last_deployment.user) { + return this.model.last_deployment.user; + } + return {}; + }, }, template: ` @@ -303,17 +371,19 @@ @@ -326,7 +396,7 @@ @@ -351,25 +421,25 @@ @@ -435,7 +435,7 @@
    + :stop_url="model.environment_path">
    From 9d3949abaa304c2c7e6b75b213e6ff0ac5128a8f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 17 Nov 2016 16:20:52 +0000 Subject: [PATCH 191/302] inline conditions --- .../environments/components/environment_item.js.es6 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0e29350d7e4..871da5790ec 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -76,8 +76,7 @@ * @returns {Boolean|Undefined} */ isFolder() { - return this.model.children && - this.model.children.length > 0; + return this.model.children && this.model.children.length > 0; }, /** @@ -97,8 +96,7 @@ * @returns {Number|Undefined} The number of environments for the current folder. */ childrenCounter() { - return this.model.children && - this.model.children.length; + return this.model.children && this.model.children.length; }, /** From d5b673da79c7c350090dc9982a9a2477fac56eb5 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 17 Nov 2016 18:00:37 +0100 Subject: [PATCH 192/302] more refactoring and added some auth checks --- .../cycle_analytics/events_controller.rb | 2 +- db/schema.rb | 1194 +++++++---------- lib/gitlab/cycle_analytics/base_event.rb | 36 +- lib/gitlab/cycle_analytics/code_event.rb | 33 +- lib/gitlab/cycle_analytics/event_config.rb | 16 - lib/gitlab/cycle_analytics/events.rb | 16 +- lib/gitlab/cycle_analytics/events_query.rb | 2 +- lib/gitlab/cycle_analytics/issue_event.rb | 30 +- lib/gitlab/cycle_analytics/plan_event.rb | 62 +- .../cycle_analytics/production_event.rb | 28 +- lib/gitlab/cycle_analytics/review_event.rb | 30 +- lib/gitlab/cycle_analytics/staging_event.rb | 20 +- lib/gitlab/cycle_analytics/test_event.rb | 20 +- .../lib/gitlab/cycle_analytics/events_spec.rb | 2 +- 14 files changed, 674 insertions(+), 817 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/event_config.rb diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 7b6214d71b5..8c2fb98a120 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -50,7 +50,7 @@ module Projects end def options - @options ||= { from: start_date(events_params) } + @options ||= { from: start_date(events_params), current_user: current_user } end def events_params diff --git a/db/schema.rb b/db/schema.rb index 633107f7db7..22318612796 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -13,142 +13,107 @@ ActiveRecord::Schema.define(version: 20161109150329) do + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + enable_extension "pg_trgm" + create_table "abuse_reports", force: :cascade do |t| - t.integer "reporter_id", limit: 4 - t.integer "user_id", limit: 4 - t.text "message", limit: 65535 + t.integer "reporter_id" + t.integer "user_id" + t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.text "message_html", limit: 65535 + t.text "message_html" end create_table "appearances", force: :cascade do |t| - t.string "title", limit: 255 - t.text "description", limit: 65535 - t.string "logo", limit: 255 - t.integer "updated_by", limit: 4 - t.datetime "created_at" - t.datetime "updated_at" - t.string "header_logo", limit: 255 - t.text "description_html", limit: 65535 + t.string "title" + t.text "description" + t.string "header_logo" + t.string "logo" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description_html" end create_table "application_settings", force: :cascade do |t| - t.integer "default_projects_limit", limit: 4 + t.integer "default_projects_limit" t.boolean "signup_enabled" t.boolean "signin_enabled" t.boolean "gravatar_enabled" - t.text "sign_in_text", limit: 65535 + t.text "sign_in_text" t.datetime "created_at" t.datetime "updated_at" - t.string "home_page_url", limit: 255 - t.integer "default_branch_protection", limit: 4, default: 2 - t.text "help_text", limit: 65535 - t.text "restricted_visibility_levels", limit: 65535 + t.string "home_page_url" + t.integer "default_branch_protection", default: 2 + t.text "restricted_visibility_levels" t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", limit: 4, default: 10, null: false - t.integer "default_project_visibility", limit: 4 - t.integer "default_snippet_visibility", limit: 4 - t.text "domain_whitelist", limit: 65535 + t.integer "max_attachment_size", default: 10, null: false + t.integer "default_project_visibility" + t.integer "default_snippet_visibility" + t.text "domain_whitelist" t.boolean "user_oauth_applications", default: true - t.string "after_sign_out_path", limit: 255 - t.integer "session_expire_delay", limit: 4, default: 10080, null: false - t.text "import_sources", limit: 65535 - t.text "help_page_text", limit: 65535 - t.string "admin_notification_email", limit: 255 + t.string "after_sign_out_path" + t.integer "session_expire_delay", default: 10080, null: false + t.text "import_sources" + t.text "help_page_text" + t.string "admin_notification_email" t.boolean "shared_runners_enabled", default: true, null: false - t.integer "max_artifacts_size", limit: 4, default: 100, null: false - t.string "runners_registration_token", limit: 255 - t.integer "max_pages_size", limit: 4, default: 100, null: false + t.integer "max_artifacts_size", default: 100, null: false + t.string "runners_registration_token" t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", limit: 4, default: 48 + t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false - t.string "metrics_host", limit: 255, default: "localhost" - t.integer "metrics_pool_size", limit: 4, default: 16 - t.integer "metrics_timeout", limit: 4, default: 10 - t.integer "metrics_method_call_threshold", limit: 4, default: 10 + t.string "metrics_host", default: "localhost" + t.integer "metrics_pool_size", default: 16 + t.integer "metrics_timeout", default: 10 + t.integer "metrics_method_call_threshold", default: 10 t.boolean "recaptcha_enabled", default: false - t.string "recaptcha_site_key", limit: 255 - t.string "recaptcha_private_key", limit: 255 - t.integer "metrics_port", limit: 4, default: 8089 + t.string "recaptcha_site_key" + t.string "recaptcha_private_key" + t.integer "metrics_port", default: 8089 t.boolean "akismet_enabled", default: false - t.string "akismet_api_key", limit: 255 - t.integer "metrics_sample_interval", limit: 4, default: 15 + t.string "akismet_api_key" + t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false - t.string "sentry_dsn", limit: 255 + t.string "sentry_dsn" t.boolean "email_author_in_body", default: false - t.integer "default_group_visibility", limit: 4 + t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.text "shared_runners_text", limit: 65535 - t.integer "metrics_packet_size", limit: 4, default: 1 - t.text "disabled_oauth_sign_in_sources", limit: 65535 - t.string "health_check_access_token", limit: 255 + t.text "shared_runners_text" + t.integer "metrics_packet_size", default: 1 + t.text "disabled_oauth_sign_in_sources" + t.string "health_check_access_token" t.boolean "send_user_confirmation_email", default: false - t.integer "container_registry_token_expire_delay", limit: 4, default: 5 - t.text "after_sign_up_text", limit: 65535 + t.integer "container_registry_token_expire_delay", default: 5 + t.text "after_sign_up_text" t.boolean "user_default_external", default: false, null: false - t.boolean "elasticsearch_indexing", default: false, null: false - t.boolean "elasticsearch_search", default: false, null: false - t.string "elasticsearch_host", limit: 255, default: "localhost" - t.string "elasticsearch_port", limit: 255, default: "9200" - t.string "repository_storages", limit: 255, default: "default" - t.string "enabled_git_access_protocol", limit: 255 + t.string "repository_storages", default: "default" + t.string "enabled_git_access_protocol" t.boolean "domain_blacklist_enabled", default: false - t.text "domain_blacklist", limit: 65535 - t.boolean "usage_ping_enabled", default: true, null: false + t.text "domain_blacklist" t.boolean "koding_enabled" - t.string "koding_url", limit: 255 - t.integer "repository_size_limit", limit: 4, default: 0 - t.text "sign_in_text_html", limit: 65535 - t.text "help_page_text_html", limit: 65535 - t.text "shared_runners_text_html", limit: 65535 - t.text "after_sign_up_text_html", limit: 65535 - t.boolean "user_activity_enabled", default: true, null: false + t.string "koding_url" + t.text "sign_in_text_html" + t.text "help_page_text_html" + t.text "shared_runners_text_html" + t.text "after_sign_up_text_html" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false - t.integer "housekeeping_incremental_repack_period", limit: 4, default: 10, null: false - t.integer "housekeeping_full_repack_period", limit: 4, default: 50, null: false - t.integer "housekeeping_gc_period", limit: 4, default: 200, null: false + t.integer "housekeeping_incremental_repack_period", default: 10, null: false + t.integer "housekeeping_full_repack_period", default: 50, null: false + t.integer "housekeeping_gc_period", default: 200, null: false t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues", limit: 255 - t.decimal "sidekiq_throttling_factor", precision: 10 + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" end - create_table "approvals", force: :cascade do |t| - t.integer "merge_request_id", limit: 4, null: false - t.integer "user_id", limit: 4, null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "approver_groups", force: :cascade do |t| - t.integer "target_id", limit: 4, null: false - t.string "target_type", limit: 255, null: false - t.integer "group_id", limit: 4, null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "approver_groups", ["group_id"], name: "index_approver_groups_on_group_id", using: :btree - add_index "approver_groups", ["target_id", "target_type"], name: "index_approver_groups_on_target_id_and_target_type", using: :btree - - create_table "approvers", force: :cascade do |t| - t.integer "target_id", limit: 4, null: false - t.string "target_type", limit: 255 - t.integer "user_id", limit: 4, null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "approvers", ["target_id", "target_type"], name: "index_approvers_on_target_id_and_target_type", using: :btree - add_index "approvers", ["user_id"], name: "index_approvers_on_user_id", using: :btree - create_table "audit_events", force: :cascade do |t| - t.integer "author_id", limit: 4, null: false - t.string "type", limit: 255, null: false - t.integer "entity_id", limit: 4, null: false - t.string "entity_type", limit: 255, null: false - t.text "details", limit: 65535 + t.integer "author_id", null: false + t.string "type", null: false + t.integer "entity_id", null: false + t.string "entity_type", null: false + t.text "details" t.datetime "created_at" t.datetime "updated_at" end @@ -156,10 +121,10 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree create_table "award_emoji", force: :cascade do |t| - t.string "name", limit: 255 - t.integer "user_id", limit: 4 - t.integer "awardable_id", limit: 4 - t.string "awardable_type", limit: 255 + t.string "name" + t.integer "user_id" + t.integer "awardable_id" + t.string "awardable_type" t.datetime "created_at" t.datetime "updated_at" end @@ -169,23 +134,22 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "award_emoji", ["user_id"], name: "index_award_emoji_on_user_id", using: :btree create_table "boards", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false + t.integer "project_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "name", limit: 255, default: "Development", null: false end add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree create_table "broadcast_messages", force: :cascade do |t| - t.text "message", limit: 65535, null: false + t.text "message", null: false t.datetime "starts_at" t.datetime "ends_at" t.datetime "created_at" t.datetime "updated_at" - t.string "color", limit: 255 - t.string "font", limit: 255 - t.text "message_html", limit: 65535 + t.string "color" + t.string "font" + t.text "message_html" end create_table "ci_application_settings", force: :cascade do |t| @@ -196,44 +160,44 @@ ActiveRecord::Schema.define(version: 20161109150329) do end create_table "ci_builds", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.string "status", limit: 255 + t.integer "project_id" + t.string "status" t.datetime "finished_at" - t.text "trace", limit: 65535 + t.text "trace" t.datetime "created_at" t.datetime "updated_at" t.datetime "started_at" - t.integer "runner_id", limit: 4 - t.float "coverage", limit: 24 - t.integer "commit_id", limit: 4 - t.text "commands", limit: 65535 - t.integer "job_id", limit: 4 - t.string "name", limit: 255 + t.integer "runner_id" + t.float "coverage" + t.integer "commit_id" + t.text "commands" + t.integer "job_id" + t.string "name" t.boolean "deploy", default: false - t.text "options", limit: 65535 + t.text "options" t.boolean "allow_failure", default: false, null: false - t.string "stage", limit: 255 - t.integer "trigger_request_id", limit: 4 - t.integer "stage_idx", limit: 4 + t.string "stage" + t.integer "trigger_request_id" + t.integer "stage_idx" t.boolean "tag" - t.string "ref", limit: 255 - t.integer "user_id", limit: 4 - t.string "type", limit: 255 - t.string "target_url", limit: 255 - t.string "description", limit: 255 - t.text "artifacts_file", limit: 65535 - t.integer "gl_project_id", limit: 4 - t.text "artifacts_metadata", limit: 65535 - t.integer "erased_by_id", limit: 4 + t.string "ref" + t.integer "user_id" + t.string "type" + t.string "target_url" + t.string "description" + t.text "artifacts_file" + t.integer "gl_project_id" + t.text "artifacts_metadata" + t.integer "erased_by_id" t.datetime "erased_at" t.datetime "artifacts_expire_at" - t.string "environment", limit: 255 + t.string "environment" t.integer "artifacts_size", limit: 8 - t.string "when", limit: 255 - t.text "yaml_variables", limit: 65535 + t.string "when" + t.text "yaml_variables" t.datetime "queued_at" - t.string "token", limit: 255 - t.integer "lock_version", limit: 4 + t.string "token" + t.integer "lock_version" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -248,23 +212,23 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree create_table "ci_commits", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.string "ref", limit: 255 - t.string "sha", limit: 255 - t.string "before_sha", limit: 255 - t.text "push_data", limit: 65535 + t.integer "project_id" + t.string "ref" + t.string "sha" + t.string "before_sha" + t.text "push_data" t.datetime "created_at" t.datetime "updated_at" t.boolean "tag", default: false - t.text "yaml_errors", limit: 65535 + t.text "yaml_errors" t.datetime "committed_at" - t.integer "gl_project_id", limit: 4 - t.string "status", limit: 255 + t.integer "gl_project_id" + t.string "status" t.datetime "started_at" t.datetime "finished_at" - t.integer "duration", limit: 4 - t.integer "user_id", limit: 4 - t.integer "lock_version", limit: 4 + t.integer "duration" + t.integer "user_id" + t.integer "lock_version" end add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree @@ -274,75 +238,75 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree create_table "ci_events", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.integer "user_id", limit: 4 - t.integer "is_admin", limit: 4 - t.text "description", limit: 65535 + t.integer "project_id" + t.integer "user_id" + t.integer "is_admin" + t.text "description" t.datetime "created_at" t.datetime "updated_at" end create_table "ci_jobs", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.text "commands", limit: 65535 + t.integer "project_id", null: false + t.text "commands" t.boolean "active", default: true, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "name", limit: 255 + t.string "name" t.boolean "build_branches", default: true, null: false t.boolean "build_tags", default: false, null: false - t.string "job_type", limit: 255, default: "parallel" - t.string "refs", limit: 255 + t.string "job_type", default: "parallel" + t.string "refs" t.datetime "deleted_at" end create_table "ci_projects", force: :cascade do |t| - t.string "name", limit: 255 - t.integer "timeout", limit: 4, default: 3600, null: false + t.string "name" + t.integer "timeout", default: 3600, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "token", limit: 255 - t.string "default_ref", limit: 255 - t.string "path", limit: 255 + t.string "token" + t.string "default_ref" + t.string "path" t.boolean "always_build", default: false, null: false - t.integer "polling_interval", limit: 4 + t.integer "polling_interval" t.boolean "public", default: false, null: false - t.string "ssh_url_to_repo", limit: 255 - t.integer "gitlab_id", limit: 4 + t.string "ssh_url_to_repo" + t.integer "gitlab_id" t.boolean "allow_git_fetch", default: true, null: false - t.string "email_recipients", limit: 255, default: "", null: false + t.string "email_recipients", default: "", null: false t.boolean "email_add_pusher", default: true, null: false t.boolean "email_only_broken_builds", default: true, null: false - t.string "skip_refs", limit: 255 - t.string "coverage_regex", limit: 255 + t.string "skip_refs" + t.string "coverage_regex" t.boolean "shared_runners_enabled", default: false - t.text "generated_yaml_config", limit: 65535 + t.text "generated_yaml_config" end create_table "ci_runner_projects", force: :cascade do |t| - t.integer "runner_id", limit: 4, null: false - t.integer "project_id", limit: 4 + t.integer "runner_id", null: false + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id", limit: 4 + t.integer "gl_project_id" end add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree create_table "ci_runners", force: :cascade do |t| - t.string "token", limit: 255 + t.string "token" t.datetime "created_at" t.datetime "updated_at" - t.string "description", limit: 255 + t.string "description" t.datetime "contacted_at" t.boolean "active", default: true, null: false t.boolean "is_shared", default: false - t.string "name", limit: 255 - t.string "version", limit: 255 - t.string "revision", limit: 255 - t.string "platform", limit: 255 - t.string "architecture", limit: 255 + t.string "name" + t.string "version" + t.string "revision" + t.string "platform" + t.string "architecture" t.boolean "run_untagged", default: true, null: false t.boolean "locked", default: false, null: false end @@ -351,18 +315,18 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree create_table "ci_sessions", force: :cascade do |t| - t.string "session_id", limit: 255, null: false - t.text "data", limit: 65535 + t.string "session_id", null: false + t.text "data" t.datetime "created_at" t.datetime "updated_at" end create_table "ci_taggings", force: :cascade do |t| - t.integer "tag_id", limit: 4 - t.integer "taggable_id", limit: 4 - t.string "taggable_type", limit: 255 - t.integer "tagger_id", limit: 4 - t.string "tagger_type", limit: 255 + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" t.string "context", limit: 128 t.datetime "created_at" end @@ -370,44 +334,44 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "ci_tags", force: :cascade do |t| - t.string "name", limit: 255 - t.integer "taggings_count", limit: 4, default: 0 + t.string "name" + t.integer "taggings_count", default: 0 end create_table "ci_trigger_requests", force: :cascade do |t| - t.integer "trigger_id", limit: 4, null: false - t.text "variables", limit: 65535 + t.integer "trigger_id", null: false + t.text "variables" t.datetime "created_at" t.datetime "updated_at" - t.integer "commit_id", limit: 4 + t.integer "commit_id" end create_table "ci_triggers", force: :cascade do |t| - t.string "token", limit: 255 - t.integer "project_id", limit: 4 + t.string "token" + t.integer "project_id" t.datetime "deleted_at" t.datetime "created_at" t.datetime "updated_at" - t.integer "gl_project_id", limit: 4 + t.integer "gl_project_id" end add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree create_table "ci_variables", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.string "key", limit: 255 - t.text "value", limit: 65535 - t.text "encrypted_value", limit: 65535 - t.string "encrypted_value_salt", limit: 255 - t.string "encrypted_value_iv", limit: 255 - t.integer "gl_project_id", limit: 4 + t.integer "project_id" + t.string "key" + t.text "value" + t.text "encrypted_value" + t.string "encrypted_value_salt" + t.string "encrypted_value_iv" + t.integer "gl_project_id" end add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree create_table "deploy_keys_projects", force: :cascade do |t| - t.integer "deploy_key_id", limit: 4, null: false - t.integer "project_id", limit: 4, null: false + t.integer "deploy_key_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -415,18 +379,18 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree create_table "deployments", force: :cascade do |t| - t.integer "iid", limit: 4, null: false - t.integer "project_id", limit: 4, null: false - t.integer "environment_id", limit: 4, null: false - t.string "ref", limit: 255, null: false + t.integer "iid", null: false + t.integer "project_id", null: false + t.integer "environment_id", null: false + t.string "ref", null: false t.boolean "tag", null: false - t.string "sha", limit: 255, null: false - t.integer "user_id", limit: 4 - t.integer "deployable_id", limit: 4 - t.string "deployable_type", limit: 255 + t.string "sha", null: false + t.integer "user_id" + t.integer "deployable_id" + t.string "deployable_type" t.datetime "created_at" t.datetime "updated_at" - t.string "on_stop", limit: 255 + t.string "on_stop" end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree @@ -435,8 +399,8 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "deployments", ["project_id"], name: "index_deployments_on_project_id", using: :btree create_table "emails", force: :cascade do |t| - t.integer "user_id", limit: 4, null: false - t.string "email", limit: 255, null: false + t.integer "user_id", null: false + t.string "email", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -445,27 +409,27 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree create_table "environments", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.string "name", limit: 255, null: false + t.integer "project_id" + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" - t.string "external_url", limit: 255 - t.string "environment_type", limit: 255 - t.string "state", limit: 255, default: "available", null: false + t.string "external_url" + t.string "environment_type" + t.string "state", default: "available", null: false end add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree create_table "events", force: :cascade do |t| - t.string "target_type", limit: 255 - t.integer "target_id", limit: 4 - t.string "title", limit: 255 - t.text "data", limit: 4294967295 - t.integer "project_id", limit: 4 + t.string "target_type" + t.integer "target_id" + t.string "title" + t.text "data" + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "action", limit: 4 - t.integer "author_id", limit: 4 + t.integer "action" + t.integer "author_id" end add_index "events", ["action"], name: "index_events_on_action", using: :btree @@ -476,59 +440,26 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree create_table "forked_project_links", force: :cascade do |t| - t.integer "forked_to_project_id", limit: 4, null: false - t.integer "forked_from_project_id", limit: 4, null: false + t.integer "forked_to_project_id", null: false + t.integer "forked_from_project_id", null: false t.datetime "created_at" t.datetime "updated_at" end add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree - create_table "geo_nodes", force: :cascade do |t| - t.string "schema", limit: 255 - t.string "host", limit: 255 - t.integer "port", limit: 4 - t.string "relative_url_root", limit: 255 - t.boolean "primary" - t.integer "geo_node_key_id", limit: 4 - t.integer "oauth_application_id", limit: 4 - t.integer "system_hook_id", limit: 4 - end - - add_index "geo_nodes", ["host"], name: "index_geo_nodes_on_host", using: :btree - add_index "geo_nodes", ["primary"], name: "index_geo_nodes_on_primary", using: :btree - - create_table "historical_data", force: :cascade do |t| - t.date "date", null: false - t.integer "active_user_count", limit: 4 - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "identities", force: :cascade do |t| - t.string "extern_uid", limit: 255 - t.string "provider", limit: 255 - t.integer "user_id", limit: 4 + t.string "extern_uid" + t.string "provider" + t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" - t.string "secondary_extern_uid", limit: 255 end add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree - create_table "index_statuses", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.datetime "indexed_at" - t.text "note", limit: 65535 - t.string "last_commit", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "index_statuses", ["project_id"], name: "index_index_statuses_on_project_id", unique: true, using: :btree - create_table "issue_metrics", force: :cascade do |t| - t.integer "issue_id", limit: 4, null: false + t.integer "issue_id", null: false t.datetime "first_mentioned_in_commit_at" t.datetime "first_associated_with_milestone_at" t.datetime "first_added_to_board_at" @@ -539,27 +470,26 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree create_table "issues", force: :cascade do |t| - t.string "title", limit: 255 - t.integer "assignee_id", limit: 4 - t.integer "author_id", limit: 4 - t.integer "project_id", limit: 4 + t.string "title" + t.integer "assignee_id" + t.integer "author_id" + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "position", limit: 4, default: 0 - t.string "branch_name", limit: 255 - t.text "description", limit: 65535 - t.integer "milestone_id", limit: 4 - t.string "state", limit: 255 - t.integer "iid", limit: 4 - t.integer "updated_by_id", limit: 4 - t.integer "weight", limit: 4 + t.integer "position", default: 0 + t.string "branch_name" + t.text "description" + t.integer "milestone_id" + t.string "state" + t.integer "iid" + t.integer "updated_by_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" - t.integer "moved_to_id", limit: 4 - t.integer "lock_version", limit: 4 - t.text "title_html", limit: 65535 - t.text "description_html", limit: 65535 + t.integer "moved_to_id" + t.integer "lock_version" + t.text "title_html" + t.text "description_html" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -567,19 +497,21 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree + add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree + add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "keys", force: :cascade do |t| - t.integer "user_id", limit: 4 + t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" - t.text "key", limit: 65535 - t.string "title", limit: 255 - t.string "type", limit: 255 - t.string "fingerprint", limit: 255 + t.text "key" + t.string "title" + t.string "type" + t.string "fingerprint" t.boolean "public", default: false, null: false end @@ -587,9 +519,9 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree create_table "label_links", force: :cascade do |t| - t.integer "label_id", limit: 4 - t.integer "target_id", limit: 4 - t.string "target_type", limit: 255 + t.integer "label_id" + t.integer "target_id" + t.string "target_type" t.datetime "created_at" t.datetime "updated_at" end @@ -598,72 +530,56 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree create_table "label_priorities", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.integer "label_id", limit: 4, null: false - t.integer "priority", limit: 4, null: false + t.integer "project_id", null: false + t.integer "label_id", null: false + t.integer "priority", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end - add_index "label_priorities", ["label_id"], name: "fk_rails_e161058b0f", using: :btree add_index "label_priorities", ["priority"], name: "index_label_priorities_on_priority", using: :btree add_index "label_priorities", ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree create_table "labels", force: :cascade do |t| - t.string "title", limit: 255 - t.string "color", limit: 255 - t.integer "project_id", limit: 4 + t.string "title" + t.string "color" + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.boolean "template", default: false - t.string "description", limit: 255 - t.text "description_html", limit: 65535 - t.string "type", limit: 255 - t.integer "group_id", limit: 4 + t.string "description" + t.text "description_html" + t.string "type" + t.integer "group_id" end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree add_index "labels", ["group_id"], name: "index_labels_on_group_id", using: :btree - create_table "ldap_group_links", force: :cascade do |t| - t.string "cn", limit: 255, null: false - t.integer "group_access", limit: 4, null: false - t.integer "group_id", limit: 4, null: false - t.datetime "created_at" - t.datetime "updated_at" - t.string "provider", limit: 255 - end - create_table "lfs_objects", force: :cascade do |t| - t.string "oid", limit: 255, null: false + t.string "oid", null: false t.integer "size", limit: 8, null: false t.datetime "created_at" t.datetime "updated_at" - t.string "file", limit: 255 + t.string "file" end add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree create_table "lfs_objects_projects", force: :cascade do |t| - t.integer "lfs_object_id", limit: 4, null: false - t.integer "project_id", limit: 4, null: false + t.integer "lfs_object_id", null: false + t.integer "project_id", null: false t.datetime "created_at" t.datetime "updated_at" end add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree - create_table "licenses", force: :cascade do |t| - t.text "data", limit: 65535, null: false - t.datetime "created_at" - t.datetime "updated_at" - end - create_table "lists", force: :cascade do |t| - t.integer "board_id", limit: 4, null: false - t.integer "label_id", limit: 4 - t.integer "list_type", limit: 4, default: 1, null: false - t.integer "position", limit: 4 + t.integer "board_id", null: false + t.integer "label_id" + t.integer "list_type", default: 1, null: false + t.integer "position" t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -673,22 +589,20 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "lists", ["label_id"], name: "index_lists_on_label_id", using: :btree create_table "members", force: :cascade do |t| - t.integer "access_level", limit: 4, null: false - t.integer "source_id", limit: 4, null: false - t.string "source_type", limit: 255, null: false - t.integer "user_id", limit: 4 - t.integer "notification_level", limit: 4, null: false - t.string "type", limit: 255 + t.integer "access_level", null: false + t.integer "source_id", null: false + t.string "source_type", null: false + t.integer "user_id" + t.integer "notification_level", null: false + t.string "type" t.datetime "created_at" t.datetime "updated_at" - t.integer "created_by_id", limit: 4 - t.string "invite_email", limit: 255 - t.string "invite_token", limit: 255 + t.integer "created_by_id" + t.string "invite_email" + t.string "invite_token" t.datetime "invite_accepted_at" t.datetime "requested_at" t.date "expires_at" - t.boolean "ldap", default: false, null: false - t.boolean "override", default: false, null: false end add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree @@ -698,29 +612,29 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree create_table "merge_request_diffs", force: :cascade do |t| - t.string "state", limit: 255 - t.text "st_commits", limit: 4294967295 - t.text "st_diffs", limit: 4294967295 - t.integer "merge_request_id", limit: 4, null: false + t.string "state" + t.text "st_commits" + t.text "st_diffs" + t.integer "merge_request_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.string "base_commit_sha", limit: 255 - t.string "real_size", limit: 255 - t.string "head_commit_sha", limit: 255 - t.string "start_commit_sha", limit: 255 + t.string "base_commit_sha" + t.string "real_size" + t.string "head_commit_sha" + t.string "start_commit_sha" end add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree create_table "merge_request_metrics", force: :cascade do |t| - t.integer "merge_request_id", limit: 4, null: false + t.integer "merge_request_id", null: false t.datetime "latest_build_started_at" t.datetime "latest_build_finished_at" t.datetime "first_deployed_to_production_at" t.datetime "merged_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "pipeline_id", limit: 4 + t.integer "pipeline_id" end add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree @@ -728,51 +642,51 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "merge_request_metrics", ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree create_table "merge_requests", force: :cascade do |t| - t.string "target_branch", limit: 255, null: false - t.string "source_branch", limit: 255, null: false - t.integer "source_project_id", limit: 4, null: false - t.integer "author_id", limit: 4 - t.integer "assignee_id", limit: 4 - t.string "title", limit: 255 + t.string "target_branch", null: false + t.string "source_branch", null: false + t.integer "source_project_id", null: false + t.integer "author_id" + t.integer "assignee_id" + t.string "title" t.datetime "created_at" t.datetime "updated_at" - t.integer "milestone_id", limit: 4 - t.string "state", limit: 255 - t.string "merge_status", limit: 255 - t.integer "target_project_id", limit: 4, null: false - t.integer "iid", limit: 4 - t.text "description", limit: 65535 - t.integer "position", limit: 4, default: 0 + t.integer "milestone_id" + t.string "state" + t.string "merge_status" + t.integer "target_project_id", null: false + t.integer "iid" + t.text "description" + t.integer "position", default: 0 t.datetime "locked_at" - t.integer "updated_by_id", limit: 4 - t.text "merge_error", limit: 65535 - t.text "merge_params", limit: 65535 + t.integer "updated_by_id" + t.text "merge_error" + t.text "merge_params" t.boolean "merge_when_build_succeeds", default: false, null: false - t.integer "merge_user_id", limit: 4 - t.string "merge_commit_sha", limit: 255 + t.integer "merge_user_id" + t.string "merge_commit_sha" t.datetime "deleted_at" - t.integer "approvals_before_merge", limit: 4 - t.string "rebase_commit_sha", limit: 255 - t.string "in_progress_merge_commit_sha", limit: 255 - t.integer "lock_version", limit: 4 - t.text "title_html", limit: 65535 - t.text "description_html", limit: 65535 + t.string "in_progress_merge_commit_sha" + t.integer "lock_version" + t.text "title_html" + t.text "description_html" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree + add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree + add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "merge_requests_closing_issues", force: :cascade do |t| - t.integer "merge_request_id", limit: 4, null: false - t.integer "issue_id", limit: 4, null: false + t.integer "merge_request_id", null: false + t.integer "issue_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -781,78 +695,73 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree create_table "milestones", force: :cascade do |t| - t.string "title", limit: 255, null: false - t.integer "project_id", limit: 4, null: false - t.text "description", limit: 65535 + t.string "title", null: false + t.integer "project_id", null: false + t.text "description" t.date "due_date" t.datetime "created_at" t.datetime "updated_at" - t.string "state", limit: 255 - t.integer "iid", limit: 4 - t.text "title_html", limit: 65535 - t.text "description_html", limit: 65535 + t.string "state" + t.integer "iid" + t.text "title_html" + t.text "description_html" end + add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree + add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} create_table "namespaces", force: :cascade do |t| - t.string "name", limit: 255, null: false - t.string "path", limit: 255, null: false - t.integer "owner_id", limit: 4 + t.string "name", null: false + t.string "path", null: false + t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" - t.string "type", limit: 255 - t.string "description", limit: 255, default: "", null: false - t.string "avatar", limit: 255 - t.boolean "membership_lock", default: false + t.string "type" + t.string "description", default: "", null: false + t.string "avatar" t.boolean "share_with_group_lock", default: false - t.integer "visibility_level", limit: 4, default: 20, null: false + t.integer "visibility_level", default: 20, null: false t.boolean "request_access_enabled", default: false, null: false t.datetime "deleted_at" - t.string "ldap_sync_status", limit: 255, default: "ready", null: false - t.string "ldap_sync_error", limit: 255 - t.datetime "ldap_sync_last_update_at" - t.datetime "ldap_sync_last_successful_update_at" - t.datetime "ldap_sync_last_sync_at" t.boolean "lfs_enabled" - t.integer "repository_size_limit", limit: 4 - t.text "description_html", limit: 65535 + t.text "description_html" end add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree - add_index "namespaces", ["ldap_sync_last_successful_update_at"], name: "index_namespaces_on_ldap_sync_last_successful_update_at", using: :btree - add_index "namespaces", ["ldap_sync_last_update_at"], name: "index_namespaces_on_ldap_sync_last_update_at", using: :btree add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree + add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: :cascade do |t| - t.text "note", limit: 65535 - t.string "noteable_type", limit: 255 - t.integer "author_id", limit: 4 + t.text "note" + t.string "noteable_type" + t.integer "author_id" t.datetime "created_at" t.datetime "updated_at" - t.integer "project_id", limit: 4 - t.string "attachment", limit: 255 - t.string "line_code", limit: 255 - t.string "commit_id", limit: 255 - t.integer "noteable_id", limit: 4 + t.integer "project_id" + t.string "attachment" + t.string "line_code" + t.string "commit_id" + t.integer "noteable_id" t.boolean "system", default: false, null: false - t.text "st_diff", limit: 4294967295 - t.integer "updated_by_id", limit: 4 - t.string "type", limit: 255 - t.text "position", limit: 65535 - t.text "original_position", limit: 65535 + t.text "st_diff" + t.integer "updated_by_id" + t.string "type" + t.text "position" + t.text "original_position" t.datetime "resolved_at" - t.integer "resolved_by_id", limit: 4 - t.string "discussion_id", limit: 255 - t.string "original_discussion_id", limit: 255 - t.text "note_html", limit: 65535 + t.integer "resolved_by_id" + t.string "discussion_id" + t.string "original_discussion_id" + t.text "note_html" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -860,6 +769,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree + add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"} add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree @@ -867,13 +777,13 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree create_table "notification_settings", force: :cascade do |t| - t.integer "user_id", limit: 4, null: false - t.integer "source_id", limit: 4 - t.string "source_type", limit: 255 - t.integer "level", limit: 4, default: 0, null: false + t.integer "user_id", null: false + t.integer "source_id" + t.string "source_type" + t.integer "level", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "events", limit: 65535 + t.text "events" end add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree @@ -881,27 +791,27 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "notification_settings", ["user_id"], name: "index_notification_settings_on_user_id", using: :btree create_table "oauth_access_grants", force: :cascade do |t| - t.integer "resource_owner_id", limit: 4, null: false - t.integer "application_id", limit: 4, null: false - t.string "token", limit: 255, null: false - t.integer "expires_in", limit: 4, null: false - t.text "redirect_uri", limit: 65535, null: false + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false t.datetime "created_at", null: false t.datetime "revoked_at" - t.string "scopes", limit: 255 + t.string "scopes" end add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree create_table "oauth_access_tokens", force: :cascade do |t| - t.integer "resource_owner_id", limit: 4 - t.integer "application_id", limit: 4 - t.string "token", limit: 255, null: false - t.string "refresh_token", limit: 255 - t.integer "expires_in", limit: 4 + t.integer "resource_owner_id" + t.integer "application_id" + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" t.datetime "revoked_at" t.datetime "created_at", null: false - t.string "scopes", limit: 255 + t.string "scopes" end add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree @@ -909,47 +819,24 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree create_table "oauth_applications", force: :cascade do |t| - t.string "name", limit: 255, null: false - t.string "uid", limit: 255, null: false - t.string "secret", limit: 255, null: false - t.text "redirect_uri", limit: 65535, null: false - t.string "scopes", limit: 255, default: "", null: false + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "owner_id", limit: 4 - t.string "owner_type", limit: 255 + t.integer "owner_id" + t.string "owner_type" end add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree - create_table "pages_domains", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.text "certificate", limit: 65535 - t.text "encrypted_key", limit: 65535 - t.string "encrypted_key_iv", limit: 255 - t.string "encrypted_key_salt", limit: 255 - t.string "domain", limit: 255 - end - - add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree - - create_table "path_locks", force: :cascade do |t| - t.string "path", limit: 255, null: false - t.integer "project_id", limit: 4 - t.integer "user_id", limit: 4 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "path_locks", ["path"], name: "index_path_locks_on_path", using: :btree - add_index "path_locks", ["project_id"], name: "index_path_locks_on_project_id", using: :btree - add_index "path_locks", ["user_id"], name: "index_path_locks_on_user_id", using: :btree - create_table "personal_access_tokens", force: :cascade do |t| - t.integer "user_id", limit: 4, null: false - t.string "token", limit: 255, null: false - t.string "name", limit: 255, null: false + t.integer "user_id", null: false + t.string "token", null: false + t.string "name", null: false t.boolean "revoked", default: false t.datetime "expires_at" t.datetime "created_at", null: false @@ -960,75 +847,64 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree create_table "project_features", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.integer "merge_requests_access_level", limit: 4 - t.integer "issues_access_level", limit: 4 - t.integer "wiki_access_level", limit: 4 - t.integer "snippets_access_level", limit: 4 - t.integer "builds_access_level", limit: 4 + t.integer "project_id" + t.integer "merge_requests_access_level" + t.integer "issues_access_level" + t.integer "wiki_access_level" + t.integer "snippets_access_level" + t.integer "builds_access_level" t.datetime "created_at" t.datetime "updated_at" - t.integer "repository_access_level", limit: 4, default: 20, null: false + t.integer "repository_access_level", default: 20, null: false end add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree create_table "project_group_links", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.integer "group_id", limit: 4, null: false + t.integer "project_id", null: false + t.integer "group_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "group_access", limit: 4, default: 30, null: false + t.integer "group_access", default: 30, null: false t.date "expires_at" end create_table "project_import_data", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.text "data", limit: 65535 - t.text "encrypted_credentials", limit: 65535 - t.string "encrypted_credentials_iv", limit: 255 - t.string "encrypted_credentials_salt", limit: 255 + t.integer "project_id" + t.text "data" + t.text "encrypted_credentials" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" end add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree create_table "projects", force: :cascade do |t| - t.string "name", limit: 255 - t.string "path", limit: 255 - t.text "description", limit: 65535 + t.string "name" + t.string "path" + t.text "description" t.datetime "created_at" t.datetime "updated_at" - t.integer "creator_id", limit: 4 - t.integer "namespace_id", limit: 4 + t.integer "creator_id" + t.integer "namespace_id" t.datetime "last_activity_at" - t.string "import_url", limit: 255 - t.integer "visibility_level", limit: 4, default: 0, null: false + t.string "import_url" + t.integer "visibility_level", default: 0, null: false t.boolean "archived", default: false, null: false - t.string "avatar", limit: 255 - t.string "import_status", limit: 255 - t.float "repository_size", limit: 24, default: 0.0 - t.text "merge_requests_template", limit: 65535 - t.integer "star_count", limit: 4, default: 0, null: false - t.boolean "merge_requests_rebase_enabled", default: false - t.string "import_type", limit: 255 - t.string "import_source", limit: 255 - t.integer "approvals_before_merge", limit: 4, default: 0, null: false - t.boolean "reset_approvals_on_push", default: true - t.integer "commit_count", limit: 4, default: 0 - t.boolean "merge_requests_ff_only_enabled", default: false - t.text "issues_template", limit: 65535 - t.boolean "mirror", default: false, null: false - t.datetime "mirror_last_update_at" - t.datetime "mirror_last_successful_update_at" - t.integer "mirror_user_id", limit: 4 - t.text "import_error", limit: 65535 - t.integer "ci_id", limit: 4 + t.string "avatar" + t.string "import_status" + t.float "repository_size", default: 0.0 + t.integer "star_count", default: 0, null: false + t.string "import_type" + t.string "import_source" + t.integer "commit_count", default: 0 + t.text "import_error" + t.integer "ci_id" t.boolean "shared_runners_enabled", default: true, null: false - t.string "runners_token", limit: 255 - t.string "build_coverage_regex", limit: 255 + t.string "runners_token" + t.string "build_coverage_regex" t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", limit: 4, default: 3600, null: false - t.boolean "mirror_trigger_builds", default: false, null: false + t.integer "build_timeout", default: 3600, null: false t.boolean "pending_delete", default: false t.boolean "public_builds", default: true, null: false t.boolean "last_repository_check_failed" @@ -1036,132 +912,90 @@ ActiveRecord::Schema.define(version: 20161109150329) do t.boolean "container_registry_enabled" t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false t.boolean "has_external_issue_tracker" - t.string "repository_storage", limit: 255, default: "default", null: false + t.string "repository_storage", default: "default", null: false t.boolean "request_access_enabled", default: false, null: false t.boolean "has_external_wiki" - t.boolean "repository_read_only" t.boolean "lfs_enabled" - t.integer "repository_size_limit", limit: 4 - t.text "description_html", limit: 65535 + t.text "description_html" t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree + add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree add_index "projects", ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed", using: :btree + add_index "projects", ["name"], name: "index_projects_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree add_index "projects", ["path"], name: "index_projects_on_path", using: :btree + add_index "projects", ["path"], name: "index_projects_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "projects", ["pending_delete"], name: "index_projects_on_pending_delete", using: :btree add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree create_table "protected_branch_merge_access_levels", force: :cascade do |t| - t.integer "protected_branch_id", limit: 4, null: false - t.integer "access_level", limit: 4, default: 40 + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "user_id", limit: 4 - t.integer "group_id", limit: 4 end - add_index "protected_branch_merge_access_levels", ["group_id"], name: "fk_rails_98f3d044fe", using: :btree add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree - add_index "protected_branch_merge_access_levels", ["user_id"], name: "index_protected_branch_merge_access_levels_on_user_id", using: :btree create_table "protected_branch_push_access_levels", force: :cascade do |t| - t.integer "protected_branch_id", limit: 4, null: false - t.integer "access_level", limit: 4, default: 40 + t.integer "protected_branch_id", null: false + t.integer "access_level", default: 40, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.integer "user_id", limit: 4 - t.integer "group_id", limit: 4 end - add_index "protected_branch_push_access_levels", ["group_id"], name: "fk_rails_7111b68cdb", using: :btree add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree - add_index "protected_branch_push_access_levels", ["user_id"], name: "index_protected_branch_push_access_levels_on_user_id", using: :btree create_table "protected_branches", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.string "name", limit: 255, null: false + t.integer "project_id", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" end add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree - create_table "push_rules", force: :cascade do |t| - t.string "force_push_regex", limit: 255 - t.string "delete_branch_regex", limit: 255 - t.string "commit_message_regex", limit: 255 - t.boolean "deny_delete_tag" - t.integer "project_id", limit: 4 - t.datetime "created_at" - t.datetime "updated_at" - t.string "author_email_regex", limit: 255 - t.boolean "member_check", default: false, null: false - t.string "file_name_regex", limit: 255 - t.boolean "is_sample", default: false - t.integer "max_file_size", limit: 4, default: 0, null: false - t.boolean "prevent_secrets", default: false, null: false - end - - add_index "push_rules", ["project_id"], name: "index_push_rules_on_project_id", using: :btree - create_table "releases", force: :cascade do |t| - t.string "tag", limit: 255 - t.text "description", limit: 65535 - t.integer "project_id", limit: 4 + t.string "tag" + t.text "description" + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.text "description_html", limit: 65535 + t.text "description_html" end add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree - create_table "remote_mirrors", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.string "url", limit: 255 - t.boolean "enabled", default: false - t.string "update_status", limit: 255 - t.datetime "last_update_at" - t.datetime "last_successful_update_at" - t.string "last_error", limit: 255 - t.text "encrypted_credentials", limit: 65535 - t.string "encrypted_credentials_iv", limit: 255 - t.string "encrypted_credentials_salt", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "remote_mirrors", ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree - create_table "sent_notifications", force: :cascade do |t| - t.integer "project_id", limit: 4 - t.integer "noteable_id", limit: 4 - t.string "noteable_type", limit: 255 - t.integer "recipient_id", limit: 4 - t.string "commit_id", limit: 255 - t.string "reply_key", limit: 255, null: false - t.string "line_code", limit: 255 - t.string "note_type", limit: 255 - t.text "position", limit: 65535 + t.integer "project_id" + t.integer "noteable_id" + t.string "noteable_type" + t.integer "recipient_id" + t.string "commit_id" + t.string "reply_key", null: false + t.string "line_code" + t.string "note_type" + t.text "position" end add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree create_table "services", force: :cascade do |t| - t.string "type", limit: 255 - t.string "title", limit: 255 - t.integer "project_id", limit: 4 + t.string "type" + t.string "title" + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" t.boolean "active", default: false, null: false - t.text "properties", limit: 65535 + t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true t.boolean "issues_events", default: true @@ -1169,7 +1003,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do t.boolean "tag_push_events", default: true t.boolean "note_events", default: true, null: false t.boolean "build_events", default: false, null: false - t.string "category", limit: 255, default: "common", null: false + t.string "category", default: "common", null: false t.boolean "default", default: false t.boolean "wiki_page_events", default: true t.boolean "pipeline_events", default: false, null: false @@ -1180,41 +1014,43 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "services", ["template"], name: "index_services_on_template", using: :btree create_table "snippets", force: :cascade do |t| - t.string "title", limit: 255 - t.text "content", limit: 4294967295 - t.integer "author_id", limit: 4, null: false - t.integer "project_id", limit: 4 + t.string "title" + t.text "content" + t.integer "author_id", null: false + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "file_name", limit: 255 - t.string "type", limit: 255 - t.integer "visibility_level", limit: 4, default: 0, null: false - t.text "title_html", limit: 65535 - t.text "content_html", limit: 65535 + t.string "file_name" + t.string "type" + t.integer "visibility_level", default: 0, null: false + t.text "title_html" + t.text "content_html" end add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree + add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"} add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree + add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "snippets", ["updated_at"], name: "index_snippets_on_updated_at", using: :btree add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree create_table "spam_logs", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.string "source_ip", limit: 255 - t.string "user_agent", limit: 255 + t.integer "user_id" + t.string "source_ip" + t.string "user_agent" t.boolean "via_api" - t.string "noteable_type", limit: 255 - t.string "title", limit: 255 - t.text "description", limit: 65535 + t.string "noteable_type" + t.string "title" + t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "submitted_as_ham", default: false, null: false end create_table "subscriptions", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.integer "subscribable_id", limit: 4 - t.string "subscribable_type", limit: 255 + t.integer "user_id" + t.integer "subscribable_id" + t.string "subscribable_type" t.boolean "subscribed" t.datetime "created_at" t.datetime "updated_at" @@ -1223,12 +1059,12 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree create_table "taggings", force: :cascade do |t| - t.integer "tag_id", limit: 4 - t.integer "taggable_id", limit: 4 - t.string "taggable_type", limit: 255 - t.integer "tagger_id", limit: 4 - t.string "tagger_type", limit: 255 - t.string "context", limit: 255 + t.integer "tag_id" + t.integer "taggable_id" + t.string "taggable_type" + t.integer "tagger_id" + t.string "tagger_type" + t.string "context" t.datetime "created_at" end @@ -1236,24 +1072,24 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree create_table "tags", force: :cascade do |t| - t.string "name", limit: 255 - t.integer "taggings_count", limit: 4, default: 0 + t.string "name" + t.integer "taggings_count", default: 0 end add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree create_table "todos", force: :cascade do |t| - t.integer "user_id", limit: 4, null: false - t.integer "project_id", limit: 4, null: false - t.integer "target_id", limit: 4 - t.string "target_type", limit: 255, null: false - t.integer "author_id", limit: 4 - t.integer "action", limit: 4, null: false - t.string "state", limit: 255, null: false + t.integer "user_id", null: false + t.integer "project_id", null: false + t.integer "target_id" + t.string "target_type", null: false + t.integer "author_id" + t.integer "action", null: false + t.string "state", null: false t.datetime "created_at" t.datetime "updated_at" - t.integer "note_id", limit: 4 - t.string "commit_id", limit: 255 + t.integer "note_id" + t.string "commit_id" end add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree @@ -1264,104 +1100,95 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree create_table "trending_projects", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false + t.integer "project_id", null: false end add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree create_table "u2f_registrations", force: :cascade do |t| - t.text "certificate", limit: 65535 - t.string "key_handle", limit: 255 - t.string "public_key", limit: 255 - t.integer "counter", limit: 4 - t.integer "user_id", limit: 4 + t.text "certificate" + t.string "key_handle" + t.string "public_key" + t.integer "counter" + t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "name", limit: 255 + t.string "name" end add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree - create_table "user_activities", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.datetime "last_activity_at", null: false - end - - add_index "user_activities", ["user_id"], name: "index_user_activities_on_user_id", unique: true, using: :btree - create_table "user_agent_details", force: :cascade do |t| - t.string "user_agent", limit: 255, null: false - t.string "ip_address", limit: 255, null: false - t.integer "subject_id", limit: 4, null: false - t.string "subject_type", limit: 255, null: false + t.string "user_agent", null: false + t.string "ip_address", null: false + t.integer "subject_id", null: false + t.string "subject_type", null: false t.boolean "submitted", default: false, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "users", force: :cascade do |t| - t.string "email", limit: 255, default: "", null: false - t.string "encrypted_password", limit: 255, default: "", null: false - t.string "reset_password_token", limit: 255 + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" t.datetime "reset_password_sent_at" t.datetime "remember_created_at" - t.integer "sign_in_count", limit: 4, default: 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.datetime "last_sign_in_at" - t.string "current_sign_in_ip", limit: 255 - t.string "last_sign_in_ip", limit: 255 + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" t.datetime "created_at" t.datetime "updated_at" - t.string "name", limit: 255 + t.string "name" t.boolean "admin", default: false, null: false - t.integer "projects_limit", limit: 4, default: 10 - t.string "skype", limit: 255, default: "", null: false - t.string "linkedin", limit: 255, default: "", null: false - t.string "twitter", limit: 255, default: "", null: false - t.string "authentication_token", limit: 255 - t.integer "theme_id", limit: 4, default: 1, null: false - t.string "bio", limit: 255 - t.integer "failed_attempts", limit: 4, default: 0 + t.integer "projects_limit", default: 10 + t.string "skype", default: "", null: false + t.string "linkedin", default: "", null: false + t.string "twitter", default: "", null: false + t.string "authentication_token" + t.integer "theme_id", default: 1, null: false + t.string "bio" + t.integer "failed_attempts", default: 0 t.datetime "locked_at" - t.string "username", limit: 255 + t.string "username" t.boolean "can_create_group", default: true, null: false t.boolean "can_create_team", default: true, null: false - t.string "state", limit: 255 - t.integer "color_scheme_id", limit: 4, default: 1, null: false + t.string "state" + t.integer "color_scheme_id", default: 1, null: false t.datetime "password_expires_at" - t.integer "created_by_id", limit: 4 + t.integer "created_by_id" t.datetime "last_credential_check_at" - t.string "avatar", limit: 255 - t.string "confirmation_token", limit: 255 + t.string "avatar" + t.string "confirmation_token" t.datetime "confirmed_at" t.datetime "confirmation_sent_at" - t.string "unconfirmed_email", limit: 255 + t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false - t.string "website_url", limit: 255, default: "", null: false - t.datetime "admin_email_unsubscribed_at" - t.string "notification_email", limit: 255 + t.string "website_url", default: "", null: false + t.string "notification_email" t.boolean "hide_no_password", default: false t.boolean "password_automatically_set", default: false - t.string "location", limit: 255 - t.string "encrypted_otp_secret", limit: 255 - t.string "encrypted_otp_secret_iv", limit: 255 - t.string "encrypted_otp_secret_salt", limit: 255 + t.string "location" + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" t.boolean "otp_required_for_login", default: false, null: false - t.text "otp_backup_codes", limit: 65535 - t.string "public_email", limit: 255, default: "", null: false - t.integer "dashboard", limit: 4, default: 0 - t.integer "project_view", limit: 4, default: 0 - t.integer "consumed_timestep", limit: 4 - t.integer "layout", limit: 4, default: 0 + t.text "otp_backup_codes" + t.string "public_email", default: "", null: false + t.integer "dashboard", default: 0 + t.integer "project_view", default: 0 + t.integer "consumed_timestep" + t.integer "layout", default: 0 t.boolean "hide_project_limit", default: false - t.text "note", limit: 65535 - t.string "unlock_token", limit: 255 + t.string "unlock_token" t.datetime "otp_grace_period_started_at" t.boolean "ldap_email", default: false, null: false t.boolean "external", default: false - t.string "organization", limit: 255 - t.string "incoming_email_token", limit: 255 + t.string "organization" + t.string "incoming_email_token" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -1370,15 +1197,18 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"} add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree add_index "users", ["name"], name: "index_users_on_name", using: :btree + add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree add_index "users", ["state"], name: "index_users_on_state", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree + add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} create_table "users_star_projects", force: :cascade do |t| - t.integer "project_id", limit: 4, null: false - t.integer "user_id", limit: 4, null: false + t.integer "project_id", null: false + t.integer "user_id", null: false t.datetime "created_at" t.datetime "updated_at" end @@ -1389,28 +1219,26 @@ ActiveRecord::Schema.define(version: 20161109150329) do create_table "web_hooks", force: :cascade do |t| t.string "url", limit: 2000 - t.integer "project_id", limit: 4 + t.integer "project_id" t.datetime "created_at" t.datetime "updated_at" - t.string "type", limit: 255, default: "ProjectHook" - t.integer "service_id", limit: 4 + t.string "type", default: "ProjectHook" + t.integer "service_id" t.boolean "push_events", default: true, null: false t.boolean "issues_events", default: false, null: false t.boolean "merge_requests_events", default: false, null: false t.boolean "tag_push_events", default: false - t.integer "group_id", limit: 4 t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true t.boolean "build_events", default: false, null: false - t.string "token", limit: 255 t.boolean "wiki_page_events", default: false, null: false + t.string "token" t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree - add_foreign_key "approver_groups", "namespaces", column: "group_id", on_delete: :cascade add_foreign_key "boards", "projects" add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade @@ -1422,17 +1250,9 @@ ActiveRecord::Schema.define(version: 20161109150329) do add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade - add_foreign_key "path_locks", "projects" - add_foreign_key "path_locks", "users" add_foreign_key "personal_access_tokens", "users" - add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_branch_merge_access_levels", "protected_branches" - add_foreign_key "protected_branch_merge_access_levels", "users" - add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_branch_push_access_levels", "protected_branches" - add_foreign_key "protected_branch_push_access_levels", "users" - add_foreign_key "remote_mirrors", "projects" add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" - add_foreign_key "user_activities", "users", on_delete: :cascade end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index c415b93c80f..1e76fbec855 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -1,26 +1,36 @@ module Gitlab module CycleAnalytics class BaseEvent - extend MetricsTables + include MetricsTables - class << self - attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections + attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query - def order - @order || @start_time_attrs + def initialize(project:, options:) + @query = EventsQuery.new(project: project, options: options) + @project = project + @options = options + end + + def fetch + @query.execute(self).map do |event| + serialize(event) if has_permission?(event['id']) end + end - def query(_base_query); end + def custom_query(_base_query); end - def fetch(query) - query.execute(self).map { |event| serialize(event, query) } - end + def order + @order || @start_time_attrs + end - private + private - def serialize(_event, _query) - raise NotImplementedError.new("Expected #{self.name} to implement serialize(event, query)") - end + def serialize(_event) + raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)") + end + + def has_permission?(_id) + true end end end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb index 609b45579b4..29f62cb22aa 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/code_event.rb @@ -1,24 +1,31 @@ module Gitlab module CycleAnalytics class CodeEvent < BaseEvent - @stage = :code - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] + def initialize(*args) + @stage = :code + @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] + @end_time_attrs = mr_table[:created_at] + @projections = [mr_table[:title], + mr_table[:iid], + mr_table[:id], + mr_table[:created_at], + mr_table[:state], + mr_table[:author_id]] + @order = mr_table[:created_at] - @end_time_attrs = mr_table[:created_at] + super(*args) + end - @projections = [mr_table[:title], - mr_table[:iid], - mr_table[:id], - mr_table[:created_at], - mr_table[:state], - mr_table[:author_id]] + private - @order = mr_table[:created_at] - - def self.serialize(event, query) + def serialize(event) event['author'] = User.find(event.delete('author_id')) - AnalyticsMergeRequestSerializer.new(project: query.project).represent(event).as_json + AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + end + + def has_permission?(id) + @options[:current_user].can?(:read_merge_request, MergeRequest.find(id)) end end end diff --git a/lib/gitlab/cycle_analytics/event_config.rb b/lib/gitlab/cycle_analytics/event_config.rb deleted file mode 100644 index 2e1400c41b9..00000000000 --- a/lib/gitlab/cycle_analytics/event_config.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Gitlab - module CycleAnalytics - class TestEvent < BaseEvent - @start_time_attrs = mr_table[:created_at] - - @end_time_attrs = mr_metrics_table[:merged_at] - - @projections = [mr_table[:title], - mr_table[:iid], - mr_table[:id], - mr_table[:created_at], - mr_table[:state], - mr_table[:author_id]] - end - end -end diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb index 6580c73128c..2d703d76cbb 100644 --- a/lib/gitlab/cycle_analytics/events.rb +++ b/lib/gitlab/cycle_analytics/events.rb @@ -3,35 +3,35 @@ module Gitlab class Events def initialize(project:, options:) @project = project - @query = EventsQuery.new(project: project, options: options) + @options = options end def issue_events - IssueEvent.fetch(@query) + IssueEvent.new(project: @project, options: @options).fetch end def plan_events - PlanEvent.fetch(@query) + PlanEvent.new(project: @project, options: @options).fetch end def code_events - CodeEvent.fetch(@query) + CodeEvent.new(project: @project, options: @options).fetch end def test_events - TestEvent.fetch(@query) + TestEvent.new(project: @project, options: @options).fetch end def review_events - ReviewEvent.fetch(@query) + ReviewEvent.new(project: @project, options: @options).fetch end def staging_events - StagingEvent.fetch(@query) + StagingEvent.new(project: @project, options: @options).fetch end def production_events - ProductionEvent.fetch(@query) + ProductionEvent.new(project: @project, options: @options).fetch end end end diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb index 7ecdc07d17f..2418832ccc2 100644 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ b/lib/gitlab/cycle_analytics/events_query.rb @@ -22,7 +22,7 @@ module Gitlab base_query = @fetcher.base_query_for(@stage_class.stage) diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs) - @stage_class.query(base_query) + @stage_class.custom_query(base_query) base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc) end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb index 9cfcdc9b20e..70c015df419 100644 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event.rb @@ -1,22 +1,30 @@ module Gitlab module CycleAnalytics class IssueEvent < BaseEvent - @stage = :issue - @start_time_attrs = issue_table[:created_at] + def initialize(*args) + @stage = :issue + @start_time_attrs = issue_table[:created_at] + @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] + super(*args) + end - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] + private - def self.serialize(event, query) + def serialize(event) event['author'] = User.find(event.delete('author_id')) - AnalyticsIssueSerializer.new(project: query.project).represent(event).as_json + AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + end + + def has_permission?(id) + @options[:current_user].can?(:read_issue, Issue.find(id)) end end end diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb index 4980a7835eb..b1ae215f348 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event.rb @@ -1,43 +1,43 @@ module Gitlab module CycleAnalytics class PlanEvent < BaseEvent - @stage = :plan - @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] + def initialize(*args) + @stage = :plan + @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] + @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], + issue_metrics_table[:first_mentioned_in_commit_at]] + @projections = [mr_diff_table[:st_commits].as('commits'), + issue_metrics_table[:first_mentioned_in_commit_at]] - @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], - issue_metrics_table[:first_mentioned_in_commit_at]] + super(*args) + end - @projections = [mr_diff_table[:st_commits].as('commits'), - issue_metrics_table[:first_mentioned_in_commit_at]] + def custom_query(base_query) + base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + end - class << self - def query(base_query) - base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + private + + def serialize(event) + st_commit = first_time_reference_commit(event.delete('commits'), event) + + return unless st_commit + + serialize_commit(event, st_commit, query) + end + + def first_time_reference_commit(commits, event) + YAML.load(commits).find do |commit| + next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] + + commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i end + end - private + def serialize_commit(event, st_commit, query) + commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project) - def serialize(event, query) - st_commit = first_time_reference_commit(event.delete('commits'), event) - - return unless st_commit - - serialize_commit(event, st_commit, query) - end - - def first_time_reference_commit(commits, event) - YAML.load(commits).find do |commit| - next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] - - commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i - end - end - - def serialize_commit(event, st_commit, query) - commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project) - - AnalyticsCommitSerializer.new(project: query.project, total_time: event['total_time']).represent(commit).as_json - end + AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json end end end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb index b0b2d94f7e7..80c0d08c039 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/production_event.rb @@ -1,21 +1,29 @@ module Gitlab module CycleAnalytics class ProductionEvent < BaseEvent - @stage = :production - @start_time_attrs = issue_table[:created_at] + def initialize(*args) + @stage = :production + @start_time_attrs = issue_table[:created_at] + @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + super(*args) + end - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] + private - def self.serialize(event, query) + def serialize(event) event['author'] = User.find(event.delete('author_id')) - AnalyticsIssueSerializer.new(project: query.project).represent(event).as_json + AnalyticsIssueSerializer.new(project: @project).represent(event).as_json + end + + def has_permission?(id) + @options[:current_user].can?(:read_issue, Issue.find(id)) end end end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb index 82095b23b71..cc89ef68be0 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/review_event.rb @@ -1,20 +1,28 @@ module Gitlab module CycleAnalytics class ReviewEvent < BaseEvent - @stage = :review - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] - @projections = [mr_table[:title], - mr_table[:iid], - mr_table[:id], - mr_table[:created_at], - mr_table[:state], - mr_table[:author_id]] + def initialize(*args) + @stage = :review + @start_time_attrs = mr_table[:created_at] + @end_time_attrs = mr_metrics_table[:merged_at] + @projections = [mr_table[:title], + mr_table[:iid], + mr_table[:id], + mr_table[:created_at], + mr_table[:state], + mr_table[:author_id]] - def self.serialize(event, query) + super(*args) + end + + def serialize(event) event['author'] = User.find(event.delete('author_id')) - AnalyticsMergeRequestSerializer.new(project: query.project).represent(event).as_json + AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json + end + + def has_permission?(id) + @options[:current_user].can?(:read_merge_request, MergeRequest.find(id)) end end end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb index a8872b50cfd..800b2b786dd 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event.rb @@ -1,17 +1,23 @@ module Gitlab module CycleAnalytics class StagingEvent < BaseEvent - @stage = :staging - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] - @projections = [build_table[:id]] - @order = build_table[:created_at] + def initialize(*args) + @stage = :staging + @start_time_attrs = mr_metrics_table[:merged_at] + @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] + @projections = [build_table[:id]] + @order = build_table[:created_at] - def self.query(base_query) + super(*args) + end + + def custom_query(base_query) base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) end - def self.serialize(event, _query) + private + + def serialize(event) build = ::Ci::Build.find(event['id']) AnalyticsBuildSerializer.new.represent(build).as_json diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb index c91d3f47da0..00404d7f6b0 100644 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ b/lib/gitlab/cycle_analytics/test_event.rb @@ -1,17 +1,23 @@ module Gitlab module CycleAnalytics class TestEvent < BaseEvent - @stage = :test - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - @projections = [build_table[:id]] - @order = build_table[:created_at] + def initialize(*args) + @stage = :test + @start_time_attrs = mr_metrics_table[:latest_build_started_at] + @end_time_attrs = mr_metrics_table[:latest_build_finished_at] + @projections = [build_table[:id]] + @order = build_table[:created_at] - def self.query(base_query) + super(*args) + end + + def custom_query(base_query) base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) end - def self.serialize(event, _query) + private + + def serialize(event) build = ::Ci::Build.find(event['id']) AnalyticsBuildSerializer.new.represent(build).as_json diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 8e2d2f8b5bd..9aeaa6b3ee8 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::CycleAnalytics::Events do let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - subject { described_class.new(project: project, options: { from: from_date }) } + subject { described_class.new(project: project, options: { from: from_date, current_user: user }) } before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) From de96f295a2c08ee2888de91b910d714a15204fb2 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 17 Nov 2016 18:10:28 +0100 Subject: [PATCH 193/302] fix schema.rb --- db/schema.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 22318612796..983e1c90682 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161109150329) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" end create_table "audit_events", force: :cascade do |t| @@ -917,7 +917,7 @@ ActiveRecord::Schema.define(version: 20161109150329) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false + t.boolean "only_allow_merge_if_all_discussions_are_resolved" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree From 551d86c09817baf783f5ac4d52bb0d8991a26f63 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 17 Nov 2016 17:12:13 +0000 Subject: [PATCH 194/302] Fix broken button in empty state --- .../environments/components/environment.js.es6 | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 89ac1cb42c0..a94fe41dede 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -193,19 +193,18 @@

    Environments are places where code gets deployed, such as staging or production. -
    - Read more about environments - - New Environment -

    + + + New Environment +
    Date: Thu, 17 Nov 2016 17:29:53 +0000 Subject: [PATCH 195/302] Adds fixed layout to prevent the columns from jumping when opening a folder --- .../stores/environments_store.js.es6 | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 928786f0741..0ef0d99e38d 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -47,6 +47,242 @@ * @returns {Array} Tree structured array with the received environments. */ storeEnvironments(environments = []) { + environments = [{ + "id": 31, + "name": "production", + "state": "stopped", + "external_url": null, + "environment_type": null, + "last_deployment": { + "id": 66, + "iid": 6, + "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", + "ref": { + "name": "master", + "ref_path": "/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": 1279, + "name": "deploy", + "build_path": "/root/ci-folders/builds/1279", + "retry_path": "/root/ci-folders/builds/1279/retry" + }, + "manual_actions": [] + }, + "stoppable?": false, + "environment_path": "/root/ci-folders/environments/31", + "created_at": "2016-11-07T11:11:16.525Z", + "updated_at": "2016-11-10T15:55:58.778Z" +}, { + "id": 33, + "name": "folder/foo", + "state": "available", + "external_url": "http://bar.filipa.com", + "environment_type": "folder", + "last_deployment": { + "id": 66, + "iid": 6, + "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", + "ref": { + "name": "master", + "ref_path": "/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": "Real big commi message adasdasdas asdasd sd sdsdfdsf 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": 1279, + "name": "deploy", + "build_path": "/root/ci-folders/builds/1279", + "retry_path": "/root/ci-folders/builds/1279/retry" + }, + "manual_actions": [] + }, + "stoppable?": false, + "environment_path": "/root/ci-folders/environments/33", + "created_at": "2016-11-07T11:17:20.604Z", + "updated_at": "2016-11-07T11:17:20.604Z" +}, { + "id": 34, + "name": "folder/bar", + "last_deployment": { + "id": 66, + "iid": 6, + "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", + "ref": { + "name": "master", + "ref_path": "/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": 1279, + "name": "deploy", + "build_path": "/root/ci-folders/builds/1279", + "retry_path": "/root/ci-folders/builds/1279/retry" + }, + "manual_actions": [] + }, + "state": "available", + "external_url": null, + "environment_type": "folder", + + "stoppable?": false, + "environment_path": "/root/ci-folders/environments/34", + "created_at": "2016-11-07T11:31:59.481Z", + "updated_at": "2016-11-07T11:31:59.481Z" +}, { + "id": 35, + "name": "review", + "state": "available", + "external_url": null, + "environment_type": null, + "last_deployment": { + "id": 67, + "iid": 7, + "sha": "9dcecbafd2514b555ad2ef9be1f40c6b46009613", + "ref": { + "name": "master", + "ref_path": "/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": "9dcecbafd2514b555ad2ef9be1f40c6b46009613", + "short_id": "9dcecbaf", + "title": "aa Update .gitlab-ci.yml", + "author_name": "Administrator", + "author_email": "admin@example.com", + "created_at": "2016-11-09T15:53:06.000+00:00", + "message": "aa 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/9dcecbafd2514b555ad2ef9be1f40c6b46009613" + }, + "deployable": { + "id": 1283, + "name": "test", + "build_path": "/root/ci-folders/builds/1283", + "retry_path": "/root/ci-folders/builds/1283/retry" + }, + "manual_actions": [{ + "id": 1292, + "name": "stop_review_app", + "build_path": "/root/ci-folders/builds/1292", + "retry_path": "/root/ci-folders/builds/1292/retry", + "play_path": "/root/ci-folders/builds/1292/play" + }] + }, + "stoppable?": true, + "environment_path": "/root/ci-folders/environments/35", + "created_at": "2016-11-09T15:53:45.108Z", + "updated_at": "2016-11-09T15:53:45.108Z" +}, { + "id": 36, + "name": "review", + "state": "stopped", + "external_url": null, + "environment_type": null, + "last_deployment": null, + "stoppable?": false, + "environment_path": "/root/ci-folders/environments/36", + "created_at": "2016-11-09T15:53:45.220Z", + "updated_at": "2016-11-09T15:53:45.273Z" +}]; + this.state.stoppedCounter = this.countByState(environments, 'stopped'); this.state.availableCounter = this.countByState(environments, 'available'); From c5a16f45d599bb9b97136b01558fcffc449b1a2a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 17 Nov 2016 17:35:27 +0000 Subject: [PATCH 196/302] Undo last commit --- .../stores/environments_store.js.es6 | 236 ------------------ 1 file changed, 236 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 0ef0d99e38d..928786f0741 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -47,242 +47,6 @@ * @returns {Array} Tree structured array with the received environments. */ storeEnvironments(environments = []) { - environments = [{ - "id": 31, - "name": "production", - "state": "stopped", - "external_url": null, - "environment_type": null, - "last_deployment": { - "id": 66, - "iid": 6, - "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", - "ref": { - "name": "master", - "ref_path": "/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": 1279, - "name": "deploy", - "build_path": "/root/ci-folders/builds/1279", - "retry_path": "/root/ci-folders/builds/1279/retry" - }, - "manual_actions": [] - }, - "stoppable?": false, - "environment_path": "/root/ci-folders/environments/31", - "created_at": "2016-11-07T11:11:16.525Z", - "updated_at": "2016-11-10T15:55:58.778Z" -}, { - "id": 33, - "name": "folder/foo", - "state": "available", - "external_url": "http://bar.filipa.com", - "environment_type": "folder", - "last_deployment": { - "id": 66, - "iid": 6, - "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", - "ref": { - "name": "master", - "ref_path": "/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": "Real big commi message adasdasdas asdasd sd sdsdfdsf 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": 1279, - "name": "deploy", - "build_path": "/root/ci-folders/builds/1279", - "retry_path": "/root/ci-folders/builds/1279/retry" - }, - "manual_actions": [] - }, - "stoppable?": false, - "environment_path": "/root/ci-folders/environments/33", - "created_at": "2016-11-07T11:17:20.604Z", - "updated_at": "2016-11-07T11:17:20.604Z" -}, { - "id": 34, - "name": "folder/bar", - "last_deployment": { - "id": 66, - "iid": 6, - "sha": "500aabcb17c97bdcf2d0c410b70cb8556f0362dd", - "ref": { - "name": "master", - "ref_path": "/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": 1279, - "name": "deploy", - "build_path": "/root/ci-folders/builds/1279", - "retry_path": "/root/ci-folders/builds/1279/retry" - }, - "manual_actions": [] - }, - "state": "available", - "external_url": null, - "environment_type": "folder", - - "stoppable?": false, - "environment_path": "/root/ci-folders/environments/34", - "created_at": "2016-11-07T11:31:59.481Z", - "updated_at": "2016-11-07T11:31:59.481Z" -}, { - "id": 35, - "name": "review", - "state": "available", - "external_url": null, - "environment_type": null, - "last_deployment": { - "id": 67, - "iid": 7, - "sha": "9dcecbafd2514b555ad2ef9be1f40c6b46009613", - "ref": { - "name": "master", - "ref_path": "/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": "9dcecbafd2514b555ad2ef9be1f40c6b46009613", - "short_id": "9dcecbaf", - "title": "aa Update .gitlab-ci.yml", - "author_name": "Administrator", - "author_email": "admin@example.com", - "created_at": "2016-11-09T15:53:06.000+00:00", - "message": "aa 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/9dcecbafd2514b555ad2ef9be1f40c6b46009613" - }, - "deployable": { - "id": 1283, - "name": "test", - "build_path": "/root/ci-folders/builds/1283", - "retry_path": "/root/ci-folders/builds/1283/retry" - }, - "manual_actions": [{ - "id": 1292, - "name": "stop_review_app", - "build_path": "/root/ci-folders/builds/1292", - "retry_path": "/root/ci-folders/builds/1292/retry", - "play_path": "/root/ci-folders/builds/1292/play" - }] - }, - "stoppable?": true, - "environment_path": "/root/ci-folders/environments/35", - "created_at": "2016-11-09T15:53:45.108Z", - "updated_at": "2016-11-09T15:53:45.108Z" -}, { - "id": 36, - "name": "review", - "state": "stopped", - "external_url": null, - "environment_type": null, - "last_deployment": null, - "stoppable?": false, - "environment_path": "/root/ci-folders/environments/36", - "created_at": "2016-11-09T15:53:45.220Z", - "updated_at": "2016-11-09T15:53:45.273Z" -}]; - this.state.stoppedCounter = this.countByState(environments, 'stopped'); this.state.availableCounter = this.countByState(environments, 'available'); From 4dbed5a249265b3690888b3fcfc91da44412f7f1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Thu, 17 Nov 2016 17:37:55 +0000 Subject: [PATCH 197/302] Adds fixed layout to prevent the columns from jumping when opening a folder --- app/assets/stylesheets/pages/environments.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index dc45fbba61f..e9ff43a8adb 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -16,6 +16,8 @@ } .environments { + table-layout: fixed; + .deployment-column { .avatar { float: none; From 67c833e90fa39b4a648828b884bb14454ccc7c9b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Nov 2016 20:19:57 +0100 Subject: [PATCH 198/302] Fix Rubocop offense for lines in environment specs --- spec/features/environment_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 51bd1d4bdfc..0c1939fd885 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -64,7 +64,6 @@ feature 'Environment', :feature do given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } scenario 'does show a play button' do - expect(page).to have_link(manual.name.humanize) end From 98773ef9745ae64da953a62fd4fbcbf290a37ea7 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 17 Nov 2016 20:42:18 +0100 Subject: [PATCH 199/302] preload ids or objects for users, merge request and issues --- lib/gitlab/cycle_analytics/author_updater.rb | 27 +++++++++++++++++++ lib/gitlab/cycle_analytics/base_event.rb | 24 ++++++++++++++--- lib/gitlab/cycle_analytics/code_event.rb | 6 ++--- lib/gitlab/cycle_analytics/issue_event.rb | 6 ++--- .../cycle_analytics/production_event.rb | 6 ++--- lib/gitlab/cycle_analytics/review_event.rb | 6 ++--- 6 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 lib/gitlab/cycle_analytics/author_updater.rb diff --git a/lib/gitlab/cycle_analytics/author_updater.rb b/lib/gitlab/cycle_analytics/author_updater.rb new file mode 100644 index 00000000000..ab18f292bc3 --- /dev/null +++ b/lib/gitlab/cycle_analytics/author_updater.rb @@ -0,0 +1,27 @@ +module Gitlab + module CycleAnalytics + class AuthorUpdater + def self.update!(*args) + new(*args).update! + end + + def initialize(event_result) + @event_result = event_result + end + + def update! + @event_result.each do |event| + event['author'] = users[event.delete('author_id').to_i].first + end + end + + def user_ids + @event_result.map { |event| event['author_id'] } + end + + def users + @users ||= User.find(user_ids).group_by { |user| user['id'] } + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index 1e76fbec855..7395561a3fc 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -12,7 +12,9 @@ module Gitlab end def fetch - @query.execute(self).map do |event| + update_author! if event_result.first['author_id'] + + event_result.map do |event| serialize(event) if has_permission?(event['id']) end end @@ -25,12 +27,28 @@ module Gitlab private + def update_author! + AuthorUpdater.update!(event_result) + end + + def event_result + @event_result ||= @query.execute(self).to_a + end + def serialize(_event) raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)") end - def has_permission?(_id) - true + def has_permission?(id) + allowed_ids.nil? || allowed_ids.include?(id.to_i) + end + + def allowed_ids + nil + end + + def event_result_ids + event_result.map { |event| event['id'] } end end end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb index 29f62cb22aa..02a3a44d544 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/code_event.rb @@ -19,13 +19,11 @@ module Gitlab private def serialize(event) - event['author'] = User.find(event.delete('author_id')) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end - def has_permission?(id) - @options[:current_user].can?(:read_merge_request, MergeRequest.find(id)) + def allowed_ids + @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) end end end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb index 70c015df419..36a990d6222 100644 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event.rb @@ -18,13 +18,11 @@ module Gitlab private def serialize(event) - event['author'] = User.find(event.delete('author_id')) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json end - def has_permission?(id) - @options[:current_user].can?(:read_issue, Issue.find(id)) + def allowed_ids + @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) end end end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb index 80c0d08c039..fcf2dbe3490 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/production_event.rb @@ -17,13 +17,11 @@ module Gitlab private def serialize(event) - event['author'] = User.find(event.delete('author_id')) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json end - def has_permission?(id) - @options[:current_user].can?(:read_issue, Issue.find(id)) + def allowed_ids + @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) end end end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb index cc89ef68be0..30650537afe 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/review_event.rb @@ -16,13 +16,11 @@ module Gitlab end def serialize(event) - event['author'] = User.find(event.delete('author_id')) - AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end - def has_permission?(id) - @options[:current_user].can?(:read_merge_request, MergeRequest.find(id)) + def allowed_ids + @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) end end end From 24c2aa39ad0f55321ef2c28ce10d740bf6b78980 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 17 Nov 2016 20:54:02 +0100 Subject: [PATCH 200/302] fix blank state error --- lib/gitlab/cycle_analytics/base_event.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index 7395561a3fc..cedc73142fa 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -12,7 +12,7 @@ module Gitlab end def fetch - update_author! if event_result.first['author_id'] + update_author! event_result.map do |event| serialize(event) if has_permission?(event['id']) @@ -28,6 +28,8 @@ module Gitlab private def update_author! + return unless event_result.any? && event_result.first['author_id'] + AuthorUpdater.update!(event_result) end From 9ed7171a6a8c98858949891b298789a97c4f3fba Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Thu, 17 Nov 2016 13:33:53 -0600 Subject: [PATCH 201/302] Fix builds/show spec; use iid instead of id --- app/helpers/environment_helper.rb | 2 +- app/views/projects/builds/show.html.haml | 2 +- spec/views/projects/builds/show.html.haml_spec.rb | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index ea34bce9367..27975b7ddb7 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -17,7 +17,7 @@ module EnvironmentHelper def deployment_link(deployment) return unless deployment - link_to "##{deployment.id}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] + link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable] end def last_deployment_link_for_environment_build(project, build) diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index efb130a5dd8..0eaa602dc71 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -43,7 +43,7 @@ - if environment.last_deployment View the most recent deployment #{deployment_link(environment.last_deployment)}. - elsif @build.complete? && !@build.success? - The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not complete. + The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed. - else This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.last_deployment diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 3b9a9c95daa..e0c77201116 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -40,6 +40,10 @@ describe 'projects/builds/show', :view do create(:ci_build, :success, environment: 'staging', pipeline: pipeline) end + let(:second_build) do + create(:ci_build, :success, environment: 'staging', pipeline: pipeline) + end + let(:environment) do create(:environment, name: 'staging', project: project) end @@ -49,12 +53,12 @@ describe 'projects/builds/show', :view do end let!(:second_deployment) do - create(:deployment, environment: environment, deployable: build) + create(:deployment, environment: environment, deployable: second_build) end it 'shows deployment message' do expected_text = 'This build is an out-of-date deployment ' \ - "to staging.\nView the most recent deployment ##{second_deployment.id}." + "to staging.\nView the most recent deployment ##{second_deployment.iid}." render expect(rendered).to have_css('.environment-information', text: expected_text) @@ -71,7 +75,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not complete.' + expected_text = 'The deployment of this build to staging did not succeed.' render expect(rendered).to have_css( @@ -107,7 +111,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not complete' + expected_text = 'The deployment of this build to staging did not succeed' render expect(rendered).to have_css( From 718b08429ce3372756186f9822091e887d91e9b3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Sun, 13 Nov 2016 20:35:47 +0100 Subject: [PATCH 202/302] Create relation between chat user and GitLab user and allow to authorize them [ci skip] --- .../profiles/chat_names_controller.rb | 64 +++++++++++++++++++ app/models/chat_name.rb | 12 ++++ app/models/user.rb | 1 + app/services/chat_names/find_user_service.rb | 15 +++++ app/services/chat_names/request_service.rb | 32 ++++++++++ app/views/layouts/nav/_profile.html.haml | 4 ++ app/views/profiles/chat_names/index.html.haml | 49 ++++++++++++++ app/views/profiles/chat_names/new.html.haml | 15 +++++ config/routes/profile.rb | 6 ++ ...1113184239_create_user_chat_names_table.rb | 20 ++++++ db/schema.rb | 16 ++++- lib/gitlab/chat_name_token.rb | 45 +++++++++++++ 12 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 app/controllers/profiles/chat_names_controller.rb create mode 100644 app/models/chat_name.rb create mode 100644 app/services/chat_names/find_user_service.rb create mode 100644 app/services/chat_names/request_service.rb create mode 100644 app/views/profiles/chat_names/index.html.haml create mode 100644 app/views/profiles/chat_names/new.html.haml create mode 100644 db/migrate/20161113184239_create_user_chat_names_table.rb create mode 100644 lib/gitlab/chat_name_token.rb diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb new file mode 100644 index 00000000000..8c5b83adaf4 --- /dev/null +++ b/app/controllers/profiles/chat_names_controller.rb @@ -0,0 +1,64 @@ +class Profiles::ChatNamesController < Profiles::ApplicationController + before_action :chat_names + before_action :chat_name_token, only: [:new] + before_action :chat_name_params, only: [:new, :create, :deny] + + def index + end + + def new + end + + def create + new_chat_name = current_user.chat_names.new(chat_name_params) + + if new_chat_name.save + flash[:notice] = "Authorized chat nickname #{new_chat_name.chat_name}" + else + flash[:alert] = "Could not authorize chat nickname. Try again!" + end + + delete_chat_name_token + redirect_to profile_chat_names_path + end + + def deny + delete_chat_name_token + + flash[:alert] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}" + + redirect_to profile_chat_names_path + end + + def destroy + @chat_name = chat_names.find(params[:id]) + + if @chat_name.destroy + flash[:notice] = "Delete chat nickname: #{@chat_name.chat_name}!" + else + flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}." + end + + redirect_to profile_chat_names_path + end + + private + + def delete_chat_name_token + chat_name_token.delete + end + + def chat_name_params + @chat_name_params ||= chat_name_token.get || render_404 + end + + def chat_name_token + return render_404 unless params[:token] || render_404 + + @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token]) + end + + def chat_names + @chat_names ||= current_user.chat_names + end +end diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb new file mode 100644 index 00000000000..f321db75eeb --- /dev/null +++ b/app/models/chat_name.rb @@ -0,0 +1,12 @@ +class ChatName < ActiveRecord::Base + belongs_to :service + belongs_to :user + + validates :user, presence: true + validates :service, presence: true + validates :team_id, presence: true + validates :chat_id, presence: true + + validates :user_id, uniqueness: { scope: [:service_id] } + validates :chat_id, uniqueness: { scope: [:service_id, :team_id] } +end diff --git a/app/models/user.rb b/app/models/user.rb index 5a2b232c4ed..519ed92e28b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,6 +56,7 @@ class User < ActiveRecord::Base has_many :personal_access_tokens, dependent: :destroy has_many :identities, dependent: :destroy, autosave: true has_many :u2f_registrations, dependent: :destroy + has_many :chat_names, dependent: :destroy # Groups has_many :members, dependent: :destroy diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb new file mode 100644 index 00000000000..6b7f75430a8 --- /dev/null +++ b/app/services/chat_names/find_user_service.rb @@ -0,0 +1,15 @@ +module ChatNames + class FindUserService + def initialize(chat_names, params) + @chat_names = chat_names + @params = params + end + + def execute + @chat_names.find_by( + team_id: @params[:team_id], + chat_id: @params[:user_id] + ) + end + end +end diff --git a/app/services/chat_names/request_service.rb b/app/services/chat_names/request_service.rb new file mode 100644 index 00000000000..c67b93f932f --- /dev/null +++ b/app/services/chat_names/request_service.rb @@ -0,0 +1,32 @@ +module ChatNames + class RequestService + include Gitlab::Routing.url_helpers + + def initialize(service, params) + @service = service + @params = params + end + + def execute + token = chat_name_token.store!(chat_name_params) + + new_profile_chat_name_url(token: token) if token + end + + private + + def chat_name_token + Gitlab::ChatNameToken.new + end + + def chat_name_params + { + service_id: @service.id, + team_id: @params[:team_id], + team_domain: @params[:team_domain], + chat_id: @params[:user_id], + chat_name: @params[:user_name] + } + end + end +end diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 6d514f669db..e06301bda14 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -17,6 +17,10 @@ = link_to applications_profile_path, title: 'Applications' do %span Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + %span + Chat = nav_link(controller: :personal_access_tokens) do = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do %span diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml new file mode 100644 index 00000000000..f90ac4c6a03 --- /dev/null +++ b/app/views/profiles/chat_names/index.html.haml @@ -0,0 +1,49 @@ +- page_title "Chat" += render 'profiles/head' + +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + You can see your Chat integrations. + + .col-lg-9 + %h5 Active chat names (#{@chat_names.length}) + + - if @chat_names.present? + .table-responsive + %table.table.chat-names + %thead + %tr + %th Project + %th Service + %th Team domain + %th Nickname + %th Created + %th + %tbody + - @chat_names.each do |chat_name| + - service = chat_name.service + - project = service.project + %tr + %td + %strong + - if can?(current_user, :read_project, project) + = link_to project.name_with_namespace, project_path(project) + - else + .light N/A + %td + %strong + - if can?(current_user, :admin_project, project) + = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service) + - else + = chat_name.service.title + %td= chat_name.team_domain + %td= chat_name.chat_name + %td= chat_name.created_at + %td= link_to "Remove", profile_chat_name_path(chat_name), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this nickname?" } + + - else + .settings-message.text-center + You don't have any active chat names. diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml new file mode 100644 index 00000000000..0b9ee8c71ad --- /dev/null +++ b/app/views/profiles/chat_names/new.html.haml @@ -0,0 +1,15 @@ +%h3.page-title Authorization required +%main{:role => "main"} + %p.h4 + Authorize the chat user + %strong.text-info= @chat_name_params[:chat_name] + to use your account? + + %hr/ + .actions + = form_tag profile_chat_names_path, method: :post do + = hidden_field_tag :token, @chat_name_token.token + = submit_tag "Authorize", class: "btn btn-success wide pull-left" + = form_tag deny_profile_chat_names_path, method: :delete do + = hidden_field_tag :token, @chat_name_token.token + = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 52b9a565db8..6b91485da9e 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -23,6 +23,12 @@ resource :profile, only: [:show, :update] do resource :preferences, only: [:show, :update] resources :keys, only: [:index, :show, :new, :create, :destroy] resources :emails, only: [:index, :create, :destroy] + resources :chat_names, only: [:index, :new, :create, :destroy] do + collection do + delete :deny + end + end + resource :avatar, only: [:destroy] resources :personal_access_tokens, only: [:index, :create] do diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb new file mode 100644 index 00000000000..f9ab2adf2a9 --- /dev/null +++ b/db/migrate/20161113184239_create_user_chat_names_table.rb @@ -0,0 +1,20 @@ +class CreateUserChatNamesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + create_table :chat_names do |t| + t.integer "user_id", null: false + t.integer "service_id", null: false + t.string "team_id" + t.string "team_domain" + t.string "chat_id" + t.string "chat_name" + t.timestamps + end + + add_index :chat_names, [:user_id, :service_id], unique: true + add_index :chat_names, [:service_id, :team_id, :chat_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index ed4dfc786f6..2742dbcf93a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161109150329) do +ActiveRecord::Schema.define(version: 20161113184239) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -152,6 +152,20 @@ ActiveRecord::Schema.define(version: 20161109150329) do t.text "message_html" end + create_table "chat_names", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "service_id", null: false + t.string "team_id" + t.string "team_domain" + t.string "chat_id" + t.string "chat_name" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "chat_names", ["service_id", "team_id", "user_id"], name: "index_chat_names_on_service_id_and_team_id_and_user_id", unique: true, using: :btree + add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree + create_table "ci_application_settings", force: :cascade do |t| t.boolean "all_broken_builds" t.boolean "add_pusher" diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb new file mode 100644 index 00000000000..c8349839219 --- /dev/null +++ b/lib/gitlab/chat_name_token.rb @@ -0,0 +1,45 @@ +require 'json' + +module Gitlab + class ChatNameToken + attr_reader :token + + TOKEN_LENGTH = 50 + EXPIRY_TIME = 1800 + + def initialize(token = new_token) + @token = token + end + + def get + Gitlab::Redis.with do |redis| + data = redis.get(redis_key) + JSON.parse(data, symbolize_names: true) if data + end + end + + def store!(params) + Gitlab::Redis.with do |redis| + params = params.to_json + redis.set(redis_key, params, ex: EXPIRY_TIME) + token + end + end + + def delete + Gitlab::Redis.with do |redis| + redis.del(redis_key) + end + end + + private + + def new_token + Devise.friendly_token(TOKEN_LENGTH) + end + + def redis_key + "gitlab:chat_names:#{token}" + end + end +end From bb988fd21498102e203f49c0968153d5ea83d03f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 16 Nov 2016 14:56:30 +0100 Subject: [PATCH 203/302] Add most of specs for chat names --- ...t_service.rb => authorize_user_service.rb} | 10 ++++- app/services/chat_names/find_user_service.rb | 17 ++++++-- app/views/profiles/chat_names/index.html.haml | 14 +++++-- ...1113184239_create_user_chat_names_table.rb | 15 +++---- db/schema.rb | 11 +++--- lib/gitlab/chat_name_token.rb | 2 +- spec/factories/chat_names.rb | 16 ++++++++ spec/lib/gitlab/chat_name_token_spec.rb | 39 +++++++++++++++++++ spec/models/chat_name_spec.rb | 16 ++++++++ spec/models/user_spec.rb | 1 + .../chat_names/authorize_user_service_spec.rb | 27 +++++++++++++ .../chat_names/find_user_service_spec.rb | 36 +++++++++++++++++ 12 files changed, 182 insertions(+), 22 deletions(-) rename app/services/chat_names/{request_service.rb => authorize_user_service.rb} (73%) create mode 100644 spec/factories/chat_names.rb create mode 100644 spec/lib/gitlab/chat_name_token_spec.rb create mode 100644 spec/models/chat_name_spec.rb create mode 100644 spec/services/chat_names/authorize_user_service_spec.rb create mode 100644 spec/services/chat_names/find_user_service_spec.rb diff --git a/app/services/chat_names/request_service.rb b/app/services/chat_names/authorize_user_service.rb similarity index 73% rename from app/services/chat_names/request_service.rb rename to app/services/chat_names/authorize_user_service.rb index c67b93f932f..321bf3a9205 100644 --- a/app/services/chat_names/request_service.rb +++ b/app/services/chat_names/authorize_user_service.rb @@ -1,5 +1,5 @@ module ChatNames - class RequestService + class AuthorizeUserService include Gitlab::Routing.url_helpers def initialize(service, params) @@ -8,13 +8,19 @@ module ChatNames end def execute - token = chat_name_token.store!(chat_name_params) + return unless chat_name_params.values.all?(&:present?) + + token = request_token new_profile_chat_name_url(token: token) if token end private + def request_token + chat_name_token.store!(chat_name_params) + end + def chat_name_token Gitlab::ChatNameToken.new end diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 6b7f75430a8..28e3e155be1 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -1,12 +1,23 @@ module ChatNames class FindUserService - def initialize(chat_names, params) - @chat_names = chat_names + def initialize(service, params) + @service = service @params = params end def execute - @chat_names.find_by( + chat_name = find_chat_name + return unless chat_name + + chat_name.update(used_at: Time.now) + chat_name.user + end + + private + + def find_chat_name + ChatName.find_by( + service: @service, team_id: @params[:team_id], chat_id: @params[:user_id] ) diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml index f90ac4c6a03..ae0b6336944 100644 --- a/app/views/profiles/chat_names/index.html.haml +++ b/app/views/profiles/chat_names/index.html.haml @@ -1,4 +1,4 @@ -- page_title "Chat" +- page_title 'Chat' = render 'profiles/head' .row.prepend-top-default @@ -20,7 +20,7 @@ %th Service %th Team domain %th Nickname - %th Created + %th Last used %th %tbody - @chat_names.each do |chat_name| @@ -41,8 +41,14 @@ = chat_name.service.title %td= chat_name.team_domain %td= chat_name.chat_name - %td= chat_name.created_at - %td= link_to "Remove", profile_chat_name_path(chat_name), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this nickname?" } + %td= + - if chat_name.used_at + time_ago_with_tooltip(chat_name.used_at) + - else + Never + + %td + = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' } - else .settings-message.text-center diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb index f9ab2adf2a9..1e138fba202 100644 --- a/db/migrate/20161113184239_create_user_chat_names_table.rb +++ b/db/migrate/20161113184239_create_user_chat_names_table.rb @@ -5,13 +5,14 @@ class CreateUserChatNamesTable < ActiveRecord::Migration def change create_table :chat_names do |t| - t.integer "user_id", null: false - t.integer "service_id", null: false - t.string "team_id" - t.string "team_domain" - t.string "chat_id" - t.string "chat_name" - t.timestamps + t.integer :user_id, null: false + t.integer :service_id, null: false + t.string :team_id, null: false + t.string :team_domain + t.string :chat_id, null: false + t.string :chat_name + t.datetime :used_at + t.timestamps null: false end add_index :chat_names, [:user_id, :service_id], unique: true diff --git a/db/schema.rb b/db/schema.rb index 2742dbcf93a..a99a2ccc1c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -155,15 +155,16 @@ ActiveRecord::Schema.define(version: 20161113184239) do create_table "chat_names", force: :cascade do |t| t.integer "user_id", null: false t.integer "service_id", null: false - t.string "team_id" + t.string "team_id", null: false t.string "team_domain" - t.string "chat_id" + t.string "chat_id", null: false t.string "chat_name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "used_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "chat_names", ["service_id", "team_id", "user_id"], name: "index_chat_names_on_service_id_and_team_id_and_user_id", unique: true, using: :btree + add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree create_table "ci_application_settings", force: :cascade do |t| diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb index c8349839219..543f038a4d4 100644 --- a/lib/gitlab/chat_name_token.rb +++ b/lib/gitlab/chat_name_token.rb @@ -5,7 +5,7 @@ module Gitlab attr_reader :token TOKEN_LENGTH = 50 - EXPIRY_TIME = 1800 + EXPIRY_TIME = 10.minutes # 10 minutes def initialize(token = new_token) @token = token diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb new file mode 100644 index 00000000000..24225468d55 --- /dev/null +++ b/spec/factories/chat_names.rb @@ -0,0 +1,16 @@ +FactoryGirl.define do + factory :chat_name, class: ChatName do + user factory: :user + service factory: :service + + team_id 'T0001' + team_domain 'Awesome Team' + + sequence :chat_id do |n| + "U#{n}" + end + sequence :chat_name do |n| + "user#{n}" + end + end +end diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb new file mode 100644 index 00000000000..8d7e7a99059 --- /dev/null +++ b/spec/lib/gitlab/chat_name_token_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::ChatNameToken, lib: true do + context 'when using unknown token' do + let(:token) { } + + subject { described_class.new(token).get } + + it 'returns empty data' do + is_expected.to be_nil + end + end + + context 'when storing data' do + let(:data) { + { key: 'value' } + } + + subject { described_class.new(@token) } + + before do + @token = described_class.new.store!(data) + end + + it 'returns stored data' do + expect(subject.get).to eq(data) + end + + context 'and after deleting them' do + before do + subject.delete + end + + it 'data are removed' do + expect(subject.get).to be_nil + end + end + end +end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb new file mode 100644 index 00000000000..b02971cab82 --- /dev/null +++ b/spec/models/chat_name_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ChatName, models: true do + subject { create(:chat_name) } + + it { is_expected.to belong_to(:service) } + it { is_expected.to belong_to(:user) } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:service) } + it { is_expected.to validate_presence_of(:team_id) } + it { is_expected.to validate_presence_of(:chat_id) } + + it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) } + it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) } +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3159243553b..36619de13f8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,6 +33,7 @@ describe User, models: true do it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } + it { is_expected.to have_many(:chat_names).dependent(:destroy) } describe '#group_members' do it 'does not include group memberships for which user is a requester' do diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb new file mode 100644 index 00000000000..f8c26e51bfc --- /dev/null +++ b/spec/services/chat_names/authorize_user_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe ChatNames::AuthorizeUserService, services: true do + describe '#execute' do + let(:service) { create(:service) } + + subject { described_class.new(service, params).execute } + + context 'when all parameters are valid' do + let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } } + + it 'requests a new token' do + is_expected.to include('http') + is_expected.to include('://') + is_expected.to include('token=') + end + end + + context 'when there are missing parameters' do + let(:params) { { } } + + it 'does not request a new token' do + is_expected.to be_nil + end + end + end +end diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb new file mode 100644 index 00000000000..cf5844069f9 --- /dev/null +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe ChatNames::FindUserService, services: true do + describe '#execute' do + let(:service) { create(:service) } + + subject { described_class.new(service, params).execute } + + context 'find user mapping' do + let(:user) { create(:user) } + let!(:chat_name) { create(:chat_name, user: user, service: service) } + + context 'when existing user is requested' do + let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + it 'returns existing user' do + is_expected.to eq(user) + end + + it 'updates when last time chat name was used' do + subject + + expect(chat_name.reload.used_at).to be_like_time(Time.now) + end + end + + context 'when different user is requested' do + let(:params) { { team_id: chat_name.team_id, user_id: 'non-existing-user' } } + + it 'returns existing user' do + is_expected.to be_nil + end + end + end + end +end From c2e6f5fc241b3a3bc980dfbdaf9af6c59a098af5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 16 Nov 2016 18:02:51 +0100 Subject: [PATCH 204/302] Fix `chat_names` before_action --- app/controllers/profiles/chat_names_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb index 8c5b83adaf4..0d3bdeff416 100644 --- a/app/controllers/profiles/chat_names_controller.rb +++ b/app/controllers/profiles/chat_names_controller.rb @@ -1,5 +1,5 @@ class Profiles::ChatNamesController < Profiles::ApplicationController - before_action :chat_names + before_action :chat_names, only: [:index] before_action :chat_name_token, only: [:new] before_action :chat_name_params, only: [:new, :create, :deny] From 9d8ca60d221a1500e31ef05ecf8c4db56adde3c0 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Sat, 12 Nov 2016 11:17:24 +0100 Subject: [PATCH 205/302] issue and mergerequest slash command for mattermost This commit includes a couple of thing: - A chatops controller - Mattermost::CommandService - Mattermost::Commands::(IssueService|MergeRequestService) The controller is the point where mattermost, and later slack will have to fire their payload to. This in turn will execute the CommandService. Thats where the authentication and authorization should happen. So far this is not yet implemented. This should happen in later commits. Per subcommand, in case of `/gitlab issue show 123` issue whould be the subcommand, there is a service to parse the data, and fetch the resource. The resource is passed back to the CommandService which structures the data. --- app/controllers/chat_ops_controller.rb | 10 +++ app/models/project.rb | 4 +- app/models/project_feature.rb | 4 + app/services/mattermost/command_service.rb | 74 +++++++++++++++++++ .../mattermost/commands/base_service.rb | 60 +++++++++++++++ .../mattermost/commands/issue_service.rb | 49 ++++++++++++ .../commands/merge_request_service.rb | 46 ++++++++++++ config/routes.rb | 5 ++ spec/controllers/chat_ops_controller_spec.rb | 11 +++ .../mattermost/command_service_spec.rb | 55 ++++++++++++++ .../mattermost/commands/issue_service_spec.rb | 70 ++++++++++++++++++ .../commands/merge_request_service_spec.rb | 68 +++++++++++++++++ 12 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 app/controllers/chat_ops_controller.rb create mode 100644 app/services/mattermost/command_service.rb create mode 100644 app/services/mattermost/commands/base_service.rb create mode 100644 app/services/mattermost/commands/issue_service.rb create mode 100644 app/services/mattermost/commands/merge_request_service.rb create mode 100644 spec/controllers/chat_ops_controller_spec.rb create mode 100644 spec/services/mattermost/command_service_spec.rb create mode 100644 spec/services/mattermost/commands/issue_service_spec.rb create mode 100644 spec/services/mattermost/commands/merge_request_service_spec.rb diff --git a/app/controllers/chat_ops_controller.rb b/app/controllers/chat_ops_controller.rb new file mode 100644 index 00000000000..2754de5e710 --- /dev/null +++ b/app/controllers/chat_ops_controller.rb @@ -0,0 +1,10 @@ +class ChatOpsController < ApplicationController + respond_to :json + + skip_before_action :verify_authenticity_token + skip_before_action :authenticate_user! + + def trigger + render json: { ok: true } + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 4aedc91dc34..1c392eb7460 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -23,7 +23,9 @@ class Project < ActiveRecord::Base cache_markdown_field :description, pipeline: :description - delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, + :merge_requests_enabled?, :issues_enabled?, to: :project_feature, + allow_nil: true default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 5c53c8f1ee5..03194fc2141 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -60,6 +60,10 @@ class ProjectFeature < ActiveRecord::Base merge_requests_access_level > DISABLED end + def issues_enabled? + issues_access_level > DISABLED + end + private # Validates builds and merge requests access level diff --git a/app/services/mattermost/command_service.rb b/app/services/mattermost/command_service.rb new file mode 100644 index 00000000000..c8329a2dca4 --- /dev/null +++ b/app/services/mattermost/command_service.rb @@ -0,0 +1,74 @@ +module Mattermost + class CommandService < BaseService + SERVICES = [ + Mattermost::Commands::IssueService, + Mattermost::Commands::MergeRequestService + ] + + def execute + return unknown_user unless current_user + return not_found_404 unless can?(current_user, :read_project, project) + + triggered_command = command + service = SERVICES.find do |service| + service.triggered_by?(triggered_command) && service.available?(project) + end + + if service + present service.new(project, current_user, params).execute + else + help_message + end + end + + private + + def command + params[:text].match(/\A(?\S+)/)[:command] + end + + def present(result) + return not_found_404 unless result + + if result.respond_to?(:count) + if count > 1 + #TODO + return resource_list(result) + else + result = result.first + end + end + + message = "### [#{result.to_reference} #{result.title}](link(result))" + message << "\n\n#{result.description}" if result.description + + { + response_type: :in_channel, + text: message + } + end + + def unknown_user + { + response_type: :ephemeral, + text: 'Hi there! I have not yet had the pleasure to get acquainted!' # TODO allow user to authenticate and authorize + } + end + + def not_found_404 + { + response_type: :ephemeral, + text: "404 not found! GitLab couldn't find what your were looking for! :boom:", + } + end + + def help_message + command_help_messages = SERVICES.map { |service| service.help_message(project) } + + { + response_type: :ephemeral, + text: "Sadly, the used command does not exist, lets take a look at your options here:\n\n#{command_help_messages.join("\n")}" + } + end + end +end diff --git a/app/services/mattermost/commands/base_service.rb b/app/services/mattermost/commands/base_service.rb new file mode 100644 index 00000000000..54d8fa088b8 --- /dev/null +++ b/app/services/mattermost/commands/base_service.rb @@ -0,0 +1,60 @@ +module Mattermost + module Commands + class BaseService < ::BaseService + class << self + def triggered_by?(_) + raise NotImplementedError + end + + def available?(_) + raise NotImplementedError + end + + def help_message(_) + NotImplementedError + end + end + + QUERY_LIMIT = 5 + + def execute + subcommand, args = parse_command + + if subcommands.include?(subcommand) + send(subcommand, args) + else + nil + end + end + + private + + # This method can only be used by a resource that has an iid. Also, the + # class should implement #collection itself. Probably project.resource + # would suffice + def show(args) + iid = args.first + + result = collection.find_by(iid: iid) + if readable?(result) + result + else + nil + end + end + + # Child class should implement #collection + def search(args) + query = args.join(' ') + + collection.search(query).limit(QUERY_LIMIT).select do |issuable| + readable?(issuable) + end + end + + def command + params[:text] + end + end + end +end diff --git a/app/services/mattermost/commands/issue_service.rb b/app/services/mattermost/commands/issue_service.rb new file mode 100644 index 00000000000..879f0c2eb21 --- /dev/null +++ b/app/services/mattermost/commands/issue_service.rb @@ -0,0 +1,49 @@ +module Mattermost + module Commands + class IssueService < Mattermost::Commands::BaseService + class << self + def triggered_by?(command) + command == 'issue' + end + + def available?(project) + project.issues_enabled? && project.default_issues_tracker? + end + + def help_message(project) + return nil unless available?(project) + + message = "issue show " + message << "issue search " + end + end + + private + + #TODO implement create + def subcommands + %w[creates search show] + end + + def collection + project.issues + end + + def readable?(issue) + can?(current_user, :read_issue, issue) + end + + # 'issue create my new title\nmy new description + # => 'create', ['my', 'new', 'title, ['my new description']] + # 'issue show 123' + # => 'show', ['123'] + def parse_command + split = command.split + subcommand = split[1] + args = split[2..-1] + + return subcommand, args + end + end + end +end diff --git a/app/services/mattermost/commands/merge_request_service.rb b/app/services/mattermost/commands/merge_request_service.rb new file mode 100644 index 00000000000..907834ee0c2 --- /dev/null +++ b/app/services/mattermost/commands/merge_request_service.rb @@ -0,0 +1,46 @@ +module Mattermost + module Commands + class MergeRequestService < Mattermost::Commands::BaseService + class << self + def triggered_by?(command) + command == 'mergerequest' + end + + def available?(project) + project.merge_requests_enabled? + end + + def help_message(project) + return nil unless available?(project) + + message = "mergerequest show \n" + message << "mergerequest search " + end + end + + private + + def subcommands + %w[show search] + end + + def collection + project.merge_requests + end + + def readable?(_) + can?(current_user, :read_merge_request, project) + end + + # 'mergerequest show 123' => 'show', ['123'] + # 'mergerequest search my query' => 'search',['my', 'query'] + def parse_command + split = command.split + subcommand = split[1] + args = split[2..-1] + + return subcommand, args + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 7bf6c03e69b..b1de4ba3821 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,6 +68,11 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] + # For slash commands to POST + namespace :chat_ops do + post :trigger + end + draw :import draw :uploads draw :explore diff --git a/spec/controllers/chat_ops_controller_spec.rb b/spec/controllers/chat_ops_controller_spec.rb new file mode 100644 index 00000000000..7303c981ba2 --- /dev/null +++ b/spec/controllers/chat_ops_controller_spec.rb @@ -0,0 +1,11 @@ +require 'rails_helper' + +RSpec.describe ChatOpsController, type: :controller do + describe "POST #trigger" do + it "returns http success" do + post :trigger + + expect(response).to have_http_status(:success) + end + end +end diff --git a/spec/services/mattermost/command_service_spec.rb b/spec/services/mattermost/command_service_spec.rb new file mode 100644 index 00000000000..35051c09f8d --- /dev/null +++ b/spec/services/mattermost/command_service_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Mattermost::CommandService, service: true do + let(:project) { build(:project) } + let(:user) { build(:user) } + let(:params) { { text: 'issue show 1' } } + + subject { described_class.new(project, user, params).execute } + + describe '#execute' do + context 'no user could be found' do + let(:user) { nil } + + it 'asks the user to introduce him/herself' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with 'Hi there!' + end + end + + context 'no project could be found' do + it 'shows a 404 not found message' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + + context 'the user has access to the project' do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + end + + context 'no command service is triggered' do + let(:params) { { text: 'unknown_command' } } + + it 'shows the help messages' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with 'Sadly, the used command' + end + end + + context 'a valid command is executed' do + let(:issue) { create(:issue, project: project) } + let(:params) { { text: "issue show #{issue.iid}" } } + + it 'a resource is presented to the user' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match issue.title + end + end + end + end +end diff --git a/spec/services/mattermost/commands/issue_service_spec.rb b/spec/services/mattermost/commands/issue_service_spec.rb new file mode 100644 index 00000000000..67629d26bc0 --- /dev/null +++ b/spec/services/mattermost/commands/issue_service_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Mattermost::Commands::IssueService do + let(:project) { create(:project) } + let(:issue ) { create(:issue, :confidential, title: 'Bird is the word', project: project) } + let(:user) { issue.author } + + subject { described_class.new(project, user, params).execute } + + before do + project.team << [user, :developer] + end + + describe '#execute' do + context 'show as subcommand' do + context 'issue can be found' do + let(:params) { { text: "issue show #{issue.iid}" } } + + it 'returns the merge request' do + expect(subject).to eq issue + end + + context 'the user has no access' do + let(:non_member) { create(:user) } + subject { described_class.new(project, non_member, params).execute } + + it 'returns nil' do + expect(subject).to eq nil + end + end + end + + context 'issue can not be found' do + let(:params) { { text: 'issue show 12345' } } + + it 'returns nil' do + expect(subject).to eq nil + end + end + end + + context 'search as a subcommand' do + context 'with results' do + let(:params) { { text: "issue search is the word" } } + + it 'returns the issue' do + expect(subject).to eq [issue] + end + end + + context 'without results' do + let(:params) { { text: 'issue search mepmep' } } + + it 'returns an empty collection' do + expect(subject).to eq [] + end + end + end + end + + describe 'help_message' do + context 'issues are disabled' do + it 'returns nil' do + allow(described_class).to receive(:available?).and_return false + + expect(described_class.help_message(project)).to eq nil + end + end + end +end diff --git a/spec/services/mattermost/commands/merge_request_service_spec.rb b/spec/services/mattermost/commands/merge_request_service_spec.rb new file mode 100644 index 00000000000..39381520a99 --- /dev/null +++ b/spec/services/mattermost/commands/merge_request_service_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe Mattermost::Commands::MergeRequestService do + let(:project) { create(:project, :private) } + let(:merge_request) { create(:merge_request, title: 'Bird is the word', source_project: project) } + let(:user) { merge_request.author } + + subject { described_class.new(project, user, params).execute } + + before do + project.team << [user, :developer] + end + + context 'show as subcommand' do + context 'merge request can be found' do + let(:params) { { text: "mergerequest show #{merge_request.iid}" } } + + it 'returns the merge request' do + expect(subject).to eq merge_request + end + + context 'the user has no access' do + let(:non_member) { create(:user) } + subject { described_class.new(project, non_member, params).execute } + + it 'returns nil' do + expect(subject).to eq nil + end + end + end + + context 'merge request can not be found' do + let(:params) { { text: 'mergerequest show 12345' } } + + it 'returns nil' do + expect(subject).to eq nil + end + end + end + + context 'search as a subcommand' do + context 'with results' do + let(:params) { { text: "mergerequest search is the word" } } + + it 'returns the merge_request' do + expect(subject).to eq [merge_request] + end + end + + context 'without results' do + let(:params) { { text: 'mergerequest search mepmep' } } + + it 'returns an empty collection' do + expect(subject).to eq [] + end + end + end + + describe 'help_message' do + context 'issues are disabled' do + it 'returns nil' do + allow(described_class).to receive(:available?).and_return false + + expect(described_class.help_message(project)).to eq nil + end + end + end +end From 657838f1c88ed823e33d0ee07f19410be908f09f Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Sat, 12 Nov 2016 14:23:28 +0100 Subject: [PATCH 206/302] fixup! issue and mergerequest slash command for mattermost --- app/models/project.rb | 2 +- app/services/mattermost/command_service.rb | 2 +- app/services/mattermost/commands/issue_service.rb | 4 ++-- app/services/mattermost/commands/merge_request_service.rb | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 1c392eb7460..ec64e4bc11d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -25,7 +25,7 @@ class Project < ActiveRecord::Base delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, :issues_enabled?, to: :project_feature, - allow_nil: true + allow_nil: true default_value_for :archived, false default_value_for :visibility_level, gitlab_config_features.visibility_level diff --git a/app/services/mattermost/command_service.rb b/app/services/mattermost/command_service.rb index c8329a2dca4..b1017a1eb53 100644 --- a/app/services/mattermost/command_service.rb +++ b/app/services/mattermost/command_service.rb @@ -32,7 +32,7 @@ module Mattermost if result.respond_to?(:count) if count > 1 - #TODO + # TODO return resource_list(result) else result = result.first diff --git a/app/services/mattermost/commands/issue_service.rb b/app/services/mattermost/commands/issue_service.rb index 879f0c2eb21..4472098e566 100644 --- a/app/services/mattermost/commands/issue_service.rb +++ b/app/services/mattermost/commands/issue_service.rb @@ -20,7 +20,7 @@ module Mattermost private - #TODO implement create + # TODO implement create def subcommands %w[creates search show] end @@ -42,7 +42,7 @@ module Mattermost subcommand = split[1] args = split[2..-1] - return subcommand, args + [subcommand, args] end end end diff --git a/app/services/mattermost/commands/merge_request_service.rb b/app/services/mattermost/commands/merge_request_service.rb index 907834ee0c2..a27b3861b23 100644 --- a/app/services/mattermost/commands/merge_request_service.rb +++ b/app/services/mattermost/commands/merge_request_service.rb @@ -39,7 +39,7 @@ module Mattermost subcommand = split[1] args = split[2..-1] - return subcommand, args + [subcommand, args] end end end From 242e291e8999705cb517b8c2dfa2c29704d4ace9 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Sat, 12 Nov 2016 14:25:40 +0100 Subject: [PATCH 207/302] Add issue create subcommand --- app/services/mattermost/command_service.rb | 2 +- .../mattermost/commands/issue_service.rb | 19 +++++++++++++------ .../mattermost/commands/issue_service_spec.rb | 13 +++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/app/services/mattermost/command_service.rb b/app/services/mattermost/command_service.rb index b1017a1eb53..2a104174d97 100644 --- a/app/services/mattermost/command_service.rb +++ b/app/services/mattermost/command_service.rb @@ -24,7 +24,7 @@ module Mattermost private def command - params[:text].match(/\A(?\S+)/)[:command] + params[:text].split.first end def present(result) diff --git a/app/services/mattermost/commands/issue_service.rb b/app/services/mattermost/commands/issue_service.rb index 4472098e566..17407355547 100644 --- a/app/services/mattermost/commands/issue_service.rb +++ b/app/services/mattermost/commands/issue_service.rb @@ -20,9 +20,20 @@ module Mattermost private - # TODO implement create + def create(_) + return nil unless can?(current_user, :create_issue, project) + + # We parse again as the previous split splits on continues whitespace + # per the ruby spec, but we loose information on where the new lines were + match = command.match(/\Aissue create (?.*)\n*/) + title = match[:title] + description = match.post_match + + Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + def subcommands - %w[creates search show] + %w[create search show] end def collection @@ -33,10 +44,6 @@ module Mattermost can?(current_user, :read_issue, issue) end - # 'issue create my new title\nmy new description - # => 'create', ['my', 'new', 'title, ['my new description']] - # 'issue show 123' - # => 'show', ['123'] def parse_command split = command.split subcommand = split[1] diff --git a/spec/services/mattermost/commands/issue_service_spec.rb b/spec/services/mattermost/commands/issue_service_spec.rb index 67629d26bc0..5ef363274ad 100644 --- a/spec/services/mattermost/commands/issue_service_spec.rb +++ b/spec/services/mattermost/commands/issue_service_spec.rb @@ -56,6 +56,19 @@ describe Mattermost::Commands::IssueService do end end end + + context 'create as subcommand' do + let(:title) { 'my new issue' } + let(:params) { { text: "issue create #{title}" } } + + it 'return the new issue' do + expect(subject).to be_a Issue + end + + it 'creates a new issue' do + expect { subject }.to change { Issue.count }.by(1) + end + end end describe 'help_message' do From 9191f538ba97f59d09c6ffbb747e4a34e30231ee Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sun, 13 Nov 2016 20:35:47 +0100 Subject: [PATCH 208/302] Create relation between chat user and GitLab user and allow to authorize them [ci skip] --- app/models/project_services/chat_service.rb | 21 +++++++++ .../mattermost_chat_service.rb | 44 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 app/models/project_services/chat_service.rb create mode 100644 app/models/project_services/mattermost_chat_service.rb diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb new file mode 100644 index 00000000000..c1b6369a5bd --- /dev/null +++ b/app/models/project_services/chat_service.rb @@ -0,0 +1,21 @@ +# Base class for Chat services +class ChatService < Service + default_value_for :category, 'chat' + + has_many :chat_users + + def valid_token?(token) + self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + end + + def supported_events + end + + def trigger(params) + # implement inside child + end + + def chat_user_params(params) + params.permit() + end +end diff --git a/app/models/project_services/mattermost_chat_service.rb b/app/models/project_services/mattermost_chat_service.rb new file mode 100644 index 00000000000..2adcbf5e5ce --- /dev/null +++ b/app/models/project_services/mattermost_chat_service.rb @@ -0,0 +1,44 @@ +# Base class for Chat services +class MattermostChatService < ChatService + def title + 'Mattermost' + end + + def description + 'Self-hosted Slack-alternative' + end + + def to_param + 'mattermost' + end + + def help + 'This service allows you to use slash commands with your Mattermost installation.<br/> + To setup this Service you need to create a new <b>"Slash commands"</b> in your Mattermost integration panel, + and enter the token below.' + end + + def fields + [ + { type: 'text', name: 'token', placeholder: 'https://hooks.slack.com/services/...' } + ] + end + + def trigger(params) + user = ChatNames::FindUserService.new(chat_names, params).execute + return authorize_chat_name(params) unless user + + Mattermost::CommandService.new(project, user, params).execute + end + + private + + def authorize_chat_name(params) + url = ChatNames::RequestService.new(service, params).execute + + { + response_type: :ephemeral, + message: "You are not authorized. Click this [link](#{url}) to authorize." + } + end +end From 4762198959fbc2bc22981749313955b65c6d6026 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 14 Nov 2016 12:41:14 +0100 Subject: [PATCH 209/302] Remove duplication of implementations --- app/controllers/chat_ops_controller.rb | 10 ---------- app/models/project_services/chat_service.rb | 3 ++- .../project_services/mattermost_chat_service.rb | 2 +- config/routes.rb | 5 ----- spec/controllers/chat_ops_controller_spec.rb | 11 ----------- 5 files changed, 3 insertions(+), 28 deletions(-) delete mode 100644 app/controllers/chat_ops_controller.rb delete mode 100644 spec/controllers/chat_ops_controller_spec.rb diff --git a/app/controllers/chat_ops_controller.rb b/app/controllers/chat_ops_controller.rb deleted file mode 100644 index 2754de5e710..00000000000 --- a/app/controllers/chat_ops_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ChatOpsController < ApplicationController - respond_to :json - - skip_before_action :verify_authenticity_token - skip_before_action :authenticate_user! - - def trigger - render json: { ok: true } - end -end diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index c1b6369a5bd..f6d190dead5 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -1,4 +1,5 @@ # Base class for Chat services +# This class is not meant to be used directly, but only to inherrit from. class ChatService < Service default_value_for :category, 'chat' @@ -12,7 +13,7 @@ class ChatService < Service end def trigger(params) - # implement inside child + raise NotImplementedError end def chat_user_params(params) diff --git a/app/models/project_services/mattermost_chat_service.rb b/app/models/project_services/mattermost_chat_service.rb index 2adcbf5e5ce..f4a392ee17e 100644 --- a/app/models/project_services/mattermost_chat_service.rb +++ b/app/models/project_services/mattermost_chat_service.rb @@ -5,7 +5,7 @@ class MattermostChatService < ChatService end def description - 'Self-hosted Slack-alternative' + 'Mattermost is an open source, self-hosted Slack-alternative' end def to_param diff --git a/config/routes.rb b/config/routes.rb index b1de4ba3821..7bf6c03e69b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -68,11 +68,6 @@ Rails.application.routes.draw do # Notification settings resources :notification_settings, only: [:create, :update] - # For slash commands to POST - namespace :chat_ops do - post :trigger - end - draw :import draw :uploads draw :explore diff --git a/spec/controllers/chat_ops_controller_spec.rb b/spec/controllers/chat_ops_controller_spec.rb deleted file mode 100644 index 7303c981ba2..00000000000 --- a/spec/controllers/chat_ops_controller_spec.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'rails_helper' - -RSpec.describe ChatOpsController, type: :controller do - describe "POST #trigger" do - it "returns http success" do - post :trigger - - expect(response).to have_http_status(:success) - end - end -end From 53271b486d296fae2e290d6948a05aeb47dbea89 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 14 Nov 2016 15:10:35 +0100 Subject: [PATCH 210/302] Make chat authorization to work [ci skip] --- .../projects/services_controller.rb | 2 ++ app/helpers/triggers_helper.rb | 4 +++ app/models/project.rb | 5 +-- app/models/project_services/chat_service.rb | 7 ++-- .../mattermost_chat_service.rb | 35 ++++++++++++++----- app/models/service.rb | 1 + app/views/shared/_service_settings.html.haml | 33 ++++++++--------- config/environments/development.rb | 3 ++ lib/api/services.rb | 29 +++++++++++++-- 9 files changed, 88 insertions(+), 31 deletions(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 40a23a6f806..30c2a5d9982 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -28,6 +28,8 @@ class Projects::ServicesController < Projects::ApplicationController end def test + return render_404 unless @service.can_test? + data = @service.test_data(project, current_user) outcome = @service.test(data) diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index c41181bab3d..b0135ea2e95 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -6,4 +6,8 @@ module TriggersHelper "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds" end end + + def service_trigger_url(service) + "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger" + end end diff --git a/app/models/project.rb b/app/models/project.rb index ec64e4bc11d..a118761d93d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -23,8 +23,8 @@ class Project < ActiveRecord::Base cache_markdown_field :description, pipeline: :description - delegate :feature_available?, :builds_enabled?, :wiki_enabled?, - :merge_requests_enabled?, :issues_enabled?, to: :project_feature, + delegate :feature_available?, :builds_enabled?, :wiki_enabled?, + :merge_requests_enabled?, :issues_enabled?, to: :project_feature, allow_nil: true default_value_for :archived, false @@ -91,6 +91,7 @@ class Project < ActiveRecord::Base has_one :assembla_service, dependent: :destroy has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy + has_one :mattermost_chat_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index f6d190dead5..db387ef3026 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -3,13 +3,16 @@ class ChatService < Service default_value_for :category, 'chat' - has_many :chat_users + has_many :chat_names, foreign_key: :service_id def valid_token?(token) - self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) + self.respond_to?(:token) && + self.token.present? && + ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end def supported_events + [] end def trigger(params) diff --git a/app/models/project_services/mattermost_chat_service.rb b/app/models/project_services/mattermost_chat_service.rb index f4a392ee17e..01ff6b0f8b4 100644 --- a/app/models/project_services/mattermost_chat_service.rb +++ b/app/models/project_services/mattermost_chat_service.rb @@ -1,5 +1,12 @@ -# Base class for Chat services class MattermostChatService < ChatService + include TriggersHelper + + prop_accessor :token + + def can_test? + false + end + def title 'Mattermost' end @@ -13,28 +20,40 @@ class MattermostChatService < ChatService end def help - 'This service allows you to use slash commands with your Mattermost installation.<br/> - To setup this Service you need to create a new <b>"Slash commands"</b> in your Mattermost integration panel, - and enter the token below.' + "This service allows you to use slash commands with your Mattermost installation.<br/> + To setup this Service you need to create a new <b>Slash commands</b> in your Mattermost integration panel.<br/> + <br/> + Create integration with URL #{service_trigger_url(self)} and enter the token below." end def fields [ - { type: 'text', name: 'token', placeholder: 'https://hooks.slack.com/services/...' } + { type: 'text', name: 'token', placeholder: '' } ] end def trigger(params) - user = ChatNames::FindUserService.new(chat_names, params).execute + return nil unless valid_token?(params[:token]) + + user = find_chat_user(params) return authorize_chat_name(params) unless user - Mattermost::CommandService.new(project, user, params).execute + Mattermost::CommandService.new(project, user, params.slice(:command, :text)). + execute end private + def find_chat_user(params) + params = params.slice(:team_id, :user_id) + ChatNames::FindUserService. + new(chat_names, params). + execute + end + def authorize_chat_name(params) - url = ChatNames::RequestService.new(service, params).execute + params = params.slice(:team_id, :team_domain, :user_id, :user_name) + url = ChatNames::AuthorizeUserService.new(self, params).execute { response_type: :ephemeral, diff --git a/app/models/service.rb b/app/models/service.rb index 9d6ff190cdf..8eda1930400 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -214,6 +214,7 @@ class Service < ActiveRecord::Base hipchat irker jira + mattermost_chat pivotaltracker pushover redmine diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 5254d265918..601ef51737a 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -10,26 +10,27 @@ .col-sm-10 = form.check_box :active -.form-group - = form.label :url, "Trigger", class: 'control-label' +- if @service.supported_events.present? + .form-group + = form.label :url, "Trigger", class: 'control-label' - .col-sm-10 - - @service.supported_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'pull-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do - %strong - = event.humanize + .col-sm-10 + - @service.supported_events.each do |event| + %div + = form.check_box service_event_field_name(event), class: 'pull-left' + .prepend-left-20 + = form.label service_event_field_name(event), class: 'list-label' do + %strong + = event.humanize - - field = @service.event_field(event) + - field = @service.event_field(event) - - if field - %p - = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] + - if field + %p + = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - %p.light - = service_event_description(event) + %p.light + = service_event_description(event) - @service.global_fields.each do |field| - type = field[:type] diff --git a/config/environments/development.rb b/config/environments/development.rb index 45a8c1add3e..6780dbef8e3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,4 +45,7 @@ Rails.application.configure do # Do not log asset requests config.assets.quiet = true + + # Make hot reloading to work with Grape API + ActiveSupport::Dependencies.explicitly_unloadable_constants << "API" end diff --git a/lib/api/services.rb b/lib/api/services.rb index fc8598daa32..b4b3bb6e41a 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,10 +1,10 @@ module API # Projects API class Services < Grape::API - before { authenticate! } - before { authorize_admin_project } - resource :projects do + before { authenticate! } + before { authorize_admin_project } + # Set <service_slug> service for project # # Example Request: @@ -59,5 +59,28 @@ module API present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin? end end + + resource :projects do + post ':id/services/:service_slug/trigger' do + project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) + + underscored_service = params[:service_slug].underscore + + not_found!('Service') unless Service.available_services_names.include?(underscored_service) + service_method = "#{underscored_service}_service" + + service = project.public_send(service_method) + + result = if service.try(:active?) && service.respond_to?(:trigger) + service.trigger(params) + end + + if result + present result, status: result[:status] || 200 + else + not_found!('Service') + end + end + end end end From 106b1e39c0d00f25e34dda0eba962c8cf1d43f1b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 15 Nov 2016 09:55:56 +0100 Subject: [PATCH 211/302] First steps on refactoring Mattermost Slash commands Now, each subcommand has its own service, plus I've introduced presenters to be able to delegate the generation of the views. --- .../mattermost_chat_service.rb | 25 +----- app/services/mattermost/command_service.rb | 74 ---------------- .../mattermost/commands/base_service.rb | 61 +++++--------- .../commands/issue_create_service.rb | 13 +++ .../commands/issue_search_service.rb | 11 +++ .../mattermost/commands/issue_service.rb | 43 +--------- .../mattermost/commands/issue_show_service.rb | 12 +++ .../commands/merge_request_search_service.rb | 11 +++ .../commands/merge_request_service.rb | 33 +------- .../commands/merge_request_show_service.rb | 12 +++ .../mattermost/slash_command_service.rb | 28 +++++++ lib/mattermost/presenter.rb | 84 +++++++++++++++++++ .../mattermost/command_service_spec.rb | 55 ------------ .../mattermost/slash_command_service_spec.rb | 30 +++++++ 14 files changed, 233 insertions(+), 259 deletions(-) delete mode 100644 app/services/mattermost/command_service.rb create mode 100644 app/services/mattermost/commands/issue_create_service.rb create mode 100644 app/services/mattermost/commands/issue_search_service.rb create mode 100644 app/services/mattermost/commands/issue_show_service.rb create mode 100644 app/services/mattermost/commands/merge_request_search_service.rb create mode 100644 app/services/mattermost/commands/merge_request_show_service.rb create mode 100644 app/services/mattermost/slash_command_service.rb create mode 100644 lib/mattermost/presenter.rb delete mode 100644 spec/services/mattermost/command_service_spec.rb create mode 100644 spec/services/mattermost/slash_command_service_spec.rb diff --git a/app/models/project_services/mattermost_chat_service.rb b/app/models/project_services/mattermost_chat_service.rb index 01ff6b0f8b4..5e9f4d255b6 100644 --- a/app/models/project_services/mattermost_chat_service.rb +++ b/app/models/project_services/mattermost_chat_service.rb @@ -33,31 +33,10 @@ class MattermostChatService < ChatService end def trigger(params) - return nil unless valid_token?(params[:token]) - - user = find_chat_user(params) - return authorize_chat_name(params) unless user + user = ChatNames::FindUserService.new(chat_names, params).execute + return Mattermost::Presenter.authorize_chat_name(params) unless user Mattermost::CommandService.new(project, user, params.slice(:command, :text)). execute end - - private - - def find_chat_user(params) - params = params.slice(:team_id, :user_id) - ChatNames::FindUserService. - new(chat_names, params). - execute - end - - def authorize_chat_name(params) - params = params.slice(:team_id, :team_domain, :user_id, :user_name) - url = ChatNames::AuthorizeUserService.new(self, params).execute - - { - response_type: :ephemeral, - message: "You are not authorized. Click this [link](#{url}) to authorize." - } - end end diff --git a/app/services/mattermost/command_service.rb b/app/services/mattermost/command_service.rb deleted file mode 100644 index 2a104174d97..00000000000 --- a/app/services/mattermost/command_service.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Mattermost - class CommandService < BaseService - SERVICES = [ - Mattermost::Commands::IssueService, - Mattermost::Commands::MergeRequestService - ] - - def execute - return unknown_user unless current_user - return not_found_404 unless can?(current_user, :read_project, project) - - triggered_command = command - service = SERVICES.find do |service| - service.triggered_by?(triggered_command) && service.available?(project) - end - - if service - present service.new(project, current_user, params).execute - else - help_message - end - end - - private - - def command - params[:text].split.first - end - - def present(result) - return not_found_404 unless result - - if result.respond_to?(:count) - if count > 1 - # TODO - return resource_list(result) - else - result = result.first - end - end - - message = "### [#{result.to_reference} #{result.title}](link(result))" - message << "\n\n#{result.description}" if result.description - - { - response_type: :in_channel, - text: message - } - end - - def unknown_user - { - response_type: :ephemeral, - text: 'Hi there! I have not yet had the pleasure to get acquainted!' # TODO allow user to authenticate and authorize - } - end - - def not_found_404 - { - response_type: :ephemeral, - text: "404 not found! GitLab couldn't find what your were looking for! :boom:", - } - end - - def help_message - command_help_messages = SERVICES.map { |service| service.help_message(project) } - - { - response_type: :ephemeral, - text: "Sadly, the used command does not exist, lets take a look at your options here:\n\n#{command_help_messages.join("\n")}" - } - end - end -end diff --git a/app/services/mattermost/commands/base_service.rb b/app/services/mattermost/commands/base_service.rb index 54d8fa088b8..16bb92b9d0d 100644 --- a/app/services/mattermost/commands/base_service.rb +++ b/app/services/mattermost/commands/base_service.rb @@ -1,59 +1,42 @@ module Mattermost module Commands class BaseService < ::BaseService - class << self - def triggered_by?(_) - raise NotImplementedError - end - - def available?(_) - raise NotImplementedError - end - - def help_message(_) - NotImplementedError - end - end - QUERY_LIMIT = 5 def execute - subcommand, args = parse_command + raise NotImplementedError + end - if subcommands.include?(subcommand) - send(subcommand, args) - else - nil - end + def available? + raise NotImplementedError + end + + def collection + raise NotImplementedError end private - # This method can only be used by a resource that has an iid. Also, the - # class should implement #collection itself. Probably project.resource - # would suffice - def show(args) - iid = args.first + def find_by_iid(iid) + resource = collection.find_by(iid: iid) - result = collection.find_by(iid: iid) - if readable?(result) - result - else - nil + readable?(resource) ? resource : nil + end + + def search + collection.search(query).limit(QUERY_LIMIT).select do |resource| + readable?(resource) end end - # Child class should implement #collection - def search(args) - query = args.join(' ') - - collection.search(query).limit(QUERY_LIMIT).select do |issuable| - readable?(issuable) - end + # params[:text] = issue search <search query> + def query + params[:text].split[2..-1].join(' ') end - def command - params[:text] + # params[:text] = 'mergerequest show 123' + def iid + params[:text].split[2] end end end diff --git a/app/services/mattermost/commands/issue_create_service.rb b/app/services/mattermost/commands/issue_create_service.rb new file mode 100644 index 00000000000..e52dad6f532 --- /dev/null +++ b/app/services/mattermost/commands/issue_create_service.rb @@ -0,0 +1,13 @@ +module Mattermost + module Commands + class IssueShowService < Mattermost::Commands::BaseService + def execute + return Mattermost::Messages::Issues.not_available unless available? + + issue = find_by_iid(iid) + + present issue + end + end + end +end diff --git a/app/services/mattermost/commands/issue_search_service.rb b/app/services/mattermost/commands/issue_search_service.rb new file mode 100644 index 00000000000..358a891ae60 --- /dev/null +++ b/app/services/mattermost/commands/issue_search_service.rb @@ -0,0 +1,11 @@ +module Mattermost + module Commands + class IssueShowService < IssueService + def execute + return Mattermost::Messages::Issues.not_available unless available? + + present search_results + end + end + end +end diff --git a/app/services/mattermost/commands/issue_service.rb b/app/services/mattermost/commands/issue_service.rb index 17407355547..9ce2fac455b 100644 --- a/app/services/mattermost/commands/issue_service.rb +++ b/app/services/mattermost/commands/issue_service.rb @@ -1,39 +1,8 @@ module Mattermost module Commands class IssueService < Mattermost::Commands::BaseService - class << self - def triggered_by?(command) - command == 'issue' - end - - def available?(project) - project.issues_enabled? && project.default_issues_tracker? - end - - def help_message(project) - return nil unless available?(project) - - message = "issue show <issue_id>" - message << "issue search <query>" - end - end - - private - - def create(_) - return nil unless can?(current_user, :create_issue, project) - - # We parse again as the previous split splits on continues whitespace - # per the ruby spec, but we loose information on where the new lines were - match = command.match(/\Aissue create (?<title>.*)\n*/) - title = match[:title] - description = match.post_match - - Issues::CreateService.new(project, current_user, title: title, description: description).execute - end - - def subcommands - %w[create search show] + def available? + project.issues_enabled? && project.default_issues_tracker? end def collection @@ -44,12 +13,8 @@ module Mattermost can?(current_user, :read_issue, issue) end - def parse_command - split = command.split - subcommand = split[1] - args = split[2..-1] - - [subcommand, args] + def present + Mattermost::Presenter.issue end end end diff --git a/app/services/mattermost/commands/issue_show_service.rb b/app/services/mattermost/commands/issue_show_service.rb new file mode 100644 index 00000000000..f8ca41b2992 --- /dev/null +++ b/app/services/mattermost/commands/issue_show_service.rb @@ -0,0 +1,12 @@ +module Mattermost + module Commands + class IssueShowService < IssueService + def execute + return Mattermost::Messages.not_available unless available? + + issue = find_by_iid(iid) + present issue + end + end + end +end diff --git a/app/services/mattermost/commands/merge_request_search_service.rb b/app/services/mattermost/commands/merge_request_search_service.rb new file mode 100644 index 00000000000..842719685e4 --- /dev/null +++ b/app/services/mattermost/commands/merge_request_search_service.rb @@ -0,0 +1,11 @@ +module Mattermost + module Commands + class MergeRequestService < Mattermost::Commands::BaseService + def execute + return Mattermost::Messages::MergeRequests.not_available unless available? + + Mattermost::Messages::IssuePresenter.present search_results + end + end + end +end diff --git a/app/services/mattermost/commands/merge_request_service.rb b/app/services/mattermost/commands/merge_request_service.rb index a27b3861b23..985ce649cac 100644 --- a/app/services/mattermost/commands/merge_request_service.rb +++ b/app/services/mattermost/commands/merge_request_service.rb @@ -1,27 +1,8 @@ module Mattermost module Commands class MergeRequestService < Mattermost::Commands::BaseService - class << self - def triggered_by?(command) - command == 'mergerequest' - end - - def available?(project) - project.merge_requests_enabled? - end - - def help_message(project) - return nil unless available?(project) - - message = "mergerequest show <merge request id>\n" - message << "mergerequest search <query>" - end - end - - private - - def subcommands - %w[show search] + def available? + project.issues_enabled? && project.default_issues_tracker? end def collection @@ -32,14 +13,8 @@ module Mattermost can?(current_user, :read_merge_request, project) end - # 'mergerequest show 123' => 'show', ['123'] - # 'mergerequest search my query' => 'search',['my', 'query'] - def parse_command - split = command.split - subcommand = split[1] - args = split[2..-1] - - [subcommand, args] + def present + Mattermost::Presenter.merge_request end end end diff --git a/app/services/mattermost/commands/merge_request_show_service.rb b/app/services/mattermost/commands/merge_request_show_service.rb new file mode 100644 index 00000000000..9d9cb0f50f6 --- /dev/null +++ b/app/services/mattermost/commands/merge_request_show_service.rb @@ -0,0 +1,12 @@ +module Mattermost + module Commands + class MergeRequestShowService < Mattermost::Commands::BaseService + def execute + return Mattermost::Messages.not_available unless available? + + merge_request = find_by_iid(iid) + present merge_request + end + end + end +end diff --git a/app/services/mattermost/slash_command_service.rb b/app/services/mattermost/slash_command_service.rb new file mode 100644 index 00000000000..fc028d78537 --- /dev/null +++ b/app/services/mattermost/slash_command_service.rb @@ -0,0 +1,28 @@ +module Mattermost + class SlashCommandService < BaseService + def self.command(command, sub_command, klass, help_message) + registry[command][sub_command] = { klass: klass, help_message: help_message } + end + + command 'issue', 'show', Mattermost::Commands::IssueShowService, 'issue show <id>' + command 'issue', 'search', Mattermost::Commands::IssueSearchService, 'issue search <query>' + command 'issue', 'create', Mattermost::Commands::IssueCreateService, 'issue create my title' + + command 'mergerequest', 'show', Mattermost::Commands::MergeRequestShowService, 'mergerequest show <id>' + command 'mergerequest', 'search', Mattermost::Commands::MergeRequestSearchService, 'mergerequest search <query>' + + def execute + service = registry[command][subcommand] + + return help_messages(registry) unless service.try(:available?) + + service.new(project, current_user, params).execute + end + + private + + def self.registry + @registry ||= Hash.new({}) + end + end +end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb new file mode 100644 index 00000000000..d8e3b3805f9 --- /dev/null +++ b/lib/mattermost/presenter.rb @@ -0,0 +1,84 @@ +module Mattermost + class Presenter + class << self + COMMAND_PREFIX = '/gitlab'.freeze + + def authorize_chat_name(params) + url = ChatNames::RequestService.new(service, params).execute + + { + response_type: :ephemeral, + message: "You are not authorized. Click this [link](#{url}) to authorize." + } + end + + # TODO figure out how I know which are available or not + def help_message(commands) + messages = ["Available commands:"] + + commands.each do |sub_command, attrs| + messages << "\t#{COMMAND_PREFIX} #{attrs[:help_message]}" + end + + { + response_type: :ephemeral, + text: messages.join("\n") + } + end + + def not_found + { + response_type: :ephemeral, + text: "404 not found! GitLab couldn't find what your were looking for! :boom:", + } + end + end + + attr_reader :result + + def initialize(result) + @result = result + end + + def present + if result.respond_to?(:count) + if result.count > 1 + return respond_collection(result) + elsif result.count == 0 + return not_found + else + result = result.first + end + end + + single_resource + end + + private + + def single_resource + message = title(resource) + message << "\n\n#{resource.description}" if resource.description + + { + response_type: :in_channel, + text: message + } + end + + def multiple_resources(resources) + message = "Multiple results were found:\n" + message << resource.map { |resource| " #{title(resource)}" }.join("\n") + + { + response_type: :ephemeral, + text: message + } + end + + def title(resource) + url = url_for([resource.project.namespace.becomes(Namespace), resource.project, resource]) + "### [#{resource.to_reference} #{resource.title}](#{url})" + end + end +end diff --git a/spec/services/mattermost/command_service_spec.rb b/spec/services/mattermost/command_service_spec.rb deleted file mode 100644 index 35051c09f8d..00000000000 --- a/spec/services/mattermost/command_service_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe Mattermost::CommandService, service: true do - let(:project) { build(:project) } - let(:user) { build(:user) } - let(:params) { { text: 'issue show 1' } } - - subject { described_class.new(project, user, params).execute } - - describe '#execute' do - context 'no user could be found' do - let(:user) { nil } - - it 'asks the user to introduce him/herself' do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with 'Hi there!' - end - end - - context 'no project could be found' do - it 'shows a 404 not found message' do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with '404 not found!' - end - end - - context 'the user has access to the project' do - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.team << [user, :master] - end - - context 'no command service is triggered' do - let(:params) { { text: 'unknown_command' } } - - it 'shows the help messages' do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with 'Sadly, the used command' - end - end - - context 'a valid command is executed' do - let(:issue) { create(:issue, project: project) } - let(:params) { { text: "issue show #{issue.iid}" } } - - it 'a resource is presented to the user' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match issue.title - end - end - end - end -end diff --git a/spec/services/mattermost/slash_command_service_spec.rb b/spec/services/mattermost/slash_command_service_spec.rb new file mode 100644 index 00000000000..e1bfd073ef4 --- /dev/null +++ b/spec/services/mattermost/slash_command_service_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Mattermost::SlashCommandService, service: true do + let(:project) { build(:project) } + let(:user) { build(:user) } + let(:params) { { text: 'issue show 1' } } + + subject { described_class.new(project, user, params).execute } + + describe '#execute' do + context 'no command service is triggered' do + let(:params) { { text: 'unknown_command' } } + + it 'shows the help messages' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with 'Sadly, the used command' + end + end + + context 'a valid command is executed' do + let(:issue) { create(:issue, project: project) } + let(:params) { { text: "issue show #{issue.iid}" } } + + it 'a resource is presented to the user' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match issue.title + end + end + end +end From 8c8bc07d32f1103bb7996b499ead6ad6eb5bd337 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 15 Nov 2016 21:50:27 +0100 Subject: [PATCH 212/302] Refactor and test Slash commands This is the structure Kamil proposed, which leaves us with a bunch of smaller classes. This commits deletes outdated files and tests everything from the SlashCommandService and down (child classes and subcommands) --- .../mattermost/commands/base_service.rb | 8 +- .../commands/issue_create_service.rb | 16 +++- .../commands/issue_search_service.rb | 4 +- .../mattermost/commands/issue_service.rb | 6 +- .../mattermost/commands/issue_show_service.rb | 5 +- .../commands/merge_request_search_service.rb | 6 +- .../commands/merge_request_service.rb | 8 +- .../commands/merge_request_show_service.rb | 7 +- .../mattermost/slash_command_service.rb | 19 ++++- lib/mattermost/presenter.rb | 78 +++++++++-------- .../commands/issue_create_service_spec.rb | 35 ++++++++ .../commands/issue_search_service_spec.rb | 39 +++++++++ .../mattermost/commands/issue_service_spec.rb | 83 ------------------- .../commands/issue_show_service_spec.rb | 31 +++++++ .../merge_request_search_service_spec.rb | 30 +++++++ .../commands/merge_request_service_spec.rb | 68 --------------- .../merge_request_show_service_spec.rb | 30 +++++++ .../mattermost/slash_command_service_spec.rb | 21 ++--- 18 files changed, 254 insertions(+), 240 deletions(-) create mode 100644 spec/services/mattermost/commands/issue_create_service_spec.rb create mode 100644 spec/services/mattermost/commands/issue_search_service_spec.rb delete mode 100644 spec/services/mattermost/commands/issue_service_spec.rb create mode 100644 spec/services/mattermost/commands/issue_show_service_spec.rb create mode 100644 spec/services/mattermost/commands/merge_request_search_service_spec.rb delete mode 100644 spec/services/mattermost/commands/merge_request_service_spec.rb create mode 100644 spec/services/mattermost/commands/merge_request_show_service_spec.rb diff --git a/app/services/mattermost/commands/base_service.rb b/app/services/mattermost/commands/base_service.rb index 16bb92b9d0d..c6bfd7c9ab4 100644 --- a/app/services/mattermost/commands/base_service.rb +++ b/app/services/mattermost/commands/base_service.rb @@ -17,13 +17,17 @@ module Mattermost private - def find_by_iid(iid) + def present(resource) + Mattermost::Presenter.present(resource) + end + + def find_by_iid resource = collection.find_by(iid: iid) readable?(resource) ? resource : nil end - def search + def search_results collection.search(query).limit(QUERY_LIMIT).select do |resource| readable?(resource) end diff --git a/app/services/mattermost/commands/issue_create_service.rb b/app/services/mattermost/commands/issue_create_service.rb index e52dad6f532..db3f868fc09 100644 --- a/app/services/mattermost/commands/issue_create_service.rb +++ b/app/services/mattermost/commands/issue_create_service.rb @@ -1,12 +1,20 @@ module Mattermost module Commands - class IssueShowService < Mattermost::Commands::BaseService + class IssueCreateService < IssueService def execute - return Mattermost::Messages::Issues.not_available unless available? + title, description = parse_command - issue = find_by_iid(iid) + present Issues::CreateService.new(project, current_user, title: title, description: description).execute + end - present issue + private + + def parse_command + match = params[:text].match(/\Aissue create (?<title>.*)\n*/) + title = match[:title] + description = match.post_match + + [title, description] end end end diff --git a/app/services/mattermost/commands/issue_search_service.rb b/app/services/mattermost/commands/issue_search_service.rb index 358a891ae60..072cb8e2590 100644 --- a/app/services/mattermost/commands/issue_search_service.rb +++ b/app/services/mattermost/commands/issue_search_service.rb @@ -1,9 +1,7 @@ module Mattermost module Commands - class IssueShowService < IssueService + class IssueSearchService < IssueService def execute - return Mattermost::Messages::Issues.not_available unless available? - present search_results end end diff --git a/app/services/mattermost/commands/issue_service.rb b/app/services/mattermost/commands/issue_service.rb index 9ce2fac455b..d27dcda21c4 100644 --- a/app/services/mattermost/commands/issue_service.rb +++ b/app/services/mattermost/commands/issue_service.rb @@ -1,7 +1,7 @@ module Mattermost module Commands class IssueService < Mattermost::Commands::BaseService - def available? + def self.available?(project) project.issues_enabled? && project.default_issues_tracker? end @@ -12,10 +12,6 @@ module Mattermost def readable?(issue) can?(current_user, :read_issue, issue) end - - def present - Mattermost::Presenter.issue - end end end end diff --git a/app/services/mattermost/commands/issue_show_service.rb b/app/services/mattermost/commands/issue_show_service.rb index f8ca41b2992..100b2780d0e 100644 --- a/app/services/mattermost/commands/issue_show_service.rb +++ b/app/services/mattermost/commands/issue_show_service.rb @@ -2,10 +2,7 @@ module Mattermost module Commands class IssueShowService < IssueService def execute - return Mattermost::Messages.not_available unless available? - - issue = find_by_iid(iid) - present issue + present find_by_iid end end end diff --git a/app/services/mattermost/commands/merge_request_search_service.rb b/app/services/mattermost/commands/merge_request_search_service.rb index 842719685e4..f675320ddae 100644 --- a/app/services/mattermost/commands/merge_request_search_service.rb +++ b/app/services/mattermost/commands/merge_request_search_service.rb @@ -1,10 +1,8 @@ module Mattermost module Commands - class MergeRequestService < Mattermost::Commands::BaseService + class MergeRequestSearchService < MergeRequestService def execute - return Mattermost::Messages::MergeRequests.not_available unless available? - - Mattermost::Messages::IssuePresenter.present search_results + present search_results end end end diff --git a/app/services/mattermost/commands/merge_request_service.rb b/app/services/mattermost/commands/merge_request_service.rb index 985ce649cac..8b81c9bbfd9 100644 --- a/app/services/mattermost/commands/merge_request_service.rb +++ b/app/services/mattermost/commands/merge_request_service.rb @@ -1,8 +1,8 @@ module Mattermost module Commands class MergeRequestService < Mattermost::Commands::BaseService - def available? - project.issues_enabled? && project.default_issues_tracker? + def self.available?(project) + project.merge_requests_enabled? end def collection @@ -12,10 +12,6 @@ module Mattermost def readable?(_) can?(current_user, :read_merge_request, project) end - - def present - Mattermost::Presenter.merge_request - end end end end diff --git a/app/services/mattermost/commands/merge_request_show_service.rb b/app/services/mattermost/commands/merge_request_show_service.rb index 9d9cb0f50f6..c5b2850169a 100644 --- a/app/services/mattermost/commands/merge_request_show_service.rb +++ b/app/services/mattermost/commands/merge_request_show_service.rb @@ -1,11 +1,8 @@ module Mattermost module Commands - class MergeRequestShowService < Mattermost::Commands::BaseService + class MergeRequestShowService < MergeRequestService def execute - return Mattermost::Messages.not_available unless available? - - merge_request = find_by_iid(iid) - present merge_request + present find_by_iid end end end diff --git a/app/services/mattermost/slash_command_service.rb b/app/services/mattermost/slash_command_service.rb index fc028d78537..0e74c929b9b 100644 --- a/app/services/mattermost/slash_command_service.rb +++ b/app/services/mattermost/slash_command_service.rb @@ -1,5 +1,9 @@ module Mattermost class SlashCommandService < BaseService + def self.registry + @registry ||= Hash.new({}) + end + def self.command(command, sub_command, klass, help_message) registry[command][sub_command] = { klass: klass, help_message: help_message } end @@ -12,17 +16,24 @@ module Mattermost command 'mergerequest', 'search', Mattermost::Commands::MergeRequestSearchService, 'mergerequest search <query>' def execute - service = registry[command][subcommand] + command, subcommand = parse_command - return help_messages(registry) unless service.try(:available?) + #TODO think how to do this to support ruby 2.1 + service = registry.dig(command, subcommand, :klass) + + return help_messages(registry) unless service.try(:available?, project) service.new(project, current_user, params).execute end private - def self.registry - @registry ||= Hash.new({}) + def parse_command + params[:text].split.first(2) + end + + def registry + self.class.registry end end end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index d8e3b3805f9..e1502e3f9ba 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -32,53 +32,59 @@ module Mattermost text: "404 not found! GitLab couldn't find what your were looking for! :boom:", } end - end - attr_reader :result + def present(resource) + return not_found unless resource - def initialize(result) - @result = result - end - - def present - if result.respond_to?(:count) - if result.count > 1 - return respond_collection(result) - elsif result.count == 0 - return not_found - else - result = result.first + if resource.respond_to?(:count) + if resource.count > 1 + return multiple_resources(resource) + elsif resource.count == 0 + return not_found + else + resource = resource.first + end end + + single_resource(resource) end - single_resource - end + private - private + def single_resource(resource) + message = title(resource) + message << "\n\n#{resource.description}" if resource.description - def single_resource - message = title(resource) - message << "\n\n#{resource.description}" if resource.description + { + response_type: :in_channel, + text: message + } + end - { - response_type: :in_channel, - text: message - } - end + def multiple_resources(resources) + message = "Multiple results were found:\n" + message << resources.map { |resource| " #{title(resource)}" }.join("\n") - def multiple_resources(resources) - message = "Multiple results were found:\n" - message << resource.map { |resource| " #{title(resource)}" }.join("\n") + { + response_type: :ephemeral, + text: message + } + end - { - response_type: :ephemeral, - text: message - } - end + def title(resource) + "### [#{resource.to_reference} #{resource.title}](#{url(resource)})" + end - def title(resource) - url = url_for([resource.project.namespace.becomes(Namespace), resource.project, resource]) - "### [#{resource.to_reference} #{resource.title}](#{url})" + def url(resource) + helper = Rails.application.routes.url_helpers + + case resource + when Issue + helper.namespace_project_issue_url(resource.project.namespace.becomes(Namespace), resource.project, resource) + when MergeRequest + helper.namespace_project_merge_request_url(resource.project.namespace.becomes(Namespace), resource.project, resource) + end + end end end end diff --git a/spec/services/mattermost/commands/issue_create_service_spec.rb b/spec/services/mattermost/commands/issue_create_service_spec.rb new file mode 100644 index 00000000000..5ed86606b8f --- /dev/null +++ b/spec/services/mattermost/commands/issue_create_service_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Mattermost::Commands::IssueCreateService, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:params) { { text: "issue create bird is the word" } } + + before { project.team << [user, :master] } + + subject { described_class.new(project, user, params).execute } + + context 'without description' do + it 'creates the issue' do + expect do + subject # this trigger the execution + end.to change { project.issues.count }.by(1) + + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match 'bird is the word' + end + end + + context 'with description' do + let(:description) { "Surfin bird" } + let(:params) { { text: "issue create The bird is the word\n#{description}" } } + + before { subject } + + it 'creates the issue with description' do + expect(Issue.last.description).to eq description + end + end + end +end diff --git a/spec/services/mattermost/commands/issue_search_service_spec.rb b/spec/services/mattermost/commands/issue_search_service_spec.rb new file mode 100644 index 00000000000..21934dfcdd0 --- /dev/null +++ b/spec/services/mattermost/commands/issue_search_service_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Mattermost::Commands::IssueSearchService, service: true do + describe '#execute' do + let!(:issue) { create(:issue, title: 'The bird is the word') } + let(:project) { issue.project } + let(:user) { issue.author } + let(:params) { { text: "issue search bird is the" } } + + before { project.team << [user, :master] } + + subject { described_class.new(project, user, params).execute } + + context 'without results' do + let(:params) { { text: "issue search no results for this one" } } + + it "returns nil" do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + + context 'with 1 result' do + it 'returns the issue' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match issue.title + end + end + + context 'with 2 or more results' do + let!(:issue2) { create(:issue, project: project, title: 'bird is the word!') } + + it 'returns multiple resources' do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with 'Multiple results were found' + end + end + end +end diff --git a/spec/services/mattermost/commands/issue_service_spec.rb b/spec/services/mattermost/commands/issue_service_spec.rb deleted file mode 100644 index 5ef363274ad..00000000000 --- a/spec/services/mattermost/commands/issue_service_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Commands::IssueService do - let(:project) { create(:project) } - let(:issue ) { create(:issue, :confidential, title: 'Bird is the word', project: project) } - let(:user) { issue.author } - - subject { described_class.new(project, user, params).execute } - - before do - project.team << [user, :developer] - end - - describe '#execute' do - context 'show as subcommand' do - context 'issue can be found' do - let(:params) { { text: "issue show #{issue.iid}" } } - - it 'returns the merge request' do - expect(subject).to eq issue - end - - context 'the user has no access' do - let(:non_member) { create(:user) } - subject { described_class.new(project, non_member, params).execute } - - it 'returns nil' do - expect(subject).to eq nil - end - end - end - - context 'issue can not be found' do - let(:params) { { text: 'issue show 12345' } } - - it 'returns nil' do - expect(subject).to eq nil - end - end - end - - context 'search as a subcommand' do - context 'with results' do - let(:params) { { text: "issue search is the word" } } - - it 'returns the issue' do - expect(subject).to eq [issue] - end - end - - context 'without results' do - let(:params) { { text: 'issue search mepmep' } } - - it 'returns an empty collection' do - expect(subject).to eq [] - end - end - end - - context 'create as subcommand' do - let(:title) { 'my new issue' } - let(:params) { { text: "issue create #{title}" } } - - it 'return the new issue' do - expect(subject).to be_a Issue - end - - it 'creates a new issue' do - expect { subject }.to change { Issue.count }.by(1) - end - end - end - - describe 'help_message' do - context 'issues are disabled' do - it 'returns nil' do - allow(described_class).to receive(:available?).and_return false - - expect(described_class.help_message(project)).to eq nil - end - end - end -end diff --git a/spec/services/mattermost/commands/issue_show_service_spec.rb b/spec/services/mattermost/commands/issue_show_service_spec.rb new file mode 100644 index 00000000000..7578cd3a22c --- /dev/null +++ b/spec/services/mattermost/commands/issue_show_service_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Mattermost::Commands::IssueShowService, service: true do + describe '#execute' do + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:user) { issue.author } + let(:params) { { text: "issue show #{issue.iid}" } } + + before { project.team << [user, :master] } + + subject { described_class.new(project, user, params).execute } + + context 'the issue exists' do + it 'returns the issue' do + + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match issue.title + end + end + + context 'the issue does not exist' do + let(:params) { { text: "issue show 12345" } } + + it "returns nil" do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + end +end diff --git a/spec/services/mattermost/commands/merge_request_search_service_spec.rb b/spec/services/mattermost/commands/merge_request_search_service_spec.rb new file mode 100644 index 00000000000..5212dc206a6 --- /dev/null +++ b/spec/services/mattermost/commands/merge_request_search_service_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Mattermost::Commands::MergeRequestSearchService, service: true do + describe '#execute' do + let!(:merge_request) { create(:merge_request, title: 'The bird is the word') } + let(:project) { merge_request.source_project } + let(:user) { merge_request.author } + let(:params) { { text: "mergerequest search #{merge_request.title}" } } + + before { project.team << [user, :master] } + + subject { described_class.new(project, user, params).execute } + + context 'the merge request exists' do + it 'returns the merge request' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match merge_request.title + end + end + + context 'no results can be found' do + let(:params) { { text: "mergerequest search 12345" } } + + it "returns a 404 message" do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + end +end diff --git a/spec/services/mattermost/commands/merge_request_service_spec.rb b/spec/services/mattermost/commands/merge_request_service_spec.rb deleted file mode 100644 index 39381520a99..00000000000 --- a/spec/services/mattermost/commands/merge_request_service_spec.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Commands::MergeRequestService do - let(:project) { create(:project, :private) } - let(:merge_request) { create(:merge_request, title: 'Bird is the word', source_project: project) } - let(:user) { merge_request.author } - - subject { described_class.new(project, user, params).execute } - - before do - project.team << [user, :developer] - end - - context 'show as subcommand' do - context 'merge request can be found' do - let(:params) { { text: "mergerequest show #{merge_request.iid}" } } - - it 'returns the merge request' do - expect(subject).to eq merge_request - end - - context 'the user has no access' do - let(:non_member) { create(:user) } - subject { described_class.new(project, non_member, params).execute } - - it 'returns nil' do - expect(subject).to eq nil - end - end - end - - context 'merge request can not be found' do - let(:params) { { text: 'mergerequest show 12345' } } - - it 'returns nil' do - expect(subject).to eq nil - end - end - end - - context 'search as a subcommand' do - context 'with results' do - let(:params) { { text: "mergerequest search is the word" } } - - it 'returns the merge_request' do - expect(subject).to eq [merge_request] - end - end - - context 'without results' do - let(:params) { { text: 'mergerequest search mepmep' } } - - it 'returns an empty collection' do - expect(subject).to eq [] - end - end - end - - describe 'help_message' do - context 'issues are disabled' do - it 'returns nil' do - allow(described_class).to receive(:available?).and_return false - - expect(described_class.help_message(project)).to eq nil - end - end - end -end diff --git a/spec/services/mattermost/commands/merge_request_show_service_spec.rb b/spec/services/mattermost/commands/merge_request_show_service_spec.rb new file mode 100644 index 00000000000..688737df0f5 --- /dev/null +++ b/spec/services/mattermost/commands/merge_request_show_service_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Mattermost::Commands::MergeRequestShowService, service: true do + describe '#execute' do + let!(:merge_request) { create(:merge_request) } + let(:project) { merge_request.source_project } + let(:user) { merge_request.author } + let(:params) { { text: "mergerequest show #{merge_request.iid}" } } + + before { project.team << [user, :master] } + + subject { described_class.new(project, user, params).execute } + + context 'the merge request exists' do + it 'returns the merge request' do + expect(subject[:response_type]).to be :in_channel + expect(subject[:text]).to match merge_request.title + end + end + + context 'the merge request does not exist' do + let(:params) { { text: "mergerequest show 12345" } } + + it "returns nil" do + expect(subject[:response_type]).to be :ephemeral + expect(subject[:text]).to start_with '404 not found!' + end + end + end +end diff --git a/spec/services/mattermost/slash_command_service_spec.rb b/spec/services/mattermost/slash_command_service_spec.rb index e1bfd073ef4..14c2d9b7c37 100644 --- a/spec/services/mattermost/slash_command_service_spec.rb +++ b/spec/services/mattermost/slash_command_service_spec.rb @@ -7,23 +7,12 @@ describe Mattermost::SlashCommandService, service: true do subject { described_class.new(project, user, params).execute } - describe '#execute' do - context 'no command service is triggered' do - let(:params) { { text: 'unknown_command' } } + xdescribe '#execute' do + context 'when issue show is triggered' do + it 'calls IssueShowService' do + expect_any_instance_of(Mattermost::Commands::IssueShowService).to receive(:new).with(project, user, params) - it 'shows the help messages' do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with 'Sadly, the used command' - end - end - - context 'a valid command is executed' do - let(:issue) { create(:issue, project: project) } - let(:params) { { text: "issue show #{issue.iid}" } } - - it 'a resource is presented to the user' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match issue.title + subject end end end From 1b4fdb9893af28606b7594ee656438c7ef21e9d8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 16 Nov 2016 18:28:38 +0100 Subject: [PATCH 213/302] Rename from service, and move to lib/gitlab --- .../mattermost/commands/base_service.rb | 47 ---------------- .../commands/issue_create_service.rb | 21 ------- .../commands/issue_search_service.rb | 9 --- .../mattermost/commands/issue_show_service.rb | 9 --- .../commands/merge_request_search_service.rb | 9 --- .../commands/merge_request_show_service.rb | 9 --- .../mattermost/slash_command_service.rb | 39 ------------- lib/gitlab/chat_commands/base_command.rb | 55 +++++++++++++++++++ lib/gitlab/chat_commands/command.rb | 49 +++++++++++++++++ .../gitlab/chat_commands/issue_command.rb | 6 +- lib/gitlab/chat_commands/issue_create.rb | 16 ++++++ lib/gitlab/chat_commands/issue_search.rb | 17 ++++++ lib/gitlab/chat_commands/issue_show.rb | 17 ++++++ .../chat_commands/merge_request_command.rb | 6 +- .../chat_commands/merge_request_search.rb | 17 ++++++ .../chat_commands/merge_request_show.rb | 17 ++++++ lib/mattermost/presenter.rb | 7 +-- .../gitlab/chat_commands/command_spec.rb} | 6 +- .../chat_commands/issue_create_spec.rb} | 8 +-- .../chat_commands/issue_search_spec.rb} | 8 +-- .../gitlab/chat_commands/issue_show_spec.rb} | 9 ++- .../merge_request_search_spec.rb} | 14 +++-- .../chat_commands/merge_request_show_spec.rb} | 15 +++-- 23 files changed, 233 insertions(+), 177 deletions(-) delete mode 100644 app/services/mattermost/commands/base_service.rb delete mode 100644 app/services/mattermost/commands/issue_create_service.rb delete mode 100644 app/services/mattermost/commands/issue_search_service.rb delete mode 100644 app/services/mattermost/commands/issue_show_service.rb delete mode 100644 app/services/mattermost/commands/merge_request_search_service.rb delete mode 100644 app/services/mattermost/commands/merge_request_show_service.rb delete mode 100644 app/services/mattermost/slash_command_service.rb create mode 100644 lib/gitlab/chat_commands/base_command.rb create mode 100644 lib/gitlab/chat_commands/command.rb rename app/services/mattermost/commands/issue_service.rb => lib/gitlab/chat_commands/issue_command.rb (73%) create mode 100644 lib/gitlab/chat_commands/issue_create.rb create mode 100644 lib/gitlab/chat_commands/issue_search.rb create mode 100644 lib/gitlab/chat_commands/issue_show.rb rename app/services/mattermost/commands/merge_request_service.rb => lib/gitlab/chat_commands/merge_request_command.rb (71%) create mode 100644 lib/gitlab/chat_commands/merge_request_search.rb create mode 100644 lib/gitlab/chat_commands/merge_request_show.rb rename spec/{services/mattermost/slash_command_service_spec.rb => lib/gitlab/chat_commands/command_spec.rb} (74%) rename spec/{services/mattermost/commands/issue_create_service_spec.rb => lib/gitlab/chat_commands/issue_create_spec.rb} (70%) rename spec/{services/mattermost/commands/issue_search_service_spec.rb => lib/gitlab/chat_commands/issue_search_spec.rb} (76%) rename spec/{services/mattermost/commands/issue_show_service_spec.rb => lib/gitlab/chat_commands/issue_show_spec.rb} (69%) rename spec/{services/mattermost/commands/merge_request_search_service_spec.rb => lib/gitlab/chat_commands/merge_request_search_spec.rb} (60%) rename spec/{services/mattermost/commands/merge_request_show_service_spec.rb => lib/gitlab/chat_commands/merge_request_show_spec.rb} (56%) diff --git a/app/services/mattermost/commands/base_service.rb b/app/services/mattermost/commands/base_service.rb deleted file mode 100644 index c6bfd7c9ab4..00000000000 --- a/app/services/mattermost/commands/base_service.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Mattermost - module Commands - class BaseService < ::BaseService - QUERY_LIMIT = 5 - - def execute - raise NotImplementedError - end - - def available? - raise NotImplementedError - end - - def collection - raise NotImplementedError - end - - private - - def present(resource) - Mattermost::Presenter.present(resource) - end - - def find_by_iid - resource = collection.find_by(iid: iid) - - readable?(resource) ? resource : nil - end - - def search_results - collection.search(query).limit(QUERY_LIMIT).select do |resource| - readable?(resource) - end - end - - # params[:text] = issue search <search query> - def query - params[:text].split[2..-1].join(' ') - end - - # params[:text] = 'mergerequest show 123' - def iid - params[:text].split[2] - end - end - end -end diff --git a/app/services/mattermost/commands/issue_create_service.rb b/app/services/mattermost/commands/issue_create_service.rb deleted file mode 100644 index db3f868fc09..00000000000 --- a/app/services/mattermost/commands/issue_create_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Mattermost - module Commands - class IssueCreateService < IssueService - def execute - title, description = parse_command - - present Issues::CreateService.new(project, current_user, title: title, description: description).execute - end - - private - - def parse_command - match = params[:text].match(/\Aissue create (?<title>.*)\n*/) - title = match[:title] - description = match.post_match - - [title, description] - end - end - end -end diff --git a/app/services/mattermost/commands/issue_search_service.rb b/app/services/mattermost/commands/issue_search_service.rb deleted file mode 100644 index 072cb8e2590..00000000000 --- a/app/services/mattermost/commands/issue_search_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Mattermost - module Commands - class IssueSearchService < IssueService - def execute - present search_results - end - end - end -end diff --git a/app/services/mattermost/commands/issue_show_service.rb b/app/services/mattermost/commands/issue_show_service.rb deleted file mode 100644 index 100b2780d0e..00000000000 --- a/app/services/mattermost/commands/issue_show_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Mattermost - module Commands - class IssueShowService < IssueService - def execute - present find_by_iid - end - end - end -end diff --git a/app/services/mattermost/commands/merge_request_search_service.rb b/app/services/mattermost/commands/merge_request_search_service.rb deleted file mode 100644 index f675320ddae..00000000000 --- a/app/services/mattermost/commands/merge_request_search_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Mattermost - module Commands - class MergeRequestSearchService < MergeRequestService - def execute - present search_results - end - end - end -end diff --git a/app/services/mattermost/commands/merge_request_show_service.rb b/app/services/mattermost/commands/merge_request_show_service.rb deleted file mode 100644 index c5b2850169a..00000000000 --- a/app/services/mattermost/commands/merge_request_show_service.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Mattermost - module Commands - class MergeRequestShowService < MergeRequestService - def execute - present find_by_iid - end - end - end -end diff --git a/app/services/mattermost/slash_command_service.rb b/app/services/mattermost/slash_command_service.rb deleted file mode 100644 index 0e74c929b9b..00000000000 --- a/app/services/mattermost/slash_command_service.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Mattermost - class SlashCommandService < BaseService - def self.registry - @registry ||= Hash.new({}) - end - - def self.command(command, sub_command, klass, help_message) - registry[command][sub_command] = { klass: klass, help_message: help_message } - end - - command 'issue', 'show', Mattermost::Commands::IssueShowService, 'issue show <id>' - command 'issue', 'search', Mattermost::Commands::IssueSearchService, 'issue search <query>' - command 'issue', 'create', Mattermost::Commands::IssueCreateService, 'issue create my title' - - command 'mergerequest', 'show', Mattermost::Commands::MergeRequestShowService, 'mergerequest show <id>' - command 'mergerequest', 'search', Mattermost::Commands::MergeRequestSearchService, 'mergerequest search <query>' - - def execute - command, subcommand = parse_command - - #TODO think how to do this to support ruby 2.1 - service = registry.dig(command, subcommand, :klass) - - return help_messages(registry) unless service.try(:available?, project) - - service.new(project, current_user, params).execute - end - - private - - def parse_command - params[:text].split.first(2) - end - - def registry - self.class.registry - end - end -end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb new file mode 100644 index 00000000000..13dc2a0f3e8 --- /dev/null +++ b/lib/gitlab/chat_commands/base_command.rb @@ -0,0 +1,55 @@ +module Gitlab + module ChatCommands + class BaseCommand + QUERY_LIMIT = 5 + + def self.match(_) + raise NotImplementedError + end + + def self.help_message + raise NotImplementedError + end + + def self.available?(_) + raise NotImplementedError + end + + def execute(_) + raise NotImplementedError + end + + def collection + raise NotImplementedError + end + + attr_accessor :project, :current_user, :params + + def initialize(project, user, params = {}) + @project, @current_user, @params = project, user, params.dup + end + + private + + def can?(object, action, subject) + Ability.allowed?(object, action, subject) + end + + def present(resource) + Mattermost::Presenter.present(resource) + end + + def find_by_iid(iid) + resource = collection.find_by(iid: iid) + + readable?(resource) ? resource : nil + end + + def search_results(query) + collection.search(query).limit(QUERY_LIMIT).select do |resource| + readable?(resource) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb new file mode 100644 index 00000000000..06d09ab0e24 --- /dev/null +++ b/lib/gitlab/chat_commands/command.rb @@ -0,0 +1,49 @@ +module Gitlab + module ChatCommands + class Command < BaseCommand + COMMANDS = [ + Gitlab::ChatCommands::IssueShow, + Gitlab::ChatCommands::IssueSearch, + Gitlab::ChatCommands::IssueCreate, + + Gitlab::ChatCommands::MergeRequestShow, + Gitlab::ChatCommands::MergeRequestSearch, + ].freeze + + def execute + klass, match = fetch_klass + + return help(help_messages) unless klass.try(:available?, project) + + klass.new(project, current_user, params).execute(match) + end + + private + + def fetch_klass + match = nil + service = COMMANDS.find do |klass| + if klass.available?(project) + false + else + match = klass.match(command) + end + end + + [service, match] + end + + def help_messages + COMMANDS.map do |klass| + next unless klass.available?(project) + + klass.help_message + end.compact + end + + def command + params[:text] + end + end + end +end diff --git a/app/services/mattermost/commands/issue_service.rb b/lib/gitlab/chat_commands/issue_command.rb similarity index 73% rename from app/services/mattermost/commands/issue_service.rb rename to lib/gitlab/chat_commands/issue_command.rb index d27dcda21c4..2426b3714b7 100644 --- a/app/services/mattermost/commands/issue_service.rb +++ b/lib/gitlab/chat_commands/issue_command.rb @@ -1,6 +1,6 @@ -module Mattermost - module Commands - class IssueService < Mattermost::Commands::BaseService +module Gitlab + module ChatCommands + class IssueCommand < BaseCommand def self.available?(project) project.issues_enabled? && project.default_issues_tracker? end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb new file mode 100644 index 00000000000..c424e845402 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -0,0 +1,16 @@ +module Gitlab + module ChatCommands + class IssueCreate < BaseCommand + def self.match(text) + /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>.*)\z/.match(text) + end + + def execute(match) + title = match[:title] + description = match[:description] + + present Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb new file mode 100644 index 00000000000..4169e2a7a88 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class IssueSearch < IssueCommand + def self.match(text) + /\Aissue\s+search\s+(?<query>.*)/.match(text) + end + + def self.help_message + "issue search <query>" + end + + def execute(match) + present search_results(match[:query]) + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb new file mode 100644 index 00000000000..e5530df31cc --- /dev/null +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class IssueShow < IssueCommand + def self.match(text) + /\Aissue\s+show\s+(?<iid>\d+)/.match(text) + end + + def self.help_message + "issue show <id>" + end + + def execute(match) + present find_by_iid(match[:iid]) + end + end + end +end diff --git a/app/services/mattermost/commands/merge_request_service.rb b/lib/gitlab/chat_commands/merge_request_command.rb similarity index 71% rename from app/services/mattermost/commands/merge_request_service.rb rename to lib/gitlab/chat_commands/merge_request_command.rb index 8b81c9bbfd9..e0f69a49afd 100644 --- a/app/services/mattermost/commands/merge_request_service.rb +++ b/lib/gitlab/chat_commands/merge_request_command.rb @@ -1,6 +1,6 @@ -module Mattermost - module Commands - class MergeRequestService < Mattermost::Commands::BaseService +module Gitlab + module ChatCommands + class MergeRequestCommand < BaseCommand def self.available?(project) project.merge_requests_enabled? end diff --git a/lib/gitlab/chat_commands/merge_request_search.rb b/lib/gitlab/chat_commands/merge_request_search.rb new file mode 100644 index 00000000000..caecb1a788e --- /dev/null +++ b/lib/gitlab/chat_commands/merge_request_search.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class MergeRequestSearch < MergeRequestCommand + def self.match(text) + /\Amergerequest\s+search\s+(?<query>.*)/.match(text) + end + + def self.help_message + "mergerequest search <query>" + end + + def execute(match) + present search_results(match[:query]) + end + end + end +end diff --git a/lib/gitlab/chat_commands/merge_request_show.rb b/lib/gitlab/chat_commands/merge_request_show.rb new file mode 100644 index 00000000000..7ed5445e4c2 --- /dev/null +++ b/lib/gitlab/chat_commands/merge_request_show.rb @@ -0,0 +1,17 @@ +module Gitlab + module ChatCommands + class MergeRequestShow < MergeRequestCommand + def self.match(text) + /\Amergerequest\s+show\s+(?<iid>\d+)/.match(text) + end + + def self.help_message + "mergerequest show <id>" + end + + def execute(match) + present find_by_iid(match[:iid]) + end + end + end +end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index e1502e3f9ba..3db55d6bd51 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -12,12 +12,11 @@ module Mattermost } end - # TODO figure out how I know which are available or not - def help_message(commands) + def help(messages) messages = ["Available commands:"] - commands.each do |sub_command, attrs| - messages << "\t#{COMMAND_PREFIX} #{attrs[:help_message]}" + messages.each do |messsage| + messages << "- #{message}" end { diff --git a/spec/services/mattermost/slash_command_service_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb similarity index 74% rename from spec/services/mattermost/slash_command_service_spec.rb rename to spec/lib/gitlab/chat_commands/command_spec.rb index 14c2d9b7c37..d28349ff1d8 100644 --- a/spec/services/mattermost/slash_command_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Mattermost::SlashCommandService, service: true do - let(:project) { build(:project) } - let(:user) { build(:user) } +describe Gitlab::ChatCommands::Command, service: true do + let(:project) { create(:project) } + let(:user) { create(:user) } let(:params) { { text: 'issue show 1' } } subject { described_class.new(project, user, params).execute } diff --git a/spec/services/mattermost/commands/issue_create_service_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb similarity index 70% rename from spec/services/mattermost/commands/issue_create_service_spec.rb rename to spec/lib/gitlab/chat_commands/issue_create_spec.rb index 5ed86606b8f..184c09708a4 100644 --- a/spec/services/mattermost/commands/issue_create_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Mattermost::Commands::IssueCreateService, service: true do +describe Gitlab::ChatCommands::IssueCreate, service: true do describe '#execute' do let(:project) { create(:empty_project) } let(:user) { create(:user) } - let(:params) { { text: "issue create bird is the word" } } + let(:regex_match) { described_class.match("issue create bird is the word") } before { project.team << [user, :master] } - subject { described_class.new(project, user, params).execute } + subject { described_class.new(project, user).execute(regex_match) } context 'without description' do it 'creates the issue' do @@ -23,7 +23,7 @@ describe Mattermost::Commands::IssueCreateService, service: true do context 'with description' do let(:description) { "Surfin bird" } - let(:params) { { text: "issue create The bird is the word\n#{description}" } } + let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } before { subject } diff --git a/spec/services/mattermost/commands/issue_search_service_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb similarity index 76% rename from spec/services/mattermost/commands/issue_search_service_spec.rb rename to spec/lib/gitlab/chat_commands/issue_search_spec.rb index 21934dfcdd0..3e54333528a 100644 --- a/spec/services/mattermost/commands/issue_search_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -1,18 +1,18 @@ require 'spec_helper' -describe Mattermost::Commands::IssueSearchService, service: true do +describe Gitlab::ChatCommands::IssueSearch, service: true do describe '#execute' do let!(:issue) { create(:issue, title: 'The bird is the word') } let(:project) { issue.project } let(:user) { issue.author } - let(:params) { { text: "issue search bird is the" } } + let(:regex_match) { described_class.match("issue search bird is the") } before { project.team << [user, :master] } - subject { described_class.new(project, user, params).execute } + subject { described_class.new(project, user).execute(regex_match) } context 'without results' do - let(:params) { { text: "issue search no results for this one" } } + let(:regex_match) { described_class.match("issue search no results for this one") } it "returns nil" do expect(subject[:response_type]).to be :ephemeral diff --git a/spec/services/mattermost/commands/issue_show_service_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb similarity index 69% rename from spec/services/mattermost/commands/issue_show_service_spec.rb rename to spec/lib/gitlab/chat_commands/issue_show_spec.rb index 7578cd3a22c..ddf7fc87c36 100644 --- a/spec/services/mattermost/commands/issue_show_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -1,26 +1,25 @@ require 'spec_helper' -describe Mattermost::Commands::IssueShowService, service: true do +describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do let(:issue) { create(:issue) } let(:project) { issue.project } let(:user) { issue.author } - let(:params) { { text: "issue show #{issue.iid}" } } + let(:regex_match) { described_class.match("issue show #{issue.iid}") } before { project.team << [user, :master] } - subject { described_class.new(project, user, params).execute } + subject { described_class.new(project, user).execute(regex_match) } context 'the issue exists' do it 'returns the issue' do - expect(subject[:response_type]).to be :in_channel expect(subject[:text]).to match issue.title end end context 'the issue does not exist' do - let(:params) { { text: "issue show 12345" } } + let(:regex_match) { described_class.match("issue show 1234") } it "returns nil" do expect(subject[:response_type]).to be :ephemeral diff --git a/spec/services/mattermost/commands/merge_request_search_service_spec.rb b/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb similarity index 60% rename from spec/services/mattermost/commands/merge_request_search_service_spec.rb rename to spec/lib/gitlab/chat_commands/merge_request_search_spec.rb index 5212dc206a6..4cb4563e589 100644 --- a/spec/services/mattermost/commands/merge_request_search_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Mattermost::Commands::MergeRequestSearchService, service: true do +describe Gitlab::ChatCommands::MergeRequestSearch, service: true do describe '#execute' do let!(:merge_request) { create(:merge_request, title: 'The bird is the word') } let(:project) { merge_request.source_project } let(:user) { merge_request.author } - let(:params) { { text: "mergerequest search #{merge_request.title}" } } + let(:regex_match) { described_class.match("mergerequest search #{merge_request.title}") } before { project.team << [user, :master] } - subject { described_class.new(project, user, params).execute } + subject { described_class.new(project, user, {}).execute(regex_match) } context 'the merge request exists' do it 'returns the merge request' do @@ -19,7 +19,7 @@ describe Mattermost::Commands::MergeRequestSearchService, service: true do end context 'no results can be found' do - let(:params) { { text: "mergerequest search 12345" } } + let(:regex_match) { described_class.match("mergerequest search 12334") } it "returns a 404 message" do expect(subject[:response_type]).to be :ephemeral @@ -27,4 +27,10 @@ describe Mattermost::Commands::MergeRequestSearchService, service: true do end end end + + describe 'self.match' do + it 'matches a valid query' do + expect(described_class.match("mergerequest search my title here")).to be_truthy + end + end end diff --git a/spec/services/mattermost/commands/merge_request_show_service_spec.rb b/spec/lib/gitlab/chat_commands/merge_request_show_spec.rb similarity index 56% rename from spec/services/mattermost/commands/merge_request_show_service_spec.rb rename to spec/lib/gitlab/chat_commands/merge_request_show_spec.rb index 688737df0f5..ed63ffa5f85 100644 --- a/spec/services/mattermost/commands/merge_request_show_service_spec.rb +++ b/spec/lib/gitlab/chat_commands/merge_request_show_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Mattermost::Commands::MergeRequestShowService, service: true do +describe Gitlab::ChatCommands::MergeRequestShow, service: true do describe '#execute' do let!(:merge_request) { create(:merge_request) } let(:project) { merge_request.source_project } let(:user) { merge_request.author } - let(:params) { { text: "mergerequest show #{merge_request.iid}" } } + let(:regex_match) { described_class.match("mergerequest show #{merge_request.iid}") } before { project.team << [user, :master] } - subject { described_class.new(project, user, params).execute } + subject { described_class.new(project, user).execute(regex_match) } context 'the merge request exists' do it 'returns the merge request' do @@ -19,7 +19,7 @@ describe Mattermost::Commands::MergeRequestShowService, service: true do end context 'the merge request does not exist' do - let(:params) { { text: "mergerequest show 12345" } } + let(:regex_match) { described_class.match("mergerequest show 12345") } it "returns nil" do expect(subject[:response_type]).to be :ephemeral @@ -27,4 +27,11 @@ describe Mattermost::Commands::MergeRequestShowService, service: true do end end end + + describe "self.match" do + it 'matches valid strings' do + expect(described_class.match("mergerequest show 123")).to be_truthy + expect(described_class.match("mergerequest show sdf23")).to be_falsy + end + end end From d4def9cbcd664b7067e7f9f4ea8be54463bd1d50 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 17 Nov 2016 12:06:45 +0100 Subject: [PATCH 214/302] Incorporate feedback, improve presenter class [ci skip] --- app/models/project_services/chat_service.rb | 4 - ...rvice.rb => mattermost_command_service.rb} | 28 +++++-- app/models/service.rb | 4 +- config/environments/development.rb | 3 - lib/api/services.rb | 8 +- lib/gitlab/chat_commands/base_command.rb | 4 + lib/gitlab/chat_commands/command.rb | 22 ++--- lib/gitlab/chat_commands/issue_create.rb | 2 + lib/gitlab/chat_commands/issue_search.rb | 2 +- .../chat_commands/merge_request_command.rb | 4 +- .../chat_commands/merge_request_search.rb | 2 +- lib/mattermost/presenter.rb | 83 ++++++++++--------- .../gitlab/chat_commands/issue_create_spec.rb | 32 +++++-- .../gitlab/chat_commands/issue_show_spec.rb | 30 +++++-- .../merge_request_search_spec.rb | 16 ++-- 15 files changed, 152 insertions(+), 92 deletions(-) rename app/models/project_services/{mattermost_chat_service.rb => mattermost_command_service.rb} (53%) diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb index db387ef3026..d36beff5fa6 100644 --- a/app/models/project_services/chat_service.rb +++ b/app/models/project_services/chat_service.rb @@ -18,8 +18,4 @@ class ChatService < Service def trigger(params) raise NotImplementedError end - - def chat_user_params(params) - params.permit() - end end diff --git a/app/models/project_services/mattermost_chat_service.rb b/app/models/project_services/mattermost_command_service.rb similarity index 53% rename from app/models/project_services/mattermost_chat_service.rb rename to app/models/project_services/mattermost_command_service.rb index 5e9f4d255b6..262dd3076f7 100644 --- a/app/models/project_services/mattermost_chat_service.rb +++ b/app/models/project_services/mattermost_command_service.rb @@ -1,4 +1,4 @@ -class MattermostChatService < ChatService +class MattermostCommandService < ChatService include TriggersHelper prop_accessor :token @@ -8,7 +8,7 @@ class MattermostChatService < ChatService end def title - 'Mattermost' + 'Mattermost Command' end def description @@ -16,7 +16,7 @@ class MattermostChatService < ChatService end def to_param - 'mattermost' + 'mattermost_command' end def help @@ -33,10 +33,24 @@ class MattermostChatService < ChatService end def trigger(params) - user = ChatNames::FindUserService.new(chat_names, params).execute - return Mattermost::Presenter.authorize_chat_name(params) unless user + return nil unless valid_token?(params[:token]) - Mattermost::CommandService.new(project, user, params.slice(:command, :text)). - execute + user = find_chat_user(params) + unless user + url = authorize_chat_name_url(params) + return Mattermost::Presenter.authorize_user(url) + end + + Mattermost::CommandService.new(project, user, params).execute + end + + private + + def find_chat_user(params) + ChatNames::FindUserService.new(chat_names, params).execute + end + + def authorize_chat_name_url(params) + ChatNames::RequestService.new(self, params).execute end end diff --git a/app/models/service.rb b/app/models/service.rb index 8eda1930400..7ca44ced20e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -202,7 +202,6 @@ class Service < ActiveRecord::Base bamboo buildkite builds_email - pipelines_email bugzilla campfire custom_issue_tracker @@ -214,7 +213,8 @@ class Service < ActiveRecord::Base hipchat irker jira - mattermost_chat + mattermost_command + pipelines_email pivotaltracker pushover redmine diff --git a/config/environments/development.rb b/config/environments/development.rb index 6780dbef8e3..45a8c1add3e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,7 +45,4 @@ Rails.application.configure do # Do not log asset requests config.assets.quiet = true - - # Make hot reloading to work with Grape API - ActiveSupport::Dependencies.explicitly_unloadable_constants << "API" end diff --git a/lib/api/services.rb b/lib/api/services.rb index b4b3bb6e41a..094fca49c28 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -61,6 +61,10 @@ module API end resource :projects do + + desc 'Trigger a slash command' do + detail 'Added in GitLab 8.13' + end post ':id/services/:service_slug/trigger' do project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) @@ -71,9 +75,7 @@ module API service = project.public_send(service_method) - result = if service.try(:active?) && service.respond_to?(:trigger) - service.trigger(params) - end + result = service.try(:active?) && service.try(:trigger, params) if result present result, status: result[:status] || 200 diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 13dc2a0f3e8..b5d58af3588 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -38,6 +38,10 @@ module Gitlab def present(resource) Mattermost::Presenter.present(resource) end + + def help(messages) + Mattermost::Presenter.help(messages) + end def find_by_iid(iid) resource = collection.find_by(iid: iid) diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 06d09ab0e24..f1490c045c3 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -13,7 +13,7 @@ module Gitlab def execute klass, match = fetch_klass - return help(help_messages) unless klass.try(:available?, project) + return help(help_messages, params[:command]) unless klass.try(:available?, project) klass.new(project, current_user, params).execute(match) end @@ -22,23 +22,23 @@ module Gitlab def fetch_klass match = nil - service = COMMANDS.find do |klass| - if klass.available?(project) - false - else - match = klass.match(command) - end + service = available_commands.find do |klass| + match = klass.match(command) end [service, match] end def help_messages - COMMANDS.map do |klass| - next unless klass.available?(project) - + available_commands.map do |klass| klass.help_message - end.compact + end + end + + def available_commands + COMMANDS.select do |klass| + klass.available?(project) + end end def command diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index c424e845402..b5cf85b58f1 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -6,6 +6,8 @@ module Gitlab end def execute(match) + present nil unless can?(current_user, :create_issue, project) + title = match[:title] description = match[:description] diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 4169e2a7a88..f64f3ad2680 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueSearch < IssueCommand def self.match(text) - /\Aissue\s+search\s+(?<query>.*)/.match(text) + /\Aissue\s+search\s+(?<query>.*)\s*/.match(text) end def self.help_message diff --git a/lib/gitlab/chat_commands/merge_request_command.rb b/lib/gitlab/chat_commands/merge_request_command.rb index e0f69a49afd..ad485483b8a 100644 --- a/lib/gitlab/chat_commands/merge_request_command.rb +++ b/lib/gitlab/chat_commands/merge_request_command.rb @@ -9,8 +9,8 @@ module Gitlab project.merge_requests end - def readable?(_) - can?(current_user, :read_merge_request, project) + def readable?(merge_request) + can?(current_user, :read_merge_request, merge_request) end end end diff --git a/lib/gitlab/chat_commands/merge_request_search.rb b/lib/gitlab/chat_commands/merge_request_search.rb index caecb1a788e..19a29546736 100644 --- a/lib/gitlab/chat_commands/merge_request_search.rb +++ b/lib/gitlab/chat_commands/merge_request_search.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class MergeRequestSearch < MergeRequestCommand def self.match(text) - /\Amergerequest\s+search\s+(?<query>.*)/.match(text) + /\Amergerequest\s+search\s+(?<query>.*)\s*/.match(text) end def self.help_message diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index 3db55d6bd51..0f2beb2cd6b 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -1,35 +1,24 @@ module Mattermost class Presenter class << self - COMMAND_PREFIX = '/gitlab'.freeze + def authorize_chat_name(url) + message = "Hi there! We've yet to get acquainted! Please [introduce yourself](#{url})!" - def authorize_chat_name(params) - url = ChatNames::RequestService.new(service, params).execute - - { - response_type: :ephemeral, - message: "You are not authorized. Click this [link](#{url}) to authorize." - } + ephemeral_response(message) end - def help(messages) - messages = ["Available commands:"] + def help(messages, command) + message = ["Available commands:"] messages.each do |messsage| - messages << "- #{message}" + message << "- #{command} #{message}" end - { - response_type: :ephemeral, - text: messages.join("\n") - } + ephemeral_response(messages.join("\n")) end def not_found - { - response_type: :ephemeral, - text: "404 not found! GitLab couldn't find what your were looking for! :boom:", - } + ephemeral_response("404 not found! GitLab couldn't find what your were looking for! :boom:") end def present(resource) @@ -51,38 +40,56 @@ module Mattermost private def single_resource(resource) - message = title(resource) + return error(resource) if resource.errors.any? + + message = "### #{title(resource)}" message << "\n\n#{resource.description}" if resource.description - { - response_type: :in_channel, - text: message - } + in_channel_response(message) end def multiple_resources(resources) message = "Multiple results were found:\n" - message << resources.map { |resource| " #{title(resource)}" }.join("\n") + message << resources.map { |resource| "- #{title(resource)}" }.join("\n") + ephemeral_response(message) + end + + def error(resource) + message = "The action was not succesfull because:\n" + message << resource.errors.messages.map { |message| "- #{message}" }.join("\n") + + ephemeral_response(resource.errors.messages.join("\n") + end + + def title(resource) + "[#{resource.to_reference} #{resource.title}](#{url(resource)})" + end + + def url(resource) + polymorphic_url( + [ + resource.project.namespace.becomes(Namespace), + resource.project, + resource + ], + id: resource_id, + routing_type: :url + ) + end + + def ephemeral_response(message) { response_type: :ephemeral, text: message } end - def title(resource) - "### [#{resource.to_reference} #{resource.title}](#{url(resource)})" - end - - def url(resource) - helper = Rails.application.routes.url_helpers - - case resource - when Issue - helper.namespace_project_issue_url(resource.project.namespace.becomes(Namespace), resource.project, resource) - when MergeRequest - helper.namespace_project_merge_request_url(resource.project.namespace.becomes(Namespace), resource.project, resource) - end + def in_channel_response(message) + { + response_type: :in_channel, + text: message + } end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 184c09708a4..5f5cc706c96 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -6,9 +6,13 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do let(:user) { create(:user) } let(:regex_match) { described_class.match("issue create bird is the word") } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end - subject { described_class.new(project, user).execute(regex_match) } + subject do + described_class.new(project, user).execute(regex_match) + end context 'without description' do it 'creates the issue' do @@ -17,7 +21,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do end.to change { project.issues.count }.by(1) expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match 'bird is the word' + expect(subject[:text]).to match('bird is the word') end end @@ -25,11 +29,29 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do let(:description) { "Surfin bird" } let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } - before { subject } + before do + subject + end it 'creates the issue with description' do - expect(Issue.last.description).to eq description + expect(Issue.last.description).to eq(description) end end end + + describe 'self.match' do + it 'matches the title without description' do + match = described_class.match("issue create my title") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq("") + end + + it 'matches the title with description' do + match = described_class.match("issue create my title\n\ndescription") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq('description') + end + end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index ddf7fc87c36..d7824dd6bf5 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,19 +2,23 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } - let(:user) { issue.author } + let(:issue) { create(:issue) } + let(:project) { issue.project } + let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end - subject { described_class.new(project, user).execute(regex_match) } + subject do + described_class.new(project, user).execute(regex_match) + end context 'the issue exists' do it 'returns the issue' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match issue.title + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to match(issue.title) end end @@ -22,9 +26,17 @@ describe Gitlab::ChatCommands::IssueShow, service: true do let(:regex_match) { described_class.match("issue show 1234") } it "returns nil" do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with '404 not found!' + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('404 not found!') end end end + + describe 'self.match' do + it 'matches the iid' do + match = described_class.match("issue show 123") + + expect(match[:iid]).to eq("123") + end + end end diff --git a/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb b/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb index 4cb4563e589..4033358ab2e 100644 --- a/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb @@ -7,14 +7,18 @@ describe Gitlab::ChatCommands::MergeRequestSearch, service: true do let(:user) { merge_request.author } let(:regex_match) { described_class.match("mergerequest search #{merge_request.title}") } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end - subject { described_class.new(project, user, {}).execute(regex_match) } + subject do + described_class.new(project, user).execute(regex_match) + end context 'the merge request exists' do it 'returns the merge request' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match merge_request.title + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to match(merge_request.title) end end @@ -22,8 +26,8 @@ describe Gitlab::ChatCommands::MergeRequestSearch, service: true do let(:regex_match) { described_class.match("mergerequest search 12334") } it "returns a 404 message" do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with '404 not found!' + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('404 not found!') end end end From 6737ada0c8d980ed1bd8f425e885fa1b89930616 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 17 Nov 2016 12:57:27 +0100 Subject: [PATCH 215/302] Remove some commands for now --- .../zj-slash-commands-mattermost.yml | 4 ++ lib/gitlab/chat_commands/command.rb | 4 -- lib/gitlab/chat_commands/issue_create.rb | 6 ++- lib/gitlab/chat_commands/issue_search.rb | 17 -------- .../chat_commands/merge_request_command.rb | 17 -------- .../chat_commands/merge_request_search.rb | 17 -------- .../chat_commands/merge_request_show.rb | 17 -------- lib/mattermost/presenter.rb | 12 +++--- spec/lib/gitlab/chat_commands/command_spec.rb | 20 +++++++--- .../gitlab/chat_commands/issue_search_spec.rb | 39 ------------------ .../merge_request_search_spec.rb | 40 ------------------- .../chat_commands/merge_request_show_spec.rb | 37 ----------------- 12 files changed, 30 insertions(+), 200 deletions(-) create mode 100644 changelogs/unreleased/zj-slash-commands-mattermost.yml delete mode 100644 lib/gitlab/chat_commands/issue_search.rb delete mode 100644 lib/gitlab/chat_commands/merge_request_command.rb delete mode 100644 lib/gitlab/chat_commands/merge_request_search.rb delete mode 100644 lib/gitlab/chat_commands/merge_request_show.rb delete mode 100644 spec/lib/gitlab/chat_commands/issue_search_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/merge_request_search_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/merge_request_show_spec.rb diff --git a/changelogs/unreleased/zj-slash-commands-mattermost.yml b/changelogs/unreleased/zj-slash-commands-mattermost.yml new file mode 100644 index 00000000000..84aca57e666 --- /dev/null +++ b/changelogs/unreleased/zj-slash-commands-mattermost.yml @@ -0,0 +1,4 @@ +--- +title: Add first slash commands +merge_request: 7438 +author: diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index f1490c045c3..0ed51d9b8fc 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,11 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::IssueCreate, - - Gitlab::ChatCommands::MergeRequestShow, - Gitlab::ChatCommands::MergeRequestSearch, ].freeze def execute diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index b5cf85b58f1..0e2b4c0e9cd 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -1,10 +1,14 @@ module Gitlab module ChatCommands - class IssueCreate < BaseCommand + class IssueCreate < IssueCommand def self.match(text) /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>.*)\z/.match(text) end + def self.help_message + 'issue create <title>\n<description>' + end + def execute(match) present nil unless can?(current_user, :create_issue, project) diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb deleted file mode 100644 index f64f3ad2680..00000000000 --- a/lib/gitlab/chat_commands/issue_search.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Gitlab - module ChatCommands - class IssueSearch < IssueCommand - def self.match(text) - /\Aissue\s+search\s+(?<query>.*)\s*/.match(text) - end - - def self.help_message - "issue search <query>" - end - - def execute(match) - present search_results(match[:query]) - end - end - end -end diff --git a/lib/gitlab/chat_commands/merge_request_command.rb b/lib/gitlab/chat_commands/merge_request_command.rb deleted file mode 100644 index ad485483b8a..00000000000 --- a/lib/gitlab/chat_commands/merge_request_command.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Gitlab - module ChatCommands - class MergeRequestCommand < BaseCommand - def self.available?(project) - project.merge_requests_enabled? - end - - def collection - project.merge_requests - end - - def readable?(merge_request) - can?(current_user, :read_merge_request, merge_request) - end - end - end -end diff --git a/lib/gitlab/chat_commands/merge_request_search.rb b/lib/gitlab/chat_commands/merge_request_search.rb deleted file mode 100644 index 19a29546736..00000000000 --- a/lib/gitlab/chat_commands/merge_request_search.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Gitlab - module ChatCommands - class MergeRequestSearch < MergeRequestCommand - def self.match(text) - /\Amergerequest\s+search\s+(?<query>.*)\s*/.match(text) - end - - def self.help_message - "mergerequest search <query>" - end - - def execute(match) - present search_results(match[:query]) - end - end - end -end diff --git a/lib/gitlab/chat_commands/merge_request_show.rb b/lib/gitlab/chat_commands/merge_request_show.rb deleted file mode 100644 index 7ed5445e4c2..00000000000 --- a/lib/gitlab/chat_commands/merge_request_show.rb +++ /dev/null @@ -1,17 +0,0 @@ -module Gitlab - module ChatCommands - class MergeRequestShow < MergeRequestCommand - def self.match(text) - /\Amergerequest\s+show\s+(?<iid>\d+)/.match(text) - end - - def self.help_message - "mergerequest show <id>" - end - - def execute(match) - present find_by_iid(match[:iid]) - end - end - end -end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index 0f2beb2cd6b..b3d6c025109 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -1,6 +1,8 @@ module Mattermost class Presenter class << self + include Rails.application.routes.url_helpers + def authorize_chat_name(url) message = "Hi there! We've yet to get acquainted! Please [introduce yourself](#{url})!" @@ -59,7 +61,7 @@ module Mattermost message = "The action was not succesfull because:\n" message << resource.errors.messages.map { |message| "- #{message}" }.join("\n") - ephemeral_response(resource.errors.messages.join("\n") + ephemeral_response(resource.errors.messages.join("\n")) end def title(resource) @@ -67,14 +69,12 @@ module Mattermost end def url(resource) - polymorphic_url( + url_for( [ resource.project.namespace.becomes(Namespace), resource.project, - resource - ], - id: resource_id, - routing_type: :url + resource + ] ) end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index d28349ff1d8..bbd47f45761 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -7,12 +7,22 @@ describe Gitlab::ChatCommands::Command, service: true do subject { described_class.new(project, user, params).execute } - xdescribe '#execute' do - context 'when issue show is triggered' do - it 'calls IssueShowService' do - expect_any_instance_of(Mattermost::Commands::IssueShowService).to receive(:new).with(project, user, params) + describe '#execute' do + context 'when the command is not available' do + let(:project) { create(:project, has_external_issue_tracker: true) } - subject + it 'displays the help message' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Available commands') + end + end + + context 'when an unknown command is triggered' do + let(:params) { { text: "unknown command 123" } } + + it 'displays the help message' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Available commands') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb deleted file mode 100644 index 3e54333528a..00000000000 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::IssueSearch, service: true do - describe '#execute' do - let!(:issue) { create(:issue, title: 'The bird is the word') } - let(:project) { issue.project } - let(:user) { issue.author } - let(:regex_match) { described_class.match("issue search bird is the") } - - before { project.team << [user, :master] } - - subject { described_class.new(project, user).execute(regex_match) } - - context 'without results' do - let(:regex_match) { described_class.match("issue search no results for this one") } - - it "returns nil" do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with '404 not found!' - end - end - - context 'with 1 result' do - it 'returns the issue' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match issue.title - end - end - - context 'with 2 or more results' do - let!(:issue2) { create(:issue, project: project, title: 'bird is the word!') } - - it 'returns multiple resources' do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with 'Multiple results were found' - end - end - end -end diff --git a/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb b/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb deleted file mode 100644 index 4033358ab2e..00000000000 --- a/spec/lib/gitlab/chat_commands/merge_request_search_spec.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::MergeRequestSearch, service: true do - describe '#execute' do - let!(:merge_request) { create(:merge_request, title: 'The bird is the word') } - let(:project) { merge_request.source_project } - let(:user) { merge_request.author } - let(:regex_match) { described_class.match("mergerequest search #{merge_request.title}") } - - before do - project.team << [user, :master] - end - - subject do - described_class.new(project, user).execute(regex_match) - end - - context 'the merge request exists' do - it 'returns the merge request' do - expect(subject[:response_type]).to be(:in_channel) - expect(subject[:text]).to match(merge_request.title) - end - end - - context 'no results can be found' do - let(:regex_match) { described_class.match("mergerequest search 12334") } - - it "returns a 404 message" do - expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('404 not found!') - end - end - end - - describe 'self.match' do - it 'matches a valid query' do - expect(described_class.match("mergerequest search my title here")).to be_truthy - end - end -end diff --git a/spec/lib/gitlab/chat_commands/merge_request_show_spec.rb b/spec/lib/gitlab/chat_commands/merge_request_show_spec.rb deleted file mode 100644 index ed63ffa5f85..00000000000 --- a/spec/lib/gitlab/chat_commands/merge_request_show_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::MergeRequestShow, service: true do - describe '#execute' do - let!(:merge_request) { create(:merge_request) } - let(:project) { merge_request.source_project } - let(:user) { merge_request.author } - let(:regex_match) { described_class.match("mergerequest show #{merge_request.iid}") } - - before { project.team << [user, :master] } - - subject { described_class.new(project, user).execute(regex_match) } - - context 'the merge request exists' do - it 'returns the merge request' do - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match merge_request.title - end - end - - context 'the merge request does not exist' do - let(:regex_match) { described_class.match("mergerequest show 12345") } - - it "returns nil" do - expect(subject[:response_type]).to be :ephemeral - expect(subject[:text]).to start_with '404 not found!' - end - end - end - - describe "self.match" do - it 'matches valid strings' do - expect(described_class.match("mergerequest show 123")).to be_truthy - expect(described_class.match("mergerequest show sdf23")).to be_falsy - end - end -end From 1607efa40081702488e22e560db2c1e30cd80093 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 17 Nov 2016 15:30:04 +0100 Subject: [PATCH 216/302] Add tests for increased converage --- lib/gitlab/chat_commands/base_command.rb | 8 ------- lib/gitlab/chat_commands/command.rb | 16 +++++++++++--- lib/gitlab/chat_commands/issue_create.rb | 6 ++--- lib/gitlab/chat_commands/issue_show.rb | 2 +- lib/mattermost/presenter.rb | 5 +++-- spec/lib/gitlab/chat_commands/command_spec.rb | 22 ++++++++++++++----- .../gitlab/chat_commands/issue_create_spec.rb | 17 +++++--------- .../gitlab/chat_commands/issue_show_spec.rb | 8 +++---- 8 files changed, 46 insertions(+), 38 deletions(-) diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index b5d58af3588..81b15bd1f7a 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -35,14 +35,6 @@ module Gitlab Ability.allowed?(object, action, subject) end - def present(resource) - Mattermost::Presenter.present(resource) - end - - def help(messages) - Mattermost::Presenter.help(messages) - end - def find_by_iid(iid) resource = collection.find_by(iid: iid) diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 0ed51d9b8fc..43144975901 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -9,9 +9,11 @@ module Gitlab def execute klass, match = fetch_klass - return help(help_messages, params[:command]) unless klass.try(:available?, project) - - klass.new(project, current_user, params).execute(match) + if klass + present klass.new(project, current_user, params).execute(match) + else + help(help_messages) + end end private @@ -40,6 +42,14 @@ module Gitlab def command params[:text] end + + def present(resource) + Mattermost::Presenter.present(resource) + end + + def help(messages) + Mattermost::Presenter.help(messages, params[:command]) + end end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 0e2b4c0e9cd..1e311e09771 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -1,6 +1,6 @@ module Gitlab module ChatCommands - class IssueCreate < IssueCommand + class IssueCreate < IssueCommand def self.match(text) /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>.*)\z/.match(text) end @@ -10,12 +10,12 @@ module Gitlab end def execute(match) - present nil unless can?(current_user, :create_issue, project) + return nil unless can?(current_user, :create_issue, project) title = match[:title] description = match[:description] - present Issues::CreateService.new(project, current_user, title: title, description: description).execute + Issues::CreateService.new(project, current_user, title: title, description: description).execute end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index e5530df31cc..f5bceb038e5 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,7 @@ module Gitlab end def execute(match) - present find_by_iid(match[:iid]) + find_by_iid(match[:iid]) end end end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index b3d6c025109..84b7b8edd9e 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -4,19 +4,20 @@ module Mattermost include Rails.application.routes.url_helpers def authorize_chat_name(url) - message = "Hi there! We've yet to get acquainted! Please [introduce yourself](#{url})!" + message = "Hi there! We've yet to get acquainted! Please introduce yourself by [connection your GitLab profile](#{url})!" ephemeral_response(message) end def help(messages, command) + return ephemeral_response("No commands configured") unless messages.count > 1 message = ["Available commands:"] messages.each do |messsage| message << "- #{command} #{message}" end - ephemeral_response(messages.join("\n")) + ephemeral_response(message.join("\n")) end def not_found diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index bbd47f45761..328187b5048 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' describe Gitlab::ChatCommands::Command, service: true do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:params) { { text: 'issue show 1' } } + let(:project) { create(:project) } + let(:user) { create(:user) } subject { described_class.new(project, user, params).execute } describe '#execute' do - context 'when the command is not available' do + context 'when no command is not available' do + let(:params) { { text: 'issue show 1' } } let(:project) { create(:project, has_external_issue_tracker: true) } it 'displays the help message' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Available commands') + expect(subject[:text]).to start_with('404 not found') end end @@ -25,5 +25,17 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to start_with('Available commands') end end + + context 'issue is succesfully created' do + let(:params) { { text: "issue create my new issue" } } + + before do + project.team << [user, :master] + end + + it 'presents the issue' do + expect(subject[:text]).to match("my new issue") + end + end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 5f5cc706c96..4831f24efed 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueCreate, service: true do describe '#execute' do - let(:project) { create(:empty_project) } - let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } let(:regex_match) { described_class.match("issue create bird is the word") } before do @@ -16,12 +16,9 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do context 'without description' do it 'creates the issue' do - expect do - subject # this trigger the execution - end.to change { project.issues.count }.by(1) + expect { subject }.to change { project.issues.count }.by(1) - expect(subject[:response_type]).to be :in_channel - expect(subject[:text]).to match('bird is the word') + expect(subject.title).to eq('bird is the word') end end @@ -29,11 +26,9 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do let(:description) { "Surfin bird" } let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } - before do - subject - end - it 'creates the issue with description' do + subject + expect(Issue.last.description).to eq(description) end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index d7824dd6bf5..331a4604e9b 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -17,17 +17,15 @@ describe Gitlab::ChatCommands::IssueShow, service: true do context 'the issue exists' do it 'returns the issue' do - expect(subject[:response_type]).to be(:in_channel) - expect(subject[:text]).to match(issue.title) + expect(subject.iid).to be issue.iid end end context 'the issue does not exist' do - let(:regex_match) { described_class.match("issue show 1234") } + let(:regex_match) { described_class.match("issue show 2343242") } it "returns nil" do - expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('404 not found!') + expect(subject).to be_nil end end end From 166ee0965bacc20e2ad1187321654499a9b0f825 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 17 Nov 2016 21:27:12 +0100 Subject: [PATCH 217/302] More refactoring, push present to base command --- app/models/project.rb | 2 +- .../mattermost_command_service.rb | 8 +-- lib/api/services.rb | 4 +- lib/gitlab/chat_commands/base_command.rb | 18 +++---- lib/gitlab/chat_commands/command.rb | 28 ++++++---- lib/gitlab/chat_commands/issue_command.rb | 2 +- lib/gitlab/chat_commands/issue_create.rb | 6 ++- lib/mattermost/presenter.rb | 53 ++++++++++++------- spec/lib/gitlab/chat_commands/command_spec.rb | 9 ++++ spec/lib/gitlab/chat_name_token_spec.rb | 4 +- spec/models/project_spec.rb | 1 + .../chat_names/authorize_user_service_spec.rb | 2 +- 12 files changed, 84 insertions(+), 53 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index a118761d93d..c5acae99c4c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -91,7 +91,7 @@ class Project < ActiveRecord::Base has_one :assembla_service, dependent: :destroy has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy - has_one :mattermost_chat_service, dependent: :destroy + has_one :mattermost_command_service, dependent: :destroy has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy diff --git a/app/models/project_services/mattermost_command_service.rb b/app/models/project_services/mattermost_command_service.rb index 262dd3076f7..3c9b14c66b4 100644 --- a/app/models/project_services/mattermost_command_service.rb +++ b/app/models/project_services/mattermost_command_service.rb @@ -38,19 +38,19 @@ class MattermostCommandService < ChatService user = find_chat_user(params) unless user url = authorize_chat_name_url(params) - return Mattermost::Presenter.authorize_user(url) + return Mattermost::Presenter.authorize_chat_name(url) end - Mattermost::CommandService.new(project, user, params).execute + Gitlab::ChatCommands::Command.new(project, user, params).execute end private def find_chat_user(params) - ChatNames::FindUserService.new(chat_names, params).execute + ChatNames::FindUserService.new(self, params).execute end def authorize_chat_name_url(params) - ChatNames::RequestService.new(self, params).execute + ChatNames::AuthorizeUserService.new(self, params).execute end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 094fca49c28..b0a94508d10 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -61,7 +61,6 @@ module API end resource :projects do - desc 'Trigger a slash command' do detail 'Added in GitLab 8.13' end @@ -78,7 +77,8 @@ module API result = service.try(:active?) && service.try(:trigger, params) if result - present result, status: result[:status] || 200 + status result[:status] || 200 + present result else not_found!('Service') end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 81b15bd1f7a..f84aca5365d 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -15,6 +15,14 @@ module Gitlab raise NotImplementedError end + def self.allowed?(_, _) + true + end + + def self.can?(object, action, subject) + Ability.allowed?(object, action, subject) + end + def execute(_) raise NotImplementedError end @@ -31,21 +39,11 @@ module Gitlab private - def can?(object, action, subject) - Ability.allowed?(object, action, subject) - end - def find_by_iid(iid) resource = collection.find_by(iid: iid) readable?(resource) ? resource : nil end - - def search_results(query) - collection.search(query).limit(QUERY_LIMIT).select do |resource| - readable?(resource) - end - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 43144975901..5f131703d40 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -7,10 +7,14 @@ module Gitlab ].freeze def execute - klass, match = fetch_klass + command, match = match_command - if klass - present klass.new(project, current_user, params).execute(match) + if command + if command.allowed?(project, current_user) + present command.new(project, current_user, params).execute(match) + else + access_denied + end else help(help_messages) end @@ -18,7 +22,7 @@ module Gitlab private - def fetch_klass + def match_command match = nil service = available_commands.find do |klass| match = klass.match(command) @@ -28,9 +32,7 @@ module Gitlab end def help_messages - available_commands.map do |klass| - klass.help_message - end + available_commands.map(&:help_message) end def available_commands @@ -43,13 +45,17 @@ module Gitlab params[:text] end - def present(resource) - Mattermost::Presenter.present(resource) - end - def help(messages) Mattermost::Presenter.help(messages, params[:command]) end + + def access_denied + Mattermost::Presenter.access_denied + end + + def present(resource) + Mattermost::Presenter.present(resource) + end end end end diff --git a/lib/gitlab/chat_commands/issue_command.rb b/lib/gitlab/chat_commands/issue_command.rb index 2426b3714b7..f1bc36239d5 100644 --- a/lib/gitlab/chat_commands/issue_command.rb +++ b/lib/gitlab/chat_commands/issue_command.rb @@ -10,7 +10,7 @@ module Gitlab end def readable?(issue) - can?(current_user, :read_issue, issue) + self.class.can?(current_user, :read_issue, issue) end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index 1e311e09771..98338ebfa27 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -9,9 +9,11 @@ module Gitlab 'issue create <title>\n<description>' end - def execute(match) - return nil unless can?(current_user, :create_issue, project) + def self.allowed?(project, user) + can?(user, :create_issue, project) + end + def execute(match) title = match[:title] description = match[:description] diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index 84b7b8edd9e..7722022c658 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -4,24 +4,19 @@ module Mattermost include Rails.application.routes.url_helpers def authorize_chat_name(url) - message = "Hi there! We've yet to get acquainted! Please introduce yourself by [connection your GitLab profile](#{url})!" + message = ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." ephemeral_response(message) end - def help(messages, command) - return ephemeral_response("No commands configured") unless messages.count > 1 - message = ["Available commands:"] + def help(commands, trigger) + if commands.count == 0 + ephemeral_response("No commands configured") unless messages.count > 1 + else + message = header_with_list("Available commands", commands) - messages.each do |messsage| - message << "- #{command} #{message}" + ephemeral_response(message) end - - ephemeral_response(message.join("\n")) - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what your were looking for! :boom:") end def present(resource) @@ -40,8 +35,16 @@ module Mattermost single_resource(resource) end + def access_denied + ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + private + def not_found + ephemeral_response("404 not found! GitLab couldn't find what your were looking for! :boom:") + end + def single_resource(resource) return error(resource) if resource.errors.any? @@ -52,23 +55,33 @@ module Mattermost end def multiple_resources(resources) - message = "Multiple results were found:\n" - message << resources.map { |resource| "- #{title(resource)}" }.join("\n") + resources.map! { |resource| title(resource) } + + message = header_with_list("Multiple results were found:", resources) ephemeral_response(message) end def error(resource) - message = "The action was not succesfull because:\n" - message << resource.errors.messages.map { |message| "- #{message}" }.join("\n") + message = header_with_list("The action was not succesful, because:", resource.errors.messages) - ephemeral_response(resource.errors.messages.join("\n")) + ephemeral_response(message) end def title(resource) "[#{resource.to_reference} #{resource.title}](#{url(resource)})" end + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + def url(resource) url_for( [ @@ -82,14 +95,16 @@ module Mattermost def ephemeral_response(message) { response_type: :ephemeral, - text: message + text: message, + status: 200 } end def in_channel_response(message) { response_type: :in_channel, - text: message + text: message, + status: 200 } end end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 328187b5048..528e690d234 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -26,6 +26,15 @@ describe Gitlab::ChatCommands::Command, service: true do end end + context 'the user can not create an issue' do + let(:params) { { text: "issue create my new issue" } } + + it 'rejects the actions' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to start_with('Whoops! That action is not allowed') + end + end + context 'issue is succesfully created' do let(:params) { { text: "issue create my new issue" } } diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb index 8d7e7a99059..10153682973 100644 --- a/spec/lib/gitlab/chat_name_token_spec.rb +++ b/spec/lib/gitlab/chat_name_token_spec.rb @@ -12,9 +12,9 @@ describe Gitlab::ChatNameToken, lib: true do end context 'when storing data' do - let(:data) { + let(:data) do { key: 'value' } - } + end subject { described_class.new(@token) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c74d9c282cf..d89a83cfc71 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -35,6 +35,7 @@ describe Project, models: true do it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } it { is_expected.to have_one(:assembla_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_command_service).dependent(:destroy) } it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } it { is_expected.to have_one(:bamboo_service).dependent(:destroy) } diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb index f8c26e51bfc..d5178176526 100644 --- a/spec/services/chat_names/authorize_user_service_spec.rb +++ b/spec/services/chat_names/authorize_user_service_spec.rb @@ -17,7 +17,7 @@ describe ChatNames::AuthorizeUserService, services: true do end context 'when there are missing parameters' do - let(:params) { { } } + let(:params) { {} } it 'does not request a new token' do is_expected.to be_nil From abe35457ee6ef7e13cf9f67a8bd0b9bbc380dfe7 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Thu, 17 Nov 2016 13:56:27 -0700 Subject: [PATCH 218/302] Create global secondary inverted button --- app/assets/stylesheets/framework/buttons.scss | 6 ++++-- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index f7d681535dc..ed493278b36 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -345,8 +345,10 @@ } } -.retry-build-btn { - @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); +.btn-inverted { + &-secondary { + @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light); + } } @media (max-width: $screen-xs-max) { diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index fea7b975df5..b3ee55fca0b 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -14,6 +14,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn retry-build-btn pull-right', method: :post + = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index decbbd80ac5..a0de125d765 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -10,7 +10,7 @@ .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - if @pipeline.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn retry-build-btn', method: :post + = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post - if @pipeline.builds.running_or_pending.any? = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post From 8bd3f52667f4682ebfefa62024b901cabc928ed1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 17 Nov 2016 20:58:38 +0000 Subject: [PATCH 219/302] Swaps url for paths in mock data --- .../javascripts/environments/mock_data.js.es6 | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 1142ace5846..9e16bc3e6a5 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -40,18 +40,18 @@ const environmentsList = [ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, - commit_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', }, deployable: { id: 1278, name: 'build', - build_url: 'http://localhost:3000/root/ci-folders/builds/1278', - retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry', + build_path: '/root/ci-folders/builds/1278', + retry_path: '/root/ci-folders/builds/1278/retry', }, manual_actions: [], }, 'stoppable?': true, - environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', }, @@ -95,18 +95,18 @@ const environmentsList = [ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, - commit_url: 'http://localhost:3000/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', }, deployable: { id: 1278, name: 'build', - build_url: 'http://localhost:3000/root/ci-folders/builds/1278', - retry_url: 'http://localhost:3000/root/ci-folders/builds/1278/retry', + build_path: '/root/ci-folders/builds/1278', + retry_path: '/root/ci-folders/builds/1278/retry', }, manual_actions: [], }, 'stoppable?': false, - environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', }, @@ -117,7 +117,7 @@ const environmentsList = [ environment_type: 'review', last_deployment: null, 'stoppable?': true, - environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', }, @@ -128,9 +128,8 @@ const environmentsList = [ environment_type: 'review', last_deployment: null, 'stoppable?': true, - environment_url: 'http://localhost:3000/root/ci-folders/environments/31', + environment_path: '/root/ci-folders/environments/31', created_at: '2016-11-07T11:11:16.525Z', updated_at: '2016-11-07T11:11:16.525Z', }, ]; - From f9237abb66aa92825c17bc6864e16783c1e54450 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 17 Nov 2016 21:26:34 +0000 Subject: [PATCH 220/302] Fix after review --- .../javascripts/environments/components/environment.js.es6 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index a94fe41dede..28377cd5183 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -140,10 +140,7 @@ * @returns {Boolean} */ convertPermissionToBoolean(string) { - if (string === 'true') { - return true; - } - return false; + return string === 'true'; }, methods: { From a22f5161146d94f548d305f053531882f866a951 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 17 Nov 2016 21:32:43 +0000 Subject: [PATCH 221/302] Adds helper to verify empty object --- .../components/environment_item.js.es6 | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 871da5790ec..4358ce96a13 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -107,7 +107,8 @@ * @returns {Boolean} */ hasLastDeploymentKey() { - if (this.model.last_deployment && this.model.last_deployment !== {}) { + if (this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { return true; } return false; @@ -326,8 +327,7 @@ * @returns {Boolean} */ deploymentHasUser() { - if (this.model.last_deployment && - this.model.last_deployment.user) { + if (this.model.last_deployment && this.model.last_deployment.user) { return true; } return false; @@ -347,6 +347,21 @@ }, }, + /** + * 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 v-bind:class="{ 'children-row': isChildren}"> From 92cff96f86597f86614116436d4b11d8a325032a Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 17 Nov 2016 21:46:41 +0000 Subject: [PATCH 222/302] Improvements after review --- .../components/environment_item.js.es6 | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 4358ce96a13..9241b5472a7 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -208,8 +208,7 @@ * @returns {Object|Undefined} */ commitRef() { - if (this.model.last_deployment && - this.model.last_deployment.ref) { + if (this.model.last_deployment && this.model.last_deployment.ref) { return this.model.last_deployment.ref; } return undefined; @@ -327,10 +326,8 @@ * @returns {Boolean} */ deploymentHasUser() { - if (this.model.last_deployment && this.model.last_deployment.user) { - return true; - } - return false; + return !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); }, /** @@ -340,11 +337,38 @@ * @returns {Object} */ deploymentUser() { - if (this.model.last_deployment && this.model.last_deployment.user) { + if (!this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { return this.model.last_deployment.user; } return {}; }, + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return !this.isFolder && + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; + }, }, /** @@ -385,7 +409,7 @@ <td class="deployment-column"> <span - v-if="!isFolder && model.last_deployment && model.last_deployment.iid" + v-if="shouldRenderDeploymentID" v-html="deploymentInternalId"> </span> @@ -401,7 +425,7 @@ </td> <td> - <a v-if="!isFolder && model.last_deployment && model.last_deployment.deployable" + <a v-if="shouldRenderBuildName" class="build-link" :href="model.last_deployment.deployable.build_path" v-html="buildName"> @@ -434,25 +458,29 @@ <td class="hidden-xs"> <div v-if="!isFolder"> - <div v-if="hasManualActions && canCreateDeployment" class="inline js-manual-actions-container"> + <div v-if="hasManualActions && canCreateDeployment" + class="inline js-manual-actions-container"> <actions-component :actions="manualActions"> </actions-component> </div> - <div v-if="model.external_url && canReadEnvironment" class="inline js-external-url-container"> + <div v-if="model.external_url && canReadEnvironment" + class="inline js-external-url-container"> <external-url-component :external_url="model.external_url"> </external_url-component> </div> - <div v-if="isStoppable && canCreateDeployment" class="inline js-stop-component-container"> + <div v-if="isStoppable && canCreateDeployment" + class="inline js-stop-component-container"> <stop-component :stop_url="model.environment_path"> </stop-component> </div> - <div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container"> + <div v-if="canRetry && canCreateDeployment" + class="inline js-rollback-component-container"> <rollback-component :is_last_deployment="isLastDeployment" :retry_url="retryUrl"> From fab9d4d3830f4a4ef3062e77c95fed03ac5cfe81 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Thu, 17 Nov 2016 15:23:38 -0700 Subject: [PATCH 223/302] Add changelog entry for 7251 --- changelogs/unreleased/changelog-update.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/changelog-update.yml diff --git a/changelogs/unreleased/changelog-update.yml b/changelogs/unreleased/changelog-update.yml new file mode 100644 index 00000000000..24fa34f4121 --- /dev/null +++ b/changelogs/unreleased/changelog-update.yml @@ -0,0 +1,4 @@ +--- +title: Add environment info to builds page +merge_request: +author: From e56c305be003ee33b093f9ba42622fba0da903f2 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 08:13:34 +0100 Subject: [PATCH 224/302] fix spec failures --- .../gitlab/cycle_analytics/code_event_spec.rb | 8 ++--- .../cycle_analytics/issue_event_spec.rb | 8 ++--- .../gitlab/cycle_analytics/plan_event_spec.rb | 8 ++--- .../cycle_analytics/production_event_spec.rb | 2 +- .../cycle_analytics/review_event_spec.rb | 2 +- .../cycle_analytics/shared_event_spec.rb | 10 +++--- .../cycle_analytics/staging_event_spec.rb | 8 ++--- .../gitlab/cycle_analytics/test_event_spec.rb | 8 ++--- ....rb => analytics_issue_serializer_spec.rb} | 4 +-- ...analytics_merge_request_serializer_spec.rb | 34 +++++++++++++++++++ 10 files changed, 64 insertions(+), 28 deletions(-) rename spec/serializers/{analytics_generic_serializer_spec.rb => analytics_issue_serializer_spec.rb} (88%) create mode 100644 spec/serializers/analytics_merge_request_serializer_spec.rb diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb index 7b854d7fea1..43f42d1bde8 100644 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::CodeEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(described_class.order).not_to eq(described_class.start_time_attrs) + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb index 2d7e7b1e0a5..1c5c308da7d 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::IssueEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(described_class.order).to eq(described_class.start_time_attrs) + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end end end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb index 3452c146584..d76a255acf5 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::PlanEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(described_class.order).to eq(described_class.start_time_attrs) + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end end end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb index 069c84efa09..6ee54f246a7 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb @@ -5,6 +5,6 @@ describe Gitlab::CycleAnalytics::ProductionEvent do it_behaves_like 'default query config' it 'has the default order' do - expect(described_class.order).to eq(described_class.start_time_attrs) + expect(event.order).to eq(event.start_time_attrs) end end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb index e9aca82ecc9..890e3ab3078 100644 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb @@ -5,6 +5,6 @@ describe Gitlab::CycleAnalytics::ReviewEvent do it_behaves_like 'default query config' it 'has the default order' do - expect(described_class.order).to eq(described_class.start_time_attrs) + expect(event.order).to eq(event.start_time_attrs) end end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 1e76edce633..7019e4c3351 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,19 +1,21 @@ require 'spec_helper' shared_examples 'default query config' do + let(:event) { described_class.new(project: double, options: {}) } + it 'has the start attributes' do - expect(described_class.start_time_attrs).not_to be_nil + expect(event.start_time_attrs).not_to be_nil end it 'has the stage attribute' do - expect(described_class.stage).not_to be_nil + expect(event.stage).not_to be_nil end it 'has the end attributes' do - expect(described_class.end_time_attrs).not_to be_nil + expect(event.end_time_attrs).not_to be_nil end it 'has the projection attributes' do - expect(described_class.projections).not_to be_nil + expect(event.projections).not_to be_nil end end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb index 9c1bec3edb1..4862d4765f2 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::StagingEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(described_class.order).not_to eq(described_class.start_time_attrs) + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end end end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb index 7238b9a34b3..e249db69fc6 100644 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::TestEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(described_class.order).not_to eq(described_class.start_time_attrs) + it_behaves_like 'default query config' do + it 'does not have the default order' do + expect(event.order).not_to eq(event.start_time_attrs) + end end end diff --git a/spec/serializers/analytics_generic_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb similarity index 88% rename from spec/serializers/analytics_generic_serializer_spec.rb rename to spec/serializers/analytics_issue_serializer_spec.rb index 99f2254d22c..2842e1ba52f 100644 --- a/spec/serializers/analytics_generic_serializer_spec.rb +++ b/spec/serializers/analytics_issue_serializer_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AnalyticsGenericSerializer do +describe AnalyticsIssueSerializer do let(:serializer) do described_class .new(project: project, entity: :merge_request) @@ -26,7 +26,7 @@ describe AnalyticsGenericSerializer do expect(json).to be_an_instance_of Hash end - it 'contains important elements of analyticsBuild' do + it 'contains important elements of the issue' do expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author) end end diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb new file mode 100644 index 00000000000..564207984df --- /dev/null +++ b/spec/serializers/analytics_merge_request_serializer_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe AnalyticsMergeRequestSerializer do + let(:serializer) do + described_class + .new(project: project, entity: :merge_request) + .represent(resource) + end + + let(:user) { create(:user) } + let(:json) { serializer.as_json } + let(:project) { create(:project) } + let(:resource) do + { + total_time: "172802.724419", + title: "Eos voluptatem inventore in sed.", + iid: "1", + id: "1", + state: 'open', + created_at: "2016-11-12 15:04:02.948604", + author: user + } + end + + context 'when there is a single object provided' do + it 'it generates payload for single object' do + expect(json).to be_an_instance_of Hash + end + + it 'contains important elements of the merge request' do + expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state) + end + end +end From bf28506f673e728bfaf0f8fb29d07d3e1cf517a2 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 17 Nov 2016 17:21:17 +0530 Subject: [PATCH 225/302] Add a migration to remove soft-deleted groups. The database should not have any soft-deleted groups. Due to a race condition (soft-delete completes after the hard-delete), soft-deleted groups were (incorrectly) left in the database, causing issues while trying to create a new group with the same name. --- .../20161117114805_remove_undeleted_groups.rb | 16 ++++++++++++++++ db/schema.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20161117114805_remove_undeleted_groups.rb diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb new file mode 100644 index 00000000000..ebc2d974ae0 --- /dev/null +++ b/db/migrate/20161117114805_remove_undeleted_groups.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveUndeletedGroups < ActiveRecord::Migration + DOWNTIME = false + + def up + execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;" + end + + def down + # This is an irreversible migration; + # If someone is trying to rollback for other reasons, we should not throw an Exception. + # raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/schema.rb b/db/schema.rb index 8f8a03e1534..9382f8adc4d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161113184239) do +ActiveRecord::Schema.define(version: 20161117114805) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From bf9ab0f33d65df1566fcde8100576d23f5c77a4f Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 17 Nov 2016 17:23:41 +0530 Subject: [PATCH 226/302] Fix a race condition that allowed soft-deleted groups to remain in the database. The intended flow is: Soft-delete group (sync) -> Delete group projects (async) -> Hard-delete group (async) The soft-delete was run in a transaction, which was committed only after the async job (for hard-deletion) was kicked off. There was a race condition here - the soft-delete transaction could complete _after_ the hard delete completed, leaving a soft-deleted record in the database. This commit removes this race condition. There is no need to run the soft-delete in a transaction. The soft-delete completes before the async job is kicked off. --- app/services/destroy_group_service.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index 0081364b8aa..a880952e274 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -6,12 +6,10 @@ class DestroyGroupService end def async_execute - group.transaction do - # Soft delete via paranoia gem - group.destroy - job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) - Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") - end + # Soft delete via paranoia gem + group.destroy + job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) + Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end def execute From f95fa7145b41dbeb0cbaea1e9677fb7dd65a2f04 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Thu, 17 Nov 2016 19:23:42 +0530 Subject: [PATCH 227/302] Write a spec covering the race condition during group deletion. - Use multiple threads / database connections to: 1. Escape the transaction the spec seems to be running in (`config.use_transactional_fixtures` is off, but `ActiveRecord::Base.connection.open_transactions` is not empty at the beginning of the spec. 2. Simulate a Sidekiq worker performing the hard delete outside of the soft-delete transaction. - The spec is a little clunky, but it was the smallest thing I could get working - and even this took a couple of hours. Let me know if you have any suggestions to improve it! --- spec/services/destroy_group_service_spec.rb | 40 +++++++++++++++++++++ spec/support/database_connection_helpers.rb | 9 +++++ 2 files changed, 49 insertions(+) create mode 100644 spec/support/database_connection_helpers.rb diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb index da724643604..538e85cdc89 100644 --- a/spec/services/destroy_group_service_spec.rb +++ b/spec/services/destroy_group_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe DestroyGroupService, services: true do + include DatabaseConnectionHelpers + let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:project) { create(:project, namespace: group) } @@ -50,6 +52,44 @@ describe DestroyGroupService, services: true do describe 'asynchronous delete' do it_behaves_like 'group destruction', true + + context 'potential race conditions' do + context "when the `GroupDestroyWorker` task runs immediately" do + it "deletes the group" do + # Commit the contents of this spec's transaction so far + # so subsequent db connections can see it. + # + # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No + # transaction is currently in progress". Without this, this + # spec will always be green, since the group created in setup + # cannot be seen by any other connections / threads in this spec. + Group.connection.commit_db_transaction + + group_record = run_with_new_database_connection do |conn| + conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first + end + + expect(group_record).not_to be_nil + + # Execute the contents of `GroupDestroyWorker` in a separate thread, to + # simulate data manipulation by the Sidekiq worker (different database + # connection / transaction). + expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id| + Thread.new { m[group_id, user_id] }.join(5) + end + + # Kick off the initial group destroy in a new thread, so that + # it doesn't share this spec's database transaction. + Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5) + + group_record = run_with_new_database_connection do |conn| + conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first + end + + expect(group_record).to be_nil + end + end + end end describe 'synchronous delete' do diff --git a/spec/support/database_connection_helpers.rb b/spec/support/database_connection_helpers.rb new file mode 100644 index 00000000000..763329499f0 --- /dev/null +++ b/spec/support/database_connection_helpers.rb @@ -0,0 +1,9 @@ +module DatabaseConnectionHelpers + def run_with_new_database_connection + pool = ActiveRecord::Base.connection_pool + conn = pool.checkout + yield conn + ensure + pool.checkin(conn) + end +end From 6806fdf080171977b36df99c4bbe8d17c669990a Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Fri, 18 Nov 2016 12:40:13 +0530 Subject: [PATCH 228/302] Update CHANGELOG for !7528. --- changelogs/unreleased/23223-group-deletion-race-condition.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/23223-group-deletion-race-condition.yml diff --git a/changelogs/unreleased/23223-group-deletion-race-condition.yml b/changelogs/unreleased/23223-group-deletion-race-condition.yml new file mode 100644 index 00000000000..6f22e85fb4b --- /dev/null +++ b/changelogs/unreleased/23223-group-deletion-race-condition.yml @@ -0,0 +1,4 @@ +--- +title: Fix race condition during group deletion and remove stale records present due to this bug +merge_request: 7528 +author: Timothy Andrew From 0d04724fa1cd670124b8ad9a3860bfa476c50f99 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 18 Nov 2016 10:00:40 +0100 Subject: [PATCH 229/302] More coverage on service level --- lib/api/helpers.rb | 9 ++ lib/api/services.rb | 7 +- lib/mattermost/presenter.rb | 6 +- spec/lib/gitlab/chat_commands/command_spec.rb | 2 +- .../project_services/chat_service_spec.rb | 15 +++ .../mattermost_command_service_spec.rb | 99 +++++++++++++++++++ spec/requests/api/services_spec.rb | 46 +++++++++ .../chat_names/find_user_service_spec.rb | 2 +- 8 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 spec/models/project_services/chat_service_spec.rb create mode 100644 spec/models/project_services/mattermost_command_service_spec.rb diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 84cc9200d1b..d6526ec4fdc 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -90,6 +90,15 @@ module API @project_service || not_found!("Service") end + def service_by_slug(project, slug) + underscored_service = slug.underscore + + not_found!('Service') unless Service.available_services_names.include?(underscored_service) + service_method = "#{underscored_service}_service" + + service = project.public_send(service_method) + end + def service_attributes @service_attributes ||= project_service.fields.inject([]) do |arr, hash| arr << hash[:name].to_sym diff --git a/lib/api/services.rb b/lib/api/services.rb index b0a94508d10..163187d450d 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -67,12 +67,7 @@ module API post ':id/services/:service_slug/trigger' do project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) - underscored_service = params[:service_slug].underscore - - not_found!('Service') unless Service.available_services_names.include?(underscored_service) - service_method = "#{underscored_service}_service" - - service = project.public_send(service_method) + service = service_by_slug(project, params[:service_slug]) result = service.try(:active?) && service.try(:trigger, params) diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index 7722022c658..d7455d39bce 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -4,7 +4,11 @@ module Mattermost include Rails.application.routes.url_helpers def authorize_chat_name(url) - message = ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." + message = if url + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end ephemeral_response(message) end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 528e690d234..11b607a6843 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::ChatCommands::Command, service: true do - let(:project) { create(:project) } + let(:project) { create(:empty_project) } let(:user) { create(:user) } subject { described_class.new(project, user, params).execute } diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb new file mode 100644 index 00000000000..c6a45a3e1be --- /dev/null +++ b/spec/models/project_services/chat_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe ChatService, models: true do + describe "Associations" do + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + it 'is false as it has no token' do + expect(subject.valid_token?('wer')).to be_falsey + end + end +end diff --git a/spec/models/project_services/mattermost_command_service_spec.rb b/spec/models/project_services/mattermost_command_service_spec.rb new file mode 100644 index 00000000000..757ad687bc4 --- /dev/null +++ b/spec/models/project_services/mattermost_command_service_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe MattermostCommandService, models: true do + describe "Associations" do + it { is_expected.to respond_to :token } + end + + describe '#valid_token?' do + subject { described_class.new } + + context 'when the token is empty' do + it 'is false' do + expect(subject.valid_token?('wer')).to be_falsey + end + end + + context 'when there is a token' do + before do + subject.token = '123' + end + + it 'accepts equal tokens' do + expect(subject.valid_token?('123')).to be_truthy + end + end + end + + describe '#trigger' do + subject { described_class.new } + + context 'no token is passed' do + let(:params) { Hash.new } + + it 'returns nil' do + expect(subject.trigger(params)).to be_nil + end + end + + context 'with a token passed' do + let(:project) { create(:empty_project) } + let(:params) { { token: 'token' } } + + before do + allow(subject).to receive(:token).and_return('token') + end + + context 'no user can be found' do + context 'when no url can be generated' do + it 'responds with the authorize url' do + response = subject.trigger(params) + + expect(response[:response_type]).to eq :ephemeral + expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" + end + end + + context 'when an auth url can be generated' do + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_mattermost_command_service( + properties: { token: 'token' } + ) + end + + it 'generates the url' do + response = service.trigger(params) + + expect(response[:text]).to start_with(':wave: Hi there!') + end + end + end + + context 'when the user is authenticated' do + let!(:chat_name) { create(:chat_name, service: service) } + let(:service) do + project.create_mattermost_command_service( + properties: { token: 'token' } + ) + end + let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + it 'triggers the command' do + expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + + service.trigger(params) + end + end + end + end +end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 2aadab3cbe1..fb234ab8ed1 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -88,4 +88,50 @@ describe API::API, api: true do end end end + + describe 'POST /projects/:id/services/:slug/trigger' do + let!(:project) { create(:empty_project) } + let(:service_name) { 'mattermost_command' } + + context 'no service is available' do + it 'returns a not found message' do + post api("/projects/#{project.id}/services/mattermost_command/trigger") + + expect(response).to have_http_status(404) + end + end + + context 'the service exists' do + context 'the service is not active' do + let!(:inactive_service) do + project.create_mattermost_command_service( + active: false, + properties: { token: 'token' } + ) + end + + it 'when the service is inactive' do + post api("/projects/#{project.id}/services/mattermost_command/trigger") + + expect(response).to have_http_status(404) + end + end + + context 'the service is active' do + let!(:active_service) do + project.create_mattermost_command_service( + active: true, + properties: { token: 'token' } + ) + end + let(:params) { { token: 'token' } } + + it 'retusn status 200' do + post api("/projects/#{project.id}/services/mattermost_command/trigger"), params + + expect(response).to have_http_status(200) + end + end + end + end end diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index 5b885b2c657..51441e8f3be 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -13,7 +13,7 @@ describe ChatNames::FindUserService, services: true do context 'when existing user is requested' do let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } } - it 'returns existing user' do + it 'returns the existing user' do is_expected.to eq(user) end From b8607576c1bb25fe2cbb575b48becdf7bd4aaa8c Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 18 Nov 2016 10:10:41 +0100 Subject: [PATCH 230/302] Projects have chat services --- app/models/project.rb | 1 + spec/models/project_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index fdaa973098a..2c6b43bafdf 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -77,6 +77,7 @@ class Project < ActiveRecord::Base has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit, dependent: :destroy + has_many :chat_services, dependent: :destroy # Project services has_one :campfire_service, dependent: :destroy diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 45e0d783885..08eb3bc9cd4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -20,6 +20,7 @@ describe Project, models: true do it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } + it { is_expected.to have_many(:chat_services).dependent(:destroy) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } From cd222357ade5f47f8d52f22dea66965c580a5843 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 10:13:42 +0100 Subject: [PATCH 231/302] refactor events --- lib/gitlab/cycle_analytics/test_event.rb | 20 +++---------------- .../cycle_analytics/production_event_spec.rb | 10 +++++----- .../cycle_analytics/review_event_spec.rb | 8 ++++---- 3 files changed, 12 insertions(+), 26 deletions(-) diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb index 00404d7f6b0..d553d0b5aec 100644 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ b/lib/gitlab/cycle_analytics/test_event.rb @@ -1,26 +1,12 @@ module Gitlab module CycleAnalytics - class TestEvent < BaseEvent + class TestEvent < StagingEvent def initialize(*args) + super(*args) + @stage = :test @start_time_attrs = mr_metrics_table[:latest_build_started_at] @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - @projections = [build_table[:id]] - @order = build_table[:created_at] - - super(*args) - end - - def custom_query(base_query) - base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) - end - - private - - def serialize(event) - build = ::Ci::Build.find(event['id']) - - AnalyticsBuildSerializer.new.represent(build).as_json end end end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb index 6ee54f246a7..5d12365c430 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ProductionEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end end -end +end \ No newline at end of file diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb index 890e3ab3078..1ff53aa0227 100644 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' describe Gitlab::CycleAnalytics::ReviewEvent do - it_behaves_like 'default query config' - - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) + it_behaves_like 'default query config' do + it 'has the default order' do + expect(event.order).to eq(event.start_time_attrs) + end end end From c76ef847817ac6346b2f367be63f482ee2df382a Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 10:50:47 +0100 Subject: [PATCH 232/302] run pipeline worker in cycle analytics dev setup --- db/fixtures/development/17_cycle_analytics.rb | 2 ++ spec/lib/gitlab/cycle_analytics/production_event_spec.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index e882a492757..916ee8dbac8 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -203,6 +203,8 @@ class Gitlab::Seeder::CycleAnalytics pipeline.run! Timecop.travel rand(1..6).hours.from_now pipeline.succeed! + + PipelineMetricsWorker.new.perform(pipeline.id) end end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb index 5d12365c430..ac17e3b4287 100644 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb @@ -7,4 +7,4 @@ describe Gitlab::CycleAnalytics::ProductionEvent do expect(event.order).to eq(event.start_time_attrs) end end -end \ No newline at end of file +end From 120c53cde2bba0d6616c9c4b1948d557399594aa Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 10:37:20 +0000 Subject: [PATCH 233/302] Improve after review --- .../javascripts/environments/components/environment.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 28377cd5183..41bef3af490 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -111,7 +111,7 @@ this.isLoading = true; - return window.gl.environmentsService.all() + return gl.environmentsService.all() .then(resp => resp.json()) .then((json) => { this.store.storeEnvironments(json); From f749fb7fe0574d07eeb38561b9af62754e518281 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 18 Nov 2016 11:38:54 +0100 Subject: [PATCH 234/302] Improve style, add more tests --- .../unreleased/zj-slash-commands-mattermost.yml | 2 +- lib/api/helpers.rb | 13 ++----------- lib/api/services.rb | 6 ++++-- lib/gitlab/chat_commands/base_command.rb | 6 +++--- lib/mattermost/presenter.rb | 6 +++--- spec/lib/gitlab/chat_commands/command_spec.rb | 6 +++++- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 2 +- spec/lib/gitlab/import_export/all_models.yml | 4 +++- .../mattermost_command_service_spec.rb | 10 +++++----- spec/requests/api/services_spec.rb | 12 +++++++++++- 10 files changed, 38 insertions(+), 29 deletions(-) diff --git a/changelogs/unreleased/zj-slash-commands-mattermost.yml b/changelogs/unreleased/zj-slash-commands-mattermost.yml index 84aca57e666..996ffe954f3 100644 --- a/changelogs/unreleased/zj-slash-commands-mattermost.yml +++ b/changelogs/unreleased/zj-slash-commands-mattermost.yml @@ -1,4 +1,4 @@ --- -title: Add first slash commands +title: Added Mattermost slash command merge_request: 7438 author: diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index d6526ec4fdc..2c593dbb4ea 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -85,20 +85,11 @@ module API end end - def project_service - @project_service ||= user_project.find_or_initialize_service(params[:service_slug].underscore) + def project_service(project = user_project) + @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore) @project_service || not_found!("Service") end - def service_by_slug(project, slug) - underscored_service = slug.underscore - - not_found!('Service') unless Service.available_services_names.include?(underscored_service) - service_method = "#{underscored_service}_service" - - service = project.public_send(service_method) - end - def service_attributes @service_attributes ||= project_service.fields.inject([]) do |arr, hash| arr << hash[:name].to_sym diff --git a/lib/api/services.rb b/lib/api/services.rb index 163187d450d..e3c6a998631 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -67,7 +67,9 @@ module API post ':id/services/:service_slug/trigger' do project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) - service = service_by_slug(project, params[:service_slug]) + not_found! unless project + + service = project_service(project) result = service.try(:active?) && service.try(:trigger, params) @@ -75,7 +77,7 @@ module API status result[:status] || 200 present result else - not_found!('Service') + not_found! end end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index f84aca5365d..e59d69b72b9 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -3,7 +3,7 @@ module Gitlab class BaseCommand QUERY_LIMIT = 5 - def self.match(_) + def self.match(_text) raise NotImplementedError end @@ -11,11 +11,11 @@ module Gitlab raise NotImplementedError end - def self.available?(_) + def self.available?(_project) raise NotImplementedError end - def self.allowed?(_, _) + def self.allowed?(_user, _ability) true end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index d7455d39bce..b4e7358770f 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -1,7 +1,7 @@ module Mattermost class Presenter class << self - include Rails.application.routes.url_helpers + include Gitlab::Routing.url_helpers def authorize_chat_name(url) message = if url @@ -14,7 +14,7 @@ module Mattermost end def help(commands, trigger) - if commands.count == 0 + if commands.empty? ephemeral_response("No commands configured") unless messages.count > 1 else message = header_with_list("Available commands", commands) @@ -50,7 +50,7 @@ module Mattermost end def single_resource(resource) - return error(resource) if resource.errors.any? + return error(resource) if resource.errors.any? || !resource.persisted? message = "### #{title(resource)}" message << "\n\n#{resource.description}" if resource.description diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 11b607a6843..f44c848479e 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::ChatCommands::Command, service: true do subject { described_class.new(project, user, params).execute } describe '#execute' do - context 'when no command is not available' do + context 'when no command is available' do let(:params) { { text: 'issue show 1' } } let(:project) { create(:project, has_external_issue_tracker: true) } @@ -45,6 +45,10 @@ describe Gitlab::ChatCommands::Command, service: true do it 'presents the issue' do expect(subject[:text]).to match("my new issue") end + + it 'shows a link to the new issue' do + expect(subject[:text]).to match(/\/issues\/\d+/) + end end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 4831f24efed..df0c317ccea 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do end end - describe 'self.match' do + describe '.match' do it 'matches the title without description' do match = described_class.match("issue create my title") diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 02b11bd999a..bc837299b9e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -116,6 +116,7 @@ project: - base_tags - tag_taggings - tags +- chat_services - creator - group - namespace @@ -127,6 +128,7 @@ project: - emails_on_push_service - builds_email_service - pipelines_email_service +- mattermost_command_service - irker_service - pivotaltracker_service - hipchat_service @@ -188,4 +190,4 @@ award_emoji: - awardable - user priorities: -- label \ No newline at end of file +- label diff --git a/spec/models/project_services/mattermost_command_service_spec.rb b/spec/models/project_services/mattermost_command_service_spec.rb index 757ad687bc4..21bf1d35e3f 100644 --- a/spec/models/project_services/mattermost_command_service_spec.rb +++ b/spec/models/project_services/mattermost_command_service_spec.rb @@ -55,11 +55,11 @@ describe MattermostCommandService, models: true do end context 'when an auth url can be generated' do - let(:params) do + let(:params) do { - team_domain: 'http://domain.tld', - team_id: 'T3423423', - user_id: 'U234234', + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', user_name: 'mepmep', token: 'token' } @@ -72,7 +72,7 @@ describe MattermostCommandService, models: true do end it 'generates the url' do - response = service.trigger(params) + response = service.trigger(params) expect(response[:text]).to start_with(':wave: Hi there!') end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index fb234ab8ed1..765d662e52b 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -102,6 +102,8 @@ describe API::API, api: true do end context 'the service exists' do + let(:params) { { token: 'token' } } + context 'the service is not active' do let!(:inactive_service) do project.create_mattermost_command_service( @@ -124,7 +126,6 @@ describe API::API, api: true do properties: { token: 'token' } ) end - let(:params) { { token: 'token' } } it 'retusn status 200' do post api("/projects/#{project.id}/services/mattermost_command/trigger"), params @@ -132,6 +133,15 @@ describe API::API, api: true do expect(response).to have_http_status(200) end end + + context 'when the project can not be found' do + it 'returns a generic 404' do + post api("/projects/404/services/mattermost_command/trigger"), params + + expect(response).to have_http_status(404) + expect(json_response["message"]).to eq '404 Not Found' + end + end end end end From 2fb862ccc6687273ada997743392d9dab7b31d08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Fri, 18 Nov 2016 12:02:45 +0100 Subject: [PATCH 235/302] Rely on Gitlab::Shell public API, not reading internal files it may use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It ensures we rely on `Gitlab::Shell`'s public API and not on its internal. Since `Gitlab::Shell` is caching the value of its token, the only way to get the correct token is to call `Gitlab::Shell.secret_token`, not to read the `Gitlab.config.gitlab_shell.secret_file` file! Signed-off-by: Rémy Coutable <remy@rymai.me> --- spec/requests/api/internal_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 8f1a1f9e827..03796f20e36 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let(:user) { create(:user) } let(:key) { create(:key, user: user) } let(:project) { create(:project) } - let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file } + let(:secret_token) { Gitlab::Shell.secret_token } describe "GET /internal/check", no_db: true do it do From cb353d655bd8802c14a1e12e0fe78a1f7cc2e4ed Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 12:05:29 +0100 Subject: [PATCH 236/302] added new build updater, specs and refactored allowed_ids --- .../cycle_analytics/events_controller.rb | 10 +++++-- lib/gitlab/cycle_analytics/author_updater.rb | 24 ++------------- lib/gitlab/cycle_analytics/build_updater.rb | 9 ++++++ lib/gitlab/cycle_analytics/code_event.rb | 6 ++-- lib/gitlab/cycle_analytics/issue_allowed.rb | 9 ++++++ lib/gitlab/cycle_analytics/issue_event.rb | 6 ++-- .../cycle_analytics/merge_request_allowed.rb | 9 ++++++ .../cycle_analytics/production_event.rb | 6 ++-- lib/gitlab/cycle_analytics/review_event.rb | 6 ++-- lib/gitlab/cycle_analytics/staging_event.rb | 10 +++++-- lib/gitlab/cycle_analytics/updater.rb | 30 +++++++++++++++++++ .../cycle_analytics/author_updater_spec.rb | 12 ++++++++ .../cycle_analytics/build_updater_spec.rb | 12 ++++++++ 13 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 lib/gitlab/cycle_analytics/build_updater.rb create mode 100644 lib/gitlab/cycle_analytics/issue_allowed.rb create mode 100644 lib/gitlab/cycle_analytics/merge_request_allowed.rb create mode 100644 lib/gitlab/cycle_analytics/updater.rb create mode 100644 spec/lib/gitlab/cycle_analytics/author_updater_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/build_updater_spec.rb diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 8c2fb98a120..d5e98228ddc 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -5,7 +5,9 @@ module Projects before_action :authorize_read_cycle_analytics! before_action :authorize_builds!, only: [:test, :staging] - + before_action :authorize_read_issue!, only: [:issue, :production] + before_action :authorize_read_merge_request!, only: [:code, :review] + def issue render_events(events.issue_events) end @@ -60,7 +62,11 @@ module Projects end def authorize_builds! - return access_denied! unless current_user.can?(:read_build, project) + return access_denied! unless can?(current_user, :read_build, project) + end + + def authorize_read_issue! + return access_denied! unless can?(current_user, :read_issue, project) end end end diff --git a/lib/gitlab/cycle_analytics/author_updater.rb b/lib/gitlab/cycle_analytics/author_updater.rb index ab18f292bc3..87d26701d3a 100644 --- a/lib/gitlab/cycle_analytics/author_updater.rb +++ b/lib/gitlab/cycle_analytics/author_updater.rb @@ -1,26 +1,8 @@ module Gitlab module CycleAnalytics - class AuthorUpdater - def self.update!(*args) - new(*args).update! - end - - def initialize(event_result) - @event_result = event_result - end - - def update! - @event_result.each do |event| - event['author'] = users[event.delete('author_id').to_i].first - end - end - - def user_ids - @event_result.map { |event| event['author_id'] } - end - - def users - @users ||= User.find(user_ids).group_by { |user| user['id'] } + class AuthorUpdater < Updater + def self.update!(event_result) + new(event_result, User, :author).update! end end end diff --git a/lib/gitlab/cycle_analytics/build_updater.rb b/lib/gitlab/cycle_analytics/build_updater.rb new file mode 100644 index 00000000000..c1190d9e7a9 --- /dev/null +++ b/lib/gitlab/cycle_analytics/build_updater.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + class BuildUpdater < Updater + def self.update!(event_result) + new(event_result, ::Ci::Build, :build, 'id').update! + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb index 02a3a44d544..2afdf0b8518 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/code_event.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class CodeEvent < BaseEvent + include MergeRequestAllowed + def initialize(*args) @stage = :code @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] @@ -21,10 +23,6 @@ module Gitlab def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end - - def allowed_ids - @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end end end end diff --git a/lib/gitlab/cycle_analytics/issue_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb new file mode 100644 index 00000000000..a7652a70641 --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_allowed.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module IssueAllowed + def allowed_ids + @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb index 36a990d6222..705b7e5ce24 100644 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class IssueEvent < BaseEvent + include IssueAllowed + def initialize(*args) @stage = :issue @start_time_attrs = issue_table[:created_at] @@ -20,10 +22,6 @@ module Gitlab def serialize(event) AnalyticsIssueSerializer.new(project: @project).represent(event).as_json end - - def allowed_ids - @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end end end end diff --git a/lib/gitlab/cycle_analytics/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb new file mode 100644 index 00000000000..28f6db44759 --- /dev/null +++ b/lib/gitlab/cycle_analytics/merge_request_allowed.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module MergeRequestAllowed + def allowed_ids + @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb index fcf2dbe3490..4868c3c6237 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/production_event.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class ProductionEvent < BaseEvent + include IssueAllowed + def initialize(*args) @stage = :production @start_time_attrs = issue_table[:created_at] @@ -19,10 +21,6 @@ module Gitlab def serialize(event) AnalyticsIssueSerializer.new(project: @project).represent(event).as_json end - - def allowed_ids - @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end end end end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb index 30650537afe..b394a02cc52 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/review_event.rb @@ -1,6 +1,8 @@ module Gitlab module CycleAnalytics class ReviewEvent < BaseEvent + include MergeRequestAllowed + def initialize(*args) @stage = :review @start_time_attrs = mr_table[:created_at] @@ -18,10 +20,6 @@ module Gitlab def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end - - def allowed_ids - @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end end end end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb index 800b2b786dd..6c42b1a3a60 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event.rb @@ -11,6 +11,12 @@ module Gitlab super(*args) end + def fetch + BuildUpdater.update!(event_result) + + super + end + def custom_query(base_query) base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) end @@ -18,9 +24,7 @@ module Gitlab private def serialize(event) - build = ::Ci::Build.find(event['id']) - - AnalyticsBuildSerializer.new.represent(build).as_json + AnalyticsBuildSerializer.new.represent(event['build']).as_json end end end diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb new file mode 100644 index 00000000000..38cbfa03030 --- /dev/null +++ b/lib/gitlab/cycle_analytics/updater.rb @@ -0,0 +1,30 @@ +module Gitlab + module CycleAnalytics + class Updater + def self.update!(*args) + new(*args).update! + end + + def initialize(event_result, update_klass, update_key, column = nil) + @event_result = event_result + @update_klass = update_klass + @update_key = update_key.to_s + @column = column || "#{@update_key}_id" + end + + def update! + @event_result.each do |event| + event[@update_key] = items[event.delete(@column).to_i].first + end + end + + def result_ids + @event_result.map { |event| event[@column] } + end + + def items + @items ||= @update_klass.find(result_ids).group_by { |item| item['id'] } + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb b/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb new file mode 100644 index 00000000000..f9e4d1714e6 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::AuthorUpdater do + let(:user) { create(:user) } + let(:events) { [{ 'author_id' => user.id }] } + + it 'maps the correct user' do + described_class.update!(events) + + expect(events.first['author']).to eq(user) + end +end diff --git a/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb b/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb new file mode 100644 index 00000000000..8e461f6c6ea --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::BuildUpdater do + let(:build) { create(:ci_build) } + let(:events) { [{ 'id' => build.id }] } + + it 'maps the correct user' do + described_class.update!(events) + + expect(events.first['build']).to eq(build) + end +end From d0552ade9882ebbd247f65812b17b2f01776eaa5 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 11:08:12 +0000 Subject: [PATCH 237/302] Try to use slots --- .../components/environment_actions.js.es6 | 43 +++++++++---------- .../stylesheets/pages/environments.scss | 4 ++ .../projects/environments/index.html.haml | 6 ++- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index ca1a5861f30..2bac40b3c6f 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -23,39 +23,38 @@ * */ mounted() { - const playIcon = document.querySelector('.play-icon-svg.hidden svg'); - - const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); - const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); - // Phantomjs does not have support to iterate a nodelist. - const actionsArray = [].slice.call(actionContainers); - - if (playIcon && actionsArray && dropdownContainer) { - dropdownContainer.appendChild(playIcon.cloneNode(true)); - - actionsArray.forEach((element) => { - element.appendChild(playIcon.cloneNode(true)); - }); - } + // const playIcon = document.querySelector('.play-icon-svg.hidden svg'); + // + // const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); + // const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); + // // Phantomjs does not have support to iterate a nodelist. + // const actionsArray = [].slice.call(actionContainers); + // + // if (playIcon && actionsArray && dropdownContainer) { + // dropdownContainer.appendChild(playIcon.cloneNode(true)); + // + // actionsArray.forEach((element) => { + // element.appendChild(playIcon.cloneNode(true)); + // }); + // } }, template: ` <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <span class="dropdown-play-icon-container"> - <!-- svg goes here --> - </span> + <slot name="actionplayicon"></slot> <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="action-play-icon-container"> - <!-- svg goes here --> - </span> - <span v-html="action.name"></span> + <a :href="action.play_path" + data-method="post" + rel="nofollow" + class="js-manual-action-link"> + <slot name="actionplayicon"></slot> + <span v-html="action.name"></span> </a> </li> </ul> diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e9ff43a8adb..cecf3f325fd 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -15,6 +15,10 @@ } } +.play-icon-svg span { + display: inline-block; +} + .environments { table-layout: fixed; diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index b641d2cec34..df94fa9cfb5 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -6,8 +6,10 @@ = page_specific_javascript_tag("environments/environments_bundle.js") .commit-icon-svg.hidden = custom_icon("icon_commit") -.play-icon-svg.hidden - = custom_icon("icon_play") + +.play-icon-svg{ slot: "actionplayicon" } + %span.hide + = custom_icon("icon_play") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, From 0aa477884c6ac3298f79f62e08e63294d81735a3 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 12:09:36 +0100 Subject: [PATCH 238/302] fix small typo --- spec/lib/gitlab/cycle_analytics/build_updater_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb b/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb index 8e461f6c6ea..70886d7b453 100644 --- a/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::CycleAnalytics::BuildUpdater do let(:build) { create(:ci_build) } let(:events) { [{ 'id' => build.id }] } - it 'maps the correct user' do + it 'maps the correct build' do described_class.update!(events) expect(events.first['build']).to eq(build) From c76d7afdf6e2575e6170928570dc35d386842042 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 11:18:03 +0000 Subject: [PATCH 239/302] Fixes typo Removes unneeded comparision --- .../environments/stores/environments_store.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 928786f0741..83bbc0145de 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -89,17 +89,17 @@ toggleFolder(envType) { const environments = this.state.environments; - const environmnetsCopy = environments.map((env) => { - if (env['vue-isChildren'] === true && env.name === envType) { + const environmentsCopy = environments.map((env) => { + if (env['vue-isChildren'] && env.name === envType) { env.isOpen = !env.isOpen; } return env; }); - this.state.environments = environmnetsCopy; + this.state.environments = environmentsCopy; - return environmnetsCopy; + return environmentsCopy; }, /** From 8996548e1353285a934fff41caa99a0da434c24b Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Fri, 18 Nov 2016 13:20:59 +0200 Subject: [PATCH 240/302] Correct curl examples for archive and unarchive project API [ci skip] --- doc/api/projects.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/projects.md b/doc/api/projects.md index bbb3bfb4995..467a880ac13 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -850,7 +850,7 @@ POST /projects/:id/archive | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive" ``` Example response: @@ -939,7 +939,7 @@ POST /projects/:id/unarchive | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | ```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive" +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive" ``` Example response: From 53f7fcd0c19a5eb1245f4c0a3c0f953c63b5558f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 11:26:20 +0000 Subject: [PATCH 241/302] Remove slots --- .../components/environment_actions.js.es6 | 36 ++++++++++--------- .../projects/environments/index.html.haml | 6 ++-- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 2bac40b3c6f..f3042bbc528 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -23,27 +23,29 @@ * */ mounted() { - // const playIcon = document.querySelector('.play-icon-svg.hidden svg'); - // - // const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); - // const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); - // // Phantomjs does not have support to iterate a nodelist. - // const actionsArray = [].slice.call(actionContainers); - // - // if (playIcon && actionsArray && dropdownContainer) { - // dropdownContainer.appendChild(playIcon.cloneNode(true)); - // - // actionsArray.forEach((element) => { - // element.appendChild(playIcon.cloneNode(true)); - // }); - // } + const playIcon = document.querySelector('.play-icon-svg.hidden svg'); + + const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container'); + const actionContainers = this.$el.querySelectorAll('.action-play-icon-container'); + // Phantomjs does not have support to iterate a nodelist. + const actionsArray = [].slice.call(actionContainers); + + if (playIcon && actionsArray && dropdownContainer) { + dropdownContainer.appendChild(playIcon.cloneNode(true)); + + actionsArray.forEach((element) => { + element.appendChild(playIcon.cloneNode(true)); + }); + } }, template: ` <div class="inline"> <div class="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown"> - <slot name="actionplayicon"></slot> + <span class="dropdown-play-icon-container"> + <!-- svg goes here --> + </span> <i class="fa fa-caret-down"></i> </a> @@ -53,7 +55,9 @@ data-method="post" rel="nofollow" class="js-manual-action-link"> - <slot name="actionplayicon"></slot> + <span class="action-play-icon-container"> + <!-- svg goes here --> + </span> <span v-html="action.name"></span> </a> </li> diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index df94fa9cfb5..b641d2cec34 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -6,10 +6,8 @@ = page_specific_javascript_tag("environments/environments_bundle.js") .commit-icon-svg.hidden = custom_icon("icon_commit") - -.play-icon-svg{ slot: "actionplayicon" } - %span.hide - = custom_icon("icon_play") +.play-icon-svg.hidden + = custom_icon("icon_play") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, From dd826a5f20837f33263c658e41a4def0fc932069 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 18 Nov 2016 12:08:30 +0100 Subject: [PATCH 242/302] Return a consistent not found message This prevents leakage of project names on an endpoint which is unauthenticated and thus open to the world. --- app/models/project.rb | 2 +- db/schema.rb | 20 ++++++++++++++++---- lib/api/services.rb | 5 +++-- lib/mattermost/presenter.rb | 4 ++-- spec/models/project_spec.rb | 2 +- spec/requests/api/services_spec.rb | 5 +++-- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 2c6b43bafdf..e6ae91f259d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -77,7 +77,7 @@ class Project < ActiveRecord::Base has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event' has_many :boards, before_add: :validate_board_limit, dependent: :destroy - has_many :chat_services, dependent: :destroy + has_many :chat_services # Project services has_one :campfire_service, dependent: :destroy diff --git a/db/schema.rb b/db/schema.rb index 8f8a03e1534..97a28ccad26 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161113184239) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" end create_table "audit_events", force: :cascade do |t| @@ -384,6 +384,17 @@ ActiveRecord::Schema.define(version: 20161113184239) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree + create_table "custom_emoji", force: :cascade do |t| + t.integer "project_id", null: false + t.string "name" + t.string "emoji" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "custom_emoji", ["project_id", "name"], name: "index_custom_emoji_on_project_id_and_name", unique: true, using: :btree + add_index "custom_emoji", ["project_id"], name: "index_custom_emoji_on_project_id", using: :btree + create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -930,7 +941,7 @@ ActiveRecord::Schema.define(version: 20161113184239) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved" + t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1254,6 +1265,7 @@ ActiveRecord::Schema.define(version: 20161113184239) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" + add_foreign_key "custom_emoji", "projects" add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade diff --git a/lib/api/services.rb b/lib/api/services.rb index e3c6a998631..4d23499aa39 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -67,7 +67,8 @@ module API post ':id/services/:service_slug/trigger' do project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) - not_found! unless project + # This is not accurate, but done to prevent leakage of the project names + not_found!('Service') unless project service = project_service(project) @@ -77,7 +78,7 @@ module API status result[:status] || 200 present result else - not_found! + not_found!('Service') end end end diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index b4e7358770f..f76d0376a98 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -14,8 +14,8 @@ module Mattermost end def help(commands, trigger) - if commands.empty? - ephemeral_response("No commands configured") unless messages.count > 1 + if commands.zero? + ephemeral_response("No commands configured") else message = header_with_list("Available commands", commands) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 08eb3bc9cd4..1972a8bb3f4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -20,7 +20,7 @@ describe Project, models: true do it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } - it { is_expected.to have_many(:chat_services).dependent(:destroy) } + it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 765d662e52b..782d76db318 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -95,9 +95,10 @@ describe API::API, api: true do context 'no service is available' do it 'returns a not found message' do - post api("/projects/#{project.id}/services/mattermost_command/trigger") + post api("/projects/#{project.id}/services/idonotexist/trigger") expect(response).to have_http_status(404) + expect(json_response["message"]).to eq("404 Service Not Found") end end @@ -139,7 +140,7 @@ describe API::API, api: true do post api("/projects/404/services/mattermost_command/trigger"), params expect(response).to have_http_status(404) - expect(json_response["message"]).to eq '404 Not Found' + expect(json_response["message"]).to eq("404 Service Not Found") end end end From 6b5aa83354b3dff903eb099bf03da733c36958fc Mon Sep 17 00:00:00 2001 From: jacopo-beschi-intersail <jacopo.beschi@intersail.it> Date: Mon, 14 Nov 2016 14:11:55 +0100 Subject: [PATCH 243/302] Fix Admin Links to new Group does not respect Default Visibility Settings This is done by passing a visibility_level option to the group edit _form. The visibility_level is set to @group.visibility_level in edit ation and to default_group_visibility in the new action. --- app/views/admin/groups/_form.html.haml | 2 +- app/views/admin/groups/edit.html.haml | 2 +- app/views/admin/groups/new.html.haml | 2 +- ...min-links-new-group-default-visibility.yml | 4 +++ spec/features/admin/admin_groups_spec.rb | 35 +++++++++++++++++++ 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml create mode 100644 spec/features/admin/admin_groups_spec.rb diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 817910f7ddf..589f4557b52 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -7,7 +7,7 @@ .col-sm-10 = render 'shared/choose_group_avatar_button', f: f - = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml index eb09a6328ed..c2b9807015d 100644 --- a/app/views/admin/groups/edit.html.haml +++ b/app/views/admin/groups/edit.html.haml @@ -1,4 +1,4 @@ - page_title "Edit", @group.name, "Groups" %h3.page-title Edit group: #{@group.name} %hr -= render 'form' += render 'form', visibility_level: @group.visibility_level diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml index c81ee552ac3..8f9fe96249f 100644 --- a/app/views/admin/groups/new.html.haml +++ b/app/views/admin/groups/new.html.haml @@ -1,4 +1,4 @@ - page_title "New Group" %h3.page-title New group %hr -= render 'form' += render 'form', visibility_level: default_group_visibility diff --git a/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml new file mode 100644 index 00000000000..18fb8a6ad45 --- /dev/null +++ b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml @@ -0,0 +1,4 @@ +--- +title: Make New Group form respect default visibility application setting +merge_request: 7454 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb new file mode 100644 index 00000000000..f6d625fa7f6 --- /dev/null +++ b/spec/features/admin/admin_groups_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +feature 'Admin Groups', feature: true do + let(:internal) { Gitlab::VisibilityLevel::INTERNAL } + + before do + login_as(:admin) + + stub_application_setting(default_group_visibility: internal) + end + + describe 'create a group' do + scenario 'shows the visibility level radio populated with the default value' do + visit new_admin_group_path + + expect_selected_visibility(internal) + end + end + + describe 'group edit' do + scenario 'shows the visibility level radio populated with the group visibility_level value' do + group = create(:group, :private) + + visit edit_admin_group_path(group) + + expect_selected_visibility(group.visibility_level) + end + end + + def expect_selected_visibility(level) + selector = "#group_visibility_level_#{level}[checked=checked]" + + expect(page).to have_selector(selector, count: 1) + end +end From 2a085e5edddbeb7927d8f8d4413bed8a1ff74f49 Mon Sep 17 00:00:00 2001 From: winniehell <git@winniehell.de> Date: Fri, 18 Nov 2016 12:32:52 +0100 Subject: [PATCH 244/302] Add missing require statements to build_spec --- spec/javascripts/build_spec.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 370944b6a8c..b329ad8062a 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -1,5 +1,7 @@ /* global Build */ /* eslint-disable no-new */ +//= require lib/utils/timeago +//= require lib/utils/datetime_utility //= require build //= require breakpoints //= require jquery.nicescroll From c3adf40fd1f12053737f3f3e30643e974ff2ef44 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Fri, 18 Nov 2016 11:52:18 +0000 Subject: [PATCH 245/302] Remove bashism from scripts/prepare_build.sh --- scripts/prepare_build.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index 1eaafdce389..6e3f76b8399 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh retry() { if eval "$@"; then @@ -24,11 +24,12 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - export FLAGS=(--path vendor --retry 3 --quiet) + export FLAGS="--path vendor --retry 3 --quiet" else - export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin + rnd=$(awk 'BEGIN { srand() ; printf("%d\n",rand()*5) }') + export PATH="$HOME/bin:/usr/local/bin:/usr/bin:/bin" cp config/database.yml.mysql config/database.yml sed "s/username\:.*$/username\: runner/" -i config/database.yml sed "s/password\:.*$/password\: 'password'/" -i config/database.yml - sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml + sed "s/gitlabhq_test/gitlabhq_test_$rnd/" -i config/database.yml fi From f5b792e22eb7bd4ecafcd2ad3bc7a388abb36ffc Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 13:00:38 +0100 Subject: [PATCH 246/302] refactored updater and updated specs --- .../cycle_analytics/events_controller.rb | 10 +------- lib/gitlab/cycle_analytics/author_updater.rb | 9 ------- lib/gitlab/cycle_analytics/base_event.rb | 2 +- lib/gitlab/cycle_analytics/build_updater.rb | 9 ------- lib/gitlab/cycle_analytics/staging_event.rb | 2 +- lib/gitlab/cycle_analytics/updater.rb | 14 +++++------ .../cycle_analytics/author_updater_spec.rb | 12 --------- .../cycle_analytics/build_updater_spec.rb | 12 --------- .../gitlab/cycle_analytics/updater_spec.rb | 25 +++++++++++++++++++ 9 files changed, 35 insertions(+), 60 deletions(-) delete mode 100644 lib/gitlab/cycle_analytics/author_updater.rb delete mode 100644 lib/gitlab/cycle_analytics/build_updater.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/author_updater_spec.rb delete mode 100644 spec/lib/gitlab/cycle_analytics/build_updater_spec.rb create mode 100644 spec/lib/gitlab/cycle_analytics/updater_spec.rb diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index d5e98228ddc..13b3eec761f 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -4,7 +4,7 @@ module Projects include CycleAnalyticsParams before_action :authorize_read_cycle_analytics! - before_action :authorize_builds!, only: [:test, :staging] + before_action :authorize_read_build!, only: [:test, :staging] before_action :authorize_read_issue!, only: [:issue, :production] before_action :authorize_read_merge_request!, only: [:code, :review] @@ -60,14 +60,6 @@ module Projects params[:events].slice(:start_date, :branch_name) end - - def authorize_builds! - return access_denied! unless can?(current_user, :read_build, project) - end - - def authorize_read_issue! - return access_denied! unless can?(current_user, :read_issue, project) - end end end end diff --git a/lib/gitlab/cycle_analytics/author_updater.rb b/lib/gitlab/cycle_analytics/author_updater.rb deleted file mode 100644 index 87d26701d3a..00000000000 --- a/lib/gitlab/cycle_analytics/author_updater.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module CycleAnalytics - class AuthorUpdater < Updater - def self.update!(event_result) - new(event_result, User, :author).update! - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb index cedc73142fa..486139b1687 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event.rb @@ -30,7 +30,7 @@ module Gitlab def update_author! return unless event_result.any? && event_result.first['author_id'] - AuthorUpdater.update!(event_result) + Updater.update!(event_result, from: 'author_id', to: 'author', klass: User) end def event_result diff --git a/lib/gitlab/cycle_analytics/build_updater.rb b/lib/gitlab/cycle_analytics/build_updater.rb deleted file mode 100644 index c1190d9e7a9..00000000000 --- a/lib/gitlab/cycle_analytics/build_updater.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module CycleAnalytics - class BuildUpdater < Updater - def self.update!(event_result) - new(event_result, ::Ci::Build, :build, 'id').update! - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb index 6c42b1a3a60..a1f30b716f6 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event.rb @@ -12,7 +12,7 @@ module Gitlab end def fetch - BuildUpdater.update!(event_result) + Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build) super end diff --git a/lib/gitlab/cycle_analytics/updater.rb b/lib/gitlab/cycle_analytics/updater.rb index 38cbfa03030..953268ebd46 100644 --- a/lib/gitlab/cycle_analytics/updater.rb +++ b/lib/gitlab/cycle_analytics/updater.rb @@ -5,25 +5,25 @@ module Gitlab new(*args).update! end - def initialize(event_result, update_klass, update_key, column = nil) + def initialize(event_result, from:, to:, klass:) @event_result = event_result - @update_klass = update_klass - @update_key = update_key.to_s - @column = column || "#{@update_key}_id" + @klass = klass + @from = from + @to = to end def update! @event_result.each do |event| - event[@update_key] = items[event.delete(@column).to_i].first + event[@to] = items[event.delete(@from).to_i].first end end def result_ids - @event_result.map { |event| event[@column] } + @event_result.map { |event| event[@from] } end def items - @items ||= @update_klass.find(result_ids).group_by { |item| item['id'] } + @items ||= @klass.find(result_ids).group_by { |item| item['id'] } end end end diff --git a/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb b/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb deleted file mode 100644 index f9e4d1714e6..00000000000 --- a/spec/lib/gitlab/cycle_analytics/author_updater_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Gitlab::CycleAnalytics::AuthorUpdater do - let(:user) { create(:user) } - let(:events) { [{ 'author_id' => user.id }] } - - it 'maps the correct user' do - described_class.update!(events) - - expect(events.first['author']).to eq(user) - end -end diff --git a/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb b/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb deleted file mode 100644 index 70886d7b453..00000000000 --- a/spec/lib/gitlab/cycle_analytics/build_updater_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe Gitlab::CycleAnalytics::BuildUpdater do - let(:build) { create(:ci_build) } - let(:events) { [{ 'id' => build.id }] } - - it 'maps the correct build' do - described_class.update!(events) - - expect(events.first['build']).to eq(build) - end -end diff --git a/spec/lib/gitlab/cycle_analytics/updater_spec.rb b/spec/lib/gitlab/cycle_analytics/updater_spec.rb new file mode 100644 index 00000000000..eff54cd3692 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/updater_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::Updater do + describe 'updates authors' do + let(:user) { create(:user) } + let(:events) { [{ 'author_id' => user.id }] } + + it 'maps the correct user' do + described_class.update!(events, from: 'author_id', to: 'author', klass: User) + + expect(events.first['author']).to eq(user) + end + end + + describe 'updates builds' do + let(:build) { create(:ci_build) } + let(:events) { [{ 'id' => build.id }] } + + it 'maps the correct build' do + described_class.update!(events, from: 'id', to: 'build', klass: ::Ci::Build) + + expect(events.first['build']).to eq(build) + end + end +end From 85dd05b5b3afb27743998b4f3f9f042b5b5bfa81 Mon Sep 17 00:00:00 2001 From: Felipe Artur <felipefac@gmail.com> Date: Wed, 9 Nov 2016 19:55:21 -0200 Subject: [PATCH 247/302] Add JIRA remotelinks and prevent duplicated closing messages --- app/models/project_services/jira_service.rb | 114 ++++++++++++------ changelogs/unreleased/issue_13232.yml | 4 + .../project_services/jira_service_spec.rb | 44 +++++++ spec/services/git_push_service_spec.rb | 10 ++ .../merge_requests/merge_service_spec.rb | 6 +- spec/services/system_note_service_spec.rb | 45 ++++++- spec/support/jira_service_helper.rb | 5 + 7 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 changelogs/unreleased/issue_13232.yml diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2dbe0075465..55c9904bd43 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -70,7 +70,7 @@ class JiraService < IssueTrackerService end def jira_project - @jira_project ||= client.Project.find(project_key) + @jira_project ||= jira_request { client.Project.find(project_key) } end def help @@ -128,12 +128,19 @@ class JiraService < IssueTrackerService # we just want to test settings test_settings else - close_issue(push, issue) + jira_issue = jira_request { client.Issue.find(issue.iid) } + + return false unless jira_issue.present? + + close_issue(push, jira_issue) end end def create_cross_reference_note(mentioned, noteable, author) - issue_key = mentioned.id + jira_issue = jira_request { client.Issue.find(mentioned.id) } + + return false unless jira_issue.present? + project = self.project noteable_name = noteable.class.name.underscore.downcase noteable_id = if noteable.is_a?(Commit) @@ -160,7 +167,7 @@ class JiraService < IssueTrackerService } } - add_comment(data, issue_key) + add_comment(data, jira_issue) end # reason why service cannot be tested @@ -181,16 +188,14 @@ class JiraService < IssueTrackerService def test_settings return unless url.present? # Test settings by getting the project - jira_project - - rescue Errno::ECONNREFUSED, JIRA::HTTPError => e - Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}." - false + jira_request { jira_project.present? } end private def close_issue(entity, issue) + return if issue.nil? || issue.resolution.present? + commit_id = if entity.is_a?(Commit) entity.id elsif entity.is_a?(MergeRequest) @@ -200,55 +205,85 @@ class JiraService < IssueTrackerService commit_url = build_entity_url(:commit, commit_id) # Depending on the JIRA project's workflow, a comment during transition - # may or may not be allowed. Split the operation in to two calls so the - # comment always works. - transition_issue(issue) - add_issue_solved_comment(issue, commit_id, commit_url) + # may or may not be allowed. Refresh the issue after transition and check + # if it is closed, so we don't have one comment for every commit. + issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue) + add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution end def transition_issue(issue) - issue = client.Issue.find(issue.iid) issue.transitions.build.save(transition: { id: jira_issue_transition_id }) end def add_issue_solved_comment(issue, commit_id, commit_url) - comment = "Issue solved with [#{commit_id}|#{commit_url}]." - send_message(issue.iid, comment) + link_title = "GitLab: Solved by commit #{commit_id}." + comment = "Issue solved with [#{commit_id}|#{commit_url}]." + link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true) + send_message(issue, comment, link_props) end - def add_comment(data, issue_key) - user_name = data[:user][:name] - user_url = data[:user][:url] - entity_name = data[:entity][:name] - entity_url = data[:entity][:url] + def add_comment(data, issue) + user_name = data[:user][:name] + user_url = data[:user][:url] + entity_name = data[:entity][:name] + entity_url = data[:entity][:url] entity_title = data[:entity][:title] project_name = data[:project][:name] - message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" + message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'" + link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}" + link_props = build_remote_link_props(url: entity_url, title: link_title) - unless comment_exists?(issue_key, message) - send_message(issue_key, message) + unless comment_exists?(issue, message) + send_message(issue, message, link_props) end end - def comment_exists?(issue_key, message) - comments = client.Issue.find(issue_key).comments - comments.map { |comment| comment.body.include?(message) }.any? + def comment_exists?(issue, message) + comments = jira_request { issue.comments } + + comments.present? && comments.any? { |comment| comment.body.include?(message) } end - def send_message(issue_key, message) + def send_message(issue, message, remote_link_props) return unless url.present? - issue = client.Issue.find(issue_key) + jira_request do + if issue.comments.build.save!(body: message) + remote_link = issue.remotelink.build + remote_link.save!(remote_link_props) + result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." + end - if issue.comments.build.save!(body: message) - result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}." + Rails.logger.info(result_message) + result_message + end + end + + # Build remote link on JIRA properties + # Icons here must be available on WEB so JIRA can read the URL + # We are using a open word graphics icon which have LGPL license + def build_remote_link_props(url:, title:, resolved: false) + status = { + resolved: resolved + } + + if resolved + status[:icon] = { + title: 'Closed', + url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png' + } end - Rails.logger.info(result_message) - result_message - rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e - Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" + { + GlobalID: 'GitLab', + object: { + url: url, + title: title, + status: status, + icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' } + } + } end def resource_url(resource) @@ -268,4 +303,13 @@ class JiraService < IssueTrackerService ) ) end + + # Handle errors when doing JIRA API calls + def jira_request + yield + + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e + Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" + nil + end end diff --git a/changelogs/unreleased/issue_13232.yml b/changelogs/unreleased/issue_13232.yml new file mode 100644 index 00000000000..6dc2de5afe4 --- /dev/null +++ b/changelogs/unreleased/issue_13232.yml @@ -0,0 +1,4 @@ +--- +title: Add JIRA remotelinks and prevent duplicated closing messages +merge_request: +author: diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 05ee4a08391..699cd9a5bb2 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -85,17 +85,30 @@ describe JiraService, models: true do project_key: 'GitLabProject' ) + # These stubs are needed to test JiraService#close_issue. + # We close the issue then do another request to API to check if it got closed. + # Here is stubbed the API return with a closed and an opened issues. + open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" }) + closed_issue = open_issue.dup + allow(open_issue).to receive(:resolution).and_return(false) + allow(closed_issue).to receive(:resolution).and_return(true) + allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) + + allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123") + @jira_service.save project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' @project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' + @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink' WebMock.stub_request(:get, @project_url) WebMock.stub_request(:get, project_issues_url) WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) + WebMock.stub_request(:post, @remote_link_url) end it "calls JIRA API" do @@ -106,6 +119,37 @@ describe JiraService, models: true do ).once end + # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links + # for more information + it "creates Remote Link reference in JIRA for comment" do + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + # Creates comment + expect(WebMock).to have_requested(:post, @comment_url) + + # Creates Remote Link in JIRA issue fields + expect(WebMock).to have_requested(:post, @remote_link_url).with( + body: hash_including( + GlobalID: "GitLab", + object: { + url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}", + title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", + icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } } + } + ) + ).once + end + + it "does not send comment or remote links to issues already closed" do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) + + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, @remote_link_url) + end + it "references the GitLab commit/merge request" do @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index cea7e6429f9..a67e071341f 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -492,6 +492,16 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\ncloses JIRA-1" } let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } + before do + open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" }) + closed_issue = open_issue.dup + allow(open_issue).to receive(:resolution).and_return(false) + allow(closed_issue).to receive(:resolution).and_return(true) + allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) + + allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1") + end + context "using right markdown" do it "initiates one api call to jira server to close the issue" do execute_service(project, commit_author, @oldrev, @newrev, @ref ) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f93d7732a9a..1fd9f5a4910 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -67,17 +67,19 @@ describe MergeRequests::MergeService, services: true do it 'closes issues on JIRA issue tracker' do jira_issue = ExternalIssue.new('JIRA-123', project) + stub_jira_urls(jira_issue) commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") allow(merge_request).to receive(:commits).and_return([commit]) - expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once + expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, an_instance_of(JIRA::Resource::Issue)).once service.execute(merge_request) end context "wrong issue markdown" do it 'does not close issues on JIRA issue tracker' do - jira_issue = ExternalIssue.new('#123', project) + jira_issue = ExternalIssue.new('#JIRA-123', project) + stub_jira_urls(jira_issue) commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}") allow(merge_request).to receive(:commits).and_return([commit]) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5bb107fdd85..56d39e9a005 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe SystemNoteService, services: true do + include Gitlab::Routing.url_helpers + let(:project) { create(:project) } let(:author) { create(:user) } let(:noteable) { create(:issue, project: project) } @@ -543,23 +545,55 @@ describe SystemNoteService, services: true do before { stub_jira_urls(jira_issue.id) } - context 'in JIRA issue tracker' do + context 'in issue' do before { jira_service_settings } describe "new reference" do subject { described_class.cross_reference(jira_issue, commit, author) } it { is_expected.to eq(success_message) } + + it "creates remote link" do + subject + + expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with( + body: hash_including( + GlobalID: "GitLab", + object: { + url: namespace_project_commit_url(project.namespace, project, commit), + title: "GitLab: Mentioned on commit - #{commit.title}", + icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + status: { resolved: false } + } + ) + ).once + end end end - context 'issue from an issue' do + context 'in commit' do context 'in JIRA issue tracker' do before { jira_service_settings } subject { described_class.cross_reference(jira_issue, issue, author) } it { is_expected.to eq(success_message) } + + it "creates remote link" do + subject + + expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with( + body: hash_including( + GlobalID: "GitLab", + object: { + url: namespace_project_issue_url(project.namespace, project, issue), + title: "GitLab: Mentioned on issue - #{issue.title}", + icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + status: { resolved: false } + } + ) + ).once + end end end @@ -572,6 +606,13 @@ describe SystemNoteService, services: true do subject { described_class.cross_reference(jira_issue, commit, author) } it { is_expected.not_to eq(success_message) } + + it 'does not try to create comment and remote link' do + subject + + expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue)) + expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue)) + end end end end diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb index 96e0dad6b55..7437ba2688d 100644 --- a/spec/support/jira_service_helper.rb +++ b/spec/support/jira_service_helper.rb @@ -57,6 +57,10 @@ module JiraServiceHelper JIRA_API + "/issue/#{issue_id}/comment" end + def jira_api_remote_link_url(issue_id) + JIRA_API + "/issue/#{issue_id}/remotelink" + end + def jira_api_transition_url(issue_id) JIRA_API + "/issue/#{issue_id}/transitions" end @@ -75,6 +79,7 @@ module JiraServiceHelper WebMock.stub_request(:get, jira_issue_url(issue_id)) WebMock.stub_request(:get, jira_api_test_url) WebMock.stub_request(:post, jira_api_comment_url(issue_id)) + WebMock.stub_request(:post, jira_api_remote_link_url(issue_id)) WebMock.stub_request(:post, jira_api_transition_url(issue_id)) end end From 9c4e0d64451bd76b829e1bb66afab892e19926da Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 18 Nov 2016 20:17:10 +0800 Subject: [PATCH 248/302] Use `Gitlab.config.gitlab.host` over `'localhost'` This would fix long standing failures running tests on my development machine, which set `Gitlab.config.gitlab.host` to another host because it's not my local computer. Now I finally cannot withstand it and decided to fix them once and for all. --- doc/development/testing.md | 1 + spec/helpers/application_helper_spec.rb | 4 ++-- spec/helpers/gitlab_markdown_helper_spec.rb | 2 +- spec/lib/gitlab/middleware/go_spec.rb | 2 +- spec/mailers/emails/profile_spec.rb | 2 +- spec/models/key_spec.rb | 2 +- .../project_services/gitlab_issue_tracker_service_spec.rb | 6 +++--- spec/models/project_spec.rb | 4 ++-- spec/requests/api/internal_spec.rb | 2 +- spec/services/git_push_service_spec.rb | 2 +- spec/services/merge_requests/get_urls_service_spec.rb | 6 +++--- 11 files changed, 17 insertions(+), 16 deletions(-) diff --git a/doc/development/testing.md b/doc/development/testing.md index 4dc535fb359..6106e47daa0 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -70,6 +70,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Try to match the ordering of tests to the ordering within the class. - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines to separate phases. +- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'` [four-phase-test]: https://robots.thoughtbot.com/four-phase-test diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index c706e418d26..15863d444f8 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -57,7 +57,7 @@ describe ApplicationHelper do it 'returns an url for the avatar' do project = create(:project, avatar: File.open(avatar_file_path)) - avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif" + avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" end @@ -67,7 +67,7 @@ describe ApplicationHelper do allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true) - avatar_url = 'http://localhost' + namespace_project_avatar_path(project.namespace, project) + avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match( image_tag(avatar_url)) end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 5368e5fab06..1d494edcd3b 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do it 'replaces commit message with emoji to link' do actual = link_to_gfm(':book:Book', '/foo') expect(actual). - to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) + to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 117a15264da..fd3769d75b5 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -22,7 +22,7 @@ describe Gitlab::Middleware::Go, lib: true do resp = middleware.call(env) expect(resp[0]).to eq(200) expect(resp[1]['Content-Type']).to eq('text/html') - expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" + expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n" expect(resp[2].body).to eq([expected_body]) end end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb index 14bc062ef12..e1877d5fde0 100644 --- a/spec/mailers/emails/profile_spec.rb +++ b/spec/mailers/emails/profile_spec.rb @@ -25,7 +25,7 @@ describe Notify do it 'includes a link for user to set password' do params = "reset_password_token=#{token}" is_expected.to have_body_text( - %r{http://localhost(:\d+)?/users/password/edit\?#{params}} + %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}} ) end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 1a26cee9f3d..90731f55470 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -19,7 +19,7 @@ describe Key, models: true do describe "#publishable_keys" do it 'replaces SSH key comment with simple identifier of username + hostname' do - expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)") + expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})") end end end diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index 652804fb444..9b80f0e7296 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -35,9 +35,9 @@ describe GitlabIssueTrackerService, models: true do end it 'gives the correct path' do - expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432") + expect(@service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432") end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 46fa00a79c4..42aa617cb1e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -700,7 +700,7 @@ describe Project, models: true do "/uploads/project/avatar/#{project.id}/uploads/avatar.png" end - it { should eq "http://localhost#{avatar_path}" } + it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } end context 'When avatar file in git' do @@ -712,7 +712,7 @@ describe Project, models: true do "/#{project.namespace.name}/#{project.path}/avatar" end - it { should eq "http://localhost#{avatar_path}" } + it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } end context 'when git repo is empty' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 8f1a1f9e827..d91628807ef 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -406,7 +406,7 @@ describe API::API, api: true do it 'returns link to create new merge request' do expect(json_response).to match [{ "branch_name" => "new_branch", - "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", "new_merge_request" => true }] end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index cea7e6429f9..6358f1fed71 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -490,7 +490,7 @@ describe GitPushService, services: true do context "closing an issue" do let(:message) { "this is some work.\n\ncloses JIRA-1" } - let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } + let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } context "using right markdown" do it "initiates one api call to jira server to close the issue" do diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 3a71776e81f..08829e4be70 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -4,8 +4,8 @@ describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public) } let(:service) { MergeRequests::GetUrlsService.new(project) } let(:source_branch) { "my_branch" } - let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } - let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -115,7 +115,7 @@ describe MergeRequests::GetUrlsService do let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } - let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } it 'returns 2 urls for both creating new and showing merge request' do result = service.execute(changes) From 7c5af3ea92e213423f8f0e376cbb8fd8b863f22c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Fri, 18 Nov 2016 14:20:45 +0200 Subject: [PATCH 249/302] More aggressively preload on merge request and issue index pages --- app/controllers/concerns/issuable_collections.rb | 4 ++-- app/controllers/concerns/issues_action.rb | 1 - app/controllers/concerns/merge_requests_action.rb | 1 - app/controllers/projects/merge_requests_controller.rb | 1 - app/helpers/issuables_helper.rb | 6 ++++-- app/views/projects/merge_requests/_merge_request.html.haml | 3 +++ changelogs/unreleased/optimize-mr-index.yml | 4 ++++ 7 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 changelogs/unreleased/optimize-mr-index.yml diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index b5e79099e39..6247934f81e 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -10,11 +10,11 @@ module IssuableCollections private def issues_collection - issues_finder.execute + issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace) end def merge_requests_collection - merge_requests_finder.execute + merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace) end def issues_finder diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b89fb94be6e..b46adcceb60 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -7,7 +7,6 @@ module IssuesAction @issues = issues_collection .non_archived - .preload(:author, :project) .page(params[:page]) respond_to do |format| diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index a1b0eee37f9..6546a07b41c 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -7,7 +7,6 @@ module MergeRequestsAction @merge_requests = merge_requests_collection .non_archived - .preload(:author, :target_project) .page(params[:page]) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index dff0213411c..036fde87619 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(:target_project) if params[:label_name].present? labels_params = { project_id: @project.id, title: params[:label_name] } diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index ce2cabd7a3a..8bebda07787 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -171,9 +171,11 @@ module IssuablesHelper def issuables_count_for_state(issuable_type, state) issuables_finder = public_send("#{issuable_type}_finder") - issuables_finder.params[:state] = state + + params = issuables_finder.params.merge(state: state) + finder = issuables_finder.class.new(issuables_finder.current_user, params) - issuables_finder.execute.page(1).total_count + finder.execute.page(1).total_count end IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page] diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 12408068834..9ffcc48eb80 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -54,15 +54,18 @@ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do = icon('code-fork') = merge_request.target_branch + - if merge_request.milestone   = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do = icon('clock-o') = merge_request.milestone.title + - if merge_request.labels.any?   - merge_request.labels.each do |label| = link_to_label(label, subject: merge_request.project, type: :merge_request) + - if merge_request.tasks?   %span.task-status diff --git a/changelogs/unreleased/optimize-mr-index.yml b/changelogs/unreleased/optimize-mr-index.yml new file mode 100644 index 00000000000..1090b6d4528 --- /dev/null +++ b/changelogs/unreleased/optimize-mr-index.yml @@ -0,0 +1,4 @@ +--- +title: More aggressively preload on merge request and issue index pages +merge_request: +author: From 65724301e6129440d0fba9cf9779297bc702c95b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 18 Nov 2016 14:14:41 +0100 Subject: [PATCH 250/302] Make CI job script a required configuration entry --- lib/gitlab/ci/config/entry/job.rb | 32 ++++++++++---------- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 4 +-- spec/lib/gitlab/ci/config/entry/job_spec.rb | 14 +++++++-- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index ab4ef333629..20dcc024b4e 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -13,12 +13,10 @@ module Gitlab type stage when artifacts cache dependencies before_script after_script variables environment] - attributes :tags, :allow_failure, :when, :dependencies - validations do validates :config, allowed_keys: ALLOWED_KEYS - validates :config, presence: true + validates :script, presence: true validates :name, presence: true validates :name, type: Symbol @@ -77,6 +75,8 @@ module Gitlab :cache, :image, :services, :only, :except, :variables, :artifacts, :commands, :environment + attributes :script, :tags, :allow_failure, :when, :dependencies + def compose!(deps = nil) super do if type_defined? && !stage_defined? @@ -118,20 +118,20 @@ module Gitlab def to_hash { name: name, - before_script: before_script, - script: script, + before_script: before_script_value, + script: script_value, commands: commands, - image: image, - services: services, - stage: stage, - cache: cache, - only: only, - except: except, - variables: variables_defined? ? variables : nil, - environment: environment_defined? ? environment : nil, - environment_name: environment_defined? ? environment[:name] : nil, - artifacts: artifacts, - after_script: after_script } + image: image_value, + services: services_value, + stage: stage_value, + cache: cache_value, + only: only_value, + except: except_value, + variables: variables_defined? ? variables_value : nil, + environment: environment_defined? ? environment_value : nil, + environment_name: environment_defined? ? environment_value[:name] : nil, + artifacts: artifacts_value, + after_script: after_script_value } end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 84f21631719..ff5dcc06ab3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1124,8 +1124,8 @@ EOT end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") end - it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do - config = YAML.dump({ extra: { services: "test" } }) + it "returns errors if services configuration is not correct" do + config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index c05711b6338..fc9b8b86dc4 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -19,8 +19,7 @@ describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: ''.to_sym) } it 'reports error' do - expect(entry.errors) - .to include "job name can't be blank" + expect(entry.errors).to include "job name can't be blank" end end end @@ -56,6 +55,15 @@ describe Gitlab::Ci::Config::Entry::Job do end end end + + context 'when script is not provided' do + let(:config) { { stage: 'test' } } + + it 'returns error about missing script entry' do + expect(entry).not_to be_valid + expect(entry.errors).to include "job script can't be blank" + end + end end end @@ -78,7 +86,7 @@ describe Gitlab::Ci::Config::Entry::Job do before { entry.compose!(deps) } let(:config) do - { image: 'some_image', cache: { key: 'test' } } + { script: 'rspec', image: 'some_image', cache: { key: 'test' } } end it 'overrides global config' do From c72c76fde3882b7c2f778bf85132cd2c80f01f5b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Fri, 18 Nov 2016 13:29:47 +0100 Subject: [PATCH 251/302] Fix typos --- .../mattermost_command_service.rb | 2 +- db/schema.rb | 20 ++++--------------- lib/mattermost/presenter.rb | 7 ++++--- spec/lib/gitlab/chat_commands/command_spec.rb | 7 ++++--- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/app/models/project_services/mattermost_command_service.rb b/app/models/project_services/mattermost_command_service.rb index 3c9b14c66b4..058b323ff20 100644 --- a/app/models/project_services/mattermost_command_service.rb +++ b/app/models/project_services/mattermost_command_service.rb @@ -12,7 +12,7 @@ class MattermostCommandService < ChatService end def description - 'Mattermost is an open source, self-hosted Slack-alternative' + "Perform common operations on GitLab in Mattermost" end def to_param diff --git a/db/schema.rb b/db/schema.rb index 97a28ccad26..8f8a03e1534 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161113184239) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" end create_table "audit_events", force: :cascade do |t| @@ -384,17 +384,6 @@ ActiveRecord::Schema.define(version: 20161113184239) do add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree - create_table "custom_emoji", force: :cascade do |t| - t.integer "project_id", null: false - t.string "name" - t.string "emoji" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "custom_emoji", ["project_id", "name"], name: "index_custom_emoji_on_project_id_and_name", unique: true, using: :btree - add_index "custom_emoji", ["project_id"], name: "index_custom_emoji_on_project_id", using: :btree - create_table "deploy_keys_projects", force: :cascade do |t| t.integer "deploy_key_id", null: false t.integer "project_id", null: false @@ -941,7 +930,7 @@ ActiveRecord::Schema.define(version: 20161113184239) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false + t.boolean "only_allow_merge_if_all_discussions_are_resolved" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree @@ -1265,7 +1254,6 @@ ActiveRecord::Schema.define(version: 20161113184239) do add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree add_foreign_key "boards", "projects" - add_foreign_key "custom_emoji", "projects" add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb index f76d0376a98..bfbb089eb02 100644 --- a/lib/mattermost/presenter.rb +++ b/lib/mattermost/presenter.rb @@ -14,9 +14,10 @@ module Mattermost end def help(commands, trigger) - if commands.zero? + if commands.none? ephemeral_response("No commands configured") else + commands.map! { |command| "#{trigger} #{command}" } message = header_with_list("Available commands", commands) ephemeral_response(message) @@ -46,7 +47,7 @@ module Mattermost private def not_found - ephemeral_response("404 not found! GitLab couldn't find what your were looking for! :boom:") + ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") end def single_resource(resource) @@ -67,7 +68,7 @@ module Mattermost end def error(resource) - message = header_with_list("The action was not succesful, because:", resource.errors.messages) + message = header_with_list("The action was not successful, because:", resource.errors.messages) ephemeral_response(message) end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index f44c848479e..8cedbb0240f 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -11,18 +11,19 @@ describe Gitlab::ChatCommands::Command, service: true do let(:params) { { text: 'issue show 1' } } let(:project) { create(:project, has_external_issue_tracker: true) } - it 'displays the help message' do + it 'displays 404 messages' do expect(subject[:response_type]).to be(:ephemeral) expect(subject[:text]).to start_with('404 not found') end end context 'when an unknown command is triggered' do - let(:params) { { text: "unknown command 123" } } + let(:params) { { command: '/gitlab', text: "unknown command 123" } } it 'displays the help message' do expect(subject[:response_type]).to be(:ephemeral) expect(subject[:text]).to start_with('Available commands') + expect(subject[:text]).to match('/gitlab issue show') end end @@ -35,7 +36,7 @@ describe Gitlab::ChatCommands::Command, service: true do end end - context 'issue is succesfully created' do + context 'issue is successfully created' do let(:params) { { text: "issue create my new issue" } } before do From bf25b5f69d8343b1d1a39a49815c9c888896a758 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 18 Nov 2016 14:30:27 +0100 Subject: [PATCH 252/302] Remove CI config helper with same name as an entry --- lib/gitlab/ci/config.rb | 41 +++++++++++-- lib/gitlab/ci/config/entry/configurable.rb | 2 - .../lib/gitlab/ci/config/entry/global_spec.rb | 58 +++++++++---------- 3 files changed, 64 insertions(+), 37 deletions(-) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 06599238d22..f7ff7ea212e 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -4,12 +4,6 @@ module Gitlab # Base GitLab CI Configuration facade # class Config - ## - # Temporary delegations that should be removed after refactoring - # - delegate :before_script, :image, :services, :after_script, :variables, - :stages, :cache, :jobs, to: :@global - def initialize(config) @config = Loader.new(config).load! @@ -28,6 +22,41 @@ module Gitlab def to_hash @config end + + ## + # Temporary method that should be removed after refactoring + # + def before_script + @global.before_script_value + end + + def image + @global.image_value + end + + def services + @global.services_value + end + + def after_script + @global.after_script_value + end + + def variables + @global.variables_value + end + + def stages + @global.stages_value + end + + def cache + @global.cache_value + end + + def jobs + @global.jobs_value + end end end end diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb index 0f438faeda2..833ae4a0ff3 100644 --- a/lib/gitlab/ci/config/entry/configurable.rb +++ b/lib/gitlab/ci/config/entry/configurable.rb @@ -66,8 +66,6 @@ module Gitlab @entries[symbol].value end - - alias_method symbol.to_sym, "#{symbol}_value".to_sym end end end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index c7726adfd27..5e5c5dcc385 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -60,9 +60,9 @@ describe Gitlab::Ci::Config::Entry::Global do end context 'when not composed' do - describe '#before_script' do + describe '#before_script_value' do it 'returns nil' do - expect(global.before_script).to be nil + expect(global.before_script_value).to be nil end end @@ -82,40 +82,40 @@ describe Gitlab::Ci::Config::Entry::Global do end end - describe '#before_script' do + describe '#before_script_value' do it 'returns correct script' do - expect(global.before_script).to eq ['ls', 'pwd'] + expect(global.before_script_value).to eq ['ls', 'pwd'] end end - describe '#image' do + describe '#image_value' do it 'returns valid image' do - expect(global.image).to eq 'ruby:2.2' + expect(global.image_value).to eq 'ruby:2.2' end end - describe '#services' do + describe '#services_value' do it 'returns array of services' do - expect(global.services).to eq ['postgres:9.1', 'mysql:5.5'] + expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5'] end end - describe '#after_script' do + describe '#after_script_value' do it 'returns after script' do - expect(global.after_script).to eq ['make clean'] + expect(global.after_script_value).to eq ['make clean'] end end - describe '#variables' do + describe '#variables_value' do it 'returns variables' do - expect(global.variables).to eq(VAR: 'value') + expect(global.variables_value).to eq(VAR: 'value') end end - describe '#stages' do + describe '#stages_value' do context 'when stages key defined' do it 'returns array of stages' do - expect(global.stages).to eq %w[build pages] + expect(global.stages_value).to eq %w[build pages] end end @@ -126,21 +126,21 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'returns array of types as stages' do - expect(global.stages).to eq %w[test deploy] + expect(global.stages_value).to eq %w[test deploy] end end end - describe '#cache' do + describe '#cache_value' do it 'returns cache configuration' do - expect(global.cache) + expect(global.cache_value) .to eq(key: 'k', untracked: true, paths: ['public/']) end end - describe '#jobs' do + describe '#jobs_value' do it 'returns jobs configuration' do - expect(global.jobs).to eq( + expect(global.jobs_value).to eq( rspec: { name: :rspec, script: %w[rspec ls], before_script: ['ls', 'pwd'], @@ -185,21 +185,21 @@ describe Gitlab::Ci::Config::Entry::Global do end end - describe '#variables' do + describe '#variables_value' do it 'returns default value for variables' do - expect(global.variables).to eq({}) + expect(global.variables_value).to eq({}) end end - describe '#stages' do + describe '#stages_value' do it 'returns an array of default stages' do - expect(global.stages).to eq %w[build test deploy] + expect(global.stages_value).to eq %w[build test deploy] end end - describe '#cache' do + describe '#cache_value' do it 'returns correct cache definition' do - expect(global.cache).to eq(key: 'a') + expect(global.cache_value).to eq(key: 'a') end end end @@ -217,9 +217,9 @@ describe Gitlab::Ci::Config::Entry::Global do { variables: nil, rspec: { script: 'rspec' } } end - describe '#variables' do + describe '#variables_value' do it 'undefined entry returns a default value' do - expect(global.variables).to eq({}) + expect(global.variables_value).to eq({}) end end end @@ -245,9 +245,9 @@ describe Gitlab::Ci::Config::Entry::Global do end end - describe '#before_script' do + describe '#before_script_value' do it 'returns nil' do - expect(global.before_script).to be_nil + expect(global.before_script_value).to be_nil end end end From 7023e7665f78d48abc4ed8690a686eab3903ac98 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Fri, 18 Nov 2016 14:31:54 +0100 Subject: [PATCH 253/302] Add Changelog entry for job script requirement fix --- .../fix-require-build-script-configuration-entry.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-require-build-script-configuration-entry.yml diff --git a/changelogs/unreleased/fix-require-build-script-configuration-entry.yml b/changelogs/unreleased/fix-require-build-script-configuration-entry.yml new file mode 100644 index 00000000000..00b3fd2681f --- /dev/null +++ b/changelogs/unreleased/fix-require-build-script-configuration-entry.yml @@ -0,0 +1,4 @@ +--- +title: Make job script a required configuration entry +merge_request: 7566 +author: From b7d7453b36e3b8266e57b622ff630e67bdae97b0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 13:46:34 +0000 Subject: [PATCH 254/302] Remove unused CSS --- app/assets/stylesheets/pages/environments.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index cecf3f325fd..e9ff43a8adb 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -15,10 +15,6 @@ } } -.play-icon-svg span { - display: inline-block; -} - .environments { table-layout: fixed; From 0c9b7e0e892d7c98d6434571743caedfbdd65e2c Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 18 Nov 2016 13:48:44 +0000 Subject: [PATCH 255/302] Improvements after review --- .../environments/stores/environments_store.js.es6 | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 83bbc0145de..0204a903ab5 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -125,15 +125,7 @@ const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); - if (nameA < nameB) { - return -1; - } - - if (nameA > nameB) { - return 1; - } - - return 0; + return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line }, }; })(); From ae51774bc45d2e15ccc61b01a30d1b588f179f85 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Fri, 18 Nov 2016 15:20:48 +0100 Subject: [PATCH 256/302] Pass correct tag target to post-receive hook when creating tag via UI We need to handle annotated tags that are created via GitLab UI. Annotated tags have their own SHA. We have to pass this SHA to post-receive hook to mirror what happens when someone creates an annotated tag in their local repository and pushes it via command line. In order to obtain tag SHA we first have to create it. This is a bit confusing because we create the tag before executing pre-hooks, but there is no way to create a tag outside the repository. If pre-hooks fail we have to clean up after ourselves. --- app/models/repository.rb | 13 ++++++++--- ...ass-correct-tag-target-to-post-receive.yml | 4 ++++ spec/models/repository_spec.rb | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml diff --git a/app/models/repository.rb b/app/models/repository.rb index 146424d2b1c..31be06be50c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -176,11 +176,18 @@ class Repository options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - rugged.tags.create(tag_name, target, options) + rugged.tags.create(tag_name, target, options) + tag = find_tag(tag_name) + + GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do + # we already created a tag, because we need tag SHA to pass correct + # values to hooks end - find_tag(tag_name) + tag + rescue GitHooksService::PreReceiveError + rugged.tags.delete(tag_name) + raise end def rm_branch(user, branch_name) diff --git a/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml new file mode 100644 index 00000000000..5e868027ed6 --- /dev/null +++ b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml @@ -0,0 +1,4 @@ +--- +title: Pass correct tag target to post-receive hook when creating tag via UI +merge_request: 7556 +author: diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2470d504c68..72ac41f3472 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1354,6 +1354,28 @@ describe Repository, models: true do repository.add_tag(user, '8.5', 'master', 'foo') end + it 'does not create a tag when a pre-hook fails' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.add_tag(user, '8.5', 'master', 'foo') + end.to raise_error(GitHooksService::PreReceiveError) + + repository.expire_tags_cache + expect(repository.find_tag('8.5')).to be_nil + end + + it 'passes tag SHA to hooks' do + spy = GitHooksService.new + allow(GitHooksService).to receive(:new).and_return(spy) + allow(spy).to receive(:execute).and_call_original + + tag = repository.add_tag(user, '8.5', 'master', 'foo') + + expect(spy).to have_received(:execute). + with(anything, anything, anything, tag.target, anything) + end + it 'returns a Gitlab::Git::Tag object' do tag = repository.add_tag(user, '8.5', 'master', 'foo') From 0a6d3533b1adb62fe09d45b8f59483204ab122c8 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Fri, 18 Nov 2016 15:23:39 +0100 Subject: [PATCH 257/302] fix migration missing ignored transaction method --- .../20161020083353_add_pipeline_id_to_merge_request_metrics.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb index ed53756cd24..f49df6802a7 100644 --- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb +++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb @@ -4,6 +4,8 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + # Set this constant to true if this migration requires downtime. DOWNTIME = true From ac4c06edb5c9078b4aeb5343236f68b3aed29d07 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Fri, 18 Nov 2016 07:30:20 -0700 Subject: [PATCH 258/302] Remove title from skipped status svg --- app/views/shared/icons/_icon_status_skipped.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index 3420af411f6..e0a2d4282f0 100644 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31 + From f20eadcbbeb88e98c2608cbaf23f0d09ca002a98 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 8 Nov 2016 11:28:30 +0100 Subject: [PATCH 259/302] Fix faulty deployment refs In the 8.13 RC cycle, so before the release, there was a time in which references in git where stored by id instead of iid. This could be fixed by time, if the iid catches up with the id, it overwrites it. But in the mean time we have wrong refs in the folder. This commit fixes that. For all projects we have deployments we'll find the ones where the ref has a higher number than the iid is now and calls `#create_ref` on the corresponding deployment. --- lib/tasks/gitlab/cleanup.rake | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index b7cbdc6cd78..4a696a52b4d 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -91,5 +91,28 @@ namespace :gitlab do puts "To block these users run this command with BLOCK=true".color(:yellow) end end + + # This is a rake task which removes faulty refs. These refs where only + # created in the 8.13.RC cycle, and fixed in the stable builds which were + # released. So likely this should only be run once on gitlab.com + # Faulty refs are moved so they are kept around, else some features break. + desc 'GitLab | Cleanup | Remove faulty deployment refs' + task move_faulty_deployment_refs: :environment do + projects = Project.where(id: Deployment.select(:project_id).distinct) + + projects.find_each do |project| + rugged = project.repository.rugged + + max_iid = project.deployments.maximum(:iid) + + rugged.references.each('refs/environments/**/*') do |ref| + id = ref.name.split('/').last.to_i + next unless id > max_iid + + project.deployments.find(id).create_ref + rugged.references.delete(ref) + end + end + end end end From 7f44b5521a873f14a85196bb04f6cf2021ff10f0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Fri, 18 Nov 2016 15:25:19 +0000 Subject: [PATCH 260/302] Removes unnecessary comments --- .../environments/components/environment_actions.js.es6 | 2 -- app/assets/javascripts/vue_common_component/commit.js.es6 | 1 - 2 files changed, 3 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index f3042bbc528..edd39c02a46 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -44,7 +44,6 @@
    - + + - - by - - - - + + by + + + -
    +
    -

    +

    No deployments yet