Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-18 12:09:34 +00:00
parent 00c8da9174
commit 213bd7e9d3
96 changed files with 1605 additions and 204 deletions

View File

@ -31,7 +31,6 @@ export default () => {
return createElement(CommitPipelinesTable, {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
},

View File

@ -25,10 +25,6 @@ export default {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,

View File

@ -35,7 +35,9 @@ export default {
this.track(this.$options.dismissEvent);
},
trackOnShow() {
if (!this.isDismissed) this.track(this.$options.displayEvent);
this.$nextTick(() => {
if (!this.isDismissed) this.track(this.$options.displayEvent);
});
},
addTrackingAttributesToButton() {
if (this.$refs.banner === undefined) return;

View File

@ -32,7 +32,7 @@ export default {
SafeHtml,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapState(['pipelinesEmptyStateSvgPath']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', [
@ -85,7 +85,6 @@ export default {
</header>
<empty-state
v-if="!latestPipeline"
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
class="mb-auto mt-auto"

View File

@ -53,7 +53,6 @@ export function initIde(el, options = {}) {
promotionSvgPath: el.dataset.promotionSvgPath,
});
this.setLinks({
ciHelpPagePath: el.dataset.ciHelpPagePath,
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
});
this.setInitialData({

View File

@ -51,7 +51,7 @@ export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selec
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
'/help/operations/metrics/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#creating-issue-templates';
'/help/user/project/description_templates#create-an-issue-template';
/* PagerDuty integration settings constants */

View File

@ -307,7 +307,7 @@ export default {
});
},
updateAndShowForm(templates = []) {
updateAndShowForm(templates = {}) {
if (!this.showForm) {
this.showForm = true;
this.store.setFormState({

View File

@ -13,9 +13,9 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: [Object, Array],
required: false,
default: () => [],
default: () => {},
},
projectPath: {
type: String,

View File

@ -26,9 +26,9 @@ export default {
required: true,
},
issuableTemplates: {
type: Array,
type: [Object, Array],
required: false,
default: () => [],
default: () => {},
},
issuableType: {
type: String,
@ -72,7 +72,7 @@ export default {
},
computed: {
hasIssuableTemplates() {
return this.issuableTemplates.length;
return Object.values(Object(this.issuableTemplates)).length;
},
showLockedWarning() {
return this.formState.lockedWarningVisible && !this.formState.updateLoading;

View File

@ -11,7 +11,7 @@ export default class Store {
lockedWarningVisible: false,
updateLoading: false,
lock_version: 0,
issuableTemplates: [],
issuableTemplates: {},
};
}

View File

@ -361,7 +361,6 @@ export default class MergeRequestTabs {
return createElement(CommitPipelinesTable, {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
canCreatePipelineInTargetProject: Boolean(

View File

@ -30,6 +30,10 @@ export default {
type: String,
required: true,
},
statsUrl: {
type: String,
required: true,
},
},
detailedMetrics: [
{
@ -169,6 +173,9 @@ export default {
class="ml-auto"
@change-current-request="changeCurrentRequest"
/>
<div v-if="statsUrl" id="peek-stats" class="view">
<a class="gl-text-blue-300" :href="statsUrl">{{ s__('PerformanceBar|Stats') }}</a>
</div>
</div>
</div>
</template>

View File

@ -29,6 +29,7 @@ const initPerformanceBar = (el) => {
requestId: performanceBarData.requestId,
peekUrl: performanceBarData.peekUrl,
profileUrl: performanceBarData.profileUrl,
statsUrl: performanceBarData.statsUrl,
};
},
mounted() {
@ -119,6 +120,7 @@ const initPerformanceBar = (el) => {
requestId: this.requestId,
peekUrl: this.peekUrl,
profileUrl: this.profileUrl,
statsUrl: this.statsUrl,
},
on: {
'add-request': this.addRequestManually,

View File

@ -1,12 +1,35 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineStatus from './pipeline_status.vue';
import ValidationSegment from './validation_segment.vue';
const baseClasses = ['gl-p-5', 'gl-bg-gray-10', 'gl-border-solid', 'gl-border-gray-100'];
const pipelineStatusClasses = [
...baseClasses,
'gl-border-1',
'gl-border-b-0!',
'gl-rounded-top-base',
];
const validationSegmentClasses = [...baseClasses, 'gl-border-1', 'gl-rounded-base'];
const validationSegmentWithPipelineStatusClasses = [
...baseClasses,
'gl-border-1',
'gl-rounded-bottom-left-base',
'gl-rounded-bottom-right-base',
];
export default {
validationSegmentClasses:
'gl-p-5 gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base',
pipelineStatusClasses,
validationSegmentClasses,
validationSegmentWithPipelineStatusClasses,
components: {
PipelineStatus,
ValidationSegment,
},
mixins: [glFeatureFlagsMixin()],
props: {
ciConfigData: {
type: Object,
@ -17,12 +40,25 @@ export default {
required: true,
},
},
computed: {
showPipelineStatus() {
return this.glFeatures.pipelineStatusForPipelineEditor;
},
// make sure corners are rounded correctly depending on if
// pipeline status is rendered
validationStyling() {
return this.showPipelineStatus
? this.$options.validationSegmentWithPipelineStatusClasses
: this.$options.validationSegmentClasses;
},
},
};
</script>
<template>
<div class="gl-mb-5">
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
<validation-segment
:class="$options.validationSegmentClasses"
:class="validationStyling"
:loading="isCiConfigDataLoading"
:ci-config="ciConfigData"
/>

View File

@ -0,0 +1,120 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
const POLL_INTERVAL = 10000;
export const i18n = {
fetchError: s__('Pipeline|We are currently unable to fetch pipeline data'),
fetchLoading: s__('Pipeline|Checking pipeline status'),
pipelineInfo: s__(
`Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}`,
),
};
export default {
i18n,
components: {
CiIcon,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
},
inject: ['projectFullPath'],
apollo: {
commitSha: {
query: getCommitSha,
},
pipeline: {
query: getPipelineQuery,
variables() {
return {
fullPath: this.projectFullPath,
sha: this.commitSha,
};
},
update: (data) => {
const { id, commitPath = '', shortSha = '', detailedStatus = {} } =
data.project?.pipeline || {};
return {
id,
commitPath,
shortSha,
detailedStatus,
};
},
error() {
this.hasError = true;
},
pollInterval: POLL_INTERVAL,
},
},
data() {
return {
hasError: false,
};
},
computed: {
hasPipelineData() {
return Boolean(this.$apollo.queries.pipeline?.id);
},
isQueryLoading() {
return this.$apollo.queries.pipeline.loading && !this.hasPipelineData;
},
status() {
return this.pipeline.detailedStatus;
},
pipelineId() {
return getIdFromGraphQLId(this.pipeline.id);
},
},
};
</script>
<template>
<div class="gl-white-space-nowrap gl-max-w-full">
<template v-if="isQueryLoading">
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
<span data-testid="pipeline-loading-msg">{{ $options.i18n.fetchLoading }}</span>
</template>
<template v-else-if="hasError">
<gl-icon class="gl-mr-auto" name="warning-solid" />
<span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
</template>
<template v-else>
<a :href="status.detailsPath" class="gl-mr-auto">
<ci-icon :status="status" :size="18" />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
<gl-link
:href="status.detailsPath"
class="pipeline-id gl-font-weight-normal pipeline-number"
target="_blank"
data-testid="pipeline-id"
>
{{ content }}{{ pipelineId }}</gl-link
>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
<gl-link
:href="pipeline.commitPath"
class="commit-sha gl-font-weight-normal"
target="_blank"
data-testid="pipeline-commit"
>
{{ pipeline.shortSha }}
</gl-link>
</template>
</gl-sprintf>
</span>
</template>
</div>
</template>

View File

@ -0,0 +1,17 @@
query getPipeline($fullPath: ID!, $sha: String!) {
project(fullPath: $fullPath) @client {
pipeline(sha: $sha) {
commitPath
id
iid
shortSha
status
detailedStatus {
detailsPath
icon
group
text
}
}
}
}

View File

@ -11,6 +11,29 @@ export const resolvers = {
}),
};
},
/* eslint-disable @gitlab/require-i18n-strings */
project() {
return {
__typename: 'Project',
pipeline: {
__typename: 'Pipeline',
commitPath: `/-/commit/aabbccdd`,
id: 'gid://gitlab/Ci::Pipeline/118',
iid: '28',
shortSha: 'aabbccdd',
status: 'SUCCESS',
detailedStatus: {
__typename: 'DetailedStatus',
detailsPath: '/root/sample-ci-project/-/pipelines/118"',
group: 'success',
icon: 'status_success',
text: 'passed',
},
},
};
},
/* eslint-enable @gitlab/require-i18n-strings */
},
Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => {

View File

@ -1,5 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
@ -14,10 +15,6 @@ export default {
GlButton,
},
props: {
helpPagePath: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
@ -27,6 +24,11 @@ export default {
required: true,
},
},
computed: {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
},
};
</script>
<template>
@ -47,7 +49,7 @@ export default {
<div class="gl-text-center">
<gl-button
:href="helpPagePath"
:href="ciHelpPagePath"
variant="info"
category="primary"
data-testid="get-started-pipelines"

View File

@ -52,10 +52,6 @@ export default {
required: false,
default: '',
},
helpPagePath: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
@ -333,7 +329,6 @@ export default {
<empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
/>

View File

@ -23,7 +23,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
const {
endpoint,
pipelineScheduleUrl,
helpPagePath,
emptyStateSvgPath,
errorStateSvgPath,
noPipelinesSvgPath,
@ -55,7 +54,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
store: this.store,
endpoint,
pipelineScheduleUrl,
helpPagePath,
emptyStateSvgPath,
errorStateSvgPath,
noPipelinesSvgPath,

View File

@ -69,21 +69,21 @@ export default {
<gl-button category="primary" variant="success" class="gl-ml-3" @click="onSubmit">
{{ s__('CompareRevisions|Compare') }}
</gl-button>
<a
<gl-button
v-if="projectMergeRequestPath"
:href="projectMergeRequestPath"
data-testid="projectMrButton"
class="btn btn-default gl-button gl-ml-3"
>
{{ s__('CompareRevisions|View open merge request') }}
</a>
<a
</gl-button>
<gl-button
v-else-if="createMrPath"
:href="createMrPath"
data-testid="createMrButton"
class="btn btn-default gl-button gl-ml-3"
>
{{ s__('CompareRevisions|Create merge request') }}
</a>
</gl-button>
</form>
</template>

View File

@ -1,10 +1,14 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlButton } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '../../../locale';
export default {
name: 'TimeTrackingHelpState',
components: {
GlButton,
},
computed: {
href() {
return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
@ -40,7 +44,7 @@ export default {
<p>{{ __('Quick actions can be used in the issues description and comment boxes.') }}</p>
<p v-html="estimateText"></p>
<p v-html="spendText"></p>
<a :href="href" class="btn btn-default learn-more-button"> {{ __('Learn more') }} </a>
<gl-button :href="href">{{ __('Learn more') }}</gl-button>
</div>
</div>
</template>

View File

@ -1,5 +1,13 @@
import { omitBy, isUndefined } from 'lodash';
export const STANDARD_CONTEXT = {
schema: 'iglu:com.gitlab/gitlab_standard/jsonschema/1-0-3',
data: {
environment: process.env.NODE_ENV,
source: 'gitlab-javascript',
},
};
const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
hostname: window.location.hostname,
@ -67,8 +75,13 @@ export default class Tracking {
// eslint-disable-next-line @gitlab/require-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.');
const { label, property, value, context } = data;
const contexts = context ? [context] : undefined;
const { label, property, value } = data;
const contexts = [STANDARD_CONTEXT];
if (data.context) {
contexts.push(data.context);
}
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
}
@ -134,7 +147,8 @@ export function initDefaultTrackers() {
if (!Tracking.enabled()) return;
window.snowplow('enableActivityTracking', 30, 30);
window.snowplow('trackPageView'); // must be after enableActivityTracking
// must be after enableActivityTracking
window.snowplow('trackPageView', null, [STANDARD_CONTEXT]);
if (window.snowplowOptions.formTracking) window.snowplow('enableFormTracking');
if (window.snowplowOptions.linkClickTracking) window.snowplow('enableLinkClickTracking');

View File

@ -59,11 +59,33 @@ const populateUserInfo = (user) => {
};
const initializedPopovers = new Map();
let domObservedForChanges = false;
export default (elements = document.querySelectorAll('.js-user-link')) => {
const addPopoversToModifiedTree = new MutationObserver(() => {
const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member');
if (userLinks) {
addPopovers(userLinks); /* eslint-disable-line no-use-before-define */
}
});
function observeBody() {
if (!domObservedForChanges) {
addPopoversToModifiedTree.observe(document.body, {
subtree: true,
childList: true,
});
domObservedForChanges = true;
}
}
export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) {
const userLinks = Array.from(elements);
const UserPopoverComponent = Vue.extend(UserPopover);
observeBody();
return userLinks
.filter(({ dataset }) => dataset.user || dataset.userId)
.map((el) => {
@ -105,4 +127,4 @@ export default (elements = document.querySelectorAll('.js-user-link')) => {
return renderedPopover;
});
};
}

View File

@ -5,6 +5,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:ci_config_visualization_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml)
end
feature_category :pipeline_authoring

View File

@ -25,7 +25,7 @@ class Projects::TemplatesController < Projects::ApplicationController
def names
respond_to do |format|
format.json { render json: TemplateFinder.all_template_names_array(project, params[:template_type].to_s.pluralize) }
format.json { render json: TemplateFinder.all_template_names_hash_or_array(project, params[:template_type].to_s) }
end
end

View File

@ -22,16 +22,26 @@ class TemplateFinder
end
end
# This is temporary and will be removed once we introduce group level inherited templates and
# remove the inherited_issuable_templates FF
def all_template_names_hash_or_array(project, issuable_type)
if project.inherited_issuable_templates_enabled?
all_template_names(project, issuable_type.pluralize)
else
all_template_names_array(project, issuable_type.pluralize)
end
end
def all_template_names(project, type)
return {} if !VENDORED_TEMPLATES.key?(type.to_s) && type.to_s != 'licenses'
build(type, project).template_names
end
# This is issues and merge requests description templates only.
# This will be removed once we introduce group level inherited templates
# This is for issues and merge requests description templates only.
# This will be removed once we introduce group level inherited templates and remove the inherited_issuable_templates FF
def all_template_names_array(project, type)
all_template_names(project, type).values.flatten.uniq
all_template_names(project, type).values.flatten.select { |tmpl| tmpl[:project_id] == project.id }.compact.uniq
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
module Resolvers
module AlertManagement
class HttpIntegrationsResolver < BaseResolver
alias_method :project, :synchronized_object
type Types::AlertManagement::HttpIntegrationType.connection_type, null: true
def resolve(**args)
http_integrations
end
private
def http_integrations
return [] unless Ability.allowed?(current_user, :admin_operations, project)
::AlertManagement::HttpIntegrationsFinder.new(project, {}).execute
end
end
end
end

View File

@ -20,3 +20,5 @@ module Types
end
end
end
Types::AlertManagement::HttpIntegrationType.prepend_ee_mod

View File

@ -273,6 +273,12 @@ module Types
description: 'Integrations which can receive alerts for the project.',
resolver: Resolvers::AlertManagement::IntegrationsResolver
field :alert_management_http_integrations,
Types::AlertManagement::HttpIntegrationType.connection_type,
null: true,
description: 'HTTP Integrations which can receive alerts for the project.',
resolver: Resolvers::AlertManagement::HttpIntegrationsResolver
field :releases,
Types::ReleaseType.connection_type,
null: true,

View File

@ -5,7 +5,8 @@ module IssuablesDescriptionTemplatesHelper
include GitlabRoutingHelper
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
selected_template = selected_template(issuable)
title = selected_template || "Choose a template"
options = {
toggle_class: 'js-issuable-selector',
title: title,
@ -15,7 +16,7 @@ module IssuablesDescriptionTemplatesHelper
data: {
data: issuable_templates(ref_project, issuable.to_ability_name),
field_name: 'issuable_template',
selected: selected_template(issuable),
selected: selected_template,
project_id: ref_project.id
}
}
@ -28,15 +29,21 @@ module IssuablesDescriptionTemplatesHelper
def issuable_templates(project, issuable_type)
@template_types ||= {}
@template_types[project.id] ||= {}
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names_array(project, issuable_type.pluralize)
@template_types[project.id][issuable_type] ||= TemplateFinder.all_template_names_hash_or_array(project, issuable_type)
end
def issuable_templates_names(issuable)
issuable_templates(ref_project, issuable.to_ability_name).map { |template| template[:name] }
all_templates = issuable_templates(ref_project, issuable.to_ability_name)
if ref_project.inherited_issuable_templates_enabled?
all_templates.values.flatten.map { |tpl| tpl[:name] if tpl[:project_id] == ref_project.id }.compact.uniq
else
all_templates.map { |template| template[:name] }
end
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(ref_project, issuable.to_ability_name).any? { |template| template[:name] == params[:issuable_template] }
params[:issuable_template] if issuable_templates_names(issuable).any? { |tmpl_name| tmpl_name == params[:issuable_template] }
end
def template_names_path(parent, issuable)

View File

@ -2532,6 +2532,10 @@ class Project < ApplicationRecord
Projects::GitGarbageCollectWorker
end
def inherited_issuable_templates_enabled?
Feature.enabled?(:inherited_issuable_templates, self, default_enabled: :yaml)
end
private
def find_service(services, name)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class MattermostService < ChatNotificationService
include ::SlackService::Notifier
include SlackMattermost::Notifier
def title
'Mattermost notifications'

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
module SlackMattermost
module Notifier
private
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
end
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
Gitlab::HTTP.post(uri, body: params)
end
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class SlackService < ChatNotificationService
include SlackMattermost::Notifier
prop_accessor EVENT_CHANNEL['alert']
def title
@ -35,27 +37,4 @@ class SlackService < ChatNotificationService
super
end
module Notifier
private
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
message.pretext,
attachments: message.attachments,
fallback: message.fallback
)
end
class HTTPClient
def self.post(uri, params = {})
params.delete(:http_options) # these are internal to the client and we do not want them
Gitlab::HTTP.post(uri, body: params)
end
end
end
include Notifier
end

View File

@ -5,6 +5,12 @@ module Issues
include Gitlab::Routing.url_helpers
include GitlabRoutingHelper
def initialize(issuables_relation, project)
super
@labels = @issuables.labels_hash.transform_values { |labels| labels.sort.join(',').presence }
end
def email(user)
Notify.issues_csv_email(user, project, csv_data, csv_builder.status).deliver_now
end
@ -12,7 +18,7 @@ module Issues
private
def associations_to_preload
%i(author assignees timelogs milestone)
%i(author assignees timelogs milestone project)
end
def header_to_value_hash
@ -41,7 +47,7 @@ module Issues
end
def issue_labels(issue)
issuables.labels_hash[issue.id].sort.join(',').presence
@labels[issue.id]
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -19,6 +19,8 @@ module Projects
@project = Project.new(params)
@project.visibility_level = @project.group.visibility_level unless @project.visibility_level_allowed_by_group?
# If a project is newly created it should have shared runners settings
# based on its group having it enabled. This is like the "default value"
@project.shared_runners_enabled = false if !params.key?(:shared_runners_enabled) && @project.group && @project.group.shared_runners_setting != 'enabled'

View File

@ -2,5 +2,6 @@
#js-peek{ data: { env: Peek.env,
request_id: peek_request_id,
stats_url: ENV.fetch('GITLAB_PERFORMANCE_BAR_STATS_URL', ''),
peek_url: "#{peek_routes_path}/results" },
class: Peek.env }

View File

@ -1,7 +1,6 @@
- disable_initialization = local_assigns.fetch(:disable_initialization, false)
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"project-id": @project.id,

View File

@ -7,7 +7,6 @@
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
project_id: @project.id,
params: params.to_json,
"help-page-path" => help_page_path('ci/quick_start/README'),
"pipeline-schedule-url" => pipeline_schedules_path(@project),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),

View File

@ -12,21 +12,21 @@
= render 'shared/ref_switcher', destination: 'badges', align_right: true
.card-body
.row
.col-md-2.text-center
.col-md-2.gl-text-center
Markdown
.col-md-10.code.js-syntax-highlight
= highlight_badge('.md', badge.to_markdown, language: 'markdown')
.row
%hr
.row
.col-md-2.text-center
.col-md-2.gl-text-center
HTML
.col-md-10.code.js-syntax-highlight
= highlight_badge('.html', badge.to_html, language: 'html')
.row
%hr
.row
.col-md-2.text-center
.col-md-2.gl-text-center
AsciiDoc
.col-md-10.code.js-syntax-highlight
= highlight_badge('.adoc', badge.to_asciidoc)

View File

@ -0,0 +1,5 @@
---
title: Send gitlab_standard context with events from the frontend
merge_request: 52959
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update 'Get Started with CI/CD' button with latest URL
merge_request: 54344
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix project import error occurring due to default visibility
merge_request: 53827
author: Jonas Wälter @wwwjon
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Apply new GitLab UI for learn more button in time tracking
merge_request: 54142
author: Yogi (@yo)
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix N+1 SQL regression in exporting issues to CSV
merge_request: 54287
author:
type: performance

View File

@ -0,0 +1,6 @@
---
title: React to new DOM nodes being added to the page to bind the user information
popover to them
merge_request: 54411
author:
type: fixed

View File

@ -0,0 +1,8 @@
---
name: inherited_issuable_templates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321247
milestone: '13.9'
type: development
group: group::project management
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: pipeline_status_for_pipeline_editor
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53797
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321518
milestone: '13.10'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -6,6 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Performance Bar **(FREE SELF)**
> The **Stats** field [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271551) in GitLab SaaS 13.9.
You can display the GitLab Performance Bar to see statistics for the performance
of a page. When activated, it looks as follows:
@ -53,6 +55,8 @@ From left to right, it displays:
- **Request Selector**: a select box displayed on the right-hand side of the
Performance Bar which enables you to view these metrics for any requests made while
the current page was open. Only the first two requests per unique URL are captured.
- **Stats** (optional): if the `GITLAB_PERFORMANCE_BAR_STATS_URL` environment variable is set,
this URL is displayed in the bar. In GitLab 13.9 and later, used only in GitLab SaaS.
## Request warnings

View File

@ -634,6 +634,21 @@ type AlertManagementHttpIntegration implements AlertManagementIntegration {
"""
name: String
"""
Extract alert fields from payload example for custom mapping.
"""
payloadAlertFields: [AlertManagementPayloadAlertField!]
"""
The custom mapping of GitLab alert attributes to fields from the payload_example.
"""
payloadAttributeMappings: [AlertManagementPayloadAlertMappingField!]
"""
The example of an alert payload.
"""
payloadExample: JsonString
"""
Token used to authenticate alert notification requests.
"""
@ -650,6 +665,41 @@ type AlertManagementHttpIntegration implements AlertManagementIntegration {
url: String
}
"""
The connection type for AlertManagementHttpIntegration.
"""
type AlertManagementHttpIntegrationConnection {
"""
A list of edges.
"""
edges: [AlertManagementHttpIntegrationEdge]
"""
A list of nodes.
"""
nodes: [AlertManagementHttpIntegration]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type AlertManagementHttpIntegrationEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: AlertManagementHttpIntegration
}
"""
Identifier of AlertManagement::HttpIntegration.
"""
@ -862,6 +912,31 @@ enum AlertManagementPayloadAlertFieldType {
STRING
}
"""
Parsed field (with its name) from an alert used for custom mappings
"""
type AlertManagementPayloadAlertMappingField {
"""
A GitLab alert field name.
"""
fieldName: AlertManagementPayloadAlertFieldName
"""
Human-readable label of the payload path.
"""
label: String
"""
Path to value inside payload JSON.
"""
path: [String!]
"""
Type of the parsed value.
"""
type: AlertManagementPayloadAlertFieldType
}
"""
An endpoint and credentials used to accept Prometheus alerts for a project
"""
@ -19031,6 +19106,31 @@ type Project {
statuses: [AlertManagementStatus!]
): AlertManagementAlertConnection
"""
HTTP Integrations which can receive alerts for the project.
"""
alertManagementHttpIntegrations(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): AlertManagementHttpIntegrationConnection
"""
Integrations which can receive alerts for the project.
"""

View File

@ -1578,6 +1578,64 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "payloadAlertFields",
"description": "Extract alert fields from payload example for custom mapping.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementPayloadAlertField",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "payloadAttributeMappings",
"description": "The custom mapping of GitLab alert attributes to fields from the payload_example.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementPayloadAlertMappingField",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "payloadExample",
"description": "The example of an alert payload.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "JsonString",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "token",
"description": "Token used to authenticate alert notification requests.",
@ -1636,6 +1694,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementHttpIntegrationConnection",
"description": "The connection type for AlertManagementHttpIntegration.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementHttpIntegrationEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "AlertManagementHttpIntegration",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementHttpIntegrationEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementHttpIntegration",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "AlertManagementHttpIntegrationID",
@ -2143,6 +2313,83 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementPayloadAlertMappingField",
"description": "Parsed field (with its name) from an alert used for custom mappings",
"fields": [
{
"name": "fieldName",
"description": "A GitLab alert field name.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "AlertManagementPayloadAlertFieldName",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "label",
"description": "Human-readable label of the payload path.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "path",
"description": "Path to value inside payload JSON.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of the parsed value.",
"args": [
],
"type": {
"kind": "ENUM",
"name": "AlertManagementPayloadAlertFieldType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "AlertManagementPrometheusIntegration",
@ -55864,6 +56111,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementHttpIntegrations",
"description": "HTTP Integrations which can receive alerts for the project.",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementHttpIntegrationConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "alertManagementIntegrations",
"description": "Integrations which can receive alerts for the project.",

View File

@ -274,6 +274,9 @@ An endpoint and credentials used to accept alerts for a project.
| `apiUrl` | String | URL at which Prometheus metrics can be queried to populate the metrics dashboard. |
| `id` | ID! | ID of the integration. |
| `name` | String | Name of the integration. |
| `payloadAlertFields` | AlertManagementPayloadAlertField! => Array | Extract alert fields from payload example for custom mapping. |
| `payloadAttributeMappings` | AlertManagementPayloadAlertMappingField! => Array | The custom mapping of GitLab alert attributes to fields from the payload_example. |
| `payloadExample` | JsonString | The example of an alert payload. |
| `token` | String | Token used to authenticate alert notification requests. |
| `type` | AlertManagementIntegrationType! | Type of integration. |
| `url` | String | Endpoint which accepts alert notifications. |
@ -288,6 +291,17 @@ Parsed field from an alert used for custom mappings.
| `path` | String! => Array | Path to value inside payload JSON. |
| `type` | AlertManagementPayloadAlertFieldType | Type of the parsed value. |
### AlertManagementPayloadAlertMappingField
Parsed field (with its name) from an alert used for custom mappings.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `fieldName` | AlertManagementPayloadAlertFieldName | A GitLab alert field name. |
| `label` | String | Human-readable label of the payload path. |
| `path` | String! => Array | Path to value inside payload JSON. |
| `type` | AlertManagementPayloadAlertFieldType | Type of the parsed value. |
### AlertManagementPrometheusIntegration
An endpoint and credentials used to accept Prometheus alerts for a project.
@ -3028,6 +3042,7 @@ Autogenerated return type of PipelineRetry.
| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project. |
| `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project. |
| `alertManagementAlerts` | AlertManagementAlertConnection | Alert Management alerts of the project. |
| `alertManagementHttpIntegrations` | AlertManagementHttpIntegrationConnection | HTTP Integrations which can receive alerts for the project. |
| `alertManagementIntegrations` | AlertManagementIntegrationConnection | Integrations which can receive alerts for the project. |
| `alertManagementPayloadFields` | AlertManagementPayloadAlertField! => Array | Extract alert fields from payload for custom mapping. |
| `allowMergeOnSkippedPipeline` | Boolean | If `only_allow_merge_if_pipeline_succeeds` is true, indicates if merge requests of the project can also be merged with skipped jobs. |

View File

@ -100,7 +100,7 @@ GET /projects/:id/templates/:type/:name
| Attribute | Type | Required | Description |
| ---------- | ------ | -------- | ----------- |
| `id` | integer / string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `type` | string | yes| The type `(dockerfiles|gitignores|gitlab_ci_ymls|licenses|issues|merge_requests)` of the template |
| `type` | string | yes| The type of the template. One of: `dockerfiles`, `gitignores`, `gitlab_ci_ymls`, `licenses`, `issues`, or `merge_requests`. |
| `name` | string | yes | The key of the template, as obtained from the collection endpoint |
| `source_template_project_id` | integer | no | The project ID where a given template is being stored. This is useful when multiple templates from different projects have the same name. If multiple templates have the same name, the match from `closest ancestor` is returned if `source_template_project_id` is not specified |
| `project` | string | no | The project name to use when expanding placeholders in the template. Only affects licenses |

View File

@ -10,8 +10,8 @@ We have implemented standard features that depend on configuration files in the
When implementing new features, please refer to these existing features to avoid conflicts:
- [Custom Dashboards](../operations/metrics/dashboards/index.md#add-a-new-dashboard-to-your-project): `.gitlab/dashboards/`.
- [Issue Templates](../user/project/description_templates.md#creating-issue-templates): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#creating-merge-request-templates): `.gitlab/merge_request_templates/`.
- [Issue Templates](../user/project/description_templates.md#create-an-issue-template): `.gitlab/issue_templates/`.
- [Merge Request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
- [GitLab Kubernetes Agents](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
- [CODEOWNERS](../user/project/code_owners.md#how-to-set-up-code-owners): `.gitlab/CODEOWNERS`.
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.

View File

@ -484,9 +484,9 @@ For GitLab.com, we're setting up a [QA and Testing environment](https://gitlab.c
### `gitlab_standard`
We are currently working towards including the [`gitlab_standard` schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/) with every event. See [Standardize Snowplow Schema](https://gitlab.com/groups/gitlab-org/-/epics/5218) for details.
We are including the [`gitlab_standard` schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_standard/jsonschema/) with every event. See [Standardize Snowplow Schema](https://gitlab.com/groups/gitlab-org/-/epics/5218) for details.
The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) class represents this schema in the application.
The [`StandardContext`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/tracking/standard_context.rb) class represents this schema in the application.
| Field Name | Required | Type | Description |
|----------------|---------------------|-----------------------|---------------------------------------------------------------------------------------------|

View File

@ -54,7 +54,7 @@ With Maintainer or higher [permissions](../../user/permissions.md), you can enab
1. Navigate to **Settings > Operations > Incidents** and expand **Incidents**.
1. Check the **Create an incident** checkbox.
1. To customize the incident, select an
[issue template](../../user/project/description_templates.md#creating-issue-templates).
[issue template](../../user/project/description_templates.md#create-an-issue-template).
1. To send [an email notification](paging.md#email-notifications) to users
with [Developer permissions](../../user/permissions.md), select
**Send a separate email notification to Developers**. Email notifications are

View File

@ -20,7 +20,7 @@ the tiers are no longer mentioned in GitLab documentation:
[per-group charts](../user/project/milestones/index.md#group-burndown-charts)
- [Code owners](../user/project/code_owners.md)
- Description templates:
- [Setting a default template for merge requests and issues](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues)
- [Setting a default template for merge requests and issues](../user/project/description_templates.md#set-a-default-template-for-merge-requests-and-issues)
- [Email from GitLab](../tools/email.md)
- Groups:
- [Creating group memberships via CN](../user/group/index.md#creating-group-links-via-cn)

View File

@ -37,10 +37,10 @@ To learn how to create templates for various file types in groups, visit
images guidelines, link to the related issue, reviewer name, and so on.
- You can also create issues and merge request templates for different
stages of your workflow, for example, feature proposal, feature improvement, or a bug report.
- You can use an [issue description template](#creating-issue-templates) as a
- You can use an [issue description template](#create-an-issue-template) as a
[Service Desk email template](service_desk.md#new-service-desk-issues).
## Creating issue templates
## Create an issue template
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
directory in your repository. Commit and push to your default branch.
@ -65,13 +65,13 @@ To create the `.gitlab/issue_templates` directory:
To check if this has worked correctly, [create a new issue](issues/managing_issues.md#create-a-new-issue)
and see if you can choose a description template.
## Creating merge request templates
## Create a merge request template
Similarly to issue templates, create a new Markdown (`.md`) file inside the
`.gitlab/merge_request_templates/` directory in your repository. Commit and
push to your default branch.
## Using the templates
## Use the templates
Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
This enables the `Bug` dropdown option when creating or editing issues. When
@ -85,9 +85,45 @@ For example: `https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_templat
![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(PREMIUM)**
### Set an issue and merge request description template at group level **(PREMIUM)**
> - Moved to GitLab Premium in 13.9.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled by default on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to
[enable it](#enable-or-disable-issue-and-merge-request-description-templates-at-group-and-instance-level).
Templates can be useful because you can create a template once and use it multiple times.
To re-use templates [you've created](../project/description_templates.md#create-an-issue-template):
1. Go to the group's **Settings > General > Templates**.
1. From the dropdown, select your template project as the template repository at group level.
![Group template settings](../group/img/group_file_template_settings.png)
### Set an issue and merge request description template at instance level **(PREMIUM ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52360) in GitLab 13.9.
> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
> - It's disabled by default on GitLab.com.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to
[enable it](#enable-or-disable-issue-and-merge-request-description-templates-at-group-and-instance-level).
Similar to group templates, issue and merge request templates can also be set up at the instance level.
This results in those templates being available in all projects within the instance.
Only instance administrators can set instance-level templates.
To set the instance-level description template repository:
1. Select the **Admin Area** icon (**{admin}**).
1. Go to **Settings > Templates**.
1. From the dropdown, select your template project as the template repository at instance level.
Learn more about [instance template repository](../admin_area/settings/instance_template_repository.md).
![Setting templates in the Admin Area](../admin_area/settings/img/file_template_admin_area.png)
### Set a default template for merge requests and issues **(PREMIUM)**
The visibility of issues or merge requests should be set to either "Everyone
with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the
@ -159,3 +195,28 @@ it's very hard to read otherwise.)
/cc @project-manager
/assign @qa-tester
```
## Enable or disable issue and merge request description templates at group and instance level
Setting issue and merge request description templates at group and instance levels
is under development and not ready for production use. It is deployed behind a
feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:inherited_issuable_templates)
```
To disable it:
```ruby
Feature.disable(:inherited_issuable_templates)
```
The feature flag affects these features:
- Setting a templates project as issue and merge request description templates source at group level.
- Setting a templates project as issue and merge request description templates source at instance level.

View File

@ -184,7 +184,7 @@ You can then see issue statuses in the [issue list](#issues-list) and the
## Other Issue actions
- [Create an issue from a template](../../project/description_templates.md#using-the-templates)
- [Create an issue from a template](../../project/description_templates.md#use-the-templates)
- [Set a due date](due_dates.md)
- [Bulk edit issues](../bulk_editing.md) - From the Issues List, select multiple issues
in order to change their status, assignee, milestone, or labels in bulk.

View File

@ -137,13 +137,13 @@ You can use these placeholders to be automatically replaced in each email:
#### New Service Desk issues
You can select one [issue description template](description_templates.md#creating-issue-templates)
You can select one [issue description template](description_templates.md#create-an-issue-template)
**per project** to be appended to every new Service Desk issue's description.
Issue description templates should reside in your repository's `.gitlab/issue_templates/` directory.
To use a custom issue template with Service Desk, in your project:
1. [Create a description template](description_templates.md#creating-issue-templates)
1. [Create a description template](description_templates.md#create-an-issue-template)
1. Go to **Settings > General > Service Desk**.
1. From the dropdown **Template to append to all Service Desk issues**, select your template.

View File

@ -102,7 +102,7 @@ To edit a file:
in the bottom-right corner.
1. When you're done, click **Submit changes...**.
1. (Optional) Adjust the default title and description of the merge request, to submit
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#creating-merge-request-templates)
with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template)
from the dropdown menu and edit it accordingly.
1. Click **Submit changes**.
1. A new merge request is automatically created and you can assign a colleague for review.

View File

@ -7,7 +7,7 @@ module Gitlab
module Samplers
class RubySampler < BaseSampler
DEFAULT_SAMPLING_INTERVAL_SECONDS = 60
GC_REPORT_BUCKETS = [0.005, 0.01, 0.02, 0.04, 0.07, 0.1, 0.5].freeze
GC_REPORT_BUCKETS = [0.01, 0.05, 0.1, 0.2, 0.3, 0.5, 1].freeze
def initialize(*)
GC::Profiler.clear

View File

@ -17,7 +17,7 @@ module Gitlab
# to a structured log
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def enqueue_stats_job(request_id)
return unless gather_stats?
return unless Feature.enabled?(:performance_bar_stats)
@client.sadd(GitlabPerformanceBarStatsWorker::STATS_KEY, request_id)
@ -43,12 +43,6 @@ module Gitlab
)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def gather_stats?
return unless Feature.enabled?(:performance_bar_stats)
Gitlab.com? || Gitlab.staging? || !Rails.env.production?
end
end
end
end

View File

@ -87,11 +87,11 @@ module Gitlab
raise NotImplementedError
end
def by_category(category, project = nil)
def by_category(category, project = nil, empty_category_title: nil)
directory = category_directory(category)
files = finder(project).list_files_for(directory)
files.map { |f| new(f, project, category: category) }.sort
files.map { |f| new(f, project, category: category.presence || empty_category_title) }.sort
end
def category_directory(category)

View File

@ -25,6 +25,10 @@ module Gitlab
# follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300279
project.repository.issue_template_names_by_category
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end

View File

@ -25,6 +25,10 @@ module Gitlab
# follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300279
project.repository.merge_request_template_names_by_category
end
def by_category(category, project = nil, empty_category_title: nil)
super(category, project, empty_category_title: _('Project Templates'))
end
end
end
end

View File

@ -3927,9 +3927,6 @@ msgstr ""
msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete \"%{name}\" Value Stream?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@ -8639,6 +8636,9 @@ msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream created"
msgstr ""
msgid "CreateValueStreamForm|'%{name}' Value Stream edited"
msgstr ""
msgid "CreateValueStreamForm|Add another stage"
msgstr ""
@ -8651,15 +8651,24 @@ msgstr ""
msgid "CreateValueStreamForm|Code stage start"
msgstr ""
msgid "CreateValueStreamForm|Create Value Stream"
msgstr ""
msgid "CreateValueStreamForm|Create from default template"
msgstr ""
msgid "CreateValueStreamForm|Create from no template"
msgstr ""
msgid "CreateValueStreamForm|Create new Value Stream"
msgstr ""
msgid "CreateValueStreamForm|Default stages"
msgstr ""
msgid "CreateValueStreamForm|Edit Value Stream"
msgstr ""
msgid "CreateValueStreamForm|Editing stage"
msgstr ""
@ -8708,6 +8717,9 @@ msgstr ""
msgid "CreateValueStreamForm|Restore stage"
msgstr ""
msgid "CreateValueStreamForm|Save Value Stream"
msgstr ""
msgid "CreateValueStreamForm|Select end event"
msgstr ""
@ -9711,6 +9723,15 @@ msgstr ""
msgid "DeleteProject|Failed to restore wiki repository. Please contact the administrator."
msgstr ""
msgid "DeleteValueStream|'%{name}' Value Stream deleted"
msgstr ""
msgid "DeleteValueStream|Are you sure you want to delete \"%{name}\" Value Stream?"
msgstr ""
msgid "DeleteValueStream|Delete %{name}"
msgstr ""
msgid "Deleted"
msgstr ""
@ -13234,6 +13255,9 @@ msgstr ""
msgid "Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo sites"
msgstr ""
msgid "GeoNodeStatusEvent|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
@ -13612,6 +13636,9 @@ msgstr ""
msgid "Getting started with releases"
msgstr ""
msgid "Git"
msgstr ""
msgid "Git LFS is not enabled on this GitLab server, contact your admin."
msgstr ""
@ -16849,6 +16876,9 @@ msgstr ""
msgid "Jira service not configured."
msgstr ""
msgid "Jira user"
msgstr ""
msgid "Jira users have been imported from the configured Jira instance. They can be mapped by selecting a GitLab user from the dropdown in the \"GitLab username\" column. When the form appears, the dropdown defaults to the user conducting the import."
msgstr ""
@ -20121,6 +20151,9 @@ msgstr ""
msgid "No application_settings found"
msgstr ""
msgid "No assignee"
msgstr ""
msgid "No authentication methods configured."
msgstr ""
@ -21668,6 +21701,9 @@ msgstr ""
msgid "PerformanceBar|SQL queries"
msgstr ""
msgid "PerformanceBar|Stats"
msgstr ""
msgid "PerformanceBar|trace"
msgstr ""
@ -21986,6 +22022,9 @@ msgstr ""
msgid "Pipeline|Canceled"
msgstr ""
msgid "Pipeline|Checking pipeline status"
msgstr ""
msgid "Pipeline|Checking pipeline status."
msgstr ""
@ -22037,6 +22076,9 @@ msgstr ""
msgid "Pipeline|Pipeline"
msgstr ""
msgid "Pipeline|Pipeline %{idStart}#%{idEnd} %{statusStart}%{statusEnd} for %{commitStart}%{commitEnd}"
msgstr ""
msgid "Pipeline|Pipelines"
msgstr ""
@ -22094,6 +22136,9 @@ msgstr ""
msgid "Pipeline|Variables"
msgstr ""
msgid "Pipeline|We are currently unable to fetch pipeline data"
msgstr ""
msgid "Pipeline|Youre about to stop pipeline %{pipelineId}."
msgstr ""
@ -23003,6 +23048,9 @@ msgstr ""
msgid "Project ID"
msgstr ""
msgid "Project Templates"
msgstr ""
msgid "Project URL"
msgstr ""
@ -29772,6 +29820,9 @@ msgstr ""
msgid "There was an error fetching the %{replicableType}"
msgstr ""
msgid "There was an error fetching the Geo Nodes"
msgstr ""
msgid "There was an error fetching the Geo Settings"
msgstr ""

View File

@ -160,13 +160,28 @@ RSpec.describe Projects::TemplatesController do
end
shared_examples 'template names request' do
it 'returns the template names' do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
context 'when feature flag enabled' do
it 'returns the template names', :aggregate_failures do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response.size).to eq(2)
expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['Project Templates'].size).to eq(2)
expect(json_response['Project Templates'].map { |x| x.slice('name') }).to match(expected_template_names)
end
end
context 'when feature flag disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns the template names', :aggregate_failures do
get(:names, params: { namespace_id: project.namespace, template_type: template_type, project_id: project }, format: :json)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response.map { |x| x.slice('name') }).to match(expected_template_names)
end
end
it 'fails for user with no access' do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Service Desk Setting', :js do
RSpec.describe 'Service Desk Setting', :js, :clean_gitlab_redis_cache do
let(:project) { create(:project_empty_repo, :private, service_desk_enabled: false) }
let(:presenter) { project.present(current_user: user) }
let(:user) { create(:user) }
@ -66,5 +66,48 @@ RSpec.describe 'Service Desk Setting', :js do
expect(find('[data-testid="incoming-email"]').value).to eq('address-suffix@example.com')
end
context 'issue description templates' do
let_it_be(:issuable_project_template_files) do
{
'.gitlab/issue_templates/project-issue-bar.md' => 'Project Issue Template Bar',
'.gitlab/issue_templates/project-issue-foo.md' => 'Project Issue Template Foo'
}
end
let_it_be(:issuable_group_template_files) do
{
'.gitlab/issue_templates/group-issue-bar.md' => 'Group Issue Template Bar',
'.gitlab/issue_templates/group-issue-foo.md' => 'Group Issue Template Foo'
}
end
let_it_be_with_reload(:group) { create(:group)}
let_it_be_with_reload(:project) { create(:project, :custom_repo, group: group, files: issuable_project_template_files) }
let_it_be(:group_template_repo) { create(:project, :custom_repo, group: group, files: issuable_group_template_files) }
before do
stub_licensed_features(custom_file_templates_for_namespace: false, custom_file_templates: false)
group.update_columns(file_template_project_id: group_template_repo.id)
end
context 'when inherited_issuable_templates enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
context 'when inherited_issuable_templates disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
visit edit_project_path(project)
end
it_behaves_like 'issue description templates from current project only'
end
end
end
end

View File

@ -49,6 +49,10 @@ RSpec.describe 'User can display performance bar', :js do
let(:group) { create(:group) }
before do
allow(GitlabPerformanceBarStatsWorker).to receive(:perform_in)
end
context 'when user is logged-out' do
before do
visit root_path
@ -97,6 +101,26 @@ RSpec.describe 'User can display performance bar', :js do
it_behaves_like 'performance bar is enabled by default in development'
it_behaves_like 'performance bar can be displayed'
it 'does not show Stats link by default' do
find('body').native.send_keys('pb')
expect(page).not_to have_link('Stats', visible: :all)
end
context 'when GITLAB_PERFORMANCE_BAR_STATS_URL environment variable is set' do
let(:stats_url) { 'https://log.gprd.gitlab.net/app/dashboards#/view/' }
before do
stub_env('GITLAB_PERFORMANCE_BAR_STATS_URL', stats_url)
end
it 'shows Stats link' do
find('body').native.send_keys('pb')
expect(page).to have_link('Stats', href: stats_url, visible: :all)
end
end
end
end
end

View File

@ -13,7 +13,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
let vm;
const props = {
endpoint: 'endpoint.json',
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
errorStateSvgPath: 'foo',
};

View File

@ -10,7 +10,6 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
cansetci="true"
class="mb-auto mt-auto"
emptystatesvgpath="http://test.host"
helppagepath="http://test.host"
/>
</div>
`;

View File

@ -19,7 +19,6 @@ describe('IDE pipelines list', () => {
let wrapper;
const defaultState = {
links: { ciHelpPagePath: TEST_HOST },
pipelinesEmptyStateSvgPath: TEST_HOST,
};
const defaultPipelinesState = {

View File

@ -35,7 +35,7 @@ exports[`Alert integration settings form default state should match the default
Incident template (optional)
<gl-link-stub
href="/help/user/project/description_templates#creating-issue-templates"
href="/help/user/project/description_templates#create-an-issue-template"
target="_blank"
>
<gl-icon-stub

View File

@ -422,7 +422,18 @@ describe('Issuable output', () => {
formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm');
});
it('shows the form if template names request is successful', () => {
it('shows the form if template names as hash request is successful', () => {
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
});
});
it('shows the form if template names as array request is successful', () => {
const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));

View File

@ -1,7 +1,50 @@
import Vue from 'vue';
import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
describe('Issue description template component', () => {
describe('Issue description template component with templates as hash', () => {
let vm;
let formState;
beforeEach(() => {
const Component = Vue.extend(descriptionTemplate);
formState = {
description: 'test',
};
vm = new Component({
propsData: {
formState,
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
},
projectId: 1,
projectPath: '/',
namespacePath: '/',
projectNamespace: '/',
},
}).$mount();
});
it('renders templates as JSON hash in data attribute', () => {
expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
'{"test":[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]}',
);
});
it('updates formState when changing template', () => {
vm.issuableTemplate.editor.setValue('test new template');
expect(formState.description).toBe('test new template');
});
it('returns formState description with editor getValue', () => {
formState.description = 'testing new template';
expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
});
});
describe('Issue description template component with templates as array', () => {
let vm;
let formState;
@ -28,16 +71,4 @@ describe('Issue description template component', () => {
'[{"name":"test","id":"test","project_path":"/","namespace_path":"/"}]',
);
});
it('updates formState when changing template', () => {
vm.issuableTemplate.editor.setValue('test new template');
expect(formState.description).toBe('test new template');
});
it('returns formState description with editor getValue', () => {
formState.description = 'testing new template';
expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
});
});

View File

@ -42,7 +42,7 @@ describe('Inline edit form component', () => {
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull();
});
it('renders template selector when templates exists', () => {
it('renders template selector when templates as array exists', () => {
createComponent({
issuableTemplates: [
{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' },
@ -52,6 +52,16 @@ describe('Inline edit form component', () => {
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});
it('renders template selector when templates as hash exists', () => {
createComponent({
issuableTemplates: {
test: [{ name: 'test', id: 'test', project_path: 'test', namespace_path: 'test' }],
},
});
expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
});
it('hides locked warning by default', () => {
createComponent();

View File

@ -9,6 +9,7 @@ describe('performance bar app', () => {
store,
env: 'development',
requestId: '123',
statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
peekUrl: '/-/peek/results',
profileUrl: '?lineprofiler=true',
},

View File

@ -19,6 +19,7 @@ describe('performance bar wrapper', () => {
peekWrapper.setAttribute('data-env', 'development');
peekWrapper.setAttribute('data-request-id', '123');
peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
mock = new MockAdapter(axios);

View File

@ -1,14 +1,24 @@
import { shallowMount } from '@vue/test-utils';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineStatus from '~/pipeline_editor/components/header/pipeline_status.vue';
import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
import { mockLintResponse } from '../../mock_data';
describe('Pipeline editor header', () => {
let wrapper;
const mockProvide = {
glFeatures: {
pipelineStatusForPipelineEditor: true,
},
};
const createComponent = () => {
const createComponent = ({ provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorHeader, {
provide: {
...mockProvide,
...provide,
},
props: {
ciConfigData: mockLintResponse,
isCiConfigDataLoading: false,
@ -16,6 +26,7 @@ describe('Pipeline editor header', () => {
});
};
const findPipelineStatus = () => wrapper.findComponent(PipelineStatus);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
afterEach(() => {
@ -27,8 +38,27 @@ describe('Pipeline editor header', () => {
beforeEach(() => {
createComponent();
});
it('renders the pipeline status', () => {
expect(findPipelineStatus().exists()).toBe(true);
});
it('renders the validation segment', () => {
expect(findValidationSegment().exists()).toBe(true);
});
});
describe('with pipeline status feature flag off', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { pipelineStatusForPipelineEditor: false },
},
});
});
it('does not render the pipeline status', () => {
expect(findPipelineStatus().exists()).toBe(false);
});
});
});

View File

@ -0,0 +1,150 @@
import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/pipeline_editor/components/header/pipeline_status.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
const mockProvide = {
projectFullPath: mockProjectFullPath,
};
describe('Pipeline Status', () => {
let wrapper;
let mockApollo;
let mockPipelineQuery;
const createComponent = ({ hasPipeline = true, isQueryLoading = false }) => {
const pipeline = hasPipeline
? { loading: isQueryLoading, ...mockProjectPipeline.pipeline }
: { loading: isQueryLoading };
wrapper = shallowMount(PipelineStatus, {
provide: mockProvide,
stubs: { GlLink, GlSprintf },
data: () => (hasPipeline ? { pipeline } : {}),
mocks: {
$apollo: {
queries: {
pipeline,
},
},
},
});
};
const createComponentWithApollo = () => {
const resolvers = {
Query: {
project: mockPipelineQuery,
},
};
mockApollo = createMockApollo([], resolvers);
wrapper = shallowMount(PipelineStatus, {
localVue,
apolloProvider: mockApollo,
provide: mockProvide,
stubs: { GlLink, GlSprintf },
data() {
return {
commitSha: mockCommitSha,
};
},
});
};
const findIcon = () => wrapper.findComponent(GlIcon);
const findCiIcon = () => wrapper.findComponent(CiIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineId = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineCommit = () => wrapper.find('[data-testid="pipeline-commit"]');
const findPipelineErrorMsg = () => wrapper.find('[data-testid="pipeline-error-msg"]');
const findPipelineLoadingMsg = () => wrapper.find('[data-testid="pipeline-loading-msg"]');
beforeEach(() => {
mockPipelineQuery = jest.fn();
});
afterEach(() => {
mockPipelineQuery.mockReset();
wrapper.destroy();
wrapper = null;
});
describe('while querying', () => {
it('renders loading icon', () => {
createComponent({ isQueryLoading: true, hasPipeline: false });
expect(findLoadingIcon().exists()).toBe(true);
expect(findPipelineLoadingMsg().text()).toBe(i18n.fetchLoading);
});
it('does not render loading icon if pipeline data is already set', () => {
createComponent({ isQueryLoading: true });
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('when querying data', () => {
describe('when data is set', () => {
beforeEach(async () => {
mockPipelineQuery.mockResolvedValue(mockProjectPipeline);
createComponentWithApollo();
await waitForPromises();
});
it('query is called with correct variables', async () => {
expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
expect(mockPipelineQuery).toHaveBeenCalledWith(
expect.anything(),
{
fullPath: mockProjectFullPath,
},
expect.anything(),
expect.anything(),
);
});
it('does not render error', () => {
expect(findIcon().exists()).toBe(false);
});
it('renders pipeline data', () => {
const { id } = mockProjectPipeline.pipeline;
expect(findCiIcon().exists()).toBe(true);
expect(findPipelineId().text()).toBe(`#${id.match(/\d+/g)[0]}`);
expect(findPipelineCommit().text()).toBe(mockCommitSha);
});
});
describe('when data cannot be fetched', () => {
beforeEach(async () => {
mockPipelineQuery.mockRejectedValue(new Error());
createComponentWithApollo();
await waitForPromises();
});
it('renders error', () => {
expect(findIcon().attributes('name')).toBe('warning-solid');
expect(findPipelineErrorMsg().text()).toBe(i18n.fetchError);
});
it('does not render pipeline data', () => {
expect(findCiIcon().exists()).toBe(false);
expect(findPipelineId().exists()).toBe(false);
expect(findPipelineCommit().exists()).toBe(false);
});
});
});
});

View File

@ -46,6 +46,24 @@ describe('~/pipeline_editor/graphql/resolvers', () => {
await expect(result.rawData).resolves.toBe(mockCiYml);
});
});
describe('pipeline', () => {
it('resolves pipeline data with type names', async () => {
const result = await resolvers.Query.project(null);
// eslint-disable-next-line no-underscore-dangle
expect(result.__typename).toBe('Project');
});
it('resolves pipeline data with necessary data', async () => {
const result = await resolvers.Query.project(null);
const pipelineKeys = Object.keys(result.pipeline);
const statusKeys = Object.keys(result.pipeline.detailedStatus);
expect(pipelineKeys).toContain('id', 'commitPath', 'detailedStatus', 'shortSha');
expect(statusKeys).toContain('detailsPath', 'text');
});
});
});
describe('Mutation', () => {

View File

@ -138,6 +138,22 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
export const mockProjectPipeline = {
pipeline: {
commitPath: '/-/commit/aabbccdd',
id: 'gid://gitlab/Ci::Pipeline/118',
iid: '28',
shortSha: mockCommitSha,
status: 'SUCCESS',
detailedStatus: {
detailsPath: '/root/sample-ci-project/-/pipelines/118"',
group: 'success',
icon: 'status_success',
text: 'passed',
},
},
};
export const mockLintResponse = {
valid: true,
mergedYaml: mockCiYml,

View File

@ -9,7 +9,6 @@ describe('Pipelines Empty State', () => {
const createWrapper = () => {
wrapper = shallowMount(EmptyState, {
propsData: {
helpPagePath: 'foo',
emptyStateSvgPath: 'foo',
canSetCi: true,
},
@ -35,7 +34,7 @@ describe('Pipelines Empty State', () => {
});
it('should render a link with provided help path', () => {
expect(findGetStartedButton().attributes('href')).toBe('foo');
expect(findGetStartedButton().attributes('href')).toBe('/help/ci/quick_start/index.md');
});
it('should render empty state information', () => {

View File

@ -34,7 +34,6 @@ describe('Pipelines', () => {
let origWindowLocation;
const paths = {
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
@ -44,7 +43,6 @@ describe('Pipelines', () => {
};
const noPermissions = {
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
@ -544,7 +542,9 @@ describe('Pipelines', () => {
'GitLab CI/CD can automatically build, test, and deploy your code.',
);
expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
expect(findEmptyState().find(GlButton).attributes('href')).toBe(paths.helpPagePath);
expect(findEmptyState().find(GlButton).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
});
it('does not render tabs nor buttons', () => {

View File

@ -1,5 +1,5 @@
import { setHTMLFixture } from 'helpers/fixtures';
import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
import Tracking, { initUserTracking, initDefaultTrackers, STANDARD_CONTEXT } from '~/tracking';
describe('Tracking', () => {
let snowplowSpy;
@ -45,7 +45,7 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
expect(snowplowSpy).toHaveBeenCalledWith('trackPageView');
expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', null, [STANDARD_CONTEXT]);
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@ -88,7 +88,7 @@ describe('Tracking', () => {
'_label_',
undefined,
undefined,
undefined,
[STANDARD_CONTEXT],
);
});

View File

@ -6,6 +6,19 @@ describe('User Popovers', () => {
preloadFixtures(fixtureTemplate);
const selector = '.js-user-link, .gfm-project_member';
const findFixtureLinks = () => {
return Array.from(document.querySelectorAll(selector)).filter(
({ dataset }) => dataset.user || dataset.userId,
);
};
const createUserLink = () => {
const link = document.createElement('a');
link.classList.add('js-user-link');
link.setAttribute('data-user', '1');
return link;
};
const dummyUser = { name: 'root' };
const dummyUserStatus = { message: 'active' };
@ -37,13 +50,20 @@ describe('User Popovers', () => {
});
it('initializes a popover for each user link with a user id', () => {
const linksWithUsers = Array.from(document.querySelectorAll(selector)).filter(
({ dataset }) => dataset.user || dataset.userId,
);
const linksWithUsers = findFixtureLinks();
expect(linksWithUsers.length).toBe(popovers.length);
});
it('adds popovers to user links added to the DOM tree after the initial call', async () => {
document.body.appendChild(createUserLink());
document.body.appendChild(createUserLink());
const linksWithUsers = findFixtureLinks();
expect(linksWithUsers.length).toBe(popovers.length + 2);
});
it('does not initialize the user popovers twice for the same element', () => {
const newPopovers = initUserPopovers(document.querySelectorAll(selector));
const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover);

View File

@ -4,7 +4,6 @@ export const IDE_DATASET = {
committedStateSvgPath: '/test/committed_state.svg',
pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
promotionSvgPath: '/test/promotion.svg',
ciHelpPagePath: '/test/ci_help_page',
webIDEHelpPagePath: '/test/web_ide_help_page',
clientsidePreviewEnabled: 'true',
renderWhitespaceInCode: 'false',

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do
include GraphqlHelpers
let_it_be(:guest) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:prometheus_integration) { create(:prometheus_service, project: project) }
let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) }
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_proj_integration) { create(:alert_management_http_integration) }
subject { sync(resolve_http_integrations) }
before do
project.add_developer(developer)
project.add_maintainer(maintainer)
end
specify do
expect(described_class).to have_nullable_graphql_type(Types::AlertManagement::HttpIntegrationType.connection_type)
end
context 'user does not have permission' do
let(:current_user) { guest }
it { is_expected.to be_empty }
end
context 'user has developer permission' do
let(:current_user) { developer }
it { is_expected.to be_empty }
end
context 'user has maintainer permission' do
let(:current_user) { maintainer }
it { is_expected.to contain_exactly(active_http_integration) }
end
private
def resolve_http_integrations(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, ctx: context)
end
end

View File

@ -13,22 +13,33 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
let_it_be(:group_member) { create(:group_member, :developer, group: parent_group, user: user) }
let_it_be(:project_member) { create(:project_member, :developer, user: user, project: project) }
it 'returns empty hash when template type does not exist' do
expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq([])
context 'when feature flag disabled' do
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns empty array when template type does not exist' do
expect(helper.issuable_templates(project, 'non-existent-template-type')).to eq([])
end
end
context 'when feature flag enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
end
it 'returns empty hash when template type does not exist' do
expect(helper.issuable_templates(build(:project), 'non-existent-template-type')).to eq({})
end
end
context 'with cached issuable templates' do
before do
allow(Gitlab::Template::IssueTemplate).to receive(:template_names).and_return({})
allow(Gitlab::Template::MergeRequestTemplate).to receive(:template_names).and_return({})
it 'does not call TemplateFinder' do
expect(Gitlab::Template::IssueTemplate).to receive(:template_names).once.and_call_original
expect(Gitlab::Template::MergeRequestTemplate).to receive(:template_names).once.and_call_original
helper.issuable_templates(project, 'issues')
helper.issuable_templates(project, 'merge_request')
end
it 'does not call TemplateFinder' do
expect(Gitlab::Template::IssueTemplate).not_to receive(:template_names)
expect(Gitlab::Template::MergeRequestTemplate).not_to receive(:template_names)
helper.issuable_templates(project, 'issues')
helper.issuable_templates(project, 'merge_request')
end
@ -63,29 +74,78 @@ RSpec.describe IssuablesDescriptionTemplatesHelper, :clean_gitlab_redis_cache do
end
describe '#issuable_templates_names' do
let(:project) { double(Project, id: 21) }
let_it_be(:project) { build(:project) }
let(:templates) do
[
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
]
end
it 'returns project templates only' do
before do
allow(helper).to receive(:ref_project).and_return(project)
allow(helper).to receive(:issuable_templates).and_return(templates)
end
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
context 'when feature flag disabled' do
let(:templates) do
[
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
]
end
before do
stub_feature_flags(inherited_issuable_templates: false)
end
it 'returns project templates only' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
end
end
context 'when feature flag enabled' do
before do
stub_feature_flags(inherited_issuable_templates: true)
end
context 'with matching project templates' do
let(:templates) do
{
"" => [
{ name: "another_issue_template", id: "another_issue_template", project_id: project.id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: project.id }
],
"Instance" => [
{ name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
{ name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
]
}
end
it 'returns project templates only' do
expect(helper.issuable_templates_names(Issue.new)).to eq(%w[another_issue_template custom_issue_template])
end
end
context 'without matching project templates' do
let(:templates) do
{
"Project Templates" => [
{ name: "another_issue_template", id: "another_issue_template", project_id: non_existing_record_id },
{ name: "custom_issue_template", id: "custom_issue_template", project_id: non_existing_record_id }
],
"Instance" => [
{ name: "first_issue_issue_template", id: "first_issue_issue_template", project_id: non_existing_record_id },
{ name: "second_instance_issue_template", id: "second_instance_issue_template", project_id: non_existing_record_id }
]
}
end
it 'returns empty array' do
expect(helper.issuable_templates_names(Issue.new)).to eq([])
end
end
end
context 'when there are not templates in the project' do
let(:templates) { {} }
it 'returns empty array' do
allow(helper).to receive(:ref_project).and_return(project)
allow(helper).to receive(:issuable_templates).and_return(templates)
expect(helper.issuable_templates_names(Issue.new)).to eq([])
end
end

View File

@ -4,11 +4,11 @@ require 'spec_helper'
RSpec.describe Issues::ExportCsvService do
let_it_be(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:bad_issue) { create(:issue, project: project, author: user) }
let(:subject) { described_class.new(Issue.all, project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:issue) { create(:issue, project: project, author: user) }
let_it_be(:bad_issue) { create(:issue, project: project, author: user) }
subject { described_class.new(Issue.all, project) }
it 'renders csv to string' do
expect(subject.csv_data).to be_a String
@ -33,11 +33,11 @@ RSpec.describe Issues::ExportCsvService do
end
context 'includes' do
let(:milestone) { create(:milestone, title: 'v1.0', project: project) }
let(:idea_label) { create(:label, project: project, title: 'Idea') }
let(:feature_label) { create(:label, project: project, title: 'Feature') }
let_it_be(:milestone) { create(:milestone, title: 'v1.0', project: project) }
let_it_be(:idea_label) { create(:label, project: project, title: 'Idea') }
let_it_be(:feature_label) { create(:label, project: project, title: 'Feature') }
before do
before_all do
# Creating a timelog touches the updated_at timestamp of issue,
# so create these first.
issue.timelogs.create!(time_spent: 360, user: user)
@ -60,6 +60,10 @@ RSpec.describe Issues::ExportCsvService do
expect(csv.headers).to include('Title', 'Description')
end
it 'returns two issues' do
expect(csv.count).to eq(2)
end
specify 'iid' do
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
@ -150,7 +154,7 @@ RSpec.describe Issues::ExportCsvService do
end
context 'with issues filtered by labels and project' do
let(:subject) do
subject do
described_class.new(
IssuesFinder.new(user,
project_id: project.id,
@ -162,6 +166,27 @@ RSpec.describe Issues::ExportCsvService do
expect(csv[0]['Issue ID']).to eq issue.iid.to_s
end
end
context 'with label links' do
let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }
it 'does not run a query for each label link' do
control_count = ActiveRecord::QueryRecorder.new { csv }.count
labeled_issues
expect { csv }.not_to exceed_query_limit(control_count)
expect(csv.count).to eq(4)
end
it 'returns the labels in sorted order' do
labeled_issues
labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
expect(labeled_rows.count).to eq(2)
expect(labeled_rows.map { |entry| entry['Labels'] }).to all( eq("Feature,Idea") )
end
end
end
context 'with minimal details' do

View File

@ -349,27 +349,38 @@ RSpec.describe Projects::CreateService, '#execute' do
context 'default visibility level' do
let(:group) { create(:group, :private) }
before do
stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
group.add_developer(user)
using RSpec::Parameterized::TableSyntax
opts.merge!(
visibility: 'private',
name: 'test',
namespace: group,
path: 'foo'
)
where(:case_name, :group_level, :project_level) do
[
['in public group', Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL],
['in internal group', Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::INTERNAL],
['in private group', Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::PRIVATE]
]
end
it 'creates a private project' do
project = create_project(user, opts)
with_them do
before do
stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
group.add_developer(user)
group.update!(visibility_level: group_level)
expect(project).to respond_to(:errors)
opts.merge!(
name: 'test',
namespace: group,
path: 'foo'
)
end
expect(project.errors.any?).to be(false)
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
expect(project.saved?).to be(true)
expect(project.valid?).to be(true)
it 'creates project with correct visibility level', :aggregate_failures do
project = create_project(user, opts)
expect(project).to respond_to(:errors)
expect(project.errors).to be_blank
expect(project.visibility_level).to eq(project_level)
expect(project).to be_saved
expect(project).to be_valid
end
end
end

View File

@ -23,11 +23,11 @@ RSpec.shared_examples 'project issuable templates' do
end
it 'returns only md files as issue templates' do
expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project))
expect(helper.issuable_templates(project, 'issue')).to eq(expected_templates('issue'))
end
it 'returns only md files as merge_request templates' do
expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project))
expect(helper.issuable_templates(project, 'merge_request')).to eq(expected_templates('merge_request'))
end
end

View File

@ -7,7 +7,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:webhook_url) { 'https://example.gitlab.com' }
def execute_with_options(options)
receive(:new).with(webhook_url, options.merge(http_client: SlackService::Notifier::HTTPClient))
receive(:new).with(webhook_url, options.merge(http_client: SlackMattermost::Notifier::HTTPClient))
.and_return(double(:slack_service).as_null_object)
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
RSpec.shared_examples 'issue description templates from current project only' do
it 'loads issue description templates from the project only' do
within('#service-desk-template-select') do
expect(page).to have_content('project-issue-bar')
expect(page).to have_content('project-issue-foo')
expect(page).not_to have_content('group-issue-bar')
expect(page).not_to have_content('group-issue-foo')
end
end
end