Commit b04b5557 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent a8491ccdc731
......@@ -325,11 +325,11 @@
}
.btn {
margin: $btn-side-margin 5px;
margin: $gl-padding-8 $gl-padding-4;
@include media-breakpoint-down(xs) {
width: 100%;
margin: $btn-side-margin 0;
margin: $gl-padding-8 0;
}
}
}
......
......@@ -179,6 +179,16 @@ def test_report
end
end
def test_reports_count
return unless Feature.enabled?(:junit_pipeline_view, project)
begin
render json: { total_count: pipeline.test_reports_count }.to_json
rescue Gitlab::Ci::Parsers::ParserError
render json: { total_count: 0 }.to_json
end
end
private
def serialize_pipelines
......
......@@ -789,6 +789,12 @@ def test_reports
end
end
def test_reports_count
Rails.cache.fetch(['project', project.id, 'pipeline', id, 'test_reports_count'], force: false) do
test_reports.total_count
end
end
def has_exposed_artifacts?
complete? && builds.latest.with_exposed_artifacts.exists?
end
......
......@@ -30,5 +30,5 @@
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
- button_path = local_assigns.fetch(:button_path, false)
.row.empty-state
.row.empty-state.mt-0
.col-12
.svg-content
= image_tag 'illustrations/snippets_empty.svg'
.text-content
.text-content.text-center.pt-0
- if current_user
%h4
= s_('SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep.')
%p
= s_('SnippetsEmptyState|They can be either public or private.')
.text-center
= s_('SnippetsEmptyState|Code snippets')
%p.mb-0
= s_('SnippetsEmptyState|Store, share, and embed small pieces of code and text.')
.mt-2<
- if button_path
= link_to s_('SnippetsEmptyState|New snippet'), button_path, class: 'btn btn-success', title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link'
- unless current_page?(dashboard_snippets_path)
= link_to s_('SnippetsEmptyState|Explore public snippets'), explore_snippets_path, class: 'btn btn-default', title: s_('SnippetsEmptyState|Explore public snippets')
= link_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), class: 'btn btn-default', title: s_('SnippetsEmptyState|Documentation')
- else
%h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.')
......
---
title: Update snippets empty state and remove explore snippets button
merge_request: 24764
author:
type: other
---
title: Update button margin of various empty states
merge_request: 24806
author:
type: other
---
title: Fix signature badge popover on Firefox
merge_request: 24756
author:
type: fixed
---
title: Add Group Import API endpoint & update Group Import/Export documentation
merge_request: 20353
author:
type: added
......@@ -341,6 +341,7 @@
get :failures
get :status
get :test_report
get :test_reports_count
end
member do
......
# Group Import/Export API
> Introduced in GitLab 12.8 as an experimental feature. May change in future releases.
Group Import/Export functionality allows to export group structure and import it at a new location.
Used in combination with [Project Import/Export](project_import_export.md) it allows you to preserve connections with group level relations
(e.g. a connection between a project issue and group epic).
Group Export includes:
1. Group Milestones
1. Group Boards
1. Group Labels
1. Group Badges
1. Group Members
1. Sub-groups (each sub-group includes all data above)
## Schedule new export
Start a new group export.
```text
POST /groups/:id/export
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | ID of the groupd owned by the authenticated user |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/export
```
```json
{
"message": "202 Accepted"
}
```
## Export download
Download the finished export.
```text
GET /groups/:id/export/download
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | ID of the group owned by the authenticated user |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --remote-header-name --remote-name https://gitlab.example.com/api/v4/groups/1/export/download
```
```shell
ls *export.tar.gz
2020-12-05_22-11-148_namespace_export.tar.gz
```
Time spent on exporting a group may vary depending on a size of the group. Export download endpoint will return exported archive once it is available. 404 is returned otherwise.
## Import a file
```text
POST /groups/import
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `name` | string | yes | The name of the group to be imported |
| `path` | string | yes | Name and path for new group |
| `file` | string | yes | The file to be uploaded |
| `parent_id` | integer | no | ID of a parent group that the group will be imported into. Defaults to the current user's namespace if not provided. |
To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "name=imported-group" --form "path=imported-group" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/groups/import
```
......@@ -875,3 +875,7 @@ And to switch pages add:
## Group badges
Read more in the [Group Badges](group_badges.md) documentation.
## Group Import/Export
Read more in the [Group Import/Export](group_import_export.md) documentation.
......@@ -130,6 +130,7 @@ class API < Grape::API
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport
mount ::API::GroupImport
mount ::API::GroupLabels
mount ::API::GroupMilestones
mount ::API::Groups
......
# frozen_string_literal: true
module API
class GroupImport < Grape::API
MAXIMUM_FILE_SIZE = 50.megabytes.freeze
helpers do
def authorize_create_group!
parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
if parent_group
authorize! :create_subgroup, parent_group
else
authorize! :create_group
end
end
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Workhorse authorize the group import upload' do
detail 'This feature was introduced in GitLab 12.8'
end
post 'import/authorize' do
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
ImportExportUploader.workhorse_authorize(has_length: false, maximum_size: MAXIMUM_FILE_SIZE)
end
desc 'Create a new group import' do
detail 'This feature was introduced in GitLab 12.8'
success Entities::Group
end
params do
requires :path, type: String, desc: 'Group path'
requires :name, type: String, desc: 'Group name'
optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace."
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
end
post 'import' do
authorize_create_group!
require_gitlab_workhorse!
uploaded_file = UploadedFile.from_params(params, :file, ImportExportUploader.workhorse_local_upload_path)
bad_request!('Unable to process group import file') unless uploaded_file
group_params = {
path: params[:path],
name: params[:name],
parent_id: params[:parent_id],
import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
}
group = ::Groups::CreateService.new(current_user, group_params).execute
if group.persisted?
GroupImportWorker.perform_async(current_user.id, group.id)
accepted!
else
render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
end
end
end
end
# frozen_string_literal: true
module API
module Helpers
module FileUploadHelpers
def file_is_valid?
params[:file] && params[:file]['tempfile'].respond_to?(:read)
end
def validate_file!
render_api_error!('Uploaded file is invalid', 400) unless file_is_valid?
end
end
end
end
......@@ -5,20 +5,13 @@ class ProjectImport < Grape::API
include PaginationParams
helpers Helpers::ProjectsHelpers
helpers Helpers::FileUploadHelpers
helpers do
def import_params
declared_params(include_missing: false)
end
def file_is_valid?
import_params[:file] && import_params[:file]['tempfile'].respond_to?(:read)
end
def validate_file!
render_api_error!('The file is invalid', 400) unless file_is_valid?
end
def throttled?(key, scope)
rate_limiter.throttled?(key, scope: scope)
end
......
......@@ -74,6 +74,15 @@ def save_project_id(result)
)
if response
# In the add_prometheus_manual_configuration method, the Prometheus
# listen_address config is saved as an api_url in the PrometheusService
# model. There are validates hooks in the PrometheusService model that
# check if the project associated with the PrometheusService is the
# self_monitoring project. It checks
# Gitlab::CurrentSettings.self_monitoring_project_id, which is why the
# Gitlab::CurrentSettings cache needs to be expired here, so that
# PrometheusService sees the latest self_monitoring_project_id.
Gitlab::CurrentSettings.expire_current_application_settings
success(result)
else
log_error("Could not save instance administration project ID, errors: %{errors}" % { errors: application_settings.errors.full_messages })
......
......@@ -17597,7 +17597,10 @@ msgstr ""
msgid "Snippets"
msgstr ""
msgid "SnippetsEmptyState|Explore public snippets"
msgid "SnippetsEmptyState|Code snippets"
msgstr ""
msgid "SnippetsEmptyState|Documentation"
msgstr ""
msgid "SnippetsEmptyState|New snippet"
......@@ -17606,15 +17609,12 @@ msgstr ""
msgid "SnippetsEmptyState|No snippets found"
msgstr ""
msgid "SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep."
msgid "SnippetsEmptyState|Store, share, and embed small pieces of code and text."
msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
msgid "SnippetsEmptyState|They can be either public or private."
msgstr ""
msgid "Snowplow"
msgstr ""
......
......@@ -682,6 +682,76 @@ def get_stage_ajax(name)
end
end
describe 'GET test_report_count.json' do
subject(:test_reports_count_json) do
get :test_reports_count, params: {
namespace_id: project.namespace,
project_id: project,
id: pipeline.id
},
format: :json
end
context 'when feature is enabled' do
before do
stub_feature_flags(junit_pipeline_view: true)
end
context 'when pipeline does not have a test report' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'renders an empty badge counter' do
test_reports_count_json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['total_count']).to eq(0)
end
end
context 'when pipeline has a test report' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
it 'renders the badge counter value' do
test_reports_count_json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['total_count']).to eq(4)
end
end
context 'when pipeline has corrupt test reports' do
let(:pipeline) { create(:ci_pipeline, project: project) }
before do
job = create(:ci_build, pipeline: pipeline)
create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
end
it 'renders 0' do
test_reports_count_json
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['total_count']).to eq(0)
end
end
end
context 'when feature is disabled' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
before do
stub_feature_flags(junit_pipeline_view: false)
end
it 'renders empty response' do
test_reports_count_json
expect(response).to have_gitlab_http_status(:no_content)
expect(response.body).to be_empty
end
end
end
describe 'GET latest' do
let(:branch_main) { project.repository.branches[0] }
let(:branch_secondary) { project.repository.branches[1] }
......
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