Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-30 09:10:21 +00:00
parent f8b2dfce12
commit ea0085de54
87 changed files with 605 additions and 721 deletions

View file

@ -44,7 +44,7 @@ docs-lint markdown:
- .default-retry - .default-retry
- .docs:rules:docs-lint - .docs:rules:docs-lint
# When updating the image version here, update it in /scripts/lint-doc.sh too. # When updating the image version here, update it in /scripts/lint-doc.sh too.
image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.12-vale-2.8.0-markdownlint-0.26.0" image: registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.13-vale-2.10.2-markdownlint-0.26.0
stage: test stage: test
needs: [] needs: []
script: script:
@ -54,7 +54,7 @@ docs-lint links:
extends: extends:
- .default-retry - .default-retry
- .docs:rules:docs-lint - .docs:rules:docs-lint
image: "registry.gitlab.com/gitlab-org/gitlab-docs/lint-html:alpine-3.12-ruby-2.7.2" image: registry.gitlab.com/gitlab-org/gitlab-docs/lint-html:alpine-3.13-ruby-2.7.2
stage: test stage: test
needs: [] needs: []
script: script:

View file

@ -1366,7 +1366,6 @@ RSpec/AnyInstanceOf:
- 'spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb' - 'spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb'
- 'spec/views/layouts/_head.html.haml_spec.rb' - 'spec/views/layouts/_head.html.haml_spec.rb'
- 'spec/views/projects/artifacts/_artifact.html.haml_spec.rb' - 'spec/views/projects/artifacts/_artifact.html.haml_spec.rb'
- 'spec/views/shared/runners/show.html.haml_spec.rb'
- 'spec/workers/archive_trace_worker_spec.rb' - 'spec/workers/archive_trace_worker_spec.rb'
- 'spec/workers/build_coverage_worker_spec.rb' - 'spec/workers/build_coverage_worker_spec.rb'
- 'spec/workers/build_hooks_worker_spec.rb' - 'spec/workers/build_hooks_worker_spec.rb'

View file

@ -600,7 +600,6 @@ Rails/ShortI18n:
Exclude: Exclude:
- 'app/models/project_services/chat_message/pipeline_message.rb' - 'app/models/project_services/chat_message/pipeline_message.rb'
- 'app/uploaders/content_type_whitelist.rb' - 'app/uploaders/content_type_whitelist.rb'
- 'spec/views/shared/runners/show.html.haml_spec.rb'
# Offense count: 1144 # Offense count: 1144
# Configuration parameters: ForbiddenMethods, AllowedMethods. # Configuration parameters: ForbiddenMethods, AllowedMethods.

View file

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 13.11.3 (2021-04-30)
### Fixed (1 change)
- Fix Instance-level Project Integration Management page for GitLab FOSS. !60354
## 13.11.2 (2021-04-27) ## 13.11.2 (2021-04-27)
### Security (5 changes) ### Security (5 changes)

View file

@ -1 +1 @@
8128ec05cf75d8af4f0b4e422106cef4adf9b3a4 46f08adbf4930f6a9c56f37ef5e4106c5b50810f

View file

@ -1,5 +1,5 @@
<script> <script>
import { GlSprintf, GlButton, GlAlert } from '@gitlab/ui'; import { GlSprintf, GlButton, GlAlert, GlCard } from '@gitlab/ui';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
@ -34,7 +34,7 @@ export default {
recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME, recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME,
i18n, i18n,
mousetrap: null, mousetrap: null,
components: { GlSprintf, GlButton, GlAlert, ClipboardButton }, components: { GlSprintf, GlButton, GlAlert, ClipboardButton, GlCard },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
props: { props: {
codes: { codes: {
@ -116,8 +116,8 @@ export default {
</gl-sprintf> </gl-sprintf>
</p> </p>
<div <gl-card
class="codes-to-print gl-my-5 gl-p-5 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base" class="codes-to-print gl-my-5"
data-testid="recovery-codes" data-testid="recovery-codes"
data-qa-selector="codes_content" data-qa-selector="codes_content"
> >
@ -126,7 +126,7 @@ export default {
<span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span> <span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span>
</li> </li>
</ul> </ul>
</div> </gl-card>
<div class="gl-my-n2 gl-mx-n2 gl-display-flex gl-flex-wrap"> <div class="gl-my-n2 gl-mx-n2 gl-display-flex gl-flex-wrap">
<div class="gl-p-2"> <div class="gl-p-2">
<clipboard-button <clipboard-button

View file

@ -29,7 +29,7 @@ export default {
const chartsToShow = ['pipelines']; const chartsToShow = ['pipelines'];
if (this.shouldRenderDoraCharts) { if (this.shouldRenderDoraCharts) {
chartsToShow.push('deployments', 'lead-time'); chartsToShow.push('deployment-frequency', 'lead-time');
} }
return chartsToShow; return chartsToShow;
@ -62,10 +62,10 @@ export default {
<pipeline-charts /> <pipeline-charts />
</gl-tab> </gl-tab>
<template v-if="shouldRenderDoraCharts"> <template v-if="shouldRenderDoraCharts">
<gl-tab :title="__('Deployments')"> <gl-tab :title="__('Deployment frequency')">
<deployment-frequency-charts /> <deployment-frequency-charts />
</gl-tab> </gl-tab>
<gl-tab :title="__('Lead Time')"> <gl-tab :title="__('Lead time')">
<lead-time-charts /> <lead-time-charts />
</gl-tab> </gl-tab>
</template> </template>

View file

@ -74,6 +74,21 @@ export default {
// we need to show the "create from" input. // we need to show the "create from" input.
this.showCreateFrom = true; this.showCreateFrom = true;
}, },
shouldShowCreateTagOption(isLoading, matches, query) {
// Show the "create tag" option if:
return (
// we're not currently loading any results, and
!isLoading &&
// the search query isn't just whitespace, and
query.trim() &&
// the `matches` object is non-null, and
matches &&
// the tag name doesn't already exist
!matches.tags.list.some(
(tagInfo) => tagInfo.name.toUpperCase() === query.toUpperCase().trim(),
)
);
},
}, },
translations: { translations: {
tagName: { tagName: {
@ -111,7 +126,7 @@ export default {
> >
<template #footer="{ isLoading, matches, query }"> <template #footer="{ isLoading, matches, query }">
<gl-dropdown-item <gl-dropdown-item
v-if="!isLoading && matches && matches.tags.totalCount === 0" v-if="shouldShowCreateTagOption(isLoading, matches, query)"
is-check-item is-check-item
:is-checked="tagName === query" :is-checked="tagName === query"
@click="createTagClicked(query)" @click="createTagClicked(query)"

View file

@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import { componentNames } from '~/reports/components/issue_body'; import { componentNames } from '~/reports/components/issue_body';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createStore from './store'; import createStore from './store';
export default { export default {
@ -12,26 +11,12 @@ export default {
components: { components: {
ReportSection, ReportSection,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
headPath: {
type: String,
required: true,
},
headBlobPath: {
type: String,
required: true,
},
basePath: { basePath: {
type: String, type: String,
required: false, required: false,
default: null, default: null,
}, },
baseBlobPath: {
type: String,
required: false,
default: null,
},
codequalityReportsPath: { codequalityReportsPath: {
type: String, type: String,
required: false, required: false,
@ -55,9 +40,6 @@ export default {
created() { created() {
this.setPaths({ this.setPaths({
basePath: this.basePath, basePath: this.basePath,
headPath: this.headPath,
baseBlobPath: this.baseBlobPath,
headBlobPath: this.headBlobPath,
reportsPath: this.codequalityReportsPath, reportsPath: this.codequalityReportsPath,
helpPath: this.codequalityHelpPath, helpPath: this.codequalityHelpPath,
}); });

View file

@ -1,34 +1,23 @@
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { parseCodeclimateMetrics, doCodeClimateComparison } from './utils/codequality_comparison'; import { parseCodeclimateMetrics } from './utils/codequality_parser';
export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths); export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
export const fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => { export const fetchReports = ({ state, dispatch, commit }) => {
commit(types.REQUEST_REPORTS); commit(types.REQUEST_REPORTS);
if (diffFeatureFlagEnabled) {
return axios
.get(state.reportsPath)
.then(({ data }) => {
return dispatch('receiveReportsSuccess', {
newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
});
})
.catch((error) => dispatch('receiveReportsError', error));
}
if (!state.basePath) { if (!state.basePath) {
return dispatch('receiveReportsError'); return dispatch('receiveReportsError');
} }
return Promise.all([axios.get(state.headPath), axios.get(state.basePath)]) return axios
.then((results) => .get(state.reportsPath)
doCodeClimateComparison( .then(({ data }) => {
parseCodeclimateMetrics(results[0].data, state.headBlobPath), return dispatch('receiveReportsSuccess', {
parseCodeclimateMetrics(results[1].data, state.baseBlobPath), newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
), resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
) });
.then((data) => dispatch('receiveReportsSuccess', data)) })
.catch((error) => dispatch('receiveReportsError', error)); .catch((error) => dispatch('receiveReportsError', error));
}; };

View file

@ -3,9 +3,6 @@ import * as types from './mutation_types';
export default { export default {
[types.SET_PATHS](state, paths) { [types.SET_PATHS](state, paths) {
state.basePath = paths.basePath; state.basePath = paths.basePath;
state.headPath = paths.headPath;
state.baseBlobPath = paths.baseBlobPath;
state.headBlobPath = paths.headBlobPath;
state.reportsPath = paths.reportsPath; state.reportsPath = paths.reportsPath;
state.helpPath = paths.helpPath; state.helpPath = paths.helpPath;
}, },

View file

@ -1,5 +1,3 @@
import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
export const parseCodeclimateMetrics = (issues = [], path = '') => { export const parseCodeclimateMetrics = (issues = [], path = '') => {
return issues.map((issue) => { return issues.map((issue) => {
const parsedIssue = { const parsedIssue = {
@ -27,17 +25,3 @@ export const parseCodeclimateMetrics = (issues = [], path = '') => {
return parsedIssue; return parsedIssue;
}); });
}; };
export const doCodeClimateComparison = (headIssues, baseIssues) => {
// Do these comparisons in worker threads to avoid blocking the main thread
return new Promise((resolve, reject) => {
const worker = new CodeQualityComparisonWorker();
worker.addEventListener('message', ({ data }) =>
data.newIssues && data.resolvedIssues ? resolve(data) : reject(data),
);
worker.postMessage({
headIssues,
baseIssues,
});
});
};

View file

@ -1,28 +0,0 @@
import { differenceBy } from 'lodash';
const KEY_TO_FILTER_BY = 'fingerprint';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', (e) => {
const { data } = e;
if (data === undefined) {
return null;
}
const { headIssues, baseIssues } = data;
if (!headIssues || !baseIssues) {
// eslint-disable-next-line no-restricted-globals
return self.postMessage({});
}
// eslint-disable-next-line no-restricted-globals
self.postMessage({
newIssues: differenceBy(headIssues, baseIssues, KEY_TO_FILTER_BY),
resolvedIssues: differenceBy(baseIssues, headIssues, KEY_TO_FILTER_BY),
});
// eslint-disable-next-line no-restricted-globals
return self.close();
});

View file

@ -460,9 +460,6 @@ export default {
<grouped-codequality-reports-app <grouped-codequality-reports-app
v-if="shouldRenderCodeQuality" v-if="shouldRenderCodeQuality"
:base-path="mr.codeclimate.base_path" :base-path="mr.codeclimate.base_path"
:head-path="mr.codeclimate.head_path"
:head-blob-path="mr.headBlobPath"
:base-blob-path="mr.baseBlobPath"
:codequality-reports-path="mr.codequalityReportsPath" :codequality-reports-path="mr.codequalityReportsPath"
:codequality-help-path="mr.codequalityHelpPath" :codequality-help-path="mr.codequalityHelpPath"
/> />

View file

@ -1,5 +1,5 @@
<script> <script>
import { mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
@ -18,6 +18,7 @@ export default {
}, },
computed: { computed: {
...mapState(['showDropdownContentsCreateView']), ...mapState(['showDropdownContentsCreateView']),
...mapGetters(['isDropdownVariantSidebar']),
dropdownContentsView() { dropdownContentsView() {
if (this.showDropdownContentsCreateView) { if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view'; return 'dropdown-contents-create-view';
@ -25,11 +26,8 @@ export default {
return 'dropdown-contents-labels-view'; return 'dropdown-contents-labels-view';
}, },
directionStyle() { directionStyle() {
if (this.renderOnTop) { const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
return { bottom: '100%' }; return this.renderOnTop ? { bottom } : {};
}
return {};
}, },
}, },
}; };
@ -37,7 +35,7 @@ export default {
<template> <template>
<div <div
class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute" class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
:style="directionStyle" :style="directionStyle"
> >

View file

@ -83,12 +83,13 @@ export default {
const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused'); const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused');
if (highlightedLabel) { if (highlightedLabel) {
const rect = highlightedLabel.getBoundingClientRect(); const container = this.$refs.labelsListContainer.getBoundingClientRect();
if (rect.bottom > this.$refs.labelsListContainer.clientHeight) { const label = highlightedLabel.getBoundingClientRect();
highlightedLabel.scrollIntoView(false);
} if (label.bottom > container.bottom) {
if (rect.top < 0) { this.$refs.labelsListContainer.scrollTop += label.bottom - container.bottom;
highlightedLabel.scrollIntoView(); } else if (label.top < container.top) {
this.$refs.labelsListContainer.scrollTop -= container.top - label.top;
} }
} }
}, },

View file

@ -22,7 +22,7 @@ export default {
const { label, highlight, isLabelSet } = props; const { label, highlight, isLabelSet } = props;
const labelColorBox = h('span', { const labelColorBox = h('span', {
class: 'dropdown-label-box', class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
style: { style: {
backgroundColor: label.color, backgroundColor: label.color,
}, },
@ -33,7 +33,7 @@ export default {
const checkedIcon = h(GlIcon, { const checkedIcon = h(GlIcon, {
class: { class: {
'mr-2 align-self-center': true, 'gl-mr-3 gl-flex-shrink-0': true,
hidden: !isLabelSet, hidden: !isLabelSet,
}, },
props: { props: {
@ -43,7 +43,7 @@ export default {
const noIcon = h('span', { const noIcon = h('span', {
class: { class: {
'mr-3 pr-2': true, 'gl-mr-5 gl-pr-3': true,
hidden: isLabelSet, hidden: isLabelSet,
}, },
attrs: { attrs: {
@ -56,7 +56,7 @@ export default {
const labelLink = h( const labelLink = h(
GlLink, GlLink,
{ {
class: 'd-flex align-items-baseline text-break-word label-item', class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal',
on: { on: {
click: () => { click: () => {
listeners.clickLabel(label); listeners.clickLabel(label);
@ -70,8 +70,8 @@ export default {
'li', 'li',
{ {
class: { class: {
'd-block': true, 'gl-display-block': true,
'text-left': true, 'gl-text-left': true,
'is-focused': highlight, 'is-focused': highlight,
}, },
}, },

View file

@ -268,7 +268,7 @@ export default {
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
}, },
setContentIsOnViewport(showDropdownContents) { setContentIsOnViewport(showDropdownContents) {
if (!this.isDropdownVariantEmbedded || !showDropdownContents) { if (!showDropdownContents) {
this.contentIsOnViewport = true; this.contentIsOnViewport = true;
return; return;
@ -276,8 +276,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.dropdownContents) { if (this.$refs.dropdownContents) {
const offset = { top: 100 }; this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el, offset);
} }
}); });
}, },
@ -313,6 +312,7 @@ export default {
<dropdown-contents <dropdown-contents
v-show="dropdownButtonVisible && showDropdownContents" v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents" ref="dropdownContents"
:render-on-top="!contentIsOnViewport"
/> />
</template> </template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded"> <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">

View file

@ -37,10 +37,6 @@
.file-title { .file-title {
@include gl-font-monospace; @include gl-font-monospace;
line-height: 35px;
padding-top: 7px;
padding-bottom: 7px;
display: flex;
} }
.editor-ref { .editor-ref {
@ -69,19 +65,15 @@
} }
.file-buttons { .file-buttons {
display: flex;
flex: 1; flex: 1;
justify-content: flex-end;
} }
.soft-wrap-toggle { .soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular-font; font-family: $regular-font;
margin: 0 $btn-side-margin; margin-left: $gl-padding-8;
.soft-wrap { .soft-wrap {
display: block; display: inline-flex;
} }
.no-wrap { .no-wrap {
@ -94,7 +86,7 @@
} }
.no-wrap { .no-wrap {
display: block; display: inline-flex;
} }
} }
} }
@ -111,17 +103,21 @@
.new-file-path { .new-file-path {
max-width: none; max-width: none;
width: 100%; width: 100%;
margin-bottom: 3px; margin-top: $gl-padding-8;
} }
.file-buttons { .file-buttons {
display: block; display: flex;
flex-direction: column;
width: 100%; width: 100%;
margin-bottom: 10px;
.md-header-toolbar {
margin: $gl-padding 0;
}
.soft-wrap-toggle { .soft-wrap-toggle {
width: 100%; width: 100%;
margin: 3px 0; margin-left: 0;
} }
@media(max-width: map-get($grid-breakpoints, md)-1) { @media(max-width: map-get($grid-breakpoints, md)-1) {

View file

@ -35,6 +35,6 @@ module RequiresWhitelistedMonitoringClient
end end
def render_404 def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404' render "errors/not_found", layout: "errors", status: :not_found
end end
end end

View file

@ -10,7 +10,6 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :continuous_integration feature_category :continuous_integration
def show def show
render 'shared/runners/show'
end end
def edit def edit

View file

@ -32,6 +32,7 @@ class Projects::HooksController < Projects::ApplicationController
end end
def edit def edit
redirect_to(action: :index) unless hook
end end
def update def update

View file

@ -108,10 +108,10 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new( params[:issue] ||= ActionController::Parameters.new(
assignee_ids: "" assignee_ids: ""
) )
build_params = issue_create_params.merge( build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve], discussion_to_resolve: params[:discussion_to_resolve],
confidential: !!Gitlab::Utils.to_boolean(issue_create_params[:confidential]) confidential: !!Gitlab::Utils.to_boolean(issue_params[:confidential])
) )
service = ::Issues::BuildService.new(project, current_user, build_params) service = ::Issues::BuildService.new(project, current_user, build_params)
@ -128,7 +128,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create def create
create_params = issue_create_params.merge(spammable_params).merge( create_params = issue_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve] discussion_to_resolve: params[:discussion_to_resolve]
) )
@ -314,17 +314,8 @@ class Projects::IssuesController < Projects::ApplicationController
task_num task_num
lock_version lock_version
discussion_locked discussion_locked
] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
end
def issue_create_params
create_params = %i[
issue_type issue_type
] ] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
params.require(:issue).permit(
*create_params
).merge(issue_params)
end end
def reorder_params def reorder_params

View file

@ -48,7 +48,6 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def show def show
render 'shared/runners/show'
end end
def toggle_shared_runners def toggle_shared_runners

View file

@ -26,6 +26,8 @@ module GroupsHelper
applications#show applications#show
applications#edit applications#edit
packages_and_registries#index packages_and_registries#index
groups/runners#show
groups/runners#edit
] ]
end end

View file

@ -723,6 +723,8 @@ module ProjectsHelper
badges#index badges#index
pages#show pages#show
packages_and_registries#index packages_and_registries#index
projects/runners#show
projects/runners#edit
] ]
end end

View file

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebexTeamsService < ChatNotificationService class WebexTeamsService < ChatNotificationService
include ActionView::Helpers::UrlHelper
def title def title
'Webex Teams' s_("WebexTeamsService|Webex Teams")
end end
def description def description
'Receive event notifications in Webex Teams' s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end end
def self.to_param def self.to_param
@ -14,13 +16,8 @@ class WebexTeamsService < ChatNotificationService
end end
def help def help
'This service sends notifications about projects events to a Webex Teams conversation.<br /> docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
To set up this service: s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
<ol>
<li><a href="https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications.</li>
</ol>'
end end
def event_field(event) def event_field(event)
@ -36,7 +33,7 @@ class WebexTeamsService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: "e.g. https://api.ciscospark.com/v1/webhooks/incoming/…", required: true }, { type: 'text', name: 'webhook', placeholder: "https://api.ciscospark.com/v1/webhooks/incoming/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices } { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
] ]

View file

@ -24,10 +24,11 @@ module Issues
def filter_params(issue) def filter_params(issue)
super super
# filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filtr_params` # filter confidential in `Issues::UpdateService` and not in `IssuableBaseService#filter_params`
# because we do allow users that cannot admin issues to set confidential flag when creating an issue # because we do allow users that cannot admin issues to set confidential flag when creating an issue
unless can_admin_issuable?(issue) unless can_admin_issuable?(issue)
params.delete(:confidential) params.delete(:confidential)
params.delete(:issue_type)
end end
end end

View file

@ -44,7 +44,7 @@
trigger: "focus", trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe }, content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} } } }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700') = sprite_icon('question-o', size: 16, css_class: 'gl-text-blue-600')
.gl-mt-3.text-uppercase .gl-mt-3.text-uppercase
= s_('AdminArea|Users') = s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2") = link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")

View file

@ -23,8 +23,8 @@
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token) %code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
= render_if_exists 'admin/health_check/health_check_url' = render_if_exists 'admin/health_check/health_check_url'
%hr %hr
.card .gl-card
.card-header .gl-card-header
Current Status: Current Status:
- if no_errors - if no_errors
= sprite_icon('check', css_class: 'cgreen') = sprite_icon('check', css_class: 'cgreen')
@ -32,7 +32,7 @@
- else - else
= sprite_icon('warning-solid', css_class: 'cred') = sprite_icon('warning-solid', css_class: 'cred')
#{ s_('HealthCheck|Unhealthy') } #{ s_('HealthCheck|Unhealthy') }
.card-body .gl-card-body
- if no_errors - if no_errors
#{ s_('HealthCheck|No Health Problems Detected') } #{ s_('HealthCheck|No Health Problems Detected') }
- else - else

View file

@ -4,7 +4,7 @@
= _('Recent Deliveries') = _('Recent Deliveries')
%p= _('When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.') %p= _('When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.')
.col-lg-9 .col-lg-9
- if hook_logs.any? - if hook_logs.present?
%table.table %table.table
%thead %thead
%tr %tr

View file

@ -1,8 +1,8 @@
- add_page_specific_style 'page_bundles/ci_status' - add_page_specific_style 'page_bundles/ci_status'
- page_title @runner.short_sha - breadcrumb_title @runner.short_sha
- page_title "##{@runner.id} (#{@runner.short_sha})"
- add_to_breadcrumbs _('Runners'), admin_runners_path - add_to_breadcrumbs _('Runners'), admin_runners_path
- breadcrumb_title page_title
- if Feature.enabled?(:runner_detailed_view_vue_ui, current_user, default_enabled: :yaml) - if Feature.enabled?(:runner_detailed_view_vue_ui, current_user, default_enabled: :yaml)
#js-runner-detail{ data: {runner_id: @runner.id} } #js-runner-detail{ data: {runner_id: @runner.id} }

View file

@ -1,4 +1,7 @@
- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners') - breadcrumb_title _('Edit')
- page_title _('Edit'), "##{@runner.id} (#{@runner.short_sha})"
- add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
- add_to_breadcrumbs "#{@runner.short_sha}", group_runner_path(@group, @runner)
%h2.page-title %h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id }) = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })

View file

@ -0,0 +1,3 @@
- add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
= render 'shared/runners/runner_details', runner: @runner

View file

@ -92,7 +92,7 @@
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do = link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do
%span.gl-sr-only %span.gl-sr-only
= s_('Nav|Help') = s_('Nav|Help')
= sprite_icon('question') = sprite_icon('question-o')
%span.notification-dot.rounded-circle.gl-absolute %span.notification-dot.rounded-circle.gl-absolute
= sprite_icon('chevron-down', css_class: 'caret-down') = sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right .dropdown-menu.dropdown-menu-right

View file

@ -170,7 +170,7 @@
%span %span
= _('Repository') = _('Repository')
= nav_link(controller: :ci_cd) do = nav_link(controller: [:ci_cd, 'groups/runners']) do
= link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do = link_to group_settings_ci_cd_path(@group), title: _('CI/CD') do
%span %span
= _('CI/CD') = _('CI/CD')

View file

@ -193,7 +193,7 @@
%span %span
= _('Repository') = _('Repository')
- if !@project.archived? && @project.feature_available?(:builds, current_user) - if !@project.archived? && @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do = nav_link(controller: [:ci_cd, 'projects/runners']) do
= link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do = link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
%span %span
= _('CI/CD') = _('CI/CD')

View file

@ -1,7 +1,7 @@
.form-actions .form-actions.gl-display-flex
= button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button' = button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
= link_to 'Cancel', cancel_path, = link_to 'Cancel', cancel_path,
class: 'gl-button btn btn-default btn-cancel', data: {confirm: leave_edit_message} class: 'gl-button btn btn-default gl-ml-3', data: {confirm: leave_edit_message}
= render 'shared/projects/edit_information' = render 'shared/projects/edit_information'

View file

@ -3,7 +3,7 @@
- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name) - is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.gl-mb-3 .file-holder-bottom-radius.file-holder.file.gl-mb-3
.js-file-title.file-title.align-items-center.clearfix{ data: { current_action: action } } .js-file-title.file-title.gl-display-flex.gl-align-items-center.clearfix{ data: { current_action: action } }
.editor-ref.block-truncated.has-tooltip{ title: ref } .editor-ref.block-truncated.has-tooltip{ title: ref }
= sprite_icon('fork', size: 12) = sprite_icon('fork', size: 12)
= ref = ref
@ -26,16 +26,18 @@
dismiss_key: @project.id, dismiss_key: @project.id,
human_access: human_access } } human_access: human_access } }
.file-buttons .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- if is_markdown - if is_markdown
= render 'shared/blob/markdown_buttons', show_fullscreen_button: false = render 'shared/blob/markdown_buttons', show_fullscreen_button: false
= button_tag class: 'soft-wrap-toggle btn gl-button', type: 'button', tabindex: '-1' do = button_tag class: 'soft-wrap-toggle btn gl-button btn-default', type: 'button', tabindex: '-1' do
%span.no-wrap .no-wrap
= custom_icon('icon_no_wrap') = sprite_icon('soft-unwrap', css_class: 'gl-button-icon')
No wrap %span.gl-button-text
%span.soft-wrap No wrap
= custom_icon('icon_soft_wrap') .soft-wrap
Soft wrap = sprite_icon('soft-wrap', css_class: 'gl-button-icon')
%span.gl-button-text
Soft wrap
.file-editor.code .file-editor.code
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }< .js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<

View file

@ -4,7 +4,7 @@
Recent Deliveries Recent Deliveries
%p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong. %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
.col-lg-9 .col-lg-9
- if hook_logs.any? - if hook_logs.present?
%table.table %table.table
%thead %thead
%tr %tr

View file

@ -51,11 +51,12 @@
= render 'new_project_fields', f: f, project_name_id: "blank-project-name" = render 'new_project_fields', f: f, project_name_id: "blank-project-name"
#create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' } #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
.card.card-slim.m-4.p-4 .gl-card.gl-my-5
%div .gl-card-body
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing' %div
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url } - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
= _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f| = form_for @project, html: { class: 'new_project' } do |f|
.project-template .project-template
.form-group .form-group

View file

@ -1,4 +1,7 @@
- page_title _('Edit'), "#{@runner.description} ##{@runner.id}", _('Runners') - breadcrumb_title _('Edit')
- page_title _('Edit'), "##{@runner.id} (#{@runner.short_sha})"
- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
- add_to_breadcrumbs "#{@runner.short_sha}", project_runner_path(@project, @runner)
%h2.page-title %h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id }) = s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })

View file

@ -0,0 +1,3 @@
- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
= render 'shared/runners/runner_details', runner: @runner

View file

@ -1,8 +1,9 @@
- page_title "#{@runner.description} ##{@runner.id}", _("Runners") - breadcrumb_title runner.short_sha
- page_title "##{runner.id} (#{runner.short_sha})"
%h2.page-title %h2.page-title
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id }) = s_('Runners|Runner #%{runner_id}' % { runner_id: runner.id })
= render 'shared/runners/runner_type_badge', runner: @runner = render 'shared/runners/runner_type_badge', runner: runner
.table-holder .table-holder
%table.table %table.table
@ -12,51 +13,51 @@
%th= s_('Runners|Value') %th= s_('Runners|Value')
%tr %tr
%td= s_('Runners|Active') %td= s_('Runners|Active')
%td= @runner.active? ? _('Yes') : _('No') %td= runner.active? ? _('Yes') : _('No')
%tr %tr
%td= s_('Runners|Protected') %td= s_('Runners|Protected')
%td= @runner.ref_protected? ? _('Yes') : _('No') %td= runner.ref_protected? ? _('Yes') : _('No')
%tr %tr
%td= s_('Runners|Can run untagged jobs') %td= s_('Runners|Can run untagged jobs')
%td= @runner.run_untagged? ? _('Yes') : _('No') %td= runner.run_untagged? ? _('Yes') : _('No')
- unless @runner.group_type? - unless runner.group_type?
%tr %tr
%td= s_('Runners|Locked to this project') %td= s_('Runners|Locked to this project')
%td= @runner.locked? ? _('Yes') : _('No') %td= runner.locked? ? _('Yes') : _('No')
%tr %tr
%td= s_('Runners|Tags') %td= s_('Runners|Tags')
%td %td
- @runner.tag_list.sort.each do |tag| - runner.tag_list.sort.each do |tag|
%span.badge.badge-primary %span.badge.badge-primary
= tag = tag
%tr %tr
%td= s_('Runners|Name') %td= s_('Runners|Name')
%td= @runner.name %td= runner.name
%tr %tr
%td= s_('Runners|Version') %td= s_('Runners|Version')
%td= @runner.version %td= runner.version
%tr %tr
%td= s_('Runners|IP Address') %td= s_('Runners|IP Address')
%td= @runner.ip_address %td= runner.ip_address
%tr %tr
%td= s_('Runners|Revision') %td= s_('Runners|Revision')
%td= @runner.revision %td= runner.revision
%tr %tr
%td= s_('Runners|Platform') %td= s_('Runners|Platform')
%td= @runner.platform %td= runner.platform
%tr %tr
%td= s_('Runners|Architecture') %td= s_('Runners|Architecture')
%td= @runner.architecture %td= runner.architecture
%tr %tr
%td= s_('Runners|Description') %td= s_('Runners|Description')
%td= @runner.description %td= runner.description
%tr %tr
%td= s_('Runners|Maximum job timeout') %td= s_('Runners|Maximum job timeout')
%td= @runner.maximum_timeout_human_readable %td= runner.maximum_timeout_human_readable
%tr %tr
%td= s_('Runners|Last contact') %td= s_('Runners|Last contact')
%td %td
- if @runner.contacted_at - if runner.contacted_at
= time_ago_with_tooltip @runner.contacted_at = time_ago_with_tooltip runner.contacted_at
- else - else
= s_('Never') = s_('Never')

View file

@ -5,7 +5,7 @@
%li{ class: active_when(params[:scope].nil?) } %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do = link_to subject_snippets_path(subject) do
= _("All") = _("All")
%span.badge.badge-pill %span.badge.badge-muted.badge-pill.gl-badge.sm
- if include_private - if include_private
= counts[:total] = counts[:total]
- else - else
@ -15,17 +15,17 @@
%li{ class: active_when(params[:scope] == "are_private") } %li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do = link_to subject_snippets_path(subject, scope: 'are_private') do
= _("Private") = _("Private")
%span.badge.badge-pill %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_private] = counts[:are_private]
%li{ class: active_when(params[:scope] == "are_internal") } %li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do = link_to subject_snippets_path(subject, scope: 'are_internal') do
= _("Internal") = _("Internal")
%span.badge.badge-pill %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_internal] = counts[:are_internal]
%li{ class: active_when(params[:scope] == "are_public") } %li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do = link_to subject_snippets_path(subject, scope: 'are_public') do
= _("Public") = _("Public")
%span.badge.badge-pill %span.badge.badge-muted.badge-pill.gl-badge.sm
= counts[:are_public] = counts[:are_public]

View file

@ -0,0 +1,5 @@
---
title: Update edit file buttons and spacing
merge_request: 60318
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Ensure we never error in web hook logs
merge_request: 60408
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Fix breadcrumbs and navigation in runner details pages
merge_request: 60129
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Refactor Webex Teams integration settings text
merge_request: 60565
author:
type: other

View file

@ -1,5 +0,0 @@
---
title: Fix Instance-level Project Integration Management page for GitLab FOSS
merge_request: 60354
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add gl-badge for badges in snippets nav
merge_request: 57966
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix tag matching behavior on New Release page
merge_request: 60035
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add internal API support for updating issue types on issues
merge_request: 60173
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Move card in billing page to gl-card utility class
merge_request: 59138
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Move 2fa recovery codes to GlCard component
merge_request: 59219
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Move to new GitLab UI for card in health check page
merge_request: 59081
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Remove template tabs in new project page for GitLab.com
merge_request: 59083
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Update to question-o and change color in admin users
merge_request: 59133
author: Yogi (@yo)
type: changed

View file

@ -0,0 +1,5 @@
---
title: Update to question-o from question icon in navbar
merge_request: 59134
author: Yogi (@yo)
type: changed

View file

@ -1037,7 +1037,7 @@ we felt we cannot assume this is true everywhere.
The cache needs a directory to store its files in. This directory The cache needs a directory to store its files in. This directory
should be: should be:
- In a filesystem with enough space. If the cache filesystem runs out of space, all - In a file system with enough space. If the cache file system runs out of space, all
fetches start failing. fetches start failing.
- On a disk with enough IO bandwidth. If the cache disk runs out of IO bandwidth, all - On a disk with enough IO bandwidth. If the cache disk runs out of IO bandwidth, all
fetches, and probably the entire server, slows down. fetches, and probably the entire server, slows down.
@ -1052,8 +1052,8 @@ uses a unique random string as part of the cache filenames it creates. This mean
- They do not reuse another process's files. - They do not reuse another process's files.
While the default directory puts the cache files in the same While the default directory puts the cache files in the same
filesystem as your repository data, this is not requirement. You can file system as your repository data, this is not requirement. You can
put the cache files on a different filesystem if that works better for put the cache files on a different file system if that works better for
your infrastructure. your infrastructure.
The amount of IO bandwidth required from the disk depends on: The amount of IO bandwidth required from the disk depends on:
@ -1157,7 +1157,7 @@ The following cache metrics are available.
|Metric|Type|Labels|Description| |Metric|Type|Labels|Description|
|:---|:---|:---|:---| |:---|:---|:---|:---|
|`gitaly_pack_objects_cache_enabled`|gauge|`dir`,`max_age`|Set to `1` when the cache is enabled via the Gitaly config file| |`gitaly_pack_objects_cache_enabled`|gauge|`dir`,`max_age`|Set to `1` when the cache is enabled via the Gitaly configuration file|
|`gitaly_pack_objects_cache_lookups_total`|counter|`result`|Hit/miss counter for cache lookups| |`gitaly_pack_objects_cache_lookups_total`|counter|`result`|Hit/miss counter for cache lookups|
|`gitaly_pack_objects_generated_bytes_total`|counter||Number of bytes written into the cache| |`gitaly_pack_objects_generated_bytes_total`|counter||Number of bytes written into the cache|
|`gitaly_pack_objects_served_bytes_total`|counter||Number of bytes read from the cache| |`gitaly_pack_objects_served_bytes_total`|counter||Number of bytes read from the cache|

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -770,6 +770,29 @@ argument :title, GraphQL::STRING_TYPE,
description: copy_field_description(Types::MergeRequestType, :title) description: copy_field_description(Types::MergeRequestType, :title)
``` ```
### Documentation references
Sometimes we want to refer to external URLs in our descriptions. To make this
easier, and provide proper markup in the generated reference documentation, we
provide a `see` property on fields. For example:
```ruby
field :genus,
type: GraphQL::STRING_TYPE,
null: true,
description: 'A taxonomic genus.'
see: { 'Wikipedia page on genera' => 'https://wikipedia.org/wiki/Genus' }
```
This will render in our documentation as:
```markdown
A taxonomic genus. See: [Wikipedia page on genera](https://wikipedia.org/wiki/Genus)
```
Multiple documentation references can be provided. The syntax for this property
is a `HashMap` where the keys are textual descriptions, and the values are URLs.
## Authorization ## Authorization
See: [GraphQL Authorization](graphql_guide/authorization.md) See: [GraphQL Authorization](graphql_guide/authorization.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View file

@ -52,7 +52,7 @@ The following table shows the supported metrics, at which level they are support
| --------------- | ----------- | --------------- | ---------- | ------- | | --------------- | ----------- | --------------- | ---------- | ------- |
| `deployment_frequency` | Project-level | [13.7+](../../api/dora/metrics.md) | [13.8+](#deployment-frequency-charts) | The [old API endopint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. | | `deployment_frequency` | Project-level | [13.7+](../../api/dora/metrics.md) | [13.8+](#deployment-frequency-charts) | The [old API endopint](../../api/dora4_project_analytics.md) was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/323713) in 13.10. |
| `deployment_frequency` | Group-level | [13.10+](../../api/dora/metrics.md) | To be supported | | | `deployment_frequency` | Group-level | [13.10+](../../api/dora/metrics.md) | To be supported | |
| `lead_time_for_changes` | Project-level | [13.10+](../../api/dora/metrics.md) | To be supported | Unit in seconds. Aggregation method is median. | | `lead_time_for_changes` | Project-level | [13.10+](../../api/dora/metrics.md) | [13.11+](#lead-time-charts) | Unit in seconds. Aggregation method is median. |
| `lead_time_for_changes` | Group-level | [13.10+](../../api/dora/metrics.md) | To be supported | Unit in seconds. Aggregation method is median. | | `lead_time_for_changes` | Group-level | [13.10+](../../api/dora/metrics.md) | To be supported | Unit in seconds. Aggregation method is median. |
| `change_failure_rate` | Project/Group-level | To be supported | To be supported | | | `change_failure_rate` | Project/Group-level | To be supported | To be supported | |
| `time_to_restore_service` | Project/Group-level | To be supported | To be supported | | | `time_to_restore_service` | Project/Group-level | To be supported | To be supported | |
@ -61,9 +61,10 @@ The following table shows the supported metrics, at which level they are support
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.8. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/275991) in GitLab 13.8.
The **Analytics > CI/CD Analytics** page shows information about the deployment frequency to the The **Analytics > CI/CD Analytics** page shows information about the deployment
`production` environment. The environment **must** be named `production` for its deployment frequency to the `production` environment. The environment must be part of the
information to appear on the graphs. [production deployment tier](../../ci/environments/index.md#deployment-tier-of-environments)
for its deployment information to appear on the graphs.
![Deployment frequency](img/deployment_frequency_chart_v13_8.png) ![Deployment frequency](img/deployment_frequency_chart_v13_8.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View file

@ -6,25 +6,32 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Webex Teams service **(FREE)** # Webex Teams service **(FREE)**
You can configure GitLab to send notifications to a Webex Teams space. You can configure GitLab to send notifications to a Webex Teams space:
1. Create a webhook for the space.
1. Add the webhook to GitLab.
## Create a webhook for the space ## Create a webhook for the space
1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/messaging/applications/incoming-webhooks-cisco-systems-38054). 1. Go to the [Incoming Webhooks app page](https://apphub.webex.com/messaging/applications/incoming-webhooks-cisco-systems-38054).
1. Click **Connect** and log in to Webex Teams, if required. 1. Select **Connect** and log in to Webex Teams, if required.
1. Enter a name for the webhook and select the space to receive the notifications. 1. Enter a name for the webhook and select the space to receive the notifications.
1. Click **ADD**. 1. Select **ADD**.
1. Copy the **Webhook URL**. 1. Copy the **Webhook URL**.
## Configure settings in GitLab ## Configure settings in GitLab
Once you have a webhook URL for your Webex Teams space, you can configure GitLab to send notifications. Once you have a webhook URL for your Webex Teams space, you can configure GitLab to send
notifications:
1. Navigate to **Project > Settings > Integrations**. 1. Navigate to:
- **Settings > Integrations** in a project to enable the integration at the project level.
- **Settings > Integrations** in a group to enable the integration at the group level.
- **Settings > Integrations** in the Admin Area (**{admin}**) to enable an instance-level integration.
1. Select the **Webex Teams** integration. 1. Select the **Webex Teams** integration.
1. Ensure that the **Active** toggle is enabled. 1. Ensure that the **Active** toggle is enabled.
1. Select the checkboxes corresponding to the GitLab events you want to receive in Webex Teams. 1. Select the checkboxes corresponding to the GitLab events you want to receive in Webex Teams.
1. Paste the **Webhook** URL for the Webex Teams space. 1. Paste the **Webhook** URL for the Webex Teams space.
1. Configure the remaining options and then click **Test settings and save changes**. 1. Configure the remaining options and then click **Test settings and save changes**.
The Webex Teams space now begins to receive all applicable GitLab events. The Webex Teams space begins to receive all applicable GitLab events.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -765,7 +765,7 @@ module API
required: true, required: true,
name: :webhook, name: :webhook,
type: String, type: String,
desc: 'The Webex Teams webhook. e.g. https://api.ciscospark.com/v1/webhooks/incoming/…' desc: 'The Webex Teams webhook. For example, https://api.ciscospark.com/v1/webhooks/incoming/...'
}, },
chat_notification_events chat_notification_events
].flatten ].flatten

View file

@ -9956,10 +9956,7 @@ msgstr ""
msgid "DORA4Metrics|Days from merge to deploy" msgid "DORA4Metrics|Days from merge to deploy"
msgstr "" msgstr ""
msgid "DORA4Metrics|Deployments" msgid "DORA4Metrics|Deployment frequency"
msgstr ""
msgid "DORA4Metrics|Deployments charts"
msgstr "" msgstr ""
msgid "DORA4Metrics|Lead time" msgid "DORA4Metrics|Lead time"
@ -9971,19 +9968,19 @@ msgstr ""
msgid "DORA4Metrics|No merge requests were deployed during this period" msgid "DORA4Metrics|No merge requests were deployed during this period"
msgstr "" msgstr ""
msgid "DORA4Metrics|Something went wrong while getting deployment frequency data" msgid "DORA4Metrics|Number of deployments"
msgstr ""
msgid "DORA4Metrics|Something went wrong while getting deployment frequency data."
msgstr "" msgstr ""
msgid "DORA4Metrics|Something went wrong while getting lead time data." msgid "DORA4Metrics|Something went wrong while getting lead time data."
msgstr "" msgstr ""
msgid "DORA4Metrics|These charts display the frequency of deployments to the production environment, as part of the DORA 4 metrics. The environment must be named %{codeStart}production%{codeEnd} for its data to appear in these charts." msgid "DORA4Metrics|The chart displays the frequency of deployments to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr "" msgstr ""
msgid "DORA4Metrics|These charts display the median time between a merge request being merged and deployed to production, as part of the DORA 4 metrics." msgid "DORA4Metrics|The chart displays the median time between a merge request being merged and deployed to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
msgid "DORA4|Lead time charts"
msgstr "" msgstr ""
msgid "Dashboard" msgid "Dashboard"
@ -10990,6 +10987,9 @@ msgstr ""
msgid "Deployment Frequency" msgid "Deployment Frequency"
msgstr "" msgstr ""
msgid "Deployment frequency"
msgstr ""
msgid "Deployments" msgid "Deployments"
msgstr "" msgstr ""
@ -18953,6 +18953,9 @@ msgstr ""
msgid "Lead Time" msgid "Lead Time"
msgstr "" msgstr ""
msgid "Lead time"
msgstr ""
msgid "Learn CI/CD syntax" msgid "Learn CI/CD syntax"
msgstr "" msgstr ""
@ -35808,6 +35811,15 @@ msgstr ""
msgid "WebIDE|You need permission to edit files directly in this project. Go to your fork to make changes and submit a merge request." msgid "WebIDE|You need permission to edit files directly in this project. Go to your fork to make changes and submit a merge request."
msgstr "" msgstr ""
msgid "WebexTeamsService|Send notifications about project events to Webex Teams."
msgstr ""
msgid "WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}"
msgstr ""
msgid "WebexTeamsService|Webex Teams"
msgstr ""
msgid "Webhook" msgid "Webhook"
msgstr "" msgstr ""

View file

@ -114,7 +114,7 @@ function run_locally_or_in_docker() {
$cmd $args $cmd $args
elif hash docker 2>/dev/null elif hash docker 2>/dev/null
then then
docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.12-vale-2.6.1-markdownlint-0.24.0 ${cmd} ${args} docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.13-vale-2.10.2-markdownlint-0.26.0 ${cmd} ${args}
else else
echo echo
echo " ✖ ERROR: '${cmd}' not found. Install '${cmd}' or Docker to proceed." >&2 echo " ✖ ERROR: '${cmd}' not found. Install '${cmd}' or Docker to proceed." >&2

View file

@ -3,11 +3,11 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Projects::HooksController do RSpec.describe Projects::HooksController do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
let(:user) { project.owner }
before do before do
project.add_maintainer(user)
sign_in(user) sign_in(user)
end end
@ -20,6 +20,56 @@ RSpec.describe Projects::HooksController do
end end
end end
describe '#edit' do
let_it_be(:hook) { create(:project_hook, project: project) }
let(:params) do
{ namespace_id: project.namespace, project_id: project, id: hook.id }
end
render_views
it 'does not error if the hook cannot be found' do
get :edit, params: params.merge(id: non_existing_record_id)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'assigns hook_logs' do
get :edit, params: params
expect(assigns[:hook]).to be_present
expect(assigns[:hook_logs]).to be_empty
it_renders_correctly
end
it 'handles when logs are present' do
create_list(:web_hook_log, 3, web_hook: hook)
get :edit, params: params
expect(assigns[:hook]).to be_present
expect(assigns[:hook_logs].count).to eq 3
it_renders_correctly
end
it 'can paginate logs' do
create_list(:web_hook_log, 21, web_hook: hook)
get :edit, params: params.merge(page: 2)
expect(assigns[:hook]).to be_present
expect(assigns[:hook_logs].count).to eq 1
it_renders_correctly
end
def it_renders_correctly
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:edit)
expect(response).to render_template('projects/hook_logs/_index')
end
end
describe '#create' do describe '#create' do
it 'sets all parameters' do it 'sets all parameters' do
hook_params = { hook_params = {

View file

@ -586,13 +586,15 @@ RSpec.describe Projects::IssuesController do
end end
describe 'PUT #update' do describe 'PUT #update' do
let(:issue_params) { { title: 'New title' } }
subject do subject do
put :update, put :update,
params: { params: {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
id: issue.to_param, id: issue.to_param,
issue: { title: 'New title' } issue: issue_params
}, },
format: :json format: :json
end end
@ -614,6 +616,17 @@ RSpec.describe Projects::IssuesController do
expect(issue.reload.title).to eq('New title') expect(issue.reload.title).to eq('New title')
end end
context 'with issue_type param' do
let(:issue_params) { { issue_type: 'incident' } }
it 'permits the parameter' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(issue.reload.issue_type).to eql('incident')
end
end
context 'when the SpamVerdictService disallows' do context 'when the SpamVerdictService disallows' do
before do before do
stub_application_setting(recaptcha_enabled: true) stub_application_setting(recaptcha_enabled: true)

View file

@ -40,7 +40,7 @@ RSpec.describe 'List issue resource label events', :js do
labels.each { |label| click_link label } labels.each { |label| click_link label }
click_on 'Edit' send_keys(:escape)
wait_for_requests wait_for_requests
end end
end end

View file

@ -54,8 +54,8 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findGlTabs().exists()).toBe(true); expect(findGlTabs().exists()).toBe(true);
expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines'); expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines');
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployments'); expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead Time'); expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
}); });
it('renders the pipeline charts', () => { it('renders the pipeline charts', () => {
@ -75,7 +75,7 @@ describe('ProjectsPipelinesChartsApp', () => {
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`); setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
mergeUrlParams.mockImplementation(({ chart }, path) => { mergeUrlParams.mockImplementation(({ chart }, path) => {
expect(chart).toBe('deployments'); expect(chart).toBe('deployment-frequency');
expect(path).toBe(window.location.pathname); expect(path).toBe(window.location.pathname);
chartsPath = `${path}?chart=${chart}`; chartsPath = `${path}?chart=${chart}`;
return chartsPath; return chartsPath;
@ -114,12 +114,12 @@ describe('ProjectsPipelinesChartsApp', () => {
describe('when provided with a query param', () => { describe('when provided with a query param', () => {
it.each` it.each`
chart | tab chart | tab
${'lead-time'} | ${'2'} ${'lead-time'} | ${'2'}
${'deployments'} | ${'1'} ${'deployment-frequency'} | ${'1'}
${'pipelines'} | ${'0'} ${'pipelines'} | ${'0'}
${'fake'} | ${'0'} ${'fake'} | ${'0'}
${''} | ${'0'} ${''} | ${'0'}
`('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => { `('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`); setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
getParameterValues.mockImplementation((name) => { getParameterValues.mockImplementation((name) => {
@ -152,7 +152,7 @@ describe('ProjectsPipelinesChartsApp', () => {
getParameterValues.mockImplementationOnce((name) => { getParameterValues.mockImplementationOnce((name) => {
expect(name).toBe('chart'); expect(name).toBe('chart');
return ['deployments']; return ['deployment-frequency'];
}); });
popstateHandler(); popstateHandler();

View file

@ -10,29 +10,35 @@ const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from'; const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag'; const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
// A mock version of the RefSelector component that simulates
// a scenario where the users has searched for "nonexistent-tag"
// and the component has found no tags that match.
const RefSelectorStub = Vue.component('RefSelectorStub', {
data() {
return {
footerSlotProps: {
isLoading: false,
matches: {
tags: { totalCount: 0 },
},
query: NONEXISTENT_TAG_NAME,
},
};
},
template: '<div><slot name="footer" v-bind="footerSlotProps"></slot></div>',
});
describe('releases/components/tag_field_new', () => { describe('releases/components/tag_field_new', () => {
let store; let store;
let wrapper; let wrapper;
let RefSelectorStub;
const createComponent = (
mountFn = shallowMount,
{ searchQuery } = { searchQuery: NONEXISTENT_TAG_NAME },
) => {
// A mock version of the RefSelector component that just renders the
// #footer slot, so that the content inside this slot can be tested.
RefSelectorStub = Vue.component('RefSelectorStub', {
data() {
return {
footerSlotProps: {
isLoading: false,
matches: {
tags: {
totalCount: 1,
list: [{ name: TEST_TAG_NAME }],
},
},
query: searchQuery,
},
};
},
template: '<div><slot name="footer" v-bind="footerSlotProps"></slot></div>',
});
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TagFieldNew, { wrapper = mountFn(TagFieldNew, {
store, store,
stubs: { stubs: {
@ -84,8 +90,6 @@ describe('releases/components/tag_field_new', () => {
describe('when the user selects a new tag name', () => { describe('when the user selects a new tag name', () => {
beforeEach(async () => { beforeEach(async () => {
findCreateNewTagOption().vm.$emit('click'); findCreateNewTagOption().vm.$emit('click');
await wrapper.vm.$nextTick();
}); });
it("updates the store's release.tagName property", () => { it("updates the store's release.tagName property", () => {
@ -102,8 +106,6 @@ describe('releases/components/tag_field_new', () => {
beforeEach(async () => { beforeEach(async () => {
findTagNameDropdown().vm.$emit('input', updatedTagName); findTagNameDropdown().vm.$emit('input', updatedTagName);
await wrapper.vm.$nextTick();
}); });
it("updates the store's release.tagName property", () => { it("updates the store's release.tagName property", () => {
@ -116,6 +118,28 @@ describe('releases/components/tag_field_new', () => {
}); });
}); });
describe('"Create tag" option', () => {
describe('when the search query exactly matches one of the search results', () => {
beforeEach(async () => {
createComponent(mount, { searchQuery: TEST_TAG_NAME });
});
it('does not show the "Create tag" option', () => {
expect(findCreateNewTagOption().exists()).toBe(false);
});
});
describe('when the search query does not exactly match one of the search results', () => {
beforeEach(async () => {
createComponent(mount, { searchQuery: NONEXISTENT_TAG_NAME });
});
it('shows the "Create tag" option', () => {
expect(findCreateNewTagOption().exists()).toBe(true);
});
});
});
describe('validation', () => { describe('validation', () => {
beforeEach(() => { beforeEach(() => {
createComponent(mount); createComponent(mount);
@ -176,8 +200,6 @@ describe('releases/components/tag_field_new', () => {
const updatedCreateFrom = 'update-create-from'; const updatedCreateFrom = 'update-create-from';
findCreateFromDropdown().vm.$emit('input', updatedCreateFrom); findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
await wrapper.vm.$nextTick();
expect(store.state.editNew.createFrom).toBe(updatedCreateFrom); expect(store.state.editNew.createFrom).toBe(updatedCreateFrom);
}); });
}); });

View file

@ -3,7 +3,7 @@ import Vuex from 'vuex';
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue'; import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue'; import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
import { getStoreConfig } from '~/reports/codequality_report/store'; import { getStoreConfig } from '~/reports/codequality_report/store';
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data'; import { parsedReportIssues } from './mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
@ -80,7 +80,7 @@ describe('Grouped code quality reports app', () => {
describe('with issues', () => { describe('with issues', () => {
describe('with new issues', () => { describe('with new issues', () => {
beforeEach(() => { beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]]; mockStore.state.newIssues = parsedReportIssues.newIssues;
mockStore.state.resolvedIssues = []; mockStore.state.resolvedIssues = [];
}); });
@ -89,14 +89,14 @@ describe('Grouped code quality reports app', () => {
}); });
it('renders custom codequality issue body', () => { it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]); expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
}); });
}); });
describe('with resolved issues', () => { describe('with resolved issues', () => {
beforeEach(() => { beforeEach(() => {
mockStore.state.newIssues = []; mockStore.state.newIssues = [];
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]]; mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
}); });
it('renders summary text', () => { it('renders summary text', () => {
@ -104,14 +104,14 @@ describe('Grouped code quality reports app', () => {
}); });
it('renders custom codequality issue body', () => { it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]); expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.resolvedIssues[0]);
}); });
}); });
describe('with new and resolved issues', () => { describe('with new and resolved issues', () => {
beforeEach(() => { beforeEach(() => {
mockStore.state.newIssues = [mockParsedHeadIssues[0]]; mockStore.state.newIssues = parsedReportIssues.newIssues;
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]]; mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
}); });
it('renders summary text', () => { it('renders summary text', () => {
@ -121,7 +121,7 @@ describe('Grouped code quality reports app', () => {
}); });
it('renders custom codequality issue body', () => { it('renders custom codequality issue body', () => {
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]); expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
}); });
}); });
}); });

View file

@ -1,94 +1,3 @@
export const headIssues = [
{
check_name: 'Rubocop/Lint/UselessAssignment',
description: 'Insecure Dependency',
location: {
path: 'lib/six.rb',
lines: {
begin: 6,
end: 7,
},
},
fingerprint: 'e879dd9bbc0953cad5037cde7ff0f627',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
];
export const mockParsedHeadIssues = [
{
...headIssues[1],
name: 'Insecure Dependency',
path: 'lib/six.rb',
urlPath: 'headPath/lib/six.rb#L6',
line: 6,
},
];
export const baseIssues = [
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 22,
end: 22,
},
},
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
},
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
lines: {
begin: 21,
end: 21,
},
},
fingerprint: 'ca2354534dee94ae60ba2f54e3857c50e5',
},
];
export const mockParsedBaseIssues = [
{
...baseIssues[1],
name: 'Insecure Dependency',
path: 'Gemfile.lock',
line: 21,
urlPath: 'basePath/Gemfile.lock#L21',
},
];
export const issueDiff = [
{
categories: ['Security'],
check_name: 'Insecure Dependency',
description: 'Insecure Dependency',
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
line: 6,
location: { lines: { begin: 22, end: 22 }, path: 'Gemfile.lock' },
name: 'Insecure Dependency',
path: 'lib/six.rb',
urlPath: 'headPath/lib/six.rb#L6',
},
];
export const reportIssues = { export const reportIssues = {
status: 'failed', status: 'failed',
new_errors: [ new_errors: [

View file

@ -5,30 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import createStore from '~/reports/codequality_report/store'; import createStore from '~/reports/codequality_report/store';
import * as actions from '~/reports/codequality_report/store/actions'; import * as actions from '~/reports/codequality_report/store/actions';
import * as types from '~/reports/codequality_report/store/mutation_types'; import * as types from '~/reports/codequality_report/store/mutation_types';
import { import { reportIssues, parsedReportIssues } from '../mock_data';
headIssues,
baseIssues,
mockParsedHeadIssues,
mockParsedBaseIssues,
reportIssues,
parsedReportIssues,
} from '../mock_data';
// mock codequality comparison worker
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
jest.fn().mockImplementation(() => {
return {
addEventListener: (eventName, callback) => {
callback({
data: {
newIssues: [mockParsedHeadIssues[0]],
resolvedIssues: [mockParsedBaseIssues[0]],
},
});
},
};
}),
);
describe('Codequality Reports actions', () => { describe('Codequality Reports actions', () => {
let localState; let localState;
@ -43,9 +20,6 @@ describe('Codequality Reports actions', () => {
it('should commit SET_PATHS mutation', (done) => { it('should commit SET_PATHS mutation', (done) => {
const paths = { const paths = {
basePath: 'basePath', basePath: 'basePath',
headPath: 'headPath',
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
reportsPath: 'reportsPath', reportsPath: 'reportsPath',
helpPath: 'codequalityHelpPath', helpPath: 'codequalityHelpPath',
}; };
@ -63,119 +37,64 @@ describe('Codequality Reports actions', () => {
describe('fetchReports', () => { describe('fetchReports', () => {
let mock; let mock;
let diffFeatureFlagEnabled;
describe('with codequalityBackendComparison feature flag enabled', () => { beforeEach(() => {
beforeEach(() => { localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
diffFeatureFlagEnabled = true; localState.basePath = '/base/path';
localState.reportsPath = `${TEST_HOST}/codequality_reports.json`; mock = new MockAdapter(axios);
mock = new MockAdapter(axios); });
});
afterEach(() => { afterEach(() => {
mock.restore(); mock.restore();
}); });
describe('on success', () => { describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => { it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues); mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
testAction( testAction(
actions.fetchReports, actions.fetchReports,
diffFeatureFlagEnabled, null,
localState, localState,
[{ type: types.REQUEST_REPORTS }], [{ type: types.REQUEST_REPORTS }],
[ [
{ {
payload: parsedReportIssues, payload: parsedReportIssues,
type: 'receiveReportsSuccess', type: 'receiveReportsSuccess',
}, },
], ],
done, done,
); );
});
});
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
done,
);
});
}); });
}); });
describe('with codequalityBackendComparison feature flag disabled', () => { describe('on error', () => {
beforeEach(() => { it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
diffFeatureFlagEnabled = false; mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
localState.headPath = `${TEST_HOST}/head.json`;
localState.basePath = `${TEST_HOST}/base.json`; testAction(
mock = new MockAdapter(axios); actions.fetchReports,
null,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
done,
);
}); });
});
afterEach(() => { describe('with no base path', () => {
mock.restore(); it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
}); localState.basePath = null;
describe('on success', () => { testAction(
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => { actions.fetchReports,
mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues); null,
mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues); localState,
[{ type: types.REQUEST_REPORTS }],
testAction( [{ type: 'receiveReportsError' }],
actions.fetchReports, done,
diffFeatureFlagEnabled, );
localState,
[{ type: types.REQUEST_REPORTS }],
[
{
payload: {
newIssues: [mockParsedHeadIssues[0]],
resolvedIssues: [mockParsedBaseIssues[0]],
},
type: 'receiveReportsSuccess',
},
],
done,
);
});
});
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
mock.onGet(`${TEST_HOST}/head.json`).reply(500);
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
done,
);
});
});
describe('with no base path', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
localState.basePath = null;
testAction(
actions.fetchReports,
diffFeatureFlagEnabled,
localState,
[{ type: types.REQUEST_REPORTS }],
[{ type: 'receiveReportsError' }],
done,
);
});
}); });
}); });
}); });

View file

@ -13,23 +13,17 @@ describe('Codequality Reports mutations', () => {
describe('SET_PATHS', () => { describe('SET_PATHS', () => {
it('sets paths to given values', () => { it('sets paths to given values', () => {
const basePath = 'base.json'; const basePath = 'base.json';
const headPath = 'head.json'; const reportsPath = 'reports.json';
const baseBlobPath = 'base/blob/path/';
const headBlobPath = 'head/blob/path/';
const helpPath = 'help.html'; const helpPath = 'help.html';
mutations.SET_PATHS(localState, { mutations.SET_PATHS(localState, {
basePath, basePath,
headPath, reportsPath,
baseBlobPath,
headBlobPath,
helpPath, helpPath,
}); });
expect(localState.basePath).toEqual(basePath); expect(localState.basePath).toEqual(basePath);
expect(localState.headPath).toEqual(headPath); expect(localState.reportsPath).toEqual(reportsPath);
expect(localState.baseBlobPath).toEqual(baseBlobPath);
expect(localState.headBlobPath).toEqual(headBlobPath);
expect(localState.helpPath).toEqual(helpPath); expect(localState.helpPath).toEqual(helpPath);
}); });
}); });

View file

@ -1,153 +0,0 @@
import {
parseCodeclimateMetrics,
doCodeClimateComparison,
} from '~/reports/codequality_report/store/utils/codequality_comparison';
import {
baseIssues,
mockParsedHeadIssues,
mockParsedBaseIssues,
reportIssues,
parsedReportIssues,
} from '../../mock_data';
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
let mockPostMessageCallback;
return jest.fn().mockImplementation(() => {
return {
addEventListener: (_, callback) => {
mockPostMessageCallback = callback;
},
postMessage: (data) => {
if (!data.headIssues) return mockPostMessageCallback({ data: {} });
if (!data.baseIssues) throw new Error();
const key = 'fingerprint';
return mockPostMessageCallback({
data: {
newIssues: data.headIssues.filter(
(item) => !data.baseIssues.find((el) => el[key] === item[key]),
),
resolvedIssues: data.baseIssues.filter(
(item) => !data.headIssues.find((el) => el[key] === item[key]),
),
},
});
},
};
});
});
describe('Codequality report store utils', () => {
let result;
describe('parseCodeclimateMetrics', () => {
it('should parse the issues from codeclimate artifacts', () => {
[result] = parseCodeclimateMetrics(baseIssues, 'path');
expect(result.name).toEqual(baseIssues[0].check_name);
expect(result.path).toEqual(baseIssues[0].location.path);
expect(result.line).toEqual(baseIssues[0].location.lines.begin);
});
it('should parse the issues from backend codequality diff', () => {
[result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path');
expect(result.name).toEqual(parsedReportIssues.newIssues[0].name);
expect(result.path).toEqual(parsedReportIssues.newIssues[0].path);
expect(result.line).toEqual(parsedReportIssues.newIssues[0].line);
});
describe('when an issue has no location or path', () => {
const issue = { description: 'Insecure Dependency' };
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
});
});
describe('when an issue has a path but no line', () => {
const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } };
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
expect(result.path).toEqual(issue.location.path);
expect(result.urlPath).toEqual(`path/${issue.location.path}`);
});
});
describe('when an issue has a line nested in positions', () => {
const issue = {
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
positions: { begin: { line: 84 } },
},
};
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
expect(result.path).toEqual(issue.location.path);
expect(result.urlPath).toEqual(
`path/${issue.location.path}#L${issue.location.positions.begin.line}`,
);
});
});
describe('with an empty issue array', () => {
beforeEach(() => {
result = parseCodeclimateMetrics([], 'path');
});
it('returns an empty array', () => {
expect(result).toEqual([]);
});
});
});
describe('doCodeClimateComparison', () => {
describe('when the comparison worker finds changed issues', () => {
beforeEach(async () => {
result = await doCodeClimateComparison(mockParsedHeadIssues, mockParsedBaseIssues);
});
it('returns the new and resolved issues', () => {
expect(result.resolvedIssues[0]).toEqual(mockParsedBaseIssues[0]);
expect(result.newIssues[0]).toEqual(mockParsedHeadIssues[0]);
});
});
describe('when the comparison worker finds no changed issues', () => {
beforeEach(async () => {
result = await doCodeClimateComparison([], []);
});
it('returns the empty issue arrays', () => {
expect(result.newIssues).toEqual([]);
expect(result.resolvedIssues).toEqual([]);
});
});
describe('when the comparison worker is given malformed data', () => {
it('rejects the promise', () => {
return expect(doCodeClimateComparison(null)).rejects.toEqual({});
});
});
describe('when the comparison worker encounters an error', () => {
it('rejects the promise and throws an error', () => {
return expect(doCodeClimateComparison([], null)).rejects.toThrow();
});
});
});
});

View file

@ -0,0 +1,74 @@
import { reportIssues, parsedReportIssues } from 'jest/reports/codequality_report/mock_data';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
describe('Codequality report store utils', () => {
let result;
describe('parseCodeclimateMetrics', () => {
it('should parse the issues from backend codequality diff', () => {
[result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path');
expect(result.name).toEqual(parsedReportIssues.newIssues[0].name);
expect(result.path).toEqual(parsedReportIssues.newIssues[0].path);
expect(result.line).toEqual(parsedReportIssues.newIssues[0].line);
});
describe('when an issue has no location or path', () => {
const issue = { description: 'Insecure Dependency' };
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
});
});
describe('when an issue has a path but no line', () => {
const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } };
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
expect(result.path).toEqual(issue.location.path);
expect(result.urlPath).toEqual(`path/${issue.location.path}`);
});
});
describe('when an issue has a line nested in positions', () => {
const issue = {
description: 'Insecure Dependency',
location: {
path: 'Gemfile.lock',
positions: { begin: { line: 84 } },
},
};
beforeEach(() => {
[result] = parseCodeclimateMetrics([issue], 'path');
});
it('is parsed', () => {
expect(result.name).toEqual(issue.description);
expect(result.path).toEqual(issue.location.path);
expect(result.urlPath).toEqual(
`path/${issue.location.path}#L${issue.location.positions.begin.line}`,
);
});
});
describe('with an empty issue array', () => {
beforeEach(() => {
result = parseCodeclimateMetrics([], 'path');
});
it('returns an empty array', () => {
expect(result).toEqual([]);
});
});
});
});

View file

@ -1,8 +1,8 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store'; import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
import { mockConfig } from './mock_data'; import { mockConfig } from './mock_data';
@ -50,13 +50,20 @@ describe('DropdownContent', () => {
describe('template', () => { describe('template', () => {
it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => { it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => {
expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents'); expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
expect(wrapper.attributes('style')).toBe(undefined); expect(wrapper.attributes('style')).toBeUndefined();
}); });
it('renders component container element with styles when `renderOnTop` is true', () => { describe('when `renderOnTop` is true', () => {
wrapper = createComponent(mockConfig, { renderOnTop: true }); it.each`
variant | expected
${DropdownVariant.Sidebar} | ${'bottom: 3rem'}
${DropdownVariant.Standalone} | ${'bottom: 2rem'}
${DropdownVariant.Embedded} | ${'bottom: 2rem'}
`('renders upward for $variant variant', ({ variant, expected }) => {
wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true });
expect(wrapper.attributes('style')).toContain('bottom: 100%'); expect(wrapper.attributes('style')).toContain(expected);
});
}); });
}); });
}); });

View file

@ -3,6 +3,7 @@ import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils'; import { isInViewport } from '~/lib/utils/common_utils';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue'; import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue'; import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
@ -190,40 +191,33 @@ describe('LabelsSelectRoot', () => {
}); });
describe('sets content direction based on viewport', () => { describe('sets content direction based on viewport', () => {
it('does not set direction when `state.variant` is not "embedded"', async () => { describe.each(Object.values(DropdownVariant))(
createComponent(); 'when labels variant is "%s"',
({ variant }) => {
wrapper.vm.$store.dispatch('toggleDropdownContents'); beforeEach(() => {
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); createComponent({ ...mockConfig, variant });
await wrapper.vm.$nextTick; wrapper.vm.$store.dispatch('toggleDropdownContents');
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
describe('when `state.variant` is "embedded"', () => {
beforeEach(() => {
createComponent({ ...mockConfig, variant: 'embedded' });
wrapper.vm.$store.dispatch('toggleDropdownContents');
});
it('set direction when out of viewport', () => {
isInViewport.mockImplementation(() => false);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
}); });
});
it('does not set direction when inside of viewport', () => { it('set direction when out of viewport', () => {
isInViewport.mockImplementation(() => true); isInViewport.mockImplementation(() => false);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state); wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false); expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
});
}); });
});
}); it('does not set direction when inside of viewport', () => {
isInViewport.mockImplementation(() => true);
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
});
});
},
);
}); });
}); });

View file

@ -282,7 +282,14 @@ RSpec.describe Issues::UpdateService, :mailer do
end end
it 'filters out params that cannot be set without the :admin_issue permission' do it 'filters out params that cannot be set without the :admin_issue permission' do
described_class.new(project, guest, opts.merge(confidential: true)).execute(issue) described_class.new(
project,
guest,
opts.merge(
confidential: true,
issue_type: 'test_case'
)
).execute(issue)
expect(issue).to be_valid expect(issue).to be_valid
expect(issue.title).to eq 'New title' expect(issue.title).to eq 'New title'
@ -293,6 +300,7 @@ RSpec.describe Issues::UpdateService, :mailer do
expect(issue.due_date).to be_nil expect(issue.due_date).to be_nil
expect(issue.discussion_locked).to be_falsey expect(issue.discussion_locked).to be_falsey
expect(issue.confidential).to be_falsey expect(issue.confidential).to be_falsey
expect(issue.issue_type).to eql('issue')
end end
end end

View file

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'shared/runners/show.html.haml' do RSpec.describe 'shared/runners/_runner_details.html.haml' do
include PageLayoutHelper include PageLayoutHelper
let(:runner) do let(:runner) do
@ -14,7 +14,7 @@ RSpec.describe 'shared/runners/show.html.haml' do
end end
before do before do
assign(:runner, runner) allow(view).to receive(:runner) { runner }
end end
subject do subject do
@ -24,7 +24,7 @@ RSpec.describe 'shared/runners/show.html.haml' do
describe 'Page title' do describe 'Page title' do
before do before do
expect_any_instance_of(PageLayoutHelper).to receive(:page_title).with("#{runner.description} ##{runner.id}", 'Runners') expect(view).to receive(:page_title).with("##{runner.id} (#{runner.short_sha})")
end end
it 'sets proper page title' do it 'sets proper page title' do
@ -147,7 +147,7 @@ RSpec.describe 'shared/runners/show.html.haml' do
context 'when runner have already contacted' do context 'when runner have already contacted' do
let(:runner) { create(:ci_runner, contacted_at: DateTime.now - 6.days) } let(:runner) { create(:ci_runner, contacted_at: DateTime.now - 6.days) }
let(:expected_contacted_at) { I18n.localize(runner.contacted_at, format: "%b %d, %Y") } let(:expected_contacted_at) { I18n.l(runner.contacted_at, format: "%b %d, %Y") }
it { is_expected.to have_content("Last contact #{expected_contacted_at}") } it { is_expected.to have_content("Last contact #{expected_contacted_at}") }
end end