Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-12-13 09:08:01 +00:00
parent b3db40398c
commit 17b91a3c6a
38 changed files with 1501 additions and 44 deletions

View File

@ -1,4 +1,6 @@
import { join as joinPaths } from 'path';
const PATH_SEPARATOR = '/';
const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
// Returns a decoded url parameter value
// - Treats '+' as '%20'
@ -6,6 +8,37 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
function cleanEndingSeparator(path) {
return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
}
/**
* Safely joins the given paths which might both start and end with a `/`
*
* Example:
* - `joinPaths('abc/', '/def') === 'abc/def'`
* - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'`
*
* @param {...String} paths
* @returns {String}
*/
export function joinPaths(...paths) {
return paths.reduce((acc, path) => {
if (!path) {
return acc;
}
if (!acc) {
return path;
}
return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join('');
}, '');
}
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@ -212,5 +245,3 @@ export function objectToQuery(obj) {
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
.join('&');
}
export { joinPaths };

View File

@ -12,7 +12,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action only: [:show] do
push_frontend_feature_flag(:job_log_json, project)
push_frontend_feature_flag(:job_log_json, project, default_enabled: true)
end
layout 'project'
@ -53,7 +53,7 @@ class Projects::JobsController < Projects::ApplicationController
format.json do
# TODO: when the feature flag is removed we should not pass
# content_format to serialize method.
content_format = Feature.enabled?(:job_log_json, @project) ? :json : :html
content_format = Feature.enabled?(:job_log_json, @project, default_enabled: true) ? :json : :html
build_trace = Ci::BuildTrace.new(
build: @build,

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Mutations
module Snippets
class Base < BaseMutation
field :snippet,
Types::SnippetType,
null: true,
description: 'The snippet after mutation'
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def authorized_resource?(snippet)
Ability.allowed?(context[:current_user], ability_for(snippet), snippet)
end
def ability_for(snippet)
"#{ability_name}_#{snippet.to_ability_name}".to_sym
end
def ability_name
raise NotImplementedError
end
end
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
module Mutations
module Snippets
class Create < BaseMutation
include Mutations::ResolvesProject
graphql_name 'CreateSnippet'
field :snippet,
Types::SnippetType,
null: true,
description: 'The snippet after mutation'
argument :title, GraphQL::STRING_TYPE,
required: true,
description: 'Title of the snippet'
argument :file_name, GraphQL::STRING_TYPE,
required: false,
description: 'File name of the snippet'
argument :content, GraphQL::STRING_TYPE,
required: true,
description: 'Content of the snippet'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description of the snippet'
argument :visibility_level, Types::VisibilityLevelsEnum,
description: 'The visibility level of the snippet',
required: true
argument :project_path, GraphQL::ID_TYPE,
required: false,
description: 'The project full path the snippet is associated with'
def resolve(args)
project_path = args.delete(:project_path)
if project_path.present?
project = find_project!(project_path: project_path)
elsif !can_create_personal_snippet?
raise_resource_not_avaiable_error!
end
snippet = CreateSnippetService.new(project,
context[:current_user],
args).execute
{
snippet: snippet.valid? ? snippet : nil,
errors: errors_on_object(snippet)
}
end
private
def find_project!(project_path:)
authorized_find!(full_path: project_path)
end
def find_object(full_path:)
resolve_project(full_path: full_path)
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :create_project_snippet, project)
end
def can_create_personal_snippet?
Ability.allowed?(context[:current_user], :create_personal_snippet)
end
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Mutations
module Snippets
class Destroy < Base
graphql_name 'DestroySnippet'
ERROR_MSG = 'Error deleting the snippet'
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the snippet to destroy'
def resolve(id:)
snippet = authorized_find!(id: id)
result = snippet.destroy
errors = result ? [] : [ERROR_MSG]
{
errors: errors
}
end
private
def ability_name
"admin"
end
end
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module Mutations
module Snippets
class Update < Base
graphql_name 'UpdateSnippet'
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the snippet to update'
argument :title, GraphQL::STRING_TYPE,
required: false,
description: 'Title of the snippet'
argument :file_name, GraphQL::STRING_TYPE,
required: false,
description: 'File name of the snippet'
argument :content, GraphQL::STRING_TYPE,
required: false,
description: 'Content of the snippet'
argument :description, GraphQL::STRING_TYPE,
required: false,
description: 'Description of the snippet'
argument :visibility_level, Types::VisibilityLevelsEnum,
description: 'The visibility level of the snippet',
required: false
def resolve(args)
snippet = authorized_find!(id: args.delete(:id))
result = UpdateSnippetService.new(snippet.project,
context[:current_user],
snippet,
args).execute
{
snippet: result ? snippet : snippet.reset,
errors: errors_on_object(snippet)
}
end
private
def ability_name
"update"
end
end
end
end

