Commit 6c220a80 authored by Simon Knox's avatar Simon Knox Committed by Mike Greiling

Convert Burndown charts to gitlab-ui component

We are using echarts for all charts
parent 2b607f09
<script>
import { GlButton, GlButtonGroup } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/charts';
import dateFormat from 'dateformat';
import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
import { s__, __, sprintf } from '~/locale';
export default {
components: {
GlButton,
GlButtonGroup,
GlLineChart,
ResizableChartContainer,
},
props: {
startDate: {
type: String,
required: true,
},
dueDate: {
type: String,
required: true,
},
openIssuesCount: {
type: Array,
required: false,
default: () => [],
},
openIssuesWeight: {
type: Array,
required: false,
default: () => [],
},
},
data() {
return {
issuesSelected: true,
tooltip: {
title: '',
content: '',
},
};
},
computed: {
dataSeries() {
let name;
let data;
if (this.issuesSelected) {
name = s__('BurndownChartLabel|Open issues');
data = this.openIssuesCount;
} else {
name = s__('BurndownChartLabel|Open issue weight');
data = this.openIssuesWeight;
}
const series = [
{
name,
data: data.map(d => [new Date(d[0]), d[1]]),
},
];
if (series[0] && series[0].data.length >= 2) {
const idealStart = [new Date(this.startDate), data[0][1]];
const idealEnd = [new Date(this.dueDate), 0];
const idealData = [idealStart, idealEnd];
series.push({
name: __('Guideline'),
data: idealData,
silent: true,
symbolSize: 0,
lineStyle: {
color: '#ddd',
type: 'dashed',
},
});
}
return series;
},
options() {
return {
xAxis: {
name: '',
type: 'time',
axisLine: {
show: true,
},
},
yAxis: {
name: this.issuesSelected ? __('Total issues') : __('Total weight'),
axisLine: {
show: true,
},
splitLine: {
show: false,
},
},
tooltip: {
trigger: 'item',
formatter: () => '',
},
};
},
},
methods: {
formatTooltipText(params) {
const [seriesData] = params.seriesData;
this.tooltip.title = dateFormat(params.value, 'dd mmm yyyy');
if (this.issuesSelected) {
this.tooltip.content = sprintf(__('%{total} open issues'), {
total: seriesData.value[1],
});
} else {
this.tooltip.content = sprintf(__('%{total} open issue weight'), {
total: seriesData.value[1],
});
}
},
showIssueCount() {
this.issuesSelected = true;
},
showIssueWeight() {
this.issuesSelected = false;
},
},
};
</script>
<template>
<div data-qa-selector="burndown_chart">
<div class="burndown-header d-flex align-items-center">
<h3>{{ __('Burndown chart') }}</h3>
<gl-button-group class="ml-3 js-burndown-data-selector">
<gl-button
ref="totalIssuesButton"
:variant="issuesSelected ? 'primary' : 'inverted-primary'"
size="sm"
@click="showIssueCount"
>
{{ __('Issues') }}
</gl-button>
<gl-button
ref="totalWeightButton"
:variant="issuesSelected ? 'inverted-primary' : 'primary'"
size="sm"
data-qa-selector="weight_button"
@click="showIssueWeight"
>
{{ __('Issue weight') }}
</gl-button>
</gl-button-group>
</div>
<resizable-chart-container class="burndown-chart">
<gl-line-chart
slot-scope="{ width }"
:width="width"
:data="dataSeries"
:option="options"
:format-tooltip-text="formatTooltipText"
>
<template slot="tooltipTitle">{{ tooltip.title }}</template>
<template slot="tooltipContent">{{ tooltip.content }}</template>
</gl-line-chart>
</resizable-chart-container>
</div>
</template>
import Vue from 'vue';
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import BurndownChart from './burndown_chart'; import BurndownChart from './components/burndown_chart.vue';
import BurndownChartData from './burndown_chart_data'; import BurndownChartData from './burndown_chart_data';
import Flash from '~/flash'; import Flash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale'; import { __ } from '~/locale';
export default () => { export default () => {
// handle hint dismissal // handle hint dismissal
...@@ -32,44 +33,22 @@ export default () => { ...@@ -32,44 +33,22 @@ export default () => {
const openIssuesCount = chartData.map(d => [d[0], d[1]]); const openIssuesCount = chartData.map(d => [d[0], d[1]]);
const openIssuesWeight = chartData.map(d => [d[0], d[2]]); const openIssuesWeight = chartData.map(d => [d[0], d[2]]);
const chart = new BurndownChart({ container, startDate, dueDate }); return new Vue({
el: container,
let currentView = 'count'; components: {
chart.setData(openIssuesCount, { BurndownChart,
label: s__('BurndownChartLabel|Open issues'), },
animate: true, render(createElement) {
}); return createElement('burndown-chart', {
props: {
$('.js-burndown-data-selector').on('click', 'button', function switchData() { startDate,
const $this = $(this); dueDate,
const show = $this.data('show'); openIssuesCount,
if (currentView !== show) { openIssuesWeight,
currentView = show; },
$this
.removeClass('btn-inverted')
.siblings()
.addClass('btn-inverted');
switch (show) {
case 'count':
chart.setData(openIssuesCount, {
label: s__('BurndownChartLabel|Open issues'),
animate: true,
}); });
break; },
case 'weight':
chart.setData(openIssuesWeight, {
label: s__('BurndownChartLabel|Open issue weight'),
animate: true,
}); });
break;
default:
break;
}
}
});
window.addEventListener('resize', () => chart.animateResize(1));
$(document).on('click', '.js-sidebar-toggle', () => chart.animateResize(2));
}) })
.catch(() => new Flash(__('Error loading burndown chart data'))); .catch(() => new Flash(__('Error loading burndown chart data')));
} }
......
...@@ -63,17 +63,8 @@ ...@@ -63,17 +63,8 @@
.burndown-chart { .burndown-chart {
width: 100%; width: 100%;
height: 380px;
margin: 5px 0; margin: 5px 0;
@include media-breakpoint-down(sm) {
height: 320px;
}
@include media-breakpoint-down(xs) {
height: 200px;
}
.axis { .axis {
font-size: 12px; font-size: 12px;
......
...@@ -6,14 +6,6 @@ ...@@ -6,14 +6,6 @@
= warning = warning
- if can_generate_chart?(milestone, burndown) - if can_generate_chart?(milestone, burndown)
.burndown-header
%h3
Burndown chart
.btn-group.js-burndown-data-selector
%button.btn.btn-sm.btn-primary{ data: { show: 'count' } }
Issues
%button.btn.btn-sm.btn-primary.btn-inverted{ data: { show: 'weight', qa_selector: 'weight_button' } }
Issue weight
.burndown-chart{ data: { start_date: burndown.start_date.strftime("%Y-%m-%d"), .burndown-chart{ data: { start_date: burndown.start_date.strftime("%Y-%m-%d"),
due_date: burndown.due_date.strftime("%Y-%m-%d"), due_date: burndown.due_date.strftime("%Y-%m-%d"),
burndown_events_path: expose_url(burndown_endpoint), qa_selector: 'burndown_chart' } } burndown_events_path: expose_url(burndown_endpoint), qa_selector: 'burndown_chart' } }
......
---
title: Style burndown charts with gitlab-ui
merge_request: 15463
author:
type: changed
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Burndown charts' do describe 'Burndown charts', :js do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
let(:milestone) do let(:milestone) do
create(:milestone, project: project, create(:milestone, project: project,
......
import { shallowMount } from '@vue/test-utils';
import BurndownChart from 'ee/burndown_chart/components/burndown_chart.vue';
describe('burndown_chart', () => {
let wrapper;
const issuesButton = () => wrapper.find({ ref: 'totalIssuesButton' });
const weightButton = () => wrapper.find({ ref: 'totalWeightButton' });
const defaultProps = {
startDate: '2019-08-07T00:00:00.000Z',
dueDate: '2019-09-09T00:00:00.000Z',
openIssuesCount: [],
openIssuesWeight: [],
};
const createComponent = (props = {}) => {
wrapper = shallowMount(BurndownChart, {
propsData: {
...defaultProps,
...props,
},
});
};
it('inclues Issues and Issue weight buttons', () => {
createComponent();
expect(issuesButton().text()).toBe('Issues');
expect(weightButton().text()).toBe('Issue weight');
});
it('defaults to total issues', () => {
createComponent();
expect(issuesButton().attributes('variant')).toBe('primary');
expect(weightButton().attributes('variant')).toBe('inverted-primary');
});
it('toggles Issue weight', () => {
createComponent();
weightButton().vm.$emit('click');
expect(issuesButton().attributes('variant')).toBe('inverted-primary');
expect(weightButton().attributes('variant')).toBe('primary');
});
describe('with single point', () => {
it('does not show guideline', () => {
createComponent({
openIssuesCount: [{ '2019-08-07T00:00:00.000Z': 100 }],
});
const data = wrapper.vm.dataSeries;
expect(data.length).toBe(1);
expect(data[0].name).not.toBe('Guideline');
});
});
describe('with multiple points', () => {
it('shows guideline', () => {
createComponent({
openIssuesCount: [
{ '2019-08-07T00:00:00.000Z': 100 },
{ '2019-08-08T00:00:00.000Z': 99 },
{ '2019-09-08T00:00:00.000Z': 1 },
],
});
const data = wrapper.vm.dataSeries;
expect(data.length).toBe(2);
expect(data[1].name).toBe('Guideline');
});
});
});
...@@ -375,6 +375,12 @@ msgstr "" ...@@ -375,6 +375,12 @@ msgstr ""
msgid "%{title} changes" msgid "%{title} changes"
msgstr "" msgstr ""
msgid "%{total} open issue weight"
msgstr ""
msgid "%{total} open issues"
msgstr ""
msgid "%{unstaged} unstaged and %{staged} staged changes" msgid "%{unstaged} unstaged and %{staged} staged changes"
msgstr "" msgstr ""
...@@ -2649,7 +2655,7 @@ msgstr "" ...@@ -2649,7 +2655,7 @@ msgstr ""
msgid "Built-in" msgid "Built-in"
msgstr "" msgstr ""
msgid "BurndownChartLabel|Guideline" msgid "Burndown chart"
msgstr "" msgstr ""
msgid "BurndownChartLabel|Open issue weight" msgid "BurndownChartLabel|Open issue weight"
...@@ -2658,12 +2664,6 @@ msgstr "" ...@@ -2658,12 +2664,6 @@ msgstr ""
msgid "BurndownChartLabel|Open issues" msgid "BurndownChartLabel|Open issues"
msgstr "" msgstr ""
msgid "BurndownChartLabel|Progress"
msgstr ""
msgid "BurndownChartLabel|Remaining"
msgstr ""
msgid "Business" msgid "Business"
msgstr "" msgstr ""
...@@ -8303,6 +8303,9 @@ msgstr "" ...@@ -8303,6 +8303,9 @@ msgstr ""
msgid "GroupsTree|Search by name" msgid "GroupsTree|Search by name"
msgstr "" msgstr ""
msgid "Guideline"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}" msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
msgstr "" msgstr ""
...@@ -8943,6 +8946,9 @@ msgstr "" ...@@ -8943,6 +8946,9 @@ msgstr ""
msgid "Issue was closed by %{name} %{reason}" msgid "Issue was closed by %{name} %{reason}"
msgstr "" msgstr ""
msgid "Issue weight"
msgstr ""
msgid "IssueBoards|Board" msgid "IssueBoards|Board"
msgstr "" msgstr ""
...@@ -17062,9 +17068,15 @@ msgstr "" ...@@ -17062,9 +17068,15 @@ msgstr ""
msgid "Total artifacts size: %{total_size}" msgid "Total artifacts size: %{total_size}"
msgstr "" msgstr ""
msgid "Total issues"
msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
msgid "Total weight"
msgstr ""
msgid "Total: %{total}" msgid "Total: %{total}"
msgstr "" msgstr ""
......
...@@ -8,13 +8,17 @@ module QA ...@@ -8,13 +8,17 @@ module QA
class Show < ::QA::Page::Base class Show < ::QA::Page::Base
view 'ee/app/views/shared/milestones/_burndown.html.haml' do view 'ee/app/views/shared/milestones/_burndown.html.haml' do
element :burndown_chart element :burndown_chart
element :weight_button
end end
view 'ee/app/views/shared/milestones/_weight.html.haml' do view 'ee/app/views/shared/milestones/_weight.html.haml' do
element :total_issue_weight_value element :total_issue_weight_value
end end
view 'ee/app/assets/javascripts/burndown_chart/components/burndown_chart.vue' do
element :burndown_chart
element :weight_button
end
def click_weight_button def click_weight_button
click_element(:weight_button) click_element(:weight_button)
end 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