Commit ae37e917 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents d656f19c 4b10592b
......@@ -18,9 +18,7 @@ $.fn.renderGFM = function renderGFM() {
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.gfm-project_member').get());
initMRPopovers(this.find('.gfm-merge_request').get());
if (gon.features && gon.features.gfmEmbeddedMetrics) {
renderMetrics(this.find('.js-render-metrics').get());
}
renderMetrics(this.find('.js-render-metrics').get());
return this;
};
......
<script>
import { __ } from '~/locale';
import { GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { GlLink, GlButton } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
......@@ -15,6 +16,7 @@ let debouncedResize;
export default {
components: {
GlAreaChart,
GlButton,
GlChartSeriesLabel,
GlLink,
Icon,
......@@ -67,6 +69,7 @@ export default {
};
},
computed: {
...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
chartData() {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
......@@ -176,6 +179,18 @@ export default {
yAxisLabel() {
return `${this.graphData.y_label}`;
},
csvText() {
const chartData = this.chartData[0].data;
const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
return chartData.reduce((csv, data) => {
const row = data.join(',');
return `${csv}${row}\r\n`;
}, header);
},
downloadLink() {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
},
watch: {
containerWidth: 'onResize',
......@@ -240,10 +255,20 @@ export default {
</script>
<template>
<div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
<div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<gl-button
v-if="exportMetricsToCsvEnabled"
:href="downloadLink"
:title="__('Download CSV')"
:aria-label="__('Download CSV')"
style="margin-left: 200px;"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-button>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
......
......@@ -13,6 +13,7 @@ export default (props = {}) => {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled,
});
}
......
......@@ -37,11 +37,17 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = (
{ commit },
{ prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled },
{
prometheusEndpointEnabled,
multipleDashboardsEnabled,
additionalPanelTypesEnabled,
exportMetricsToCsvEnabled,
},
) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
......
......@@ -17,3 +17,4 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED';
......@@ -99,4 +99,7 @@ export default {
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
[types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) {
state.exportMetricsToCsvEnabled = enabled;
},
};
......@@ -10,6 +10,7 @@ export default () => ({
useDashboardEndpoint: false,
multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false,
exportMetricsToCsvEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
......
......@@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:export_metrics_to_csv_enabled)
end
def index
......@@ -160,7 +161,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics_dashboard
if Feature.enabled?(:gfm_embedded_metrics, project) && params[:embedded]
if params[:embedded]
result = dashboard_finder.find(
project,
current_user,
......
---
title: Prevent turning plain links into embedded when moving issues
merge_request: 31489
author:
type: fixed
---
title: Export and download CSV from metrics charts
merge_request: 30760
author:
type: added
---
title: Link and embed metrics in GitLab Flavored Markdown
merge_request: 31106
author:
type: added
......@@ -10,8 +10,6 @@ module Banzai
# the link, and insert this node after any html content
# surrounding the link.
def call
return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
doc.xpath(xpath_search).each do |node|
next unless element = element_to_embed(node)
......
......@@ -13,8 +13,6 @@ module Banzai
# uses to identify the embedded content, removing
# only unnecessary nodes.
def call
return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
nodes.each do |node|
path = paths_by_node[node]
user_has_access = user_access_by_path[path]
......
......@@ -27,7 +27,15 @@ module Gitlab
klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
moved = klass.copy_to(file, target_parent)
moved.markdown_link
moved_markdown = moved.markdown_link
# Prevents rewrite of plain links as embedded
if was_embedded?(markdown)
moved_markdown
else
moved_markdown.sub(/\A!/, "")
end
end
end
......@@ -43,6 +51,10 @@ module Gitlab
referenced_files.compact.select(&:exists?)
end
def was_embedded?(markdown)
markdown.starts_with?("!")
end
private
def find_file(project, secret, file)
......
......@@ -38,11 +38,6 @@ module Gitlab
gon.current_user_fullname = current_user.name
gon.current_user_avatar_url = current_user.avatar_url
end
# Flag controls a GFM feature used across many routes.
# Pushing the flag from one place simplifies control
# and facilitates easy removal.
push_frontend_feature_flag(:gfm_embedded_metrics)
end
# Exposes the state of a feature flag to the frontend code.
......
......@@ -5048,6 +5048,9 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Download CSV"
msgstr ""
msgid "Download artifacts"
msgstr ""
......
......@@ -613,31 +613,13 @@ describe Projects::EnvironmentsController do
end
end
shared_examples_for 'dashboard cannot be embedded' do
context 'when the embedded flag is included' do
let(:dashboard_params) { { format: :json, embedded: true } }
it_behaves_like 'the default dashboard'
end
end
let(:dashboard_params) { { format: :json } }
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard can be embedded'
context 'when multiple dashboards is enabled and embedding metrics is disabled' do
before do
stub_feature_flags(gfm_embedded_metrics: false)
end
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard cannot be embedded'
end
context 'when multiple dashboards is disabled and embedding metrics is enabled' do
context 'when multiple dashboards is disabled' do
before do
stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
end
......@@ -646,19 +628,6 @@ describe Projects::EnvironmentsController do
it_behaves_like 'dashboard cannot be specified'
it_behaves_like 'dashboard can be embedded'
end
context 'when multiple dashboards and embedding metrics are disabled' do
before do
stub_feature_flags(
environment_metrics_show_multiple_dashboards: false,
gfm_embedded_metrics: false
)
end
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard cannot be specified'
it_behaves_like 'dashboard cannot be embedded'
end
end
describe 'GET #search' do
......
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
import Area from '~/monitoring/components/charts/area.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import { TEST_HOST } from 'spec/test_constants';
import MonitoringMock, { deploymentData } from '../mock_data';
......@@ -17,13 +17,14 @@ describe('Area component', () => {
let mockGraphData;
let areaChart;
let spriteSpy;
let store;
beforeEach(() => {
const store = createStore();
store = createStore();
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
[mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
areaChart = shallowMount(Area, {
......@@ -36,6 +37,7 @@ describe('Area component', () => {
slots: {
default: mockWidgets,
},
store,
});
spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake(
......@@ -107,6 +109,16 @@ describe('Area component', () => {
});
});
describe('when exportMetricsToCsvEnabled is disabled', () => {
beforeEach(() => {
store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
});
it('does not render the Download CSV button', () => {
expect(areaChart.contains('glbutton-stub')).toBe(false);
});
});
describe('methods', () => {
describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at;
......@@ -252,5 +264,23 @@ describe('Area component', () => {
expect(areaChart.vm.yAxisLabel).toBe('CPU');
});
});
describe('csvText', () => {
it('converts data from json to csv', () => {
const header = `timestamp,${mockGraphData.y_label}`;
const data = mockGraphData.queries[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`;
expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
});
});
describe('downloadLink', () => {
it('produces a link to download metrics as csv', () => {
const link = areaChart.vm.downloadLink;
expect(link).toContain('blob:');
});
});
});
});
......@@ -40,16 +40,6 @@ describe Banzai::Filter::InlineMetricsFilter do
expect(doc.at_css('p').to_s).to include paragraph
expect(doc.at_css('.js-render-metrics')).to be_present
end
context 'when the feature is disabled' do
before do
stub_feature_flags(gfm_embedded_metrics: false)
end
it 'does nothing' do
expect(doc.to_s).to eq input
end
end
end
end
end
......@@ -11,16 +11,6 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
let(:input) { %(<a href="#{url}">example</a>) }
let(:doc) { filter(input) }
context 'when the feature is disabled' do
before do
stub_feature_flags(gfm_embedded_metrics: false)
end
it 'does nothing' do
expect(doc.to_s).to eq input
end
end
context 'without a metrics charts placeholder' do
it 'leaves regular non-metrics links unchanged' do
expect(doc.to_s).to eq input
......
......@@ -55,6 +55,17 @@ describe Gitlab::Gfm::UploadsRewriter do
end
end
it 'does not rewrite plain links as embedded' do
embedded_link = image_uploader.markdown_link
plain_image_link = embedded_link.sub(/\A!/, "")
text = "#{plain_image_link} and #{embedded_link}"
moved_text = described_class.new(text, old_project, user).rewrite(new_project)
expect(moved_text.scan(/!\[.*?\]/).count).to eq(1)
expect(moved_text.scan(/\A\[.*?\]/).count).to eq(1)
end
context "file are stored locally" do
include_examples "files are accessible"
end
......
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