View File

@ -2,6 +2,8 @@
module Resolvers
class BaseResolver < GraphQL::Schema::Resolver
extend ::Gitlab::Utils::Override
def self.single
@single ||= Class.new(self) do
def resolve(**args)
@ -36,5 +38,13 @@ module Resolvers
# complexity difference is minimal in this case.
[args[:iid], args[:iids]].any? ? 0 : 0.01
end
override :object
def object
super.tap do |obj|
# If the field this resolver is used in is wrapped in a presenter, go back to it's subject
break obj.subject if obj.is_a?(Gitlab::View::Presenter::Base)
end
end
end
end

View File

@ -25,6 +25,9 @@ module Types
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone
mount_mutation Mutations::Snippets::Destroy
mount_mutation Mutations::Snippets::Update
mount_mutation Mutations::Snippets::Create
end
end

View File

@ -44,8 +44,8 @@ module Types
description: 'Description of the snippet',
null: true
field :visibility, GraphQL::STRING_TYPE,
description: 'Visibility of the snippet',
field :visibility_level, Types::VisibilityLevelsEnum,
description: 'Visibility Level of the snippet',
null: false
field :created_at, Types::TimeType,

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Types
class VisibilityLevelsEnum < BaseEnum
Gitlab::VisibilityLevel.string_options.each do |name, int_value|
value name.downcase, value: int_value
end
end
end

View File

@ -30,6 +30,6 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
end
def ability_name(ability_prefix)
"#{ability_prefix}_#{snippet.class.underscore}".to_sym
"#{ability_prefix}_#{snippet.to_ability_name}".to_sym
end
end

View File

@ -10,7 +10,7 @@
%br
%div
%span= _('Each Runner can be in one of the following states:')
%span= _('Each Runner can be in one of the following states and/or belong to one of the following types:')
%ul
%li
%span.badge.badge-success shared
@ -120,7 +120,7 @@
.runners-content.content-list
.table-holder
.gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-10{ role: 'rowheader' }= _('Type')
.table-section.section-10{ role: 'rowheader' }= _('Type/State')
.table-section.section-10{ role: 'rowheader' }= _('Runner token')
.table-section.section-20{ role: 'rowheader' }= _('Description')
.table-section.section-10{ role: 'rowheader' }= _('Version')

View File

@ -0,0 +1,5 @@
---
title: Fixes wording on runner admin
merge_request:
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Added Snippets GraphQL mutations
merge_request: 20956
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Enable new job log by default
merge_request: 21543
author:
type: added

View File

