Commit 5e436de6 authored by Lukas Eipert's avatar Lukas Eipert Committed by Clement Ho

Make deploy keys table more clearly structured

parent 924ea97a
<script> <script>
import eventHub from '../eventhub'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import eventHub from '../eventhub';
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
},
props: {
deployKey: {
type: Object,
required: true,
}, },
props: { type: {
deployKey: { type: String,
type: Object, required: true,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
}, },
data() { btnCssClass: {
return { type: String,
isLoading: false, required: false,
}; default: 'btn-default',
}, },
computed: { },
text() { data() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`; return {
}, isLoading: false,
}, };
methods: { },
doAction() { methods: {
this.isLoading = true; doAction() {
this.isLoading = true;
eventHub.$emit(`${this.type}.key`, this.deployKey, () => { eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false; this.isLoading = false;
}); });
},
}, },
}; },
};
</script> </script>
<template> <template>
<button <button
class="btn btn-sm prepend-left-10" class="btn"
:class="[{ disabled: isLoading }, btnCssClass]" :class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading" :disabled="isLoading"
@click="doAction"> @click="doAction">
{{ text }} <slot></slot>
<loading-icon <loading-icon
v-if="isLoading" v-if="isLoading"
:inline="true" :inline="true"
......
<script> <script>
import Flash from '../../flash'; import { s__ } from '~/locale';
import eventHub from '../eventhub'; import Flash from '~/flash';
import DeployKeysService from '../service'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DeployKeysStore from '../store'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import keysPanel from './keys_panel.vue'; import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
export default { export default {
components: { components: {
keysPanel, KeysPanel,
loadingIcon, LoadingIcon,
NavigationTabs,
},
props: {
endpoint: {
type: String,
required: true,
}, },
props: { projectId: {
endpoint: { type: String,
type: String, required: true,
required: true,
},
}, },
data() { },
return { data() {
isLoading: false, return {
store: new DeployKeysStore(), currentTab: 'enabled_keys',
}; isLoading: false,
store: new DeployKeysStore(),
};
},
scopes: {
enabled_keys: s__('DeployKeys|Enabled deploy keys'),
available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
},
computed: {
tabs() {
return Object.keys(this.$options.scopes).map(scope => {
const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
return {
name: this.$options.scopes[scope],
scope,
isActive: scope === this.currentTab,
count,
};
});
},
hasKeys() {
return Object.keys(this.keys).length;
}, },
computed: { keys() {
hasKeys() { return this.store.keys;
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
}, },
created() { },
this.service = new DeployKeysService(this.endpoint); created() {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey); eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey); eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.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);
},
methods: {
onChangeTab(tab) {
this.currentTab = tab;
}, },
mounted() { fetchKeys() {
this.fetchKeys(); this.isLoading = true;
return this.service
.getKeys()
.then(data => {
this.isLoading = false;
this.store.keys = data;
})
.catch(() => {
this.isLoading = false;
this.store.keys = {};
return new Flash(s__('DeployKeys|Error getting deploy keys'));
});
}, },
beforeDestroy() { enableKey(deployKey) {
eventHub.$off('enable.key', this.enableKey); this.service
eventHub.$off('remove.key', this.disableKey); .enableKey(deployKey.id)
eventHub.$off('disable.key', this.disableKey); .then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
}, },
methods: { disableKey(deployKey, callback) {
fetchKeys() { // eslint-disable-next-line no-alert
this.isLoading = true; if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
this.service.getKeys() .disableKey(deployKey.id)
.then((data) => { .then(this.fetchKeys)
this.isLoading = false; .then(callback)
this.store.keys = data; .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
}) } else {
.catch(() => new Flash('Error getting deploy keys')); callback();
}, }
enableKey(deployKey) {
this.service.enableKey(deployKey.id)
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
disableKey(deployKey, callback) {
// 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())
.then(callback)
.catch(() => new Flash('Error removing deploy key'));
} else {
callback();
}
},
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -82,29 +117,38 @@ ...@@ -82,29 +117,38 @@
<loading-icon <loading-icon
v-if="isLoading && !hasKeys" v-if="isLoading && !hasKeys"
size="2" size="2"
label="Loading deploy keys" :label="s__('DeployKeys|Loading deploy keys')"
/> />
<div v-else-if="hasKeys"> <template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<div class="fade-left">
<i
class="fa fa-angle-left"
aria-hidden="true"
>
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
aria-hidden="true"
>
</i>
</div>
<navigation-tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="deployKeys"
/>
</div>
<keys-panel <keys-panel
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys" class="qa-project-deploy-keys"
:keys="keys.enabled_keys" :project-id="projectId"
:store="store" :keys="keys[currentTab]"
:endpoint="endpoint"
/>
<keys-panel
title="Deploy keys from projects you have access to"
:keys="keys.available_project_keys"
:store="store"
:endpoint="endpoint"
/>
<keys-panel
v-if="keys.public_keys.length"
title="Public deploy keys available to any project"
:keys="keys.public_keys"
:store="store" :store="store"
:endpoint="endpoint" :endpoint="endpoint"
/> />
</div> </template>
</div> </div>
</template> </template>
<script> <script>
import actionBtn from './action_btn.vue'; import _ from 'underscore';
import { getTimeago } from '../../lib/utils/datetime_utility'; import { s__, sprintf } from '~/locale';
import tooltip from '../../vue_shared/directives/tooltip'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default { import actionBtn from './action_btn.vue';
components: {
actionBtn, export default {
}, components: {
directives: { actionBtn,
tooltip, icon,
}, },
props: { directives: {
deployKey: { tooltip,
type: Object, },
required: true, mixins: [timeagoMixin],
}, props: {
store: { deployKey: {
type: Object, type: Object,
required: true, required: true,
}, },
endpoint: { store: {
type: String, type: Object,
required: true, required: true,
}, },
}, endpoint: {
computed: { type: String,
timeagoDate() { required: true,
return getTimeago().format(this.deployKey.created_at); },
}, projectId: {
editDeployKeyPath() { type: String,
return `${this.endpoint}/${this.deployKey.id}/edit`; required: false,
}, default: null,
}, },
methods: { },
isEnabled(id) { data() {
return this.store.findEnabledKey(id) !== undefined; return {
}, projectsExpanded: false,
tooltipTitle(project) { };
return project.can_push ? 'Write access allowed' : 'Read access only'; },
}, computed: {
}, editDeployKeyPath() {
}; return `${this.endpoint}/${this.deployKey.id}/edit`;
},
projects() {
const projects = [...this.deployKey.deploy_keys_projects];
if (this.projectId !== null) {
const indexOfCurrentProject = _.findIndex(
projects,
project =>
project &&
project.project &&
project.project.id &&
project.project.id.toString() === this.projectId,
);
if (indexOfCurrentProject > -1) {
const currentProject = projects.splice(indexOfCurrentProject, 1);
currentProject[0].project.full_name = s__('DeployKeys|Current project');
return currentProject.concat(projects);
}
}
return projects;
},
firstProject() {
return _.head(this.projects);
},
restProjects() {
return _.tail(this.projects);
},
restProjectsTooltip() {
return sprintf(s__('DeployKeys|Expand %{count} other projects'), {
count: this.restProjects.length,
});
},
restProjectsLabel() {
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
},
isEnabled() {
return this.store.isEnabled(this.deployKey.id);
},
isRemovable() {
return (
this.store.isEnabled(this.deployKey.id) &&
this.deployKey.destroyed_when_orphaned &&
this.deployKey.almost_orphaned
);
},
isExpandable() {
return !this.projectsExpanded && this.restProjects.length > 1;
},
isExpanded() {
return this.projectsExpanded || this.restProjects.length === 1;
},
},
methods: {
projectTooltipTitle(project) {
return project.can_push
? s__('DeployKeys|Write access allowed')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
this.projectsExpanded = !this.projectsExpanded;
},
},
};
</script> </script>
<template> <template>
<div> <div class="gl-responsive-table-row deploy-key">
<div class="pull-left append-right-10 hidden-xs"> <div class="table-section section-40">
<i <div
aria-hidden="true" role="rowheader"
class="fa fa-key key-icon" class="table-mobile-header">
> {{ s__('DeployKeys|Deploy key') }}
</i> </div>
<div class="table-mobile-content">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="fingerprint qa-key-fingerprint">
{{ deployKey.fingerprint }}
</div>
</div>
</div> </div>
<div class="deploy-key-content key-list-item-info"> <div class="table-section section-30 section-wrap">
<strong class="title qa-key-title"> <div
{{ deployKey.title }} role="rowheader"
</strong> class="table-mobile-header">
<div class="description qa-key-fingerprint"> {{ s__('DeployKeys|Project usage') }}
{{ deployKey.fingerprint }} </div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
class="label deploy-project-label"
:title="projectTooltipTitle(firstProject)"
v-tooltip
>
<span>
{{ firstProject.project.full_name }}
</span>
<icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/>
</a>
<a
v-if="isExpandable"
class="label deploy-project-label"
@click="toggleExpanded"
:title="restProjectsTooltip"
v-tooltip
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
v-else-if="isExpanded"
v-for="deployKeysProject in restProjects"
:key="deployKeysProject.project.full_path"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="projectTooltipTitle(deployKeysProject)"
v-tooltip
>
<span>
{{ deployKeysProject.project.full_name }}
</span>
<icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'"/>
</a>
</template>
<span
v-else
class="text-secondary">{{ __('None') }}</span>
</div> </div>
</div> </div>
<div class="deploy-key-content prepend-left-default deploy-key-projects"> <div class="table-section section-15 text-right">
<a <div
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects" role="rowheader"
:key="i" class="table-mobile-header">
class="label deploy-project-label" {{ __('Created') }}
:href="deployKeysProject.project.full_path" </div>
:title="tooltipTitle(deployKeysProject)" <div class="table-mobile-content text-secondary key-created-at">
v-tooltip <span
> :title="tooltipTitle(deployKey.created_at)"
{{ deployKeysProject.project.full_name }} v-tooltip>
<i <icon name="calendar"/>
v-if="!deployKeysProject.can_push" <span>{{ timeFormated(deployKey.created_at) }}</span>
aria-hidden="true" </span>
class="fa fa-lock" </div>
>
</i>
</a>
</div> </div>
<div class="deploy-key-content"> <div class="table-section section-15 table-button-footer deploy-key-actions">
<span class="key-created-at"> <div class="btn-group table-action-buttons">
created {{ timeagoDate }} <action-btn
</span> v-if="!isEnabled"
<a :deploy-key="deployKey"
v-if="deployKey.can_edit" type="enable"
class="btn btn-sm" >
:href="editDeployKeyPath" {{ __('Enable') }}
> </action-btn>
Edit <a
</a> v-if="deployKey.can_edit"
<action-btn class="btn btn-default text-secondary"
v-if="!isEnabled(deployKey.id)" :href="editDeployKeyPath"
:deploy-key="deployKey" :title="__('Edit')"
type="enable" data-container="body"
/> v-tooltip
<action-btn >
v-else-if="deployKey.destroyed_when_orphaned && deployKey.almost_orphaned" <icon name="pencil"/>
:deploy-key="deployKey" </a>
btn-css-class="btn-warning" <action-btn
type="remove" v-if="isRemovable"
/> :deploy-key="deployKey"
<action-btn btn-css-class="btn-danger"
v-else type="remove"
:deploy-key="deployKey" :title="__('Remove')"
btn-css-class="btn-warning" data-container="body"
type="disable" v-tooltip
/> >
<icon name="remove"/>
</action-btn>
<action-btn
v-else-if="isEnabled"
:deploy-key="deployKey"
btn-css-class="btn-warning"
type="disable"
:title="__('Disable')"
data-container="body"
v-tooltip
>
<icon name="cancel"/>
</action-btn>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import key from './key.vue'; import deployKey from './key.vue';
export default { export default {
components: { components: {
key, deployKey,
},
props: {
keys: {
type: Array,
required: true,
}, },
props: { store: {
title: { type: Object,
type: String, required: true,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
}, },
}; endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
};
</script> </script>
<template> <template>
<div class="deploy-keys-panel"> <div class="deploy-keys-panel table-holder">
<h5> <template v-if="keys.length > 0">
{{ title }} <div
({{ keys.length }}) role="row"
</h5> class="gl-responsive-table-row table-row-header">
<ul <div
class="well-list" role="rowheader"
v-if="keys.length" class="table-section section-40">
> {{ s__('DeployKeys|Deploy key') }}
<li </div>
<div
role="rowheader"
class="table-section section-30">
{{ s__('DeployKeys|Project usage') }}
</div>
<div
role="rowheader"
class="table-section section-15 text-right">
{{ __('Created') }}
</div>
</div>
<deploy-key
v-for="deployKey in keys" v-for="deployKey in keys"
:key="deployKey.id" :key="deployKey.id"
> :deploy-key="deployKey"
<key :store="store"
:deploy-key="deployKey" :endpoint="endpoint"
:store="store" :project-id="projectId"
:endpoint="endpoint" />
/> </template>
</li>
</ul>
<div <div
class="settings-message text-center" class="settings-message text-center"
v-else-if="showHelpBox" v-else
> >
No deploy keys found. Create one with the form above. {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }}
</div> </div>
</div> </div>
</template> </template>
import Vue from 'vue'; import Vue from 'vue';
import deployKeysApp from './components/app.vue'; import deployKeysApp from './components/app.vue';
export default () => new Vue({ export default () =>
el: document.getElementById('js-deploy-keys'), new Vue({
components: { el: document.getElementById('js-deploy-keys'),
deployKeysApp, components: {
}, deployKeysApp,
data() { },
return { data() {
endpoint: this.$options.el.dataset.endpoint, return {
}; endpoint: this.$options.el.dataset.endpoint,
}, projectId: this.$options.el.dataset.projectId,
render(createElement) { };
return createElement('deploy-keys-app', { },
props: { render(createElement) {
endpoint: this.endpoint, return createElement('deploy-keys-app', {
}, props: {
}); endpoint: this.endpoint,
}, projectId: this.projectId,
}); },
});
},
});
...@@ -7,21 +7,24 @@ export default class DeployKeysService { ...@@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) { constructor(endpoint) {
this.endpoint = endpoint; this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, { this.resource = Vue.resource(
enable: { `${this.endpoint}{/id}`,
method: 'PUT', {},
url: `${this.endpoint}{/id}/enable`, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
},
disable: {
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
}, },
disable: { );
method: 'PUT',
url: `${this.endpoint}{/id}/disable`,
},
});
} }
getKeys() { getKeys() {
return this.resource.get() return this.resource.get().then(response => response.json());
.then(response => response.json());
} }
enableKey(id) { enableKey(id) {
......
...@@ -3,7 +3,7 @@ export default class DeployKeysStore { ...@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {}; this.keys = {};
} }
findEnabledKey(id) { isEnabled(id) {
return this.keys.enabled_keys.find(key => key.id === id); return this.keys.enabled_keys.some(key => key.id === id);
} }
} }
...@@ -65,6 +65,9 @@ export default { ...@@ -65,6 +65,9 @@ export default {
spriteHref() { spriteHref() {
return `${gon.sprite_icons}#${this.name}`; return `${gon.sprite_icons}#${this.name}`;
}, },
iconTestClass() {
return `ic-${this.name}`;
},
iconSizeClass() { iconSizeClass() {
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
...@@ -74,7 +77,7 @@ export default { ...@@ -74,7 +77,7 @@ export default {
<template> <template>
<svg <svg
:class="[iconSizeClass, cssClasses]" :class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
/** /**
* Given an array of tabs, renders non linked bootstrap tabs. * Given an array of tabs, renders non linked bootstrap tabs.
* When a tab is clicked it will trigger an event and provide the clicked scope. * When a tab is clicked it will trigger an event and provide the clicked scope.
* *
* This component is used in apps that handle the API call. * This component is used in apps that handle the API call.
* If you only need to change the URL this component should not be used. * If you only need to change the URL this component should not be used.
* *
* @example * @example
* <navigation-tabs * <navigation-tabs
* :tabs="[ * :tabs="[
* { * {
* name: String, * name: String,
* scope: String, * scope: String,
* count: Number || Undefined, * count: Number || Undefined || Null,
* isActive: Boolean, * isActive: Boolean,
* }, * },
* ]" * ]"
* @onChangeTab="onChangeTab" * @onChangeTab="onChangeTab"
* /> * />
*/ */
export default { export default {
name: 'NavigationTabs', name: 'NavigationTabs',
props: { props: {
tabs: { tabs: {
type: Array, type: Array,
required: true, required: true,
},
scope: {
type: String,
required: false,
default: '',
},
}, },
mounted() { scope: {
$(document).trigger('init.scrolling-tabs'); type: String,
required: false,
default: '',
},
},
mounted() {
$(document).trigger('init.scrolling-tabs');
},
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined or null
return !(count === undefined || count === null);
}, },
methods: {
shouldRenderBadge(count) {
// 0 is valid in a badge, but evaluates to false, we need to check for undefined
return count !== undefined;
},
onTabClick(tab) { onTabClick(tab) {
this.$emit('onChangeTab', tab.scope); this.$emit('onChangeTab', tab.scope);
},
}, },
}; },
};
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs separator"> <ul class="nav-links scrolling-tabs separator">
......
...@@ -354,30 +354,48 @@ ...@@ -354,30 +354,48 @@
min-width: 200px; min-width: 200px;
} }
.deploy-key-content { .deploy-keys {
@media (min-width: $screen-sm-min) { .scrolling-tabs-container {
float: left; position: relative;
}
}
&:last-child { .deploy-key {
float: right; // Ensure that the fingerprint does not overflow on small screens
.fingerprint {
word-break: break-all;
white-space: normal;
}
.deploy-project-label,
.key-created-at {
svg {
vertical-align: text-top;
} }
} }
}
.deploy-key-projects { .btn svg {
@media (min-width: $screen-sm-min) { vertical-align: top;
line-height: 42px; }
.key-created-at {
line-height: unset;
} }
} }
a.deploy-project-label { .deploy-project-list {
padding: 5px; margin-bottom: -$gl-padding-4;
margin-right: 5px;
color: $gl-text-color;
background-color: $row-hover;
&:hover { a.deploy-project-label {
color: $gl-link-color; margin-right: $gl-padding-4;
margin-bottom: $gl-padding-4;
color: $gl-text-color-secondary;
background-color: $theme-gray-100;
line-height: $gl-btn-line-height;
&:hover {
color: $gl-link-color;
}
} }
} }
......
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
Create a new deploy key for this project Create a new deploy key for this project
= render @deploy_keys.form_partial_path = render @deploy_keys.form_partial_path
%hr %hr
#js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project) } } #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
---
title: Make project deploy keys table more clearly structured
merge_request: 18279
author:
type: changed
...@@ -9,18 +9,21 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps ...@@ -9,18 +9,21 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'I should see project deploy key' do step 'I should see project deploy key' do
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-enabled_keys').click()
expect(page).to have_content deploy_key.title expect(page).to have_content deploy_key.title
end end
end end
step 'I should see other project deploy key' do step 'I should see other project deploy key' do
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-available_project_keys').click()
expect(page).to have_content other_deploy_key.title expect(page).to have_content other_deploy_key.title
end end
end end
step 'I should see public deploy key' do step 'I should see public deploy key' do
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-public_keys').click()
expect(page).to have_content public_deploy_key.title expect(page).to have_content public_deploy_key.title
end end
end end
...@@ -42,6 +45,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps ...@@ -42,6 +45,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'I should see newly created deploy key' do step 'I should see newly created deploy key' do
@project.reload @project.reload
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-enabled_keys').click()
expect(page).to have_content(deploy_key.title) expect(page).to have_content(deploy_key.title)
end end
end end
...@@ -58,7 +62,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps ...@@ -58,7 +62,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'I should only see the same deploy key once' do step 'I should only see the same deploy key once' do
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
expect(page).to have_selector('ul li', count: 1) expect(find('.js-deployKeys-tab-available_project_keys .badge')).to have_content('1')
end end
end end
...@@ -68,6 +72,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps ...@@ -68,6 +72,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'I click attach deploy key' do step 'I click attach deploy key' do
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
find('.badge', text: '1').click()
click_button 'Enable' click_button 'Enable'
expect(page).not_to have_selector('.fa-spinner') expect(page).not_to have_selector('.fa-spinner')
end end
......
...@@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do ...@@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do
visit project_settings_repository_path(project) visit project_settings_repository_path(project)
page.within(find('.deploy-keys')) do page.within(find('.deploy-keys')) do
expect(page).to have_selector('.deploy-keys li', count: 1) expect(page).to have_selector('.deploy-key', count: 1)
accept_confirm { find(:button, text: 'Remove').send_keys(:return) } accept_confirm { find('.ic-remove').click() }
expect(page).not_to have_selector('.fa-spinner', count: 0) expect(page).not_to have_selector('.fa-spinner', count: 0)
expect(page).to have_selector('.deploy-keys li', count: 0) expect(page).to have_selector('.deploy-key', count: 0)
end end
end end
end end
......
...@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do ...@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project) visit project_settings_repository_path(project)
find('li', text: private_deploy_key.title).click_link('Edit') find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key' fill_in 'deploy_key_title', with: 'updated_deploy_key'
check 'deploy_key_deploy_keys_projects_attributes_0_can_push' check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
...@@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do ...@@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do
visit project_settings_repository_path(project) visit project_settings_repository_path(project)
find('li', text: private_deploy_key.title).click_link('Edit') find('.js-deployKeys-tab-available_project_keys').click()
find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click()
fill_in 'deploy_key_title', with: 'updated_deploy_key' fill_in 'deploy_key_title', with: 'updated_deploy_key'
click_button 'Save changes' click_button 'Save changes'
find('.js-deployKeys-tab-available_project_keys').click()
expect(page).to have_content('updated_deploy_key') expect(page).to have_content('updated_deploy_key')
end end
...@@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do ...@@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project) visit project_settings_repository_path(project)
accept_confirm { find('li', text: private_deploy_key.title).click_button('Remove') } accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() }
expect(page).not_to have_content(private_deploy_key.title) expect(page).not_to have_content(private_deploy_key.title)
end end
......
...@@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => { ...@@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => {
const deployKey = data.enabled_keys[0]; const deployKey = data.enabled_keys[0];
let vm; let vm;
beforeEach((done) => { beforeEach(done => {
const ActionBtnComponent = Vue.extend(actionBtn); const ActionBtnComponent = Vue.extend({
components: {
vm = new ActionBtnComponent({ actionBtn,
propsData: { },
deployKey, data() {
type: 'enable', return {
deployKey,
};
}, },
}).$mount(); template: `
<action-btn
:deploy-key="deployKey"
type="enable">
Enable
</action-btn>`,
});
vm = new ActionBtnComponent().$mount();
setTimeout(done); Vue.nextTick()
.then(done)
.catch(done.fail);
}); });
it('renders the type as uppercase', () => { it('renders the default slot', () => {
expect( expect(vm.$el.textContent.trim()).toBe('Enable');
vm.$el.textContent.trim(),
).toBe('Enable');
}); });
it('sends eventHub event with btn type', (done) => { it('sends eventHub event with btn type', done => {
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
vm.$el.click(); vm.$el.click();
setTimeout(() => { Vue.nextTick(() => {
expect( expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
eventHub.$emit,
).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
done(); done();
}); });
}); });
it('shows loading spinner after click', (done) => { it('shows loading spinner after click', done => {
vm.$el.click(); vm.$el.click();
setTimeout(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.fa')).toBeDefined();
vm.$el.querySelector('.fa'),
).toBeDefined();
done(); done();
}); });
}); });
it('disables button after click', (done) => { it('disables button after click', done => {
vm.$el.click(); vm.$el.click();
setTimeout(() => { Vue.nextTick(() => {
expect( expect(vm.$el.classList.contains('disabled')).toBeTruthy();
vm.$el.classList.contains('disabled'),
).toBeTruthy();
expect( expect(vm.$el.getAttribute('disabled')).toBe('disabled');
vm.$el.getAttribute('disabled'),
).toBe('disabled');
done(); done();
}); });
......
...@@ -8,12 +8,14 @@ describe('Deploy keys app component', () => { ...@@ -8,12 +8,14 @@ describe('Deploy keys app component', () => {
let vm; let vm;
const deployKeysResponse = (request, next) => { const deployKeysResponse = (request, next) => {
next(request.respondWith(JSON.stringify(data), { next(
status: 200, request.respondWith(JSON.stringify(data), {
})); status: 200,
}),
);
}; };
beforeEach((done) => { beforeEach(done => {
const Component = Vue.extend(deployKeysApp); const Component = Vue.extend(deployKeysApp);
Vue.http.interceptors.push(deployKeysResponse); Vue.http.interceptors.push(deployKeysResponse);
...@@ -21,6 +23,7 @@ describe('Deploy keys app component', () => { ...@@ -21,6 +23,7 @@ describe('Deploy keys app component', () => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
endpoint: '/test', endpoint: '/test',
projectId: '8',
}, },
}).$mount(); }).$mount();
...@@ -31,117 +34,112 @@ describe('Deploy keys app component', () => { ...@@ -31,117 +34,112 @@ describe('Deploy keys app component', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse); Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
}); });
it('renders loading icon', (done) => { it('renders loading icon', done => {
vm.store.keys = {}; vm.store.keys = {};
vm.isLoading = false; vm.isLoading = false;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
vm.$el.querySelectorAll('.deploy-keys-panel').length,
).toBe(0);
expect( expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
vm.$el.querySelector('.fa-spinner'),
).toBeDefined();
done(); done();
}); });
}); });
it('renders keys panels', () => { it('renders keys panels', () => {
expect( expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3);
vm.$el.querySelectorAll('.deploy-keys-panel').length,
).toBe(3);
}); });
it('does not render key panels when keys object is empty', (done) => { it('renders the titles with keys count', () => {
vm.store.keys = {}; const textContent = selector => {
const element = vm.$el.querySelector(`${selector}`);
Vue.nextTick(() => {
expect( expect(element).not.toBeNull();
vm.$el.querySelectorAll('.deploy-keys-panel').length, return element.textContent.trim();
).toBe(0); };
done(); expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys');
}); expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain(
'Privately accessible deploy keys',
);
expect(textContent('.js-deployKeys-tab-public_keys')).toContain(
'Publicly accessible deploy keys',
);
expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe(
`${vm.store.keys.enabled_keys.length}`,
);
expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe(
`${vm.store.keys.available_project_keys.length}`,
);
expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe(
`${vm.store.keys.public_keys.length}`,
);
}); });
it('does not render public panel when empty', (done) => { it('does not render key panels when keys object is empty', done => {
vm.store.keys.public_keys = []; vm.store.keys = {};
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
vm.$el.querySelectorAll('.deploy-keys-panel').length,
).toBe(2);
done(); done();
}); });
}); });
it('re-fetches deploy keys when enabling a key', (done) => { it('re-fetches deploy keys when enabling a key', done => {
const key = data.public_keys[0]; const key = data.public_keys[0];
spyOn(vm.service, 'getKeys'); spyOn(vm.service, 'getKeys');
spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => { spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
resolve();
setTimeout(() => {
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}));
eventHub.$emit('enable.key', key); eventHub.$emit('enable.key', key);
expect(vm.service.enableKey).toHaveBeenCalledWith(key.id); Vue.nextTick(() => {
expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}); });
it('re-fetches deploy keys when disabling a key', (done) => { it('re-fetches deploy keys when disabling a key', done => {
const key = data.public_keys[0]; const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true); spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys'); spyOn(vm.service, 'getKeys');
spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => { spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
resolve();
setTimeout(() => {
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}));
eventHub.$emit('disable.key', key); eventHub.$emit('disable.key', key);
expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); Vue.nextTick(() => {
expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}); });
it('calls disableKey when removing a key', (done) => { it('calls disableKey when removing a key', done => {
const key = data.public_keys[0]; const key = data.public_keys[0];
spyOn(window, 'confirm').and.returnValue(true); spyOn(window, 'confirm').and.returnValue(true);
spyOn(vm.service, 'getKeys'); spyOn(vm.service, 'getKeys');
spyOn(vm.service, 'disableKey').and.callFake(() => new Promise((resolve) => { spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
resolve();
setTimeout(() => {
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}));
eventHub.$emit('remove.key', key); eventHub.$emit('remove.key', key);
expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); Vue.nextTick(() => {
expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}); });
it('hasKeys returns true when there are keys', () => { it('hasKeys returns true when there are keys', () => {
expect(vm.hasKeys).toEqual(3); expect(vm.hasKeys).toEqual(3);
}); });
it('resets remove button loading state', (done) => { it('resets disable button loading state', done => {
spyOn(window, 'confirm').and.returnValue(false); spyOn(window, 'confirm').and.returnValue(false);
const btn = vm.$el.querySelector('.btn-warning'); const btn = vm.$el.querySelector('.btn-warning');
...@@ -149,7 +147,7 @@ describe('Deploy keys app component', () => { ...@@ -149,7 +147,7 @@ describe('Deploy keys app component', () => {
btn.click(); btn.click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(btn.querySelector('.fa')).toBeNull(); expect(btn.querySelector('.btn-warning')).not.toExist();
done(); done();
}); });
......
...@@ -7,7 +7,7 @@ describe('Deploy keys key', () => { ...@@ -7,7 +7,7 @@ describe('Deploy keys key', () => {
let vm; let vm;
const KeyComponent = Vue.extend(key); const KeyComponent = Vue.extend(key);
const data = getJSONFixture('deploy_keys/keys.json'); const data = getJSONFixture('deploy_keys/keys.json');
const createComponent = (deployKey) => { const createComponent = deployKey => {
const store = new DeployKeysStore(); const store = new DeployKeysStore();
store.keys = data; store.keys = data;
...@@ -23,37 +23,42 @@ describe('Deploy keys key', () => { ...@@ -23,37 +23,42 @@ describe('Deploy keys key', () => {
describe('enabled key', () => { describe('enabled key', () => {
const deployKey = data.enabled_keys[0]; const deployKey = data.enabled_keys[0];
beforeEach((done) => { beforeEach(done => {
createComponent(deployKey); createComponent(deployKey);
setTimeout(done); setTimeout(done);
}); });
it('renders the keys title', () => { it('renders the keys title', () => {
expect( expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title');
vm.$el.querySelector('.title').textContent.trim(),
).toContain('My title');
}); });
it('renders human friendly formatted created date', () => { it('renders human friendly formatted created date', () => {
expect( expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
vm.$el.querySelector('.key-created-at').textContent.trim(), `${getTimeago().format(deployKey.created_at)}`,
).toBe(`created ${getTimeago().format(deployKey.created_at)}`); );
}); });
it('shows edit button', () => { it('shows pencil button for editing', () => {
expect( expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
).toBe('Edit');
}); });
it('shows remove button', () => { it('shows disable button when the project is not deletable', () => {
expect( expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Remove');
}); });
it('shows write access title when key has write access', (done) => { it('shows remove button when the project is deletable', done => {
vm.deployKey.destroyed_when_orphaned = true;
vm.deployKey.almost_orphaned = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .ic-remove')).toExist();
done();
});
});
});
describe('deploy key labels', () => {
it('shows write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = true; vm.deployKey.deploy_keys_projects[0].can_push = true;
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -64,7 +69,7 @@ describe('Deploy keys key', () => { ...@@ -64,7 +69,7 @@ describe('Deploy keys key', () => {
}); });
}); });
it('does not show write access title when key has write access', (done) => { it('does not show write access title when key has write access', done => {
vm.deployKey.deploy_keys_projects[0].can_push = false; vm.deployKey.deploy_keys_projects[0].can_push = false;
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -74,36 +79,73 @@ describe('Deploy keys key', () => { ...@@ -74,36 +79,73 @@ describe('Deploy keys key', () => {
done(); done();
}); });
}); });
it('shows expandable button if more than two projects', () => {
const labels = vm.$el.querySelectorAll('.deploy-project-label');
expect(labels.length).toBe(2);
expect(labels[1].textContent).toContain('others');
expect(labels[1].getAttribute('data-original-title')).toContain('Expand');
});
it('expands all project labels after click', done => {
const length = vm.deployKey.deploy_keys_projects.length;
vm.$el.querySelectorAll('.deploy-project-label')[1].click();
Vue.nextTick(() => {
const labels = vm.$el.querySelectorAll('.deploy-project-label');
expect(labels.length).toBe(length);
expect(labels[1].textContent).not.toContain(`+${length} others`);
expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand');
done();
});
});
it('shows two projects', done => {
vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2);
Vue.nextTick(() => {
const labels = vm.$el.querySelectorAll('.deploy-project-label');
expect(labels.length).toBe(2);
expect(labels[1].textContent).toContain(
vm.deployKey.deploy_keys_projects[1].project.full_name,
);
done();
});
});
}); });
describe('public keys', () => { describe('public keys', () => {
const deployKey = data.public_keys[0]; const deployKey = data.public_keys[0];
beforeEach((done) => { beforeEach(done => {
createComponent(deployKey); createComponent(deployKey);
setTimeout(done); setTimeout(done);
}); });
it('shows edit button', () => { it('renders deploy keys without any enabled projects', done => {
expect( vm.deployKey.deploy_keys_projects = [];
vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
).toBe('Edit'); Vue.nextTick(() => {
expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
done();
});
}); });
it('shows enable button', () => { it('shows enable button', () => {
expect( expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable');
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Enable');
}); });
it('shows disable button when key is enabled', (done) => { it('shows pencil button for editing', () => {
expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
});
it('shows disable button when key is enabled', done => {
vm.store.keys.enabled_keys.push(deployKey); vm.store.keys.enabled_keys.push(deployKey);
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Disable');
done(); done();
}); });
......
...@@ -6,7 +6,7 @@ describe('Deploy keys panel', () => { ...@@ -6,7 +6,7 @@ describe('Deploy keys panel', () => {
const data = getJSONFixture('deploy_keys/keys.json'); const data = getJSONFixture('deploy_keys/keys.json');
let vm; let vm;
beforeEach((done) => { beforeEach(done => {
const DeployKeysPanelComponent = Vue.extend(deployKeysPanel); const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
const store = new DeployKeysStore(); const store = new DeployKeysStore();
store.keys = data; store.keys = data;
...@@ -24,46 +24,38 @@ describe('Deploy keys panel', () => { ...@@ -24,46 +24,38 @@ describe('Deploy keys panel', () => {
setTimeout(done); setTimeout(done);
}); });
it('renders the title with keys count', () => { it('renders list of keys', () => {
expect( expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length);
vm.$el.querySelector('h5').textContent.trim(),
).toContain('test');
expect(
vm.$el.querySelector('h5').textContent.trim(),
).toContain(`(${vm.keys.length})`);
}); });
it('renders list of keys', () => { it('renders table header', () => {
expect( const tableHeader = vm.$el.querySelector('.table-row-header');
vm.$el.querySelectorAll('li').length,
).toBe(vm.keys.length); expect(tableHeader).toExist();
expect(tableHeader.textContent).toContain('Deploy key');
expect(tableHeader.textContent).toContain('Project usage');
expect(tableHeader.textContent).toContain('Created');
}); });
it('renders help box if keys are empty', (done) => { it('renders help box if keys are empty', done => {
vm.keys = []; vm.keys = [];
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.settings-message')).toBeDefined();
vm.$el.querySelector('.settings-message'),
).toBeDefined();
expect( expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe(
vm.$el.querySelector('.settings-message').textContent.trim(), 'No deploy keys found. Create one with the form above.',
).toBe('No deploy keys found. Create one with the form above.'); );
done(); done();
}); });
}); });
it('does not render help box if keys are empty & showHelpBox is false', (done) => { it('renders no table header if keys are empty', done => {
vm.keys = []; vm.keys = [];
vm.showHelpBox = false;
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(vm.$el.querySelector('.table-row-header')).not.toExist();
vm.$el.querySelector('.settings-message'),
).toBeNull();
done(); done();
}); });
......
...@@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control ...@@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal)} let(:project2) { create(:project, :internal)}
let(:project3) { create(:project, :internal)}
let(:project4) { create(:project, :internal)}
before(:all) do before(:all) do
clean_frontend_fixtures('deploy_keys/') clean_frontend_fixtures('deploy_keys/')
...@@ -28,6 +30,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control ...@@ -28,6 +30,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== 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: project, deploy_key: project_key)
create(:deploy_keys_project, project: project2, deploy_key: internal_key) create(:deploy_keys_project, project: project2, deploy_key: internal_key)
create(:deploy_keys_project, project: project3, deploy_key: project_key)
create(:deploy_keys_project, project: project4, deploy_key: project_key)
get :index, get :index,
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment