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
01d2ad15
Commit
01d2ad15
authored
Mar 07, 2022
by
Jose Ivan Vargas
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'initial-secure-files-ui' into 'master'
Secure Files UI MVC See merge request gitlab-org/gitlab!79784
parents
2f929d21
406444b4
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
447 additions
and
0 deletions
+447
-0
app/assets/javascripts/api.js
app/assets/javascripts/api.js
+8
-0
app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
...ascripts/ci_secure_files/components/secure_files_list.vue
+133
-0
app/assets/javascripts/ci_secure_files/index.js
app/assets/javascripts/ci_secure_files/index.js
+17
-0
app/assets/javascripts/pages/projects/ci/secure_files/show/index.js
.../javascripts/pages/projects/ci/secure_files/show/index.js
+3
-0
app/controllers/projects/ci/secure_files_controller.rb
app/controllers/projects/ci/secure_files_controller.rb
+16
-0
app/views/projects/ci/secure_files/show.html.haml
app/views/projects/ci/secure_files/show.html.haml
+5
-0
config/routes/project.rb
config/routes/project.rb
+1
-0
lib/api/entities/ci/secure_file.rb
lib/api/entities/ci/secure_file.rb
+1
-0
locale/gitlab.pot
locale/gitlab.pot
+9
-0
spec/controllers/projects/ci/secure_files_controller_spec.rb
spec/controllers/projects/ci/secure_files_controller_spec.rb
+49
-0
spec/features/projects/ci/secure_files_spec.rb
spec/features/projects/ci/secure_files_spec.rb
+19
-0
spec/frontend/api_spec.js
spec/frontend/api_spec.js
+22
-0
spec/frontend/ci_secure_files/components/secure_files_list_spec.js
...tend/ci_secure_files/components/secure_files_list_spec.js
+139
-0
spec/frontend/ci_secure_files/mock_data.js
spec/frontend/ci_secure_files/mock_data.js
+18
-0
spec/requests/api/ci/secure_files_spec.rb
spec/requests/api/ci/secure_files_spec.rb
+1
-0
spec/routing/project_routing_spec.rb
spec/routing/project_routing_spec.rb
+6
-0
No files found.
app/assets/javascripts/api.js
View file @
01d2ad15
...
...
@@ -92,6 +92,7 @@ const Api = {
groupNotificationSettingsPath
:
'
/api/:version/groups/:id/notification_settings
'
,
notificationSettingsPath
:
'
/api/:version/notification_settings
'
,
deployKeysPath
:
'
/api/:version/deploy_keys
'
,
secureFilesPath
:
'
/api/:version/projects/:project_id/secure_files
'
,
group
(
groupId
,
callback
=
()
=>
{})
{
const
url
=
Api
.
buildUrl
(
Api
.
groupPath
).
replace
(
'
:id
'
,
groupId
);
...
...
@@ -957,6 +958,13 @@ const Api = {
return
axios
.
get
(
url
,
{
params
:
{
per_page
:
DEFAULT_PER_PAGE
,
...
params
}
});
},
// TODO: replace this when GraphQL support has been added https://gitlab.com/gitlab-org/gitlab/-/issues/352184
projectSecureFiles
(
projectId
,
options
=
{})
{
const
url
=
Api
.
buildUrl
(
this
.
secureFilesPath
).
replace
(
'
:project_id
'
,
projectId
);
return
axios
.
get
(
url
,
{
params
:
{
per_page
:
DEFAULT_PER_PAGE
,
...
options
}
});
},
async
updateNotificationSettings
(
projectId
,
groupId
,
data
=
{})
{
let
url
=
Api
.
buildUrl
(
this
.
notificationSettingsPath
);
...
...
app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
0 → 100644
View file @
01d2ad15
<
script
>
import
{
GlLink
,
GlLoadingIcon
,
GlPagination
,
GlTable
}
from
'
@gitlab/ui
'
;
import
Api
,
{
DEFAULT_PER_PAGE
}
from
'
~/api
'
;
import
{
helpPagePath
}
from
'
~/helpers/help_page_helper
'
;
import
{
__
}
from
'
~/locale
'
;
import
TimeagoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
components
:
{
GlLink
,
GlLoadingIcon
,
GlPagination
,
GlTable
,
TimeagoTooltip
,
},
inject
:
[
'
projectId
'
],
docsLink
:
helpPagePath
(
'
ci/secure_files/index
'
),
DEFAULT_PER_PAGE
,
i18n
:
{
pagination
:
{
next
:
__
(
'
Next
'
),
prev
:
__
(
'
Prev
'
),
},
title
:
__
(
'
Secure Files
'
),
overviewMessage
:
__
(
'
Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates.
'
,
),
moreInformation
:
__
(
'
More information
'
),
},
data
()
{
return
{
page
:
1
,
totalItems
:
0
,
loading
:
false
,
projectSecureFiles
:
[],
};
},
fields
:
[
{
key
:
'
name
'
,
label
:
__
(
'
Filename
'
),
},
{
key
:
'
permissions
'
,
label
:
__
(
'
Permissions
'
),
},
{
key
:
'
created_at
'
,
label
:
__
(
'
Uploaded
'
),
},
],
computed
:
{
fields
()
{
return
this
.
$options
.
fields
;
},
},
watch
:
{
page
(
newPage
)
{
this
.
getProjectSecureFiles
(
newPage
);
},
},
created
()
{
this
.
getProjectSecureFiles
();
},
methods
:
{
async
getProjectSecureFiles
(
page
)
{
this
.
loading
=
true
;
const
response
=
await
Api
.
projectSecureFiles
(
this
.
projectId
,
{
page
});
this
.
totalItems
=
parseInt
(
response
.
headers
?.[
'
x-total
'
],
10
)
||
0
;
this
.
projectSecureFiles
=
response
.
data
;
this
.
loading
=
false
;
},
},
};
</
script
>
<
template
>
<div>
<h1
data-testid=
"title"
class=
"gl-font-size-h1 gl-mt-3 gl-mb-0"
>
{{
$options
.
i18n
.
title
}}
</h1>
<p>
<span
data-testid=
"info-message"
class=
"gl-mr-2"
>
{{
$options
.
i18n
.
overviewMessage
}}
<gl-link
:href=
"$options.docsLink"
target=
"_blank"
>
{{
$options
.
i18n
.
moreInformation
}}
</gl-link>
</span>
</p>
<gl-table
:busy=
"loading"
:fields=
"fields"
:items=
"projectSecureFiles"
tbody-tr-class=
"js-ci-secure-files-row"
data-qa-selector=
"ci_secure_files_table_content"
sort-by=
"key"
sort-direction=
"asc"
stacked=
"lg"
table-class=
"text-secondary"
show-empty
sort-icon-left
no-sort-reset
>
<template
#table-busy
>
<gl-loading-icon
size=
"lg"
class=
"gl-my-5"
/>
</
template
>
<
template
#cell(name)=
"{ item }"
>
{{
item
.
name
}}
</
template
>
<
template
#cell(permissions)=
"{ item }"
>
{{
item
.
permissions
}}
</
template
>
<
template
#cell(created_at)=
"{ item }"
>
<timeago-tooltip
:time=
"item.created_at"
/>
</
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"
/>
</div>
</template>
app/assets/javascripts/ci_secure_files/index.js
0 → 100644
View file @
01d2ad15
import
Vue
from
'
vue
'
;
import
SecureFilesList
from
'
./components/secure_files_list.vue
'
;
export
const
initCiSecureFiles
=
(
selector
=
'
#js-ci-secure-files
'
)
=>
{
const
containerEl
=
document
.
querySelector
(
selector
);
const
{
projectId
}
=
containerEl
.
dataset
;
return
new
Vue
({
el
:
containerEl
,
provide
:
{
projectId
,
},
render
(
createElement
)
{
return
createElement
(
SecureFilesList
);
},
});
};
app/assets/javascripts/pages/projects/ci/secure_files/show/index.js
0 → 100644
View file @
01d2ad15
import
{
initCiSecureFiles
}
from
'
~/ci_secure_files
'
;
initCiSecureFiles
();
app/controllers/projects/ci/secure_files_controller.rb
0 → 100644
View file @
01d2ad15
# frozen_string_literal: true
class
Projects::Ci::SecureFilesController
<
Projects
::
ApplicationController
before_action
:check_can_collaborate!
feature_category
:pipeline_authoring
def
show
end
private
def
check_can_collaborate!
render_404
unless
can_collaborate_with_project?
(
project
)
end
end
app/views/projects/ci/secure_files/show.html.haml
0 → 100644
View file @
01d2ad15
-
@content_class
=
"limit-container-width"
-
page_title
s_
(
'Secure Files'
)
#js-ci-secure-files
{
data:
{
project_id:
@project
.
id
}
}
config/routes/project.rb
View file @
01d2ad15
...
...
@@ -96,6 +96,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace
:ci
do
resource
:lint
,
only:
[
:show
,
:create
]
resource
:pipeline_editor
,
only:
[
:show
],
controller: :pipeline_editor
,
path:
'editor'
resource
:secure_files
,
only:
[
:show
],
controller: :secure_files
,
path:
'secure_files'
resources
:daily_build_group_report_results
,
only:
[
:index
],
constraints:
{
format:
/(csv|json)/
}
namespace
:prometheus_metrics
do
resources
:histograms
,
only:
[
:create
],
constraints:
{
format:
'json'
}
...
...
lib/api/entities/ci/secure_file.rb
View file @
01d2ad15
...
...
@@ -9,6 +9,7 @@ module API
expose
:permissions
expose
:checksum
expose
:checksum_algorithm
expose
:created_at
end
end
end
...
...
locale/gitlab.pot
View file @
01d2ad15
...
...
@@ -32383,6 +32383,9 @@ msgstr ""
msgid "Secret token"
msgstr ""
msgid "Secure Files"
msgstr ""
msgid "Secure token that identifies an external storage request."
msgstr ""
...
...
@@ -39496,6 +39499,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
msgid "Uploaded"
msgstr ""
msgid "Uploading changes to terminal"
msgstr ""
...
...
@@ -39790,6 +39796,9 @@ msgstr ""
msgid "Use GitLab Runner in AWS"
msgstr ""
msgid "Use Secure Files to store files used by your pipelines such as Android keystores, or Apple provisioning profiles and signing certificates."
msgstr ""
msgid "Use a one-time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
msgstr ""
...
...
spec/controllers/projects/ci/secure_files_controller_spec.rb
0 → 100644
View file @
01d2ad15
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
Projects
::
Ci
::
SecureFilesController
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:user
)
{
create
(
:user
)
}
subject
(
:show_request
)
{
get
:show
,
params:
{
namespace_id:
project
.
namespace
,
project_id:
project
}
}
describe
'GET #show'
do
context
'with enough privileges'
do
before
do
sign_in
(
user
)
project
.
add_developer
(
user
)
show_request
end
it
{
expect
(
response
).
to
have_gitlab_http_status
(
:ok
)
}
it
'renders show page'
do
expect
(
response
).
to
render_template
:show
end
end
context
'without enough privileges'
do
before
do
sign_in
(
user
)
project
.
add_reporter
(
user
)
show_request
end
it
'responds with 404'
do
expect
(
response
).
to
have_gitlab_http_status
(
:not_found
)
end
end
context
'an unauthenticated user'
do
before
do
show_request
end
it
'redirects to sign in'
do
expect
(
response
).
to
have_gitlab_http_status
(
:found
)
expect
(
response
).
to
redirect_to
(
'/users/sign_in'
)
end
end
end
end
spec/features/projects/ci/secure_files_spec.rb
0 → 100644
View file @
01d2ad15
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
'Secure Files'
,
:js
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:user
)
{
create
(
:user
)
}
before
do
project
.
add_maintainer
(
user
)
sign_in
(
user
)
visit
project_ci_secure_files_path
(
project
)
end
it
'user sees the Secure Files list component'
do
expect
(
page
).
to
have_content
(
'There are no records to show'
)
end
end
spec/frontend/api_spec.js
View file @
01d2ad15
...
...
@@ -1619,6 +1619,28 @@ describe('Api', () => {
});
});
describe
(
'
projectSecureFiles
'
,
()
=>
{
it
(
'
fetches secure files for a project
'
,
async
()
=>
{
const
projectId
=
1
;
const
secureFiles
=
[
{
id
:
projectId
,
title
:
'
File Name
'
,
permissions
:
'
read_only
'
,
checksum
:
'
12345
'
,
checksum_algorithm
:
'
sha256
'
,
created_at
:
'
2022-02-21T15:27:18
'
,
},
];
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects/
${
projectId
}
/secure_files`
;
mock
.
onGet
(
expectedUrl
).
reply
(
httpStatus
.
OK
,
secureFiles
);
const
{
data
}
=
await
Api
.
projectSecureFiles
(
projectId
,
{});
expect
(
data
).
toEqual
(
secureFiles
);
});
});
describe
(
'
Feature Flag User List
'
,
()
=>
{
let
expectedUrl
;
let
projectId
;
...
...
spec/frontend/ci_secure_files/components/secure_files_list_spec.js
0 → 100644
View file @
01d2ad15
import
{
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
axios
from
'
~/lib/utils/axios_utils
'
;
import
SecureFilesList
from
'
~/ci_secure_files/components/secure_files_list.vue
'
;
import
TimeAgoTooltip
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
{
secureFiles
}
from
'
../mock_data
'
;
const
dummyApiVersion
=
'
v3000
'
;
const
dummyProjectId
=
1
;
const
dummyUrlRoot
=
'
/gitlab
'
;
const
dummyGon
=
{
api_version
:
dummyApiVersion
,
relative_url_root
:
dummyUrlRoot
,
};
let
originalGon
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects/
${
dummyProjectId
}
/secure_files`
;
describe
(
'
SecureFilesList
'
,
()
=>
{
let
wrapper
;
let
mock
;
beforeEach
(()
=>
{
originalGon
=
window
.
gon
;
window
.
gon
=
{
...
dummyGon
};
});
afterEach
(()
=>
{
wrapper
.
destroy
();
mock
.
restore
();
window
.
gon
=
originalGon
;
});
const
createWrapper
=
(
props
=
{})
=>
{
wrapper
=
mount
(
SecureFilesList
,
{
provide
:
{
projectId
:
dummyProjectId
},
...
props
,
});
};
const
findRows
=
()
=>
wrapper
.
findAll
(
'
tbody tr
'
);
const
findRowAt
=
(
i
)
=>
findRows
().
at
(
i
);
const
findCell
=
(
i
,
col
)
=>
findRowAt
(
i
).
findAll
(
'
td
'
).
at
(
col
);
const
findHeaderAt
=
(
i
)
=>
wrapper
.
findAll
(
'
thead th
'
).
at
(
i
);
const
findPagination
=
()
=>
wrapper
.
findAll
(
'
ul.pagination
'
);
const
findLoadingIcon
=
()
=>
wrapper
.
findComponent
(
GlLoadingIcon
);
describe
(
'
when secure files exist in a project
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
secureFiles
);
createWrapper
();
await
waitForPromises
();
});
it
(
'
displays a table with expected headers
'
,
()
=>
{
const
headers
=
[
'
Filename
'
,
'
Permissions
'
,
'
Uploaded
'
];
headers
.
forEach
((
header
,
i
)
=>
{
expect
(
findHeaderAt
(
i
).
text
()).
toBe
(
header
);
});
});
it
(
'
displays a table with rows
'
,
()
=>
{
expect
(
findRows
()).
toHaveLength
(
secureFiles
.
length
);
const
[
secureFile
]
=
secureFiles
;
expect
(
findCell
(
0
,
0
).
text
()).
toBe
(
secureFile
.
name
);
expect
(
findCell
(
0
,
1
).
text
()).
toBe
(
secureFile
.
permissions
);
expect
(
findCell
(
0
,
2
).
find
(
TimeAgoTooltip
).
props
(
'
time
'
)).
toBe
(
secureFile
.
created_at
);
});
});
describe
(
'
when no secure files exist in a project
'
,
()
=>
{
beforeEach
(
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
[]);
createWrapper
();
await
waitForPromises
();
});
it
(
'
displays a table with expected headers
'
,
()
=>
{
const
headers
=
[
'
Filename
'
,
'
Permissions
'
,
'
Uploaded
'
];
headers
.
forEach
((
header
,
i
)
=>
{
expect
(
findHeaderAt
(
i
).
text
()).
toBe
(
header
);
});
});
it
(
'
displays a table with a no records message
'
,
()
=>
{
expect
(
findCell
(
0
,
0
).
text
()).
toBe
(
'
There are no records to show
'
);
});
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
displays the pagination component with there are more than 20 items
'
,
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
secureFiles
,
{
'
x-total
'
:
30
});
createWrapper
();
await
waitForPromises
();
expect
(
findPagination
().
exists
()).
toBe
(
true
);
});
it
(
'
does not display the pagination component with there are 20 items
'
,
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
secureFiles
,
{
'
x-total
'
:
20
});
createWrapper
();
await
waitForPromises
();
expect
(
findPagination
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
loading state
'
,
()
=>
{
it
(
'
displays the loading icon while waiting for the backend request
'
,
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
secureFiles
);
createWrapper
();
expect
(
findLoadingIcon
().
exists
()).
toBe
(
true
);
});
it
(
'
does not display the loading icon after the backend request has completed
'
,
async
()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onGet
(
expectedUrl
).
reply
(
200
,
secureFiles
);
createWrapper
();
await
waitForPromises
();
expect
(
findLoadingIcon
().
exists
()).
toBe
(
false
);
});
});
});
spec/frontend/ci_secure_files/mock_data.js
0 → 100644
View file @
01d2ad15
export
const
secureFiles
=
[
{
id
:
1
,
name
:
'
myfile.jks
'
,
checksum
:
'
16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac
'
,
checksum_algorithm
:
'
sha256
'
,
permissions
:
'
read_only
'
,
created_at
:
'
2022-02-22T22:22:22.222Z
'
,
},
{
id
:
2
,
name
:
'
myotherfile.jks
'
,
checksum
:
'
16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2
'
,
checksum_algorithm
:
'
sha256
'
,
permissions
:
'
execute
'
,
created_at
:
'
2022-02-22T22:22:22.222Z
'
,
},
];
spec/requests/api/ci/secure_files_spec.rb
View file @
01d2ad15
...
...
@@ -154,6 +154,7 @@ RSpec.describe API::Ci::SecureFiles do
Digest
::
SHA256
.
hexdigest
(
fixture_file
(
'ci_secure_files/upload-keystore.jks'
))
)
expect
(
json_response
[
'id'
]).
to
eq
(
secure_file
.
id
)
expect
(
Time
.
parse
(
json_response
[
'created_at'
])).
to
be_like_time
(
secure_file
.
created_at
)
end
it
'creates a secure file with read_only permissions by default'
do
...
...
spec/routing/project_routing_spec.rb
View file @
01d2ad15
...
...
@@ -899,6 +899,12 @@ RSpec.describe 'project routing' do
end
end
describe
Projects
::
Ci
::
SecureFilesController
,
'routing'
do
it
'to #show'
do
expect
(
get
(
'/gitlab/gitlabhq/-/ci/secure_files'
)).
to
route_to
(
'projects/ci/secure_files#show'
,
namespace_id:
'gitlab'
,
project_id:
'gitlabhq'
)
end
end
context
'with a non-existent project'
do
it
'routes to 404 with get request'
do
expect
(
get:
"/gitlab/not_exist"
).
to
route_to
(
...
...
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