Commit eac1bfe8 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '218459-group-activity-mrs-chart-w-single-series-app-skeleton' into 'master'

Resolve "Group Activity - MRs Chart w/ Single Series: App skeleton"

See merge request gitlab-org/gitlab!35143
parents 381dbae8 b6189c2d
...@@ -2,26 +2,27 @@ ...@@ -2,26 +2,27 @@
import Api from 'ee/api'; import Api from 'ee/api';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import MetricCard from '../../shared/components/metric_card.vue'; import MetricCard from '../../shared/components/metric_card.vue';
const ENABLED_REPORT_PAGES = ['mergeRequests'];
export default { export default {
name: 'GroupActivityCard', name: 'GroupActivityCard',
components: { components: {
MetricCard, MetricCard,
}, },
props: { inject: ['groupFullPath', 'groupName', 'reportPagesPath', 'enableReportPages'],
groupFullPath: {
type: String,
required: true,
},
},
data() { data() {
return { return {
isLoading: false, isLoading: false,
metrics: { metrics: {
mergeRequests: { value: null, label: s__('GroupActivyMetrics|Merge Requests created') }, mergeRequests: {
issues: { value: null, label: s__('GroupActivyMetrics|Issues created') }, value: null,
newMembers: { value: null, label: s__('GroupActivityMetrics|New Members created') }, label: s__('GroupActivityMetrics|Merge Requests opened'),
},
issues: { value: null, label: s__('GroupActivityMetrics|Issues opened') },
newMembers: { value: null, label: s__('GroupActivityMetrics|Members added') },
}, },
}; };
}, },
...@@ -33,6 +34,7 @@ export default { ...@@ -33,6 +34,7 @@ export default {
key, key,
value, value,
label, label,
link: this.generateReportPageLink(key),
}; };
}); });
}, },
...@@ -60,13 +62,27 @@ export default { ...@@ -60,13 +62,27 @@ export default {
this.isLoading = false; this.isLoading = false;
}); });
}, },
displayReportLink(key) {
return this.enableReportPages && ENABLED_REPORT_PAGES.includes(key);
},
generateReportPageLink(key) {
return this.displayReportLink(key)
? mergeUrlParams(
{
groupPath: this.groupFullPath,
groupName: this.groupName,
},
this.reportPagesPath,
)
: null;
},
}, },
}; };
</script> </script>
<template> <template>
<metric-card <metric-card
:title="s__('GroupActivyMetrics|Recent activity (last 90 days)')" :title="s__('GroupActivityMetrics|Recent activity (last 90 days)')"
:metrics="metricsArray" :metrics="metricsArray"
:is-loading="isLoading" :is-loading="isLoading"
/> />
......
...@@ -6,17 +6,20 @@ export default () => { ...@@ -6,17 +6,20 @@ export default () => {
if (!container) return; if (!container) return;
const { groupFullPath } = container.dataset; const { groupFullPath, groupName, reportPagesPath } = container.dataset;
const { reportPages: enableReportPages } = gon?.features;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: container, el: container,
render(h) { provide: {
return h(GroupActivityCard, {
props: {
groupFullPath, groupFullPath,
groupName,
reportPagesPath,
enableReportPages,
}, },
}); render(h) {
return h(GroupActivityCard);
}, },
}); });
}; };
<script>
import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default {
name: 'ReportsApp',
components: {
GlBreadcrumb,
GlIcon,
},
methods: {
breadcrumbs() {
const { groupName = null, groupPath = null } = queryToObject(document.location.search);
return [
groupName && groupPath ? { text: groupName, href: `/${groupPath}` } : null,
{ text: s__('GenericReports|Report'), href: '' },
].filter(Boolean);
},
},
};
</script>
<template>
<div>
<gl-breadcrumb :items="breadcrumbs()">
<template #separator>
<gl-icon name="angle-right" :size="8" />
</template>
</gl-breadcrumb>
</div>
</template>
import Vue from 'vue';
import ReportsApp from './components/app.vue';
export default () => {
const el = document.querySelector('#js-reports-app');
if (!el) return false;
return new Vue({
el,
name: 'ReportsApp',
render: createElement =>
createElement(ReportsApp, {
props: {},
}),
});
};
<script> <script>
import { GlCard, GlSkeletonLoading } from '@gitlab/ui'; import { GlCard, GlSkeletonLoading, GlLink } from '@gitlab/ui';
export default { export default {
name: 'MetricCard', name: 'MetricCard',
components: { components: {
GlCard, GlCard,
GlSkeletonLoading, GlSkeletonLoading,
GlLink,
}, },
props: { props: {
title: { title: {
...@@ -45,7 +46,10 @@ export default { ...@@ -45,7 +46,10 @@ export default {
ref="metricItem" ref="metricItem"
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 class="my-2">{{ valueText(metric) }}</h3> <gl-link v-if="metric.link" :href="metric.link">
<h3 class="gl-my-2 gl-text-blue-700">{{ valueText(metric) }}</h3>
</gl-link>
<h3 v-else class="gl-my-2">{{ valueText(metric) }}</h3>
<p class="text-secondary gl-font-sm mb-2">{{ metric.label }}</p> <p class="text-secondary gl-font-sm mb-2">{{ metric.label }}</p>
</div> </div>
</div> </div>
......
import initReportsApp from 'ee/analytics/reports';
document.addEventListener('DOMContentLoaded', () => {
initReportsApp();
});
...@@ -13,6 +13,10 @@ module EE ...@@ -13,6 +13,10 @@ module EE
before_action only: :issues do before_action only: :issues do
push_frontend_feature_flag(:scoped_labels, @group) push_frontend_feature_flag(:scoped_labels, @group)
end end
before_action only: :show do
push_frontend_feature_flag(:report_pages)
end
end end
override :render_show_html override :render_show_html
......
- @hide_breadcrumbs = true
- page_title _("Reports") - page_title _("Reports")
#js-reports-app
- return unless show_group_activity_analytics? - return unless show_group_activity_analytics?
#js-group-activity{ data: { group_full_path: @group.full_path } } #js-group-activity{ data: { group_full_path: @group.full_path, group_name: @group.name, report_pages_path: analytics_report_pages_path } }
...@@ -25,7 +25,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = ` ...@@ -25,7 +25,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = `
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
4 4
</h3> </h3>
...@@ -40,7 +40,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = ` ...@@ -40,7 +40,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = `
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
- -
</h3> </h3>
...@@ -55,7 +55,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = ` ...@@ -55,7 +55,7 @@ exports[`RecentActivityCard matches the snapshot 1`] = `
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
- -
</h3> </h3>
......
...@@ -25,7 +25,7 @@ exports[`GroupActivity component matches the snapshot 1`] = ` ...@@ -25,7 +25,7 @@ exports[`GroupActivity component matches the snapshot 1`] = `
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
10 10
</h3> </h3>
...@@ -33,14 +33,14 @@ exports[`GroupActivity component matches the snapshot 1`] = ` ...@@ -33,14 +33,14 @@ exports[`GroupActivity component matches the snapshot 1`] = `
<p <p
class="text-secondary gl-font-sm mb-2" class="text-secondary gl-font-sm mb-2"
> >
Merge Requests created Merge Requests opened
</p> </p>
</div> </div>
<div <div
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
20 20
</h3> </h3>
...@@ -48,14 +48,14 @@ exports[`GroupActivity component matches the snapshot 1`] = ` ...@@ -48,14 +48,14 @@ exports[`GroupActivity component matches the snapshot 1`] = `
<p <p
class="text-secondary gl-font-sm mb-2" class="text-secondary gl-font-sm mb-2"
> >
Issues created Issues opened
</p> </p>
</div> </div>
<div <div
class="js-metric-card-item flex-grow text-center" class="js-metric-card-item flex-grow text-center"
> >
<h3 <h3
class="my-2" class="gl-my-2"
> >
30 30
</h3> </h3>
...@@ -63,7 +63,7 @@ exports[`GroupActivity component matches the snapshot 1`] = ` ...@@ -63,7 +63,7 @@ exports[`GroupActivity component matches the snapshot 1`] = `
<p <p
class="text-secondary gl-font-sm mb-2" class="text-secondary gl-font-sm mb-2"
> >
New Members created Members added
</p> </p>
</div> </div>
</div> </div>
......
...@@ -7,9 +7,11 @@ import MetricCard from 'ee/analytics/shared/components/metric_card.vue'; ...@@ -7,9 +7,11 @@ import MetricCard from 'ee/analytics/shared/components/metric_card.vue';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
const TEST_GROUP_ID = 'gitlab-org'; const TEST_GROUP_ID = 'gitlab-org';
const TEST_GROUP_NAME = 'Gitlab Org';
const TEST_MERGE_REQUESTS_COUNT = { data: { merge_requests_count: 10 } }; const TEST_MERGE_REQUESTS_COUNT = { data: { merge_requests_count: 10 } };
const TEST_ISSUES_COUNT = { data: { issues_count: 20 } }; const TEST_ISSUES_COUNT = { data: { issues_count: 20 } };
const TEST_NEW_MEMBERS_COUNT = { data: { new_members_count: 30 } }; const TEST_NEW_MEMBERS_COUNT = { data: { new_members_count: 30 } };
const REPORT_PAGES_PATH = 'report_pages';
describe('GroupActivity component', () => { describe('GroupActivity component', () => {
let wrapper; let wrapper;
...@@ -17,8 +19,11 @@ describe('GroupActivity component', () => { ...@@ -17,8 +19,11 @@ describe('GroupActivity component', () => {
const createComponent = () => { const createComponent = () => {
wrapper = mount(GroupActivityCard, { wrapper = mount(GroupActivityCard, {
propsData: { provide: {
groupFullPath: TEST_GROUP_ID, groupFullPath: TEST_GROUP_ID,
groupName: TEST_GROUP_NAME,
reportPagesPath: REPORT_PAGES_PATH,
enableReportPages: false,
}, },
}); });
}; };
...@@ -85,9 +90,9 @@ describe('GroupActivity component', () => { ...@@ -85,9 +90,9 @@ describe('GroupActivity component', () => {
.then(waitForPromises) .then(waitForPromises)
.then(() => { .then(() => {
expect(findMetricCard().props('metrics')).toEqual([ expect(findMetricCard().props('metrics')).toEqual([
{ key: 'mergeRequests', value: 10, label: 'Merge Requests created' }, { key: 'mergeRequests', value: 10, label: 'Merge Requests opened', link: null },
{ key: 'issues', value: 20, label: 'Issues created' }, { key: 'issues', value: 20, label: 'Issues opened', link: null },
{ key: 'newMembers', value: 30, label: 'New Members created' }, { key: 'newMembers', value: 30, label: 'Members added', link: null },
]); ]);
}); });
}); });
......
import ReportsApp from 'ee/analytics/reports/components/app.vue';
import { shallowMount } from '@vue/test-utils';
import { GlBreadcrumb } from '@gitlab/ui';
import { objectToQuery } from '~/lib/utils/url_utility';
const GROUP_NAME = 'Gitlab Org';
const GROUP_PATH = 'gitlab-org';
const DEFAULT_REPORT_TITLE = 'Report';
const GROUP_URL_QUERY = objectToQuery({
groupName: GROUP_NAME,
groupPath: GROUP_PATH,
});
describe('ReportsApp', () => {
let wrapper;
const createComponent = () => {
return shallowMount(ReportsApp);
};
const findGlBreadcrumb = () => wrapper.find(GlBreadcrumb);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('contains the correct breadcrumbs', () => {
it('displays the report title by default', () => {
wrapper = createComponent();
const breadcrumbs = findGlBreadcrumb();
expect(breadcrumbs.props('items')).toStrictEqual([{ text: DEFAULT_REPORT_TITLE, href: '' }]);
});
describe('with a group in the URL', () => {
beforeEach(() => {
window.history.replaceState({}, null, `?${GROUP_URL_QUERY}`);
});
it('displays the group name and report title', () => {
wrapper = createComponent();
const breadcrumbs = findGlBreadcrumb();
expect(breadcrumbs.props('items')).toStrictEqual([
{ text: GROUP_NAME, href: `/${GROUP_PATH}` },
{ text: DEFAULT_REPORT_TITLE, href: '' },
]);
});
});
});
});
...@@ -3,7 +3,7 @@ import { GlSkeletonLoading } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlSkeletonLoading } from '@gitlab/ui';
import MetricCard from 'ee/analytics/shared/components/metric_card.vue'; import MetricCard from 'ee/analytics/shared/components/metric_card.vue';
const metrics = [ const metrics = [
{ key: 'first_metric', value: 10, label: 'First metric', unit: 'days' }, { key: 'first_metric', value: 10, label: 'First metric', unit: 'days', link: 'some_link' },
{ key: 'second_metric', value: 20, label: 'Yet another metric' }, { key: 'second_metric', value: 20, label: 'Yet another metric' },
{ key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' }, { key: 'third_metric', value: null, label: 'Null metric without value', unit: 'parsecs' },
{ key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' }, { key: 'fourth_metric', value: '-', label: 'Metric without value', unit: 'parsecs' },
...@@ -75,18 +75,23 @@ describe('MetricCard', () => { ...@@ -75,18 +75,23 @@ describe('MetricCard', () => {
}); });
describe.each` describe.each`
columnIndex | label | value | unit columnIndex | label | value | unit | link
${0} | ${'First metric'} | ${10} | ${' days'} ${0} | ${'First metric'} | ${10} | ${' days'} | ${'some_link'}
${1} | ${'Yet another metric'} | ${20} | ${''} ${1} | ${'Yet another metric'} | ${20} | ${''} | ${null}
${2} | ${'Null metric without value'} | ${'-'} | ${''} ${2} | ${'Null metric without value'} | ${'-'} | ${''} | ${null}
${3} | ${'Metric without value'} | ${'-'} | ${''} ${3} | ${'Metric without value'} | ${'-'} | ${''} | ${null}
`('metric columns', ({ columnIndex, label, value, unit }) => { `('metric columns', ({ columnIndex, label, value, unit, link }) => {
it(`renders ${value}${unit} ${label}`, () => { it(`renders ${value}${unit} ${label} with URL ${link}`, () => {
expect( const allMetricItems = findMetricItem();
findMetricItem() const metricItem = allMetricItems.at(columnIndex);
.at(columnIndex)
.text(), expect(metricItem.text()).toBe(`${value}${unit} ${label}`);
).toEqual(`${value}${unit} ${label}`);
if (link) {
expect(metricItem.find('a').attributes('href')).toBe(link);
} else {
expect(metricItem.find('a').exists()).toBe(false);
}
}); });
}); });
}); });
......
...@@ -10280,6 +10280,9 @@ msgstr "" ...@@ -10280,6 +10280,9 @@ msgstr ""
msgid "Generate new export" msgid "Generate new export"
msgstr "" msgstr ""
msgid "GenericReports|Report"
msgstr ""
msgid "Geo" msgid "Geo"
msgstr "" msgstr ""
...@@ -11234,16 +11237,16 @@ msgstr "" ...@@ -11234,16 +11237,16 @@ msgstr ""
msgid "Group: %{name}" msgid "Group: %{name}"
msgstr "" msgstr ""
msgid "GroupActivityMetrics|New Members created" msgid "GroupActivityMetrics|Issues opened"
msgstr "" msgstr ""
msgid "GroupActivyMetrics|Issues created" msgid "GroupActivityMetrics|Members added"
msgstr "" msgstr ""
msgid "GroupActivyMetrics|Merge Requests created" msgid "GroupActivityMetrics|Merge Requests opened"
msgstr "" msgstr ""
msgid "GroupActivyMetrics|Recent activity (last 90 days)" msgid "GroupActivityMetrics|Recent activity (last 90 days)"
msgstr "" msgstr ""
msgid "GroupImport|Failed to import group." msgid "GroupImport|Failed to import group."
......
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