@ -442,6 +442,66 @@ type CreateNotePayload {
note: Note
}
"""
Autogenerated input type of CreateSnippet
"""
input CreateSnippetInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Content of the snippet
"""
content: String!
"""
Description of the snippet
"""
description: String
"""
File name of the snippet
"""
fileName: String
"""
The project full path the snippet is associated with
"""
projectPath: ID
"""
Title of the snippet
"""
title: String!
"""
The visibility level of the snippet
"""
visibilityLevel: VisibilityLevelsEnum!
}
"""
Autogenerated return type of CreateSnippet
"""
type CreateSnippetPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The snippet after mutation
"""
snippet: Snippet
}
type Design implements Noteable {
diffRefs: DiffRefs!
@ -861,6 +921,41 @@ type DestroyNotePayload {
note: Note
}
"""
Autogenerated input type of DestroySnippet
"""
input DestroySnippetInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the snippet to destroy
"""
id: ID!
}
"""
Autogenerated return type of DestroySnippet
"""
type DestroySnippetPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The snippet after mutation
"""
snippet: Snippet
}
type DetailedStatus {
detailsPath: String!
favicon: String!
@ -3737,9 +3832,11 @@ type Mutation {
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
createNote(input: CreateNoteInput!): CreateNotePayload
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
@ -3757,6 +3854,7 @@ type Mutation {
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
updateNote(input: UpdateNoteInput!): UpdateNotePayload
updateSnippet(input: UpdateSnippetInput!): UpdateSnippetPayload
}
"""
@ -5396,9 +5494,9 @@ type Snippet implements Noteable {
userPermissions: SnippetPermissions!
"""
Visibility of the snippet
Visibility Level of the snippet
"""
visibility: String!
visibilityLevel: VisibilityLevelsEnum!
"""
Web URL of the snippet
@ -6120,6 +6218,66 @@ type UpdateNotePayload {
note: Note
}
"""
Autogenerated input type of UpdateSnippet
"""
input UpdateSnippetInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Content of the snippet
"""
content: String
"""
Description of the snippet
"""
description: String
"""
File name of the snippet
"""
fileName: String
"""
The global id of the snippet to update
"""
id: ID!
"""
Title of the snippet
"""
title: String
"""
The visibility level of the snippet
"""
visibilityLevel: VisibilityLevelsEnum
}
"""
Autogenerated return type of UpdateSnippet
"""
type UpdateSnippetPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The snippet after mutation
"""
snippet: Snippet
}
scalar Upload
type User {
@ -6286,6 +6444,12 @@ type UserPermissions {
createSnippet: Boolean!
}
enum VisibilityLevelsEnum {
internal
private
public
}
enum VisibilityScopesEnum {
internal
private

View File

@ -6442,8 +6442,8 @@
"deprecationReason": null
},
{
"name": "visibility",
"description": "Visibility of the snippet",
"name": "visibilityLevel",
"description": "Visibility Level of the snippet",
"args": [
],
@ -6451,8 +6451,8 @@
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"kind": "ENUM",
"name": "VisibilityLevelsEnum",
"ofType": null
}
},
@ -6828,6 +6828,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VisibilityLevelsEnum",
"description": null,
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "private",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "internal",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "public",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VisibilityScopesEnum",
@ -15804,6 +15833,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createSnippet",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateSnippetInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "CreateSnippetPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "designManagementDelete",
"description": null,
@ -15885,6 +15941,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroySnippet",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DestroySnippetInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DestroySnippetPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "epicSetSubscription",
"description": null,
@ -16343,6 +16426,33 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateSnippet",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateSnippetInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateSnippetPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -19093,6 +19203,420 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DestroySnippetPayload",
"description": "Autogenerated return type of DestroySnippet",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippet",
"description": "The snippet after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Snippet",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DestroySnippetInput",
"description": "Autogenerated input type of DestroySnippet",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to destroy",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateSnippetPayload",
"description": "Autogenerated return type of UpdateSnippet",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippet",
"description": "The snippet after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Snippet",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateSnippetInput",
"description": "Autogenerated input type of UpdateSnippet",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "title",
"description": "Title of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "fileName",
"description": "File name of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "content",
"description": "Content of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "description",
"description": "Description of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "visibilityLevel",
"description": "The visibility level of the snippet",
"type": {
"kind": "ENUM",
"name": "VisibilityLevelsEnum",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CreateSnippetPayload",
"description": "Autogenerated return type of CreateSnippet",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippet",
"description": "The snippet after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Snippet",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "CreateSnippetInput",
"description": "Autogenerated input type of CreateSnippet",
"fields": null,
"inputFields": [
{
"name": "title",
"description": "Title of the snippet",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "fileName",
"description": "File name of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "content",
"description": "Content of the snippet",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "description",
"description": "Description of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "visibilityLevel",
"description": "The visibility level of the snippet",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "VisibilityLevelsEnum",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "projectPath",
"description": "The project full path the snippet is associated with",
"type": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DesignManagementUploadPayload",

View File

@ -92,6 +92,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
### CreateSnippetPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `snippet` | Snippet | The snippet after mutation |
### Design
| Name | Type | Description |
@ -145,6 +153,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
### DestroySnippetPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `snippet` | Snippet | The snippet after mutation |
### DetailedStatus
| Name | Type | Description |
@ -802,7 +818,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `fileName` | String | File Name of the snippet |
| `content` | String! | Content of the snippet |
| `description` | String | Description of the snippet |
| `visibility` | String! | Visibility of the snippet |
| `visibilityLevel` | VisibilityLevelsEnum! | Visibility Level of the snippet |
| `createdAt` | Time! | Timestamp this snippet was created |
| `updatedAt` | Time! | Timestamp this snippet was updated |
| `webUrl` | String! | Web URL of the snippet |
@ -929,6 +945,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
### UpdateSnippetPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `snippet` | Snippet | The snippet after mutation |
### User
| Name | Type | Description |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 272 KiB

View File

@ -149,12 +149,13 @@ The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/14664) in GitLab
> 12.0.
Job logs are divided into sections that can be collapsed or expanded.
Job logs are divided into sections that can be collapsed or expanded. Each section will display
the duration.
In the following example:
- Two sections are expanded and can be collapsed.
- One section is collapsed and can be expanded.
- Two sections are collapsed and can be expanded.
- Three sections are expanded and can be collapsed.
![Collapsible sections](img/collapsible_log.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -76,7 +76,7 @@ To the right of the filters, you should see a **Hide dismissed** toggle button.
NOTE: **Note:**
The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
![dashboard with action buttons and metrics](img/group_security_dashboard_v12_4.png)
![dashboard with action buttons and metrics](img/group_security_dashboard_v12_6.png)
Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed**
toggle button will let you also see vulnerabilities that have been dismissed.
@ -97,6 +97,17 @@ vulnerabilities your projects had at various points in time. You can filter amon
90 days, with the default being 90. Hover over the chart to get more details about
the open vulnerabilities at a specific time.
Below the timeline chart is a list of projects, grouped and sorted by the severity of the vulnerability found:
- F: 1 or more "critical"
- D: 1 or more "high" or "unknown"
- C: 1 or more "medium"
- B: 1 or more "low"
- A: 0 vulnerabilities
Projects with no vulnerability tests configured will not appear in the list. Additionally, dismissed
vulnerabilities are not included either.
Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
## Keeping the dashboards up to date

View File

@ -6,6 +6,8 @@ module Gitlab
module AuthorizeResource
extend ActiveSupport::Concern
RESOURCE_ACCESS_ERROR = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
class_methods do
def required_permissions
# If the `#authorize` call is used on multiple classes, we add the
@ -38,8 +40,7 @@ module Gitlab
def authorize!(object)
unless authorized_resource?(object)
raise Gitlab::Graphql::Errors::ResourceNotAvailable,
"The resource that you are attempting to access does not exist or you don't have permission to perform this action"
raise_resource_not_avaiable_error!
end
end
@ -61,6 +62,10 @@ module Gitlab
Ability.allowed?(current_user, ability, object, scope: :user)
end
end
def raise_resource_not_avaiable_error!
raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR
end
end
end
end

View File

@ -153,6 +153,11 @@ msgid_plural "%d more comments"
msgstr[0] ""
msgstr[1] ""
msgid "%d project"
msgid_plural "%d projects"
msgstr[0] ""
msgstr[1] ""
msgid "%d request with warnings"
msgid_plural "%d requests with warnings"
msgstr[0] ""
@ -5139,6 +5144,9 @@ msgstr ""
msgid "Creation date"
msgstr ""
msgid "Critical vulnerabilities present"
msgstr ""
msgid "Cron Timezone"
msgstr ""
@ -6225,6 +6233,9 @@ msgstr ""
msgid "Dynamic Application Security Testing (DAST)"
msgstr ""
msgid "Each Runner can be in one of the following states and/or belong to one of the following types:"
msgstr ""
msgid "Each Runner can be in one of the following states:"
msgstr ""
@ -9205,6 +9216,9 @@ msgstr ""
msgid "Hiding all labels"
msgstr ""
msgid "High or unknown vulnerabilities present"
msgstr ""
msgid "Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0."
msgstr ""
@ -10638,6 +10652,9 @@ msgstr ""
msgid "Logs|To see the pod logs, deploy your code to an environment."
msgstr ""
msgid "Low vulnerabilities present"
msgstr ""
msgid "MB"
msgstr ""
@ -10887,6 +10904,9 @@ msgstr ""
msgid "Median"
msgstr ""
msgid "Medium vulnerabilities present"
msgstr ""
msgid "Member lock"
msgstr ""
@ -11818,6 +11838,9 @@ msgstr ""
msgid "No vulnerabilities found for this project"
msgstr ""
msgid "No vulnerabilities present"
msgstr ""
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
@ -13575,6 +13598,12 @@ msgstr ""
msgid "Project path"
msgstr ""
msgid "Project security status"
msgstr ""
msgid "Project security status help page"
msgstr ""
msgid "Project slug"
msgstr ""
@ -13944,6 +13973,9 @@ msgstr ""
msgid "Projects Successfully Retrieved"
msgstr ""
msgid "Projects are graded based on the highest severity vulnerability present"
msgstr ""
msgid "Projects shared with %{group_name}"
msgstr ""
@ -13953,6 +13985,21 @@ msgstr ""
msgid "Projects to index"
msgstr ""
msgid "Projects with critical vulnerabilities"
msgstr ""
msgid "Projects with high or unknown vulnerabilities"
msgstr ""
msgid "Projects with low vulnerabilities"
msgstr ""
msgid "Projects with medium vulnerabilities"
msgstr ""
msgid "Projects with no vulnerabilities and security scanning enabled"
msgstr ""
msgid "Projects with write access"
msgstr ""
@ -18870,6 +18917,9 @@ msgstr ""
msgid "Type"
msgstr ""
msgid "Type/State"
msgstr ""
msgid "U2F Devices (%{length})"
msgstr ""
@ -18903,6 +18953,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
msgid "Unable to fetch vulnerable projects"
msgstr ""
msgid "Unable to generate new instance ID"
msgstr ""
@ -21756,6 +21809,9 @@ msgstr ""
msgid "severity|Medium"
msgstr ""
msgid "severity|None"
msgstr ""
msgid "severity|Undefined"
msgstr ""

View File

@ -11,7 +11,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
stub_feature_flags(ci_enable_live_trace: true)
stub_feature_flags(job_log_json: false)
stub_not_protect_default_branch
end
@ -527,7 +526,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET trace.json' do
before do
stub_feature_flags(job_log_json: true)
get_trace
end
@ -634,6 +632,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET legacy trace.json' do
before do
stub_feature_flags(job_log_json: false)
get_trace
end

View File

@ -10,7 +10,6 @@ describe 'Project Jobs Permissions' do
let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
stub_feature_flags(job_log_json: true)
sign_in(user)
project.enable_ci

View File

@ -10,8 +10,6 @@ describe 'User browses a job', :js do
let!(:build) { create(:ci_build, :success, :trace_artifact, :coverage, pipeline: pipeline) }
before do
stub_feature_flags(job_log_json: false)
project.add_maintainer(user)
project.enable_ci
@ -24,7 +22,7 @@ describe 'User browses a job', :js do
wait_for_requests
expect(page).to have_content("Job ##{build.id}")
expect(page).to have_css('.js-build-trace')
expect(page).to have_css('.job-log')
# scroll to the top of the page first
execute_script "window.scrollTo(0,0)"

View File

@ -22,7 +22,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
before do
project.add_role(user, user_access_level)
sign_in(user)
stub_feature_flags(job_log_json: false)
end
describe "GET /:project/jobs" do
@ -810,7 +809,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders job log' do
wait_for_all_requests
expect(page).to have_selector('.js-build-trace')
expect(page).to have_selector('.job-log')
end
end

View File

@ -7,10 +7,6 @@ describe "Internal Project Access" do
set(:project) { create(:project, :internal, :repository) }
before do
stub_feature_flags(job_log_json: false)
end
describe "Project should be internal" do
describe '#internal?' do
subject { project.internal? }

View File

@ -7,10 +7,6 @@ describe "Private Project Access" do
set(:project) { create(:project, :private, :repository, public_builds: false) }
before do
stub_feature_flags(job_log_json: false)
end
describe "Project should be private" do
describe '#private?' do
subject { project.private? }

View File

@ -7,10 +7,6 @@ describe "Public Project Access" do
set(:project) { create(:project, :public, :repository) }
before do
stub_feature_flags(job_log_json: false)
end
describe "Project should be public" do
describe '#public?' do
subject { project.public? }

View File

@ -298,4 +298,28 @@ describe('URL utility', () => {
expect(urlUtils.objectToQuery(searchQueryObject)).toEqual('one=1&two=2');
});
});
describe('joinPaths', () => {
it.each`
paths | expected
${['foo', 'bar']} | ${'foo/bar'}
${['foo/', 'bar']} | ${'foo/bar'}
${['foo//', 'bar']} | ${'foo/bar'}
${['abc/', '/def']} | ${'abc/def'}
${['foo', '/bar']} | ${'foo/bar'}
${['foo', '/bar/']} | ${'foo/bar/'}
${['foo', '//bar/']} | ${'foo/bar/'}
${['foo', '', '/bar']} | ${'foo/bar'}
${['foo', '/bar', '']} | ${'foo/bar'}
${['/', '', 'foo/bar/ ', '', '/ninja']} | ${'/foo/bar/ /ninja'}
${['', '/ninja', '/', ' ', '', 'bar', ' ']} | ${'/ninja/ /bar/ '}
${['http://something/bar/', 'foo']} | ${'http://something/bar/foo'}
${['foo/bar', null, 'ninja', null]} | ${'foo/bar/ninja'}
${[null, 'abc/def', 'zoo']} | ${'abc/def/zoo'}
${['', '', '']} | ${''}
${['///', '/', '//']} | ${'/'}
`('joins paths $paths => $expected', ({ paths, expected }) => {
expect(urlUtils.joinPaths(...paths)).toBe(expected);
});
});
});

