...@@ -122,9 +122,14 @@ export default { ...@@ -122,9 +122,14 @@ export default {
this.$store.subscribeAction({ this.$store.subscribeAction({
after: this.handleVuexActionDispatch, after: this.handleVuexActionDispatch,
}); });
document.addEventListener('click', this.handleDocumentClick);
},
beforeDestroy() {
document.removeEventListener('click', this.handleDocumentClick);
}, },
methods: { methods: {
...mapActions(['setInitialState']), ...mapActions(['setInitialState', 'toggleDropdownContents']),
/** /**
* This method differentiates between * This method differentiates between
* dispatched actions and calls necessary method. * dispatched actions and calls necessary method.
...@@ -138,6 +143,22 @@ export default { ...@@ -138,6 +143,22 @@ export default {
this.handleDropdownClose(state.labels.filter(label => label.touched)); this.handleDropdownClose(state.labels.filter(label => label.touched));
} }
}, },
/**
* This method listens for document-wide click event
* and toggle dropdown if user clicks anywhere outside
* the dropdown while dropdown is visible.
*/
handleDocumentClick({ target }) {
if (
this.showDropdownButton &&
this.showDropdownContents &&
!target?.classList.contains('js-sidebar-dropdown-toggle') &&
!this.$refs.dropdownButtonCollapsed?.$el.contains(target) &&
!this.$refs.dropdownContents?.$el.contains(target)
) {
this.toggleDropdownContents();
}
},
handleDropdownClose(labels) { handleDropdownClose(labels) {
// Only emit label updates if there are any labels to update // Only emit label updates if there are any labels to update
// on UI. // on UI.
...@@ -156,6 +177,7 @@ export default { ...@@ -156,6 +177,7 @@ export default {
<div v-if="!dropdownOnly"> <div v-if="!dropdownOnly">
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="allowLabelCreate" v-if="allowLabelCreate"
ref="dropdownButtonCollapsed"
:labels="selectedLabels" :labels="selectedLabels"
@onValueClick="handleCollapsedValueClick" @onValueClick="handleCollapsedValueClick"
/> />
...@@ -167,7 +189,7 @@ export default { ...@@ -167,7 +189,7 @@ export default {
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-button v-show="showDropdownButton" /> <dropdown-button v-show="showDropdownButton" />
<dropdown-contents v-if="showDropdownButton && showDropdownContents" /> <dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" />
</div> </div>
</div> </div>
</template> </template>
...@@ -413,6 +413,7 @@ img.emoji { ...@@ -413,6 +413,7 @@ img.emoji {
.prepend-left-20 { margin-left: 20px; } .prepend-left-20 { margin-left: 20px; }
.prepend-left-32 { margin-left: 32px; } .prepend-left-32 { margin-left: 32px; }
.prepend-left-64 { margin-left: 64px; } .prepend-left-64 { margin-left: 64px; }
.append-right-2 { margin-right: 2px; }
.append-right-4 { margin-right: 4px; } .append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; } .append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; } .append-right-8 { margin-right: 8px; }
...@@ -424,6 +425,7 @@ img.emoji { ...@@ -424,6 +425,7 @@ img.emoji {
.append-right-48 { margin-right: 48px; } .append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; } .prepend-right-32 { margin-right: 32px; }
.append-bottom-0 { margin-bottom: 0; } .append-bottom-0 { margin-bottom: 0; }
.append-bottom-2 { margin-bottom: 2px; }
.append-bottom-4 { margin-bottom: $gl-padding-4; } .append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; } .append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; } .append-bottom-8 { margin-bottom: $grid-size; }
... ...
......
...@@ -33,7 +33,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -33,7 +33,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def starred def starred
@projects = load_projects(params.merge(starred: true)) @projects = load_projects(params.merge(starred: true))
.includes(:forked_from_project, :tags).page(params[:page]) .includes(:forked_from_project, :tags)
@groups = [] @groups = []
...@@ -51,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -51,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private private
def projects def projects
@projects ||= load_projects(params.merge(non_public: true)).page(params[:page]) @projects ||= load_projects(params.merge(non_public: true))
end end
def render_projects def render_projects
...@@ -73,6 +73,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -73,6 +73,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.execute .execute
.includes(:route, :creator, :group, namespace: [:route, :owner]) .includes(:route, :creator, :group, namespace: [:route, :owner])
.preload(:project_feature) .preload(:project_feature)
.page(finder_params[:page])
prepare_projects_for_rendering(projects) prepare_projects_for_rendering(projects)
end end
... ...
......
...@@ -67,6 +67,7 @@ class Issue < ApplicationRecord ...@@ -67,6 +67,7 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) } scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) } scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) } scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :preload_associated_models, -> { preload(:labels, project: :namespace) } scope :preload_associated_models, -> { preload(:labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
... ...
......
...@@ -32,9 +32,12 @@ class Service < ApplicationRecord ...@@ -32,9 +32,12 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services belongs_to :project, inverse_of: :services
has_one :service_hook has_one :service_hook
validates :project_id, presence: true, unless: -> { template? } validates :project_id, presence: true, unless: -> { template? || instance? }
validates :project_id, absence: true, if: -> { instance? }
validates :type, presence: true validates :type, presence: true
validates :template, uniqueness: { scope: :type }, if: -> { template? } validates :template, uniqueness: { scope: :type }, if: -> { template? }
validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
validate :validate_is_instance_or_template
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
...@@ -326,6 +329,10 @@ class Service < ApplicationRecord ...@@ -326,6 +329,10 @@ class Service < ApplicationRecord
private private
def validate_is_instance_or_template
errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
end
def cache_project_has_external_issue_tracker def cache_project_has_external_issue_tracker
if project && !project.destroyed? if project && !project.destroyed?
project.cache_has_external_issue_tracker project.cache_has_external_issue_tracker
... ...
......
...@@ -10,6 +10,8 @@ module Boards ...@@ -10,6 +10,8 @@ module Boards
end end
def execute def execute
return fetch_issues.order_closed_date_desc if list&.closed?
fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?) fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?)
end end
... ...
......
...@@ -138,7 +138,9 @@ module Metrics ...@@ -138,7 +138,9 @@ module Metrics
end end
# Identifies the name of the datasource for a dashboard # Identifies the name of the datasource for a dashboard
# based on the panelId query parameter found in the url # based on the panelId query parameter found in the url.
#
# If no panel is specified, defaults to the first valid panel.
class DatasourceNameParser class DatasourceNameParser
def initialize(grafana_url, grafana_dashboard) def initialize(grafana_url, grafana_dashboard)
@grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard @grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
...@@ -146,15 +148,29 @@ module Metrics ...@@ -146,15 +148,29 @@ module Metrics
def parse def parse
@grafana_dashboard[:dashboard][:panels] @grafana_dashboard[:dashboard][:panels]
.find { |panel| panel[:id].to_s == query_params[:panelId] } .find { |panel| panel_id ? matching_panel?(panel) : valid_panel?(panel) }
.try(:[], :datasource) .try(:[], :datasource)
end end
private private
def panel_id
query_params[:panelId]
end
def query_params def query_params
Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url) Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
end end
def matching_panel?(panel)
panel[:id].to_s == panel_id
end
def valid_panel?(panel)
::Grafana::Validator
.new(@grafana_dashboard, nil, panel, query_params)
.valid?
end
end end
end end
end end
---
title: Sort closed issues on issue boards using time of closing
merge_request: 23442
author: briankabiro
type: changed
---
title: Add instance column to services table
merge_request: 25714
author:
type: other
---
title: Put System Metrics chart group first in default dashboard
merge_request: 26355
author:
type: other
---
title: Optimize Project counters with respository enabled counter
merge_request: 26698
author:
type: performance
---
title: Fix missing RSS feed events
merge_request: 19524
author:
type: fixed
---
title: Default to first valid panel in unspecified Grafana embeds
merge_request: 21932
author:
type: changed
---
title: Update cluster-applications image to v0.11 with a runner bugfix, updated cert-manager,
and vault as a new app
merge_request: 26842
author:
type: changed
dashboard: 'Environment metrics' dashboard: 'Environment metrics'
priority: 1 priority: 1
panel_groups: panel_groups:
- group: System metrics (Kubernetes)
priority: 15
panels:
- title: "Memory Usage (Total)"
type: "area-chart"
y_label: "Total Memory Used (GB)"
weight: 4
metrics:
- id: system_metrics_kubernetes_container_memory_total
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024'
label: Total (GB)
unit: GB
- title: "Core Usage (Total)"
type: "area-chart"
y_label: "Total Cores"
weight: 3
metrics:
- id: system_metrics_kubernetes_container_cores_total
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average_canary
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage_canary
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
track: canary
- title: "Knative function invocations"
type: "area-chart"
y_label: "Invocations"
weight: 1
metrics:
- id: system_metrics_knative_function_invocation_count
query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))'
label: invocations / minute
unit: requests
# NGINX Ingress metrics for pre-0.16.0 versions # NGINX Ingress metrics for pre-0.16.0 versions
- group: Response metrics (NGINX Ingress VTS) - group: Response metrics (NGINX Ingress VTS)
priority: 10 priority: 10
...@@ -150,79 +218,3 @@ panel_groups: ...@@ -150,79 +218,3 @@ panel_groups:
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))' query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors label: HTTP Errors
unit: "errors / sec" unit: "errors / sec"
- group: System metrics (Kubernetes)
priority: 5
panels:
- title: "Memory Usage (Total)"
type: "area-chart"
y_label: "Total Memory Used (GB)"
y_axis:
format: "gibibytes"
weight: 4
metrics:
- id: system_metrics_kubernetes_container_memory_total
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024'
label: Total (GB)
unit: GB
- title: "Core Usage (Total)"
type: "area-chart"
y_label: "Total Cores"
weight: 3
metrics:
- id: system_metrics_kubernetes_container_cores_total
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
y_axis:
format: "mebibytes"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
y_axis:
format: "mebibytes"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average_canary
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage_canary
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
track: canary
- title: "Knative function invocations"
type: "area-chart"
y_label: "Invocations"
y_axis:
precision: 0
weight: 1
metrics:
- id: system_metrics_knative_function_invocation_count
query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))'
label: invocations / minute
unit: requests
# frozen_string_literal: true
class AddIndexOnProjectIdAndRepositoryAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_project_features_on_project_id_ral_20'
disable_ddl_transaction!
def up
add_concurrent_index :project_features, :project_id, where: 'repository_access_level = 20', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :project_features, INDEX_NAME
end
end
# frozen_string_literal: true
class AddInstanceToServices < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :instance, :boolean, default: false)
end
def down
remove_column(:services, :instance)
end
end
# frozen_string_literal: true
class AddIndexToServiceUniqueInstancePerType < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:services, [:type, :instance], unique: true, where: 'instance IS TRUE')
end
def down
remove_concurrent_index(:services, [:type, :instance])
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_03_09_195710) do ActiveRecord::Schema.define(version: 2020_03_10_135823) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -3265,6 +3265,7 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do ...@@ -3265,6 +3265,7 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do
t.integer "forking_access_level" t.integer "forking_access_level"
t.index ["project_id"], name: "index_project_features_on_project_id", unique: true t.index ["project_id"], name: "index_project_features_on_project_id", unique: true
t.index ["project_id"], name: "index_project_features_on_project_id_bal_20", where: "(builds_access_level = 20)" t.index ["project_id"], name: "index_project_features_on_project_id_bal_20", where: "(builds_access_level = 20)"
t.index ["project_id"], name: "index_project_features_on_project_id_ral_20", where: "(repository_access_level = 20)"
end end
create_table "project_group_links", id: :serial, force: :cascade do |t| create_table "project_group_links", id: :serial, force: :cascade do |t|
...@@ -3939,8 +3940,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do ...@@ -3939,8 +3940,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do
t.string "description", limit: 500 t.string "description", limit: 500
t.boolean "comment_on_event_enabled", default: true, null: false t.boolean "comment_on_event_enabled", default: true, null: false
t.boolean "template", default: false t.boolean "template", default: false
t.boolean "instance", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id" t.index ["project_id"], name: "index_services_on_project_id"
t.index ["template"], name: "index_services_on_template" t.index ["template"], name: "index_services_on_template"
t.index ["type", "instance"], name: "index_services_on_type_and_instance", unique: true, where: "(instance IS TRUE)"
t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)" t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)"
t.index ["type"], name: "index_services_on_type" t.index ["type"], name: "index_services_on_type"
end end
... ...
......
...@@ -2023,6 +2023,11 @@ type Epic implements Noteable { ...@@ -2023,6 +2023,11 @@ type Epic implements Noteable {
""" """
hasIssues: Boolean! hasIssues: Boolean!
"""
Current health status of the epic
"""
healthStatus: EpicHealthStatus
""" """
ID of the epic ID of the epic
""" """
...@@ -2349,6 +2354,26 @@ type EpicEdge { ...@@ -2349,6 +2354,26 @@ type EpicEdge {
node: Epic node: Epic
} }
"""
Health status of child issues
"""
type EpicHealthStatus {
"""
Number of issues at risk
"""
issuesAtRisk: Int
"""
Number of issues that need attention
"""
issuesNeedingAttention: Int
"""
Number of issues on track
"""
issuesOnTrack: Int
}
""" """
Relationship between an epic and an issue Relationship between an epic and an issue
""" """
... ...
......