Skip to content
Snippets Groups Projects
Commit 5e436de6 authored by Lukas Eipert's avatar Lukas Eipert Committed by Clement Ho
Browse files

Make deploy keys table more clearly structured

parent 924ea97a
No related branches found
No related tags found
No related merge requests found
Showing
with 766 additions and 518 deletions
<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 {
components: {
loadingIcon,
export default {
components: {
loadingIcon,
},
props: {
deployKey: {
type: Object,
required: true,
},
props: {
deployKey: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
type: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
};
btnCssClass: {
type: String,
required: false,
default: 'btn-default',
},
computed: {
text() {
return `${this.type.charAt(0).toUpperCase()}${this.type.slice(1)}`;
},
},
methods: {
doAction() {
this.isLoading = true;
},
data() {
return {
isLoading: false,
};
},
methods: {
doAction() {
this.isLoading = true;
 
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
this.isLoading = false;
});
},
};
},
};
</script>
 
<template>
<button
class="btn btn-sm prepend-left-10"
class="btn"
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
@click="doAction">
{{ text }}
<slot></slot>
<loading-icon
v-if="isLoading"
:inline="true"
Loading
Loading
<script>
import Flash from '../../flash';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import keysPanel from './keys_panel.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import { s__ } from '~/locale';
import Flash from '~/flash';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import KeysPanel from './keys_panel.vue';
 
export default {
components: {
keysPanel,
loadingIcon,
export default {
components: {
KeysPanel,
LoadingIcon,
NavigationTabs,
},
props: {
endpoint: {
type: String,
required: true,
},
props: {
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
},
data() {
return {
isLoading: false,
store: new DeployKeysStore(),
};
},
data() {
return {
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: {
hasKeys() {
return Object.keys(this.keys).length;
},
keys() {
return this.store.keys;
},
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('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
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);
},
methods: {
onChangeTab(tab) {
this.currentTab = tab;
},
mounted() {
this.fetchKeys();
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() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
enableKey(deployKey) {
this.service
.enableKey(deployKey.id)
.then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
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, 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();
}
},
disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
.then(callback)
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
} else {
callback();
}
},
};
},
};
</script>
 
<template>
Loading
Loading
@@ -82,29 +117,38 @@
<loading-icon
v-if="isLoading && !hasKeys"
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
title="Enabled deploy keys for this project"
class="qa-project-deploy-keys"
:keys="keys.enabled_keys"
:store="store"
: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"
:project-id="projectId"
:keys="keys[currentTab]"
:store="store"
:endpoint="endpoint"
/>
</div>
</template>
</div>
</template>
<script>
import actionBtn from './action_btn.vue';
import { getTimeago } from '../../lib/utils/datetime_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import timeagoMixin from '~/vue_shared/mixins/timeago';
 
export default {
components: {
actionBtn,
},
directives: {
tooltip,
},
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
computed: {
timeagoDate() {
return getTimeago().format(this.deployKey.created_at);
},
editDeployKeyPath() {
return `${this.endpoint}/${this.deployKey.id}/edit`;
},
},
methods: {
isEnabled(id) {
return this.store.findEnabledKey(id) !== undefined;
},
tooltipTitle(project) {
return project.can_push ? 'Write access allowed' : 'Read access only';
},
},
};
import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
icon,
},
directives: {
tooltip,
},
mixins: [timeagoMixin],
props: {
deployKey: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
data() {
return {
projectsExpanded: false,
};
},
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>
 
<template>
<div>
<div class="pull-left append-right-10 hidden-xs">
<i
aria-hidden="true"
class="fa fa-key key-icon"
>
</i>
<div class="gl-responsive-table-row deploy-key">
<div class="table-section section-40">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Deploy key') }}
</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 class="deploy-key-content key-list-item-info">
<strong class="title qa-key-title">
{{ deployKey.title }}
</strong>
<div class="description qa-key-fingerprint">
{{ deployKey.fingerprint }}
<div class="table-section section-30 section-wrap">
<div
role="rowheader"
class="table-mobile-header">
{{ s__('DeployKeys|Project usage') }}
</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 class="deploy-key-content prepend-left-default deploy-key-projects">
<a
v-for="(deployKeysProject, i) in deployKey.deploy_keys_projects"
:key="i"
class="label deploy-project-label"
:href="deployKeysProject.project.full_path"
:title="tooltipTitle(deployKeysProject)"
v-tooltip
>
{{ deployKeysProject.project.full_name }}
<i
v-if="!deployKeysProject.can_push"
aria-hidden="true"
class="fa fa-lock"
>
</i>
</a>
<div class="table-section section-15 text-right">
<div
role="rowheader"
class="table-mobile-header">
{{ __('Created') }}
</div>
<div class="table-mobile-content text-secondary key-created-at">
<span
:title="tooltipTitle(deployKey.created_at)"
v-tooltip>
<icon name="calendar"/>
<span>{{ timeFormated(deployKey.created_at) }}</span>
</span>
</div>
</div>
<div class="deploy-key-content">
<span class="key-created-at">
created {{ timeagoDate }}
</span>
<a
v-if="deployKey.can_edit"
class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
</a>
<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 class="table-section section-15 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
<action-btn
v-if="!isEnabled"
:deploy-key="deployKey"
type="enable"
>
{{ __('Enable') }}
</action-btn>
<a
v-if="deployKey.can_edit"
class="btn btn-default text-secondary"
:href="editDeployKeyPath"
:title="__('Edit')"
data-container="body"
v-tooltip
>
<icon name="pencil"/>
</a>
<action-btn
v-if="isRemovable"
:deploy-key="deployKey"
btn-css-class="btn-danger"
type="remove"
:title="__('Remove')"
data-container="body"
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>
</template>
<script>
import key from './key.vue';
import deployKey from './key.vue';
 
export default {
components: {
key,
export default {
components: {
deployKey,
},
props: {
keys: {
type: Array,
required: true,
},
props: {
title: {
type: String,
required: true,
},
keys: {
type: Array,
required: true,
},
showHelpBox: {
type: Boolean,
required: false,
default: true,
},
store: {
type: Object,
required: true,
},
endpoint: {
type: String,
required: true,
},
store: {
type: Object,
required: true,
},
};
endpoint: {
type: String,
required: true,
},
projectId: {
type: String,
required: false,
default: null,
},
},
};
</script>
 
<template>
<div class="deploy-keys-panel">
<h5>
{{ title }}
({{ keys.length }})
</h5>
<ul
class="well-list"
v-if="keys.length"
>
<li
<div class="deploy-keys-panel table-holder">
<template v-if="keys.length > 0">
<div
role="row"
class="gl-responsive-table-row table-row-header">
<div
role="rowheader"
class="table-section section-40">
{{ s__('DeployKeys|Deploy key') }}
</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"
:key="deployKey.id"
>
<key
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
/>
</li>
</ul>
:deploy-key="deployKey"
:store="store"
:endpoint="endpoint"
:project-id="projectId"
/>
</template>
<div
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>
</template>
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
 
export default () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
},
});
},
});
export default () =>
new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
},
data() {
return {
endpoint: this.$options.el.dataset.endpoint,
projectId: this.$options.el.dataset.projectId,
};
},
render(createElement) {
return createElement('deploy-keys-app', {
props: {
endpoint: this.endpoint,
projectId: this.projectId,
},
});
},
});
Loading
Loading
@@ -7,21 +7,24 @@ export default class DeployKeysService {
constructor(endpoint) {
this.endpoint = endpoint;
 
this.resource = Vue.resource(`${this.endpoint}{/id}`, {}, {
enable: {
method: 'PUT',
url: `${this.endpoint}{/id}/enable`,
this.resource = Vue.resource(
`${this.endpoint}{/id}`,
{},
{
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() {
return this.resource.get()
.then(response => response.json());
return this.resource.get().then(response => response.json());
}
 
enableKey(id) {
Loading
Loading
Loading
Loading
@@ -3,7 +3,7 @@ export default class DeployKeysStore {
this.keys = {};
}
 
findEnabledKey(id) {
return this.keys.enabled_keys.find(key => key.id === id);
isEnabled(id) {
return this.keys.enabled_keys.some(key => key.id === id);
}
}
Loading
Loading
@@ -65,6 +65,9 @@ export default {
spriteHref() {
return `${gon.sprite_icons}#${this.name}`;
},
iconTestClass() {
return `ic-${this.name}`;
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
},
Loading
Loading
@@ -74,7 +77,7 @@ export default {
 
<template>
<svg
:class="[iconSizeClass, cssClasses]"
:class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
Loading
Loading
<script>
import $ from 'jquery';
import $ from 'jquery';
 
/**
* 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.
*
* 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.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default {
name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
scope: {
type: String,
required: false,
default: '',
},
/**
* 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.
*
* 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.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined || Null,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default {
name: 'NavigationTabs',
props: {
tabs: {
type: Array,
required: true,
},
mounted() {
$(document).trigger('init.scrolling-tabs');
scope: {
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) {
this.$emit('onChangeTab', tab.scope);
},
onTabClick(tab) {
this.$emit('onChangeTab', tab.scope);
},
};
},
};
</script>
<template>
<ul class="nav-links scrolling-tabs separator">
Loading
Loading
Loading
Loading
@@ -354,30 +354,48 @@
min-width: 200px;
}
 
.deploy-key-content {
@media (min-width: $screen-sm-min) {
float: left;
.deploy-keys {
.scrolling-tabs-container {
position: relative;
}
}
 
&:last-child {
float: right;
.deploy-key {
// 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 {
@media (min-width: $screen-sm-min) {
line-height: 42px;
.btn svg {
vertical-align: top;
}
.key-created-at {
line-height: unset;
}
}
 
a.deploy-project-label {
padding: 5px;
margin-right: 5px;
color: $gl-text-color;
background-color: $row-hover;
.deploy-project-list {
margin-bottom: -$gl-padding-4;
 
&:hover {
color: $gl-link-color;
a.deploy-project-label {
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;
}
}
}
 
Loading
Loading
Loading
Loading
@@ -12,4 +12,4 @@
Create a new deploy key for this project
= render @deploy_keys.form_partial_path
%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
Loading
Loading
@@ -9,18 +9,21 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
 
step 'I should see project deploy key' do
page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-enabled_keys').click()
expect(page).to have_content deploy_key.title
end
end
 
step 'I should see other project deploy key' do
page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-available_project_keys').click()
expect(page).to have_content other_deploy_key.title
end
end
 
step 'I should see public deploy key' do
page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-public_keys').click()
expect(page).to have_content public_deploy_key.title
end
end
Loading
Loading
@@ -42,6 +45,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
step 'I should see newly created deploy key' do
@project.reload
page.within(find('.deploy-keys')) do
find('.js-deployKeys-tab-enabled_keys').click()
expect(page).to have_content(deploy_key.title)
end
end
Loading
Loading
@@ -58,7 +62,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
 
step 'I should only see the same deploy key once' 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
 
Loading
Loading
@@ -68,6 +72,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
 
step 'I click attach deploy key' do
page.within(find('.deploy-keys')) do
find('.badge', text: '1').click()
click_button 'Enable'
expect(page).not_to have_selector('.fa-spinner')
end
Loading
Loading
Loading
Loading
@@ -18,12 +18,12 @@ describe 'Project deploy keys', :js do
visit project_settings_repository_path(project)
 
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).to have_selector('.deploy-keys li', count: 0)
expect(page).to have_selector('.deploy-key', count: 0)
end
end
end
Loading
Loading
Loading
Loading
@@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
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'
check 'deploy_key_deploy_keys_projects_attributes_0_can_push'
Loading
Loading
@@ -71,11 +71,15 @@ describe 'Projects > Settings > Repository settings' do
 
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'
click_button 'Save changes'
 
find('.js-deployKeys-tab-available_project_keys').click()
expect(page).to have_content('updated_deploy_key')
end
 
Loading
Loading
@@ -83,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
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)
end
Loading
Loading
Loading
Loading
@@ -7,62 +7,64 @@ describe('Deploy keys action btn', () => {
const deployKey = data.enabled_keys[0];
let vm;
 
beforeEach((done) => {
const ActionBtnComponent = Vue.extend(actionBtn);
vm = new ActionBtnComponent({
propsData: {
deployKey,
type: 'enable',
beforeEach(done => {
const ActionBtnComponent = Vue.extend({
components: {
actionBtn,
},
data() {
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', () => {
expect(
vm.$el.textContent.trim(),
).toBe('Enable');
it('renders the default slot', () => {
expect(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');
 
vm.$el.click();
 
setTimeout(() => {
expect(
eventHub.$emit,
).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
Vue.nextTick(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
 
done();
});
});
 
it('shows loading spinner after click', (done) => {
it('shows loading spinner after click', done => {
vm.$el.click();
 
setTimeout(() => {
expect(
vm.$el.querySelector('.fa'),
).toBeDefined();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.fa')).toBeDefined();
 
done();
});
});
 
it('disables button after click', (done) => {
it('disables button after click', done => {
vm.$el.click();
 
setTimeout(() => {
expect(
vm.$el.classList.contains('disabled'),
).toBeTruthy();
Vue.nextTick(() => {
expect(vm.$el.classList.contains('disabled')).toBeTruthy();
 
expect(
vm.$el.getAttribute('disabled'),
).toBe('disabled');
expect(vm.$el.getAttribute('disabled')).toBe('disabled');
 
done();
});
Loading
Loading
Loading
Loading
@@ -8,12 +8,14 @@ describe('Deploy keys app component', () => {
let vm;
 
const deployKeysResponse = (request, next) => {
next(request.respondWith(JSON.stringify(data), {
status: 200,
}));
next(
request.respondWith(JSON.stringify(data), {
status: 200,
}),
);
};
 
beforeEach((done) => {
beforeEach(done => {
const Component = Vue.extend(deployKeysApp);
 
Vue.http.interceptors.push(deployKeysResponse);
Loading
Loading
@@ -21,6 +23,7 @@ describe('Deploy keys app component', () => {
vm = new Component({
propsData: {
endpoint: '/test',
projectId: '8',
},
}).$mount();
 
Loading
Loading
@@ -31,117 +34,112 @@ describe('Deploy keys app component', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, deployKeysResponse);
});
 
it('renders loading icon', (done) => {
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.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
 
expect(
vm.$el.querySelector('.fa-spinner'),
).toBeDefined();
expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
 
done();
});
});
 
it('renders keys panels', () => {
expect(
vm.$el.querySelectorAll('.deploy-keys-panel').length,
).toBe(3);
expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').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('renders the titles with keys count', () => {
const textContent = selector => {
const element = vm.$el.querySelector(`${selector}`);
expect(element).not.toBeNull();
return element.textContent.trim();
};
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) => {
vm.store.keys.public_keys = [];
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(2);
expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
 
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];
 
spyOn(vm.service, 'getKeys');
spyOn(vm.service, 'enableKey').and.callFake(() => new Promise((resolve) => {
resolve();
setTimeout(() => {
expect(vm.service.getKeys).toHaveBeenCalled();
done();
});
}));
spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
 
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];
 
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();
});
}));
spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
 
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];
 
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();
});
}));
spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
 
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', () => {
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);
 
const btn = vm.$el.querySelector('.btn-warning');
Loading
Loading
@@ -149,7 +147,7 @@ describe('Deploy keys app component', () => {
btn.click();
 
Vue.nextTick(() => {
expect(btn.querySelector('.fa')).toBeNull();
expect(btn.querySelector('.btn-warning')).not.toExist();
 
done();
});
Loading
Loading
Loading
Loading
@@ -7,7 +7,7 @@ describe('Deploy keys key', () => {
let vm;
const KeyComponent = Vue.extend(key);
const data = getJSONFixture('deploy_keys/keys.json');
const createComponent = (deployKey) => {
const createComponent = deployKey => {
const store = new DeployKeysStore();
store.keys = data;
 
Loading
Loading
@@ -23,37 +23,42 @@ describe('Deploy keys key', () => {
describe('enabled key', () => {
const deployKey = data.enabled_keys[0];
 
beforeEach((done) => {
beforeEach(done => {
createComponent(deployKey);
 
setTimeout(done);
});
 
it('renders the keys title', () => {
expect(
vm.$el.querySelector('.title').textContent.trim(),
).toContain('My 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 ${getTimeago().format(deployKey.created_at)}`);
expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
`${getTimeago().format(deployKey.created_at)}`,
);
});
 
it('shows edit button', () => {
expect(
vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
).toBe('Edit');
it('shows pencil button for editing', () => {
expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
});
 
it('shows remove button', () => {
expect(
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Remove');
it('shows disable button when the project is not deletable', () => {
expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
});
 
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;
 
Vue.nextTick(() => {
Loading
Loading
@@ -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;
 
Vue.nextTick(() => {
Loading
Loading
@@ -74,36 +79,73 @@ describe('Deploy keys key', () => {
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', () => {
const deployKey = data.public_keys[0];
 
beforeEach((done) => {
beforeEach(done => {
createComponent(deployKey);
 
setTimeout(done);
});
 
it('shows edit button', () => {
expect(
vm.$el.querySelectorAll('.btn')[0].textContent.trim(),
).toBe('Edit');
it('renders deploy keys without any enabled projects', done => {
vm.deployKey.deploy_keys_projects = [];
Vue.nextTick(() => {
expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
done();
});
});
 
it('shows enable button', () => {
expect(
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Enable');
expect(vm.$el.querySelectorAll('.btn')[0].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);
 
Vue.nextTick(() => {
expect(
vm.$el.querySelectorAll('.btn')[1].textContent.trim(),
).toBe('Disable');
expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
 
done();
});
Loading
Loading
Loading
Loading
@@ -6,7 +6,7 @@ describe('Deploy keys panel', () => {
const data = getJSONFixture('deploy_keys/keys.json');
let vm;
 
beforeEach((done) => {
beforeEach(done => {
const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
const store = new DeployKeysStore();
store.keys = data;
Loading
Loading
@@ -24,46 +24,38 @@ describe('Deploy keys panel', () => {
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('.deploy-key').length).toBe(vm.keys.length);
});
 
it('renders list of keys', () => {
expect(
vm.$el.querySelectorAll('li').length,
).toBe(vm.keys.length);
it('renders table header', () => {
const tableHeader = vm.$el.querySelector('.table-row-header');
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 = [];
 
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.settings-message'),
).toBeDefined();
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.');
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) => {
it('renders no table header if keys are empty', done => {
vm.keys = [];
vm.showHelpBox = false;
 
Vue.nextTick(() => {
expect(
vm.$el.querySelector('.settings-message'),
).toBeNull();
expect(vm.$el.querySelector('.table-row-header')).not.toExist();
 
done();
});
Loading
Loading
Loading
Loading
@@ -7,6 +7,8 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') }
let(:project2) { create(:project, :internal)}
let(:project3) { create(:project, :internal)}
let(:project4) { create(:project, :internal)}
 
before(:all) do
clean_frontend_fixtures('deploy_keys/')
Loading
Loading
@@ -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')
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: project3, deploy_key: project_key)
create(:deploy_keys_project, project: project4, deploy_key: project_key)
 
get :index,
namespace_id: project.namespace.to_param,
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment