Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f8b2dfce12
commit
ea0085de54
87 changed files with 605 additions and 721 deletions
|
@ -44,7 +44,7 @@ docs-lint markdown:
|
|||
- .default-retry
|
||||
- .docs:rules:docs-lint
|
||||
# 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
|
||||
needs: []
|
||||
script:
|
||||
|
@ -54,7 +54,7 @@ docs-lint links:
|
|||
extends:
|
||||
- .default-retry
|
||||
- .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
|
||||
needs: []
|
||||
script:
|
||||
|
|
|
@ -1366,7 +1366,6 @@ RSpec/AnyInstanceOf:
|
|||
- 'spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb'
|
||||
- 'spec/views/layouts/_head.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/build_coverage_worker_spec.rb'
|
||||
- 'spec/workers/build_hooks_worker_spec.rb'
|
||||
|
|
|
@ -600,7 +600,6 @@ Rails/ShortI18n:
|
|||
Exclude:
|
||||
- 'app/models/project_services/chat_message/pipeline_message.rb'
|
||||
- 'app/uploaders/content_type_whitelist.rb'
|
||||
- 'spec/views/shared/runners/show.html.haml_spec.rb'
|
||||
|
||||
# Offense count: 1144
|
||||
# Configuration parameters: ForbiddenMethods, AllowedMethods.
|
||||
|
|
|
@ -2,6 +2,13 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
### Security (5 changes)
|
||||
|
|
|
@ -1 +1 @@
|
|||
8128ec05cf75d8af4f0b4e422106cef4adf9b3a4
|
||||
46f08adbf4930f6a9c56f37ef5e4106c5b50810f
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
|
||||
import { GlSprintf, GlButton, GlAlert, GlCard } from '@gitlab/ui';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
|
@ -34,7 +34,7 @@ export default {
|
|||
recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME,
|
||||
i18n,
|
||||
mousetrap: null,
|
||||
components: { GlSprintf, GlButton, GlAlert, ClipboardButton },
|
||||
components: { GlSprintf, GlButton, GlAlert, ClipboardButton, GlCard },
|
||||
mixins: [Tracking.mixin()],
|
||||
props: {
|
||||
codes: {
|
||||
|
@ -116,8 +116,8 @@ export default {
|
|||
</gl-sprintf>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="codes-to-print gl-my-5 gl-p-5 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base"
|
||||
<gl-card
|
||||
class="codes-to-print gl-my-5"
|
||||
data-testid="recovery-codes"
|
||||
data-qa-selector="codes_content"
|
||||
>
|
||||
|
@ -126,7 +126,7 @@ export default {
|
|||
<span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</gl-card>
|
||||
<div class="gl-my-n2 gl-mx-n2 gl-display-flex gl-flex-wrap">
|
||||
<div class="gl-p-2">
|
||||
<clipboard-button
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
const chartsToShow = ['pipelines'];
|
||||
|
||||
if (this.shouldRenderDoraCharts) {
|
||||
chartsToShow.push('deployments', 'lead-time');
|
||||
chartsToShow.push('deployment-frequency', 'lead-time');
|
||||
}
|
||||
|
||||
return chartsToShow;
|
||||
|
@ -62,10 +62,10 @@ export default {
|
|||
<pipeline-charts />
|
||||
</gl-tab>
|
||||
<template v-if="shouldRenderDoraCharts">
|
||||
<gl-tab :title="__('Deployments')">
|
||||
<gl-tab :title="__('Deployment frequency')">
|
||||
<deployment-frequency-charts />
|
||||
</gl-tab>
|
||||
<gl-tab :title="__('Lead Time')">
|
||||
<gl-tab :title="__('Lead time')">
|
||||
<lead-time-charts />
|
||||
</gl-tab>
|
||||
</template>
|
||||
|
|
|
@ -74,6 +74,21 @@ export default {
|
|||
// we need to show the "create from" input.
|
||||
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: {
|
||||
tagName: {
|
||||
|
@ -111,7 +126,7 @@ export default {
|
|||
>
|
||||
<template #footer="{ isLoading, matches, query }">
|
||||
<gl-dropdown-item
|
||||
v-if="!isLoading && matches && matches.tags.totalCount === 0"
|
||||
v-if="shouldShowCreateTagOption(isLoading, matches, query)"
|
||||
is-check-item
|
||||
:is-checked="tagName === query"
|
||||
@click="createTagClicked(query)"
|
||||
|
|
|
@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
|
|||
import { s__, sprintf } from '~/locale';
|
||||
import { componentNames } from '~/reports/components/issue_body';
|
||||
import ReportSection from '~/reports/components/report_section.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import createStore from './store';
|
||||
|
||||
export default {
|
||||
|
@ -12,26 +11,12 @@ export default {
|
|||
components: {
|
||||
ReportSection,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
headPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headBlobPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
baseBlobPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
codequalityReportsPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -55,9 +40,6 @@ export default {
|
|||
created() {
|
||||
this.setPaths({
|
||||
basePath: this.basePath,
|
||||
headPath: this.headPath,
|
||||
baseBlobPath: this.baseBlobPath,
|
||||
headBlobPath: this.headBlobPath,
|
||||
reportsPath: this.codequalityReportsPath,
|
||||
helpPath: this.codequalityHelpPath,
|
||||
});
|
||||
|
|
|
@ -1,34 +1,23 @@
|
|||
import axios from '~/lib/utils/axios_utils';
|
||||
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 fetchReports = ({ state, dispatch, commit }, diffFeatureFlagEnabled) => {
|
||||
export const fetchReports = ({ state, dispatch, commit }) => {
|
||||
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) {
|
||||
return dispatch('receiveReportsError');
|
||||
}
|
||||
return Promise.all([axios.get(state.headPath), axios.get(state.basePath)])
|
||||
.then((results) =>
|
||||
doCodeClimateComparison(
|
||||
parseCodeclimateMetrics(results[0].data, state.headBlobPath),
|
||||
parseCodeclimateMetrics(results[1].data, state.baseBlobPath),
|
||||
),
|
||||
)
|
||||
.then((data) => dispatch('receiveReportsSuccess', data))
|
||||
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));
|
||||
};
|
||||
|
||||
|
|
|
@ -3,9 +3,6 @@ import * as types from './mutation_types';
|
|||
export default {
|
||||
[types.SET_PATHS](state, paths) {
|
||||
state.basePath = paths.basePath;
|
||||
state.headPath = paths.headPath;
|
||||
state.baseBlobPath = paths.baseBlobPath;
|
||||
state.headBlobPath = paths.headBlobPath;
|
||||
state.reportsPath = paths.reportsPath;
|
||||
state.helpPath = paths.helpPath;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import CodeQualityComparisonWorker from '../../workers/codequality_comparison_worker';
|
||||
|
||||
export const parseCodeclimateMetrics = (issues = [], path = '') => {
|
||||
return issues.map((issue) => {
|
||||
const parsedIssue = {
|
||||
|
@ -27,17 +25,3 @@ export const parseCodeclimateMetrics = (issues = [], path = '') => {
|
|||
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,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
});
|
|
@ -460,9 +460,6 @@ export default {
|
|||
<grouped-codequality-reports-app
|
||||
v-if="shouldRenderCodeQuality"
|
||||
: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-help-path="mr.codequalityHelpPath"
|
||||
/>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
|
||||
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
|
||||
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
|
||||
|
@ -18,6 +18,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapState(['showDropdownContentsCreateView']),
|
||||
...mapGetters(['isDropdownVariantSidebar']),
|
||||
dropdownContentsView() {
|
||||
if (this.showDropdownContentsCreateView) {
|
||||
return 'dropdown-contents-create-view';
|
||||
|
@ -25,11 +26,8 @@ export default {
|
|||
return 'dropdown-contents-labels-view';
|
||||
},
|
||||
directionStyle() {
|
||||
if (this.renderOnTop) {
|
||||
return { bottom: '100%' };
|
||||
}
|
||||
|
||||
return {};
|
||||
const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
|
||||
return this.renderOnTop ? { bottom } : {};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -37,7 +35,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<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"
|
||||
:style="directionStyle"
|
||||
>
|
||||
|
|
|
@ -83,12 +83,13 @@ export default {
|
|||
const highlightedLabel = this.$refs.labelsListContainer.querySelector('.is-focused');
|
||||
|
||||
if (highlightedLabel) {
|
||||
const rect = highlightedLabel.getBoundingClientRect();
|
||||
if (rect.bottom > this.$refs.labelsListContainer.clientHeight) {
|
||||
highlightedLabel.scrollIntoView(false);
|
||||
}
|
||||
if (rect.top < 0) {
|
||||
highlightedLabel.scrollIntoView();
|
||||
const container = this.$refs.labelsListContainer.getBoundingClientRect();
|
||||
const label = highlightedLabel.getBoundingClientRect();
|
||||
|
||||
if (label.bottom > container.bottom) {
|
||||
this.$refs.labelsListContainer.scrollTop += label.bottom - container.bottom;
|
||||
} else if (label.top < container.top) {
|
||||
this.$refs.labelsListContainer.scrollTop -= container.top - label.top;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@ export default {
|
|||
const { label, highlight, isLabelSet } = props;
|
||||
|
||||
const labelColorBox = h('span', {
|
||||
class: 'dropdown-label-box',
|
||||
class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
|
||||
style: {
|
||||
backgroundColor: label.color,
|
||||
},
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
|
||||
const checkedIcon = h(GlIcon, {
|
||||
class: {
|
||||
'mr-2 align-self-center': true,
|
||||
'gl-mr-3 gl-flex-shrink-0': true,
|
||||
hidden: !isLabelSet,
|
||||
},
|
||||
props: {
|
||||
|
@ -43,7 +43,7 @@ export default {
|
|||
|
||||
const noIcon = h('span', {
|
||||
class: {
|
||||
'mr-3 pr-2': true,
|
||||
'gl-mr-5 gl-pr-3': true,
|
||||
hidden: isLabelSet,
|
||||
},
|
||||
attrs: {
|
||||
|
@ -56,7 +56,7 @@ export default {
|
|||
const labelLink = h(
|
||||
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: {
|
||||
click: () => {
|
||||
listeners.clickLabel(label);
|
||||
|
@ -70,8 +70,8 @@ export default {
|
|||
'li',
|
||||
{
|
||||
class: {
|
||||
'd-block': true,
|
||||
'text-left': true,
|
||||
'gl-display-block': true,
|
||||
'gl-text-left': true,
|
||||
'is-focused': highlight,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -268,7 +268,7 @@ export default {
|
|||
this.$emit('toggleCollapse');
|
||||
},
|
||||
setContentIsOnViewport(showDropdownContents) {
|
||||
if (!this.isDropdownVariantEmbedded || !showDropdownContents) {
|
||||
if (!showDropdownContents) {
|
||||
this.contentIsOnViewport = true;
|
||||
|
||||
return;
|
||||
|
@ -276,8 +276,7 @@ export default {
|
|||
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.dropdownContents) {
|
||||
const offset = { top: 100 };
|
||||
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el, offset);
|
||||
this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -313,6 +312,7 @@ export default {
|
|||
<dropdown-contents
|
||||
v-show="dropdownButtonVisible && showDropdownContents"
|
||||
ref="dropdownContents"
|
||||
:render-on-top="!contentIsOnViewport"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
|
||||
|
|
|
@ -37,10 +37,6 @@
|
|||
|
||||
.file-title {
|
||||
@include gl-font-monospace;
|
||||
line-height: 35px;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.editor-ref {
|
||||
|
@ -69,19 +65,15 @@
|
|||
}
|
||||
|
||||
.file-buttons {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.soft-wrap-toggle {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
font-family: $regular-font;
|
||||
margin: 0 $btn-side-margin;
|
||||
margin-left: $gl-padding-8;
|
||||
|
||||
.soft-wrap {
|
||||
display: block;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
|
@ -94,7 +86,7 @@
|
|||
}
|
||||
|
||||
.no-wrap {
|
||||
display: block;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,17 +103,21 @@
|
|||
.new-file-path {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
margin-top: $gl-padding-8;
|
||||
}
|
||||
|
||||
.file-buttons {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.md-header-toolbar {
|
||||
margin: $gl-padding 0;
|
||||
}
|
||||
|
||||
.soft-wrap-toggle {
|
||||
width: 100%;
|
||||
margin: 3px 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media(max-width: map-get($grid-breakpoints, md)-1) {
|
||||
|
|
|
@ -35,6 +35,6 @@ module RequiresWhitelistedMonitoringClient
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -10,7 +10,6 @@ class Groups::RunnersController < Groups::ApplicationController
|
|||
feature_category :continuous_integration
|
||||
|
||||
def show
|
||||
render 'shared/runners/show'
|
||||
end
|
||||
|
||||
def edit
|
||||
|
|
|
@ -32,6 +32,7 @@ class Projects::HooksController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def edit
|
||||
redirect_to(action: :index) unless hook
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
|
@ -108,10 +108,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
params[:issue] ||= ActionController::Parameters.new(
|
||||
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],
|
||||
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)
|
||||
|
||||
|
@ -128,7 +128,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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],
|
||||
discussion_to_resolve: params[:discussion_to_resolve]
|
||||
)
|
||||
|
@ -314,17 +314,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
task_num
|
||||
lock_version
|
||||
discussion_locked
|
||||
] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
|
||||
end
|
||||
|
||||
def issue_create_params
|
||||
create_params = %i[
|
||||
issue_type
|
||||
]
|
||||
|
||||
params.require(:issue).permit(
|
||||
*create_params
|
||||
).merge(issue_params)
|
||||
] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
|
||||
end
|
||||
|
||||
def reorder_params
|
||||
|
|
|
@ -48,7 +48,6 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
render 'shared/runners/show'
|
||||
end
|
||||
|
||||
def toggle_shared_runners
|
||||
|
|
|
@ -26,6 +26,8 @@ module GroupsHelper
|
|||
applications#show
|
||||
applications#edit
|
||||
packages_and_registries#index
|
||||
groups/runners#show
|
||||
groups/runners#edit
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -723,6 +723,8 @@ module ProjectsHelper
|
|||
badges#index
|
||||
pages#show
|
||||
packages_and_registries#index
|
||||
projects/runners#show
|
||||
projects/runners#edit
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class WebexTeamsService < ChatNotificationService
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
def title
|
||||
'Webex Teams'
|
||||
s_("WebexTeamsService|Webex Teams")
|
||||
end
|
||||
|
||||
def description
|
||||
'Receive event notifications in Webex Teams'
|
||||
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
|
@ -14,13 +16,8 @@ class WebexTeamsService < ChatNotificationService
|
|||
end
|
||||
|
||||
def help
|
||||
'This service sends notifications about projects events to a Webex Teams conversation.<br />
|
||||
To set up this service:
|
||||
<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>'
|
||||
docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
|
||||
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
|
||||
end
|
||||
|
||||
def event_field(event)
|
||||
|
@ -36,7 +33,7 @@ class WebexTeamsService < ChatNotificationService
|
|||
|
||||
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: 'select', name: 'branches_to_be_notified', choices: branch_choices }
|
||||
]
|
||||
|
|
|
@ -24,10 +24,11 @@ module Issues
|
|||
def filter_params(issue)
|
||||
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
|
||||
unless can_admin_issuable?(issue)
|
||||
params.delete(:confidential)
|
||||
params.delete(:issue_type)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
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 },
|
||||
} }
|
||||
= 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
|
||||
= s_('AdminArea|Users')
|
||||
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
|
||||
= render_if_exists 'admin/health_check/health_check_url'
|
||||
%hr
|
||||
.card
|
||||
.card-header
|
||||
.gl-card
|
||||
.gl-card-header
|
||||
Current Status:
|
||||
- if no_errors
|
||||
= sprite_icon('check', css_class: 'cgreen')
|
||||
|
@ -32,7 +32,7 @@
|
|||
- else
|
||||
= sprite_icon('warning-solid', css_class: 'cred')
|
||||
#{ s_('HealthCheck|Unhealthy') }
|
||||
.card-body
|
||||
.gl-card-body
|
||||
- if no_errors
|
||||
#{ s_('HealthCheck|No Health Problems Detected') }
|
||||
- else
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
= _('Recent Deliveries')
|
||||
%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
|
||||
- if hook_logs.any?
|
||||
- if hook_logs.present?
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
- 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
|
||||
- breadcrumb_title page_title
|
||||
|
||||
- if Feature.enabled?(:runner_detailed_view_vue_ui, current_user, default_enabled: :yaml)
|
||||
#js-runner-detail{ data: {runner_id: @runner.id} }
|
||||
|
|
|
@ -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
|
||||
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
|
||||
|
|
3
app/views/groups/runners/show.html.haml
Normal file
3
app/views/groups/runners/show.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- add_to_breadcrumbs _('CI/CD Settings'), group_settings_ci_cd_path(@group)
|
||||
|
||||
= render 'shared/runners/runner_details', runner: @runner
|
|
@ -92,7 +92,7 @@
|
|||
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do
|
||||
%span.gl-sr-only
|
||||
= s_('Nav|Help')
|
||||
= sprite_icon('question')
|
||||
= sprite_icon('question-o')
|
||||
%span.notification-dot.rounded-circle.gl-absolute
|
||||
= sprite_icon('chevron-down', css_class: 'caret-down')
|
||||
.dropdown-menu.dropdown-menu-right
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
%span
|
||||
= _('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
|
||||
%span
|
||||
= _('CI/CD')
|
||||
|
|
|
@ -193,7 +193,7 @@
|
|||
%span
|
||||
= _('Repository')
|
||||
- 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
|
||||
%span
|
||||
= _('CI/CD')
|
||||
|
|
|
@ -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'
|
||||
|
||||
= 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'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
|
||||
|
||||
.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 }
|
||||
= sprite_icon('fork', size: 12)
|
||||
= ref
|
||||
|
@ -26,16 +26,18 @@
|
|||
dismiss_key: @project.id,
|
||||
human_access: human_access } }
|
||||
|
||||
.file-buttons
|
||||
.file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
|
||||
- if is_markdown
|
||||
= render 'shared/blob/markdown_buttons', show_fullscreen_button: false
|
||||
= button_tag class: 'soft-wrap-toggle btn gl-button', type: 'button', tabindex: '-1' do
|
||||
%span.no-wrap
|
||||
= custom_icon('icon_no_wrap')
|
||||
No wrap
|
||||
%span.soft-wrap
|
||||
= custom_icon('icon_soft_wrap')
|
||||
Soft wrap
|
||||
= button_tag class: 'soft-wrap-toggle btn gl-button btn-default', type: 'button', tabindex: '-1' do
|
||||
.no-wrap
|
||||
= sprite_icon('soft-unwrap', css_class: 'gl-button-icon')
|
||||
%span.gl-button-text
|
||||
No wrap
|
||||
.soft-wrap
|
||||
= sprite_icon('soft-wrap', css_class: 'gl-button-icon')
|
||||
%span.gl-button-text
|
||||
Soft wrap
|
||||
|
||||
.file-editor.code
|
||||
.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
Recent Deliveries
|
||||
%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
|
||||
- if hook_logs.any?
|
||||
- if hook_logs.present?
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
@ -51,11 +51,12 @@
|
|||
= 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' }
|
||||
.card.card-slim.m-4.p-4
|
||||
%div
|
||||
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
|
||||
- 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 }
|
||||
.gl-card.gl-my-5
|
||||
.gl-card-body
|
||||
%div
|
||||
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
|
||||
- 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|
|
||||
.project-template
|
||||
.form-group
|
||||
|
|
|
@ -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
|
||||
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
|
||||
|
|
3
app/views/projects/runners/show.html.haml
Normal file
3
app/views/projects/runners/show.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- add_to_breadcrumbs _('CI/CD Settings'), project_settings_ci_cd_path(@project)
|
||||
|
||||
= render 'shared/runners/runner_details', runner: @runner
|
|
@ -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
|
||||
= s_('Runners|Runner #%{runner_id}' % { runner_id: @runner.id })
|
||||
= render 'shared/runners/runner_type_badge', runner: @runner
|
||||
= s_('Runners|Runner #%{runner_id}' % { runner_id: runner.id })
|
||||
= render 'shared/runners/runner_type_badge', runner: runner
|
||||
|
||||
.table-holder
|
||||
%table.table
|
||||
|
@ -12,51 +13,51 @@
|
|||
%th= s_('Runners|Value')
|
||||
%tr
|
||||
%td= s_('Runners|Active')
|
||||
%td= @runner.active? ? _('Yes') : _('No')
|
||||
%td= runner.active? ? _('Yes') : _('No')
|
||||
%tr
|
||||
%td= s_('Runners|Protected')
|
||||
%td= @runner.ref_protected? ? _('Yes') : _('No')
|
||||
%td= runner.ref_protected? ? _('Yes') : _('No')
|
||||
%tr
|
||||
%td= s_('Runners|Can run untagged jobs')
|
||||
%td= @runner.run_untagged? ? _('Yes') : _('No')
|
||||
- unless @runner.group_type?
|
||||
%td= runner.run_untagged? ? _('Yes') : _('No')
|
||||
- unless runner.group_type?
|
||||
%tr
|
||||
%td= s_('Runners|Locked to this project')
|
||||
%td= @runner.locked? ? _('Yes') : _('No')
|
||||
%td= runner.locked? ? _('Yes') : _('No')
|
||||
%tr
|
||||
%td= s_('Runners|Tags')
|
||||
%td
|
||||
- @runner.tag_list.sort.each do |tag|
|
||||
- runner.tag_list.sort.each do |tag|
|
||||
%span.badge.badge-primary
|
||||
= tag
|
||||
%tr
|
||||
%td= s_('Runners|Name')
|
||||
%td= @runner.name
|
||||
%td= runner.name
|
||||
%tr
|
||||
%td= s_('Runners|Version')
|
||||
%td= @runner.version
|
||||
%td= runner.version
|
||||
%tr
|
||||
%td= s_('Runners|IP Address')
|
||||
%td= @runner.ip_address
|
||||
%td= runner.ip_address
|
||||
%tr
|
||||
%td= s_('Runners|Revision')
|
||||
%td= @runner.revision
|
||||
%td= runner.revision
|
||||
%tr
|
||||
%td= s_('Runners|Platform')
|
||||
%td= @runner.platform
|
||||
%td= runner.platform
|
||||
%tr
|
||||
%td= s_('Runners|Architecture')
|
||||
%td= @runner.architecture
|
||||
%td= runner.architecture
|
||||
%tr
|
||||
%td= s_('Runners|Description')
|
||||
%td= @runner.description
|
||||
%td= runner.description
|
||||
%tr
|
||||
%td= s_('Runners|Maximum job timeout')
|
||||
%td= @runner.maximum_timeout_human_readable
|
||||
%td= runner.maximum_timeout_human_readable
|
||||
%tr
|
||||
%td= s_('Runners|Last contact')
|
||||
%td
|
||||
- if @runner.contacted_at
|
||||
= time_ago_with_tooltip @runner.contacted_at
|
||||
- if runner.contacted_at
|
||||
= time_ago_with_tooltip runner.contacted_at
|
||||
- else
|
||||
= s_('Never')
|
|
@ -5,7 +5,7 @@
|
|||
%li{ class: active_when(params[:scope].nil?) }
|
||||
= link_to subject_snippets_path(subject) do
|
||||
= _("All")
|
||||
%span.badge.badge-pill
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm
|
||||
- if include_private
|
||||
= counts[:total]
|
||||
- else
|
||||
|
@ -15,17 +15,17 @@
|
|||
%li{ class: active_when(params[:scope] == "are_private") }
|
||||
= link_to subject_snippets_path(subject, scope: 'are_private') do
|
||||
= _("Private")
|
||||
%span.badge.badge-pill
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= counts[:are_private]
|
||||
|
||||
%li{ class: active_when(params[:scope] == "are_internal") }
|
||||
= link_to subject_snippets_path(subject, scope: 'are_internal') do
|
||||
= _("Internal")
|
||||
%span.badge.badge-pill
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= counts[:are_internal]
|
||||
|
||||
%li{ class: active_when(params[:scope] == "are_public") }
|
||||
= link_to subject_snippets_path(subject, scope: 'are_public') do
|
||||
= _("Public")
|
||||
%span.badge.badge-pill
|
||||
%span.badge.badge-muted.badge-pill.gl-badge.sm
|
||||
= counts[:are_public]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update edit file buttons and spacing
|
||||
merge_request: 60318
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Ensure we never error in web hook logs
|
||||
merge_request: 60408
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix breadcrumbs and navigation in runner details pages
|
||||
merge_request: 60129
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refactor Webex Teams integration settings text
|
||||
merge_request: 60565
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix Instance-level Project Integration Management page for GitLab FOSS
|
||||
merge_request: 60354
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/gl-badge-snippets.yml
Normal file
5
changelogs/unreleased/gl-badge-snippets.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add gl-badge for badges in snippets nav
|
||||
merge_request: 57966
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix tag matching behavior on New Release page
|
||||
merge_request: 60035
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/sy-change-issue-type.yml
Normal file
5
changelogs/unreleased/sy-change-issue-type.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add internal API support for updating issue types on issues
|
||||
merge_request: 60173
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/yogi-gl-card-billing.yml
Normal file
5
changelogs/unreleased/yogi-gl-card-billing.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move card in billing page to gl-card utility class
|
||||
merge_request: 59138
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
5
changelogs/unreleased/yogi-gl-card-recovery-codes.yml
Normal file
5
changelogs/unreleased/yogi-gl-card-recovery-codes.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move 2fa recovery codes to GlCard component
|
||||
merge_request: 59219
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
5
changelogs/unreleased/yogi-health-check-gl-card.yml
Normal file
5
changelogs/unreleased/yogi-health-check-gl-card.yml
Normal 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
|
5
changelogs/unreleased/yogi-hide-templates-buttons.yml
Normal file
5
changelogs/unreleased/yogi-hide-templates-buttons.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove template tabs in new project page for GitLab.com
|
||||
merge_request: 59083
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
5
changelogs/unreleased/yogi-question-o-admin.yml
Normal file
5
changelogs/unreleased/yogi-question-o-admin.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update to question-o and change color in admin users
|
||||
merge_request: 59133
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
5
changelogs/unreleased/yogi-question-o-navbar.yml
Normal file
5
changelogs/unreleased/yogi-question-o-navbar.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update to question-o from question icon in navbar
|
||||
merge_request: 59134
|
||||
author: Yogi (@yo)
|
||||
type: changed
|
|
@ -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
|
||||
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.
|
||||
- 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.
|
||||
|
@ -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.
|
||||
|
||||
While the default directory puts the cache files in the same
|
||||
filesystem as your repository data, this is not requirement. You can
|
||||
put the cache files on a different filesystem if that works better for
|
||||
file system as your repository data, this is not requirement. You can
|
||||
put the cache files on a different file system if that works better for
|
||||
your infrastructure.
|
||||
|
||||
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|
|
||||
|:---|:---|:---|:---|
|
||||
|`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_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|
|
||||
|
|
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 |
|
@ -770,6 +770,29 @@ argument :title, GraphQL::STRING_TYPE,
|
|||
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
|
||||
|
||||
See: [GraphQL Authorization](graphql_guide/authorization.md)
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 31 KiB |
|
@ -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` | 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. |
|
||||
| `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 | |
|
||||
|
@ -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.
|
||||
|
||||
The **Analytics > CI/CD Analytics** page shows information about the deployment frequency to the
|
||||
`production` environment. The environment **must** be named `production` for its deployment
|
||||
information to appear on the graphs.
|
||||
The **Analytics > CI/CD Analytics** page shows information about the deployment
|
||||
frequency to the `production` environment. The environment must be part of the
|
||||
[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)
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 80 KiB |
|
@ -6,25 +6,32 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# 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
|
||||
|
||||
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. Click **ADD**.
|
||||
1. Select **ADD**.
|
||||
1. Copy the **Webhook URL**.
|
||||
|
||||
## 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. Ensure that the **Active** toggle is enabled.
|
||||
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. 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 |
|
@ -765,7 +765,7 @@ module API
|
|||
required: true,
|
||||
name: :webhook,
|
||||
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
|
||||
].flatten
|
||||
|
|
|
@ -9956,10 +9956,7 @@ msgstr ""
|
|||
msgid "DORA4Metrics|Days from merge to deploy"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Deployments"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Deployments charts"
|
||||
msgid "DORA4Metrics|Deployment frequency"
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4Metrics|Lead time"
|
||||
|
@ -9971,19 +9968,19 @@ msgstr ""
|
|||
msgid "DORA4Metrics|No merge requests were deployed during this period"
|
||||
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 ""
|
||||
|
||||
msgid "DORA4Metrics|Something went wrong while getting lead time data."
|
||||
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 ""
|
||||
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "DORA4|Lead time charts"
|
||||
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 "Dashboard"
|
||||
|
@ -10990,6 +10987,9 @@ msgstr ""
|
|||
msgid "Deployment Frequency"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployment frequency"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deployments"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18953,6 +18953,9 @@ msgstr ""
|
|||
msgid "Lead Time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Lead time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn CI/CD syntax"
|
||||
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."
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ function run_locally_or_in_docker() {
|
|||
$cmd $args
|
||||
elif hash docker 2>/dev/null
|
||||
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
|
||||
echo
|
||||
echo " ✖ ERROR: '${cmd}' not found. Install '${cmd}' or Docker to proceed." >&2
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::HooksController do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let(:user) { project.owner }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
@ -20,6 +20,56 @@ RSpec.describe Projects::HooksController do
|
|||
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
|
||||
it 'sets all parameters' do
|
||||
hook_params = {
|
||||
|
|
|
@ -586,13 +586,15 @@ RSpec.describe Projects::IssuesController do
|
|||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:issue_params) { { title: 'New title' } }
|
||||
|
||||
subject do
|
||||
put :update,
|
||||
params: {
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: issue.to_param,
|
||||
issue: { title: 'New title' }
|
||||
issue: issue_params
|
||||
},
|
||||
format: :json
|
||||
end
|
||||
|
@ -614,6 +616,17 @@ RSpec.describe Projects::IssuesController do
|
|||
expect(issue.reload.title).to eq('New title')
|
||||
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
|
||||
before do
|
||||
stub_application_setting(recaptcha_enabled: true)
|
||||
|
|
|
@ -40,7 +40,7 @@ RSpec.describe 'List issue resource label events', :js do
|
|||
|
||||
labels.each { |label| click_link label }
|
||||
|
||||
click_on 'Edit'
|
||||
send_keys(:escape)
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,8 +54,8 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
expect(findGlTabs().exists()).toBe(true);
|
||||
|
||||
expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines');
|
||||
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployments');
|
||||
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead Time');
|
||||
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
|
||||
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
|
||||
});
|
||||
|
||||
it('renders the pipeline charts', () => {
|
||||
|
@ -75,7 +75,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
|
||||
|
||||
mergeUrlParams.mockImplementation(({ chart }, path) => {
|
||||
expect(chart).toBe('deployments');
|
||||
expect(chart).toBe('deployment-frequency');
|
||||
expect(path).toBe(window.location.pathname);
|
||||
chartsPath = `${path}?chart=${chart}`;
|
||||
return chartsPath;
|
||||
|
@ -114,12 +114,12 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
|
||||
describe('when provided with a query param', () => {
|
||||
it.each`
|
||||
chart | tab
|
||||
${'lead-time'} | ${'2'}
|
||||
${'deployments'} | ${'1'}
|
||||
${'pipelines'} | ${'0'}
|
||||
${'fake'} | ${'0'}
|
||||
${''} | ${'0'}
|
||||
chart | tab
|
||||
${'lead-time'} | ${'2'}
|
||||
${'deployment-frequency'} | ${'1'}
|
||||
${'pipelines'} | ${'0'}
|
||||
${'fake'} | ${'0'}
|
||||
${''} | ${'0'}
|
||||
`('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
|
||||
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
|
||||
getParameterValues.mockImplementation((name) => {
|
||||
|
@ -152,7 +152,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
|
||||
getParameterValues.mockImplementationOnce((name) => {
|
||||
expect(name).toBe('chart');
|
||||
return ['deployments'];
|
||||
return ['deployment-frequency'];
|
||||
});
|
||||
|
||||
popstateHandler();
|
||||
|
|
|
@ -10,29 +10,35 @@ const TEST_PROJECT_ID = '1234';
|
|||
const TEST_CREATE_FROM = 'test-create-from';
|
||||
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', () => {
|
||||
let store;
|
||||
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, {
|
||||
store,
|
||||
stubs: {
|
||||
|
@ -84,8 +90,6 @@ describe('releases/components/tag_field_new', () => {
|
|||
describe('when the user selects a new tag name', () => {
|
||||
beforeEach(async () => {
|
||||
findCreateNewTagOption().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it("updates the store's release.tagName property", () => {
|
||||
|
@ -102,8 +106,6 @@ describe('releases/components/tag_field_new', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
findTagNameDropdown().vm.$emit('input', updatedTagName);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(mount);
|
||||
|
@ -176,8 +200,6 @@ describe('releases/components/tag_field_new', () => {
|
|||
const updatedCreateFrom = 'update-create-from';
|
||||
findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(store.state.editNew.createFrom).toBe(updatedCreateFrom);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import Vuex from 'vuex';
|
|||
import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
|
||||
import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
|
||||
import { getStoreConfig } from '~/reports/codequality_report/store';
|
||||
import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
|
||||
import { parsedReportIssues } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -80,7 +80,7 @@ describe('Grouped code quality reports app', () => {
|
|||
describe('with issues', () => {
|
||||
describe('with new issues', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
|
||||
mockStore.state.newIssues = parsedReportIssues.newIssues;
|
||||
mockStore.state.resolvedIssues = [];
|
||||
});
|
||||
|
||||
|
@ -89,14 +89,14 @@ describe('Grouped code quality reports app', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [];
|
||||
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
|
||||
mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
|
||||
});
|
||||
|
||||
it('renders summary text', () => {
|
||||
|
@ -104,14 +104,14 @@ describe('Grouped code quality reports app', () => {
|
|||
});
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.newIssues = [mockParsedHeadIssues[0]];
|
||||
mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
|
||||
mockStore.state.newIssues = parsedReportIssues.newIssues;
|
||||
mockStore.state.resolvedIssues = parsedReportIssues.resolvedIssues;
|
||||
});
|
||||
|
||||
it('renders summary text', () => {
|
||||
|
@ -121,7 +121,7 @@ describe('Grouped code quality reports app', () => {
|
|||
});
|
||||
|
||||
it('renders custom codequality issue body', () => {
|
||||
expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
|
||||
expect(findIssueBody().props('issue')).toEqual(parsedReportIssues.newIssues[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = {
|
||||
status: 'failed',
|
||||
new_errors: [
|
||||
|
|
|
@ -5,30 +5,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import createStore from '~/reports/codequality_report/store';
|
||||
import * as actions from '~/reports/codequality_report/store/actions';
|
||||
import * as types from '~/reports/codequality_report/store/mutation_types';
|
||||
import {
|
||||
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]],
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
import { reportIssues, parsedReportIssues } from '../mock_data';
|
||||
|
||||
describe('Codequality Reports actions', () => {
|
||||
let localState;
|
||||
|
@ -43,9 +20,6 @@ describe('Codequality Reports actions', () => {
|
|||
it('should commit SET_PATHS mutation', (done) => {
|
||||
const paths = {
|
||||
basePath: 'basePath',
|
||||
headPath: 'headPath',
|
||||
baseBlobPath: 'baseBlobPath',
|
||||
headBlobPath: 'headBlobPath',
|
||||
reportsPath: 'reportsPath',
|
||||
helpPath: 'codequalityHelpPath',
|
||||
};
|
||||
|
@ -63,119 +37,64 @@ describe('Codequality Reports actions', () => {
|
|||
|
||||
describe('fetchReports', () => {
|
||||
let mock;
|
||||
let diffFeatureFlagEnabled;
|
||||
|
||||
describe('with codequalityBackendComparison feature flag enabled', () => {
|
||||
beforeEach(() => {
|
||||
diffFeatureFlagEnabled = true;
|
||||
localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
beforeEach(() => {
|
||||
localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
|
||||
localState.basePath = '/base/path';
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('on success', () => {
|
||||
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
|
||||
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
|
||||
describe('on success', () => {
|
||||
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
|
||||
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
|
||||
|
||||
testAction(
|
||||
actions.fetchReports,
|
||||
diffFeatureFlagEnabled,
|
||||
localState,
|
||||
[{ type: types.REQUEST_REPORTS }],
|
||||
[
|
||||
{
|
||||
payload: parsedReportIssues,
|
||||
type: 'receiveReportsSuccess',
|
||||
},
|
||||
],
|
||||
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,
|
||||
);
|
||||
});
|
||||
testAction(
|
||||
actions.fetchReports,
|
||||
null,
|
||||
localState,
|
||||
[{ type: types.REQUEST_REPORTS }],
|
||||
[
|
||||
{
|
||||
payload: parsedReportIssues,
|
||||
type: 'receiveReportsSuccess',
|
||||
},
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with codequalityBackendComparison feature flag disabled', () => {
|
||||
beforeEach(() => {
|
||||
diffFeatureFlagEnabled = false;
|
||||
localState.headPath = `${TEST_HOST}/head.json`;
|
||||
localState.basePath = `${TEST_HOST}/base.json`;
|
||||
mock = new MockAdapter(axios);
|
||||
describe('on error', () => {
|
||||
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
|
||||
mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
|
||||
|
||||
testAction(
|
||||
actions.fetchReports,
|
||||
null,
|
||||
localState,
|
||||
[{ type: types.REQUEST_REPORTS }],
|
||||
[{ type: 'receiveReportsError', payload: expect.any(Error) }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
describe('with no base path', () => {
|
||||
it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
|
||||
localState.basePath = null;
|
||||
|
||||
describe('on success', () => {
|
||||
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
|
||||
mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
|
||||
mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
|
||||
|
||||
testAction(
|
||||
actions.fetchReports,
|
||||
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,
|
||||
);
|
||||
});
|
||||
testAction(
|
||||
actions.fetchReports,
|
||||
null,
|
||||
localState,
|
||||
[{ type: types.REQUEST_REPORTS }],
|
||||
[{ type: 'receiveReportsError' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,23 +13,17 @@ describe('Codequality Reports mutations', () => {
|
|||
describe('SET_PATHS', () => {
|
||||
it('sets paths to given values', () => {
|
||||
const basePath = 'base.json';
|
||||
const headPath = 'head.json';
|
||||
const baseBlobPath = 'base/blob/path/';
|
||||
const headBlobPath = 'head/blob/path/';
|
||||
const reportsPath = 'reports.json';
|
||||
const helpPath = 'help.html';
|
||||
|
||||
mutations.SET_PATHS(localState, {
|
||||
basePath,
|
||||
headPath,
|
||||
baseBlobPath,
|
||||
headBlobPath,
|
||||
reportsPath,
|
||||
helpPath,
|
||||
});
|
||||
|
||||
expect(localState.basePath).toEqual(basePath);
|
||||
expect(localState.headPath).toEqual(headPath);
|
||||
expect(localState.baseBlobPath).toEqual(baseBlobPath);
|
||||
expect(localState.headBlobPath).toEqual(headBlobPath);
|
||||
expect(localState.reportsPath).toEqual(reportsPath);
|
||||
expect(localState.helpPath).toEqual(helpPath);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
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 labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
|
||||
|
||||
import { mockConfig } from './mock_data';
|
||||
|
@ -50,13 +50,20 @@ describe('DropdownContent', () => {
|
|||
describe('template', () => {
|
||||
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('style')).toBe(undefined);
|
||||
expect(wrapper.attributes('style')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renders component container element with styles when `renderOnTop` is true', () => {
|
||||
wrapper = createComponent(mockConfig, { renderOnTop: true });
|
||||
describe('when `renderOnTop` is 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@ import Vuex from 'vuex';
|
|||
|
||||
import { isInViewport } from '~/lib/utils/common_utils';
|
||||
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 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';
|
||||
|
@ -190,40 +191,33 @@ describe('LabelsSelectRoot', () => {
|
|||
});
|
||||
|
||||
describe('sets content direction based on viewport', () => {
|
||||
it('does not set direction when `state.variant` is not "embedded"', async () => {
|
||||
createComponent();
|
||||
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContents');
|
||||
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
|
||||
await wrapper.vm.$nextTick;
|
||||
|
||||
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);
|
||||
describe.each(Object.values(DropdownVariant))(
|
||||
'when labels variant is "%s"',
|
||||
({ variant }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ ...mockConfig, variant });
|
||||
wrapper.vm.$store.dispatch('toggleDropdownContents');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set direction when inside of viewport', () => {
|
||||
isInViewport.mockImplementation(() => true);
|
||||
wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
|
||||
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(false);
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -282,7 +282,14 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
end
|
||||
|
||||
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.title).to eq 'New title'
|
||||
|
@ -293,6 +300,7 @@ RSpec.describe Issues::UpdateService, :mailer do
|
|||
expect(issue.due_date).to be_nil
|
||||
expect(issue.discussion_locked).to be_falsey
|
||||
expect(issue.confidential).to be_falsey
|
||||
expect(issue.issue_type).to eql('issue')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'shared/runners/show.html.haml' do
|
||||
RSpec.describe 'shared/runners/_runner_details.html.haml' do
|
||||
include PageLayoutHelper
|
||||
|
||||
let(:runner) do
|
||||
|
@ -14,7 +14,7 @@ RSpec.describe 'shared/runners/show.html.haml' do
|
|||
end
|
||||
|
||||
before do
|
||||
assign(:runner, runner)
|
||||
allow(view).to receive(:runner) { runner }
|
||||
end
|
||||
|
||||
subject do
|
||||
|
@ -24,7 +24,7 @@ RSpec.describe 'shared/runners/show.html.haml' do
|
|||
|
||||
describe 'Page title' 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
|
||||
|
||||
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
|
||||
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}") }
|
||||
end
|
Loading…
Reference in a new issue