Commit 8c801481 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Generalize burnup chart service for other metrics

Renames TimeboxBurnupChartService to TimeboxReportService since this
will also be used to compute other statistics.

This also moves the burnup time series in GraphQL under a `report`
field.
parent b1719cb4
---
title: Add GraphQL burnup endpoint under milestone and iteration reports
merge_request: 45121
author:
type: added
......@@ -10818,7 +10818,7 @@ enum IssueType {
"""
Represents an iteration object
"""
type Iteration implements TimeboxBurnupTimeSeriesInterface {
type Iteration implements TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
......@@ -10854,6 +10854,11 @@ type Iteration implements TimeboxBurnupTimeSeriesInterface {
"""
iid: ID!
"""
Historically accurate report about the timebox
"""
report: TimeboxReport
"""
Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts
"""
......@@ -12831,7 +12836,7 @@ type MetricsDashboardAnnotationEdge {
"""
Represents a milestone
"""
type Milestone implements TimeboxBurnupTimeSeriesInterface {
type Milestone implements TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
......@@ -12867,6 +12872,11 @@ type Milestone implements TimeboxBurnupTimeSeriesInterface {
"""
projectMilestone: Boolean!
"""
Historically accurate report about the timebox
"""
report: TimeboxReport
"""
Timestamp of the milestone start date
"""
......@@ -20155,13 +20165,28 @@ Time represented in ISO 8601
"""
scalar Time
interface TimeboxBurnupTimeSeriesInterface {
"""
Represents a historically accurate report about the timebox
"""
type TimeboxReport {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
}
interface TimeboxReportInterface {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Historically accurate report about the timebox
"""
report: TimeboxReport
}
"""
A time-frame defined as a closed inclusive range of two dates
"""
......
......@@ -29533,6 +29533,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "report",
"description": "Historically accurate report about the timebox",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "TimeboxReport",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopedPath",
"description": "Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts",
......@@ -29670,7 +29684,7 @@
"interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"name": "TimeboxReportInterface",
"ofType": null
}
],
......@@ -35318,6 +35332,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "report",
"description": "Historically accurate report about the timebox",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "TimeboxReport",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startDate",
"description": "Timestamp of the milestone start date",
......@@ -35441,7 +35469,7 @@
"interfaces": [
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"name": "TimeboxReportInterface",
"ofType": null
}
],
......@@ -58503,9 +58531,44 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TimeboxReport",
"description": "Represents a historically accurate report about the timebox",
"fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "TimeboxBurnupTimeSeriesInterface",
"name": "TimeboxReportInterface",
"description": null,
"fields": [
{
......@@ -58529,6 +58592,20 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "report",
"description": "Historically accurate report about the timebox",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "TimeboxReport",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -1652,6 +1652,7 @@ Represents an iteration object.
| `dueDate` | Time | Timestamp of the iteration due date |
| `id` | ID! | ID of the iteration |
| `iid` | ID! | Internal ID of the iteration |
| `report` | TimeboxReport | Historically accurate report about the timebox |
| `scopedPath` | String | Web path of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts |
| `scopedUrl` | String | Web URL of the iteration, scoped to the query parent. Only valid for Project parents. Returns null in other contexts |
| `startDate` | Time | Timestamp of the iteration start date |
......@@ -1964,6 +1965,7 @@ Represents a milestone.
| `groupMilestone` | Boolean! | Indicates if milestone is at group level |
| `id` | ID! | ID of the milestone |
| `projectMilestone` | Boolean! | Indicates if milestone is at project level |
| `report` | TimeboxReport | Historically accurate report about the timebox |
| `startDate` | Time | Timestamp of the milestone start date |
| `state` | MilestoneStateEnum! | State of the milestone |
| `stats` | MilestoneStats | Milestone statistics |
......@@ -2958,6 +2960,14 @@ Represents a requirement test report.
| `id` | ID! | ID of the test report |
| `state` | TestReportState! | State of the test report |
### TimeboxReport
Represents a historically accurate report about the timebox.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
### Timelog
| Field | Type | Description |
......
......@@ -6,7 +6,7 @@ module EE
extend ActiveSupport::Concern
prepended do
implements ::Types::TimeboxBurnupTimeSeriesInterface
implements ::Types::TimeboxReportInterface
end
end
end
......
......@@ -9,11 +9,11 @@ module Resolvers
def resolve(*args)
return [] unless timebox.burnup_charts_available?
response = TimeboxBurnupChartService.new(timebox).execute
response = TimeboxReportService.new(timebox).execute
raise GraphQL::ExecutionError, response.message if response.error?
response.payload
response.payload[:burnup_time_series]
end
end
end
# frozen_string_literal: true
module Resolvers
class TimeboxReportResolver < BaseResolver
type Types::TimeboxReportType, null: true
alias_method :timebox, :synchronized_object
def resolve(*args)
return {} unless timebox.burnup_charts_available?
response = TimeboxReportService.new(timebox).execute
raise GraphQL::ExecutionError, response.message if response.error?
response.payload
end
end
end
......@@ -9,7 +9,7 @@ module Types
authorize :read_iteration
implements ::Types::TimeboxBurnupTimeSeriesInterface
implements ::Types::TimeboxReportInterface
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the iteration'
......
# frozen_string_literal: true
module Types
module TimeboxBurnupTimeSeriesInterface
module TimeboxReportInterface
include BaseInterface
field :report, Types::TimeboxReportType, null: true,
resolver: ::Resolvers::TimeboxReportResolver,
description: 'Historically accurate report about the timebox',
complexity: 175
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true,
resolver: ::Resolvers::TimeboxBurnupTimeSeriesResolver,
description: 'Daily scope and completed totals for burnup charts',
......
# frozen_string_literal: true
# rubocop: disable Graphql/AuthorizeTypes
module Types
class TimeboxReportType < BaseObject
graphql_name 'TimeboxReport'
description 'Represents a historically accurate report about the timebox'
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true,
description: 'Daily scope and completed totals for burnup charts'
end
end
......@@ -7,7 +7,7 @@
# This is implemented by iterating over all relevant resource events ordered by time. We need to do this
# so that we can keep track of the issue's state during that point in time and handle the events based on that.
class TimeboxBurnupChartService
class TimeboxReportService
include Gitlab::Utils::StrongMemoize
EVENT_COUNT_LIMIT = 50_000
......@@ -35,7 +35,9 @@ class TimeboxBurnupChartService
end
end
ServiceResponse.success(payload: chart_data)
ServiceResponse.success(payload: {
burnup_time_series: chart_data
})
end
private
......
......@@ -3,5 +3,11 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Milestone'] do
it { expect(described_class).to have_graphql_field(:burnup_time_series) }
it 'has the expected fields' do
expected_fields = %w[
report burnup_time_series
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
end
......@@ -54,7 +54,7 @@ RSpec.describe Resolvers::TimeboxBurnupTimeSeriesResolver do
context 'when the service returns an error' do
before do
stub_const('TimeboxBurnupChartService::EVENT_COUNT_LIMIT', 1)
stub_const('TimeboxReportService::EVENT_COUNT_LIMIT', 1)
end
it 'raises a GraphQL exception' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::TimeboxReportResolver do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:issues) { create_list(:issue, 2, project: project) }
let_it_be(:start_date) { Date.today }
let_it_be(:due_date) { start_date + 2.weeks }
before do
stub_licensed_features(milestone_charts: true, issue_weights: true, iterations: true)
end
RSpec.shared_examples 'timebox time series' do
subject { resolve(described_class, obj: timebox) }
context 'when the feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false, iteration_charts: false)
end
it 'returns empty data' do
expect(subject).to be_empty
end
end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true, iteration_charts: true)
end
it 'returns burnup chart data' do
expect(subject).to eq(burnup_time_series: [
{
date: start_date + 4.days,
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: start_date + 9.days,
scope_count: 2,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
context 'when the service returns an error' do
before do
stub_const('TimeboxReportService::EVENT_COUNT_LIMIT', 1)
end
it 'raises a GraphQL exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError, 'Burnup chart could not be generated due to too many events')
end
end
end
end
context 'when timebox is a milestone' do
let_it_be(:timebox) { create(:milestone, project: project, start_date: start_date, due_date: due_date) }
before_all do
create(:resource_milestone_event, issue: issues[0], milestone: timebox, action: :add, created_at: start_date + 4.days)
create(:resource_milestone_event, issue: issues[1], milestone: timebox, action: :add, created_at: start_date + 9.days)
end
it_behaves_like 'timebox time series'
end
context 'when timebox is an iteration' do
let_it_be(:timebox) { create(:iteration, group: group, start_date: start_date, due_date: due_date) }
before_all do
create(:resource_iteration_event, issue: issues[0], iteration: timebox, action: :add, created_at: start_date + 4.days)
create(:resource_iteration_event, issue: issues[1], iteration: timebox, action: :add, created_at: start_date + 9.days)
end
it_behaves_like 'timebox time series'
end
end
......@@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Iteration'] do
it 'has the expected fields' do
expected_fields = %w[
id id title description state web_path web_url scoped_path scoped_url
due_date start_date created_at updated_at burnup_time_series
due_date start_date created_at updated_at report burnup_time_series
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['TimeboxReport'] do
it { expect(described_class.graphql_name).to eq('TimeboxReport') }
it { expect(described_class).to have_graphql_field(:burnup_time_series) }
end
......@@ -31,7 +31,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
end
it 'returns an error when the number of events exceeds the limit' do
stub_const('TimeboxBurnupChartService::EVENT_COUNT_LIMIT', 1)
stub_const('TimeboxReportService::EVENT_COUNT_LIMIT', 1)
create(:"resource_#{timebox_type}_event", issue: issues[0], "#{timebox_type}" => timebox, action: :add, created_at: timebox_start_date - 21.days)
create(:"resource_#{timebox_type}_event", issue: issues[1], "#{timebox_type}" => timebox, action: :add, created_at: timebox_start_date - 20.days)
......@@ -56,7 +56,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
create(:resource_state_event, issue: issues[3], state: :closed, created_at: timebox_start_date - 6.days)
expect(response.success?).to eq(true)
expect(response.payload).to eq([
expect(response.payload[:burnup_time_series]).to eq([
{
date: timebox_start_date,
scope_count: 4,
......@@ -98,7 +98,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
create(:"resource_#{timebox_type}_event", issue: issues[0], "#{timebox_type}" => timebox, action: :remove, created_at: timebox_start_date + 21.days)
expect(response.success?).to eq(true)
expect(response.payload).to eq([
expect(response.payload[:burnup_time_series]).to eq([
{
date: timebox_start_date + 4.days,
scope_count: 2,
......@@ -159,7 +159,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
create(:resource_state_event, issue: issues[1], state: :closed, created_at: timebox_start_date + 9.days)
expect(response.success?).to eq(true)
expect(response.payload).to eq([
expect(response.payload[:burnup_time_series]).to eq([
{
date: timebox_start_date,
scope_count: 1,
......@@ -230,7 +230,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
create(:resource_weight_event, issue: issues[0], weight: 10, created_at: timebox_start_date + 5.days)
expect(response.success?).to eq(true)
expect(response.payload).to eq([
expect(response.payload[:burnup_time_series]).to eq([
{
date: timebox_start_date,
scope_count: 1,
......@@ -271,7 +271,7 @@ RSpec.shared_examples 'timebox chart' do |timebox_type|
end
end
RSpec.describe TimeboxBurnupChartService do
RSpec.describe TimeboxReportService do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:timebox_start_date) { Date.today }
......
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