Commit 47c30eb1 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'psi-iteration-issue-summary' into 'master'

Add issue summary section to iteration report

See merge request gitlab-org/gitlab!35824
parents 0d9362bf 5d573fa1
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
} from '@gitlab/ui'; } from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale'; import { __ } from '~/locale';
import IterationReportSummary from './iteration_report_summary.vue';
import IterationForm from './iteration_form.vue'; import IterationForm from './iteration_form.vue';
import IterationReportTabs from './iteration_report_tabs.vue'; import IterationReportTabs from './iteration_report_tabs.vue';
import query from '../queries/group_iteration.query.graphql'; import query from '../queries/group_iteration.query.graphql';
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
GlNewDropdown, GlNewDropdown,
GlNewDropdownItem, GlNewDropdownItem,
IterationForm, IterationForm,
IterationReportSummary,
IterationReportTabs, IterationReportTabs,
}, },
apollo: { apollo: {
...@@ -156,6 +158,7 @@ export default { ...@@ -156,6 +158,7 @@ export default {
</div> </div>
<h3 ref="title" class="page-title">{{ iteration.title }}</h3> <h3 ref="title" class="page-title">{{ iteration.title }}</h3>
<div ref="description" v-html="iteration.description"></div> <div ref="description" v-html="iteration.description"></div>
<iteration-report-summary :group-path="groupPath" :iteration-id="iteration.id" />
<iteration-report-tabs :group-path="groupPath" :iteration-id="iteration.id" /> <iteration-report-tabs :group-path="groupPath" :iteration-id="iteration.id" />
</template> </template>
</div> </div>
......
<script>
import { GlCard, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import query from '../queries/iteration_issues_summary.query.graphql';
export default {
cardBodyClass: 'gl-text-center gl-py-3',
cardClass: 'gl-bg-gray-10 gl-border-0',
components: {
GlCard,
GlIcon,
},
apollo: {
issues: {
query,
variables() {
return this.queryVariables;
},
update(data) {
return {
open: data?.group?.openIssues?.count || 0,
assigned: data?.group?.assignedIssues?.count || 0,
closed: data?.group?.closedIssues?.count || 0,
};
},
error() {
this.error = __('Error loading issues');
},
},
},
props: {
groupPath: {
type: String,
required: true,
},
iterationId: {
type: String,
required: true,
},
},
data() {
return {
issues: {},
};
},
computed: {
queryVariables() {
return {
groupPath: this.groupPath,
id: getIdFromGraphQLId(this.iterationId),
};
},
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);
},
showCards() {
return !this.$apollo.queries.issues.loading && Object.values(this.issues).every(a => a >= 0);
},
columns() {
return [
{
title: __('Complete'),
value: `${this.completedPercent}%`,
},
{
title: __('Open'),
value: this.issues.open,
icon: true,
},
{
title: __('In progress'),
value: this.issues.assigned,
icon: true,
},
{
title: __('Completed'),
value: this.issues.closed,
icon: true,
},
];
},
},
};
</script>
<template>
<div v-if="showCards" class="row gl-mt-6">
<div v-for="(column, index) in columns" :key="index" class="col-sm-3">
<gl-card :class="$options.cardClass" :body-class="$options.cardBodyClass">
<span>{{ column.title }}</span>
<span class="gl-font-size-h2 gl-font-weight-bold">{{ column.value }}</span>
<gl-icon v-if="column.icon" name="issues" :size="12" class="gl-text-gray-700" />
</gl-card>
</div>
</div>
</template>
...@@ -181,7 +181,7 @@ export default { ...@@ -181,7 +181,7 @@ export default {
v-else v-else
:items="issues.list" :items="issues.list"
:fields="$options.fields" :fields="$options.fields"
:empty-text="__('No iterations found')" :empty-text="__('No issues found')"
:show-empty="true" :show-empty="true"
fixed fixed
stacked="sm" stacked="sm"
......
query GroupIteration($groupPath: ID!, $id: ID!) {
group(fullPath: $groupPath) {
openIssues: issues(iterationId: [$id], state: opened, assigneeId: "none") {
count
}
assignedIssues: issues(iterationId: [$id], state: opened, assigneeId: "any") {
count
}
closedIssues: issues(iterationId: [$id], state: closed) {
count
}
}
}
...@@ -51,7 +51,7 @@ describe('Iterations report tabs', () => { ...@@ -51,7 +51,7 @@ describe('Iterations report tabs', () => {
expect(wrapper.contains(GlLoadingIcon)).toBe(false); expect(wrapper.contains(GlLoadingIcon)).toBe(false);
expect(wrapper.contains(GlTable)).toBe(true); expect(wrapper.contains(GlTable)).toBe(true);
expect(wrapper.text()).toContain('No iterations found'); expect(wrapper.text()).toContain('No issues found');
}); });
it('shows error in a gl-alert', () => { it('shows error in a gl-alert', () => {
......
import IterationReportSummary from 'ee/iterations/components/iteration_report_summary.vue';
import { mount } from '@vue/test-utils';
import { GlCard } from '@gitlab/ui';
describe('Iterations report tabs', () => {
let wrapper;
const id = 3;
const groupPath = 'gitlab-org';
const defaultProps = {
groupPath,
iterationId: `gid://gitlab/Iteration/${id}`,
};
const mountComponent = ({ props = defaultProps, loading = false, data = {} } = {}) => {
wrapper = mount(IterationReportSummary, {
propsData: props,
data() {
return data;
},
mocks: {
$apollo: {
queries: { issues: { loading } },
},
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findPercentageCard = () => wrapper.findAll(GlCard).at(0);
const findOpenCard = () => wrapper.findAll(GlCard).at(1);
const findInProgressCard = () => wrapper.findAll(GlCard).at(2);
const findCompletedCard = () => wrapper.findAll(GlCard).at(3);
describe('with valid totals', () => {
beforeEach(() => {
mountComponent();
wrapper.setData({
issues: {
open: 15,
assigned: 5,
closed: 10,
},
});
});
it('shows complete percentage', () => {
expect(findPercentageCard().text()).toContain('33%');
});
it('shows open issues', () => {
expect(findOpenCard().text()).toContain('Open');
expect(findOpenCard().text()).toContain('15');
});
it('shows in progress issues', () => {
expect(findInProgressCard().text()).toContain('In progress');
expect(findInProgressCard().text()).toContain('5');
});
it('shows completed issues', () => {
expect(findCompletedCard().text()).toContain('Completed');
expect(findCompletedCard().text()).toContain('10');
});
});
describe('with no issues', () => {
beforeEach(() => {
mountComponent();
wrapper.setData({
issues: {
open: 0,
assigned: 0,
closed: 0,
},
});
});
it('shows complete percentage', () => {
expect(findPercentageCard().text()).toContain('0%');
expect(findOpenCard().text()).toContain('0');
expect(findInProgressCard().text()).toContain('0');
expect(findCompletedCard().text()).toContain('0');
});
});
});
...@@ -6110,6 +6110,9 @@ msgstr "" ...@@ -6110,6 +6110,9 @@ msgstr ""
msgid "Complete" msgid "Complete"
msgstr "" msgstr ""
msgid "Completed"
msgstr ""
msgid "Compliance" msgid "Compliance"
msgstr "" msgstr ""
...@@ -15749,10 +15752,10 @@ msgstr "" ...@@ -15749,10 +15752,10 @@ msgstr ""
msgid "No grouping" msgid "No grouping"
msgstr "" msgstr ""
msgid "No iteration" msgid "No issues found"
msgstr "" msgstr ""
msgid "No iterations found" msgid "No iteration"
msgstr "" msgstr ""
msgid "No iterations to show" msgid "No iterations to show"
......
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