diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /app/graphql | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'app/graphql')
66 files changed, 1730 insertions, 30 deletions
diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb new file mode 100644 index 00000000000..ca2057d4845 --- /dev/null +++ b/app/graphql/mutations/alert_management/base.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + class Base < BaseMutation + include Mutations::ResolvesProject + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project the alert to mutate is in" + + argument :iid, GraphQL::STRING_TYPE, + required: true, + description: "The iid of the alert to mutate" + + field :alert, + Types::AlertManagement::AlertType, + null: true, + description: "The alert after mutation" + + field :issue, + Types::IssueType, + null: true, + description: "The issue created after mutation" + + authorize :update_alert_management_alert + + private + + def find_object(project_path:, iid:) + project = resolve_project(full_path: project_path) + + return unless project + + resolver = Resolvers::AlertManagementAlertResolver.single.new(object: project, context: context, field: nil) + resolver.resolve(iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb new file mode 100644 index 00000000000..adb048a4479 --- /dev/null +++ b/app/graphql/mutations/alert_management/create_alert_issue.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + class CreateAlertIssue < Base + graphql_name 'CreateAlertIssue' + + def resolve(args) + alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + result = create_alert_issue(alert, current_user) + + prepare_response(alert, result) + end + + private + + def create_alert_issue(alert, user) + ::AlertManagement::CreateAlertIssueService.new(alert, user).execute + end + + def prepare_response(alert, result) + { + alert: alert, + issue: result.payload[:issue], + errors: Array(result.message) + } + end + end + end +end diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb new file mode 100644 index 00000000000..e73a591378a --- /dev/null +++ b/app/graphql/mutations/alert_management/update_alert_status.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Mutations + module AlertManagement + class UpdateAlertStatus < Base + graphql_name 'UpdateAlertStatus' + + argument :status, Types::AlertManagement::StatusEnum, + required: true, + description: 'The status to set the alert' + + def resolve(args) + alert = authorized_find!(project_path: args[:project_path], iid: args[:iid]) + result = update_status(alert, args[:status]) + + prepare_response(result) + end + + private + + def update_status(alert, status) + ::AlertManagement::UpdateAlertStatusService + .new(alert, current_user, status) + .execute + end + + def prepare_response(result) + { + alert: result.payload[:alert], + errors: result.error? ? [result.message] : [] + } + end + end + end +end diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index 623f7c27584..30510cfab50 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -9,7 +9,7 @@ module Mutations field :errors, [GraphQL::STRING_TYPE], null: false, - description: "Reasons why the mutation failed." + description: "Errors encountered during execution of the mutation." def current_user context[:current_user] diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb new file mode 100644 index 00000000000..127d5447d0a --- /dev/null +++ b/app/graphql/mutations/branches/create.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Mutations + module Branches + class Create < BaseMutation + include Mutations::ResolvesProject + + graphql_name 'CreateBranch' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Project full path the branch is associated with' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the branch' + + argument :ref, + GraphQL::STRING_TYPE, + required: true, + description: 'Branch name or commit SHA to create branch from' + + field :branch, + Types::BranchType, + null: true, + description: 'Branch after mutation' + + authorize :push_code + + def resolve(project_path:, name:, ref:) + project = authorized_find!(full_path: project_path) + + context.scoped_set!(:branch_project, project) + + result = ::Branches::CreateService.new(project, current_user) + .execute(name, ref) + + { + branch: (result[:branch] if result[:status] == :success), + errors: Array.wrap(result[:message]) + } + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + end + end +end diff --git a/app/graphql/mutations/design_management/base.rb b/app/graphql/mutations/design_management/base.rb new file mode 100644 index 00000000000..918e5709b94 --- /dev/null +++ b/app/graphql/mutations/design_management/base.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Mutations + module DesignManagement + class Base < ::Mutations::BaseMutation + include Mutations::ResolvesIssuable + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: "The project where the issue is to upload designs for" + + argument :iid, GraphQL::ID_TYPE, + required: true, + description: "The iid of the issue to modify designs for" + + private + + def find_object(project_path:, iid:) + resolve_issuable(type: :issue, parent_path: project_path, iid: iid) + end + end + end +end diff --git a/app/graphql/mutations/design_management/delete.rb b/app/graphql/mutations/design_management/delete.rb new file mode 100644 index 00000000000..d2ef2c9bcca --- /dev/null +++ b/app/graphql/mutations/design_management/delete.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Mutations + module DesignManagement + class Delete < Base + Errors = ::Gitlab::Graphql::Errors + + graphql_name "DesignManagementDelete" + + argument :filenames, [GraphQL::STRING_TYPE], + required: true, + description: "The filenames of the designs to delete", + prepare: ->(names, _ctx) do + names.presence || (raise Errors::ArgumentError, 'no filenames') + end + + field :version, Types::DesignManagement::VersionType, + null: true, # null on error + description: 'The new version in which the designs are deleted' + + authorize :destroy_design + + def resolve(project_path:, iid:, filenames:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + designs = resolve_designs(issue, filenames) + + result = ::DesignManagement::DeleteDesignsService + .new(project, current_user, issue: issue, designs: designs) + .execute + + { + version: result[:version], + errors: Array.wrap(result[:message]) + } + end + + private + + # Here we check that: + # * we find exactly as many designs as filenames + def resolve_designs(issue, filenames) + designs = issue.design_collection.designs_by_filename(filenames) + + validate_all_were_found!(designs, filenames) + + designs + end + + def validate_all_were_found!(designs, filenames) + found_filenames = designs.map(&:filename) + missing = filenames.difference(found_filenames) + + if missing.present? + raise Errors::ArgumentError, <<~MSG + Not all the designs you named currently exist. + The following filenames were not found: + #{missing.join(', ')} + + They may have already been deleted. + MSG + end + end + end + end +end diff --git a/app/graphql/mutations/design_management/upload.rb b/app/graphql/mutations/design_management/upload.rb new file mode 100644 index 00000000000..1ed7f8e49e6 --- /dev/null +++ b/app/graphql/mutations/design_management/upload.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + module DesignManagement + class Upload < Base + graphql_name "DesignManagementUpload" + + argument :files, [ApolloUploadServer::Upload], + required: true, + description: "The files to upload" + + authorize :create_design + + field :designs, [Types::DesignManagement::DesignType], + null: false, + description: "The designs that were uploaded by the mutation" + + field :skipped_designs, [Types::DesignManagement::DesignType], + null: false, + description: "Any designs that were skipped from the upload due to there " \ + "being no change to their content since their last version" + + def resolve(project_path:, iid:, files:) + issue = authorized_find!(project_path: project_path, iid: iid) + project = issue.project + + result = ::DesignManagement::SaveDesignsService.new(project, current_user, issue: issue, files: files) + .execute + + { + designs: Array.wrap(result[:designs]), + skipped_designs: Array.wrap(result[:skipped_designs]), + errors: Array.wrap(result[:message]) + } + end + end + end +end diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb new file mode 100644 index 00000000000..f99688aeac6 --- /dev/null +++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Mutations + module Metrics + module Dashboard + module Annotations + class Create < BaseMutation + graphql_name 'CreateAnnotation' + + ANNOTATION_SOURCE_ARGUMENT_ERROR = 'Either a cluster or environment global id is required' + INVALID_ANNOTATION_SOURCE_ERROR = 'Invalid cluster or environment id' + + authorize :create_metrics_dashboard_annotation + + field :annotation, + Types::Metrics::Dashboards::AnnotationType, + null: true, + description: 'The created annotation' + + argument :environment_id, + GraphQL::ID_TYPE, + required: false, + description: 'The global id of the environment to add an annotation to' + + argument :cluster_id, + GraphQL::ID_TYPE, + required: false, + description: 'The global id of the cluster to add an annotation to' + + argument :starting_at, Types::TimeType, + required: true, + description: 'Timestamp indicating starting moment to which the annotation relates' + + argument :ending_at, Types::TimeType, + required: false, + description: 'Timestamp indicating ending moment to which the annotation relates' + + argument :dashboard_path, + GraphQL::STRING_TYPE, + required: true, + description: 'The path to a file defining the dashboard on which the annotation should be added' + + argument :description, + GraphQL::STRING_TYPE, + required: true, + description: 'The description of the annotation' + + AnnotationSource = Struct.new(:object, keyword_init: true) do + def type_keys + { 'Clusters::Cluster' => :cluster, 'Environment' => :environment } + end + + def klass + object.class.name + end + + def type + raise Gitlab::Graphql::Errors::ArgumentError, INVALID_ANNOTATION_SOURCE_ERROR unless type_keys[klass] + + type_keys[klass] + end + end + + def resolve(args) + annotation_response = ::Metrics::Dashboard::Annotations::CreateService.new(context[:current_user], annotation_create_params(args)).execute + + annotation = annotation_response[:annotation] + + { + annotation: annotation.valid? ? annotation : nil, + errors: errors_on_object(annotation) + } + end + + private + + def ready?(**args) + # Raise error if both cluster_id and environment_id are present or neither is present + unless args[:cluster_id].present? ^ args[:environment_id].present? + raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR + end + + super(args) + end + + def find_object(id:) + GitlabSchema.object_from_id(id) + end + + def annotation_create_params(args) + annotation_source = AnnotationSource.new(object: annotation_source(args)) + + args[annotation_source.type] = annotation_source.object + + args + end + + def annotation_source(args) + annotation_source_id = args[:cluster_id] || args[:environment_id] + authorized_find!(id: annotation_source_id) + end + end + end + end + end +end diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb index 9dc6d49774e..c8cc721b2e0 100644 --- a/app/graphql/mutations/snippets/base.rb +++ b/app/graphql/mutations/snippets/base.rb @@ -15,6 +15,8 @@ module Mutations end def authorized_resource?(snippet) + return false if snippet.nil? + Ability.allowed?(context[:current_user], ability_for(snippet), snippet) end diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb index 266a123de82..6fc223fbee7 100644 --- a/app/graphql/mutations/snippets/create.rb +++ b/app/graphql/mutations/snippets/create.rb @@ -36,6 +36,10 @@ module Mutations required: false, description: 'The project full path the snippet is associated with' + argument :uploaded_files, [GraphQL::STRING_TYPE], + required: false, + description: 'The paths to files uploaded in the snippet description' + def resolve(args) project_path = args.delete(:project_path) @@ -45,9 +49,14 @@ module Mutations raise_resource_not_available_error! end + # We need to rename `uploaded_files` into `files` because + # it's the expected key param + args[:files] = args.delete(:uploaded_files) + service_response = ::Snippets::CreateService.new(project, context[:current_user], args).execute + snippet = service_response.payload[:snippet] { diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb new file mode 100644 index 00000000000..7f4346632ca --- /dev/null +++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Resolvers + module AlertManagement + class AlertStatusCountsResolver < BaseResolver + type Types::AlertManagement::AlertStatusCountsType, null: true + + def resolve(**args) + ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args) + end + end + end +end diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb new file mode 100644 index 00000000000..51ebbb96476 --- /dev/null +++ b/app/graphql/resolvers/alert_management_alert_resolver.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Resolvers + class AlertManagementAlertResolver < BaseResolver + argument :iid, GraphQL::STRING_TYPE, + required: false, + description: 'IID of the alert. For example, "1"' + + argument :statuses, [Types::AlertManagement::StatusEnum], + as: :status, + required: false, + description: 'Alerts with the specified statues. For example, [TRIGGERED]' + + argument :sort, Types::AlertManagement::AlertSortEnum, + description: 'Sort alerts by this criteria', + required: false + + argument :search, GraphQL::STRING_TYPE, + description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.', + required: false + + type Types::AlertManagement::AlertType, null: true + + def resolve(**args) + parent = object.respond_to?(:sync) ? object.sync : object + return ::AlertManagement::Alert.none if parent.nil? + + ::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute + end + end +end diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb new file mode 100644 index 00000000000..f8d62ba86af --- /dev/null +++ b/app/graphql/resolvers/board_lists_resolver.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Resolvers + class BoardListsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::BoardListType, null: true + + alias_method :board, :object + + def resolve(lookahead: nil) + authorize!(board) + + lists = board_lists + + if load_preferences?(lookahead) + List.preload_preferences_for_user(lists, context[:current_user]) + end + + Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(lists) + end + + private + + def board_lists + service = Boards::Lists::ListService.new(board.resource_parent, context[:current_user]) + service.execute(board, create_default_lists: false) + end + + def authorized_resource?(board) + Ability.allowed?(context[:current_user], :read_list, board) + end + + def load_preferences?(lookahead) + lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed) + end + end +end diff --git a/app/graphql/resolvers/branch_commit_resolver.rb b/app/graphql/resolvers/branch_commit_resolver.rb new file mode 100644 index 00000000000..11c49e17bc5 --- /dev/null +++ b/app/graphql/resolvers/branch_commit_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + class BranchCommitResolver < BaseResolver + type Types::CommitType, null: true + + alias_method :branch, :object + + def resolve(**args) + return unless branch + + commit = branch.dereferenced_target + + ::Commit.new(commit, context[:branch_project]) if commit + end + end +end diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb new file mode 100644 index 00000000000..fd9b349f974 --- /dev/null +++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class DesignAtVersionResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::DesignManagement::DesignAtVersionType, null: false + + authorize :read_design + + argument :id, GraphQL::ID_TYPE, + required: true, + description: 'The Global ID of the design at this version' + + def resolve(id:) + authorized_find!(id: id) + end + + def find_object(id:) + dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) + return unless consistent?(dav) + + dav + end + + def self.single + self + end + + private + + # If this resolver is mounted on something that has an issue + # (such as design collection for instance), then we should check + # that the DesignAtVersion as found by its ID does in fact belong + # to this issue. + def consistent?(dav) + issue.nil? || (dav&.design&.issue_id == issue.id) + end + + def issue + object&.issue + end + end + end +end diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb new file mode 100644 index 00000000000..05bdbbbe407 --- /dev/null +++ b/app/graphql/resolvers/design_management/design_resolver.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class DesignResolver < BaseResolver + argument :id, GraphQL::ID_TYPE, + required: false, + description: 'Find a design by its ID' + + argument :filename, GraphQL::STRING_TYPE, + required: false, + description: 'Find a design by its filename' + + def resolve(filename: nil, id: nil) + params = parse_args(filename, id) + + build_finder(params).execute.first + end + + def self.single + self + end + + private + + def issue + object.issue + end + + def build_finder(params) + ::DesignManagement::DesignsFinder.new(issue, current_user, params) + end + + def error(msg) + raise ::Gitlab::Graphql::Errors::ArgumentError, msg + end + + def parse_args(filename, id) + provided = [filename, id].map(&:present?) + + if provided.none? + error('one of id or filename must be passed') + elsif provided.all? + error('only one of id or filename may be passed') + elsif filename.present? + { filenames: [filename] } + else + { ids: [parse_gid(id)] } + end + end + + def parse_gid(gid) + GitlabSchema.parse_gid(gid, expected_type: ::DesignManagement::Design).model_id + end + end + end +end diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb new file mode 100644 index 00000000000..81f94d5cb30 --- /dev/null +++ b/app/graphql/resolvers/design_management/designs_resolver.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class DesignsResolver < BaseResolver + argument :ids, + [GraphQL::ID_TYPE], + required: false, + description: 'Filters designs by their ID' + argument :filenames, + [GraphQL::STRING_TYPE], + required: false, + description: 'Filters designs by their filename' + argument :at_version, + GraphQL::ID_TYPE, + required: false, + description: 'Filters designs to only those that existed at the version. ' \ + 'If argument is omitted or nil then all designs will reflect the latest version' + + def self.single + ::Resolvers::DesignManagement::DesignResolver + end + + def resolve(ids: nil, filenames: nil, at_version: nil) + ::DesignManagement::DesignsFinder.new( + issue, + current_user, + ids: design_ids(ids), + filenames: filenames, + visible_at_version: version(at_version), + order: :id + ).execute + end + + private + + def version(at_version) + GitlabSchema.object_from_id(at_version)&.sync if at_version + end + + def design_ids(ids) + ids&.map { |id| GlobalID.parse(id).model_id } + end + + def issue + object.issue + end + end + end +end diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb new file mode 100644 index 00000000000..03f7908780c --- /dev/null +++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + module Version + # Resolver for a DesignAtVersion object given an implicit version context + class DesignAtVersionResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::DesignManagement::DesignAtVersionType, null: true + + authorize :read_design + + argument :id, GraphQL::ID_TYPE, + required: false, + as: :design_at_version_id, + description: 'The ID of the DesignAtVersion' + argument :design_id, GraphQL::ID_TYPE, + required: false, + description: 'The ID of a specific design' + argument :filename, GraphQL::STRING_TYPE, + required: false, + description: 'The filename of a specific design' + + def self.single + self + end + + def resolve(design_id: nil, filename: nil, design_at_version_id: nil) + validate_arguments(design_id, filename, design_at_version_id) + + return unless Ability.allowed?(current_user, :read_design, issue) + return specific_design_at_version(design_at_version_id) if design_at_version_id + + find(design_id, filename).map { |d| make(d) }.first + end + + private + + def validate_arguments(design_id, filename, design_at_version_id) + args = { filename: filename, id: design_at_version_id, design_id: design_id } + passed = args.compact.keys + + return if passed.size == 1 + + msg = "Exactly one of #{args.keys.join(', ')} expected, got #{passed}" + + raise Gitlab::Graphql::Errors::ArgumentError, msg + end + + def specific_design_at_version(id) + dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion) + return unless consistent?(dav) + + dav + end + + # Test that the DAV found by ID actually belongs on this version, and + # that it is visible at this version. + def consistent?(dav) + return false unless dav.present? + + dav.design.issue_id == issue.id && + dav.version.id == version.id && + dav.design.visible_in?(version) + end + + def find(id, filename) + ids = [parse_design_id(id).model_id] if id + filenames = [filename] if filename + + ::DesignManagement::DesignsFinder + .new(issue, current_user, ids: ids, filenames: filenames, visible_at_version: version) + .execute + end + + def parse_design_id(id) + GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design) + end + + def issue + version.issue + end + + def version + object + end + + def make(design) + ::DesignManagement::DesignAtVersion.new(design: design, version: version) + end + end + end + end +end diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb new file mode 100644 index 00000000000..5ccb2f3e311 --- /dev/null +++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + module Version + # Resolver for DesignAtVersion objects given an implicit version context + class DesignsAtVersionResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::DesignManagement::DesignAtVersionType, null: true + + authorize :read_design + + argument :ids, + [GraphQL::ID_TYPE], + required: false, + description: 'Filters designs by their ID' + argument :filenames, + [GraphQL::STRING_TYPE], + required: false, + description: 'Filters designs by their filename' + + def self.single + ::Resolvers::DesignManagement::Version::DesignAtVersionResolver + end + + def resolve(ids: nil, filenames: nil) + find(ids, filenames).execute.map { |d| make(d) } + end + + private + + def find(ids, filenames) + ids = ids&.map { |id| parse_design_id(id).model_id } + + ::DesignManagement::DesignsFinder.new(issue, current_user, + ids: ids, + filenames: filenames, + visible_at_version: version) + end + + def parse_design_id(id) + GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design) + end + + def issue + version.issue + end + + def version + object + end + + def make(design) + ::DesignManagement::DesignAtVersion.new(design: design, version: version) + end + end + end + end +end diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb new file mode 100644 index 00000000000..9e729172881 --- /dev/null +++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class VersionInCollectionResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::DesignManagement::VersionType, null: true + + authorize :read_design + + alias_method :collection, :object + + argument :sha, GraphQL::STRING_TYPE, + required: false, + description: "The SHA256 of a specific version" + argument :id, GraphQL::ID_TYPE, + required: false, + description: 'The Global ID of the version' + + def resolve(id: nil, sha: nil) + check_args(id, sha) + + gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id + + ::DesignManagement::VersionsFinder + .new(collection, current_user, sha: sha, version_id: gid&.model_id) + .execute + .first + end + + def self.single + self + end + + private + + def check_args(id, sha) + return if id.present? || sha.present? + + raise ::Gitlab::Graphql::Errors::ArgumentError, 'one of id or sha is required' + end + end + end +end diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb new file mode 100644 index 00000000000..b0e0843e6c8 --- /dev/null +++ b/app/graphql/resolvers/design_management/version_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class VersionResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::DesignManagement::VersionType, null: true + + authorize :read_design + + argument :id, GraphQL::ID_TYPE, + required: true, + description: 'The Global ID of the version' + + def resolve(id:) + authorized_find!(id: id) + end + + def find_object(id:) + GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version) + end + end + end +end diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb new file mode 100644 index 00000000000..a62258dad5c --- /dev/null +++ b/app/graphql/resolvers/design_management/versions_resolver.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Resolvers + module DesignManagement + class VersionsResolver < BaseResolver + type Types::DesignManagement::VersionType.connection_type, null: false + + alias_method :design_or_collection, :object + + argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE, + as: :sha, + required: false, + description: 'The SHA256 of the most recent acceptable version' + + argument :earlier_or_equal_to_id, GraphQL::ID_TYPE, + as: :id, + required: false, + description: 'The Global ID of the most recent acceptable version' + + # This resolver has a custom singular resolver + def self.single + ::Resolvers::DesignManagement::VersionInCollectionResolver + end + + def resolve(parent: nil, id: nil, sha: nil) + version = cutoff(parent, id, sha) + + raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present? + + if version == :unconstrained + find + else + find(earlier_or_equal_to: version) + end + end + + private + + # Find the most recent version that the client will accept + def cutoff(parent, id, sha) + if sha.present? || id.present? + specific_version(id, sha) + elsif at_version = at_version_arg(parent) + by_id(at_version) + else + :unconstrained + end + end + + def specific_version(id, sha) + gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id + find(sha: sha, version_id: gid&.model_id).first + end + + def find(**params) + ::DesignManagement::VersionsFinder + .new(design_or_collection, current_user, params) + .execute + end + + def by_id(id) + GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version).sync + end + + # Find an `at_version` argument passed to a parent node. + # + # If one is found, then a design collection further up the AST + # has been filtered to reflect designs at that version, and so + # for consistency we should only present versions up to the given + # version here. + def at_version_arg(parent) + ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4) + end + end + end +end diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index 04da54a6bb6..f103da07666 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -52,6 +52,10 @@ module Resolvers type Types::IssueType, null: true + NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc + label_priority_asc label_priority_desc + milestone_due_asc milestone_due_desc].freeze + def resolve(**args) # The project could have been loaded in batch by `BatchLoader`. # At this point we need the `id` of the project to query for issues, so @@ -70,7 +74,15 @@ module Resolvers args[:iids] ||= [args[:iid]].compact args[:attempt_project_search_optimizations] = args[:search].present? - IssuesFinder.new(context[:current_user], args).execute + issues = IssuesFinder.new(context[:current_user], args).execute + + if non_stable_cursor_sort?(args[:sort]) + # Certain complex sorts are not supported by the stable cursor pagination yet. + # In these cases, we use offset pagination, so we return the correct connection. + Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues) + else + issues + end end def self.resolver_complexity(args, child_complexity:) @@ -79,5 +91,9 @@ module Resolvers complexity end + + def non_stable_cursor_sort?(sort) + NON_STABLE_CURSOR_SORTS.include?(sort) + end end end diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb index 068323a3073..2dd224bb17b 100644 --- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb +++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb @@ -18,7 +18,6 @@ module Resolvers def resolve(**args) return [] unless dashboard - return [] unless Feature.enabled?(:metrics_dashboard_annotations, dashboard.environment&.project) ::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute end diff --git a/app/graphql/resolvers/milestone_resolver.rb b/app/graphql/resolvers/milestone_resolver.rb index 2e7b6fdfd5f..6c6513e0ee4 100644 --- a/app/graphql/resolvers/milestone_resolver.rb +++ b/app/graphql/resolvers/milestone_resolver.rb @@ -9,6 +9,10 @@ module Resolvers required: false, description: 'Filter milestones by state' + argument :include_descendants, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Return also milestones in all subgroups and subprojects' + type Types::MilestoneType, null: true def resolve(**args) @@ -26,16 +30,16 @@ module Resolvers state: args[:state] || 'all', start_date: args[:start_date], end_date: args[:end_date] - }.merge(parent_id_parameter) + }.merge(parent_id_parameter(args)) end def parent @parent ||= object.respond_to?(:sync) ? object.sync : object end - def parent_id_parameter + def parent_id_parameter(args) if parent.is_a?(Group) - { group_ids: parent.id } + group_parameters(args) elsif parent.is_a?(Project) { project_ids: parent.id } end @@ -46,5 +50,26 @@ module Resolvers def authorize! Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error! end + + def group_parameters(args) + return { group_ids: parent.id } unless include_descendants?(args) + + { + group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id), + project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user) + } + end + + def include_descendants?(args) + args[:include_descendants].present? && Feature.enabled?(:group_milestone_descendants, parent) + end + + def group_projects + GroupProjectsFinder.new( + group: parent, + current_user: current_user, + options: { include_subgroups: true } + ).execute + end end end diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb index f5b60f91be6..e841132eea7 100644 --- a/app/graphql/resolvers/namespace_projects_resolver.rb +++ b/app/graphql/resolvers/namespace_projects_resolver.rb @@ -29,3 +29,5 @@ module Resolvers end end end + +Resolvers::NamespaceProjectsResolver.prepend_if_ee('::EE::Resolvers::NamespaceProjectsResolver') diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb new file mode 100644 index 00000000000..068546cd39f --- /dev/null +++ b/app/graphql/resolvers/projects_resolver.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Resolvers + class ProjectsResolver < BaseResolver + type Types::ProjectType, null: true + + argument :membership, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Limit projects that the current user is a member of' + + argument :search, GraphQL::STRING_TYPE, + required: false, + description: 'Search criteria' + + def resolve(**args) + ProjectsFinder + .new(current_user: current_user, params: project_finder_params(args)) + .execute + end + + private + + def project_finder_params(params) + { + without_deleted: true, + non_public: params[:membership], + search: params[:search] + }.compact + end + end +end diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb new file mode 100644 index 00000000000..9bae8b8cd13 --- /dev/null +++ b/app/graphql/resolvers/release_resolver.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Resolvers + class ReleaseResolver < BaseResolver + type Types::ReleaseType, null: true + + argument :tag_name, GraphQL::STRING_TYPE, + required: true, + description: 'The name of the tag associated to the release' + + alias_method :project, :object + + def self.single + self + end + + def resolve(tag_name:) + ReleasesFinder.new( + project, + current_user, + { tag: tag_name } + ).execute.first + end + end +end diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb new file mode 100644 index 00000000000..b2afbb92684 --- /dev/null +++ b/app/graphql/resolvers/releases_resolver.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Resolvers + class ReleasesResolver < BaseResolver + type Types::ReleaseType.connection_type, null: true + + alias_method :project, :object + + # This resolver has a custom singular resolver + def self.single + Resolvers::ReleaseResolver + end + + def resolve(**args) + ReleasesFinder.new( + project, + current_user + ).execute + end + end +end diff --git a/app/graphql/types/alert_management/alert_sort_enum.rb b/app/graphql/types/alert_management/alert_sort_enum.rb new file mode 100644 index 00000000000..e6d38af8170 --- /dev/null +++ b/app/graphql/types/alert_management/alert_sort_enum.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class AlertSortEnum < SortEnum + graphql_name 'AlertManagementAlertSort' + description 'Values for sorting alerts' + + value 'START_TIME_ASC', 'Start time by ascending order', value: :start_time_asc + value 'START_TIME_DESC', 'Start time by descending order', value: :start_time_desc + value 'END_TIME_ASC', 'End time by ascending order', value: :end_time_asc + value 'END_TIME_DESC', 'End time by descending order', value: :end_time_desc + value 'CREATED_TIME_ASC', 'Created time by ascending order', value: :created_at_asc + value 'CREATED_TIME_DESC', 'Created time by descending order', value: :created_at_desc + value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_asc + value 'UPDATED_TIME_DESC', 'Created time by descending order', value: :updated_at_desc + value 'EVENTS_COUNT_ASC', 'Events count by ascending order', value: :events_count_asc + value 'EVENTS_COUNT_DESC', 'Events count by descending order', value: :events_count_desc + value 'SEVERITY_ASC', 'Severity by ascending order', value: :severity_asc + value 'SEVERITY_DESC', 'Severity by descending order', value: :severity_desc + value 'STATUS_ASC', 'Status by ascending order', value: :status_asc + value 'STATUS_DESC', 'Status by descending order', value: :status_desc + end + end +end diff --git a/app/graphql/types/alert_management/alert_status_counts_type.rb b/app/graphql/types/alert_management/alert_status_counts_type.rb new file mode 100644 index 00000000000..f80b289eabc --- /dev/null +++ b/app/graphql/types/alert_management/alert_status_counts_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Service for managing alert counts and cache updates. +module Types + module AlertManagement + class AlertStatusCountsType < BaseObject + graphql_name 'AlertManagementAlertStatusCountsType' + description "Represents total number of alerts for the represented categories" + + authorize :read_alert_management_alert + + ::Gitlab::AlertManagement::AlertStatusCounts::STATUSES.each_key do |status| + field status, + GraphQL::INT_TYPE, + null: true, + description: "Number of alerts with status #{status.upcase} for the project" + end + + field :open, + GraphQL::INT_TYPE, + null: true, + description: 'Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project' + + field :all, + GraphQL::INT_TYPE, + null: true, + description: 'Total number of alerts for the project' + end + end +end diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb new file mode 100644 index 00000000000..a766fb3236d --- /dev/null +++ b/app/graphql/types/alert_management/alert_type.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class AlertType < BaseObject + graphql_name 'AlertManagementAlert' + description "Describes an alert from the project's Alert Management" + + authorize :read_alert_management_alert + + field :iid, + GraphQL::ID_TYPE, + null: false, + description: 'Internal ID of the alert' + + field :issue_iid, + GraphQL::ID_TYPE, + null: true, + description: 'Internal ID of the GitLab issue attached to the alert' + + field :title, + GraphQL::STRING_TYPE, + null: true, + description: 'Title of the alert' + + field :description, + GraphQL::STRING_TYPE, + null: true, + description: 'Description of the alert' + + field :severity, + AlertManagement::SeverityEnum, + null: true, + description: 'Severity of the alert' + + field :status, + AlertManagement::StatusEnum, + null: true, + description: 'Status of the alert' + + field :service, + GraphQL::STRING_TYPE, + null: true, + description: 'Service the alert came from' + + field :monitoring_tool, + GraphQL::STRING_TYPE, + null: true, + description: 'Monitoring tool the alert came from' + + field :hosts, + [GraphQL::STRING_TYPE], + null: true, + description: 'List of hosts the alert came from' + + field :started_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert was raised' + + field :ended_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert ended' + + field :event_count, + GraphQL::INT_TYPE, + null: true, + description: 'Number of events of this alert', + method: :events + + field :details, + GraphQL::Types::JSON, + null: true, + description: 'Alert details' + + field :created_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert was created' + + field :updated_at, + Types::TimeType, + null: true, + description: 'Timestamp the alert was last updated' + end + end +end diff --git a/app/graphql/types/alert_management/severity_enum.rb b/app/graphql/types/alert_management/severity_enum.rb new file mode 100644 index 00000000000..99ea56da02c --- /dev/null +++ b/app/graphql/types/alert_management/severity_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class SeverityEnum < BaseEnum + graphql_name 'AlertManagementSeverity' + description 'Alert severity values' + + ::AlertManagement::Alert.severities.keys.each do |severity| + value severity.upcase, value: severity, description: "#{severity.titleize} severity" + end + end + end +end diff --git a/app/graphql/types/alert_management/status_enum.rb b/app/graphql/types/alert_management/status_enum.rb new file mode 100644 index 00000000000..4ff6c4a9505 --- /dev/null +++ b/app/graphql/types/alert_management/status_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module AlertManagement + class StatusEnum < BaseEnum + graphql_name 'AlertManagementStatus' + description 'Alert status values' + + ::AlertManagement::Alert::STATUSES.each do |name, value| + value name.upcase, value: value, description: "#{name.to_s.titleize} status" + end + end + end +end diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb new file mode 100644 index 00000000000..e94ff898807 --- /dev/null +++ b/app/graphql/types/board_list_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class BoardListType < BaseObject + graphql_name 'BoardList' + description 'Represents a list for an issue board' + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID (global ID) of the list' + field :title, GraphQL::STRING_TYPE, null: false, + description: 'Title of the list' + field :list_type, GraphQL::STRING_TYPE, null: false, + description: 'Type of the list' + field :position, GraphQL::INT_TYPE, null: true, + description: 'Position of list within the board' + field :label, Types::LabelType, null: true, + description: 'Label of the list' + field :collapsed, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Indicates if list is collapsed for this user', + resolve: -> (list, _args, ctx) { list.collapsed?(ctx[:current_user]) } + end + # rubocop: enable Graphql/AuthorizeTypes +end + +Types::BoardListType.prepend_if_ee('::EE::Types::BoardListType') diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb index 9c95a987fe4..c0be782ed1e 100644 --- a/app/graphql/types/board_type.rb +++ b/app/graphql/types/board_type.rb @@ -11,6 +11,13 @@ module Types description: 'ID (global ID) of the board' field :name, type: GraphQL::STRING_TYPE, null: true, description: 'Name of the board' + + field :lists, + Types::BoardListType.connection_type, + null: true, + description: 'Lists of the project board', + resolver: Resolvers::BoardListsResolver, + extras: [:lookahead] end end diff --git a/app/graphql/types/branch_type.rb b/app/graphql/types/branch_type.rb new file mode 100644 index 00000000000..b15038a46de --- /dev/null +++ b/app/graphql/types/branch_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class BranchType < BaseObject + graphql_name 'Branch' + + field :name, + GraphQL::STRING_TYPE, + null: false, + description: 'Name of the branch' + + field :commit, Types::CommitType, + null: true, resolver: Resolvers::BranchCommitResolver, + description: 'Commit for the branch' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index aaf2dfd8488..be5165da545 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -14,6 +14,7 @@ module Types description: 'SHA1 ID of the commit' field :title, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true, description: 'Title of the commit message' + markdown_field :title_html, null: true field :description, type: GraphQL::STRING_TYPE, null: true, description: 'Description of the commit message' field :message, type: GraphQL::STRING_TYPE, null: true, diff --git a/app/graphql/types/design_management/design_at_version_type.rb b/app/graphql/types/design_management/design_at_version_type.rb new file mode 100644 index 00000000000..343d4cf4ff4 --- /dev/null +++ b/app/graphql/types/design_management/design_at_version_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class DesignAtVersionType < BaseObject + graphql_name 'DesignAtVersion' + + description 'A design pinned to a specific version. ' \ + 'The image field reflects the design as of the associated version.' + + authorize :read_design + + delegate :design, :version, to: :object + delegate :issue, :filename, :full_path, :diff_refs, to: :design + + implements ::Types::DesignManagement::DesignFields + + field :version, + Types::DesignManagement::VersionType, + null: false, + description: 'The version this design-at-versions is pinned to' + + field :design, + Types::DesignManagement::DesignType, + null: false, + description: 'The underlying design.' + + def cached_stateful_version(_parent) + version + end + + def notes_count + design.user_notes_count + end + end + end +end diff --git a/app/graphql/types/design_management/design_collection_type.rb b/app/graphql/types/design_management/design_collection_type.rb new file mode 100644 index 00000000000..194910831c6 --- /dev/null +++ b/app/graphql/types/design_management/design_collection_type.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class DesignCollectionType < BaseObject + graphql_name 'DesignCollection' + description 'A collection of designs.' + + authorize :read_design + + field :project, Types::ProjectType, null: false, + description: 'Project associated with the design collection' + field :issue, Types::IssueType, null: false, + description: 'Issue associated with the design collection' + + field :designs, + Types::DesignManagement::DesignType.connection_type, + null: false, + resolver: Resolvers::DesignManagement::DesignsResolver, + description: 'All designs for the design collection', + complexity: 5 + + field :versions, + Types::DesignManagement::VersionType.connection_type, + resolver: Resolvers::DesignManagement::VersionsResolver, + description: 'All versions related to all designs, ordered newest first' + + field :version, + Types::DesignManagement::VersionType, + resolver: Resolvers::DesignManagement::VersionsResolver.single, + description: 'A specific version' + + field :design_at_version, ::Types::DesignManagement::DesignAtVersionType, + null: true, + resolver: ::Resolvers::DesignManagement::DesignAtVersionResolver, + description: 'Find a design as of a version' + + field :design, ::Types::DesignManagement::DesignType, + null: true, + resolver: ::Resolvers::DesignManagement::DesignResolver, + description: 'Find a specific design' + end + end +end diff --git a/app/graphql/types/design_management/design_fields.rb b/app/graphql/types/design_management/design_fields.rb new file mode 100644 index 00000000000..b03b3927392 --- /dev/null +++ b/app/graphql/types/design_management/design_fields.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + module DesignFields + include BaseInterface + + field_class Types::BaseField + + field :id, GraphQL::ID_TYPE, description: 'The ID of this design', null: false + field :project, Types::ProjectType, null: false, description: 'The project the design belongs to' + field :issue, Types::IssueType, null: false, description: 'The issue the design belongs to' + field :filename, GraphQL::STRING_TYPE, null: false, description: 'The filename of the design' + field :full_path, GraphQL::STRING_TYPE, null: false, description: 'The full path to the design file' + field :image, GraphQL::STRING_TYPE, null: false, extras: [:parent], description: 'The URL of the full-sized image' + field :image_v432x230, GraphQL::STRING_TYPE, null: true, extras: [:parent], + description: 'The URL of the design resized to fit within the bounds of 432x230. ' \ + 'This will be `null` if the image has not been generated' + field :diff_refs, Types::DiffRefsType, + null: false, + calls_gitaly: true, + extras: [:parent], + description: 'The diff refs for this design' + field :event, Types::DesignManagement::DesignVersionEventEnum, + null: false, + extras: [:parent], + description: 'How this design was changed in the current version' + field :notes_count, + GraphQL::INT_TYPE, + null: false, + method: :user_notes_count, + description: 'The total count of user-created notes for this design' + + def diff_refs(parent:) + version = cached_stateful_version(parent) + version.diff_refs + end + + def image(parent:) + sha = cached_stateful_version(parent).sha + + Gitlab::UrlBuilder.build(design, ref: sha) + end + + def image_v432x230(parent:) + version = cached_stateful_version(parent) + action = design.actions.up_to_version(version).most_recent.first + + # A `nil` return value indicates that the image has not been processed + return unless action.image_v432x230.file + + Gitlab::UrlBuilder.build(design, ref: version.sha, size: :v432x230) + end + + def event(parent:) + version = cached_stateful_version(parent) + + action = cached_actions_for_version(version)[design.id] + + action&.event || ::Types::DesignManagement::DesignVersionEventEnum::NONE + end + + def cached_actions_for_version(version) + Gitlab::SafeRequestStore.fetch(['DesignFields', 'actions_for_version', version.id]) do + version.actions.to_h { |dv| [dv.design_id, dv] } + end + end + + def project + ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Project, design.project_id).find + end + + def issue + ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Issue, design.issue_id).find + end + end + end +end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb new file mode 100644 index 00000000000..3c84dc151bd --- /dev/null +++ b/app/graphql/types/design_management/design_type.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class DesignType < BaseObject + graphql_name 'Design' + description 'A single design' + + authorize :read_design + + alias_method :design, :object + + implements(Types::Notes::NoteableType) + implements(Types::DesignManagement::DesignFields) + + field :versions, + Types::DesignManagement::VersionType.connection_type, + resolver: Resolvers::DesignManagement::VersionsResolver, + description: "All versions related to this design ordered newest first", + extras: [:parent] + + # Returns a `DesignManagement::Version` for this query based on the + # `atVersion` argument passed to a parent node if present, or otherwise + # the most recent `Version` for the issue. + def cached_stateful_version(parent_node) + version_gid = Gitlab::Graphql::FindArgumentInParent.find(parent_node, :at_version) + + # Caching is scoped to an `issue_id` to allow us to cache the + # most recent `Version` for an issue + Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do + if version_gid + GitlabSchema.object_from_id(version_gid)&.sync + else + object.issue.design_versions.most_recent + end + end + end + + def request_cache_base_key + self.class.name + end + end + end +end diff --git a/app/graphql/types/design_management/design_version_event_enum.rb b/app/graphql/types/design_management/design_version_event_enum.rb new file mode 100644 index 00000000000..ea4bc1ffbfa --- /dev/null +++ b/app/graphql/types/design_management/design_version_event_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class DesignVersionEventEnum < BaseEnum + graphql_name 'DesignVersionEvent' + description 'Mutation event of a design within a version' + + NONE = 'NONE' + + value NONE, 'No change' + + ::DesignManagement::Action.events.keys.each do |event_name| + value event_name.upcase, value: event_name, description: "A #{event_name} event" + end + end + end +end diff --git a/app/graphql/types/design_management/version_type.rb b/app/graphql/types/design_management/version_type.rb new file mode 100644 index 00000000000..c774f5d1bdf --- /dev/null +++ b/app/graphql/types/design_management/version_type.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Types + module DesignManagement + class VersionType < ::Types::BaseObject + # Just `Version` might be a bit to general to expose globally so adding + # a `Design` prefix to specify the class exposed in GraphQL + graphql_name 'DesignVersion' + + description 'A specific version in which designs were added, modified or deleted' + + authorize :read_design + + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the design version' + field :sha, GraphQL::ID_TYPE, null: false, + description: 'SHA of the design version' + + field :designs, + ::Types::DesignManagement::DesignType.connection_type, + null: false, + description: 'All designs that were changed in the version' + + field :designs_at_version, + ::Types::DesignManagement::DesignAtVersionType.connection_type, + null: false, + description: 'All designs that are visible at this version, as of this version', + resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver + + field :design_at_version, + ::Types::DesignManagement::DesignAtVersionType, + null: false, + description: 'A particular design as of this version, provided it is visible at this version', + resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver.single + end + end +end diff --git a/app/graphql/types/design_management_type.rb b/app/graphql/types/design_management_type.rb new file mode 100644 index 00000000000..ec85b8a0c1f --- /dev/null +++ b/app/graphql/types/design_management_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# rubocop: disable Graphql/AuthorizeTypes +module Types + class DesignManagementType < BaseObject + graphql_name 'DesignManagement' + + field :version, ::Types::DesignManagement::VersionType, + null: true, + resolver: ::Resolvers::DesignManagement::VersionResolver, + description: 'Find a version' + + field :design_at_version, ::Types::DesignManagement::DesignAtVersionType, + null: true, + resolver: ::Resolvers::DesignManagement::DesignAtVersionResolver, + description: 'Find a design as of a version' + end +end diff --git a/app/graphql/types/grafana_integration_type.rb b/app/graphql/types/grafana_integration_type.rb index c0582b266ab..7db733fc62a 100644 --- a/app/graphql/types/grafana_integration_type.rb +++ b/app/graphql/types/grafana_integration_type.rb @@ -9,7 +9,7 @@ module Types field :id, GraphQL::ID_TYPE, null: false, description: 'Internal ID of the Grafana integration' field :grafana_url, GraphQL::STRING_TYPE, null: false, - description: 'Url for the Grafana host for the Grafana integration' + description: 'URL for the Grafana host for the Grafana integration' field :enabled, GraphQL::BOOLEAN_TYPE, null: false, description: 'Indicates whether Grafana integration is enabled' field :created_at, Types::TimeType, null: false, diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb index 9fb1249d582..a6d52124d99 100644 --- a/app/graphql/types/issuable_sort_enum.rb +++ b/app/graphql/types/issuable_sort_enum.rb @@ -4,5 +4,12 @@ module Types class IssuableSortEnum < SortEnum graphql_name 'IssuableSort' description 'Values for sorting issuables' + + value 'PRIORITY_ASC', 'Priority by ascending order', value: :priority_asc + value 'PRIORITY_DESC', 'Priority by descending order', value: :priority_desc + value 'LABEL_PRIORITY_ASC', 'Label priority by ascending order', value: :label_priority_asc + value 'LABEL_PRIORITY_DESC', 'Label priority by descending order', value: :label_priority_desc + value 'MILESTONE_DUE_ASC', 'Milestone due date by ascending order', value: :milestone_due_asc + value 'MILESTONE_DUE_DESC', 'Milestone due date by descending order', value: :milestone_due_desc end end diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index c8d8f3ef079..e458d6e02c5 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -5,9 +5,9 @@ module Types graphql_name 'IssueSort' description 'Values for sorting issues' - value 'DUE_DATE_ASC', 'Due date by ascending order', value: 'due_date_asc' - value 'DUE_DATE_DESC', 'Due date by descending order', value: 'due_date_desc' - value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: 'relative_position_asc' + value 'DUE_DATE_ASC', 'Due date by ascending order', value: :due_date_asc + value 'DUE_DATE_DESC', 'Due date by descending order', value: :due_date_desc + value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: :relative_position_asc end end diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 11850e5865f..73219ca9e1e 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -85,6 +85,14 @@ module Types field :task_completion_status, Types::TaskCompletionStatus, null: false, description: 'Task completion status of the issue' + + field :designs, Types::DesignManagement::DesignCollectionType, null: true, + method: :design_collection, + deprecated: { reason: 'Use `designCollection`', milestone: '12.2' }, + description: 'The designs associated with this issue' + + field :design_collection, Types::DesignManagement::DesignCollectionType, null: true, + description: 'Collection of design images associated with this issue' end end diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb index ccd463370b6..4a124566ffb 100644 --- a/app/graphql/types/jira_import_type.rb +++ b/app/graphql/types/jira_import_type.rb @@ -7,9 +7,10 @@ module Types class JiraImportType < BaseObject graphql_name 'JiraImport' - field :scheduled_at, Types::TimeType, null: true, - method: :created_at, + field :created_at, Types::TimeType, null: true, description: 'Timestamp of when the Jira import was created' + field :scheduled_at, Types::TimeType, null: true, + description: 'Timestamp of when the Jira import was scheduled' field :scheduled_by, Types::UserType, null: true, description: 'User that started the Jira import' field :jira_project_key, GraphQL::STRING_TYPE, null: false, diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb index e7d09866bb5..d684533ff94 100644 --- a/app/graphql/types/metrics/dashboard_type.rb +++ b/app/graphql/types/metrics/dashboard_type.rb @@ -11,8 +11,7 @@ module Types description: 'Path to a file with the dashboard definition' field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true, - description: 'Annotations added to the dashboard. Will always return `null` ' \ - 'if `metrics_dashboard_annotations` feature flag is disabled', + description: 'Annotations added to the dashboard', resolver: Resolvers::Metrics::Dashboards::AnnotationResolver end # rubocop: enable Graphql/AuthorizeTypes diff --git a/app/graphql/types/metrics/dashboards/annotation_type.rb b/app/graphql/types/metrics/dashboards/annotation_type.rb index 055d2544eff..0f8f95c187b 100644 --- a/app/graphql/types/metrics/dashboards/annotation_type.rb +++ b/app/graphql/types/metrics/dashboards/annotation_type.rb @@ -16,10 +16,10 @@ module Types field :panel_id, GraphQL::STRING_TYPE, null: true, description: 'ID of a dashboard panel to which the annotation should be scoped' - field :starting_at, GraphQL::STRING_TYPE, null: true, + field :starting_at, Types::TimeType, null: true, description: 'Timestamp marking start of annotated time span' - field :ending_at, GraphQL::STRING_TYPE, null: true, + field :ending_at, Types::TimeType, null: true, description: 'Timestamp marking end of annotated time span' def panel_id diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index ab25d5baf71..aeff84b83b8 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -7,9 +7,12 @@ module Types graphql_name 'Mutation' mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs + mount_mutation Mutations::AlertManagement::CreateAlertIssue + mount_mutation Mutations::AlertManagement::UpdateAlertStatus mount_mutation Mutations::AwardEmojis::Add mount_mutation Mutations::AwardEmojis::Remove mount_mutation Mutations::AwardEmojis::Toggle + mount_mutation Mutations::Branches::Create, calls_gitaly: true mount_mutation Mutations::Issues::SetConfidential mount_mutation Mutations::Issues::SetDueDate mount_mutation Mutations::Issues::Update @@ -19,6 +22,7 @@ module Types mount_mutation Mutations::MergeRequests::SetSubscription mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetAssignees + mount_mutation Mutations::Metrics::Dashboard::Annotations::Create mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true @@ -40,6 +44,8 @@ module Types mount_mutation Mutations::Snippets::Create mount_mutation Mutations::Snippets::MarkAsSpam mount_mutation Mutations::JiraImport::Start + mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true + mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true end end diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb index 2ac66452841..187c9109f8c 100644 --- a/app/graphql/types/notes/noteable_type.rb +++ b/app/graphql/types/notes/noteable_type.rb @@ -17,6 +17,8 @@ module Types Types::MergeRequestType when Snippet Types::SnippetType + when ::DesignManagement::Design + Types::DesignManagement::DesignType else raise "Unknown GraphQL type for #{object}" end @@ -25,5 +27,3 @@ module Types end end end - -Types::Notes::NoteableType.extend_if_ee('::EE::Types::Notes::NoteableType') diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb index e26c5950e73..94e1bffd685 100644 --- a/app/graphql/types/permission_types/issue.rb +++ b/app/graphql/types/permission_types/issue.rb @@ -6,11 +6,9 @@ module Types description 'Check permissions for the current user on a issue' graphql_name 'IssuePermissions' - abilities :read_issue, :admin_issue, - :update_issue, :create_note, - :reopen_issue + abilities :read_issue, :admin_issue, :update_issue, :reopen_issue, + :read_design, :create_design, :destroy_design, + :create_note end end end - -Types::PermissionTypes::Issue.prepend_if_ee('::EE::Types::PermissionTypes::Issue') diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index f773fce0c63..5747e63d195 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -17,7 +17,7 @@ module Types :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content, :admin_operations, - :read_merge_request + :read_merge_request, :read_design, :create_design, :destroy_design permission_field :create_snippet @@ -27,5 +27,3 @@ module Types end end end - -Types::PermissionTypes::Project.prepend_if_ee('EE::Types::PermissionTypes::Project') diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 8356e763be9..4e438ed2576 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -205,6 +205,38 @@ module Types null: true, description: 'Project services', resolver: Resolvers::Projects::ServicesResolver + + field :alert_management_alerts, + Types::AlertManagement::AlertType.connection_type, + null: true, + description: 'Alert Management alerts of the project', + resolver: Resolvers::AlertManagementAlertResolver + + field :alert_management_alert, + Types::AlertManagement::AlertType, + null: true, + description: 'A single Alert Management alert of the project', + resolver: Resolvers::AlertManagementAlertResolver.single + + field :alert_management_alert_status_counts, + Types::AlertManagement::AlertStatusCountsType, + null: true, + description: 'Counts of alerts by status for the project', + resolver: Resolvers::AlertManagement::AlertStatusCountsResolver + + field :releases, + Types::ReleaseType.connection_type, + null: true, + description: 'Releases of the project', + resolver: Resolvers::ReleasesResolver, + feature_flag: :graphql_release_data + + field :release, + Types::ReleaseType, + null: true, + description: 'A single release of the project', + resolver: Resolvers::ReleasesResolver.single, + feature_flag: :graphql_release_data end end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index e8f6eeff3e9..70cdcb62bc6 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -4,11 +4,19 @@ module Types class QueryType < ::Types::BaseObject graphql_name 'Query' + # The design management context object needs to implement #issue + DesignManagementObject = Struct.new(:issue) + field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver, description: "Find a project" + field :projects, Types::ProjectType.connection_type, + null: true, + resolver: Resolvers::ProjectsResolver, + description: "Find projects visible to the current user" + field :group, Types::GroupType, null: true, resolver: Resolvers::GroupResolver, @@ -35,9 +43,17 @@ module Types resolver: Resolvers::SnippetsResolver, description: 'Find Snippets visible to the current user' + field :design_management, Types::DesignManagementType, + null: false, + description: 'Fields related to design management' + field :echo, GraphQL::STRING_TYPE, null: false, description: 'Text to echo back', resolver: Resolvers::EchoResolver + + def design_management + DesignManagementObject.new(nil) + end end end diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb new file mode 100644 index 00000000000..632351be5d3 --- /dev/null +++ b/app/graphql/types/release_type.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Types + class ReleaseType < BaseObject + graphql_name 'Release' + + authorize :read_release + + alias_method :release, :object + + present_using ReleasePresenter + + field :tag_name, GraphQL::STRING_TYPE, null: false, method: :tag, + description: 'Name of the tag associated with the release' + field :tag_path, GraphQL::STRING_TYPE, null: true, + description: 'Relative web path to the tag associated with the release' + field :description, GraphQL::STRING_TYPE, null: true, + description: 'Description (also known as "release notes") of the release' + markdown_field :description_html, null: true + field :name, GraphQL::STRING_TYPE, null: true, + description: 'Name of the release' + field :created_at, Types::TimeType, null: true, + description: 'Timestamp of when the release was created' + field :released_at, Types::TimeType, null: true, + description: 'Timestamp of when the release was released' + field :milestones, Types::MilestoneType.connection_type, null: true, + description: 'Milestones associated to the release' + + field :author, Types::UserType, null: true, + description: 'User that created the release' + + def author + Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find + end + + field :commit, Types::CommitType, null: true, + complexity: 10, calls_gitaly: true, + description: 'The commit associated with the release', + authorize: :reporter_access + + def commit + return if release.sha.nil? + + release.project.commit_by(oid: release.sha) + end + end +end diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb index 4ebdbd5766c..b23c4f71ffa 100644 --- a/app/graphql/types/snippet_type.rb +++ b/app/graphql/types/snippet_type.rb @@ -14,7 +14,7 @@ module Types expose_permissions Types::PermissionTypes::Snippet field :id, GraphQL::ID_TYPE, - description: 'Id of the snippet', + description: 'ID of the snippet', null: false field :title, GraphQL::STRING_TYPE, diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb index feff5d20874..dcde1e5a73b 100644 --- a/app/graphql/types/snippets/blob_type.rb +++ b/app/graphql/types/snippets/blob_type.rb @@ -14,6 +14,7 @@ module Types field :plain_data, GraphQL::STRING_TYPE, description: 'Blob plain highlighted data', + calls_gitaly: true, null: true field :raw_path, GraphQL::STRING_TYPE, @@ -48,6 +49,15 @@ module Types field :mode, type: GraphQL::STRING_TYPE, description: 'Blob mode', null: true + + field :external_storage, type: GraphQL::STRING_TYPE, + description: 'Blob external storage', + null: true + + field :rendered_as_text, type: GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob is rendered as text', + method: :rendered_as_text?, + null: false end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/graphql/types/snippets/blob_viewer_type.rb b/app/graphql/types/snippets/blob_viewer_type.rb index 3e653576d07..50d0b0522d6 100644 --- a/app/graphql/types/snippets/blob_viewer_type.rb +++ b/app/graphql/types/snippets/blob_viewer_type.rb @@ -17,12 +17,14 @@ module Types field :collapsed, GraphQL::BOOLEAN_TYPE, description: 'Shows whether the blob should be displayed collapsed', method: :collapsed?, - null: false + null: false, + resolve: -> (viewer, _args, _ctx) { !!viewer&.collapsed? } field :too_large, GraphQL::BOOLEAN_TYPE, description: 'Shows whether the blob too large to be displayed', method: :too_large?, - null: false + null: false, + resolve: -> (viewer, _args, _ctx) { !!viewer&.too_large? } field :render_error, GraphQL::STRING_TYPE, description: 'Error rendering the blob content', diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb index 8358a86b35c..a377c3aafdc 100644 --- a/app/graphql/types/todo_target_enum.rb +++ b/app/graphql/types/todo_target_enum.rb @@ -5,6 +5,7 @@ module Types value 'COMMIT', value: 'Commit', description: 'A Commit' value 'ISSUE', value: 'Issue', description: 'An Issue' value 'MERGEREQUEST', value: 'MergeRequest', description: 'A MergeRequest' + value 'DESIGN', value: 'DesignManagement::Design', description: 'A Design' end end diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 5ce5093c55e..08e7fabeb74 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -10,7 +10,7 @@ module Types authorize :read_todo field :id, GraphQL::ID_TYPE, - description: 'Id of the todo', + description: 'ID of the todo', null: false field :project, Types::ProjectType, diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index e530641d6ae..29a3f5d452f 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -10,8 +10,12 @@ module Types expose_permissions Types::PermissionTypes::User + field :id, GraphQL::ID_TYPE, null: false, + description: 'ID of the user' field :name, GraphQL::STRING_TYPE, null: false, description: 'Human-readable name of the user' + field :state, GraphQL::STRING_TYPE, null: false, + description: 'State of the issue' field :username, GraphQL::STRING_TYPE, null: false, description: 'Username of the user. Unique within this instance of GitLab' field :avatar_url, GraphQL::STRING_TYPE, null: true, |