提交 cbc45b28 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 3827baae
......@@ -151,10 +151,6 @@
content: '\f0da';
}
.fa-info-circle::before {
content: '\f05a';
}
.fa-refresh::before {
content: '\f021';
}
......
......@@ -29,12 +29,6 @@
}
}
.is-showing-fly-out {
.feature-highlight {
display: none;
}
}
.feature-highlight-popover-content {
display: none;
......
......@@ -760,11 +760,6 @@ $mr-widget-min-height: 69px;
color: $gl-text-color;
}
.fa-info-circle {
color: $orange-500;
padding-right: 5px;
}
// Shortening button height by 1px to make compare-versions
// header 56px and fit into our 8px design grid
button {
......
......@@ -161,18 +161,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics_redirect
environment = project.default_environment
if environment
redirect_to environment_metrics_path(environment)
else
render :empty_metrics
end
redirect_to project_metrics_dashboard_path(project)
end
def metrics
respond_to do |format|
format.html
format.html do
redirect_to project_metrics_dashboard_path(project, environment: environment )
end
format.json do
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
......
......@@ -22,7 +22,7 @@ module Projects
if environment
render 'projects/environments/metrics'
else
render_404
render 'projects/environments/empty_metrics'
end
end
......
......@@ -748,6 +748,7 @@ module ProjectsHelper
gcp
logs
product_analytics
metrics_dashboard
]
end
......
......@@ -13,7 +13,8 @@ module UserCalloutsHelper
end
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
active_nav_link?(controller: sidebar_operations_paths) &&
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
......
......@@ -417,7 +417,7 @@ class Note < ApplicationRecord
end
def can_create_todo?
# Skip system notes, and notes on project snippet
# Skip system notes, and notes on snippets
!system? && !for_snippet?
end
......
......@@ -25,7 +25,7 @@ module Notes
note.note = content
end
unless only_commands
unless only_commands || note.for_personal_snippet?
note.create_new_cross_references!(current_user)
update_todos(note, old_mentioned_users)
......
......@@ -18,8 +18,8 @@
= number_to_percentage(card.percentage_score, precision: 1)
.board-card-buttons
- if card.blog
%a{ href: card.blog }
= icon('info-circle', 'aria-hidden' => 'true')
%a.btn-svg{ href: card.blog }
= sprite_icon('information-o')
- if card.docs
%a{ href: card.docs }
= icon('question-circle', 'aria-hidden' => 'true')
%a.btn-svg{ href: card.docs }
= sprite_icon('question-o')
......@@ -223,8 +223,8 @@
%li.divider.fly-out-top-item
- if project_nav_tab? :metrics_dashboards
= nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
= link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
= nav_link(controller: :metrics_dashboard, action: [:show]) do
= link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
%span
= _('Metrics')
......
= icon('info-circle fw')
= sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
= succeed '.' do
To learn more about this project, read
= link_to "the wiki", wiki_path(viewer.project.wiki)
......@@ -93,7 +93,7 @@
- if @merge_request
.well-segment
= icon('info-circle fw')
= sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
- link_to_merge_request = link_to(@merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id))
= _('This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request.').html_safe % { link_to_merge_request: link_to_merge_request }
---
title: Replace fa-info-circle icons with GitLab SVG information icon
merge_request: 38505
author:
type: changed
---
title: Increase sidebar performance by not rendering k8s highlight when not needed
merge_request: 39228
author:
type: performance
---
title: Use active version of Redis for an example
merge_request: 39404
author: Takuya Noguchi
type: other
---
title: Redirect to new metrics dashboard page
merge_request: 38364
author:
type: added
......@@ -979,6 +979,7 @@ They reflect configuration defined for this instance of Praefect.
> - Introduced in GitLab 13.1 in [alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga), disabled by default.
> - Entered [beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga) in GitLab 13.2, disabled by default.
> - From GitLab 13.3, disabled unless primary-wins reference transactions strategy is disabled.
Praefect guarantees eventual consistency by replicating all writes to secondary nodes
after the write to the primary Gitaly node has happened.
......@@ -991,15 +992,21 @@ information, see the [strong consistency epic](https://gitlab.com/groups/gitlab-
To enable strong consistency:
- In GitLab 13.2 and later, enable the `:gitaly_reference_transactions` feature flag.
- In GitLab 13.3 and later, reference transactions are enabled by default with
a primary-wins strategy. This strategy causes all transactions to succeed for
the primary and thus does not ensure strong consistency. To enable strong
consistency, disable the `:gitaly_reference_transactions_primary_wins`
feature flag.
- In GitLab 13.2, enable the `:gitaly_reference_transactions` feature flag.
- In GitLab 13.1, enable the `:gitaly_reference_transactions` and `:gitaly_hooks_rpc`
feature flags.
Enabling feature flags requires [access to the Rails console](../feature_flags.md#start-the-gitlab-rails-console).
Changing feature flags requires [access to the Rails console](../feature_flags.md#start-the-gitlab-rails-console).
In the Rails console, enable or disable the flags as required. For example:
```ruby
Feature.enable(:gitaly_reference_transactions)
Feature.disable(:gitaly_reference_transactions_primary_wins)
```
To monitor strong consistency, use the `gitaly_praefect_transactions_total` and
......@@ -1181,7 +1188,7 @@ automatically.
Repositories may be moved from one storage location using the [Project repository storage moves API](../../api/project_repository_storage_moves.md):
```shell
curl --request POST --header "PRIVATE_TOKEN: <your_access_token>" --header "Content-Type: application/json" \
curl --request POST --header "Private-Token: <your_access_token>" --header "Content-Type: application/json" \
--data '{"destination_storage_name":"praefect"}' "https://gitlab.example.com/api/v4/projects/123/repository_storage_moves"
```
......
......@@ -34,7 +34,7 @@ And that's it. Redis will now be available to be used within your testing
framework.
You can also use any other Docker image available on [Docker Hub](https://hub.docker.com/_/redis).
For example, to use Redis 2.8 the service becomes `redis:2.8`.
For example, to use Redis 6.0 the service becomes `redis:6.0`.
## Use Redis with the Shell executor
......
# GraphQL development guidelines
This guide contains all the information to successfully contribute to GitLab's
GraphQL API. This is a living document, and we welcome contributions,
feedback, and suggestions.
## Resources
- [GraphQL API development style guide](../api_graphql_styleguide.md): development style guide for
GraphQL.
- [GraphQL API documentation style guide](../documentation/styleguide.md#graphql-api): documentation
style guide for GraphQL.
- [GraphQL API](../../api/graphql/index.md): user documentation for the GitLab GraphQL API.
......@@ -236,3 +236,35 @@ Where:
- `<package_id>` is the package ID.
- `<package_version>` (Optional) is the package version.
## Publishing a NuGet package with CI/CD
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36424) in GitLab 13.3.
If you’re using NuGet with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token inherits the permissions of the user that generates the pipeline.
This example shows how to create a new package each time the `master` branch
is updated:
1. Add a `deploy` job to your `.gitlab-ci.yml` file:
```yaml
image: mcr.microsoft.com/dotnet/core/sdk:3.1
stages:
- deploy
deploy:
stage: deploy
script:
- dotnet restore -p:Configuration=Release
- dotnet build -c Release
- dotnet pack -c Release
- dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username gitlab-ci-token --password $CI_JOB_TOKEN --store-password-in-clear-text
- dotnet nuget push "bin/Release/*.nupkg" --source gitlab
only:
- master
```
1. Commit the changes and push it to your GitLab repository to trigger a new CI build.
......@@ -26,13 +26,12 @@ For information on how to create and upload a package, view the GitLab documenta
## Use GitLab CI/CD to build packages
You can use [GitLab CI/CD](../../../ci/README.md) to build packages.
For Maven and NPM packages, and Composer dependencies, you can
For Maven, NuGet and NPM packages, and Composer dependencies, you can
authenticate with GitLab by using the `CI_JOB_TOKEN`.
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd)
and [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd) and [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd).
If you use CI/CD to build a package, extended activity
information is displayed when you view the package details:
......
......@@ -3,7 +3,7 @@
module Gitlab
module Analytics
class UniqueVisits
TARGET_IDS = Set[
ANALYTICS_IDS = Set[
'g_analytics_contribution',
'g_analytics_insights',
'g_analytics_issues',
......@@ -17,6 +17,13 @@ module Gitlab
'p_analytics_repo',
'i_analytics_cohorts',
'i_analytics_dev_ops_score'
]
COMPLIANCE_IDS = Set[
'g_compliance_dashboard',
'g_compliance_audit_events',
'i_compliance_credential_inventory',
'i_compliance_audit_events'
].freeze
KEY_EXPIRY_LENGTH = 12.weeks
......@@ -34,8 +41,10 @@ module Gitlab
# @param [Integer] weeks time frame length in weeks
# @return [Integer] number of unique visitors
def unique_visits_for(targets:, start_week: 7.days.ago, weeks: 1)
target_ids = if targets == :any
TARGET_IDS
target_ids = if targets == :analytics
ANALYTICS_IDS
elsif targets == :compliance
COMPLIANCE_IDS
else
Array(targets)
end
......@@ -50,9 +59,12 @@ module Gitlab
private
def key(target_id, time)
raise "Invalid target id #{target_id}" unless TARGET_IDS.include?(target_id.to_s)
target_ids = ANALYTICS_IDS + COMPLIANCE_IDS
raise "Invalid target id #{target_id}" unless target_ids.include?(target_id.to_s)
target_key = target_id.to_s.gsub('analytics', '{analytics}').gsub('compliance', '{compliance}')
target_key = target_id.to_s.gsub('analytics', '{analytics}')
year_week = time.strftime('%G-%V')
"#{target_key}-#{year_week}"
......
......@@ -37,6 +37,7 @@ module Gitlab
.merge(usage_activity_by_stage)
.merge(usage_activity_by_stage(:usage_activity_by_stage_monthly, last_28_days_time_period))
.merge(analytics_unique_visits_data)
.merge(compliance_unique_visits_data)
end
end
......@@ -581,15 +582,25 @@ module Gitlab
end
def analytics_unique_visits_data
results = ::Gitlab::Analytics::UniqueVisits::TARGET_IDS.each_with_object({}) do |target_id, hash|
results = ::Gitlab::Analytics::UniqueVisits::ANALYTICS_IDS.each_with_object({}) do |target_id, hash|
hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) }
end
results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :any) }
results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :any, weeks: 4) }
results['analytics_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) }
results['analytics_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics, weeks: 4) }
{ analytics_unique_visits: results }
end
def compliance_unique_visits_data
results = ::Gitlab::Analytics::UniqueVisits::COMPLIANCE_IDS.each_with_object({}) do |target_id, hash|
hash[target_id] = redis_usage_data { unique_visit_service.unique_visits_for(targets: target_id) }
end
results['compliance_unique_visits_for_any_target'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance) }
results['compliance_unique_visits_for_any_target_monthly'] = redis_usage_data { unique_visit_service.unique_visits_for(targets: :compliance, weeks: 4) }
{ compliance_unique_visits: results }
end
def action_monthly_active_users(time_period)
return {} unless Feature.enabled?(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG)
......
......@@ -348,34 +348,10 @@ RSpec.describe Projects::EnvironmentsController do
end
describe 'GET #metrics_redirect' do
it 'redirects to environment if it exists' do
it 'redirects to metrics dashboard page' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to redirect_to(environment_metrics_path(environment))
end
context 'with anonymous user and public dashboard visibility' do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
it 'redirects successfully' do
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to redirect_to(environment_metrics_path(environment))
end
end
context 'when there are no environments' do
let(:environment) { }
it 'redirects to empty metrics page' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_ok
expect(response).to render_template 'empty_metrics'
end
expect(response).to redirect_to(project_metrics_dashboard_path(project))
end
end
......@@ -385,12 +361,12 @@ RSpec.describe Projects::EnvironmentsController do
end
context 'when environment has no metrics' do
it 'returns a metrics page' do
it 'redirects to metrics dashboard page' do
expect(environment).not_to receive(:metrics)
get :metrics, params: environment_params
expect(response).to be_ok
expect(response).to redirect_to(project_metrics_dashboard_path(project, environment: environment))
end
context 'when requesting metrics as JSON' do
......@@ -440,12 +416,12 @@ RSpec.describe Projects::EnvironmentsController do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
it 'returns success' do
it 'redirects to metrics dashboard page' do
project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED)
get :metrics, params: environment_params
expect(response).to have_gitlab_http_status(:ok)
expect(response).to redirect_to(project_metrics_dashboard_path(project, environment: environment))
end
end
end
......
......@@ -28,9 +28,8 @@ RSpec.describe 'Environment > Metrics' do
shared_examples 'has environment selector' do
it 'has a working environment selector', :js do
click_link('See metrics')
# TODO: See metrics on the sidebar still points to the old metrics URL
# https://gitlab.com/gitlab-org/gitlab/-/issues/229277
expect(page).to have_current_path(metrics_project_environment_path(project, id: environment.id))
expect(page).to have_current_path(project_metrics_dashboard_path(project, environment: environment.id))
expect(page).to have_css('[data-qa-selector="environments_dropdown"]')
within('[data-qa-selector="environments_dropdown"]') do
......
......@@ -25,7 +25,21 @@ RSpec.describe UserCalloutsHelper do
allow(helper).to receive(:user_dismissed?).and_return(false)
end
it { is_expected.to be true }
context 'when active_nav_link is in the operations section' do
before do
allow(helper).to receive(:active_nav_link?).and_return(true)
end
it { is_expected.to be true }
end
context 'when active_nav_link is not in the operations section' do
before do
allow(helper).to receive(:active_nav_link?).and_return(false)
end
it { is_expected.to be false }
end
end
context 'when user dismissed' do
......
......@@ -7,8 +7,11 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state
let(:target1_id) { 'g_analytics_contribution' }
let(:target2_id) { 'g_analytics_insights' }
let(:target3_id) { 'g_analytics_issues' }
let(:target4_id) { 'g_compliance_dashboard' }
let(:target5_id) { 'i_compliance_credential_inventory' }
let(:visitor1_id) { 'dfb9d2d2-f56c-4c77-8aeb-6cddc4a1f857' }
let(:visitor2_id) { '1dd9afb2-a3ee-4de1-8ae3-a405579c8584' }
let(:visitor3_id) { '34rfjuuy-ce56-sa35-ds34-dfer567dfrf2' }
around do |example|
# We need to freeze to a reference time
......@@ -29,18 +32,32 @@ RSpec.describe Gitlab::Analytics::UniqueVisits, :clean_gitlab_redis_shared_state
unique_visits.track_visit(visitor1_id, target2_id, 8.days.ago)
unique_visits.track_visit(visitor1_id, target2_id, 15.days.ago)
unique_visits.track_visit(visitor3_id, target4_id, 7.days.ago)
unique_visits.track_visit(visitor3_id, target5_id, 15.days.ago)
unique_visits.track_visit(visitor2_id, target5_id, 15.days.ago)
expect(unique_visits.unique_visits_for(targets: target1_id)).to eq(2)
expect(unique_visits.unique_visits_for(targets: target2_id)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target4_id)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target2_id, start_week: 15.days.ago)).to eq(1)
expect(unique_visits.unique_visits_for(targets: target3_id)).to eq(0)
expect(unique_visits.unique_visits_for(targets: :any)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :any, start_week: 15.days.ago)).to eq(1)
expect(unique_visits.unique_visits_for(targets: :any, start_week: 30.days.ago)).to eq(0)
expect(unique_visits.unique_visits_for(targets: target5_id, start_week: 15.days.ago)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :analytics)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :analytics, start_week: 15.days.ago)).to eq(1)
expect(unique_visits.unique_visits_for(targets: :analytics, start_week: 30.days.ago)).to eq(0)
expect(unique_visits.unique_visits_for(targets: :analytics, weeks: 4)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :compliance)).to eq(1)
expect(unique_visits.unique_visits_for(targets: :compliance, start_week: 15.days.ago)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :compliance, start_week: 30.days.ago)).to eq(0)
expect(unique_visits.unique_visits_for(targets: :any, weeks: 4)).to eq(2)
expect(unique_visits.unique_visits_for(targets: :compliance, weeks: 4)).to eq(2)
end
it 'sets the keys in Redis to expire automatically after 12 weeks' do
......
......@@ -958,12 +958,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.analytics_unique_visits_data }
it 'returns the number of unique visits to pages with analytics features' do
::Gitlab::Analytics::UniqueVisits::TARGET_IDS.each do |target_id|
::Gitlab::Analytics::UniqueVisits::ANALYTICS_IDS.each do |target_id|
expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: target_id).and_return(123)
end
expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :any).and_return(543)
expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :any, weeks: 4).and_return(987)
expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics).and_return(543)
expect_any_instance_of(::Gitlab::Analytics::UniqueVisits).to receive(:unique_visits_for).with(targets: :analytics, weeks: 4).and_return(987)
expect(subject).to eq({
analytics_unique_visits: {
......@@ -987,6 +987,37 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
describe '.compliance_unique_visits_data' do
subject { described_class.compliance_unique_visits_data }
before do
described_class.clear_memoization(:unique_visit_service)
allow_next_instance_of(::Gitlab::Analytics::UniqueVisits) do |instance|
::Gitlab::Analytics::UniqueVisits::COMPLIANCE_IDS.each do |target_id|
allow(instance).to receive(:unique_visits_for).with(targets: target_id).and_return(123)
end
allow(instance).to receive(:unique_visits_for).with(targets: :compliance).and_return(543)
allow(instance).to receive(:unique_visits_for).with(targets: :compliance, weeks: 4).and_return(987)
end
end
it 'returns the number of unique visits to pages with compliance features' do
expect(subject).to eq({
compliance_unique_visits: {
'g_compliance_dashboard' => 123,
'g_compliance_audit_events' => 123,
'i_compliance_credential_inventory' => 123,
'i_compliance_audit_events' => 123,
'compliance_unique_visits_for_any_target' => 543,
'compliance_unique_visits_for_any_target_monthly' => 987
}
})
end
end
describe '.service_desk_counts' do
subject { described_class.send(:service_desk_counts) }
......
......@@ -23,6 +23,22 @@ RSpec.describe 'metrics dashboard page' do
send_request
expect(assigns(:environment).id).to eq(environment.id)
end
context 'with anonymous user and public dashboard visibility' do
let(:anonymous_user) { create(:user) }
let(:project) do
create(:project, :public, metrics_dashboard_access_level: 'enabled')
end
before do
login_as(anonymous_user)
end
it 'returns 200' do
send_request
expect(response).to have_gitlab_http_status(:ok)
end
end
end
describe 'GET /:namespace/:project/-/metrics?environment=:environment.id' do
......
......@@ -13,6 +13,17 @@ RSpec.describe Notes::UpdateService do
let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: private_project) }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
```suggestion
foo
```
```suggestion
bar
```
MARKDOWN
end
before do
project.add_maintainer(user)
......@@ -48,16 +59,6 @@ RSpec.describe Notes::UpdateService do
context 'suggestions' do
it 'refreshes note suggestions' do
markdown = <<-MARKDOWN.strip_heredoc
```suggestion
foo
```
```suggestion
bar
```
MARKDOWN
suggestion = create(:suggestion)
note = suggestion.note
......@@ -201,5 +202,24 @@ RSpec.describe Notes::UpdateService do
end
end
end
context 'for a personal snippet' do
let_it_be(:snippet) { create(:personal_snippet, :public) }
let(:note) { create(:note, project: nil, noteable: snippet, author: user, note: "Note on a snippet with reference #{issue.to_reference}" ) }
it 'does not create todos' do
expect { update_note({ note: "Mentioning user #{user2}" }) }.not_to change { note.todos.count }
end
it 'does not create suggestions' do
expect { update_note({ note: "Updated snippet with markdown suggestion #{markdown}" }) }
.not_to change { note.suggestions.count }
end
it 'does not create mentions' do
expect(note).not_to receive(:create_new_cross_references!)
update_note({ note: "Updated with new reference: #{issue.to_reference}" })
end
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册