提交 4279f24a 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 51c20446
......@@ -419,7 +419,7 @@ end
gem 'octokit', '~> 4.15'
# https://gitlab.com/gitlab-org/gitlab/issues/207207
gem 'gitlab-mail_room', '~> 0.0.3', require: 'mail_room'
gem 'gitlab-mail_room', '~> 0.0.4', require: 'mail_room'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
......
......@@ -391,7 +391,7 @@ GEM
opentracing (~> 0.4)
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.0.0)
gitlab-mail_room (0.0.3)
gitlab-mail_room (0.0.4)
gitlab-markup (1.7.0)
gitlab-net-dns (0.9.1)
gitlab-puma (4.3.3.gitlab.2)
......@@ -1244,7 +1244,7 @@ DEPENDENCIES
gitlab-chronic (~> 0.10.5)
gitlab-labkit (= 0.12.0)
gitlab-license (~> 1.0)
gitlab-mail_room (~> 0.0.3)
gitlab-mail_room (~> 0.0.4)
gitlab-markup (~> 1.7.0)
gitlab-net-dns (~> 0.9.1)
gitlab-puma (~> 4.3.3.gitlab.2)
......
......@@ -79,7 +79,7 @@ export default {
<icon name="chevron-left" /> {{ __('View jobs') }}
</button>
</header>
<div class="top-bar d-flex border-left-0">
<div class="top-bar d-flex border-left-0 mr-3">
<job-description :job="detailJob" />
<div class="controllers ml-auto">
<a
......@@ -97,7 +97,7 @@ export default {
<scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" />
</div>
</div>
<pre ref="buildTrace" class="build-trace mb-0 h-100" @scroll="scrollBuildLog">
<pre ref="buildTrace" class="build-trace mb-0 h-100 mr-3" @scroll="scrollBuildLog">
<code
v-show="!detailJob.isLoading"
class="bash"
......
......@@ -19,7 +19,7 @@ export default () => {
const { endpoint } = el.dataset;
const store = createStore();
const router = createRouter(endpoint, store);
const router = createRouter(endpoint);
store.dispatch('setInitialState', el.dataset);
const attachMainComponent = () =>
......
......@@ -157,6 +157,9 @@ export default {
return config;
},
},
mounted() {
this.requestTagsList({ params: this.$route.params.id });
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
setModalDescription(itemIndex = -1) {
......
......@@ -103,8 +103,16 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE;
},
},
mounted() {
this.loadImageList(this.$route.name);
},
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
loadImageList(fromName) {
if (!fromName || !this.images?.length) {
this.requestImagesList();
}
},
deleteImage(item) {
this.track('click_button');
this.itemToDelete = item;
......
......@@ -7,7 +7,7 @@ import { decodeAndParse } from './utils';
Vue.use(VueRouter);
export default function createRouter(base, store) {
export default function createRouter(base) {
const router = new VueRouter({
base,
mode: 'history',
......@@ -20,12 +20,6 @@ export default function createRouter(base, store) {
nameGenerator: () => s__('ContainerRegistry|Container Registry'),
root: true,
},
beforeEnter: (to, from, next) => {
if (!from.name || !store.state.images?.length) {
store.dispatch('requestImagesList');
}
next();
},
},
{
name: 'details',
......@@ -34,10 +28,6 @@ export default function createRouter(base, store) {
meta: {
nameGenerator: route => decodeAndParse(route.params.id).name,
},
beforeEnter: (to, from, next) => {
store.dispatch('requestTagsList', { params: to.params.id });
next();
},
},
],
});
......
......@@ -15,4 +15,5 @@ export const createStore = () =>
mutations,
});
// Deprecated and to be removed
export default createStore();
......@@ -10,6 +10,7 @@ import {
GlDropdown,
GlDropdownItem,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -30,6 +31,9 @@ export default {
TimeAgoTooltip,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
apollo: {
canCreateSnippet: {
query() {
......@@ -67,6 +71,10 @@ export default {
condition: this.snippet.userPermissions.updateSnippet,
text: __('Edit'),
href: this.editLink,
disabled: this.snippet.blob.binary,
title: this.snippet.blob.binary
? __('Snippets with non-text files can only be edited via Git.')
: undefined,
},
{
condition: this.snippet.userPermissions.adminSnippet,
......@@ -186,18 +194,24 @@ export default {
<div class="detail-page-header-actions">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
<gl-button
<div
v-if="action.condition"
:key="index"
:disabled="action.disabled"
:variant="action.variant"
:category="action.category"
:class="action.cssClass"
:href="action.href"
@click="action.click ? action.click() : undefined"
v-gl-tooltip
:title="action.title"
class="d-inline-block"
>
{{ action.text }}
</gl-button>
<gl-button
:disabled="action.disabled"
:variant="action.variant"
:category="action.category"
:class="action.cssClass"
:href="action.href"
@click="action.click ? action.click() : undefined"
>
{{ action.text }}
</gl-button>
</div>
</template>
</div>
<div class="d-block d-sm-none dropdown">
......@@ -205,6 +219,8 @@ export default {
<gl-dropdown-item
v-for="(action, index) in personalSnippetActions"
:key="index"
:disabled="action.disabled"
:title="action.title"
:href="action.href"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-dropdown-item
......
......@@ -9,7 +9,6 @@
top: 0;
font-size: 12px;
border-top-right-radius: $border-radius-default;
margin-left: -$gl-padding;
.controllers {
@include build-controllers(15px, center, false, 0, inline, 0);
......
......@@ -890,11 +890,15 @@ $ide-commit-header-height: 48px;
.multi-file-commit-panel-inner {
width: 350px;
padding: $grid-size $gl-padding;
padding: $grid-size 0;
background-color: $white;
border-left: 1px solid $white-dark;
}
.ide-right-sidebar-jobs-detail {
padding-bottom: 0;
}
.ide-right-sidebar-clientside {
padding: 0;
}
......@@ -915,15 +919,12 @@ $ide-commit-header-height: 48px;
margin: 0;
}
}
.build-trace {
margin-left: -$gl-padding;
}
}
.ide-pipeline-list {
flex: 1;
overflow: auto;
padding: 0 $gl-padding;
}
.ide-pipeline-header {
......@@ -966,6 +967,7 @@ $ide-commit-header-height: 48px;
.ide-job-header {
min-height: 60px;
padding: 0 $gl-padding;
}
.ide-nav-form {
......
......@@ -33,7 +33,7 @@
$diff-insert: rgba(155, 185, 85, 0.2);
$diff-remove: rgba(255, 0, 0, 0.2);
a {
a:not(.btn) {
color: $link-color;
}
......@@ -57,27 +57,46 @@
textarea,
.md-area.is-focused,
.ide-entry-dropdown-toggle,
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover,
.dropdown-menu li button,
.ide-merge-request-project-path,
.dropdown-menu-selectable li a.is-active,
.dropdown-menu-inner-title,
.dropdown-menu-inner-content {
.dropdown-menu-inner-content,
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover,
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a.active .badge.badge-pill,
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover .badge.badge-pill,
.badge.badge-pill,
.ide-navigator-button,
.bs-callout,
.ide-navigator-btn,
.ide-pipeline .top-bar,
.ide-pipeline .top-bar .controllers .controllers-buttons {
color: $text-color;
}
.drag-handle:hover,
.card-header .badge.badge-pill {
background-color: $dropdown-hover-background;
}
.modal-body {
color: $gl-text-color;
}
.dropdown-menu-toggle svg,
.dropdown-menu-toggle svg:hover,
.ide-tree-header svg,
.ide-tree-header:not(.ide-pipeline-header) svg,
.file-row .file-row-icon svg,
.file-row:hover .file-row-icon svg {
.file-row:hover .file-row-icon svg,
.controllers-buttons svg {
fill: $text-color;
}
.ide-pipeline svg {
--svg-status-bg: transparent;
}
.multi-file-tab-close:hover {
background-color: $input-border;
}
......@@ -118,7 +137,12 @@
.ide-commit-editor-header,
.ide-file-templates,
.ide-entry-dropdown-toggle,
.ide-staged-action-btn {
.ide-staged-action-btn,
.badge.badge-pill,
.card-header,
.bs-callout,
.ide-pipeline .top-bar,
.ide-terminal .top-bar {
background-color: $background;
}
......@@ -126,6 +150,18 @@
background-color: inherit;
}
.bs-callout {
border-color: $dropdown-background;
code {
background-color: $dropdown-background;
}
}
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover {
border-color: $dropdown-hover-background;
}
.ide-sidebar-link:hover,
.multi-file-tabs li {
background-color: $background-hover;
......@@ -144,7 +180,10 @@
.ide-sidebar-link.active::after,
.ide-right-sidebar .multi-file-commit-panel-inner,
.common-note-form .md-area,
.ide-commit-message-field {
.ide-commit-message-field,
.card,
.multi-file-commit-panel-success-message,
.ide-preview-header {
background-color: $highlight-background;
}
......@@ -163,7 +202,12 @@
.multi-file-tabs li,
.ide-status-bar,
.ide-commit-editor-header,
.ide-file-templates {
.ide-file-templates,
.card,
.card-header,
.ide-job-item:not(:last-child),
.ide-terminal .top-bar,
.ide-pipeline .top-bar {
border-color: $border-color;
}
......@@ -179,7 +223,9 @@
.multi-file-commit-form > form,
.multi-file-commit-form hr,
.ide-commit-list-container.is-first,
.multi-file-commit-form .nav-links:not(.quick-links) {
.multi-file-commit-form .nav-links:not(.quick-links),
.ide-pipeline-list .nav-links:not(.quick-links),
.ide-preview-header {
border-color: $background;
}
......@@ -201,7 +247,8 @@
}
}
.nav-links li.active a {
.nav-links li.active a,
.nav-links li a.active {
border-color: $highlight-accent;
color: $text-color;
}
......@@ -223,7 +270,7 @@
input[type='search'],
.filtered-search-box {
border-color: $input-border;
background-color: $input-background;
background: $input-background !important;
}
input[type='text'],
......@@ -252,16 +299,16 @@
background: $gray-800;
}
.btn:not(.btn-link):hover {
.btn:not(.btn-link):not([disabled]):hover {
border-width: 2px;
padding: 5px 9px;
}
.btn.btn-sm:hover {
.btn:not([disabled]).btn-sm:hover {
padding: 3px 9px;
}
.btn.btn-block:hover {
.btn:not([disabled]).btn-block:hover {
padding: 5px 0;
}
......
# 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"
authorize :update_alert_management_alerts
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
# 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, status).execute
end
def prepare_response(result)
{
alert: result.payload[:alert],
errors: result.error? ? [result.message] : []
}
end
end
end
end
......@@ -18,6 +18,11 @@ module Types
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,
......@@ -38,6 +43,11 @@ module Types
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,
......@@ -53,6 +63,21 @@ module Types
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
......@@ -7,6 +7,7 @@ module Types
graphql_name 'Mutation'
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
mount_mutation Mutations::AlertManagement::UpdateAlertStatus
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
......
......@@ -53,6 +53,12 @@ module AlertManagement
end
end
def details
details_payload = payload.except(*attributes.keys)
Gitlab::Utils::InlineHash.merge_keys(details_payload)
end
private
def hosts_length
......
......@@ -14,16 +14,12 @@ module Ci
delegate :ref_exists?, :create_ref, :delete_refs, to: :repository
def exist?
return unless enabled?
ref_exists?(path)
rescue
false
end
def create
return unless enabled?
create_ref(sha, path)
rescue => e
Gitlab::ErrorTracking
......@@ -31,8 +27,6 @@ module Ci
end
def delete
return unless enabled?
delete_refs(path)
rescue Gitlab::Git::Repository::NoRepository
# no-op
......@@ -44,11 +38,5 @@ module Ci
def path
"refs/#{Repository::REF_PIPELINES}/#{pipeline.id}"
end
private
def enabled?
Feature.enabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true)
end
end
end
......@@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :read_alert_management_alerts
enable :update_alert_management_alerts
enable :metrics_dashboard
end
......
......@@ -34,7 +34,6 @@ module Ci
def refspecs
specs = []
specs << refspec_for_pipeline_ref if should_expose_merge_request_ref?
specs << refspec_for_persistent_ref if persistent_ref_exist?
if git_depth > 0
......@@ -50,19 +49,6 @@ module Ci
private
# We will stop exposing merge request refs when we fully depend on persistent refs
# (i.e. remove `refspec_for_pipeline_ref` when we remove `depend_on_persistent_pipeline_ref` feature flag.)
# `ci_force_exposing_merge_request_refs` is an extra feature flag that allows us to
# forcibly expose MR refs even if the `depend_on_persistent_pipeline_ref` feature flag enabled.
# This is useful when we see an unexpected behaviors/reports from users.
# See https://gitlab.com/gitlab-org/gitlab/issues/35140.
def should_expose_merge_request_ref?
return false unless merge_request_ref?
return true if Feature.enabled?(:ci_force_exposing_merge_request_refs, project)
Feature.disabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true)
end
def create_archive(artifacts)
return unless artifacts[:untracked] || artifacts[:paths]
......@@ -100,10 +86,6 @@ module Ci
"+#{Gitlab::Git::TAG_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_TAG_PREFIX}#{ref}"
end
def refspec_for_pipeline_ref
"+#{ref}:#{ref}"
end
def refspec_for_persistent_ref
"+#{persistent_ref_path}:#{persistent_ref_path}"
end
......
# frozen_string_literal: true
module AlertManagement
class UpdateAlertStatusService
def initialize(alert, status)
@alert = alert
@status = status
end
def execute
return error('Invalid status') unless AlertManagement::Alert.statuses.key?(status.to_s)
alert.status = status
if alert.save
success
else
error(alert.errors.full_messages.to_sentence)
end
end
private
attr_reader :alert, :status
def success
ServiceResponse.success(payload: { alert: alert })
end
def error(message)
ServiceResponse.error(payload: { alert: alert }, message: message)
end
end
end
......@@ -28,6 +28,7 @@ module Spam
# update the spam log accordingly.
SpamLog.verify_recaptcha!(user_id: user.id, id: spam_log_id)
else
return if allowlisted?(user)
return unless request
return unless check_for_spam?
......@@ -39,6 +40,10 @@ module Spam
private
def allowlisted?(user)
user.respond_to?(:gitlab_employee) && user.gitlab_employee?
end
def perform_spam_service_check(api)
# since we can check for spam, and recaptcha is not verified,
# ask the SpamVerdictService what to do with the target.
......
- page_title _('Alert Details')
- add_to_breadcrumbs s_('AlertManagement|Alerts'), project_alert_management_index_path(@project)
- page_title s_('AlertManagement|Alert detail')
#js-alert_details
---
title: Skip spam check for GitLab team members on gitlab.com
merge_request: 31052
author:
type: added
---
title: Disabled Edit button for binary snippets
merge_request: 30904
author:
type: added
---
title: Add mutation for AlertManagement's Alert status
merge_request: 30576
author:
type: added
---
title: Exposes description, hosts, details, and timestamps for Alert Management Alert GraphQL
merge_request: 31091
author:
type: changed
---
title: Uses Kubernetes API conventions to create or update a resource leandrogs
merge_request: 29010
author: Leandro Silva
type: performance
---
title: Remove deprecated Sidekiq rake tasks
merge_request:
author:
type: removed
......@@ -62,11 +62,20 @@ This solution is appropriate for many teams that have a single server at their d
You can also optionally configure GitLab to use an [external PostgreSQL service](../external_database.md) or an [external object storage service](../high_availability/object_storage.md) for added performance and reliability at a relatively low complexity cost.
<!--
## Up to 2,000 users
For up to 2,000 users, defining the reference architecture is [being worked on](https://gitlab.com/gitlab-org/quality/performance/-/issues/223).
-->
> - **Supported users (approximate):** 2,000
> - **High Availability:** False
> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|---------------|----------------------------|
| GitLab Rails, Sidekiq, Consul ([1](#footnotes)) | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
| Cloud Object Storage ([4](#footnotes)) | - | - | - | - |
| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
## Up to 3,000 users
......@@ -79,7 +88,8 @@ server, a PostgreSQL server and a Redis server. A reference architecture with
this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/quality/performance/-/issues/223).
> - **Supported users (approximate):** 3,000
> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
> - **High Availability:** True
> - **Test RPS rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
|--------------------------------------------------------------|-------|---------------------------------|---------------|----------------------------|
......@@ -99,6 +109,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual
## Up to 5,000 users
> - **Supported users (approximate):** 5,000
> - **High Availability:** True
> - **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
......@@ -119,6 +130,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual
## Up to 10,000 users
> - **Supported users (approximate):** 10,000
> - **High Availability:** True
> - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
| Service | Nodes | GCP Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
......@@ -142,6 +154,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual
## Up to 25,000 users
> - **Supported users (approximate):** 25,000
> - **High Availability:** True
> - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
......@@ -165,6 +178,7 @@ this alternative in mind is [being worked on](https://gitlab.com/gitlab-org/qual
## Up to 50,000 users
> - **Supported users (approximate):** 50,000
> - **High Availability:** True
> - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
| Service | Nodes | Configuration ([8](#footnotes)) | GCP type | AWS type ([9](#footnotes)) |
......@@ -288,7 +302,10 @@ column.
## Footnotes
1. In our architectures we run each GitLab Rails node using the Puma webserver
and have its number of workers set to 90% of available CPUs along with four threads.
and have its number of workers set to 90% of available CPUs along with four threads. For
nodes that are running Rails with other components the worker value should be reduced
accordingly where we've found 50% achieves a good balance but this is dependent
on workload.
1. Gitaly node requirements are dependent on customer data, specifically the number of
projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
......
......@@ -142,6 +142,21 @@ type AdminSidekiqQueuesDeleteJobsPayload {
Describes an alert from the project's Alert Management
"""
type AlertManagementAlert {
"""
Timestamp the alert was created
"""
createdAt: Time
"""
Description of the alert
"""
description: String
"""
Alert details
"""
details: JSON
"""
Timestamp the alert ended
"""
......@@ -152,6 +167,11 @@ type AlertManagementAlert {
"""
eventCount: Int
"""
List of hosts the alert came from
"""
hosts: [String!]
"""
Internal ID of the alert
"""
......@@ -186,6 +206,11 @@ type AlertManagementAlert {
Title of the alert
"""
title: String
"""
Timestamp the alert was last updated
"""
updatedAt: Time
}
"""
......@@ -6074,6 +6099,7 @@ type Mutation {
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
"""
......@@ -9729,6 +9755,51 @@ enum TypeEnum {
project
}
"""
Autogenerated input type of UpdateAlertStatus
"""
input UpdateAlertStatusInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the alert to mutate
"""
iid: String!
"""
The project the alert to mutate is in
"""
projectPath: ID!
"""
The status to set the alert
"""
status: AlertManagementStatus!
}
"""
Autogenerated return type of UpdateAlertStatus
"""
type UpdateAlertStatusPayload {
"""
The alert after mutation
"""
alert: AlertManagementAlert
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
}
input UpdateDiffImagePositionInput {
"""
Total height of the image
......
......@@ -394,6 +394,48 @@
"name": "AlertManagementAlert",
"description": "Describes an alert from the project's Alert Management",
"fields": [
{
"name": "createdAt",
"description": "Timestamp the alert was created",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the alert",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "details",
"description": "Alert details",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "JSON",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endedAt",
"description": "Timestamp the alert ended",
......@@ -422,6 +464,28 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hosts",
"description": "List of hosts the alert came from",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "iid",
"description": "Internal ID of the alert",
......@@ -523,6 +587,20 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "Timestamp the alert was last updated",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -18129,6 +18207,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateAlertStatus",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateAlertStatusInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateAlertStatusPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateEpic",
"description": null,
......@@ -29248,6 +29353,136 @@
],
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateAlertStatusInput",
"description": "Autogenerated input type of UpdateAlertStatus",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the alert to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the alert to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "status",
"description": "The status to set the alert",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "AlertManagementStatus",
"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": "UpdateAlertStatusPayload",
"description": "Autogenerated return type of UpdateAlertStatus",
"fields": [
{
"name": "alert",
"description": "The alert after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"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
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateDiffImagePositionInput",
......
......@@ -52,8 +52,12 @@ Describes an alert from the project's Alert Management
| Name | Type | Description |
| --- | ---- | ---------- |
| `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert |
| `details` | JSON | Alert details |
| `endedAt` | Time | Timestamp the alert ended |
| `eventCount` | Int | Number of events of this alert |
| `hosts` | String! => Array | List of hosts the alert came from |
| `iid` | ID! | Internal ID of the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `service` | String | Service the alert came from |
......@@ -61,6 +65,7 @@ Describes an alert from the project's Alert Management
| `startedAt` | Time | Timestamp the alert was raised |
| `status` | AlertManagementStatus | Status of the alert |
| `title` | String | Title of the alert |
| `updatedAt` | Time | Timestamp the alert was last updated |
## AwardEmoji
......@@ -1515,6 +1520,16 @@ Represents a directory
| `type` | EntryType! | Type of tree entry |
| `webUrl` | String | Web URL for the tree entry (directory) |
## UpdateAlertStatusPayload
Autogenerated return type of UpdateAlertStatus
| Name | Type | Description |
| --- | ---- | ---------- |
| `alert` | AlertManagementAlert | The alert after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
## UpdateEpicPayload
Autogenerated return type of UpdateEpic
......
......@@ -549,15 +549,3 @@ To illustrate its life cycle:
even if the commit history of the `example` branch has been overwritten by force-push.
1. GitLab Runner fetches the persistent pipeline ref and gets source code from the checkout-SHA.
1. When the pipeline finished, its persistent ref is cleaned up in a background process.
NOTE: **NOTE**: At this moment, this feature is on by default and can be manually disabled
by disabling `depend_on_persistent_pipeline_ref` feature flag. If you're interested in
manually disabling this behavior, please ask the administrator
to execute the following commands in rails console.
```shell
> sudo gitlab-rails console # Login to Rails console of GitLab instance.
> project = Project.find_by_full_path('namespace/project-name') # Get the project instance.
> Feature.disable(:depend_on_persistent_pipeline_ref, project) # Disable the feature flag for specific project
> Feature.disable(:depend_on_persistent_pipeline_ref) # Disable the feature flag system-wide
```
......@@ -63,17 +63,52 @@ Here's a list of the AWS services we will use, with links to pricing information
NOTE: **Note:** Please note that while we will be using EBS for storage, we do not recommend using EFS as it may negatively impact GitLab's performance. You can review the [relevant documentation](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
## Creating an IAM EC2 instance role and profile
## Create an IAM EC2 instance role and profile
As we'll be using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances need to have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab config, we'll make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We'll need to create an IAM policy to attach to our IAM role:
### Create an IAM Policy
1. Navigate to the IAM dashboard and click on **Policies** in the left menu.
1. Click **Create policy**, select the `JSON` tab, and add a policy. We want to [follow security best practices and grant _least privilege_](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), giving our role only the permissions needed to perform the required actions.
1. Assuming you prefix the S3 bucket names with `gl-` as shown in the diagram, add the following policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:AbortMultipartUpload",
"s3::CompleteMultipartUpload",
"s3:ListBucket",
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:PutObjectAcl"
],
"Resource": [
"arn:aws:s3:::gl-*/*"
]
}
]
}
```
1. Click **Review policy**, give your policy a name (we'll use `gl-s3-policy`), and click **Create policy**.
To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
role with limited access:
### Create an IAM Role
1. Navigate to the IAM dashboard <https://console.aws.amazon.com/iam/home>, click on **Roles** in the left menu, and
1. Still on the IAM dashboard, click on **Roles** in the left menu, and
click **Create role**.
1. Create a new role by selecting **AWS service > EC2**, then click
**Next: Permissions**.
1. Choose **AmazonEC2FullAccess** and **AmazonS3FullAccess**, click **Tags** and add tags if needed.
1. Click **Review**, give your role the name (we'll use `GitLabAdmin`), and click **Create role**.
1. In the policy filter, search for the `gl-s3-policy` we created above, select it, and click **Tags**.
1. Add tags if needed and click **Review**.
1. Give the role a name (we'll use `GitLabS3Access`) and click **Create Role**.
We'll use this role when we [create a launch configuration](#create-a-launch-configuration) later on.
## Configuring the network
......@@ -575,7 +610,10 @@ HostKey /etc/ssh_static/ssh_host_ed25519_key
#### Amazon S3 object storage
Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our [documentation includes configuration instructions](../../administration/object_storage.md) for each of these, and other information about using object storage with GitLab.
Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our documentation includes [instructions on how to configure object storage](../../administration/object_storage.md) for each of these data types, and other information about using object storage with GitLab.
NOTE: **Note:**
Since we are using the [AWS IAM profile](#create-an-iam-role) we created earlier, be sure to omit the AWS access key and secret access key/value pairs when configuring object storage. Instead, use `'use_iam_profile' => true` in your configuration as shown in the object storage documentation linked above.
Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
......@@ -611,7 +649,7 @@ From the EC2 dashboard:
1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and click **Configure details**.
1. Enter a name for your launch configuration (we'll use `gitlab-ha-launch-config`).
1. **Do not** check **Request Spot Instance**.
1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#creating-an-iam-ec2-instance-role-and-profile).
1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#create-an-iam-ec2-instance-role-and-profile).
1. Leave the rest as defaults and click **Add Storage**.
1. The root volume is 8GiB by default and should be enough given that we won’t store any data there. Click **Configure Security Group**.
1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
......
......@@ -70,7 +70,10 @@ module Gitlab
end
def filter_allowed(current_user, resolved_type, authorizing_object)
if authorizing_object
if resolved_type.nil?
# We're not rendering anything, for example when a record was not found
# no need to do anything
elsif authorizing_object
# Authorizing fields representing scalars, or a simple field with an object
resolved_type if allowed_access?(current_user, authorizing_object)
elsif @field.connection?
......@@ -83,9 +86,6 @@ module Gitlab
resolved_type.select do |single_object_type|
allowed_access?(current_user, single_object_type.object)
end
elsif resolved_type.nil?
# We're not rendering anything, for example when a record was not found
# no need to do anything
else
raise "Can't authorize #{@field}"
end
......
......@@ -99,11 +99,7 @@ module Gitlab
command.cluster_role_binding_resource.tap do |cluster_role_binding_resource|
break unless cluster_role_binding_resource
if cluster_role_binding_exists?(cluster_role_binding_resource)
kubeclient.update_cluster_role_binding(cluster_role_binding_resource)
else
kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
end
kubeclient.update_cluster_role_binding(cluster_role_binding_resource)
end
end
......
......@@ -57,9 +57,7 @@ module Gitlab
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client
delegate :create_cluster_role_binding,
:get_cluster_role_binding,
:update_cluster_role_binding,
delegate :update_cluster_role_binding,
to: :rbac_client
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
......@@ -71,9 +69,7 @@ module Gitlab
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client
delegate :create_role_binding,
:get_role_binding,
:update_role_binding,
delegate :update_role_binding,
to: :rbac_client
# non-entity methods that can only work with the core client
......@@ -134,19 +130,11 @@ module Gitlab
end
def create_or_update_cluster_role_binding(resource)
if cluster_role_binding_exists?(resource)
update_cluster_role_binding(resource)
else
create_cluster_role_binding(resource)
end
update_cluster_role_binding(resource)
end
def create_or_update_role_binding(resource)
if role_binding_exists?(resource)
update_role_binding(resource)
else
create_role_binding(resource)
end
update_role_binding(resource)
end
def create_or_update_service_account(resource)
......@@ -173,18 +161,6 @@ module Gitlab
Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false)
end
def cluster_role_binding_exists?(resource)
get_cluster_role_binding(resource.metadata.name)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def role_binding_exists?(resource)
get_role_binding(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
false
end
def service_account_exists?(resource)
get_service_account(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
......
......@@ -48,7 +48,6 @@ module Gitlab
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def worker_queues(rails_path = Rails.root.to_s)
# https://gitlab.com/gitlab-org/gitlab/issues/199230
worker_names(all_queues(rails_path))
end
......@@ -75,7 +74,7 @@ module Gitlab
private
def worker_names(workers)
workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue }
workers.map { |queue| queue[:name] }
end
def query_string_to_lambda(query_string)
......
namespace :sidekiq do
def deprecation_warning!
warn <<~WARNING
This task is deprecated and will be removed in 13.0 as it is thought to be unused.
If you are using this task, please comment on the below issue:
https://gitlab.com/gitlab-org/gitlab/issues/196731
WARNING
end
desc '[DEPRECATED] GitLab | Sidekiq | Stop sidekiq'
task :stop do
deprecation_warning!
system(*%w(bin/background_jobs stop))
end
desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq'
task :start do
deprecation_warning!
system(*%w(bin/background_jobs start))
end
desc '[DEPRECATED] GitLab | Sidekiq | Restart sidekiq'
task :restart do
deprecation_warning!
system(*%w(bin/background_jobs restart))
end
desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq with launchd on Mac OS X'
task :launchd do
deprecation_warning!
system(*%w(bin/background_jobs start_silent))
end
end
......@@ -1697,15 +1697,18 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
msgid "Alert Details"
msgstr ""
msgid "AlertManagement|Acknowledged"
msgstr ""
msgid "AlertManagement|Alert"
msgstr ""
msgid "AlertManagement|Alert detail"
msgstr ""
msgid "AlertManagement|Alerts"
msgstr ""
msgid "AlertManagement|Authorize external service"
msgstr ""
......@@ -19264,6 +19267,9 @@ msgstr ""
msgid "Snippets"
msgstr ""
msgid "Snippets with non-text files can only be edited via Git."
msgstr ""
msgid "SnippetsEmptyState|Code snippets"
msgstr ""
......
......@@ -4,7 +4,12 @@ import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue';
import { createStore } from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING, SET_INITIAL_STATE } from '~/registry/explorer/stores/mutation_types/';
import {
SET_MAIN_LOADING,
SET_INITIAL_STATE,
SET_TAGS_LIST_SUCCESS,
SET_TAGS_PAGINATION,
} from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
......@@ -60,7 +65,9 @@ describe('Details Page', () => {
beforeEach(() => {
store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveTagsListSuccess', tagsListResponse);
dispatchSpy.mockResolvedValue();
store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
jest.spyOn(Tracking, 'event');
});
......
import VueRouter from 'vue-router';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
......@@ -7,22 +6,25 @@ import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdo
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue';
import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { createStore } from '~/registry/explorer/stores/';
import {
SET_MAIN_LOADING,
SET_IMAGES_LIST_SUCCESS,
SET_PAGINATION,
SET_INITIAL_STATE,
} from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
} from '~/registry/explorer/constants';
import { imagesListResponse } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
import { GlModal, GlEmptyState, RouterLink } from '../stubs';
import { $toast } from '../../shared/mocks';
const localVue = createLocalVue();
localVue.use(VueRouter);
describe('List Page', () => {
let wrapper;
let dispatchSpy;
let store;
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal);
......@@ -39,21 +41,31 @@ describe('List Page', () => {
const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert);
const findDeleteAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
const mountComponent = ({ mocks } = {}) => {
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlModal,
GlEmptyState,
GlSprintf,
RouterLink,
},
mocks: {
$toast,
$route: {
name: 'foo',
},
...mocks,
},
});
};
beforeEach(() => {
store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
store.dispatch('receiveImagesListSuccess', imagesListResponse);
dispatchSpy.mockResolvedValue();
store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data);
store.commit(SET_PAGINATION, imagesListResponse.headers);
});
afterEach(() => {
......@@ -61,17 +73,38 @@ describe('List Page', () => {
});
describe('Expiration policy notification', () => {
beforeEach(() => {
mountComponent();
});
it('shows up on project page', () => {
expect(findProjectPolicyAlert().exists()).toBe(true);
});
it('does show up on group page', () => {
store.dispatch('setInitialState', { isGroupPage: true });
store.commit(SET_INITIAL_STATE, { isGroupPage: true });
return wrapper.vm.$nextTick().then(() => {
expect(findProjectPolicyAlert().exists()).toBe(false);
});
});
});
describe('API calls', () => {
it.each`
imageList | name | called
${[]} | ${'foo'} | ${['requestImagesList']}
${imagesListResponse.data} | ${undefined} | ${['requestImagesList']}
${imagesListResponse.data} | ${'foo'} | ${undefined}
`(
'with images equal $imageList and name $name dispatch calls $called',
({ imageList, name, called }) => {
store.commit(SET_IMAGES_LIST_SUCCESS, imageList);
dispatchSpy.mockClear();
mountComponent({ mocks: { $route: { name } } });
expect(dispatchSpy.mock.calls[0]).toEqual(called);
},
);
});
describe('connection error', () => {
const config = {
characterError: true,
......@@ -79,12 +112,13 @@ describe('List Page', () => {
helpPagePath: 'bar',
};
beforeAll(() => {
store.dispatch('setInitialState', config);
beforeEach(() => {
store.commit(SET_INITIAL_STATE, config);
mountComponent();
});
afterAll(() => {
store.dispatch('setInitialState', {});
afterEach(() => {
store.commit(SET_INITIAL_STATE, {});
});
it('should show an empty state', () => {
......@@ -106,9 +140,12 @@ describe('List Page', () => {
});
describe('isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
beforeEach(() => {
store.commit(SET_MAIN_LOADING, true);
mountComponent();
});
afterAll(() => store.commit(SET_MAIN_LOADING, false));
afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
......@@ -125,7 +162,8 @@ describe('List Page', () => {
describe('list is empty', () => {
beforeEach(() => {
store.dispatch('receiveImagesListSuccess', { data: [] });
store.commit(SET_IMAGES_LIST_SUCCESS, []);
mountComponent();
});
it('quick start is not visible', () => {
......@@ -137,12 +175,13 @@ describe('List Page', () => {
});
describe('is group page is true', () => {
beforeAll(() => {
store.dispatch('setInitialState', { isGroupPage: true });
beforeEach(() => {
store.commit(SET_INITIAL_STATE, { isGroupPage: true });
mountComponent();
});
afterAll(() => {
store.dispatch('setInitialState', { isGroupPage: undefined });
afterEach(() => {
store.commit(SET_INITIAL_STATE, { isGroupPage: undefined });
});
it('group empty state is visible', () => {
......@@ -156,6 +195,10 @@ describe('List Page', () => {
});
describe('list is not empty', () => {
beforeEach(() => {
mountComponent();
});
it('quick start is visible', () => {
expect(findQuickStartDropdown().exists()).toBe(true);
});
......
......@@ -9,3 +9,8 @@ export const GlEmptyState = {
template: '<div><slot name="description"></slot></div>',
name: 'GlEmptyStateSTub',
};
export const RouterLink = {
template: `<div><slot></slot></div>`,
props: ['to'],
};
......@@ -7,26 +7,27 @@ import { shallowMount } from '@vue/test-utils';
describe('Snippet header component', () => {
let wrapper;
const snippet = {
snippet: {
id: 'gid://gitlab/PersonalSnippet/50',
title: 'The property of Thor',
visibilityLevel: 'private',
webUrl: 'http://personal.dev.null/42',
userPermissions: {
adminSnippet: true,
updateSnippet: true,
reportSnippet: false,
},
project: null,
author: {
name: 'Thor Odinson',
},
id: 'gid://gitlab/PersonalSnippet/50',
title: 'The property of Thor',
visibilityLevel: 'private',
webUrl: 'http://personal.dev.null/42',
userPermissions: {
adminSnippet: true,
updateSnippet: true,
reportSnippet: false,
},
project: null,
author: {
name: 'Thor Odinson',
},
blob: {
binary: false,
},
};
const mutationVariables = {
mutation: DeleteSnippetMutation,
variables: {
id: snippet.snippet.id,
id: snippet.id,
},
};
const errorMsg = 'Foo bar';
......@@ -46,10 +47,12 @@ describe('Snippet header component', () => {
loading = false,
permissions = {},
mutationRes = mutationTypes.RESOLVE,
snippetProps = {},
} = {}) {
const defaultProps = Object.assign({}, snippet);
// const defaultProps = Object.assign({}, snippet, snippetProps);
const defaultProps = Object.assign(snippet, snippetProps);
if (permissions) {
Object.assign(defaultProps.snippet.userPermissions, {
Object.assign(defaultProps.userPermissions, {
...permissions,
});
}
......@@ -65,7 +68,9 @@ describe('Snippet header component', () => {
wrapper = shallowMount(SnippetHeader, {
mocks: { $apollo },
propsData: {
...defaultProps,
snippet: {
...defaultProps,
},
},
stubs: {
ApolloMutation,
......@@ -126,6 +131,17 @@ describe('Snippet header component', () => {
expect(wrapper.find(GlModal).exists()).toBe(true);
});
it('renders Edit button as disabled for binary snippets', () => {
createComponent({
snippetProps: {
blob: {
binary: true,
},
},
});
expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true);
});
describe('Delete mutation', () => {
const { location } = window;
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::AlertManagement::UpdateAlertStatus do
let_it_be(:current_user) { create(:user) }
let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') }
let_it_be(:project) { alert.project }
let(:new_status) { 'acknowledged' }
let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } }
specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alerts) }
describe '#resolve' do
subject(:resolve) { mutation_for(project, current_user).resolve(args) }
context 'user has access to project' do
before do
project.add_developer(current_user)
end
it 'changes the status' do
expect { resolve }.to change { alert.reload.status }.from(alert.status).to(new_status)
end
it 'returns the alert with no errors' do
expect(resolve).to eq(
alert: alert,
errors: []
)
end
context 'error occurs when updating' do
it 'returns the alert with errors' do
# Stub an error on the alert
allow_next_instance_of(Resolvers::AlertManagementAlertResolver) do |resolver|
allow(resolver).to receive(:resolve).and_return(alert)
end
allow(alert).to receive(:save).and_return(false)
allow(alert).to receive(:errors).and_return(
double(full_messages: %w(foo bar))
)
expect(resolve).to eq(
alert: alert,
errors: ['foo and bar']
)
end
context 'invalid status given' do
let(:new_status) { 'invalid_status' }
it 'returns the alert with errors' do
expect(resolve).to eq(
alert: alert,
errors: ['Invalid status']
)
end
end
end
end
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
private
def mutation_for(project, user)
described_class.new(object: project, context: { current_user: user }, field: nil)
end
end
......@@ -11,13 +11,18 @@ describe GitlabSchema.types['AlertManagementAlert'] do
expected_fields = %i[
iid
title
description
severity
status
service
monitoring_tool
hosts
started_at
ended_at
event_count
details
created_at
updated_at
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
......@@ -84,6 +84,16 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end
end
context 'when the field is a connection' do
context 'when it resolves to nil' do
let(:field) { type_with_field(Types::QueryType.connection_type, :read_field, nil).fields['testField'].to_graphql }
it 'does not fail when authorizing' do
expect(resolved).to be_nil
end
end
end
context 'when the field is a specific type' do
let(:custom_type) { type(:read_type) }
let(:object_in_field) { double('presented in field') }
......
......@@ -92,7 +92,6 @@ describe Gitlab::Kubernetes::Helm::API do
allow(client).to receive(:get_config_map).and_return(nil)
allow(client).to receive(:create_config_map).and_return(nil)
allow(client).to receive(:create_service_account).and_return(nil)
allow(client).to receive(:create_cluster_role_binding).and_return(nil)
allow(client).to receive(:delete_pod).and_return(nil)
allow(namespace).to receive(:ensure_exists!).once
end
......@@ -136,7 +135,7 @@ describe Gitlab::Kubernetes::Helm::API do
context 'without a service account' do
it 'does not create a service account on kubeclient' do
expect(client).not_to receive(:create_service_account)
expect(client).not_to receive(:create_cluster_role_binding)
expect(client).not_to receive(:update_cluster_role_binding)
subject.install(command)
end
......@@ -160,15 +159,14 @@ describe Gitlab::Kubernetes::Helm::API do
)
end
context 'service account and cluster role binding does not exist' do
context 'service account does not exist' do
before do
expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'creates a service account, followed the cluster role binding on kubeclient' do
expect(client).to receive(:create_service_account).with(service_account_resource).once.ordered
expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
expect(client).to receive(:update_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
subject.install(command)
end
......@@ -177,21 +175,6 @@ describe Gitlab::Kubernetes::Helm::API do
context 'service account already exists' do
before do
expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'updates the service account, followed by creating the cluster role binding' do
expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered
expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
subject.install(command)
end
end
context 'service account and cluster role binding already exists' do
before do
expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_return(cluster_role_binding_resource)
end
it 'updates the service account, followed by creating the cluster role binding' do
......@@ -216,7 +199,7 @@ describe Gitlab::Kubernetes::Helm::API do
context 'legacy abac cluster' do
it 'does not create a service account on kubeclient' do
expect(client).not_to receive(:create_service_account)
expect(client).not_to receive(:create_cluster_role_binding)
expect(client).not_to receive(:update_cluster_role_binding)
subject.install(command)
end
......
......@@ -234,8 +234,6 @@ describe Gitlab::Kubernetes::KubeClient do
:create_role,
:get_role,
:update_role,
:create_cluster_role_binding,
:get_cluster_role_binding,
:update_cluster_role_binding
].each do |method|
describe "##{method}" do
......@@ -354,6 +352,16 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
shared_examples 'create_or_update method using put' do
let(:update_method) { "update_#{resource_type}" }
it 'calls the update method' do
expect(client).to receive(update_method).with(resource)
subject
end
end
shared_examples 'create_or_update method' do
let(:get_method) { "get_#{resource_type}" }
let(:update_method) { "update_#{resource_type}" }
......@@ -393,7 +401,7 @@ describe Gitlab::Kubernetes::KubeClient do
subject { client.create_or_update_cluster_role_binding(resource) }
it_behaves_like 'create_or_update method'
it_behaves_like 'create_or_update method using put'
end
describe '#create_or_update_role_binding' do
......@@ -405,7 +413,7 @@ describe Gitlab::Kubernetes::KubeClient do
subject { client.create_or_update_role_binding(resource) }
it_behaves_like 'create_or_update method'
it_behaves_like 'create_or_update method using put'
end
describe '#create_or_update_service_account' do
......
......@@ -54,14 +54,6 @@ describe Gitlab::SidekiqConfig::CliMethods do
end
end
context 'when the file contains an array of strings' do
before do
stub_contents(['queue_a'], ['queue_b'])
end
include_examples 'valid file contents'
end
context 'when the file contains an array of hashes' do
before do
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
......
......@@ -126,4 +126,30 @@ describe AlertManagement::Alert do
it { is_expected.to match_array(alert_1) }
end
describe '.details' do
let(:payload) do
{
'title' => 'Details title',
'custom' => {
'alert' => {
'fields' => %w[one two]
}
},
'yet' => {
'another' => 'field'
}
}
end
let(:alert) { build(:alert_management_alert, title: 'Details title', payload: payload) }
subject { alert.details }
it 'renders the payload as inline hash' do
is_expected.to eq(
'custom.alert.fields' => %w[one two],
'yet.another' => 'field'
)
end
end
end
......@@ -45,18 +45,6 @@ describe Ci::PersistentRef do
expect(pipeline.persistent_ref).to be_exist
end
context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do
before do
stub_feature_flags(depend_on_persistent_pipeline_ref: false)
end
it 'does not create a persistent ref' do
expect(project.repository).not_to receive(:create_ref)
subject
end
end
context 'when sha does not exist in the repository' do
let(:sha) { 'not-exist' }
......
......@@ -6,17 +6,20 @@ describe AlertManagement::AlertPolicy, :models do
let(:alert) { create(:alert_management_alert) }
let(:project) { alert.project }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, alert) }
subject(:policy) { described_class.new(user, alert) }
describe 'rules' do
it { expect(policy).to be_disallowed :read_alert_management_alerts }
it { is_expected.to be_disallowed :read_alert_management_alerts }
it { is_expected.to be_disallowed :update_alert_management_alerts }
context 'when developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_allowed :read_alert_management_alerts }
it { is_expected.to be_allowed :read_alert_management_alerts }
it { is_expected.to be_allowed :update_alert_management_alerts }
end
end
end
......@@ -173,81 +173,34 @@ describe Ci::BuildRunnerPresenter do
let(:pipeline) { merge_request.all_pipelines.first }
let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
context 'when depend_on_persistent_pipeline_ref feature flag is enabled' do
before do
stub_feature_flags(ci_force_exposing_merge_request_refs: false)
pipeline.persistent_ref.create
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
context 'when ci_force_exposing_merge_request_refs feature flag is enabled' do
before do
stub_feature_flags(ci_force_exposing_merge_request_refs: true)
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
'+refs/merge-requests/1/head:refs/merge-requests/1/head')
end
end
context 'when GIT_DEPTH is zero' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
'+refs/heads/*:refs/remotes/origin/*',
'+refs/tags/*:refs/tags/*')
end
end
context 'when pipeline is legacy detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
before do
pipeline.persistent_ref.create
end
it 'returns the correct refspecs' do
is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
"+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do
context 'when GIT_DEPTH is zero' do
before do
stub_feature_flags(depend_on_persistent_pipeline_ref: false)
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head')
end
context 'when GIT_DEPTH is zero' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
end
it 'returns the correct refspecs' do
is_expected
.to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head',
'+refs/heads/*:refs/remotes/origin/*',
'+refs/tags/*:refs/tags/*')
end
.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
'+refs/heads/*:refs/remotes/origin/*',
'+refs/tags/*:refs/tags/*')
end
end
context 'when pipeline is legacy detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
context 'when pipeline is legacy detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
it 'returns the correct refspecs' do
is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
it 'returns the correct refspecs' do
is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
"+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
end
......
......@@ -4,10 +4,11 @@ require 'spec_helper'
describe 'getting Alert Management Alerts' do
include GraphqlHelpers
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project, payload: payload) }
let(:fields) do
<<~QUERY
......@@ -55,13 +56,18 @@ describe 'getting Alert Management Alerts' do
expect(first_alert).to include(
'iid' => alert_2.iid.to_s,
'title' => alert_2.title,
'description' => alert_2.description,
'severity' => alert_2.severity.upcase,
'status' => alert_2.status.upcase,
'monitoringTool' => alert_2.monitoring_tool,
'service' => alert_2.service,
'hosts' => alert_2.hosts,
'eventCount' => alert_2.events,
'startedAt' => alert_2.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'endedAt' => alert_2.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
'endedAt' => alert_2.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'details' => { 'custom.alert' => 'payload' },
'createdAt' => alert_2.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'updatedAt' => alert_2.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
end
......
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::UpdateAlertStatusService do
let_it_be(:alert) { create(:alert_management_alert, status: 'triggered') }
describe '#execute' do
subject(:execute) { described_class.new(alert, new_status).execute }
let(:new_status) { 'acknowledged' }
it 'updates the status' do
expect { execute }.to change { alert.status }.to(new_status)
end
context 'with unknown status' do
let(:new_status) { 'random_status' }
it 'returns an error' do
expect(execute.status).to eq(:error)
end
it 'does not update the status' do
expect { execute }.not_to change { alert.status }
end
end
end
end
......@@ -108,8 +108,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
}
)
stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
stub_kubeclient_create_cluster_role_binding(api_url)
stub_kubeclient_put_cluster_role_binding(api_url, 'gitlab-admin')
end
end
......
......@@ -28,7 +28,6 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
stub_kubeclient_get_secret_error(api_url, 'gitlab-token')
stub_kubeclient_create_secret(api_url)
stub_kubeclient_get_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
stub_kubeclient_put_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace)
......
......@@ -83,8 +83,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
before do
cluster.platform_kubernetes.rbac!
stub_kubeclient_get_cluster_role_binding_error(api_url, cluster_role_binding_name)
stub_kubeclient_create_cluster_role_binding(api_url)
stub_kubeclient_put_cluster_role_binding(api_url, cluster_role_binding_name)
end
it_behaves_like 'creates service account and token'
......@@ -92,9 +91,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
it 'creates a cluster role binding with cluster-admin access' do
subject
expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/gitlab-admin").with(
body: hash_including(
kind: 'ClusterRoleBinding',
metadata: { name: 'gitlab-admin' },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
......@@ -143,8 +141,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
before do
cluster.platform_kubernetes.rbac!
stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_create_role_binding(api_url, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace)
......@@ -166,9 +163,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
it 'creates a namespaced role binding with edit access' do
subject
expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with(
expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{role_binding_name}").with(
body: hash_including(
kind: 'RoleBinding',
metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
......
......@@ -73,11 +73,13 @@ describe Spam::SpamActionService do
describe '#execute' do
let(:request) { double(:request, env: env) }
let(:fake_verdict_service) { double(:spam_verdict_service) }
let(:allowlisted) { false }
let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
subject do
described_service = described_class.new(spammable: issue, request: request)
allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
described_service.execute(user: user, api: nil, recaptcha_verified: recaptcha_verified, spam_log_id: existing_spam_log.id)
end
......@@ -121,6 +123,16 @@ describe Spam::SpamActionService do
issue.description = 'SPAM!'
end
context 'if allowlisted' do
let(:allowlisted) { true }
it 'does not perform spam check' do
expect(Spam::SpamVerdictService).not_to receive(:new)
subject
end
end
context 'when disallowed by the spam verdict service' do
before do
allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
......
......@@ -201,28 +201,8 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404)
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
.to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_cluster_role_binding(api_url)
WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings')
.to_return(kube_response({}))
end
def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default')
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
.to_return(kube_response({}))
end
def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404)
WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
.to_return(status: [status, "Internal Server Error"])
end
def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
def stub_kubeclient_put_cluster_role_binding(api_url, name)
WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
.to_return(kube_response({}))
end
......
......@@ -787,10 +787,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.125.0.tgz#59c667dae8f7e4c80b482f5f6cc35367c016387b"
integrity sha512-MKfFYa8f+9P2tJ/JN/E9oDBSSo/gRz2zuGui4XHQPoaw/DkIMn7EyAzeSpRgbgs1LgMcEqqKsIEx+spCga3jsQ==
"@gitlab/ui@13.9.0":
version "13.9.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-13.9.0.tgz#c75c3c6adc92e71a5e7915fe6d1c9fa6e2d5f85d"
integrity sha512-fpjjMXAyOGIITR/Jb7zmw7ul5EAwdSdivmJsiQnwb9eetjNgVlguYu0ZZM0YAdgRXeeIRyVaS8OCqTeyD02yFQ==
"@gitlab/ui@14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.0.0.tgz#d478b1454659c0f54b72cdecce1c2014fc5f8564"
integrity sha512-R+unP0mOBYQ+uRJLm/tI+2znsbsHY2rumSYtMqM3vGCXasteySQIMZ8huWGa5Cf4ZUdy1lNa0J/zxKj6TLdjCQ==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册