View File

@ -8,8 +8,12 @@ describe Resolvers::BaseResolver do
let(:resolver) do
Class.new(described_class) do
def resolve(**args)
process(object)
[args, args]
end
def process(obj); end
end
end
@ -69,4 +73,26 @@ describe Resolvers::BaseResolver do
expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
end
end
describe '#object' do
let_it_be(:user) { create(:user) }
it 'returns object' do
expect_next_instance_of(resolver) do |r|
expect(r).to receive(:process).with(user)
end
resolve(resolver, obj: user)
end
context 'when object is a presenter' do
it 'returns presented object' do
expect_next_instance_of(resolver) do |r|
expect(r).to receive(:process).with(user)
end
resolve(resolver, obj: UserPresenter.new(user))
end
end
end
end

View File

@ -6,7 +6,7 @@ describe GitlabSchema.types['Snippet'] do
it 'has the correct fields' do
expected_fields = [:id, :title, :project, :author,
:file_name, :content, :description,
:visibility, :created_at, :updated_at,
:visibility_level, :created_at, :updated_at,
:web_url, :raw_url, :notes, :discussions,
:user_permissions, :description_html]

View File

@ -0,0 +1,144 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Creating a Snippet' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:content) { 'Initial content' }
let(:description) { 'Initial description' }
let(:title) { 'Initial title' }
let(:file_name) { 'Initial file_name' }
let(:visibility_level) { 'public' }
let(:project_path) { nil }
let(:mutation) do
variables = {
content: content,
description: description,
visibility_level: visibility_level,
file_name: file_name,
title: title,
project_path: project_path
}
graphql_mutation(:create_snippet, variables)
end
def mutation_response
graphql_mutation_response(:create_snippet)
end
context 'when the user does not have permission' do
let(:current_user) { nil }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not create the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Snippet.count }
end
context 'when user is not authorized in the project' do
let(:project_path) { project.full_path }
it 'does not create the snippet when the user is not authorized' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Snippet.count }
end
end
end
context 'when the user has permission' do
let(:current_user) { user }
context 'with PersonalSnippet' do
it 'creates the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['content']).to eq(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
expect(mutation_response['snippet']['project']).to be_nil
end
end
context 'with ProjectSnippet' do
let(:project_path) { project.full_path }
before do
project.add_developer(current_user)
end
it 'creates the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['content']).to eq(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
expect(mutation_response['snippet']['project']['fullPath']).to eq(project_path)
end
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
it 'returns an an error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when the feature is disabled' do
it 'returns an an error' do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
context 'when there are ActiveRecord validation errors' do
let(:title) { '' }
it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
it 'does not create the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Snippet.count }
end
it 'does not return Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']).to be_nil
end
end
end
end

View File

@ -0,0 +1,89 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Destroying a Snippet' do
include GraphqlHelpers
let(:current_user) { snippet.author }
let(:mutation) do
variables = {
id: snippet.to_global_id.to_s
}
graphql_mutation(:destroy_snippet, variables)
end
def mutation_response
graphql_mutation_response(:destroy_snippet)
end
shared_examples 'graphql delete actions' do
context 'when the user does not have permission' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not destroy the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { Snippet.count }
end
end
context 'when the user has permission' do
it 'destroys the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change { Snippet.count }.by(-1)
end
it 'returns an empty Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response).to have_key('snippet')
expect(mutation_response['snippet']).to be_nil
end
end
end
describe 'PersonalSnippet' do
it_behaves_like 'graphql delete actions' do
let_it_be(:snippet) { create(:personal_snippet) }
end
end
describe 'ProjectSnippet' do
let_it_be(:project) { create(:project, :private) }
let_it_be(:snippet) { create(:project_snippet, :private, project: project, author: create(:user)) }
context 'when the author is not a member of the project' do
it 'returns an an error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when the author is a member of the project' do
before do
project.add_developer(current_user)
end
it_behaves_like 'graphql delete actions'
context 'when the snippet project feature is disabled' do
it 'returns an an error' do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
end
end

