Commit a88493f3 authored by charlie ablett's avatar charlie ablett

Merge branch 'jira-import-graphql-api' into 'master'

Add graphql api for querying jira imports and jira import status

See merge request gitlab-org/gitlab!27587
parents dc178732 6eb27f46
# frozen_string_literal: true
module Resolvers
module Projects
class JiraImportsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
alias_method :project, :object
def resolve(**args)
return JiraImportData.none unless project&.import_data.present?
authorize!(project)
project.import_data.becomes(JiraImportData).projects
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :admin_project, project)
end
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is at project level for owners or admins,
# so it is added directly to the Resolvers::JiraImportsResolver
class JiraImportType < BaseObject
graphql_name 'JiraImport'
field :scheduled_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created/started'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
description: 'Project key for the imported Jira project',
method: :key
def scheduled_at
DateTime.parse(object.scheduled_at)
end
def scheduled_by
::Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.scheduled_by['user_id']).find
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
...@@ -90,8 +90,9 @@ module Types ...@@ -90,8 +90,9 @@ module Types
end end
field :import_status, GraphQL::STRING_TYPE, null: true, field :import_status, GraphQL::STRING_TYPE, null: true,
description: 'Status of project import background job of the project' description: 'Status of import background job of the project'
field :jira_import_status, GraphQL::STRING_TYPE, null: true,
description: 'Status of Jira import background job of the project'
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true, field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if merge requests of the project can only be merged with successful jobs' description: 'Indicates if merge requests of the project can only be merged with successful jobs'
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true, field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
...@@ -192,6 +193,12 @@ module Types ...@@ -192,6 +193,12 @@ module Types
null: true, null: true,
description: 'A single board of the project', description: 'A single board of the project',
resolver: Resolvers::BoardsResolver.single resolver: Resolvers::BoardsResolver.single
field :jira_imports,
Types::JiraImportType.connection_type,
null: true,
description: 'Jira imports into the project',
resolver: Resolvers::Projects::JiraImportsResolver
end end
end end
......
...@@ -857,6 +857,12 @@ class Project < ApplicationRecord ...@@ -857,6 +857,12 @@ class Project < ApplicationRecord
import_state&.status || 'none' import_state&.status || 'none'
end end
def jira_import_status
return import_status if jira_force_import?
import_data&.becomes(JiraImportData)&.projects.blank? ? 'none' : 'finished'
end
def human_import_status_name def human_import_status_name
import_state&.human_status_name || 'none' import_state&.human_status_name || 'none'
end end
......
---
title: Allow querying of Jira imports and their status via GraphQL
merge_request: 27587
author:
type: added
...@@ -4093,6 +4093,58 @@ Represents untyped JSON ...@@ -4093,6 +4093,58 @@ Represents untyped JSON
""" """
scalar JSON scalar JSON
type JiraImport {
"""
Project key for the imported Jira project
"""
jiraProjectKey: String!
"""
Timestamp of when the Jira import was created/started
"""
scheduledAt: Time
"""
User that started the Jira import
"""
scheduledBy: User
}
"""
The connection type for JiraImport.
"""
type JiraImportConnection {
"""
A list of edges.
"""
edges: [JiraImportEdge]
"""
A list of nodes.
"""
nodes: [JiraImport]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type JiraImportEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: JiraImport
}
type Label { type Label {
""" """
Background color of the label Background color of the label
...@@ -5749,7 +5801,7 @@ type Project { ...@@ -5749,7 +5801,7 @@ type Project {
id: ID! id: ID!
""" """
Status of project import background job of the project Status of import background job of the project
""" """
importStatus: String importStatus: String
...@@ -5938,6 +5990,36 @@ type Project { ...@@ -5938,6 +5990,36 @@ type Project {
""" """
issuesEnabled: Boolean issuesEnabled: Boolean
"""
Status of Jira import background job of the project
"""
jiraImportStatus: String
"""
Jira imports into the project
"""
jiraImports(
"""
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
): JiraImportConnection
""" """
(deprecated) Enable jobs for this project. Use `builds_access_level` instead (deprecated) Enable jobs for this project. Use `builds_access_level` instead
""" """
......
...@@ -11611,6 +11611,177 @@ ...@@ -11611,6 +11611,177 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "JiraImport",
"description": null,
"fields": [
{
"name": "jiraProjectKey",
"description": "Project key for the imported Jira project",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheduledAt",
"description": "Timestamp of when the Jira import was created/started",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scheduledBy",
"description": "User that started the Jira import",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraImportConnection",
"description": "The connection type for JiraImport.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "JiraImportEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "JiraImport",
"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": "JiraImportEdge",
"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": "JiraImport",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Label", "name": "Label",
...@@ -17441,7 +17612,7 @@ ...@@ -17441,7 +17612,7 @@
}, },
{ {
"name": "importStatus", "name": "importStatus",
"description": "Status of project import background job of the project", "description": "Status of import background job of the project",
"args": [ "args": [
], ],
...@@ -17865,6 +18036,73 @@ ...@@ -17865,6 +18036,73 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "jiraImportStatus",
"description": "Status of Jira import background job of the project",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraImports",
"description": "Jira imports into the project",
"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": "JiraImportConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "jobsEnabled", "name": "jobsEnabled",
"description": "(deprecated) Enable jobs for this project. Use `builds_access_level` instead", "description": "(deprecated) Enable jobs for this project. Use `builds_access_level` instead",
......
...@@ -605,6 +605,14 @@ Autogenerated return type of IssueSetWeight ...@@ -605,6 +605,14 @@ Autogenerated return type of IssueSetWeight
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `issue` | Issue | The issue after mutation | | `issue` | Issue | The issue after mutation |
## JiraImport
| Name | Type | Description |
| --- | ---- | ---------- |
| `jiraProjectKey` | String! | Project key for the imported Jira project |
| `scheduledAt` | Time | Timestamp of when the Jira import was created/started |
| `scheduledBy` | User | User that started the Jira import |
## Label ## Label
| Name | Type | Description | | Name | Type | Description |
...@@ -879,9 +887,10 @@ Information about pagination in a connection. ...@@ -879,9 +887,10 @@ Information about pagination in a connection.
| `group` | Group | Group of the project | | `group` | Group | Group of the project |
| `httpUrlToRepo` | String | URL to connect to the project via HTTPS | | `httpUrlToRepo` | String | URL to connect to the project via HTTPS |
| `id` | ID! | ID of the project | | `id` | ID! | ID of the project |
| `importStatus` | String | Status of project import background job of the project | | `importStatus` | String | Status of import background job of the project |
| `issue` | Issue | A single issue of the project | | `issue` | Issue | A single issue of the project |
| `issuesEnabled` | Boolean | (deprecated) Does this project have issues enabled?. Use `issues_access_level` instead | | `issuesEnabled` | Boolean | (deprecated) Does this project have issues enabled?. Use `issues_access_level` instead |
| `jiraImportStatus` | String | Status of Jira import background job of the project |
| `jobsEnabled` | Boolean | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | | `jobsEnabled` | Boolean | (deprecated) Enable jobs for this project. Use `builds_access_level` instead |
| `lastActivityAt` | Time | Timestamp of the project last activity | | `lastActivityAt` | Time | Timestamp of the project last activity |
| `lfsEnabled` | Boolean | Indicates if the project has Large File Storage (LFS) enabled | | `lfsEnabled` | Boolean | Indicates if the project has Large File Storage (LFS) enabled |
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::Projects::JiraImportsResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:jira_import_data) do
data = JiraImportData.new
data << JiraImportData::JiraProjectDetails.new('AA', 2.days.ago.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name })
data << JiraImportData::JiraProjectDetails.new('BB', 5.days.ago.strftime('%Y-%m-%d %H:%M:%S'), { user_id: user.id, name: user.name })
data
end
context 'when project does not have Jira import data' do
let_it_be(:project) { create(:project, :private, import_data: nil) }
context 'when user cannot read Jira import data' do
context 'when anonymous user' do
it_behaves_like 'no jira import data present'
end
context 'when user developer' do
before do
project.add_developer(user)
end
it_behaves_like 'no jira import data present'
end
end
context 'when user can read Jira import data' do
before do
project.add_maintainer(user)
end
it_behaves_like 'no jira import data present'
end
end
context 'when project has Jira import data' do
let_it_be(:project) { create(:project, :private, import_data: jira_import_data) }
context 'when user cannot read Jira import data' do
context 'when anonymous user' do
it_behaves_like 'no jira import access'
end
context 'when user developer' do
before do
project.add_developer(user)
end
it_behaves_like 'no jira import access'
end
end
context 'when user can access Jira import data' do
before do
project.add_maintainer(user)
end
it 'returns Jira imports sorted ascending by scheduledAt time' do
imports = resolve_imports
expect(imports.size).to eq 2
expect(imports.map(&:key)).to eq %w(BB AA)
end
end
end
end
def resolve_imports(args = {}, context = { current_user: user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['JiraImport'] do
it { expect(described_class.graphql_name).to eq('JiraImport') }
it 'has the expected fields' do
expect(described_class).to have_graphql_fields(:jira_project_key, :scheduled_at, :scheduled_by)
end
end
...@@ -24,7 +24,7 @@ describe GitlabSchema.types['Project'] do ...@@ -24,7 +24,7 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards boards jira_import_status jira_imports
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -2282,6 +2282,44 @@ describe Project do ...@@ -2282,6 +2282,44 @@ describe Project do
end end
end end
describe '#jira_import_status' do
let(:project) { create(:project, :import_started, import_type: 'jira') }
context 'when import_data is nil' do
it 'returns none' do
expect(project.import_data).to be nil
expect(project.jira_import_status).to eq('none')
end
end
context 'when import_data is set' do
let(:jira_import_data) { JiraImportData.new }
let(:project) { create(:project, :import_started, import_data: jira_import_data, import_type: 'jira') }
it 'returns none' do
expect(project.import_data.becomes(JiraImportData).force_import?).to be false
expect(project.jira_import_status).to eq('none')
end
context 'when jira_force_import is true' do
let(:imported_jira_project) do
JiraImportData::JiraProjectDetails.new('xx', Time.now.strftime('%Y-%m-%d %H:%M:%S'), { user_id: 1, name: 'root' })
end
before do
jira_import_data = project.import_data.becomes(JiraImportData)
jira_import_data << imported_jira_project
jira_import_data.force_import!
end
it 'returns started' do
expect(project.import_data.becomes(JiraImportData).force_import?).to be true
expect(project.jira_import_status).to eq('started')
end
end
end
end
describe '#human_import_status_name' do describe '#human_import_status_name' do
context 'with import_state' do context 'with import_state' do
it 'returns the right human import status' do it 'returns the right human import status' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'query jira import data' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:jira_import_data) do
data = JiraImportData.new
data << JiraImportData::JiraProjectDetails.new(
'AA', 2.days.ago.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: current_user.id, name: current_user.name }
)
data << JiraImportData::JiraProjectDetails.new(
'BB', 5.days.ago.strftime('%Y-%m-%d %H:%M:%S'),
{ user_id: current_user.id, name: current_user.name }
)
data
end
let_it_be(:project) { create(:project, :private, :import_started, import_data: jira_import_data, import_type: 'jira') }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
jiraImportStatus
jiraImports {
nodes {
jiraProjectKey
scheduledAt
scheduledBy {
username
}
}
}
}
}
)
end
let(:jira_imports) { graphql_data.dig('project', 'jiraImports', 'nodes')}
let(:jira_import_status) { graphql_data.dig('project', 'jiraImportStatus')}
context 'when user cannot read Jira import data' do
before do
post_graphql(query, current_user: current_user)
end
context 'when anonymous user' do
let(:current_user) { nil }
it { expect(jira_imports).to be nil }
end
context 'when user developer' do
before do
project.add_developer(current_user)
end
it { expect(jira_imports).to be nil }
end
end
context 'when user can access Jira import data' do
before do
project.add_maintainer(current_user)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
context 'list of jira imports sorted ascending by scheduledAt time' do
it 'retuns list of jira imports' do
jira_proket_keys = jira_imports.map {|ji| ji['jiraProjectKey']}
usernames = jira_imports.map {|ji| ji.dig('scheduledBy', 'username')}
expect(jira_imports.size).to eq 2
expect(jira_proket_keys).to eq %w(BB AA)
expect(usernames).to eq [current_user.username, current_user.username]
end
end
describe 'jira imports pagination' do
context 'first jira import' do
let(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
jiraImports(first: 1) {
nodes {
jiraProjectKey
scheduledBy {
username
}
}
}
}
}
)
end
it 'returns latest jira import data' do
first_jira_import = jira_imports.first
expect(first_jira_import['jiraProjectKey']).to eq 'BB'
expect(first_jira_import.dig('scheduledBy', 'username')).to eq current_user.username
end
end
context 'lastest jira import' do
let(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
jiraImports(last: 1) {
nodes {
jiraProjectKey
scheduledBy {
username
}
}
}
}
}
)
end
it 'returns latest jira import data' do
latest_jira_import = jira_imports.first
expect(latest_jira_import['jiraProjectKey']).to eq 'AA'
expect(latest_jira_import.dig('scheduledBy', 'username')).to eq current_user.username
end
end
end
end
context 'jira import status' do
context 'when user cannot access project' do
it 'does not return import status' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).to be nil
end
end
context 'when user can access project' do
before do
project.add_guest(current_user)
end
context 'when import never ran' do
let(:project) { create(:project) }
it 'returns import status' do
post_graphql(query, current_user: current_user)
expect(jira_import_status).to eq('none')
end
end
context 'when import finished' do
it 'returns import status' do
post_graphql(query, current_user: current_user)
expect(jira_import_status).to eq('finished')
end
end
context 'when import running, i.e. force-import: true' do
before do
project.import_data.becomes(JiraImportData).force_import!
project.save!
end
it 'returns import status' do
post_graphql(query, current_user: current_user)
expect(jira_import_status).to eq('started')
end
end
end
end
end
# frozen_string_literal: true
shared_examples 'no jira import data present' do
it 'returns none' do
expect(resolve_imports).to eq JiraImportData.none
end
end
shared_examples 'no jira import access' do
it 'raises error' do
expect do
resolve_imports
end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
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