Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
f3355c81
Commit
f3355c81
authored
Nov 23, 2021
by
Peter Hegman
Committed by
Vitaly Slobodin
Nov 23, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add table and pagination to deploy keys Vue conversion
parent
c3033257
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
400 additions
and
12 deletions
+400
-12
app/assets/javascripts/admin/deploy_keys/components/table.vue
...assets/javascripts/admin/deploy_keys/components/table.vue
+149
-3
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+7
-0
locale/gitlab.pot
locale/gitlab.pot
+6
-0
spec/frontend/admin/deploy_keys/components/table_spec.js
spec/frontend/admin/deploy_keys/components/table_spec.js
+168
-8
spec/frontend/api_spec.js
spec/frontend/api_spec.js
+46
-1
spec/frontend/fixtures/api_deploy_keys.rb
spec/frontend/fixtures/api_deploy_keys.rb
+24
-0
No files found.
app/assets/javascripts/admin/deploy_keys/components/table.vue
View file @
f3355c81
<
script
>
import
{
GlTable
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlTable
,
GlButton
,
GlPagination
,
GlLoadingIcon
,
GlEmptyState
}
from
'
@gitlab/ui
'
;
import
{
__
}
from
'
~/locale
'
;
import
Api
,
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
{
cleanLeadingSeparator
}
from
'
~/lib/utils/url_utility
'
;
import
createFlash
from
'
~/flash
'
;
export
default
{
name
:
'
DeployKeysTable
'
,
i18n
:
{
pageTitle
:
__
(
'
Public deploy keys
'
),
newDeployKeyButtonText
:
__
(
'
New deploy key
'
),
emptyStateTitle
:
__
(
'
No public deploy keys
'
),
emptyStateDescription
:
__
(
'
Deploy keys grant read/write access to all repositories in your instance
'
,
),
remove
:
__
(
'
Remove deploy key
'
),
edit
:
__
(
'
Edit deploy key
'
),
pagination
:
{
next
:
__
(
'
Next
'
),
prev
:
__
(
'
Prev
'
),
},
apiErrorMessage
:
__
(
'
An error occurred fetching the public deploy keys. Please try again.
'
),
},
fields
:
[
{
...
...
@@ -29,13 +44,83 @@ export default {
{
key
:
'
actions
'
,
label
:
__
(
'
Actions
'
),
tdClass
:
'
gl-lg-w-1px gl-white-space-nowrap
'
,
thClass
:
'
gl-lg-w-1px gl-white-space-nowrap
'
,
},
],
DEFAULT_PER_PAGE
,
components
:
{
GlTable
,
GlButton
,
GlPagination
,
TimeAgoTooltip
,
GlLoadingIcon
,
GlEmptyState
,
},
inject
:
[
'
editPath
'
,
'
deletePath
'
,
'
createPath
'
,
'
emptyStateSvgPath
'
],
data
()
{
return
{
page
:
1
,
totalItems
:
0
,
loading
:
false
,
items
:
[],
};
},
computed
:
{
shouldShowTable
()
{
return
this
.
totalItems
!==
0
||
this
.
loading
;
},
},
watch
:
{
page
(
newPage
)
{
this
.
fetchDeployKeys
(
newPage
);
},
},
mounted
()
{
this
.
fetchDeployKeys
();
},
methods
:
{
editHref
(
id
)
{
return
this
.
editPath
.
replace
(
'
:id
'
,
id
);
},
projectHref
(
project
)
{
return
`/
${
cleanLeadingSeparator
(
project
.
path_with_namespace
)}
`
;
},
async
fetchDeployKeys
(
page
)
{
this
.
loading
=
true
;
try
{
const
{
headers
,
data
:
items
}
=
await
Api
.
deployKeys
({
page
,
public
:
true
,
});
if
(
this
.
totalItems
===
0
)
{
this
.
totalItems
=
parseInt
(
headers
?.[
'
x-total
'
],
10
)
||
0
;
}
this
.
items
=
items
.
map
(
({
id
,
title
,
fingerprint
,
projects_with_write_access
,
created_at
})
=>
({
id
,
title
,
fingerprint
,
projects
:
projects_with_write_access
,
created
:
created_at
,
}),
);
}
catch
(
error
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
apiErrorMessage
,
captureError
:
true
,
error
,
});
this
.
totalItems
=
0
;
this
.
items
=
[];
}
this
.
loading
=
false
;
},
},
};
</
script
>
...
...
@@ -45,10 +130,71 @@ export default {
<h4
class=
"gl-m-0"
>
{{
$options
.
i18n
.
pageTitle
}}
</h4>
<gl-button
variant=
"confirm"
:href=
"createPath"
>
{{
<gl-button
variant=
"confirm"
:href=
"createPath"
data-testid=
"new-deploy-key-button"
>
{{
$options
.
i18n
.
newDeployKeyButtonText
}}
</gl-button>
</div>
<gl-table
:fields=
"$options.fields"
data-testid=
"deploy-keys-list"
/>
<template
v-if=
"shouldShowTable"
>
<gl-table
:busy=
"loading"
:items=
"items"
:fields=
"$options.fields"
stacked=
"lg"
data-testid=
"deploy-keys-list"
>
<template
#table-busy
>
<gl-loading-icon
size=
"lg"
class=
"gl-my-5"
/>
</
template
>
<
template
#cell(projects)=
"{ item: { projects } }"
>
<a
v-for=
"project in projects"
:key=
"project.id"
:href=
"projectHref(project)"
class=
"gl-display-block"
>
{{
project
.
name_with_namespace
}}
</a
>
</
template
>
<
template
#cell(fingerprint)=
"{ item: { fingerprint } }"
>
<code>
{{
fingerprint
}}
</code>
</
template
>
<
template
#cell(created)=
"{ item: { created } }"
>
<time-ago-tooltip
:time=
"created"
/>
</
template
>
<
template
#head(actions)=
"{ label }"
>
<span
class=
"gl-sr-only"
>
{{
label
}}
</span>
</
template
>
<
template
#cell(actions)=
"{ item: { id } }"
>
<gl-button
icon=
"pencil"
:aria-label=
"$options.i18n.edit"
:href=
"editHref(id)"
class=
"gl-mr-2"
/>
<gl-button
variant=
"danger"
icon=
"remove"
:aria-label=
"$options.i18n.remove"
/>
</
template
>
</gl-table>
<gl-pagination
v-if=
"!loading"
v-model=
"page"
:per-page=
"$options.DEFAULT_PER_PAGE"
:total-items=
"totalItems"
:next-text=
"$options.i18n.pagination.next"
:prev-text=
"$options.i18n.pagination.prev"
align=
"center"
/>
</template>
<gl-empty-state
v-else
:svg-path=
"emptyStateSvgPath"
:title=
"$options.i18n.emptyStateTitle"
:description=
"$options.i18n.emptyStateDescription"
:primary-button-text=
"$options.i18n.newDeployKeyButtonText"
:primary-button-link=
"createPath"
/>
</div>
</template>
app/assets/javascripts/api.js
View file @
f3355c81
...
...
@@ -91,6 +91,7 @@ const Api = {
projectNotificationSettingsPath
:
'
/api/:version/projects/:id/notification_settings
'
,
groupNotificationSettingsPath
:
'
/api/:version/groups/:id/notification_settings
'
,
notificationSettingsPath
:
'
/api/:version/notification_settings
'
,
deployKeysPath
:
'
/api/:version/deploy_keys
'
,
group
(
groupId
,
callback
=
()
=>
{})
{
const
url
=
Api
.
buildUrl
(
Api
.
groupPath
).
replace
(
'
:id
'
,
groupId
);
...
...
@@ -950,6 +951,12 @@ const Api = {
return
axios
.
delete
(
url
);
},
deployKeys
(
params
=
{})
{
const
url
=
Api
.
buildUrl
(
this
.
deployKeysPath
);
return
axios
.
get
(
url
,
{
params
:
{
per_page
:
DEFAULT_PER_PAGE
,
...
params
}
});
},
async
updateNotificationSettings
(
projectId
,
groupId
,
data
=
{})
{
let
url
=
Api
.
buildUrl
(
this
.
notificationSettingsPath
);
...
...
locale/gitlab.pot
View file @
f3355c81
...
...
@@ -3576,6 +3576,9 @@ msgstr ""
msgid "An error occurred fetching the project authors."
msgstr ""
msgid "An error occurred fetching the public deploy keys. Please try again."
msgstr ""
msgid "An error occurred previewing the blob"
msgstr ""
...
...
@@ -23539,6 +23542,9 @@ msgstr ""
msgid "No projects found"
msgstr ""
msgid "No public deploy keys"
msgstr ""
msgid "No public groups"
msgstr ""
...
...
spec/frontend/admin/deploy_keys/components/table_spec.js
View file @
f3355c81
import
{
merge
}
from
'
lodash
'
;
import
{
GlTable
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
GlLoadingIcon
,
GlEmptyState
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
nextTick
}
from
'
vue
'
;
import
responseBody
from
'
test_fixtures/api/deploy_keys/index.json
'
;
import
{
mountExtended
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
DeployKeysTable
from
'
~/admin/deploy_keys/components/table.vue
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
Api
,
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
createFlash
from
'
~/flash
'
;
jest
.
mock
(
'
~/api
'
);
jest
.
mock
(
'
~/flash
'
);
describe
(
'
DeployKeysTable
'
,
()
=>
{
let
wrapper
;
...
...
@@ -14,12 +23,53 @@ describe('DeployKeysTable', () => {
emptyStateSvgPath
:
'
/assets/illustrations/empty-state/empty-deploy-keys.svg
'
,
};
const
deployKey
=
responseBody
[
0
];
const
deployKey2
=
responseBody
[
1
];
const
createComponent
=
(
provide
=
{})
=>
{
wrapper
=
mountExtended
(
DeployKeysTable
,
{
provide
:
merge
({},
defaultProvide
,
provide
),
});
};
const
findEditButton
=
(
index
)
=>
wrapper
.
findAllByLabelText
(
DeployKeysTable
.
i18n
.
edit
,
{
selector
:
'
a
'
}).
at
(
index
);
const
findRemoveButton
=
(
index
)
=>
wrapper
.
findAllByLabelText
(
DeployKeysTable
.
i18n
.
remove
,
{
selector
:
'
button
'
}).
at
(
index
);
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
const
findTimeAgoTooltip
=
(
index
)
=>
wrapper
.
findAllComponents
(
TimeAgoTooltip
).
at
(
index
);
const
findPagination
=
()
=>
wrapper
.
findComponent
(
GlPagination
);
const
expectDeployKeyIsRendered
=
(
expectedDeployKey
,
expectedRowIndex
)
=>
{
const
editButton
=
findEditButton
(
expectedRowIndex
);
const
timeAgoTooltip
=
findTimeAgoTooltip
(
expectedRowIndex
);
expect
(
wrapper
.
findByText
(
expectedDeployKey
.
title
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
findByText
(
expectedDeployKey
.
fingerprint
,
{
selector
:
'
code
'
}).
exists
()).
toBe
(
true
,
);
expect
(
timeAgoTooltip
.
exists
()).
toBe
(
true
);
expect
(
timeAgoTooltip
.
props
(
'
time
'
)).
toBe
(
expectedDeployKey
.
created_at
);
expect
(
editButton
.
exists
()).
toBe
(
true
);
expect
(
editButton
.
attributes
(
'
href
'
)).
toBe
(
`/admin/deploy_keys/
${
expectedDeployKey
.
id
}
/edit`
);
expect
(
findRemoveButton
(
expectedRowIndex
).
exists
()).
toBe
(
true
);
};
const
itRendersTheEmptyState
=
()
=>
{
it
(
'
renders empty state
'
,
()
=>
{
const
emptyState
=
wrapper
.
findComponent
(
GlEmptyState
);
expect
(
emptyState
.
exists
()).
toBe
(
true
);
expect
(
emptyState
.
props
()).
toMatchObject
({
svgPath
:
defaultProvide
.
emptyStateSvgPath
,
title
:
DeployKeysTable
.
i18n
.
emptyStateTitle
,
description
:
DeployKeysTable
.
i18n
.
emptyStateDescription
,
primaryButtonText
:
DeployKeysTable
.
i18n
.
newDeployKeyButtonText
,
primaryButtonLink
:
defaultProvide
.
createPath
,
});
});
};
afterEach
(()
=>
{
wrapper
.
destroy
();
});
...
...
@@ -30,18 +80,128 @@ describe('DeployKeysTable', () => {
expect
(
wrapper
.
findByText
(
DeployKeysTable
.
i18n
.
pageTitle
).
exists
()).
toBe
(
true
);
});
it
(
'
renders
table
'
,
()
=>
{
it
(
'
renders
`New deploy key` button
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
findComponent
(
GlTable
).
exists
()).
toBe
(
true
);
const
newDeployKeyButton
=
wrapper
.
findByTestId
(
'
new-deploy-key-button
'
);
expect
(
newDeployKeyButton
.
exists
()).
toBe
(
true
);
expect
(
newDeployKeyButton
.
attributes
(
'
href
'
)).
toBe
(
defaultProvide
.
createPath
);
});
it
(
'
renders `New deploy key` button
'
,
()
=>
{
describe
(
'
when `/deploy_keys` API request is pending
'
,
()
=>
{
beforeEach
(()
=>
{
Api
.
deployKeys
.
mockImplementation
(()
=>
new
Promise
(()
=>
{}));
});
it
(
'
shows loading icon
'
,
async
()
=>
{
createComponent
();
const
newDeployKeyButton
=
wrapper
.
findComponent
(
GlButton
);
await
nextTick
(
);
expect
(
newDeployKeyButton
.
text
()).
toBe
(
DeployKeysTable
.
i18n
.
newDeployKeyButtonText
);
expect
(
newDeployKeyButton
.
attributes
(
'
href
'
)).
toBe
(
defaultProvide
.
createPath
);
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
when `/deploy_keys` API request is successful
'
,
()
=>
{
describe
(
'
when there are deploy keys
'
,
()
=>
{
beforeEach
(()
=>
{
Api
.
deployKeys
.
mockResolvedValue
({
data
:
responseBody
,
headers
:
{
'
x-total
'
:
`
${
responseBody
.
length
}
`
},
});
createComponent
();
});
it
(
'
renders deploy keys in table
'
,
()
=>
{
expectDeployKeyIsRendered
(
deployKey
,
0
);
expectDeployKeyIsRendered
(
deployKey2
,
1
);
});
});
describe
(
'
pagination
'
,
()
=>
{
beforeEach
(()
=>
{
Api
.
deployKeys
.
mockResolvedValueOnce
({
data
:
[
deployKey
],
headers
:
{
'
x-total
'
:
'
2
'
},
});
createComponent
();
});
it
(
'
renders pagination
'
,
()
=>
{
const
pagination
=
findPagination
();
expect
(
pagination
.
exists
()).
toBe
(
true
);
expect
(
pagination
.
props
()).
toMatchObject
({
value
:
1
,
perPage
:
DEFAULT_PER_PAGE
,
totalItems
:
responseBody
.
length
,
nextText
:
DeployKeysTable
.
i18n
.
pagination
.
next
,
prevText
:
DeployKeysTable
.
i18n
.
pagination
.
prev
,
align
:
'
center
'
,
});
});
describe
(
'
when pagination is changed
'
,
()
=>
{
it
(
'
calls API with `page` parameter
'
,
async
()
=>
{
const
pagination
=
findPagination
();
expectDeployKeyIsRendered
(
deployKey
,
0
);
Api
.
deployKeys
.
mockResolvedValue
({
data
:
[
deployKey2
],
headers
:
{
'
x-total
'
:
'
2
'
},
});
pagination
.
vm
.
$emit
(
'
input
'
,
2
);
await
nextTick
();
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
expect
(
pagination
.
exists
()).
toBe
(
false
);
await
waitForPromises
();
expect
(
Api
.
deployKeys
).
toHaveBeenCalledWith
({
page
:
2
,
public
:
true
,
});
expectDeployKeyIsRendered
(
deployKey2
,
0
);
});
});
});
describe
(
'
when there are no deploy keys
'
,
()
=>
{
beforeEach
(()
=>
{
Api
.
deployKeys
.
mockResolvedValue
({
data
:
[],
headers
:
{
'
x-total
'
:
'
0
'
},
});
createComponent
();
});
itRendersTheEmptyState
();
});
});
describe
(
'
when `deploy_keys` API request is unsuccessful
'
,
()
=>
{
const
error
=
new
Error
(
'
Network Error
'
);
beforeEach
(()
=>
{
Api
.
deployKeys
.
mockRejectedValue
(
error
);
createComponent
();
});
itRendersTheEmptyState
();
it
(
'
displays flash
'
,
()
=>
{
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
DeployKeysTable
.
i18n
.
apiErrorMessage
,
captureError
:
true
,
error
,
});
});
});
});
spec/frontend/api_spec.js
View file @
f3355c81
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Api
from
'
~/api
'
;
import
Api
,
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
httpStatus
from
'
~/lib/utils/http_status
'
;
...
...
@@ -1574,6 +1574,51 @@ describe('Api', () => {
});
});
describe
(
'
deployKeys
'
,
()
=>
{
it
(
'
fetches deploy keys
'
,
async
()
=>
{
const
deployKeys
=
[
{
id
:
7
,
title
:
'
My title 1
'
,
created_at
:
'
2021-10-29T16:59:55.229Z
'
,
expires_at
:
null
,
key
:
'
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDLvQzRX960N7dxPdge9o5a96+M4GEGQ7rxT2D3wAQDtQFjQV5ZcKb5wfeLtYLe3kRVI4lCO10PXeQppb1XBaYmVO31IaRkcgmMEPVyfp76Dp4CJZz6aMEbbcqfaHkDre0Fa8kzTXnBJVh2NeDbBfGMjFM5NRQLhKykodNsepO6dQ== dummy@gitlab.com
'
,
fingerprint
:
'
81:93:63:b9:1e:24:a2:aa:e0:87:d3:3f:42:81:f2:c2
'
,
projects_with_write_access
:
[
{
id
:
11
,
description
:
null
,
name
:
'
project1
'
,
name_with_namespace
:
'
John Doe3 / project1
'
,
path
:
'
project1
'
,
path_with_namespace
:
'
namespace1/project1
'
,
created_at
:
'
2021-10-29T16:59:54.668Z
'
,
},
{
id
:
12
,
description
:
null
,
name
:
'
project2
'
,
name_with_namespace
:
'
John Doe4 / project2
'
,
path
:
'
project2
'
,
path_with_namespace
:
'
namespace2/project2
'
,
created_at
:
'
2021-10-29T16:59:55.116Z
'
,
},
],
},
];
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/deploy_keys`
;
mock
.
onGet
(
expectedUrl
).
reply
(
httpStatus
.
OK
,
deployKeys
);
const
params
=
{
page
:
2
,
public
:
true
};
const
{
data
}
=
await
Api
.
deployKeys
(
params
);
expect
(
data
).
toEqual
(
deployKeys
);
expect
(
mock
.
history
.
get
[
0
].
params
).
toEqual
({
...
params
,
per_page
:
DEFAULT_PER_PAGE
});
});
});
describe
(
'
Feature Flag User List
'
,
()
=>
{
let
expectedUrl
;
let
projectId
;
...
...
spec/frontend/fixtures/api_deploy_keys.rb
0 → 100644
View file @
f3355c81
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
API
::
DeployKeys
,
'(JavaScript fixtures)'
,
type: :request
do
include
ApiHelpers
include
JavaScriptFixturesHelpers
let_it_be
(
:admin
)
{
create
(
:admin
)
}
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project2
)
{
create
(
:project
)
}
let_it_be
(
:deploy_key
)
{
create
(
:deploy_key
,
public:
true
)
}
let_it_be
(
:deploy_key2
)
{
create
(
:deploy_key
,
public:
true
)
}
let_it_be
(
:deploy_keys_project
)
{
create
(
:deploy_keys_project
,
:write_access
,
project:
project
,
deploy_key:
deploy_key
)
}
let_it_be
(
:deploy_keys_project2
)
{
create
(
:deploy_keys_project
,
:write_access
,
project:
project2
,
deploy_key:
deploy_key
)
}
let_it_be
(
:deploy_keys_project3
)
{
create
(
:deploy_keys_project
,
:write_access
,
project:
project
,
deploy_key:
deploy_key2
)
}
let_it_be
(
:deploy_keys_project4
)
{
create
(
:deploy_keys_project
,
:write_access
,
project:
project2
,
deploy_key:
deploy_key2
)
}
it
'api/deploy_keys/index.json'
do
get
api
(
"/deploy_keys"
,
admin
)
expect
(
response
).
to
be_successful
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment