Commit 96530200 authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-07-18

# Conflicts:
#	locale/gitlab.pot

[ci skip]
parents 3090c6b1 1df32177
...@@ -162,8 +162,10 @@ export default class Clusters { ...@@ -162,8 +162,10 @@ export default class Clusters {
if (type === 'password') { if (type === 'password') {
this.tokenField.setAttribute('type', 'text'); this.tokenField.setAttribute('type', 'text');
this.showTokenButton.textContent = s__('ClusterIntegration|Hide');
} else { } else {
this.tokenField.setAttribute('type', 'password'); this.tokenField.setAttribute('type', 'password');
this.showTokenButton.textContent = s__('ClusterIntegration|Show');
} }
} }
......
...@@ -190,7 +190,6 @@ export default { ...@@ -190,7 +190,6 @@ export default {
</button> </button>
<a <a
v-if="lineNumber" v-if="lineNumber"
v-once
:data-linenumber="lineNumber" :data-linenumber="lineNumber"
:href="lineHref" :href="lineHref"
> >
......
...@@ -101,7 +101,6 @@ export default { ...@@ -101,7 +101,6 @@ export default {
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
v-once
:class="line.type" :class="line.type"
class="line_content" class="line_content"
v-html="line.richText" v-html="line.richText"
......
...@@ -119,7 +119,6 @@ export default { ...@@ -119,7 +119,6 @@ export default {
class="diff-line-num old_line" class="diff-line-num old_line"
/> />
<td <td
v-once
:id="line.left.lineCode" :id="line.left.lineCode"
:class="parallelViewLeftLineType" :class="parallelViewLeftLineType"
class="line_content parallel left-side" class="line_content parallel left-side"
...@@ -140,7 +139,6 @@ export default { ...@@ -140,7 +139,6 @@ export default {
class="diff-line-num new_line" class="diff-line-num new_line"
/> />
<td <td
v-once
:id="line.right.lineCode" :id="line.right.lineCode"
:class="line.right.type" :class="line.right.type"
class="line_content parallel right-side" class="line_content parallel right-side"
......
...@@ -125,6 +125,7 @@ export default { ...@@ -125,6 +125,7 @@ export default {
:class="flagOrientation" :class="flagOrientation"
class="prometheus-graph-flag popover" class="prometheus-graph-flag popover"
> >
<div class="arrow-shadow"></div>
<div class="arrow"></div> <div class="arrow"></div>
<div class="popover-header"> <div class="popover-header">
<h5 v-if="deploymentFlagData"> <h5 v-if="deploymentFlagData">
......
...@@ -46,7 +46,6 @@ ...@@ -46,7 +46,6 @@
.card-body { .card-body {
padding: $gl-padding; padding: $gl-padding;
background-color: $white-light;
.form-actions { .form-actions {
margin: -$gl-padding; margin: -$gl-padding;
......
...@@ -455,6 +455,8 @@ ...@@ -455,6 +455,8 @@
.prometheus-graph-flag { .prometheus-graph-flag {
display: block; display: block;
min-width: 160px; min-width: 160px;
border: 0;
box-shadow: 0 1px 4px 0 $black-transparent;
h5 { h5 {
padding: 0; padding: 0;
...@@ -474,7 +476,6 @@ ...@@ -474,7 +476,6 @@
&.popover { &.popover {
padding: 0; padding: 0;
border: 1px solid $border-color;
&.left { &.left {
left: auto; left: auto;
...@@ -482,12 +483,19 @@ ...@@ -482,12 +483,19 @@
margin-right: 10px; margin-right: 10px;
> .arrow { > .arrow {
right: -16px; right: -14px;
border-left-color: $border-color; border-left-color: $border-color;
} }
> .arrow::after { > .arrow::after {
border-left-color: $theme-gray-50; border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 4px solid $theme-gray-50;
}
.arrow-shadow {
right: -3px;
box-shadow: 1px 0 9px 0 $black-transparent;
} }
} }
...@@ -497,19 +505,35 @@ ...@@ -497,19 +505,35 @@
margin-left: 10px; margin-left: 10px;
> .arrow { > .arrow {
left: -16px; left: -7px;
border-right-color: $border-color; border-right-color: $border-color;
} }
> .arrow::after { > .arrow::after {
border-right-color: $theme-gray-50; border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 4px solid $theme-gray-50;
}
.arrow-shadow {
left: -3px;
box-shadow: 1px 0 8px 0 $black-transparent;
} }
} }
> .arrow { > .arrow {
top: 16px; top: 10px;
margin-top: -8px; margin: 0;
border-width: 8px; }
.arrow-shadow {
content: "";
position: absolute;
width: 7px;
height: 7px;
background-color: transparent;
transform: rotate(45deg);
top: 13px;
} }
> .popover-header, > .popover-header,
...@@ -517,10 +541,12 @@ ...@@ -517,10 +541,12 @@
padding: 8px; padding: 8px;
font-size: 12px; font-size: 12px;
white-space: nowrap; white-space: nowrap;
position: relative;
} }
> .popover-header { > .popover-header {
background-color: $theme-gray-50; background-color: $theme-gray-50;
border-radius: $border-radius-default $border-radius-default 0 0;
} }
} }
......
...@@ -136,10 +136,6 @@ class FileUploader < GitlabUploader ...@@ -136,10 +136,6 @@ class FileUploader < GitlabUploader
} }
end end
def filename
self.file.filename
end
def upload=(value) def upload=(value)
super super
......
...@@ -160,8 +160,8 @@ ...@@ -160,8 +160,8 @@
%br %br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- else - else
.card.bg-warning .card.border-warning
.card-header .card-header.bg-warning.text-white
Block this user Block this user
.card-body .card-body
%p Blocking user has the following effects: %p Blocking user has the following effects:
...@@ -181,8 +181,8 @@ ...@@ -181,8 +181,8 @@
%br %br
= link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } = link_to 'Unlock user', unlock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
.card.bg-danger .card.border-danger
.card-header .card-header.bg-danger.text-white
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
.card-body .card-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
...@@ -207,8 +207,8 @@ ...@@ -207,8 +207,8 @@
%p %p
You don't have access to delete this user. You don't have access to delete this user.
.card.bg-danger .card.border-danger
.card-header .card-header.bg-danger.text-white
= s_('AdminUsers|Delete user and contributions') = s_('AdminUsers|Delete user and contributions')
.card-body .card-body
- if can?(current_user, :destroy_user, @user) - if can?(current_user, :destroy_user, @user)
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
%hr %hr
- if @project.import_failed? - if @project.import_failed?
.card.bg-danger .card.border-danger
.card-header The repository could not be imported. .card-header.bg-danger.text-white The repository could not be imported.
.card-body .card-body
%pre %pre
:preserve :preserve
......
- if @project.pages_deployed? - if @project.pages_deployed?
- if can?(current_user, :remove_pages, @project) - if can?(current_user, :remove_pages, @project)
.card.bg-danger .card.border-danger
.card-header Remove pages .card-header.bg-danger.text-white Remove pages
.errors-holder .errors-holder
.card-body .card-body
%p %p
......
---
title: Update design for system metrics popovers
merge_request: 20655
author:
type: fixed
---
title: Add support for tar.gz AUTO_DEVOPS_CHART charts (#49324)
merge_request: 20691
author: '@kondi1'
type: added
---
title: Fix rendering of the context lines in MR diffs page
merge_request: 20642
author:
type: fixed
---
title: Fix serialization of LegacyDiffNote
merge_request:
author:
type: fixed
---
title: Fix filename for accelerated uploads
merge_request:
author:
type: fixed
---
title: Toggle Show / Hide Button for Kubernetes Password
merge_request: 20659
author: gfyoung
type: fixed
---
title: Remove background color from card-body style
merge_request: 20689
author: George Tsiolis
type: fixed
...@@ -16,16 +16,8 @@ The default deployment includes: ...@@ -16,16 +16,8 @@ The default deployment includes:
### Limitations ### Limitations
Some features and functions are not currently available in the beta release: Some features and functions are not currently available in the beta release.
* [GitLab Pages](../../user/project/pages/) For details, see [known issues and limitations](https://gitlab.com/charts/gitlab/blob/master/doc/architecture/beta.md#known-issues-and-limitations) in the charts repository.
* [Reply by email](../../administration/reply_by_email.html)
* [Project templates](../../gitlab-basics/create-project.html)
* [Project import/export](../../user/project/settings/import_export.html)
* [Geo](https://docs.gitlab.com/ee/administration/geo/replication/)
Currently out of scope:
* [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/)
* [MySQL support](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only)
## Prerequisites ## Prerequisites
......
...@@ -729,7 +729,7 @@ module API ...@@ -729,7 +729,7 @@ module API
expose :system?, as: :system expose :system?, as: :system
expose :noteable_id, :noteable_type expose :noteable_id, :noteable_type
expose :position, if: ->(note, options) { note.diff_note? } do |note| expose :position, if: ->(note, options) { note.is_a?(DiffNote) } do |note|
note.position.to_h note.position.to_h
end end
......
...@@ -21,7 +21,7 @@ class UploadedFile ...@@ -21,7 +21,7 @@ class UploadedFile
raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path) raise InvalidPathError, "#{path} file does not exist" unless ::File.exist?(path)
@content_type = content_type @content_type = content_type
@original_filename = filename || ::File.basename(path) @original_filename = sanitize_filename(filename || path)
@content_type = content_type @content_type = content_type
@sha256 = sha256 @sha256 = sha256
@remote_id = remote_id @remote_id = remote_id
...@@ -55,6 +55,16 @@ class UploadedFile ...@@ -55,6 +55,16 @@ class UploadedFile
end end
end end
# copy-pasted from CarrierWave::SanitizedFile
def sanitize_filename(name)
name = name.tr("\\", "/") # work-around for IE
name = ::File.basename(name)
name = name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, "_")
name = "_#{name}" if name =~ /\A\.+\z/
name = "unnamed" if name.empty?
name.mb_chars.to_s
end
def path def path
@tempfile.path @tempfile.path
end end
......
...@@ -1531,10 +1531,14 @@ msgstr "" ...@@ -1531,10 +1531,14 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller" msgid "ClusterIntegration|Helm Tiller"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}." msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}."
msgstr "" msgstr ""
msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data." msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
=======
msgid "ClusterIntegration|Hide"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "ClusterIntegration|Ingress" msgid "ClusterIntegration|Ingress"
......
...@@ -59,7 +59,7 @@ module QA ...@@ -59,7 +59,7 @@ module QA
end end
def add_file(name, contents) def add_file(name, contents)
File.write(name, contents) ::File.write(name, contents)
`git add #{name}` `git add #{name}`
end end
......
...@@ -15,7 +15,7 @@ module QA ...@@ -15,7 +15,7 @@ module QA
# instantiated on one page because there is no distinguishing # instantiated on one page because there is no distinguishing
# attribute per dropzone file field. # attribute per dropzone file field.
def attach_file(attachment) def attach_file(attachment)
filename = File.basename(attachment) filename = ::File.basename(attachment)
field_style = { visibility: 'visible', height: '', width: '' } field_style = { visibility: 'visible', height: '', width: '' }
page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style) page.attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
......
...@@ -28,7 +28,7 @@ module QA ...@@ -28,7 +28,7 @@ module QA
# #
# Returns the relative path to the requested API resource # Returns the relative path to the requested API resource
def request_path(path, version: API_VERSION, **query_string) def request_path(path, version: API_VERSION, **query_string)
full_path = File.join('/api', version, path) full_path = ::File.join('/api', version, path)
if query_string.any? if query_string.any?
full_path << (path.include?('?') ? '&' : '?') full_path << (path.include?('?') ? '&' : '?')
......
...@@ -86,7 +86,7 @@ module QA ...@@ -86,7 +86,7 @@ module QA
end end
Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example| Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example|
File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', '')) ::File.join(QA::Runtime::Namespace.name, example.file_path.sub('./qa/specs/features/', ''))
end end
Capybara.configure do |config| Capybara.configure do |config|
...@@ -94,7 +94,7 @@ module QA ...@@ -94,7 +94,7 @@ module QA
config.javascript_driver = :chrome config.javascript_driver = :chrome
config.default_max_wait_time = 10 config.default_max_wait_time = 10
# https://github.com/mattheworiordan/capybara-screenshot/issues/164 # https://github.com/mattheworiordan/capybara-screenshot/issues/164
config.save_path = File.expand_path('../../tmp', __dir__) config.save_path = ::File.expand_path('../../tmp', __dir__)
end end
end end
......
...@@ -25,8 +25,8 @@ module QA ...@@ -25,8 +25,8 @@ module QA
end end
def populate_key_data(path) def populate_key_data(path)
@private_key = File.binread(path) @private_key = ::File.binread(path)
@public_key = File.binread("#{path}.pub") @public_key = ::File.binread("#{path}.pub")
@fingerprint = @fingerprint =
`ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp
end end
......
...@@ -13,7 +13,7 @@ module QA ...@@ -13,7 +13,7 @@ module QA
end end
def version def version
@version ||= File.directory?("#{__dir__}/../ee") ? :EE : :CE @version ||= ::File.directory?("#{__dir__}/../ee") ? :EE : :CE
end end
def strategy def strategy
......
...@@ -26,7 +26,7 @@ module QA ...@@ -26,7 +26,7 @@ module QA
if rspec_options.any? if rspec_options.any?
rspec_options rspec_options
else else
File.expand_path('../../specs/features', __dir__) ::File.expand_path('../../specs/features', __dir__)
end end
end end
end end
......
...@@ -29,7 +29,7 @@ describe QA::Git::Repository do ...@@ -29,7 +29,7 @@ describe QA::Git::Repository do
def cd_empty_temp_directory def cd_empty_temp_directory
tmp_dir = 'tmp/git-repository-spec/' tmp_dir = 'tmp/git-repository-spec/'
FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir) FileUtils.rm_r(tmp_dir) if ::File.exist?(tmp_dir)
FileUtils.mkdir_p tmp_dir FileUtils.mkdir_p tmp_dir
FileUtils.cd tmp_dir FileUtils.cd tmp_dir
end end
......
...@@ -32,7 +32,7 @@ describe QA::Page::View do ...@@ -32,7 +32,7 @@ describe QA::Page::View do
context 'when pattern is found' do context 'when pattern is found' do
before do before do
allow(File).to receive(:foreach) allow(::File).to receive(:foreach)
.and_yield('some element').once .and_yield('some element').once
allow(element).to receive(:matches?) allow(element).to receive(:matches?)
.with('some element').and_return(true) .with('some element').and_return(true)
...@@ -45,7 +45,7 @@ describe QA::Page::View do ...@@ -45,7 +45,7 @@ describe QA::Page::View do
context 'when pattern has not been found' do context 'when pattern has not been found' do
before do before do
allow(File).to receive(:foreach) allow(::File).to receive(:foreach)
.and_yield('some element').once .and_yield('some element').once
allow(element).to receive(:matches?) allow(element).to receive(:matches?)
.with('some element').and_return(false) .with('some element').and_return(false)
......
...@@ -30,7 +30,7 @@ describe QA::Scenario::Test::Instance do ...@@ -30,7 +30,7 @@ describe QA::Scenario::Test::Instance do
subject.perform("test") subject.perform("test")
expect(runner).to have_received(:options=) expect(runner).to have_received(:options=)
.with(File.expand_path('../../../qa/specs/features', __dir__)) .with(::File.expand_path('../../../qa/specs/features', __dir__))
end end
end end
......
require_relative '../qa' require_relative '../qa'
Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
RSpec.configure do |config| RSpec.configure do |config|
config.expect_with :rspec do |expectations| config.expect_with :rspec do |expectations|
......
...@@ -39,6 +39,7 @@ FactoryBot.define do ...@@ -39,6 +39,7 @@ FactoryBot.define do
factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do factory :legacy_diff_note_on_merge_request, traits: [:on_merge_request, :legacy_diff_note], class: LegacyDiffNote do
association :project, :repository association :project, :repository
position ''
end end
factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do factory :diff_note_on_merge_request, traits: [:on_merge_request], class: DiffNote do
......
...@@ -45,17 +45,33 @@ describe('Clusters', () => { ...@@ -45,17 +45,33 @@ describe('Clusters', () => {
}); });
describe('showToken', () => { describe('showToken', () => {
it('should update tye field type', () => { it('should update token field type', () => {
cluster.showTokenButton.click(); cluster.showTokenButton.click();
expect( expect(
cluster.tokenField.getAttribute('type'), cluster.tokenField.getAttribute('type'),
).toEqual('text'); ).toEqual('text');
cluster.showTokenButton.click(); cluster.showTokenButton.click();
expect( expect(
cluster.tokenField.getAttribute('type'), cluster.tokenField.getAttribute('type'),
).toEqual('password'); ).toEqual('password');
}); });
it('should update show token button text', () => {
cluster.showTokenButton.click();
expect(
cluster.showTokenButton.textContent,
).toEqual('Hide');
cluster.showTokenButton.click();
expect(
cluster.showTokenButton.textContent,
).toEqual('Show');
});
}); });
describe('checkForNewInstalls', () => { describe('checkForNewInstalls', () => {
......
require 'spec_helper' require 'spec_helper'
describe UploadedFile do describe UploadedFile do
describe ".from_params" do let(:temp_dir) { Dir.tmpdir }
let(:temp_dir) { Dir.tmpdir } let(:temp_file) { Tempfile.new("test", temp_dir) }
let(:temp_file) { Tempfile.new("test", temp_dir) }
let(:upload_path) { nil }
subject do before do
described_class.from_params(params, :file, upload_path) FileUtils.touch(temp_file)
end end
before do after do
FileUtils.touch(temp_file) FileUtils.rm_f(temp_file)
end end
describe ".from_params" do
let(:upload_path) { nil }
after do after do
FileUtils.rm_f(temp_file)
FileUtils.rm_r(upload_path) if upload_path FileUtils.rm_r(upload_path) if upload_path
end end
subject do
described_class.from_params(params, :file, upload_path)
end
context 'when valid file is specified' do context 'when valid file is specified' do
context 'only local path is specified' do context 'only local path is specified' do
let(:params) do let(:params) do
...@@ -37,7 +41,7 @@ describe UploadedFile do ...@@ -37,7 +41,7 @@ describe UploadedFile do
context 'all parameters are specified' do context 'all parameters are specified' do
let(:params) do let(:params) do
{ 'file.path' => temp_file.path, { 'file.path' => temp_file.path,
'file.name' => 'my_file.txt', 'file.name' => 'dir/my file&.txt',
'file.type' => 'my/type', 'file.type' => 'my/type',
'file.sha256' => 'sha256', 'file.sha256' => 'sha256',
'file.remote_id' => 'remote_id' } 'file.remote_id' => 'remote_id' }
...@@ -48,7 +52,7 @@ describe UploadedFile do ...@@ -48,7 +52,7 @@ describe UploadedFile do
end end
it "generates filename from path" do it "generates filename from path" do
expect(subject.original_filename).to eq('my_file.txt') expect(subject.original_filename).to eq('my_file_.txt')
expect(subject.content_type).to eq('my/type') expect(subject.content_type).to eq('my/type')
expect(subject.sha256).to eq('sha256') expect(subject.sha256).to eq('sha256')
expect(subject.remote_id).to eq('remote_id') expect(subject.remote_id).to eq('remote_id')
...@@ -113,4 +117,11 @@ describe UploadedFile do ...@@ -113,4 +117,11 @@ describe UploadedFile do
end end
end end
end end
describe '#sanitize_filename' do
it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') }
it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') }
it { expect(described_class.new(temp_file.path).sanitize_filename('..')).to eq('_..') }
it { expect(described_class.new(temp_file.path).sanitize_filename('')).to eq('unnamed') }
end
end end
...@@ -166,4 +166,50 @@ describe FileUploader do ...@@ -166,4 +166,50 @@ describe FileUploader do
uploader.upload = upload uploader.upload = upload
end end
end end
describe '#cache!' do
subject do
uploader.store!(uploaded_file)
end
context 'when remote file is used' do
let(:temp_file) { Tempfile.new("test") }
let!(:fog_connection) do
stub_uploads_object_storage(described_class)
end
let(:uploaded_file) do
UploadedFile.new(temp_file.path, filename: "my file.txt", remote_id: "test/123123")
end
let!(:fog_file) do
fog_connection.directories.get('uploads').files.create(
key: 'tmp/uploads/test/123123',
body: 'content'
)
end
before do
FileUtils.touch(temp_file)
end
after do
FileUtils.rm_f(temp_file)
end
it 'file is stored remotely in permament location with sanitized name' do
subject
expect(uploader).to be_exists
expect(uploader).not_to be_cached
expect(uploader).not_to be_file_storage
expect(uploader.path).not_to be_nil
expect(uploader.path).not_to include('tmp/upload')
expect(uploader.path).not_to include('tmp/cache')
expect(uploader.url).to include('/my_file.txt')
expect(uploader.object_store).to eq(described_class::Store::REMOTE)
end
end
end
end end
...@@ -677,6 +677,7 @@ rollout 100%: ...@@ -677,6 +677,7 @@ rollout 100%:
auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app} auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
auto_chart_name=$(basename $auto_chart) auto_chart_name=$(basename $auto_chart)
auto_chart_name=${auto_chart_name%.tgz} auto_chart_name=${auto_chart_name%.tgz}
auto_chart_name=${auto_chart_name%.tar.gz}
else else
auto_chart="chart" auto_chart="chart"
auto_chart_name="chart" auto_chart_name="chart"
......
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