View File

@ -0,0 +1,144 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Updating a Snippet' do
include GraphqlHelpers
let_it_be(:original_content) { 'Initial content' }
let_it_be(:original_description) { 'Initial description' }
let_it_be(:original_title) { 'Initial title' }
let_it_be(:original_file_name) { 'Initial file_name' }
let(:updated_content) { 'Updated content' }
let(:updated_description) { 'Updated description' }
let(:updated_title) { 'Updated_title' }
let(:updated_file_name) { 'Updated file_name' }
let(:current_user) { snippet.author }
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(snippet).to_s,
content: updated_content,
description: updated_description,
visibility_level: 'public',
file_name: updated_file_name,
title: updated_title
}
graphql_mutation(:update_snippet, variables)
end
def mutation_response
graphql_mutation_response(:update_snippet)
end
shared_examples 'graphql update actions' do
context 'when the user does not have permission' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it 'does not update the Snippet' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { snippet.reload }
end
end
context 'when the user has permission' do
it 'updates the Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(snippet.reload.title).to eq(updated_title)
end
it 'returns the updated Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['content']).to eq(updated_content)
expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description)
expect(mutation_response['snippet']['fileName']).to eq(updated_file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq('public')
end
context 'when there are ActiveRecord validation errors' do
let(:updated_title) { '' }
it_behaves_like 'a mutation that returns errors in the response', errors: ["Title can't be blank"]
it 'does not update the Snippet' do
post_graphql_mutation(mutation, current_user: current_user)
expect(snippet.reload.title).to eq(original_title)
end
it 'returns the Snippet with its original values' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['content']).to eq(original_content)
expect(mutation_response['snippet']['title']).to eq(original_title)
expect(mutation_response['snippet']['description']).to eq(original_description)
expect(mutation_response['snippet']['fileName']).to eq(original_file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
end
end
end
end
describe 'PersonalSnippet' do
it_behaves_like 'graphql update actions' do
let_it_be(:snippet) do
create(:personal_snippet,
:private,
file_name: original_file_name,
title: original_title,
content: original_content,
description: original_description)
end
end
end
describe 'ProjectSnippet' do
let_it_be(:project) { create(:project, :private) }
let_it_be(:snippet) do
create(:project_snippet,
:private,
project: project,
author: create(:user),
file_name: original_file_name,
title: original_title,
content: original_content,
description: original_description)
end
context 'when the author is not a member of the project' do
it 'returns an an error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when the author is a member of the project' do
before do
project.add_developer(current_user)
end
it_behaves_like 'graphql update actions'
context 'when the snippet project feature is disabled' do
it 'returns an an error' do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
end
end
end