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

Make deploy keys table more clearly structured

parent 924ea97a
<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"
......
<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>
......@@ -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,
},
});
},
});
......@@ -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) {
......
......@@ -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);
}
}
......@@ -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}` : '';
},
......@@ -74,7 +77,7 @@ export default {
<template>
<svg
:class="[iconSizeClass, cssClasses]"
:class="[iconSizeClass, iconTestClass, cssClasses]"
:width="width"
:height="height"
:x="x"
......
<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">
......
......@@ -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;
}
}
}
......
......@@ -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
......@@ -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
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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'
......@@ -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
......@@ -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
......
......@@ -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();
});
......
......@@ -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);
......@@ -21,6 +23,7 @@ describe('Deploy keys app component', () => {
vm = new Component({
propsData: {
endpoint: '/test',
projectId: '8',
},
}).$mount();
......@@ -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');
......@@ -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();
});
......
......@@ -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;
......@@ -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(() => {
......@@ -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(() => {
......@@ -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();
});
......
......@@ -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;
......@@ -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();
});
......
......@@ -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/')
......@@ -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,
......
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