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
- .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:

View file

@ -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'

View file

@ -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.

View file

@ -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)

View file

@ -1 +1 @@
8128ec05cf75d8af4f0b4e422106cef4adf9b3a4
46f08adbf4930f6a9c56f37ef5e4106c5b50810f

View file

@ -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

View file

@ -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>

View file

@ -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)"

View file

@ -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,
});

View file

@ -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));
};

View file

@ -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;
},

View file

@ -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,
});
});
};

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
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"
/>

View file

@ -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"
>

View file

@ -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;
}
}
},

View file

@ -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,
},
},

View file

@ -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">

View file

@ -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) {

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -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 }
]

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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} }

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
= 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
%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

View file

@ -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')

View file

@ -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')

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'
= 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'

View file

@ -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 } }<

View file

@ -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

View file

@ -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

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
= 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
= 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')

View file

@ -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]

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
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

View file

@ -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

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` | 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

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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

View file

@ -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 = {

View file

@ -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)

View file

@ -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

View file

@ -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();

View file

@ -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);
});
});

View file

@ -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]);
});
});
});

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 = {
status: 'failed',
new_errors: [

View file

@ -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,
);
});
});
});

View file

@ -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);
});
});

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 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);
});
});
});
});

View file

@ -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);
});
});
},
);
});
});

View file

@ -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

View file

@ -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