Commit 4c5e842f authored by Simon Knox's avatar Simon Knox

Move iteration report summary stats below weight toggle

Also make weight toggle affect summary stats
parent 0bfd480e
......@@ -6,7 +6,10 @@ import { __ } from '~/locale';
import { getDayDifference, nDaysAfter, newDateAsLocaleTime } from '~/lib/utils/datetime_utility';
import BurndownChart from './burndown_chart.vue';
import BurnupChart from './burnup_chart.vue';
import BurnupQuery from '../queries/burnup.query.graphql';
import TimeboxSummaryCards from './timebox_summary_cards.vue';
import OpenTimeboxSummary from './open_timebox_summary.vue';
import { Namespace } from '../constants';
import BurnupQuery from '../graphql/burnup.query.graphql';
import BurndownChartData from '../burn_chart_data';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
......@@ -19,6 +22,8 @@ export default {
BurndownChart,
BurnupChart,
GlSprintf,
OpenTimeboxSummary,
TimeboxSummaryCards,
},
mixins: [glFeatureFlagsMixin()],
props: {
......@@ -40,6 +45,21 @@ export default {
required: false,
default: '',
},
iterationState: {
type: String,
required: false,
default: '',
},
fullPath: {
type: String,
required: false,
default: '',
},
namespaceType: {
type: String,
required: false,
default: Namespace.Group,
},
burndownEventsPath: {
type: String,
required: false,
......@@ -52,7 +72,7 @@ export default {
},
},
apollo: {
burnupData: {
report: {
skip() {
return !this.milestoneId && !this.iterationId;
},
......@@ -61,12 +81,21 @@ export default {
return {
id: this.iterationId || this.milestoneId,
isIteration: Boolean(this.iterationId),
weight: !this.issuesSelected,
};
},
update(data) {
const sparseBurnupData = data?.[this.parent]?.report.burnupTimeSeries || [];
const sparseBurnupData = data[this.parent]?.report.burnupTimeSeries || [];
const stats = data[this.parent]?.report?.stats || {};
return this.padSparseBurnupData(sparseBurnupData);
return {
burnupData: this.padSparseBurnupData(sparseBurnupData),
stats: {
complete: stats.complete?.[this.displayValue] || 0,
incomplete: stats.incomplete?.[this.displayValue] || 0,
total: stats.total?.[this.displayValue] || 0,
},
};
},
error() {
this.error = __('Error fetching burnup chart data');
......@@ -78,19 +107,44 @@ export default {
openIssuesCount: [],
openIssuesWeight: [],
issuesSelected: true,
burnupData: [],
report: {
burnupData: [],
stats: {
complete: 0,
incomplete: 0,
total: 0,
},
},
useLegacyBurndown: false,
showInfo: this.showNewOldBurndownToggle,
error: '',
};
},
computed: {
loading() {
return this.$apollo.queries.report.loading;
},
burnupData() {
return this.report.burnupData;
},
columns() {
return [
{
title: __('Completed'),
value: this.report.stats.complete,
},
{
title: __('Incomplete'),
value: this.report.stats.incomplete,
},
];
},
displayValue() {
return this.issuesSelected ? 'count' : 'weight';
},
parent() {
return this.iterationId ? 'iteration' : 'milestone';
},
title() {
return __('Charts');
},
issueButtonCategory() {
return this.issuesSelected ? 'primary' : 'secondary';
},
......@@ -239,7 +293,7 @@ export default {
</gl-sprintf>
</gl-alert>
<div class="burndown-header gl-display-flex gl-align-items-center gl-flex-wrap">
<h3 ref="chartsTitle">{{ title }}</h3>
<strong ref="filterLabel">{{ __('Filter by') }}</strong>
<gl-button-group>
<gl-button
ref="totalIssuesButton"
......@@ -283,8 +337,30 @@ export default {
</gl-button>
</gl-button-group>
</div>
<template v-if="iterationId">
<timebox-summary-cards
v-if="iterationState === 'closed'"
:columns="columns"
:loading="loading"
:total="report.stats.total"
/>
<open-timebox-summary
v-else
:full-path="fullPath"
:iteration-id="iterationId"
:namespace-type="namespaceType"
:display-value="displayValue"
>
<timebox-summary-cards
slot-scope="{ columns: openColumns, loading: summaryLoading, total }"
:columns="openColumns"
:loading="summaryLoading"
:total="total"
/>
</open-timebox-summary>
</template>
<div class="row">
<gl-alert v-if="error" variant="danger" class="col-12" @dismiss="error = ''">
<gl-alert v-if="error" variant="danger" class="col-12" @dismiss="error = null">
{{ error }}
</gl-alert>
<burndown-chart
......
......@@ -117,12 +117,6 @@ export default {
});
}
},
showIssueCount() {
this.issuesSelected = true;
},
showIssueWeight() {
this.issuesSelected = false;
},
},
};
</script>
......
<script>
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import IterationReportSummaryCards from './iteration_report_summary_cards.vue';
import summaryStatsQuery from '../queries/iteration_issues_summary.query.graphql';
import { Namespace } from '../constants';
import summaryStatsQuery from '../graphql/iteration_issues_summary.query.graphql';
import { Namespace, Unit } from '../constants';
export default {
components: {
IterationReportSummaryCards,
},
apollo: {
issues: {
query: summaryStatsQuery,
......@@ -17,9 +13,9 @@ export default {
},
update(data) {
return {
open: data[this.namespaceType]?.openIssues?.count || 0,
assigned: data[this.namespaceType]?.assignedIssues?.count || 0,
closed: data[this.namespaceType]?.closedIssues?.count || 0,
open: data[this.namespaceType]?.openIssues?.[this.displayValue] || 0,
assigned: data[this.namespaceType]?.assignedIssues?.[this.displayValue] || 0,
closed: data[this.namespaceType]?.closedIssues?.[this.displayValue] || 0,
};
},
error() {
......@@ -42,6 +38,12 @@ export default {
default: Namespace.Group,
validator: value => Object.values(Namespace).includes(value),
},
displayValue: {
type: String,
required: false,
default: Unit.count,
validator: val => Unit[val],
},
},
data() {
return {
......@@ -58,16 +60,9 @@ export default {
fullPath: this.fullPath,
id: getIdFromGraphQLId(this.iterationId),
isGroup: this.namespaceType === Namespace.Group,
weight: this.displayValue === Unit.weight,
};
},
completedPercent() {
const open = this.issues.open + this.issues.assigned;
const { closed } = this.issues;
if (closed <= 0) {
return 0;
}
return ((closed / (open + closed)) * 100).toFixed(0);
},
columns() {
return [
{
......
export { Namespace } from '../iterations/constants';
export const Unit = {
count: 'count',
weight: 'weight',
};
query IterationBurnupTimesSeriesData($id: ID!, $isIteration: Boolean = false) {
#import "./burnup_timebox_report.fragment.graphql"
query BurnupTimesSeriesData($id: ID!, $isIteration: Boolean = false, $weight: Boolean = false) {
milestone(id: $id) @skip(if: $isIteration) {
title
id
title
report {
burnupTimeSeries {
date
scopeCount
scopeWeight
completedCount
completedWeight
}
...BurnupTimeboxReport
}
}
iteration(id: $id) @include(if: $isIteration) {
title
id
title
report {
burnupTimeSeries {
date
scopeCount
scopeWeight
completedCount
completedWeight
}
...BurnupTimeboxReport
}
}
}
fragment BurnupTimeboxReport on TimeboxReport {
burnupTimeSeries {
date
completedCount @skip(if: $weight)
scopeCount @skip(if: $weight)
completedWeight @include(if: $weight)
scopeWeight @include(if: $weight)
}
stats {
total {
count @skip(if: $weight)
weight @include(if: $weight)
}
complete {
count @skip(if: $weight)
weight @include(if: $weight)
}
incomplete {
count @skip(if: $weight)
weight @include(if: $weight)
}
}
}
query IterationIssuesSummary($fullPath: ID!, $id: ID!, $isGroup: Boolean = true) {
query IterationIssuesSummary(
$fullPath: ID!
$id: ID!
$isGroup: Boolean = true
$weight: Boolean = false
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
openIssues: issues(
iterationId: [$id]
......@@ -6,8 +11,8 @@ query IterationIssuesSummary($fullPath: ID!, $id: ID!, $isGroup: Boolean = true)
assigneeId: "none"
includeSubgroups: true
) {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
assignedIssues: issues(
iterationId: [$id]
......@@ -15,26 +20,26 @@ query IterationIssuesSummary($fullPath: ID!, $id: ID!, $isGroup: Boolean = true)
assigneeId: "any"
includeSubgroups: true
) {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
closedIssues: issues(iterationId: [$id], state: closed, includeSubgroups: true) {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
}
project(fullPath: $fullPath) @skip(if: $isGroup) {
openIssues: issues(iterationId: [$id], state: opened, assigneeId: "none") {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
assignedIssues: issues(iterationId: [$id], state: opened, assigneeId: "any") {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
closedIssues: issues(iterationId: [$id], state: closed) {
count
weight
count @skip(if: $weight)
weight @include(if: $weight)
}
}
}
......@@ -13,9 +13,6 @@ import BurnCharts from 'ee/burndown_chart/components/burn_charts.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import IterationReportSummaryCards from './iteration_report_summary_cards.vue';
import IterationReportSummaryClosed from './iteration_report_summary_closed.vue';
import IterationReportSummaryOpen from './iteration_report_summary_open.vue';
import IterationForm from './iteration_form.vue';
import IterationReportTabs from './iteration_report_tabs.vue';
import query from '../queries/iteration.query.graphql';
......@@ -43,9 +40,6 @@ export default {
GlEmptyState,
GlLoadingIcon,
IterationForm,
IterationReportSummaryCards,
IterationReportSummaryClosed,
IterationReportSummaryOpen,
IterationReportTabs,
},
apollo: {
......@@ -138,11 +132,6 @@ export default {
return { text: __('Open'), variant: 'success' };
}
},
summaryComponent() {
return this.iteration.state === 'closed'
? IterationReportSummaryClosed
: IterationReportSummaryOpen;
},
},
mounted() {
this.boundOnPopState = this.onPopState.bind(this);
......@@ -226,31 +215,13 @@ export default {
</div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3>
<div ref="description" v-html="iteration.descriptionHtml"></div>
<component
:is="summaryComponent"
:full-path="fullPath"
:iteration-id="iteration.id"
:namespace-type="namespaceType"
>
<iteration-report-summary-cards
slot-scope="{ columns, loading: summaryLoading, total }"
:columns="columns"
:loading="summaryLoading"
:total="total"
/>
</component>
<!-- <iteration-report-summary-closed
v-if="iteration.state === 'closed'"
:iteration-id="iteration.id"
/>
<iteration-report-summary-open
v-else
/> -->
<burn-charts
:start-date="iteration.startDate"
:due-date="iteration.dueDate"
:iteration-id="iteration.id"
:iteration-state="iteration.state"
:full-path="fullPath"
:namespace-type="namespaceType"
/>
<iteration-report-tabs
:full-path="fullPath"
......
<script>
import { __ } from '~/locale';
import IterationReportSummaryCards from './iteration_report_summary_cards.vue';
import summaryStatsQuery from '../queries/iteration_issues_summary_stats.query.graphql';
export default {
components: {
IterationReportSummaryCards,
},
apollo: {
issues: {
query: summaryStatsQuery,
variables() {
return this.queryVariables;
},
update(data) {
const stats = data.iteration?.report?.stats || {};
return {
complete: stats.complete?.count || 0,
incomplete: stats.incomplete?.count || 0,
total: stats.total?.count || 0,
};
},
},
},
props: {
iterationId: {
type: String,
required: true,
},
},
data() {
return {
issues: {
complete: 0,
incomplete: 0,
total: 0,
},
};
},
computed: {
queryVariables() {
return {
id: this.iterationId,
};
},
columns() {
return [
{
title: __('Completed'),
value: this.issues.complete,
},
{
title: __('Incomplete'),
value: this.issues.incomplete,
},
];
},
},
render() {
return this.$scopedSlots.default({
columns: this.columns,
loading: this.$apollo.queries.issues.loading,
total: this.issues.total,
});
},
};
</script>
query IterationIssuesSummaryStats($id: ID!) {
iteration(id: $id) {
id
report {
stats {
total {
weight
count
}
complete {
weight
count
}
incomplete {
weight
count
}
}
}
}
}
---
title: Move iteration report summary stats underneath toggle buttons
merge_request: 48659
author:
type: added
......@@ -5,6 +5,8 @@ import MockAdapter from 'axios-mock-adapter';
import BurnCharts from 'ee/burndown_chart/components/burn_charts.vue';
import BurndownChart from 'ee/burndown_chart/components/burndown_chart.vue';
import BurnupChart from 'ee/burndown_chart/components/burnup_chart.vue';
import OpenTimeboxSummary from 'ee/burndown_chart/components/open_timebox_summary.vue';
import TimeboxSummaryCards from 'ee/burndown_chart/components/timebox_summary_cards.vue';
import { useFakeDate } from 'helpers/fake_date';
import { day1, day2, day3, day4 } from '../mock_data';
......@@ -18,7 +20,7 @@ describe('burndown_chart', () => {
let wrapper;
let mock;
const findChartsTitle = () => wrapper.find({ ref: 'chartsTitle' });
const findFilterLabel = () => wrapper.find({ ref: 'filterLabel' });
const findIssuesButton = () => wrapper.find({ ref: 'totalIssuesButton' });
const findWeightButton = () => wrapper.find({ ref: 'totalWeightButton' });
const findActiveButtons = () =>
......@@ -29,6 +31,7 @@ describe('burndown_chart', () => {
const findNewBurndownChartButton = () => wrapper.find({ ref: 'newBurndown' });
const defaultProps = {
fullPath: 'gitlab-org/subgroup',
startDate: '2020-08-07',
dueDate: '2020-09-09',
openIssuesCount: [],
......@@ -42,6 +45,15 @@ describe('burndown_chart', () => {
...defaultProps,
...props,
},
mocks: {
$apollo: {
queries: {
report: {
loading: false,
},
},
},
},
data() {
return data;
},
......@@ -101,7 +113,7 @@ describe('burndown_chart', () => {
it('sets section title and chart title correctly', () => {
createComponent();
expect(findChartsTitle().text()).toBe('Charts');
expect(findFilterLabel().text()).toBe('Filter by');
expect(findBurndownChart().props().showTitle).toBe(true);
});
......@@ -115,10 +127,49 @@ describe('burndown_chart', () => {
expect(findBurnupChart().props('issuesSelected')).toBe(false);
});
it('renders IterationReportSummaryOpen for open iteration', () => {
createComponent({
data: {
report: {
stats: {},
},
},
props: {
iterationState: 'open',
iterationId: 'gid://gitlab/Iteration/11',
},
});
expect(wrapper.find(OpenTimeboxSummary).props()).toEqual({
iterationId: 'gid://gitlab/Iteration/11',
displayValue: 'count',
namespaceType: 'group',
fullPath: defaultProps.fullPath,
});
});
it('renders TimeboxSummaryCards for closed iterations', () => {
createComponent({
data: {
report: {
stats: {},
},
},
props: {
iterationState: 'closed',
iterationId: 'gid://gitlab/Iteration/1',
},
});
expect(wrapper.find(TimeboxSummaryCards).exists()).toBe(true);
});
it('uses burndown data computed from burnup data', () => {
createComponent({
data: {
burnupData: [day1],
report: {
burnupData: [day1],
},
},
});
const { openIssuesCount, openIssuesWeight } = findBurndownChart().props();
......
import IterationReportSummary from 'ee/iterations/components/iteration_report_summary_open.vue';
import IterationReportSummaryCards from 'ee/iterations/components/iteration_report_summary_cards.vue';
import OpenTimeboxSummary from 'ee/burndown_chart/components/open_timebox_summary.vue';
import { shallowMount } from '@vue/test-utils';
describe('Iterations report summary', () => {
......@@ -15,11 +14,8 @@ describe('Iterations report summary', () => {
const mountComponent = ({ props = defaultProps, loading = false, data = {} } = {}) => {
slotSpy = jest.fn();
wrapper = shallowMount(IterationReportSummary, {
wrapper = shallowMount(OpenTimeboxSummary, {
propsData: props,
components: {
IterationReportSummaryCards,
},
data() {
return data;
},
......
import IterationReportSummaryCards from 'ee/iterations/components/iteration_report_summary_cards.vue';
import TimeboxSummaryCards from 'ee/burndown_chart/components/timebox_summary_cards.vue';
import { mount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui';
......@@ -24,7 +24,7 @@ describe('Iterations report summary cards', () => {
};
const mountComponent = (props = defaultProps) => {
wrapper = mount(IterationReportSummaryCards, {
wrapper = mount(TimeboxSummaryCards, {
propsData: props,
});
};
......
import { GlDropdown, GlDropdownItem, GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IterationForm from 'ee/iterations/components/iteration_form.vue';
import IterationReportSummaryOpen from 'ee/iterations/components/iteration_report_summary_open.vue';
import IterationReportSummaryClosed from 'ee/iterations/components/iteration_report_summary_closed.vue';
import IterationReport from 'ee/iterations/components/iteration_report.vue';
import IterationReportTabs from 'ee/iterations/components/iteration_report_tabs.vue';
import { Namespace } from 'ee/iterations/constants';
......@@ -107,27 +105,6 @@ describe('Iterations report', () => {
expect(findActionsDropdown().exists()).toBe(false);
});
it('renders IterationReportSummaryOpen for open iteration', () => {
expect(wrapper.find(IterationReportSummaryOpen).props()).toEqual({
iterationId: iteration.id,
namespaceType: Namespace.Group,
fullPath: defaultProps.fullPath,
});
});
it('renders IterationReportSummaryClosed for closed iteration', async () => {
await wrapper.setData({
iteration: {
...iteration,
state: 'closed',
},
});
expect(wrapper.find(IterationReportSummaryClosed).props()).toEqual({
iterationId: iteration.id,
});
});
it('shows IterationReportTabs component', () => {
const iterationReportTabs = wrapper.find(IterationReportTabs);
......
import IterationReportSummaryClosed from 'ee/iterations/components/iteration_report_summary_closed.vue';
import { shallowMount } from '@vue/test-utils';
describe('Iterations report summary', () => {
let wrapper;
let slotSpy;
const id = 3;
const defaultProps = {
iterationId: `gid://gitlab/Iteration/${id}`,
};
const mountComponent = ({ props = defaultProps, loading = false, data = {} } = {}) => {
slotSpy = jest.fn();
wrapper = shallowMount(IterationReportSummaryClosed, {
propsData: props,
data() {
return data;
},
mocks: {
$apollo: {
queries: { issues: { loading } },
},
},
scopedSlots: {
default: slotSpy,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with valid totals', () => {
beforeEach(() => {
mountComponent({
data: {
issues: {
complete: 10,
incomplete: 3,
total: 13,
},
},
});
});
it('renders cards for each issue type', () => {
expect(slotSpy).toHaveBeenCalledWith({
loading: false,
columns: [
{
title: 'Completed',
value: 10,
},
{
title: 'Incomplete',
value: 3,
},
],
total: 13,
});
});
});
});
......@@ -5179,9 +5179,6 @@ msgstr ""
msgid "Channel handle (e.g. town-square)"
msgstr ""
msgid "Charts"
msgstr ""
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
......@@ -12137,6 +12134,9 @@ msgstr ""
msgid "Filter"
msgstr ""
msgid "Filter by"
msgstr ""
msgid "Filter by %{issuable_type} that are currently closed."
msgstr ""
......
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