Commit 93c1b5da authored by Adam Hegyi's avatar Adam Hegyi

Expose Devops Adoption segments via GraphQL

This change exposes Devops Adoption segments via Graphql.
parent f1bdac24
......@@ -4,10 +4,16 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
ALLOWED_SEGMENT_COUNT = 20
has_many :segment_selections
has_many :groups, through: :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validate :validate_segment_count
accepts_nested_attributes_for :segment_selections, allow_destroy: true
scope :ordered_by_name, -> { order(:name) }
scope :with_groups, -> { preload(:groups) }
private
def validate_segment_count
......
......@@ -6025,6 +6025,81 @@ type DetailedStatus {
tooltip: String
}
"""
Segment
"""
type DevopsAdoptionSegment {
"""
Assigned groups
"""
groups(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): GroupConnection
"""
ID of the segment
"""
id: ID!
"""
Name of the segment
"""
name: String!
}
"""
The connection type for DevopsAdoptionSegment.
"""
type DevopsAdoptionSegmentConnection {
"""
A list of edges.
"""
edges: [DevopsAdoptionSegmentEdge]
"""
A list of nodes.
"""
nodes: [DevopsAdoptionSegment]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type DevopsAdoptionSegmentEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DevopsAdoptionSegment
}
input DiffImagePositionInput {
"""
Merge base of the branch the comment was made on
......@@ -9165,6 +9240,41 @@ type Group {
webUrl: String!
}
"""
The connection type for Group.
"""
type GroupConnection {
"""
A list of edges.
"""
edges: [GroupEdge]
"""
A list of nodes.
"""
nodes: [Group]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type GroupEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Group
}
"""
Identifier of Group
"""
......@@ -16540,6 +16650,31 @@ type Query {
"""
designManagement: DesignManagement!
"""
Get configured DevOps adoption segments on the instance
"""
devopsAdoptionSegments(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DevopsAdoptionSegmentConnection
"""
Text to echo back
"""
......
......@@ -16527,6 +16527,220 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSegment",
"description": "Segment",
"fields": [
{
"name": "groups",
"description": "Assigned groups",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "GroupConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the segment",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the segment",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentConnection",
"description": "The connection type for DevopsAdoptionSegment.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegment",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DiffImagePositionInput",
......@@ -24901,6 +25115,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupConnection",
"description": "The connection type for Group.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GroupEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "GroupID",
......@@ -48034,6 +48360,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "devopsAdoptionSegments",
"description": "Get configured DevOps adoption segments on the instance",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "echo",
"description": "Text to echo back",
......@@ -1000,6 +1000,16 @@ Autogenerated return type of DestroySnippet.
| `text` | String | Text of the status |
| `tooltip` | String | Tooltip associated with the status |
### DevopsAdoptionSegment
Segment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `groups` | GroupConnection | Assigned groups |
| `id` | ID! | ID of the segment |
| `name` | String! | Name of the segment |
### DiffPosition
| Field | Type | Description |
......
......@@ -51,6 +51,11 @@ module EE
null: true,
resolver: ::Resolvers::InstanceSecurityDashboardResolver,
description: 'Fields related to Instance Security Dashboard'
field :devops_adoption_segments, ::Types::Admin::Analytics::DevopsAdoption::SegmentType.connection_type,
null: true,
description: 'Get configured DevOps adoption segments on the instance',
resolver: ::Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver
end
def vulnerability(id:)
......
# frozen_string_literal: true
module Resolvers
module Admin
module Analytics
module DevopsAdoption
class SegmentsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Admin::Analytics::DevopsAdoption::SegmentType, null: true
def resolve
authorize!
if segments_feature_available?
::Analytics::DevopsAdoption::Segment.with_groups.ordered_by_name
else
::Analytics::DevopsAdoption::Segment.none
end
end
private
def segments_feature_available?
License.feature_available?(:instance_level_devops_adoption)
end
def authorize!
admin? || raise_resource_not_available_error!
end
def admin?
context[:current_user].present? && context[:current_user].admin?
end
end
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Graphql/AuthorizeTypes
module Types
module Admin
module Analytics
module DevopsAdoption
class SegmentType < BaseObject
graphql_name 'DevopsAdoptionSegment'
description 'Segment'
field :id, GraphQL::ID_TYPE, null: false,
description: "ID of the segment"
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Name of the segment'
field :groups, Types::GroupType.connection_type, null: true,
description: 'Assigned groups'
end
end
end
end
end
......@@ -19,6 +19,7 @@ class License < ApplicationRecord
group_activity_analytics
group_bulk_edit
group_webhooks
instance_level_devops_adoption
issuable_default_templates
issue_weights
iterations
......
---
title: Expose Devops Adoption segments via GraphQL
merge_request: 46879
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Admin::Analytics::DevopsAdoption::SegmentsResolver do
include GraphqlHelpers
let_it_be(:admin_user) { create(:user, :admin) }
let(:current_user) { admin_user }
def resolve_segments(args = {}, context = {})
resolve(described_class, args: args, ctx: context)
end
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:segment_1) { create(:devops_adoption_segment, name: 'bbb') }
let_it_be(:segment_2) { create(:devops_adoption_segment, name: 'aaa') }
subject { resolve_segments({}, { current_user: current_user }) }
before do
stub_licensed_features(instance_level_devops_adoption: true)
end
context 'when requesting project count measurements' do
context 'as an admin user' do
let(:current_user) { admin_user }
it 'returns the records, ordered by name' do
expect(subject).to eq([segment_2, segment_1])
end
end
context 'when the feature is not available' do
let(:current_user) { admin_user }
before do
stub_licensed_features(instance_level_devops_adoption: false)
end
it 'returns the records, ordered by name' do
expect(subject).to be_empty
end
end
context 'as a non-admin user' do
let(:current_user) { user }
it 'raises ResourceNotAvailable error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'as an unauthenticated user' do
let(:current_user) { nil }
it 'raises ResourceNotAvailable error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
......@@ -26,4 +26,15 @@ RSpec.describe Analytics::DevopsAdoption::Segment, type: :model do
end
end
end
describe '.ordered_by_name' do
let(:segment_1) { create(:devops_adoption_segment, name: 'bbb') }
let(:segment_2) { create(:devops_adoption_segment, name: 'aaa') }
subject { described_class.ordered_by_name }
it 'orders segments by name' do
expect(subject).to eq([segment_2, segment_1])
end
end
end
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