summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <filipa@gitlab.com>2017-05-04 18:30:41 +0000
committerFilipa Lacerda <filipa@gitlab.com>2017-05-04 18:30:41 +0000
commit7a66cd688aea6bd7d1103d0024204a1470153975 (patch)
treed3f4b3774d2b8673209304705a23462414490586
parent97a9a38b689e2ff7ab6dcd04d8bae65ef778e411 (diff)
parent136baeda508ddf46f6d91c03d4128b2ee890d205 (diff)
downloadgitlab-ce-7a66cd688aea6bd7d1103d0024204a1470153975.tar.gz
Merge branch 'deploy-keys-load-async' into 'master'
Deploy keys load async Closes #29667 See merge request !10973
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue54
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue102
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue80
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue52
-rw-r--r--app/assets/javascripts/deploy_keys/eventhub.js3
-rw-r--r--app/assets/javascripts/deploy_keys/index.js21
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js34
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js9
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb20
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb11
-rw-r--r--app/serializers/deploy_key_entity.rb14
-rw-r--r--app/serializers/deploy_key_serializer.rb3
-rw-r--r--app/serializers/project_entity.rb14
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml23
-rw-r--r--app/views/projects/settings/repository/show.html.haml4
-rw-r--r--changelogs/unreleased/deploy-keys-load-async.yml4
-rw-r--r--config/webpack.config.js2
-rw-r--r--features/project/deploy_keys.feature6
-rw-r--r--features/steps/project/deploy_keys.rb16
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb66
-rw-r--r--spec/features/projects/deploy_keys_spec.rb12
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js70
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js142
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js92
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js70
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb36
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb38
27 files changed, 961 insertions, 37 deletions
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
new file mode 100644
index 00000000000..3ff3a9d977e
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -0,0 +1,54 @@
+<script>
+ import eventHub from '../eventhub';
+
+ export default {
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ btnCssClass: {
+ type: String,
+ required: false,
+ default: 'btn-default',
+ },
+ },
+ methods: {
+ doAction() {
+ this.isLoading = true;
+
+ eventHub.$emit(`${this.type}.key`, this.deployKey);
+ },
+ },
+ computed: {
+ text() {
+ return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <button
+ class="btn btn-sm prepend-left-10"
+ :class="[{ disabled: isLoading }, btnCssClass]"
+ :disabled="isLoading"
+ @click="doAction">
+ {{ text }}
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="Loading">
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
new file mode 100644
index 00000000000..7315a9e11cb
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -0,0 +1,102 @@
+<script>
+ /* global Flash */
+ import eventHub from '../eventhub';
+ import DeployKeysService from '../service';
+ import DeployKeysStore from '../store';
+ import keysPanel from './keys_panel.vue';
+
+ export default {
+ data() {
+ return {
+ isLoading: false,
+ store: new DeployKeysStore(),
+ };
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ hasKeys() {
+ return Object.keys(this.keys).length;
+ },
+ keys() {
+ return this.store.keys;
+ },
+ },
+ components: {
+ keysPanel,
+ },
+ methods: {
+ fetchKeys() {
+ this.isLoading = true;
+
+ this.service.getKeys()
+ .then((data) => {
+ this.isLoading = false;
+ this.store.keys = data;
+ })
+ .catch(() => new Flash('Error getting deploy keys'));
+ },
+ enableKey(deployKey) {
+ this.service.enableKey(deployKey.id)
+ .then(() => this.fetchKeys())
+ .catch(() => new Flash('Error enabling deploy key'));
+ },
+ disableKey(deployKey) {
+ // eslint-disable-next-line no-alert
+ if (confirm('You are going to remove this deploy key. Are you sure?')) {
+ this.service.disableKey(deployKey.id)
+ .then(() => this.fetchKeys())
+ .catch(() => new Flash('Error removing deploy key'));
+ }
+ },
+ },
+ created() {
+ this.service = new DeployKeysService(this.endpoint);
+
+ eventHub.$on('enable.key', this.enableKey);
+ eventHub.$on('remove.key', this.disableKey);
+ eventHub.$on('disable.key', this.disableKey);
+ },
+ mounted() {
+ this.fetchKeys();
+ },
+ beforeDestroy() {
+ eventHub.$off('enable.key', this.enableKey);
+ eventHub.$off('remove.key', this.disableKey);
+ eventHub.$off('disable.key', this.disableKey);
+ },
+ };
+</script>
+
+<template>
+ <div class="col-lg-9 col-lg-offset-3 append-bottom-default deploy-keys">
+ <div
+ class="text-center"
+ v-if="isLoading && !hasKeys">
+ <i
+ class="fa fa-spinner fa-spin fa-2x"
+ aria-hidden="true"
+ aria-label="Loading deploy keys">
+ </i>
+ </div>
+ <div v-else-if="hasKeys">
+ <keys-panel
+ title="Enabled deploy keys for this project"
+ :keys="keys.enabled_keys"
+ :store="store" />
+ <keys-panel
+ title="Deploy keys from projects you have access to"
+ :keys="keys.available_project_keys"
+ :store="store" />
+ <keys-panel
+ v-if="keys.public_keys.length"
+ title="Public deploy keys available to any project"
+ :keys="keys.public_keys"
+ :store="store" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
new file mode 100644
index 00000000000..0a06a481b96
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -0,0 +1,80 @@
+<script>
+ import actionBtn from './action_btn.vue';
+
+ export default {
+ props: {
+ deployKey: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ actionBtn,
+ },
+ computed: {
+ timeagoDate() {
+ return gl.utils.getTimeago().format(this.deployKey.created_at);
+ },
+ },
+ methods: {
+ isEnabled(id) {
+ return this.store.findEnabledKey(id) !== undefined;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <div class="pull-left append-right-10 hidden-xs">
+ <i
+ aria-hidden="true"
+ class="fa fa-key key-icon">
+ </i>
+ </div>
+ <div class="deploy-key-content key-list-item-info">
+ <strong class="title">
+ {{ deployKey.title }}
+ </strong>
+ <div class="description">
+ {{ deployKey.fingerprint }}
+ </div>
+ <div
+ v-if="deployKey.can_push"
+ class="write-access-allowed">
+ Write access allowed
+ </div>
+ </div>
+ <div class="deploy-key-content prepend-left-default deploy-key-projects">
+ <a
+ v-for="project in deployKey.projects"
+ class="label deploy-project-label"
+ :href="project.full_path">
+ {{ project.full_name }}
+ </a>
+ </div>
+ <div class="deploy-key-content">
+ <span class="key-created-at">
+ created {{ timeagoDate }}
+ </span>
+ <action-btn
+ v-if="!isEnabled(deployKey.id)"
+ :deploy-key="deployKey"
+ type="enable"/>
+ <action-btn
+ v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned"
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="remove" />
+ <action-btn
+ v-else
+ :deploy-key="deployKey"
+ btn-css-class="btn-warning"
+ type="disable" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
new file mode 100644
index 00000000000..eccc470578b
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -0,0 +1,52 @@
+<script>
+ import key from './key.vue';
+
+ export default {
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ keys: {
+ type: Array,
+ required: true,
+ },
+ showHelpBox: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ key,
+ },
+ };
+</script>
+
+<template>
+ <div class="deploy-keys-panel">
+ <h5>
+ {{ title }}
+ ({{ keys.length }})
+ </h5>
+ <ul class="well-list"
+ v-if="keys.length">
+ <li
+ v-for="deployKey in keys"
+ :key="deployKey.id">
+ <key
+ :deploy-key="deployKey"
+ :store="store" />
+ </li>
+ </ul>
+ <div
+ class="settings-message text-center"
+ v-else-if="showHelpBox">
+ No deploy keys found. Create one with the form above.
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_keys/eventhub.js b/app/assets/javascripts/deploy_keys/eventhub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
new file mode 100644
index 00000000000..a5f232f950a
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import deployKeysApp from './components/app.vue';
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: document.getElementById('js-deploy-keys'),
+ data() {
+ return {
+ endpoint: this.$options.el.dataset.endpoint,
+ };
+ },
+ components: {
+ deployKeysApp,
+ },
+ render(createElement) {
+ return createElement('deploy-keys-app', {
+ props: {
+ endpoint: this.endpoint,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
new file mode 100644
index 00000000000..fe6dbaa9498
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class DeployKeysService {
+ constructor(endpoint) {
+ this.endpoint = endpoint;
+
+ this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
+ enable: {
+ method: 'PUT',
+ url: `${this.endpoint}{/id}/enable`,
+ },
+ disable: {
+ method: 'PUT',
+ url: `${this.endpoint}{/id}/disable`,
+ },
+ });
+ }
+
+ getKeys() {
+ return this.resource.get()
+ .then(response => response.json());
+ }
+
+ enableKey(id) {
+ return this.resource.enable({ id }, {});
+ }
+
+ disableKey(id) {
+ return this.resource.disable({ id }, {});
+ }
+}
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
new file mode 100644
index 00000000000..6210361af26
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/store/index.js
@@ -0,0 +1,9 @@
+export default class DeployKeysStore {
+ constructor() {
+ this.keys = {};
+ }
+
+ findEnabledKey(id) {
+ return this.keys.enabled_keys.find(key => key.id === id);
+ }
+}
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index d0c44e297e3..f27089b8590 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -8,7 +8,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
- redirect_to_repository_settings(@project)
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(@project) }
+ format.json do
+ render json: Projects::Settings::DeployKeysPresenter.new(@project, current_user: current_user).as_json
+ end
+ end
end
def new
@@ -19,7 +24,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
unless @key.valid? && @project.deploy_keys << @key
- flash[:alert] = @key.errors.full_messages.join(', ').html_safe
+ flash[:alert] = @key.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
@@ -27,7 +32,10 @@ class Projects::DeployKeysController < Projects::ApplicationController
def enable
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
- redirect_to_repository_settings(@project)
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(@project) }
+ format.json { head :ok }
+ end
end
def disable
@@ -35,7 +43,11 @@ class Projects::DeployKeysController < Projects::ApplicationController
return render_404 unless deploy_key_project
deploy_key_project.destroy!
- redirect_to_repository_settings(@project)
+
+ respond_to do |format|
+ format.html { redirect_to_repository_settings(@project) }
+ format.json { head :ok }
+ end
end
protected
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 86ac513b3c0..070b0c35e36 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -48,6 +48,17 @@ module Projects
available_public_keys.any?
end
+ def as_json
+ serializer = DeployKeySerializer.new
+ opts = { user: current_user }
+
+ {
+ enabled_keys: serializer.represent(enabled_keys, opts),
+ available_project_keys: serializer.represent(available_project_keys, opts),
+ public_keys: serializer.represent(available_public_keys, opts)
+ }
+ end
+
def to_partial_path
'projects/deploy_keys/index'
end
diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb
new file mode 100644
index 00000000000..d75a83d0fa5
--- /dev/null
+++ b/app/serializers/deploy_key_entity.rb
@@ -0,0 +1,14 @@
+class DeployKeyEntity < Grape::Entity
+ expose :id
+ expose :user_id
+ expose :title
+ expose :fingerprint
+ expose :can_push
+ expose :destroyed_when_orphaned?, as: :destroyed_when_orphaned
+ expose :almost_orphaned?, as: :almost_orphaned
+ expose :created_at
+ expose :updated_at
+ expose :projects, using: ProjectEntity do |deploy_key|
+ deploy_key.projects.select { |project| options[:user].can?(:read_project, project) }
+ end
+end
diff --git a/app/serializers/deploy_key_serializer.rb b/app/serializers/deploy_key_serializer.rb
new file mode 100644
index 00000000000..8f849eb88b7
--- /dev/null
+++ b/app/serializers/deploy_key_serializer.rb
@@ -0,0 +1,3 @@
+class DeployKeySerializer < BaseSerializer
+ entity DeployKeyEntity
+end
diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb
new file mode 100644
index 00000000000..a471a7e6a88
--- /dev/null
+++ b/app/serializers/project_entity.rb
@@ -0,0 +1,14 @@
+class ProjectEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :name
+
+ expose :full_path do |project|
+ namespace_project_path(project.namespace, project)
+ end
+
+ expose :full_name do |project|
+ project.full_name
+ end
+end
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 4cfbd9add00..74756b58439 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -10,25 +10,4 @@
= render @deploy_keys.form_partial_path
.col-lg-9.col-lg-offset-3
%hr
- .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
- %h5.prepend-top-0
- Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size})
- - if @deploy_keys.any_keys_enabled?
- %ul.well-list
- = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key
- - else
- .settings-message.text-center
- No deploy keys found. Create one with the form above.
- %h5.prepend-top-default
- Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size})
- - if @deploy_keys.any_available_project_keys_enabled?
- %ul.well-list
- = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
- - else
- .settings-message.text-center
- No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- - if @deploy_keys.any_available_public_keys_enabled?
- %h5.prepend-top-default
- Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
- %ul.well-list
- = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key
+ #js-deploy-keys{ data: { endpoint: namespace_project_deploy_keys_path } }
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 5402320cb66..4e59033c4a3 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,6 +1,10 @@
- page_title "Repository"
= render "projects/settings/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('deploy_keys')
+
= render @deploy_keys
= render "projects/protected_branches/index"
= render "projects/protected_tags/index"
diff --git a/changelogs/unreleased/deploy-keys-load-async.yml b/changelogs/unreleased/deploy-keys-load-async.yml
new file mode 100644
index 00000000000..e90910278e8
--- /dev/null
+++ b/changelogs/unreleased/deploy-keys-load-async.yml
@@ -0,0 +1,4 @@
+---
+title: Deploy keys load are loaded async
+merge_request:
+author:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0ec9e48845e..239bb5ec436 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -26,6 +26,7 @@ var config = {
common_d3: ['d3'],
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
+ deploy_keys: './deploy_keys/index.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
@@ -122,6 +123,7 @@ var config = {
'boards',
'commit_pipelines',
'cycle_analytics',
+ 'deploy_keys',
'diff_notes',
'environments',
'environments_folder',
diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature
index 960b4100ee5..6f1ed9ff5b6 100644
--- a/features/project/deploy_keys.feature
+++ b/features/project/deploy_keys.feature
@@ -3,28 +3,33 @@ Feature: Project Deploy Keys
Given I sign in as a user
And I own project "Shop"
+ @javascript
Scenario: I should see deploy keys list
Given project has deploy key
When I visit project deploy keys page
Then I should see project deploy key
+ @javascript
Scenario: I should see project deploy keys
Given other projects have deploy keys
When I visit project deploy keys page
Then I should see other project deploy key
And I should only see the same deploy key once
+ @javascript
Scenario: I should see public deploy keys
Given public deploy key exists
When I visit project deploy keys page
Then I should see public deploy key
+ @javascript
Scenario: I add new deploy key
Given I visit project deploy keys page
And I submit new deploy key
Then I should be on deploy keys page
And I should see newly created deploy key
+ @javascript
Scenario: I attach other project deploy key to project
Given other projects have deploy keys
And I visit project deploy keys page
@@ -32,6 +37,7 @@ Feature: Project Deploy Keys
Then I should be on deploy keys page
And I should see newly created deploy key
+ @javascript
Scenario: I attach public deploy key to project
Given public deploy key exists
And I visit project deploy keys page
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index ec59a2c094e..8ad9d4a4741 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -8,19 +8,19 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I should see project deploy key' do
- page.within '.deploy-keys' do
+ page.within(find('.deploy-keys')) do
expect(page).to have_content deploy_key.title
end
end
step 'I should see other project deploy key' do
- page.within '.deploy-keys' do
+ page.within(find('.deploy-keys')) do
expect(page).to have_content other_deploy_key.title
end
end
step 'I should see public deploy key' do
- page.within '.deploy-keys' do
+ page.within(find('.deploy-keys')) do
expect(page).to have_content public_deploy_key.title
end
end
@@ -40,7 +40,8 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I should see newly created deploy key' do
- page.within '.deploy-keys' do
+ @project.reload
+ page.within(find('.deploy-keys')) do
expect(page).to have_content(deploy_key.title)
end
end
@@ -56,7 +57,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I should only see the same deploy key once' do
- page.within '.deploy-keys' do
+ page.within(find('.deploy-keys')) do
expect(page).to have_selector('ul li', count: 1)
end
end
@@ -66,8 +67,9 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I click attach deploy key' do
- page.within '.deploy-keys' do
- click_link 'Enable'
+ page.within(find('.deploy-keys')) do
+ click_button 'Enable'
+ expect(page).not_to have_selector('.fa-spinner')
end
end
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
new file mode 100644
index 00000000000..efe1a78415b
--- /dev/null
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::DeployKeysController do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ let(:params) do
+ { namespace_id: project.namespace, project_id: project }
+ end
+
+ context 'when html requested' do
+ it 'redirects to blob' do
+ get :index, params
+
+ expect(response).to redirect_to(namespace_project_settings_repository_path(params))
+ end
+ end
+
+ context 'when json requested' do
+ let(:project2) { create(:empty_project, :internal)}
+ let(:project_private) { create(:empty_project, :private)}
+
+ let(:deploy_key_internal) do
+ create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
+ end
+ let(:deploy_key_actual) do
+ create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
+ end
+ let!(:deploy_key_public) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project_internal) do
+ create(:deploy_keys_project, project: project2, deploy_key: deploy_key_internal)
+ end
+
+ let!(:deploy_keys_actual_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key_actual)
+ end
+
+ let!(:deploy_keys_project_private) do
+ create(:deploy_keys_project, project: project_private, deploy_key: create(:another_deploy_key))
+ end
+
+ before do
+ project2.team << [user, :developer]
+ end
+
+ it 'returns json in a correct format' do
+ get :index, params.merge(format: :json)
+
+ json = JSON.parse(response.body)
+
+ expect(json.keys).to match_array(%w(enabled_keys available_project_keys public_keys))
+ expect(json['enabled_keys'].count).to eq(1)
+ expect(json['available_project_keys'].count).to eq(1)
+ expect(json['public_keys'].count).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb
index 0b997f130ea..06abfbbc86b 100644
--- a/spec/features/projects/deploy_keys_spec.rb
+++ b/spec/features/projects/deploy_keys_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Project deploy keys', feature: true do
+describe 'Project deploy keys', :js, :feature do
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo) }
@@ -17,9 +17,13 @@ describe 'Project deploy keys', feature: true do
it 'removes association between project and deploy key' do
visit namespace_project_settings_repository_path(project.namespace, project)
- page.within '.deploy-keys' do
- expect { click_on 'Remove' }
- .to change { project.deploy_keys.count }.by(-1)
+ page.within(find('.deploy-keys')) do
+ expect(page).to have_selector('.deploy-keys li', count: 1)
+
+ click_on 'Remove'
+
+ expect(page).not_to have_selector('.fa-spinner', count: 0)
+ expect(page).to have_selector('.deploy-keys li', count: 0)
end
end
end
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
new file mode 100644
index 00000000000..5b93fbc5575
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import eventHub from '~/deploy_keys/eventhub';
+import actionBtn from '~/deploy_keys/components/action_btn.vue';
+
+describe('Deploy keys action btn', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const deployKey = data.enabled_keys[0];
+ let vm;
+
+ beforeEach((done) => {
+ const ActionBtnComponent = Vue.extend(actionBtn);
+
+ vm = new ActionBtnComponent({
+ propsData: {
+ deployKey,
+ type: 'enable',
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ it('renders the type as uppercase', () => {
+ expect(
+ vm.$el.textContent.trim(),
+ ).toBe('Enable');
+ });
+
+ it('sends eventHub event with btn type', (done) => {
+ spyOn(eventHub, '$emit');
+
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ eventHub.$emit,
+ ).toHaveBeenCalledWith('enable.key', deployKey);
+
+ done();
+ });
+ });
+
+ it('shows loading spinner after click', (done) => {
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.fa'),
+ ).toBeDefined();
+
+ done();
+ });
+ });
+
+ it('disables button after click', (done) => {
+ vm.$el.click();
+
+ setTimeout(() => {
+ expect(
+ vm.$el.classList.contains('disabled'),
+ ).toBeTruthy();
+
+ expect(
+ vm.$el.getAttribute('disabled'),
+ ).toBe('disabled');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
new file mode 100644
index 00000000000..700897f50b0
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -0,0 +1,142 @@
+import Vue from 'vue';
+import eventHub from '~/deploy_keys/eventhub';
+import deployKeysApp from '~/deploy_keys/components/app.vue';
+
+describe('Deploy keys app component', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let vm;
+
+ const deployKeysResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify(data), {
+ status: 200,
+ }));
+ };
+
+ beforeEach((done) => {
+ const Component = Vue.extend(deployKeysApp);
+
+ Vue.http.interceptors.push(deployKeysResponse);
+
+ vm = new Component({
+ propsData: {
+ endpoint: '/test',
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
+ });
+
+ it('renders loading icon', (done) => {
+ vm.store.keys = {};
+ vm.isLoading = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(0);
+
+ expect(
+ vm.$el.querySelector('.fa-spinner'),
+ ).toBeDefined();
+
+ done();
+ });
+ });
+
+ it('renders keys panels', () => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(3);
+ });
+
+ it('does not render key panels when keys object is empty', (done) => {
+ vm.store.keys = {};
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(0);
+
+ done();
+ });
+ });
+
+ it('does not render public panel when empty', (done) => {
+ vm.store.keys.public_keys = [];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.deploy-keys-panel').length,
+ ).toBe(2);
+
+ done();
+ });
+ });
+
+ it('re-fetches deploy keys when enabling a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('enable.key', key);
+
+ expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('re-fetches deploy keys when disabling a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('disable.key', key);
+
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('calls disableKey when removing a key', (done) => {
+ const key = data.public_keys[0];
+
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(vm.service, 'getKeys');
+ spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => {
+ resolve();
+
+ setTimeout(() => {
+ expect(vm.service.getKeys).toHaveBeenCalled();
+
+ done();
+ });
+ }));
+
+ eventHub.$emit('remove.key', key);
+
+ expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ });
+
+ it('hasKeys returns true when there are keys', () => {
+ expect(vm.hasKeys).toEqual(3);
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
new file mode 100644
index 00000000000..793ab8c451d
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import DeployKeysStore from '~/deploy_keys/store';
+import key from '~/deploy_keys/components/key.vue';
+
+describe('Deploy keys key', () => {
+ let vm;
+ const KeyComponent = Vue.extend(key);
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const createComponent = (deployKey) => {
+ const store = new DeployKeysStore();
+ store.keys = data;
+
+ vm = new KeyComponent({
+ propsData: {
+ deployKey,
+ store,
+ },
+ }).$mount();
+ };
+
+ describe('enabled key', () => {
+ const deployKey = data.enabled_keys[0];
+
+ beforeEach((done) => {
+ createComponent(deployKey);
+
+ setTimeout(done);
+ });
+
+ it('renders the keys title', () => {
+ expect(
+ vm.$el.querySelector('.title').textContent.trim(),
+ ).toContain('My title');
+ });
+
+ it('renders human friendly formatted created date', () => {
+ expect(
+ vm.$el.querySelector('.key-created-at').textContent.trim(),
+ ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`);
+ });
+
+ it('shows remove button', () => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Remove');
+ });
+
+ it('shows write access text when key has write access', (done) => {
+ vm.deployKey.can_push = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.write-access-allowed'),
+ ).not.toBeNull();
+
+ expect(
+ vm.$el.querySelector('.write-access-allowed').textContent.trim(),
+ ).toBe('Write access allowed');
+
+ done();
+ });
+ });
+ });
+
+ describe('public keys', () => {
+ const deployKey = data.public_keys[0];
+
+ beforeEach((done) => {
+ createComponent(deployKey);
+
+ setTimeout(done);
+ });
+
+ it('shows enable button', () => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Enable');
+ });
+
+ it('shows disable button when key is enabled', (done) => {
+ vm.store.keys.enabled_keys.push(deployKey);
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.btn').textContent.trim(),
+ ).toBe('Disable');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
new file mode 100644
index 00000000000..a69b39c35c4
--- /dev/null
+++ b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
@@ -0,0 +1,70 @@
+import Vue from 'vue';
+import DeployKeysStore from '~/deploy_keys/store';
+import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
+
+describe('Deploy keys panel', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let vm;
+
+ beforeEach((done) => {
+ const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
+ const store = new DeployKeysStore();
+ store.keys = data;
+
+ vm = new DeployKeysPanelComponent({
+ propsData: {
+ title: 'test',
+ keys: data.enabled_keys,
+ showHelpBox: true,
+ store,
+ },
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ it('renders the title with keys count', () => {
+ expect(
+ vm.$el.querySelector('h5').textContent.trim(),
+ ).toContain('test');
+
+ expect(
+ vm.$el.querySelector('h5').textContent.trim(),
+ ).toContain(`(${vm.keys.length})`);
+ });
+
+ it('renders list of keys', () => {
+ expect(
+ vm.$el.querySelectorAll('li').length,
+ ).toBe(vm.keys.length);
+ });
+
+ it('renders help box if keys are empty', (done) => {
+ vm.keys = [];
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.settings-message'),
+ ).toBeDefined();
+
+ expect(
+ vm.$el.querySelector('.settings-message').textContent.trim(),
+ ).toBe('No deploy keys found. Create one with the form above.');
+
+ done();
+ });
+ });
+
+ it('does not render help box if keys are empty & showHelpBox is false', (done) => {
+ vm.keys = [];
+ vm.showHelpBox = false;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.settings-message'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
new file mode 100644
index 00000000000..16e598a4b29
--- /dev/null
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
+ let(:project2) { create(:empty_project, :internal)}
+
+ before(:all) do
+ clean_frontend_fixtures('deploy_keys/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ render_views
+
+ it 'deploy_keys/keys.json' do |example|
+ create(:deploy_key, public: true)
+ project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
+ internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
+ create(:deploy_keys_project, project: project, deploy_key: project_key)
+ create(:deploy_keys_project, project: project2, deploy_key: internal_key)
+
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ format: :json
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
new file mode 100644
index 00000000000..e73fbe190ca
--- /dev/null
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe DeployKeyEntity do
+ include RequestAwareEntity
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :internal)}
+ let(:project_private) { create(:empty_project, :private)}
+ let(:deploy_key) { create(:deploy_key) }
+ let!(:deploy_key_internal) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
+ let!(:deploy_key_private) { create(:deploy_keys_project, project: project_private, deploy_key: deploy_key) }
+
+ let(:entity) { described_class.new(deploy_key, user: user) }
+
+ it 'returns deploy keys with projects a user can read' do
+ expected_result = {
+ id: deploy_key.id,
+ user_id: deploy_key.user_id,
+ title: deploy_key.title,
+ fingerprint: deploy_key.fingerprint,
+ can_push: deploy_key.can_push,
+ destroyed_when_orphaned: true,
+ almost_orphaned: false,
+ created_at: deploy_key.created_at,
+ updated_at: deploy_key.updated_at,
+ projects: [
+ {
+ id: project.id,
+ name: project.name,
+ full_path: namespace_project_path(project.namespace, project),
+ full_name: project.full_name
+ }
+ ]
+ }
+
+ expect(entity.as_json).to eq(expected_result)
+ end
+end