Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9c15dfa1ef
commit
2f2c8f84bf
|
@ -168,7 +168,7 @@ export default {
|
|||
};
|
||||
},
|
||||
error() {
|
||||
this.errored = true;
|
||||
this.hasError = true;
|
||||
},
|
||||
},
|
||||
alertsCount: {
|
||||
|
@ -187,10 +187,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
errored: false,
|
||||
hasError: false,
|
||||
errorMessage: '',
|
||||
isAlertDismissed: false,
|
||||
isErrorAlertDismissed: false,
|
||||
sort: 'STARTED_AT_DESC',
|
||||
statusFilter: [],
|
||||
filteredByStatus: '',
|
||||
|
@ -203,16 +202,13 @@ export default {
|
|||
computed: {
|
||||
showNoAlertsMsg() {
|
||||
return (
|
||||
!this.errored &&
|
||||
!this.hasError &&
|
||||
!this.loading &&
|
||||
this.alertsCount?.all === 0 &&
|
||||
!this.searchTerm &&
|
||||
!this.isAlertDismissed
|
||||
);
|
||||
},
|
||||
showErrorMsg() {
|
||||
return this.errored && !this.isErrorAlertDismissed;
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.alerts.loading;
|
||||
},
|
||||
|
@ -306,11 +302,11 @@ export default {
|
|||
};
|
||||
},
|
||||
handleAlertError(errorMessage) {
|
||||
this.errored = true;
|
||||
this.hasError = true;
|
||||
this.errorMessage = errorMessage;
|
||||
},
|
||||
dismissError() {
|
||||
this.isErrorAlertDismissed = true;
|
||||
this.hasError = false;
|
||||
this.errorMessage = '';
|
||||
},
|
||||
},
|
||||
|
@ -332,12 +328,7 @@ export default {
|
|||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
<gl-alert
|
||||
v-if="showErrorMsg"
|
||||
variant="danger"
|
||||
data-testid="alert-error"
|
||||
@dismiss="dismissError"
|
||||
>
|
||||
<gl-alert v-if="hasError" variant="danger" data-testid="alert-error" @dismiss="dismissError">
|
||||
<p v-html="errorMessage || $options.i18n.errorMsg"></p>
|
||||
</gl-alert>
|
||||
|
||||
|
|
|
@ -11,13 +11,15 @@ import {
|
|||
GlSearchBoxByType,
|
||||
GlIcon,
|
||||
GlPagination,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
} from '@gitlab/ui';
|
||||
import { debounce } from 'lodash';
|
||||
import { debounce, trim } from 'lodash';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { s__ } from '~/locale';
|
||||
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
|
||||
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
|
||||
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY } from '../constants';
|
||||
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } from '../constants';
|
||||
|
||||
const tdClass =
|
||||
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
|
||||
|
@ -35,6 +37,7 @@ const initialPaginationState = {
|
|||
|
||||
export default {
|
||||
i18n: I18N,
|
||||
stateTabs: INCIDENT_STATE_TABS,
|
||||
fields: [
|
||||
{
|
||||
key: 'title',
|
||||
|
@ -67,6 +70,8 @@ export default {
|
|||
GlSearchBoxByType,
|
||||
GlIcon,
|
||||
GlPagination,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -78,6 +83,7 @@ export default {
|
|||
variables() {
|
||||
return {
|
||||
searchTerm: this.searchTerm,
|
||||
state: this.stateFilter,
|
||||
projectPath: this.projectPath,
|
||||
labelNames: ['incident'],
|
||||
firstPageSize: this.pagination.firstPageSize,
|
||||
|
@ -105,6 +111,7 @@ export default {
|
|||
searchTerm: '',
|
||||
pagination: initialPaginationState,
|
||||
incidents: {},
|
||||
stateFilter: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -138,14 +145,17 @@ export default {
|
|||
return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchTerm: debounce(function debounceSearch(input) {
|
||||
if (input !== this.searchTerm) {
|
||||
this.searchTerm = input;
|
||||
methods: {
|
||||
onInputChange: debounce(function debounceSearch(input) {
|
||||
const trimmedInput = trim(input);
|
||||
if (trimmedInput !== this.searchTerm) {
|
||||
this.searchTerm = trimmedInput;
|
||||
}
|
||||
}, INCIDENT_SEARCH_DELAY),
|
||||
},
|
||||
methods: {
|
||||
filterIncidentsByState(tabIndex) {
|
||||
const { filters } = this.$options.stateTabs[tabIndex];
|
||||
this.stateFilter = filters;
|
||||
},
|
||||
hasAssignees(assignees) {
|
||||
return Boolean(assignees.nodes?.length);
|
||||
},
|
||||
|
@ -183,9 +193,17 @@ export default {
|
|||
{{ $options.i18n.errorMsg }}
|
||||
</gl-alert>
|
||||
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<div class="incident-management-list-header gl-display-flex gl-justify-content-space-between">
|
||||
<gl-tabs content-class="gl-p-0" @input="filterIncidentsByState">
|
||||
<gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state">
|
||||
<template #title>
|
||||
<span>{{ tab.title }}</span>
|
||||
</template>
|
||||
</gl-tab>
|
||||
</gl-tabs>
|
||||
|
||||
<gl-button
|
||||
class="gl-mt-3 gl-mb-3 create-incident-button"
|
||||
class="gl-my-3 create-incident-button"
|
||||
data-testid="createIncidentBtn"
|
||||
:loading="redirecting"
|
||||
:disabled="redirecting"
|
||||
|
@ -200,9 +218,10 @@ export default {
|
|||
|
||||
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
:value="searchTerm"
|
||||
class="gl-bg-white"
|
||||
:placeholder="$options.i18n.searchPlaceholder"
|
||||
@input="onInputChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -221,7 +240,7 @@ export default {
|
|||
@row-clicked="navigateToIncidentDetails"
|
||||
>
|
||||
<template #cell(title)="{ item }">
|
||||
<div class="gl-display-flex gl-justify-content-center">
|
||||
<div class="gl-display-sm-flex gl-align-items-center">
|
||||
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
|
||||
<gl-icon
|
||||
v-if="item.state === 'closed'"
|
||||
|
|
|
@ -8,5 +8,23 @@ export const I18N = {
|
|||
searchPlaceholder: __('Search or filter results...'),
|
||||
};
|
||||
|
||||
export const INCIDENT_STATE_TABS = [
|
||||
{
|
||||
title: s__('IncidentManagement|Open'),
|
||||
state: 'OPENED',
|
||||
filters: 'opened',
|
||||
},
|
||||
{
|
||||
title: s__('IncidentManagement|Closed'),
|
||||
state: 'CLOSED',
|
||||
filters: 'closed',
|
||||
},
|
||||
{
|
||||
title: s__('IncidentManagement|All incidents'),
|
||||
state: 'ALL',
|
||||
filters: 'all',
|
||||
},
|
||||
];
|
||||
|
||||
export const INCIDENT_SEARCH_DELAY = 300;
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
|
|
|
@ -80,31 +80,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.fa-stack {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.fa-stack-1x,
|
||||
.fa-stack-2x {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fa-stack-1x {
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.fa-stack-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-inverse {
|
||||
color: $white;
|
||||
}
|
||||
|
@ -256,10 +231,6 @@
|
|||
content: '\f111';
|
||||
}
|
||||
|
||||
.fa-certificate::before {
|
||||
content: '\f0a3';
|
||||
}
|
||||
|
||||
.fa-bitbucket::before {
|
||||
content: '\f171';
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@
|
|||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.incident-management-list-header {
|
||||
flex-direction: column-reverse;
|
||||
};
|
||||
|
||||
.create-incident-button {
|
||||
@include gl-w-full;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:multiline_comments, @project)
|
||||
push_frontend_feature_flag(:file_identifier_hash)
|
||||
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project)
|
||||
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, @project)
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module ResolvesSubscription
|
||||
extend ActiveSupport::Concern
|
||||
included do
|
||||
argument :subscribed_state,
|
||||
GraphQL::BOOLEAN_TYPE,
|
||||
required: true,
|
||||
description: 'The desired state of the subscription'
|
||||
end
|
||||
|
||||
def resolve(project_path:, iid:, subscribed_state:)
|
||||
resource = authorized_find!(project_path: project_path, iid: iid)
|
||||
project = resource.project
|
||||
|
||||
resource.set_subscription(current_user, subscribed_state, project)
|
||||
|
||||
{
|
||||
resource.class.name.underscore.to_sym => resource,
|
||||
errors: errors_on_object(resource)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Mutations
|
||||
module Issues
|
||||
class SetSubscription < Base
|
||||
graphql_name 'IssueSetSubscription'
|
||||
|
||||
include ResolvesSubscription
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,22 +5,7 @@ module Mutations
|
|||
class SetSubscription < Base
|
||||
graphql_name 'MergeRequestSetSubscription'
|
||||
|
||||
argument :subscribed_state,
|
||||
GraphQL::BOOLEAN_TYPE,
|
||||
required: true,
|
||||
description: 'The desired state of the subscription'
|
||||
|
||||
def resolve(project_path:, iid:, subscribed_state:)
|
||||
merge_request = authorized_find!(project_path: project_path, iid: iid)
|
||||
project = merge_request.project
|
||||
|
||||
merge_request.set_subscription(current_user, subscribed_state, project)
|
||||
|
||||
{
|
||||
merge_request: merge_request,
|
||||
errors: errors_on_object(merge_request)
|
||||
}
|
||||
end
|
||||
include ResolvesSubscription
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,5 +8,6 @@ module Types
|
|||
value 'opened'
|
||||
value 'closed'
|
||||
value 'locked'
|
||||
value 'all'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ module Types
|
|||
mount_mutation Mutations::Issues::SetConfidential
|
||||
mount_mutation Mutations::Issues::SetLocked
|
||||
mount_mutation Mutations::Issues::SetDueDate
|
||||
mount_mutation Mutations::Issues::SetSubscription
|
||||
mount_mutation Mutations::Issues::Update
|
||||
mount_mutation Mutations::MergeRequests::Create
|
||||
mount_mutation Mutations::MergeRequests::Update
|
||||
|
|
|
@ -205,7 +205,7 @@ module IssuablesHelper
|
|||
author_output
|
||||
end
|
||||
|
||||
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
|
||||
output << content_tag(:span, (sprite_icon('first-contribution', size: 16, css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
|
||||
|
||||
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3")
|
||||
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
|
||||
|
@ -247,13 +247,6 @@ module IssuablesHelper
|
|||
html.html_safe
|
||||
end
|
||||
|
||||
def issuable_first_contribution_icon
|
||||
content_tag(:span, class: 'fa-stack') do
|
||||
concat(icon('certificate', class: "fa-stack-2x"))
|
||||
concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
|
||||
end
|
||||
end
|
||||
|
||||
def assigned_issuables_count(issuable_type)
|
||||
case issuable_type
|
||||
when :issues
|
||||
|
|
|
@ -1801,7 +1801,6 @@ class Project < ApplicationRecord
|
|||
return unless namespace
|
||||
|
||||
mark_pages_as_not_deployed unless destroyed?
|
||||
::Projects::UpdatePagesConfigurationService.new(self).execute
|
||||
|
||||
# 1. We rename pages to temporary directory
|
||||
# 2. We wait 5 minutes, due to NFS caching
|
||||
|
|
|
@ -33,7 +33,7 @@ module Metrics
|
|||
|
||||
def from_cache(project_id, user_id, grafana_url)
|
||||
project = Project.find(project_id)
|
||||
user = User.find(user_id)
|
||||
user = User.find(user_id) if user_id.present?
|
||||
|
||||
new(project, user, grafana_url: grafana_url)
|
||||
end
|
||||
|
@ -56,7 +56,7 @@ module Metrics
|
|||
end
|
||||
|
||||
def cache_key(*args)
|
||||
[project.id, current_user.id, grafana_url]
|
||||
[project.id, current_user&.id, grafana_url]
|
||||
end
|
||||
|
||||
# Required for ReactiveCaching; Usage overridden by
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- access = note_max_access_for_user(note)
|
||||
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
|
||||
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
|
||||
= issuable_first_contribution_icon
|
||||
= sprite_icon('first-contribution', size: 16, css_class: 'gl-icon gl-vertical-align-top')
|
||||
- if access.nonzero?
|
||||
%span.note-role.user-access-role= Gitlab::Access.human_access(access)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -33,7 +33,10 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
|
||||
flush_ref_caches(project) if task == :gc
|
||||
|
||||
project.repository.expire_statistics_caches if task != :pack_refs
|
||||
if task != :pack_refs
|
||||
project.repository.expire_statistics_caches
|
||||
Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
|
||||
end
|
||||
|
||||
# In case pack files are deleted, release libgit2 cache and open file
|
||||
# descriptors ASAP instead of waiting for Ruby garbage collection
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
feature_category :source_code_management
|
||||
feature_category :integrations
|
||||
|
||||
LEASE_TIMEOUT = 4.hours.to_i
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow anonymous users to view embedded Grafana metrics in public project
|
||||
merge_request: 37844
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace fa-certificate icon with first-contribution svg
|
||||
merge_request: 38154
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Immediately update project statistics when running housekeeping or repository
|
||||
cleanup
|
||||
merge_request: 37579
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add incident state columns
|
||||
merge_request: 37889
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allows setting of issue subscribe status in GraphQL API.
|
||||
merge_request: 38051
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix 500 for pipeline charts page
|
||||
merge_request: 38226
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Auto expand collapsed diffs when viewing diffs file-by-file
|
||||
merge_request: 38296
|
||||
author:
|
||||
type: added
|
|
@ -14,10 +14,10 @@ if ENV['ENABLE_SIDEKIQ_CLUSTER']
|
|||
if Process.ppid != parent
|
||||
Process.kill(:TERM, Process.pid)
|
||||
|
||||
# Wait for just a few extra seconds for a final attempt to
|
||||
# gracefully terminate. Considering the parent (cluster) process
|
||||
# have changed (SIGKILL'd), it shouldn't take long to shutdown.
|
||||
sleep(5)
|
||||
# Allow sidekiq to cleanly terminate and push any running jobs back
|
||||
# into the queue. We use the configured timeout and add a small
|
||||
# grace period
|
||||
sleep(Sidekiq.options[:timeout] + 5)
|
||||
|
||||
# Signaling the Sidekiq Pgroup as KILL is not forwarded to
|
||||
# a possible child process. In Sidekiq Cluster, all child Sidekiq
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ReplaceUniqueIndexOnCycleAnalyticsStages < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
OLD_INDEX_NAME = 'index_analytics_ca_group_stages_on_group_id_and_name'
|
||||
NEW_INDEX_NAME = 'index_group_stages_on_group_id_group_value_stream_id_and_name'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(:analytics_cycle_analytics_group_stages,
|
||||
[:group_id, :group_value_stream_id, :name],
|
||||
unique: true,
|
||||
name: NEW_INDEX_NAME)
|
||||
|
||||
remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, OLD_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
# Removing duplicated records (group_id, name) that would prevent re-creating the old index.
|
||||
execute <<-SQL
|
||||
DELETE FROM analytics_cycle_analytics_group_stages
|
||||
USING (
|
||||
SELECT group_id, name, MIN(id) as min_id
|
||||
FROM analytics_cycle_analytics_group_stages
|
||||
GROUP BY group_id, name
|
||||
HAVING COUNT(id) > 1
|
||||
) as analytics_cycle_analytics_group_stages_name_duplicates
|
||||
WHERE analytics_cycle_analytics_group_stages_name_duplicates.group_id = analytics_cycle_analytics_group_stages.group_id
|
||||
AND analytics_cycle_analytics_group_stages_name_duplicates.name = analytics_cycle_analytics_group_stages.name
|
||||
AND analytics_cycle_analytics_group_stages_name_duplicates.min_id <> analytics_cycle_analytics_group_stages.id
|
||||
SQL
|
||||
|
||||
add_concurrent_index(:analytics_cycle_analytics_group_stages,
|
||||
[:group_id, :name],
|
||||
unique: true,
|
||||
name: OLD_INDEX_NAME)
|
||||
|
||||
remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, NEW_INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToCiPipelineProjectIdCreatedAt < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :ci_pipelines, [:project_id, :created_at]
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :ci_pipelines, [:project_id, :created_at]
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
546555a009e8923ea8b976ce38d882d387407fb03e7bbcb9c760df53bafd1f91
|
|
@ -0,0 +1 @@
|
|||
2976f459ac9cd0780e90077ebe4ce5ca8dc41e62b4dab1f96e39738624ad9d04
|
|
@ -18880,8 +18880,6 @@ CREATE INDEX index_analytics_ca_group_stages_on_end_event_label_id ON public.ana
|
|||
|
||||
CREATE INDEX index_analytics_ca_group_stages_on_group_id ON public.analytics_cycle_analytics_group_stages USING btree (group_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_analytics_ca_group_stages_on_group_id_and_name ON public.analytics_cycle_analytics_group_stages USING btree (group_id, name);
|
||||
|
||||
CREATE INDEX index_analytics_ca_group_stages_on_relative_position ON public.analytics_cycle_analytics_group_stages USING btree (relative_position);
|
||||
|
||||
CREATE INDEX index_analytics_ca_group_stages_on_start_event_label_id ON public.analytics_cycle_analytics_group_stages USING btree (start_event_label_id);
|
||||
|
@ -19150,6 +19148,8 @@ CREATE INDEX index_ci_pipelines_on_merge_request_id ON public.ci_pipelines USING
|
|||
|
||||
CREATE INDEX index_ci_pipelines_on_pipeline_schedule_id ON public.ci_pipelines USING btree (pipeline_schedule_id);
|
||||
|
||||
CREATE INDEX index_ci_pipelines_on_project_id_and_created_at ON public.ci_pipelines USING btree (project_id, created_at);
|
||||
|
||||
CREATE INDEX index_ci_pipelines_on_project_id_and_id_desc ON public.ci_pipelines USING btree (project_id, id DESC);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_pipelines_on_project_id_and_iid ON public.ci_pipelines USING btree (project_id, iid) WHERE (iid IS NOT NULL);
|
||||
|
@ -19654,6 +19654,8 @@ CREATE INDEX index_group_group_links_on_shared_with_group_id ON public.group_gro
|
|||
|
||||
CREATE INDEX index_group_import_states_on_group_id ON public.group_import_states USING btree (group_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_group_stages_on_group_id_group_value_stream_id_and_name ON public.analytics_cycle_analytics_group_stages USING btree (group_id, group_value_stream_id, name);
|
||||
|
||||
CREATE UNIQUE INDEX index_group_wiki_repositories_on_disk_path ON public.group_wiki_repositories USING btree (disk_path);
|
||||
|
||||
CREATE INDEX index_group_wiki_repositories_on_shard_id ON public.group_wiki_repositories USING btree (shard_id);
|
||||
|
|
|
@ -5861,6 +5861,7 @@ type InstanceSecurityDashboard {
|
|||
State of a GitLab issue or merge request
|
||||
"""
|
||||
enum IssuableState {
|
||||
all
|
||||
closed
|
||||
locked
|
||||
opened
|
||||
|
@ -6433,6 +6434,51 @@ type IssueSetLockedPayload {
|
|||
issue: Issue
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of IssueSetSubscription
|
||||
"""
|
||||
input IssueSetSubscriptionInput {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
The IID of the issue to mutate
|
||||
"""
|
||||
iid: String!
|
||||
|
||||
"""
|
||||
The project the issue to mutate is in
|
||||
"""
|
||||
projectPath: ID!
|
||||
|
||||
"""
|
||||
The desired state of the subscription
|
||||
"""
|
||||
subscribedState: Boolean!
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated return type of IssueSetSubscription
|
||||
"""
|
||||
type IssueSetSubscriptionPayload {
|
||||
"""
|
||||
A unique identifier for the client performing the mutation.
|
||||
"""
|
||||
clientMutationId: String
|
||||
|
||||
"""
|
||||
Errors encountered during execution of the mutation.
|
||||
"""
|
||||
errors: [String!]!
|
||||
|
||||
"""
|
||||
The issue after mutation
|
||||
"""
|
||||
issue: Issue
|
||||
}
|
||||
|
||||
"""
|
||||
Autogenerated input type of IssueSetWeight
|
||||
"""
|
||||
|
@ -6562,6 +6608,7 @@ enum IssueSort {
|
|||
State of a GitLab issue
|
||||
"""
|
||||
enum IssueState {
|
||||
all
|
||||
closed
|
||||
locked
|
||||
opened
|
||||
|
@ -7987,6 +8034,7 @@ type MergeRequestSetWipPayload {
|
|||
State of a GitLab merge request
|
||||
"""
|
||||
enum MergeRequestState {
|
||||
all
|
||||
closed
|
||||
locked
|
||||
merged
|
||||
|
@ -8351,6 +8399,7 @@ type Mutation {
|
|||
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
|
||||
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
|
||||
issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload
|
||||
issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload
|
||||
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
|
||||
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
|
||||
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
|
||||
|
|
|
@ -16186,6 +16186,12 @@
|
|||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "all",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
@ -17852,6 +17858,136 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "IssueSetSubscriptionInput",
|
||||
"description": "Autogenerated input type of IssueSetSubscription",
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "projectPath",
|
||||
"description": "The project the issue 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 issue to mutate",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "subscribedState",
|
||||
"description": "The desired state of the subscription",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"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": "IssueSetSubscriptionPayload",
|
||||
"description": "Autogenerated return type of IssueSetSubscription",
|
||||
"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": "Errors encountered during execution of the mutation.",
|
||||
"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": "issue",
|
||||
"description": "The issue after mutation",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Issue",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "IssueSetWeightInput",
|
||||
|
@ -18108,6 +18244,12 @@
|
|||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "all",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
|
@ -22394,6 +22536,12 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "all",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "merged",
|
||||
"description": null,
|
||||
|
@ -24357,6 +24505,33 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "issueSetSubscription",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "IssueSetSubscriptionInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "IssueSetSubscriptionPayload",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "issueSetWeight",
|
||||
"description": null,
|
||||
|
|
|
@ -985,6 +985,16 @@ Autogenerated return type of IssueSetLocked
|
|||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `issue` | Issue | The issue after mutation |
|
||||
|
||||
## IssueSetSubscriptionPayload
|
||||
|
||||
Autogenerated return type of IssueSetSubscription
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `issue` | Issue | The issue after mutation |
|
||||
|
||||
## IssueSetWeightPayload
|
||||
|
||||
Autogenerated return type of IssueSetWeight
|
||||
|
|
|
@ -230,7 +230,7 @@ Instead these should be sent to the [Release Manager](https://about.gitlab.com/c
|
|||
- Ask for clarification. ("I didn't understand. Can you clarify?")
|
||||
- Avoid selective ownership of code. ("mine", "not mine", "yours")
|
||||
- Avoid using terms that could be seen as referring to personal traits. ("dumb",
|
||||
"stupid"). Assume everyone is attractive, intelligent, and well-meaning.
|
||||
"stupid"). Assume everyone is intelligent and well-meaning.
|
||||
- Be explicit. Remember people don't always understand your intentions online.
|
||||
- Be humble. ("I'm not sure - let's look it up.")
|
||||
- Don't use hyperbole. ("always", "never", "endlessly", "nothing")
|
||||
|
|
|
@ -481,7 +481,7 @@ We treat documentation as code, and so use tests in our CI pipeline to maintain
|
|||
standards and quality of the docs. The current tests, which run in CI jobs when a
|
||||
merge request with new or changed docs is submitted, are:
|
||||
|
||||
- [`docs lint`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L48):
|
||||
- [`docs lint`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L41):
|
||||
Runs several tests on the content of the docs themselves:
|
||||
- [`lint-doc.sh` script](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/lint-doc.sh)
|
||||
runs the following checks and linters:
|
||||
|
@ -492,33 +492,20 @@ merge request with new or changed docs is submitted, are:
|
|||
- [markdownlint](#markdownlint).
|
||||
- [Vale](#vale).
|
||||
- Nanoc tests:
|
||||
- [`internal_links`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L67)
|
||||
- [`internal_links`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L58)
|
||||
checks that all internal links (ex: `[link](../index.md)`) are valid.
|
||||
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69)
|
||||
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L60)
|
||||
checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`)
|
||||
are valid.
|
||||
- [`ui-docs-links lint`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L62)
|
||||
checks that all links to docs from UI elements (`app/views` files, for example)
|
||||
are linking to valid docs and anchors.
|
||||
|
||||
### Running tests
|
||||
### Run tests locally
|
||||
|
||||
Apart from [previewing your changes locally](#previewing-the-changes-live), you can also run all lint checks
|
||||
and Nanoc tests locally.
|
||||
|
||||
#### Nanoc tests
|
||||
|
||||
To execute Nanoc tests locally:
|
||||
|
||||
1. Navigate to the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory.
|
||||
1. Run:
|
||||
|
||||
```shell
|
||||
# Check for broken internal links
|
||||
bundle exec nanoc check internal_links
|
||||
|
||||
# Check for broken external links (might take a lot of time to complete).
|
||||
# This test is set to be allowed to fail and is run only in the gitlab-docs project CI
|
||||
bundle exec nanoc check internal_anchors
|
||||
```
|
||||
|
||||
#### Lint checks
|
||||
|
||||
Lint checks are performed by the [`lint-doc.sh`](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/lint-doc.sh)
|
||||
|
@ -550,6 +537,57 @@ The output should be similar to:
|
|||
Note that this requires you to either have the required lint tools installed on your machine,
|
||||
or a working Docker installation, in which case an image with these tools pre-installed will be used.
|
||||
|
||||
#### Nanoc tests
|
||||
|
||||
To execute Nanoc tests locally:
|
||||
|
||||
1. Navigate to the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory.
|
||||
1. Run:
|
||||
|
||||
```shell
|
||||
# Check for broken internal links
|
||||
bundle exec nanoc check internal_links
|
||||
|
||||
# Check for broken external links (might take a lot of time to complete).
|
||||
# This test is set to be allowed to fail and is run only in the gitlab-docs project CI
|
||||
bundle exec nanoc check internal_anchors
|
||||
```
|
||||
|
||||
#### `ui-docs-links` test
|
||||
|
||||
The `ui-docs-links lint` job uses `haml-lint` to test that all links to docs from
|
||||
UI elements (`app/views` files, for example) are linking to valid docs and anchors.
|
||||
|
||||
To run the `ui-docs-links` test locally:
|
||||
|
||||
1. Open the `gitlab` directory in a terminal window.
|
||||
1. Run:
|
||||
|
||||
```shell
|
||||
bundle exec haml-lint -i DocumentationLinks
|
||||
```
|
||||
|
||||
If you receive an error the first time you run this test, run `bundle install`, which
|
||||
installs GitLab's dependencies, and try again.
|
||||
|
||||
If you don't want to install all of GitLab's dependencies to test the links, you can:
|
||||
|
||||
1. Open the `gitlab` directory in a terminal window.
|
||||
1. Install `haml-lint`:
|
||||
|
||||
```shell
|
||||
gem install haml_lint
|
||||
```
|
||||
|
||||
1. Run:
|
||||
|
||||
```shell
|
||||
haml-lint -i DocumentationLinks
|
||||
```
|
||||
|
||||
If you manually install `haml-lint` with this process, it will not update automatically
|
||||
and you should make sure your version matches the version used by GitLab.
|
||||
|
||||
### Local linters
|
||||
|
||||
To help adhere to the [documentation style guidelines](styleguide.md), and improve the content
|
||||
|
|
|
@ -85,6 +85,7 @@ Default client accepts two parameters: `resolvers` and `config`.
|
|||
- `cacheConfig` field accepts an optional object of settings to [customize Apollo cache](https://www.apollographql.com/docs/react/caching/cache-configuration/#configuring-the-cache)
|
||||
- `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.`${gon.relative_url_root}/api/graphql`)
|
||||
- `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, will assume that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`.
|
||||
- `fetchPolicy` determines how you want your component to interact with the Apollo cache. Defaults to "cache-first".
|
||||
|
||||
## GraphQL Queries
|
||||
|
||||
|
@ -167,9 +168,7 @@ import VueApollo from 'vue-apollo';
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const defaultClient = createDefaultClient({
|
||||
resolvers: {}
|
||||
});
|
||||
const defaultClient = createDefaultClient();
|
||||
|
||||
defaultClient.cache.writeData({
|
||||
data: {
|
||||
|
@ -257,10 +256,7 @@ We need to pass resolvers object to our existing Apollo Client:
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
import resolvers from './graphql/resolvers';
|
||||
|
||||
const defaultClient = createDefaultClient(
|
||||
{},
|
||||
resolvers,
|
||||
);
|
||||
const defaultClient = createDefaultClient(resolvers);
|
||||
```
|
||||
|
||||
Now every single time on attempt to fetch a version, our client will fetch `id` and `sha` from the remote API endpoint and will assign our hardcoded values to `author` and `createdAt` version properties. With this data, frontend developers are able to work on UI part without being blocked by backend. When actual response is added to the API, a custom local resolver can be removed fast and the only change to query/fragment is `@client` directive removal.
|
||||
|
|
|
@ -218,9 +218,9 @@ Project.select(:id, :user_id).joins(:merge_requests)
|
|||
|
||||
## Plucking IDs
|
||||
|
||||
This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a
|
||||
set of values into memory only to use them as an argument for another query. For
|
||||
example, this will make the database **very** sad:
|
||||
Never use ActiveRecord's `pluck` to pluck a set of values into memory only to
|
||||
use them as an argument for another query. For example, this will execute an
|
||||
extra unecessary database query and load a lot of unecessary data into memory:
|
||||
|
||||
```ruby
|
||||
projects = Project.all.pluck(:id)
|
||||
|
|
|
@ -31,8 +31,6 @@ file path fragments to start seeing results.
|
|||
|
||||
## Syntax highlighting
|
||||
|
||||
> Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||
|
||||
As expected from an IDE, syntax highlighting for many languages within
|
||||
the Web IDE will make your direct editing even easier.
|
||||
|
||||
|
@ -44,14 +42,6 @@ The Web IDE currently provides:
|
|||
- IntelliSense and validation support (displaying errors and warnings, providing
|
||||
smart completions, formatting, and outlining) for some languages. For example:
|
||||
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
|
||||
- Validation support for certain JSON and YAML files using schemas based on the
|
||||
[JSON Schema Store](https://www.schemastore.org/json/). This feature
|
||||
is only supported for the `.gitlab-ci.yml` file.
|
||||
|
||||
NOTE: **Note:**
|
||||
Validation support based on schemas is hidden behind
|
||||
the feature flag `:schema_linting` on self-managed installations. To enable the
|
||||
feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
|
||||
|
||||
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
|
||||
you can find a more complete list of supported languages in the
|
||||
|
@ -63,6 +53,37 @@ If you are missing Syntax Highlighting support for any language, we prepared a s
|
|||
NOTE: **Note:**
|
||||
Single file editing is based on the [Ace Editor](https://ace.c9.io).
|
||||
|
||||
### Schema based validation
|
||||
|
||||
> - Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
|
||||
> - It was deployed behind a feature flag, disabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It cannot be enabled or disabled per-project.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-schema-based-validation-core-only).
|
||||
|
||||
The Web IDE provides validation support for certain JSON and YAML files using schemas
|
||||
based on the [JSON Schema Store](https://www.schemastore.org/json/). This feature is
|
||||
only supported for the `.gitlab-ci.yml` file.
|
||||
|
||||
#### Enable or disable Schema based validation **(CORE ONLY)**
|
||||
|
||||
Schema based validation is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default** for self-managed instances,
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it for your instance.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:schema_linting)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:schema_linting)
|
||||
```
|
||||
|
||||
### Themes
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.
|
||||
|
|
|
@ -8653,6 +8653,9 @@ msgstr ""
|
|||
msgid "Edit Release"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit Requirement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit Slack integration"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12713,9 +12716,15 @@ msgstr ""
|
|||
msgid "Incident Management Limits"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|All incidents"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Assignees"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Closed"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Create incident"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12731,6 +12740,9 @@ msgstr ""
|
|||
msgid "IncidentManagement|No incidents to display."
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "IncidentManagement|There was an error displaying the incidents."
|
||||
msgstr ""
|
||||
|
||||
|
@ -15803,6 +15815,9 @@ msgstr ""
|
|||
msgid "New Project"
|
||||
msgstr ""
|
||||
|
||||
msgid "New Requirement"
|
||||
msgstr ""
|
||||
|
||||
msgid "New Snippet"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20335,9 +20350,6 @@ msgstr ""
|
|||
msgid "Required in this project."
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement %{reference} has been added"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -217,7 +217,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
|
|||
|
||||
it_behaves_like 'all pipelines'
|
||||
|
||||
it 'includes custom filters' do
|
||||
it 'includes custom filters', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/233077' do
|
||||
aggregate_failures 'UploadLinkFilter' do
|
||||
expect(doc).to parse_upload_links
|
||||
end
|
||||
|
@ -282,7 +282,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
|
|||
|
||||
it_behaves_like 'all pipelines'
|
||||
|
||||
it 'includes custom filters' do
|
||||
it 'includes custom filters', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/233077' do
|
||||
aggregate_failures 'UploadLinkFilter' do
|
||||
expect(doc).to parse_upload_links
|
||||
end
|
||||
|
|
|
@ -2,9 +2,28 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples_for 'snippet editor' do
|
||||
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
|
||||
include DropzoneHelper
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) do
|
||||
create(:project, :public, creator: user).tap do |p|
|
||||
p.add_maintainer(user)
|
||||
end
|
||||
end
|
||||
|
||||
let(:title) { 'My Snippet Title' }
|
||||
let(:file_content) { 'Hello World!' }
|
||||
let(:md_description) { 'My Snippet **Description**' }
|
||||
let(:description) { 'My Snippet Description' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
stub_feature_flags(snippets_edit_vue: false)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit new_project_snippet_path(project)
|
||||
end
|
||||
|
||||
def description_field
|
||||
|
@ -12,137 +31,81 @@ RSpec.shared_examples_for 'snippet editor' do
|
|||
end
|
||||
|
||||
def fill_form
|
||||
fill_in 'project_snippet_title', with: 'My Snippet Title'
|
||||
fill_in 'project_snippet_title', with: title
|
||||
|
||||
# Click placeholder first to expand full description field
|
||||
description_field.click
|
||||
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
|
||||
fill_in 'project_snippet_description', with: md_description
|
||||
|
||||
page.within('.file-editor') do
|
||||
el = find('.inputarea')
|
||||
el.send_keys 'Hello World!'
|
||||
el.send_keys file_content
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is authenticated' do
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
it 'shows collapsible description input' do
|
||||
collapsed = description_field
|
||||
|
||||
visit project_snippets_path(project)
|
||||
expect(page).not_to have_field('project_snippet_description')
|
||||
expect(collapsed).to be_visible
|
||||
|
||||
# Wait for the SVG to ensure the button location doesn't shift
|
||||
within('.empty-state') { find('img.js-lazy-loaded') }
|
||||
click_on('New snippet')
|
||||
wait_for_requests
|
||||
end
|
||||
collapsed.click
|
||||
|
||||
it 'shows collapsible description input' do
|
||||
collapsed = description_field
|
||||
expect(page).to have_field('project_snippet_description')
|
||||
expect(collapsed).not_to be_visible
|
||||
end
|
||||
|
||||
expect(page).not_to have_field('project_snippet_description')
|
||||
expect(collapsed).to be_visible
|
||||
it 'creates a new snippet' do
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
collapsed.click
|
||||
|
||||
expect(page).to have_field('project_snippet_description')
|
||||
expect(collapsed).not_to be_visible
|
||||
end
|
||||
|
||||
it 'creates a new snippet' do
|
||||
fill_form
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('My Snippet Title')
|
||||
expect(page).to have_content('Hello World!')
|
||||
page.within('.snippet-header .description') do
|
||||
expect(page).to have_content('My Snippet Description')
|
||||
expect(page).to have_selector('strong')
|
||||
end
|
||||
end
|
||||
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
expect(page.find_field("project_snippet_description").value).to have_content('banana_sample')
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'creates a snippet when all required fields are filled in after validation failing' do
|
||||
fill_in 'project_snippet_title', with: 'My Snippet Title'
|
||||
click_button('Create snippet')
|
||||
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
find("input[value='Create snippet']").send_keys(:return)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_content('My Snippet Title')
|
||||
expect(page).to have_content('Hello World!')
|
||||
page.within('.snippet-header .description') do
|
||||
expect(page).to have_content('My Snippet Description')
|
||||
expect(page).to have_selector('strong')
|
||||
end
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
end
|
||||
|
||||
fill_form
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
end
|
||||
|
||||
it 'renders new page' do
|
||||
expect(page).to have_content('New Snippet')
|
||||
end
|
||||
expect(page).to have_content(title)
|
||||
expect(page).to have_content(file_content)
|
||||
page.within('.snippet-header .description') do
|
||||
expect(page).to have_content(description)
|
||||
expect(page).to have_selector('strong')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a user is not authenticated' do
|
||||
it 'uploads a file when dragging into textarea' do
|
||||
fill_form
|
||||
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
|
||||
|
||||
expect(page.find_field('project_snippet_description').value).to have_content('banana_sample')
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
|
||||
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
|
||||
end
|
||||
|
||||
it 'displays validation errors' do
|
||||
fill_in 'project_snippet_title', with: title
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_selector('#error_explanation')
|
||||
end
|
||||
|
||||
context 'when the git operation fails' do
|
||||
let(:error) { 'Error creating the snippet' }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
allow_next_instance_of(Snippets::CreateService) do |instance|
|
||||
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
|
||||
end
|
||||
|
||||
fill_form
|
||||
|
||||
click_button('Create snippet')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows a public snippet on the index page but not the New snippet button' do
|
||||
snippet = create(:project_snippet, :public, :repository, project: project)
|
||||
|
||||
visit project_snippets_path(project)
|
||||
|
||||
expect(page).to have_content(snippet.title)
|
||||
expect(page).not_to have_content('New snippet')
|
||||
it 'renders the new page and displays the error' do
|
||||
expect(page).to have_content(error)
|
||||
expect(page).to have_content('New Snippet')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
|
||||
include DropzoneHelper
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
|
||||
it_behaves_like "snippet editor"
|
||||
end
|
||||
|
|
|
@ -3,157 +3,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Projects > Snippets > Project snippet', :js do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) do
|
||||
create(:project, creator: user).tap do |p|
|
||||
p.add_maintainer(user)
|
||||
end
|
||||
end
|
||||
|
||||
let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
project.add_maintainer(user)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'Ruby file' do
|
||||
let(:file_name) { 'popen.rb' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
|
||||
it_behaves_like 'show and render proper snippet blob' do
|
||||
let(:anchor) { nil }
|
||||
|
||||
before do
|
||||
visit project_snippet_path(project, snippet)
|
||||
subject do
|
||||
visit project_snippet_path(project, snippet, anchor: anchor)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows highlighted Ruby code
|
||||
expect(page).to have_content("require 'fileutils'")
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown file' do
|
||||
let(:file_name) { 'ruby-style-guide.md' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
|
||||
|
||||
context 'visiting directly' do
|
||||
before do
|
||||
visit project_snippet_path(project, snippet)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows rendered Markdown
|
||||
expect(page).to have_link("PEP-8")
|
||||
|
||||
# shows a viewer switcher
|
||||
expect(page).to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows a disabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the simple viewer' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the rich viewer again' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'visiting with a line number anchor' do
|
||||
before do
|
||||
visit project_snippet_path(project, snippet, anchor: 'L1')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# highlights the line in question
|
||||
expect(page).to have_selector('#LC1.hll')
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'showing user status' do
|
||||
let(:file_name) { 'ruby-style-guide.md' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
|
||||
|
||||
let(:file_path) { 'files/ruby/popen.rb' }
|
||||
let(:user_with_status) { snippet.author }
|
||||
|
||||
subject do
|
||||
visit project_snippet_path(project, snippet)
|
||||
wait_for_requests
|
||||
end
|
||||
subject { visit project_snippet_path(project, snippet) }
|
||||
end
|
||||
|
||||
it_behaves_like 'does not show New Snippet button' do
|
||||
let(:file_path) { 'files/ruby/popen.rb' }
|
||||
|
||||
subject { visit project_snippet_path(project, snippet) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,180 +3,33 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Snippet', :js do
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(snippets_vue: false)
|
||||
end
|
||||
|
||||
context 'Ruby file' do
|
||||
let(:file_name) { 'popen.rb' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
|
||||
it_behaves_like 'show and render proper snippet blob' do
|
||||
let(:anchor) { nil }
|
||||
|
||||
before do
|
||||
visit snippet_path(snippet)
|
||||
subject do
|
||||
visit snippet_path(snippet, anchor: anchor)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
aggregate_failures do
|
||||
# shows highlighted Ruby code
|
||||
expect(page).to have_content("require 'fileutils'")
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown file' do
|
||||
let(:file_name) { 'ruby-style-guide.md' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
|
||||
|
||||
context 'visiting directly' do
|
||||
before do
|
||||
visit snippet_path(snippet)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows rendered Markdown
|
||||
expect(page).to have_link("PEP-8")
|
||||
|
||||
# shows a viewer switcher
|
||||
expect(page).to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows a disabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown rendering' do
|
||||
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
|
||||
let(:file_name) { 'test.md' }
|
||||
let(:content) { "1. one\n - sublist\n" }
|
||||
|
||||
context 'when rendering default markdown' do
|
||||
it 'renders using CommonMark' do
|
||||
expect(page).to have_content("sublist")
|
||||
expect(page).not_to have_xpath("//ol//li//ul")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the simple viewer' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the rich viewer again' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'visiting with a line number anchor' do
|
||||
before do
|
||||
visit snippet_path(snippet, anchor: 'L1')
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# highlights the line in question
|
||||
expect(page).to have_selector('#LC1.hll')
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'showing user status' do
|
||||
let(:file_name) { 'popen.rb' }
|
||||
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
|
||||
let(:file_path) { 'files/ruby/popen.rb' }
|
||||
let(:user_with_status) { snippet.author }
|
||||
|
||||
subject { visit snippet_path(snippet) }
|
||||
end
|
||||
|
||||
context 'when user cannot create snippets' do
|
||||
let(:user) { create(:user, :external) }
|
||||
let(:snippet) { create(:personal_snippet, :public) }
|
||||
it_behaves_like 'does not show New Snippet button' do
|
||||
let(:file_path) { 'files/ruby/popen.rb' }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
visit snippet_path(snippet)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'does not show the "New Snippet" button' do
|
||||
expect(page).not_to have_link('New snippet')
|
||||
end
|
||||
subject { visit snippet_path(snippet) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
GlSearchBoxByType,
|
||||
} from '@gitlab/ui';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
|
||||
import { ALERTS_STATUS_TABS, trackAlertStatusUpdateOptions } from '~/alert_management/constants';
|
||||
|
@ -44,6 +45,7 @@ describe('AlertManagementTable', () => {
|
|||
const findPagination = () => wrapper.find(GlPagination);
|
||||
const findSearch = () => wrapper.find(GlSearchBoxByType);
|
||||
const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]');
|
||||
const findAlertError = () => wrapper.find('[data-testid="alert-error"]');
|
||||
const alertsCount = {
|
||||
open: 14,
|
||||
triggered: 10,
|
||||
|
@ -51,6 +53,11 @@ describe('AlertManagementTable', () => {
|
|||
resolved: 1,
|
||||
all: 16,
|
||||
};
|
||||
const selectFirstStatusOption = () => {
|
||||
findFirstStatusOption().vm.$emit('click');
|
||||
|
||||
return waitForPromises();
|
||||
};
|
||||
|
||||
function mountComponent({
|
||||
props = {
|
||||
|
@ -138,7 +145,7 @@ describe('AlertManagementTable', () => {
|
|||
it('error state', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true },
|
||||
data: { alerts: { errors: ['error'] }, alertsCount: null, hasError: true },
|
||||
loading: false,
|
||||
});
|
||||
expect(findAlertsTable().exists()).toBe(true);
|
||||
|
@ -155,7 +162,7 @@ describe('AlertManagementTable', () => {
|
|||
it('empty state', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false },
|
||||
data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
expect(findAlertsTable().exists()).toBe(true);
|
||||
|
@ -172,7 +179,7 @@ describe('AlertManagementTable', () => {
|
|||
it('has data state', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
expect(findLoader().exists()).toBe(false);
|
||||
|
@ -188,7 +195,7 @@ describe('AlertManagementTable', () => {
|
|||
it('displays status dropdown', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
expect(findStatusDropdown().exists()).toBe(true);
|
||||
|
@ -197,7 +204,7 @@ describe('AlertManagementTable', () => {
|
|||
it('does not display a dropdown status header', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
expect(findStatusDropdown().contains('.dropdown-title')).toBe(false);
|
||||
|
@ -206,7 +213,7 @@ describe('AlertManagementTable', () => {
|
|||
it('shows correct severity icons', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -223,7 +230,7 @@ describe('AlertManagementTable', () => {
|
|||
it('renders severity text', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -237,7 +244,7 @@ describe('AlertManagementTable', () => {
|
|||
it('renders Unassigned when no assignee(s) present', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -251,7 +258,7 @@ describe('AlertManagementTable', () => {
|
|||
it('renders username(s) when assignee(s) present', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -265,7 +272,7 @@ describe('AlertManagementTable', () => {
|
|||
it('navigates to the detail page when alert row is clicked', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -279,7 +286,7 @@ describe('AlertManagementTable', () => {
|
|||
beforeEach(() => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
@ -323,7 +330,7 @@ describe('AlertManagementTable', () => {
|
|||
],
|
||||
},
|
||||
alertsCount,
|
||||
errored: false,
|
||||
hasError: false,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
@ -343,7 +350,7 @@ describe('AlertManagementTable', () => {
|
|||
},
|
||||
],
|
||||
alertsCount,
|
||||
errored: false,
|
||||
hasError: false,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
@ -358,7 +365,7 @@ describe('AlertManagementTable', () => {
|
|||
it('should highlight the row when alert is new', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: [newAlert] }, alertsCount, errored: false },
|
||||
data: { alerts: { list: [newAlert] }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -372,7 +379,7 @@ describe('AlertManagementTable', () => {
|
|||
it('should not highlight the row when alert is not new', () => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: [oldAlert] }, alertsCount, errored: false },
|
||||
data: { alerts: { list: [oldAlert] }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -392,7 +399,7 @@ describe('AlertManagementTable', () => {
|
|||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: {
|
||||
alerts: { list: mockAlerts },
|
||||
errored: false,
|
||||
hasError: false,
|
||||
sort: 'STARTED_AT_DESC',
|
||||
alertsCount,
|
||||
},
|
||||
|
@ -429,7 +436,7 @@ describe('AlertManagementTable', () => {
|
|||
beforeEach(() => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
@ -448,19 +455,36 @@ describe('AlertManagementTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('shows an error when request fails', () => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
|
||||
findFirstStatusOption().vm.$emit('click');
|
||||
wrapper.setData({
|
||||
errored: true,
|
||||
describe('when a request fails', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
|
||||
});
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find('[data-testid="alert-error"]').exists()).toBe(true);
|
||||
it('shows an error', async () => {
|
||||
await selectFirstStatusOption();
|
||||
|
||||
expect(findAlertError().text()).toContain(
|
||||
'There was an error while updating the status of the alert.',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an error when triggered a second time', async () => {
|
||||
await selectFirstStatusOption();
|
||||
|
||||
wrapper.find(GlAlert).vm.$emit('dismiss');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Assert that the error has been dismissed in the setup
|
||||
expect(findAlertError().exists()).toBe(false);
|
||||
|
||||
await selectFirstStatusOption();
|
||||
|
||||
expect(findAlertError().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows an error when response includes HTML errors', () => {
|
||||
it('shows an error when response includes HTML errors', async () => {
|
||||
const mockUpdatedMutationErrorResult = {
|
||||
data: {
|
||||
updateAlertStatus: {
|
||||
|
@ -474,13 +498,11 @@ describe('AlertManagementTable', () => {
|
|||
};
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult);
|
||||
findFirstStatusOption().vm.$emit('click');
|
||||
wrapper.setData({ errored: true });
|
||||
|
||||
return wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.contains('[data-testid="alert-error"]')).toBe(true);
|
||||
expect(wrapper.contains('[data-testid="htmlError"]')).toBe(true);
|
||||
});
|
||||
await selectFirstStatusOption();
|
||||
|
||||
expect(findAlertError().exists()).toBe(true);
|
||||
expect(findAlertError().contains('[data-testid="htmlError"]')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -510,7 +532,7 @@ describe('AlertManagementTable', () => {
|
|||
beforeEach(() => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
@ -570,7 +592,7 @@ describe('AlertManagementTable', () => {
|
|||
beforeEach(() => {
|
||||
mountComponent({
|
||||
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
|
||||
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,11 +6,12 @@ import {
|
|||
GlAvatar,
|
||||
GlPagination,
|
||||
GlSearchBoxByType,
|
||||
GlTab,
|
||||
} from '@gitlab/ui';
|
||||
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
|
||||
import IncidentsList from '~/incidents/components/incidents_list.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { I18N } from '~/incidents/constants';
|
||||
import { I18N, INCIDENT_STATE_TABS } from '~/incidents/constants';
|
||||
import mockIncidents from '../mocks/incidents.json';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
|
@ -34,6 +35,7 @@ describe('Incidents List', () => {
|
|||
const findSearch = () => wrapper.find(GlSearchBoxByType);
|
||||
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
|
||||
const findPagination = () => wrapper.find(GlPagination);
|
||||
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
|
||||
|
||||
function mountComponent({ data = { incidents: [] }, loading = false }) {
|
||||
wrapper = mount(IncidentsList, {
|
||||
|
@ -280,5 +282,25 @@ describe('Incidents List', () => {
|
|||
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Filter Tabs', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({
|
||||
data: { incidents: mockIncidents },
|
||||
loading: false,
|
||||
stubs: {
|
||||
GlTab: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should display filter tabs', () => {
|
||||
const tabs = findStatusFilterTabs().wrappers;
|
||||
|
||||
tabs.forEach((tab, i) => {
|
||||
expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::Issues::SetSubscription do
|
||||
it_behaves_like 'a subscribeable graphql resource' do
|
||||
let_it_be(:resource) { create(:issue) }
|
||||
let(:permission_name) { :update_issue }
|
||||
end
|
||||
end
|
|
@ -3,44 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Mutations::MergeRequests::SetSubscription do
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:project) { merge_request.project }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:subscribe) { true }
|
||||
let(:mutated_merge_request) { subject[:merge_request] }
|
||||
|
||||
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, subscribed_state: subscribe) }
|
||||
|
||||
it 'raises an error if the resource is not accessible to the user' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
|
||||
context 'when the user can update the merge request' do
|
||||
before do
|
||||
merge_request.project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns the merge request as discussion locked' do
|
||||
expect(mutated_merge_request).to eq(merge_request)
|
||||
expect(mutated_merge_request.subscribed?(user, project)).to eq(true)
|
||||
expect(subject[:errors]).to be_empty
|
||||
end
|
||||
|
||||
context 'when passing subscribe as false' do
|
||||
let(:subscribe) { false }
|
||||
|
||||
it 'unsubscribes from the discussion' do
|
||||
merge_request.subscribe(user, project)
|
||||
|
||||
expect(mutated_merge_request.subscribed?(user, project)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
it_behaves_like 'a subscribeable graphql resource' do
|
||||
let_it_be(:resource) { create(:merge_request) }
|
||||
let(:permission_name) { :update_merge_request }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'migrate', '20200728080250_replace_unique_index_on_cycle_analytics_stages.rb')
|
||||
|
||||
RSpec.describe ReplaceUniqueIndexOnCycleAnalyticsStages, :migration, schema: 20200728080250 do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
|
||||
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
|
||||
|
||||
let(:group) { namespaces.create!(type: 'Group', name: 'test', path: 'test') }
|
||||
|
||||
let(:value_stream_1) { group_value_streams.create!(group_id: group.id, name: 'vs1') }
|
||||
let(:value_stream_2) { group_value_streams.create!(group_id: group.id, name: 'vs2') }
|
||||
|
||||
let(:duplicated_stage_1) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_1.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
|
||||
let(:duplicated_stage_2) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
|
||||
|
||||
let(:stage_record) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'other stage', start_event_identifier: 1, end_event_identifier: 1) }
|
||||
|
||||
describe '#down' do
|
||||
subject { described_class.new.down }
|
||||
|
||||
before do
|
||||
described_class.new.up
|
||||
|
||||
duplicated_stage_1
|
||||
duplicated_stage_2
|
||||
stage_record
|
||||
end
|
||||
|
||||
it 'removes duplicated stage records' do
|
||||
subject
|
||||
|
||||
stage = group_stages.find_by_id(duplicated_stage_2.id)
|
||||
expect(stage).to be_nil
|
||||
end
|
||||
|
||||
it 'does not change the first duplicated stage record' do
|
||||
expect { subject }.not_to change { duplicated_stage_1.reload.attributes }
|
||||
end
|
||||
|
||||
it 'does not change not duplicated stage record' do
|
||||
expect { subject }.not_to change { stage_record.reload.attributes }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4125,7 +4125,6 @@ RSpec.describe Project do
|
|||
end
|
||||
|
||||
it 'removes the pages directory and marks the project as not having pages deployed' do
|
||||
expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
|
||||
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
|
||||
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Setting subscribed status of an issue' do
|
||||
include GraphqlHelpers
|
||||
|
||||
it_behaves_like 'a subscribable resource api' do
|
||||
let_it_be(:resource) { create(:issue) }
|
||||
let(:mutation_name) { :issue_set_subscription }
|
||||
end
|
||||
end
|
|
@ -5,59 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe 'Setting subscribed status of a merge request' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:current_user) { create(:user) }
|
||||
let(:merge_request) { create(:merge_request) }
|
||||
let(:project) { merge_request.project }
|
||||
let(:input) { { subscribed_state: true } }
|
||||
|
||||
let(:mutation) do
|
||||
variables = {
|
||||
project_path: project.full_path,
|
||||
iid: merge_request.iid.to_s
|
||||
}
|
||||
graphql_mutation(:merge_request_set_subscription, variables.merge(input),
|
||||
<<-QL.strip_heredoc
|
||||
clientMutationId
|
||||
errors
|
||||
mergeRequest {
|
||||
id
|
||||
subscribed
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
def mutation_response
|
||||
graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed']
|
||||
end
|
||||
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'returns an error if the user is not allowed to update the merge request' do
|
||||
post_graphql_mutation(mutation, current_user: create(:user))
|
||||
|
||||
expect(graphql_errors).not_to be_empty
|
||||
end
|
||||
|
||||
it 'marks the merge request as WIP' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response).to eq(true)
|
||||
end
|
||||
|
||||
context 'when passing subscribe false as input' do
|
||||
let(:input) { { subscribed_state: false } }
|
||||
|
||||
it 'unmarks the merge request as subscribed' do
|
||||
merge_request.subscribe(current_user, project)
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response).to eq(false)
|
||||
end
|
||||
it_behaves_like 'a subscribable resource api' do
|
||||
let_it_be(:resource) { create(:merge_request) }
|
||||
let(:mutation_name) { :merge_request_set_subscription }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
|
|||
include ReactiveCachingHelpers
|
||||
include GrafanaApiHelpers
|
||||
|
||||
let_it_be(:project) { build(:project) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
|
||||
|
||||
|
@ -15,7 +15,7 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
|
|||
valid_grafana_dashboard_link(grafana_integration.grafana_url)
|
||||
end
|
||||
|
||||
before do
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
|
@ -58,6 +58,31 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
|
|||
expect(subject.current_user).to eq(user)
|
||||
expect(subject.params[:grafana_url]).to eq(grafana_url)
|
||||
end
|
||||
|
||||
context 'with unknown users' do
|
||||
let(:params) { [project.id, current_user_id, grafana_url] }
|
||||
|
||||
context 'when anonymous' do
|
||||
where(:current_user_id) do
|
||||
[nil, '']
|
||||
end
|
||||
|
||||
with_them do
|
||||
it 'sets current_user as nil' do
|
||||
expect(subject.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid' do
|
||||
let(:current_user_id) { non_existing_record_id }
|
||||
|
||||
it 'raise record not found error' do
|
||||
expect { subject }
|
||||
.to raise_error(ActiveRecord::RecordNotFound, /Couldn't find User/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_dashboard', :use_clean_rails_memory_store_caching do
|
||||
|
@ -145,7 +170,17 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
|
|||
stub_datasource_request(grafana_integration.grafana_url)
|
||||
end
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
context 'when project is private and user is member' do
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
end
|
||||
|
||||
context 'when project is public and user is anonymous' do
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:user) { nil }
|
||||
let(:grafana_integration) { create(:grafana_integration, project: project) }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -50,3 +50,145 @@ RSpec.shared_examples 'tabs with counts' do
|
|||
expect(tab.find('.badge').text).to eq(counts[:public])
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'does not show New Snippet button' do
|
||||
let(:user) { create(:user, :external) }
|
||||
|
||||
specify do
|
||||
sign_in(user)
|
||||
|
||||
subject
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_link('New snippet')
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'show and render proper snippet blob' do
|
||||
before do
|
||||
allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)])
|
||||
end
|
||||
|
||||
context 'Ruby file' do
|
||||
let(:file_path) { 'files/ruby/popen.rb' }
|
||||
|
||||
it 'displays the blob' do
|
||||
subject
|
||||
|
||||
aggregate_failures do
|
||||
# shows highlighted Ruby code
|
||||
expect(page).to have_content("require 'fileutils'")
|
||||
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Markdown file' do
|
||||
let(:file_path) { 'files/markdown/ruby-style-guide.md' }
|
||||
|
||||
context 'visiting directly' do
|
||||
before do
|
||||
subject
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows rendered Markdown
|
||||
expect(page).to have_link("PEP-8")
|
||||
|
||||
# shows a viewer switcher
|
||||
expect(page).to have_selector('.js-blob-viewer-switcher')
|
||||
|
||||
# shows a disabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
|
||||
|
||||
# shows a raw button
|
||||
expect(page).to have_link('Open raw')
|
||||
|
||||
# shows a download button
|
||||
expect(page).to have_link('Download')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the simple viewer' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'switching to the rich viewer again' do
|
||||
before do
|
||||
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'displays the blob using the rich viewer' do
|
||||
aggregate_failures do
|
||||
# hides the simple viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'visiting with a line number anchor' do
|
||||
let(:anchor) { 'L1' }
|
||||
|
||||
it 'displays the blob using the simple viewer' do
|
||||
subject
|
||||
|
||||
aggregate_failures do
|
||||
# hides the rich viewer
|
||||
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
|
||||
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
|
||||
|
||||
# highlights the line in question
|
||||
expect(page).to have_selector('#LC1.hll')
|
||||
|
||||
# shows highlighted Markdown code
|
||||
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
|
||||
|
||||
# shows an enabled copy button
|
||||
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'a subscribeable graphql resource' do
|
||||
let(:project) { resource.project }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
|
||||
|
||||
specify { expect(described_class).to require_graphql_authorizations(permission_name) }
|
||||
|
||||
describe '#resolve' do
|
||||
let(:subscribe) { true }
|
||||
let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] }
|
||||
|
||||
subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) }
|
||||
|
||||
it 'raises an error if the resource is not accessible to the user' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
|
||||
context 'when the user can update the resource' do
|
||||
before do
|
||||
resource.project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'subscribes to the resource' do
|
||||
expect(mutated_resource).to eq(resource)
|
||||
expect(mutated_resource.subscribed?(user, project)).to eq(true)
|
||||
expect(subject[:errors]).to be_empty
|
||||
end
|
||||
|
||||
context 'when passing subscribe as false' do
|
||||
let(:subscribe) { false }
|
||||
|
||||
it 'unsubscribes from the discussion' do
|
||||
resource.subscribe(user, project)
|
||||
|
||||
expect(mutated_resource.subscribed?(user, project)).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.shared_examples 'a subscribable resource api' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let(:project) { resource.project }
|
||||
let(:input) { { subscribed_state: true } }
|
||||
let(:resource_ref) { resource.class.name.camelize(:lower) }
|
||||
|
||||
let(:mutation) do
|
||||
variables = {
|
||||
project_path: project.full_path,
|
||||
iid: resource.iid.to_s
|
||||
}
|
||||
|
||||
graphql_mutation(
|
||||
mutation_name,
|
||||
variables.merge(input),
|
||||
<<-QL.strip_heredoc
|
||||
clientMutationId
|
||||
errors
|
||||
#{resource_ref} {
|
||||
id
|
||||
subscribed
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
def mutation_response
|
||||
graphql_mutation_response(mutation_name)[resource_ref]['subscribed']
|
||||
end
|
||||
|
||||
context 'when the user is not authorized' do
|
||||
it_behaves_like 'a mutation that returns top-level errors',
|
||||
errors: ["The resource that you are attempting to access "\
|
||||
"does not exist or you don't have permission to "\
|
||||
"perform this action"]
|
||||
end
|
||||
|
||||
context 'when user is authorized' do
|
||||
before do
|
||||
project.add_developer(current_user)
|
||||
end
|
||||
|
||||
it 'marks the resource as subscribed' do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response).to eq(true)
|
||||
end
|
||||
|
||||
context 'when passing subscribe false as input' do
|
||||
let(:input) { { subscribed_state: false } }
|
||||
|
||||
it 'unmarks the resource as subscribed' do
|
||||
resource.subscribe(current_user, project)
|
||||
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -78,6 +78,12 @@ RSpec.shared_examples 'raises error for users with insufficient permissions' do
|
|||
|
||||
it_behaves_like 'misconfigured dashboard service response', :unauthorized
|
||||
end
|
||||
|
||||
context 'when the user is anonymous' do
|
||||
let(:user) { nil }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence|
|
||||
|
|
|
@ -11,31 +11,57 @@ RSpec.describe GitGarbageCollectWorker do
|
|||
let(:shell) { Gitlab::Shell.new }
|
||||
let!(:lease_uuid) { SecureRandom.uuid }
|
||||
let!(:lease_key) { "project_housekeeping:#{project.id}" }
|
||||
let(:params) { [project.id, task, lease_key, lease_uuid] }
|
||||
|
||||
subject { described_class.new }
|
||||
|
||||
shared_examples 'it calls Gitaly' do
|
||||
specify do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(gitaly_task)
|
||||
.and_return(nil)
|
||||
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'it updates the project statistics' do
|
||||
specify do
|
||||
expect_any_instance_of(Projects::UpdateStatisticsService).to receive(:execute).and_call_original
|
||||
expect(Projects::UpdateStatisticsService)
|
||||
.to receive(:new)
|
||||
.with(project, nil, statistics: [:repository_size, :lfs_objects_size])
|
||||
.and_call_original
|
||||
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#perform" do
|
||||
let(:gitaly_task) { :garbage_collect }
|
||||
let(:task) { :gc }
|
||||
|
||||
context 'with active lease_uuid' do
|
||||
before do
|
||||
allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
|
||||
end
|
||||
|
||||
it_behaves_like 'it calls Gitaly'
|
||||
it_behaves_like 'it updates the project statistics'
|
||||
|
||||
it "flushes ref caches when the task if 'gc'" do
|
||||
expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
|
||||
.and_return(nil)
|
||||
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
|
||||
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
|
||||
expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
|
||||
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
|
||||
|
||||
subject.perform(project.id, :gc, lease_key, lease_uuid)
|
||||
subject.perform(*params)
|
||||
end
|
||||
|
||||
it 'handles gRPC errors' do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect).and_raise(GRPC::NotFound)
|
||||
|
||||
expect { subject.perform(project.id, :gc, lease_key, lease_uuid) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
|
||||
expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -49,11 +75,13 @@ RSpec.describe GitGarbageCollectWorker do
|
|||
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
|
||||
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
|
||||
|
||||
subject.perform(project.id, :gc, lease_key, lease_uuid)
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no active lease' do
|
||||
let(:params) { [project.id] }
|
||||
|
||||
before do
|
||||
allow(subject).to receive(:get_lease_uuid).and_return(false)
|
||||
end
|
||||
|
@ -63,15 +91,16 @@ RSpec.describe GitGarbageCollectWorker do
|
|||
allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
|
||||
end
|
||||
|
||||
it_behaves_like 'it calls Gitaly'
|
||||
it_behaves_like 'it updates the project statistics'
|
||||
|
||||
it "flushes ref caches when the task if 'gc'" do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
|
||||
.and_return(nil)
|
||||
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
|
||||
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
|
||||
expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
|
||||
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
|
||||
|
||||
subject.perform(project.id)
|
||||
subject.perform(*params)
|
||||
end
|
||||
|
||||
context 'when the repository has joined a pool' do
|
||||
|
@ -81,7 +110,7 @@ RSpec.describe GitGarbageCollectWorker do
|
|||
it 'ensures the repositories are linked' do
|
||||
expect_any_instance_of(PoolRepository).to receive(:link_repository).once
|
||||
|
||||
subject.perform(project.id)
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -97,48 +126,55 @@ RSpec.describe GitGarbageCollectWorker do
|
|||
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
|
||||
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
|
||||
|
||||
subject.perform(project.id)
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "repack_full" do
|
||||
let(:task) { :full_repack }
|
||||
let(:gitaly_task) { :repack_full }
|
||||
|
||||
before do
|
||||
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
|
||||
end
|
||||
|
||||
it "calls Gitaly" do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full)
|
||||
.and_return(nil)
|
||||
|
||||
subject.perform(project.id, :full_repack, lease_key, lease_uuid)
|
||||
end
|
||||
it_behaves_like 'it calls Gitaly'
|
||||
it_behaves_like 'it updates the project statistics'
|
||||
end
|
||||
|
||||
context "pack_refs" do
|
||||
let(:task) { :pack_refs }
|
||||
let(:gitaly_task) { :pack_refs }
|
||||
|
||||
before do
|
||||
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
|
||||
end
|
||||
|
||||
it "calls Gitaly" do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:pack_refs)
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(task)
|
||||
.and_return(nil)
|
||||
|
||||
subject.perform(project.id, :pack_refs, lease_key, lease_uuid)
|
||||
subject.perform(*params)
|
||||
end
|
||||
|
||||
it 'does not update the project statistics' do
|
||||
expect(Projects::UpdateStatisticsService).not_to receive(:new)
|
||||
|
||||
subject.perform(*params)
|
||||
end
|
||||
end
|
||||
|
||||
context "repack_incremental" do
|
||||
let(:task) { :incremental_repack }
|
||||
let(:gitaly_task) { :repack_incremental }
|
||||
|
||||
before do
|
||||
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
|
||||
end
|
||||
|
||||
it "calls Gitaly" do
|
||||
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental)
|
||||
.and_return(nil)
|
||||
|
||||
subject.perform(project.id, :incremental_repack, lease_key, lease_uuid)
|
||||
end
|
||||
it_behaves_like 'it calls Gitaly'
|
||||
it_behaves_like 'it updates the project statistics'
|
||||
end
|
||||
|
||||
shared_examples 'gc tasks' do
|
||||
|
|
Loading…
Reference in New Issue