Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-12 12:09:15 +00:00
parent ede2fbdc87
commit d7fd035dc3
101 changed files with 1691 additions and 719 deletions

View File

@ -604,10 +604,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- ee/spec/services/incident_management/incidents/upload_metric_service_spec.rb
- ee/spec/services/incident_management/oncall_rotations/edit_service_spec.rb
- ee/spec/services/merge_request_approval_settings/update_service_spec.rb
- ee/spec/services/merge_trains/check_status_service_spec.rb
- ee/spec/services/merge_trains/create_pipeline_service_spec.rb
- ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb
- ee/spec/services/merge_trains/refresh_service_spec.rb
- ee/spec/services/personal_access_tokens/create_service_audit_log_spec.rb
- ee/spec/services/personal_access_tokens/groups/update_lifetime_service_spec.rb
- ee/spec/services/projects/after_rename_service_spec.rb
@ -779,9 +775,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/lib/gitlab/gitaly_client/operation_service_spec.rb
- spec/lib/gitlab/gl_repository/repo_type_spec.rb
- spec/lib/gitlab/group_search_results_spec.rb
- spec/lib/gitlab/hook_data/issue_builder_spec.rb
- spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
- spec/lib/gitlab/hook_data/release_builder_spec.rb
- spec/lib/gitlab/json_cache_spec.rb
- spec/lib/gitlab/language_detection_spec.rb
- spec/lib/gitlab/project_search_results_spec.rb
@ -1010,25 +1003,12 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/services/feature_flags/enable_service_spec.rb
- spec/services/feature_flags/update_service_spec.rb
- spec/services/git/branch_push_service_spec.rb
- spec/services/groups/auto_devops_service_spec.rb
- spec/services/groups/group_links/update_service_spec.rb
- spec/services/groups/transfer_service_spec.rb
- spec/services/groups/update_shared_runners_service_spec.rb
- spec/services/ide/base_config_service_spec.rb
- spec/services/ide/schemas_config_service_spec.rb
- spec/services/ide/terminal_config_service_spec.rb
- spec/services/import/bitbucket_server_service_spec.rb
- spec/services/incident_management/incidents/create_service_spec.rb
- spec/services/incident_management/pager_duty/create_incident_issue_service_spec.rb
- spec/services/incident_management/pager_duty/process_webhook_service_spec.rb
- spec/services/integrations/test/project_service_spec.rb
- spec/services/issuable/bulk_update_service_spec.rb
- spec/services/issues/build_service_spec.rb
- spec/services/issues/clone_service_spec.rb
- spec/services/issues/create_service_spec.rb
- spec/services/issues/export_csv_service_spec.rb
- spec/services/issues/move_service_spec.rb
- spec/services/issues/related_branches_service_spec.rb
- spec/services/jira_connect/sync_service_spec.rb
- spec/services/jira_import/start_import_service_spec.rb
- spec/services/jira_import/users_importer_spec.rb

View File

@ -4,13 +4,13 @@ import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import DivergenceGraph from './components/divergence_graph.vue';
export function createGraphVueApp(el, data, maxCommits) {
export function createGraphVueApp(el, data, maxCommits, defaultBranch) {
return new Vue({
el,
render(h) {
return h(DivergenceGraph, {
props: {
defaultBranch: 'master',
defaultBranch,
distance: data.distance ? parseInt(data.distance, 10) : null,
aheadCount: parseInt(data.ahead, 10),
behindCount: parseInt(data.behind, 10),
@ -21,7 +21,7 @@ export function createGraphVueApp(el, data, maxCommits) {
});
}
export default (endpoint) => {
export default (endpoint, defaultBranch) => {
const names = [...document.querySelectorAll('.js-branch-item')].map(
({ dataset }) => dataset.name,
);
@ -47,7 +47,7 @@ export default (endpoint) => {
if (!el) return;
createGraphVueApp(el, val, maxCommits);
createGraphVueApp(el, val, maxCommits, defaultBranch);
});
})
.catch(() =>

View File

@ -0,0 +1,16 @@
import Vue from 'vue';
import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
const mountDeleteLabelModal = (optionalProps) =>
new Vue({
render(h) {
return h(DeleteLabelModal, {
props: {
selector: '.js-delete-label-modal-button',
...optionalProps,
},
});
},
}).$mount();
export default (optionalProps = {}) => mountDeleteLabelModal(optionalProps);

View File

@ -39,11 +39,12 @@ export const removeSubscription = async (removePath) => {
});
};
export const fetchGroups = async (groupsPath, { page, perPage }) => {
export const fetchGroups = async (groupsPath, { page, perPage, search }) => {
return axios.get(groupsPath, {
params: {
page,
per_page: perPage,
search,
},
});
};

View File

@ -1,5 +1,5 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlPagination, GlAlert } from '@gitlab/ui';
import { GlLoadingIcon, GlPagination, GlAlert, GlSearchBoxByType } from '@gitlab/ui';
import { fetchGroups } from '~/jira_connect/api';
import { defaultPerPage } from '~/jira_connect/constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
@ -8,11 +8,10 @@ import GroupsListItem from './groups_list_item.vue';
export default {
components: {
GlTabs,
GlTab,
GlLoadingIcon,
GlPagination,
GlAlert,
GlSearchBoxByType,
GroupsListItem,
},
inject: {
@ -23,7 +22,8 @@ export default {
data() {
return {
groups: [],
isLoading: false,
isLoadingInitial: true,
isLoadingMore: false,
page: 1,
perPage: defaultPerPage,
totalItems: 0,
@ -31,15 +31,18 @@ export default {
};
},
mounted() {
this.loadGroups();
return this.loadGroups().finally(() => {
this.isLoadingInitial = false;
});
},
methods: {
loadGroups() {
this.isLoading = true;
loadGroups({ searchTerm } = {}) {
this.isLoadingMore = true;
fetchGroups(this.groupsPath, {
return fetchGroups(this.groupsPath, {
page: this.page,
perPage: this.perPage,
search: searchTerm,
})
.then((response) => {
const { page, total } = parseIntPagination(normalizeHeaders(response.headers));
@ -51,50 +54,61 @@ export default {
this.errorMessage = s__('Integrations|Failed to load namespaces. Please try again.');
})
.finally(() => {
this.isLoading = false;
this.isLoadingMore = false;
});
},
onGroupSearch(searchTerm) {
return this.loadGroups({ searchTerm });
},
},
};
</script>
<template>
<div>
<gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" @dismiss="errorMessage = null">
<gl-alert v-if="errorMessage" class="gl-mb-5" variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
<gl-tabs>
<gl-tab :title="__('Groups and subgroups')" class="gl-pt-3">
<gl-loading-icon v-if="isLoading" size="md" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
{{
s__('Integrations|You must have owner or maintainer permissions to link namespaces.')
}}
</p>
</div>
<ul v-else class="gl-list-style-none gl-pl-0">
<groups-list-item
v-for="group in groups"
:key="group.id"
:group="group"
@error="errorMessage = $event"
/>
</ul>
<gl-search-box-by-type
class="gl-mb-5"
debounce="500"
:placeholder="__('Search by name')"
:is-loading="isLoadingMore"
@input="onGroupSearch"
/>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
v-if="totalItems > perPage && groups.length > 0"
v-model="page"
class="gl-mb-0"
:per-page="perPage"
:total-items="totalItems"
@input="loadGroups"
/>
</div>
</gl-tab>
</gl-tabs>
<gl-loading-icon v-if="isLoadingInitial" size="md" />
<div v-else-if="groups.length === 0" class="gl-text-center">
<h5>{{ s__('Integrations|No available namespaces.') }}</h5>
<p class="gl-mt-5">
{{ s__('Integrations|You must have owner or maintainer permissions to link namespaces.') }}
</p>
</div>
<ul
v-else
class="gl-list-style-none gl-pl-0 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
:class="{ 'gl-opacity-5': isLoadingMore }"
data-testid="groups-list"
>
<groups-list-item
v-for="group in groups"
:key="group.id"
:group="group"
:disabled="isLoadingMore"
@error="errorMessage = $event"
/>
</ul>
<div class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-pagination
v-if="totalItems > perPage && groups.length > 0"
v-model="page"
class="gl-mb-0"
:per-page="perPage"
:total-items="totalItems"
@input="loadGroups"
/>
</div>
</div>
</template>

View File

@ -21,6 +21,11 @@ export default {
type: Object,
required: true,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -60,7 +65,7 @@ export default {
</script>
<template>
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
<li class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<gl-icon name="folder-o" class="gl-mr-3" />
<div class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3">
@ -83,11 +88,13 @@ export default {
<gl-button
category="secondary"
variant="success"
variant="confirm"
:loading="isLoading"
:disabled="disabled"
@click.prevent="onClick"
>{{ __('Link') }}</gl-button
>
{{ __('Link') }}
</gl-button>
</div>
</div>
</li>

View File

@ -1,3 +1,5 @@
import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
initLabels();
initDeleteLabelModal();

View File

@ -5,5 +5,10 @@ import initDiverganceGraph from '~/branches/divergence_graph';
AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new
initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint);
const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
'.js-branch-list',
).dataset;
initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown();

View File

@ -1,4 +1,5 @@
import Vue from 'vue';
import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
@ -9,6 +10,7 @@ Vue.use(Translate);
const initLabelIndex = () => {
initLabels();
initDeleteLabelModal();
const onRequestFinished = ({ labelUrl, successful }) => {
const button = document.querySelector(

View File

@ -0,0 +1,81 @@
<script>
import { GlModal, GlSprintf, GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
export default {
components: {
GlModal,
GlSprintf,
GlButton,
},
props: {
selector: {
type: String,
required: true,
},
},
data() {
return {
labelName: '',
subjectName: '',
destroyPath: '',
modalId: uniqueId('modal-delete-label-'),
};
},
mounted() {
document.querySelectorAll(this.selector).forEach((button) => {
button.addEventListener('click', (e) => {
e.preventDefault();
const { labelName, subjectName, destroyPath } = button.dataset;
this.labelName = labelName;
this.subjectName = subjectName;
this.destroyPath = destroyPath;
this.openModal();
});
});
},
methods: {
openModal() {
this.$refs.modal.show();
},
closeModal() {
this.$refs.modal.hide();
},
},
};
</script>
<template>
<gl-modal ref="modal" :modal-id="modalId">
<template #modal-title>
<gl-sprintf :message="__('Delete label: %{labelName}')">
<template #labelName>
{{ labelName }}
</template>
</gl-sprintf>
</template>
<gl-sprintf
:message="
__(
`%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`,
)
"
>
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
<template #modal-footer>
<gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button>
<gl-button
category="primary"
variant="danger"
:href="destroyPath"
data-method="delete"
data-testid="delete-button"
>{{ __('Delete label') }}</gl-button
>
</template>
</gl-modal>
</template>

View File

@ -4,7 +4,6 @@
@import 'bootstrap-vue/src/index';
@import '@gitlab/ui/src/scss/utilities';
@import '@gitlab/ui/src/components/base/alert/alert';
// We should only import styles that we actually use.
@import '@gitlab/ui/src/components/base/alert/alert';
@ -16,8 +15,8 @@
@import '@gitlab/ui/src/components/base/loading_icon/loading_icon';
@import '@gitlab/ui/src/components/base/modal/modal';
@import '@gitlab/ui/src/components/base/pagination/pagination';
@import '@gitlab/ui/src/components/base/tabs/tabs/tabs';
@import '@gitlab/ui/src/components/base/tooltip/tooltip';
@import '@gitlab/ui/src/components/base/search_box_by_type/search_box_by_type';
$atlaskit-border-color: #dfe1e6;
$header-height: 40px;

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Resolvers
class BlobsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Tree::BlobType.connection_type, null: true
authorize :download_code
calls_gitaly!
alias_method :repository, :object
argument :paths, [GraphQL::STRING_TYPE],
required: true,
description: 'Array of desired blob paths.'
argument :ref, GraphQL::STRING_TYPE,
required: false,
default_value: nil,
description: 'The commit ref to get the blobs from. Default value is HEAD.'
# We fetch blobs from Gitaly efficiently but it still scales O(N) with the
# number of paths being fetched, so apply a scaling limit to that.
def self.resolver_complexity(args, child_complexity:)
super + args.fetch(:paths, []).size
end
def resolve(paths:, ref:)
authorize!(repository.container)
return [] if repository.empty?
ref ||= repository.root_ref
repository.blobs_at(paths.map { |path| [ref, path] })
end
end
end

View File

@ -50,7 +50,8 @@ module ResolvesMergeRequests
approved_by: [:approved_by_users],
milestone: [:milestone],
security_auto_fix: [:author],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
timelogs: [:timelogs]
}
end
end

View File

@ -44,7 +44,8 @@ module Resolvers
{
alert_management_alert: [:alert_management_alert],
labels: [:labels],
assignees: [:assignees]
assignees: [:assignees],
timelogs: [:timelogs]
}
end

View File

@ -31,7 +31,7 @@ module Resolvers
end
else
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
finder = ::Ci::PipelinesFinder.new(project, current_user, shas: shas)
finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas)
finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
end

View File

@ -124,6 +124,9 @@ module Types
field :create_note_email, GraphQL::STRING_TYPE, null: true,
description: 'User specific email address for the issue.'
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the issue.'
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end

View File

@ -186,6 +186,8 @@ module Types
description: 'Selected auto merge strategy.'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request.'
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the merge request.'
def approved_by
object.approved_by_users

View File

@ -14,5 +14,7 @@ module Types
description: 'Indicates a corresponding Git repository exists on disk.'
field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
description: 'Tree of the repository.'
field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
end
end

View File

@ -37,4 +37,18 @@ module ProfilesHelper
def user_status_set_to_busy?(status)
status&.availability == availability_values[:busy]
end
# Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip
def ssh_key_expiration_tooltip(key)
return key.errors.full_messages.join(', ') if key.errors.full_messages.any?
s_('Profiles|Key usable beyond expiration date.') if key.expired?
end
# Overridden in EE::ProfilesHelper#ssh_key_expires_field_description
def ssh_key_expires_field_description
s_('Profiles|Key can still be used after expiration.')
end
end
ProfilesHelper.prepend_ee_mod

View File

@ -14,6 +14,10 @@ module SidebarsHelper
end
end
def project_sidebar_context(project, user)
Sidebars::Context.new(**project_sidebar_context_data(project, user))
end
private
def sidebar_project_tracking_attrs
@ -27,4 +31,12 @@ module SidebarsHelper
def sidebar_user_profile_tracking_attrs
tracking_attrs('user_side_navigation', 'render', 'user_side_navigation')
end
def project_sidebar_context_data(project, user)
{
current_user: user,
container: project,
project: project
}
end
end

View File

@ -2,6 +2,7 @@
# Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob
class Blob < SimpleDelegator
include GlobalID::Identification
include Presentable
include BlobLanguageFromGitAttributes
include BlobActiveModel

View File

@ -2,9 +2,10 @@
module HasTimelogsReport
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
def timelogs(start_time, end_time)
@timelogs ||= timelogs_for(start_time, end_time)
strong_memoize(:timelogs) { timelogs_for(start_time, end_time) }
end
def user_can_access_group_timelogs?(current_user)

View File

@ -1378,7 +1378,7 @@ class Project < ApplicationRecord
def find_or_initialize_service(name)
return if disabled_services.include?(name)
find_service(services, name) || build_from_instance_or_template(name) || public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
find_service(services, name) || build_from_instance_or_template(name) || build_service(name)
end
# rubocop: disable CodeReuse/ServiceClass
@ -2596,6 +2596,10 @@ class Project < ApplicationRecord
return Service.build_from_integration(template, project_id: id) if template
end
def build_service(name)
"#{name}_service".classify.constantize.new(project_id: id)
end
def services_templates
@services_templates ||= Service.for_template
end

View File

@ -51,5 +51,25 @@ module Sidebars
def renderable_menus
@renderable_menus ||= @menus.select(&:render?)
end
def container
context.container
end
# Auxiliar method that helps with the migration from
# regular views to the new logic
def render_raw_scope_menu_partial
# No-op
end
# Auxiliar method that helps with the migration from
# regular views to the new logic.
#
# Any menu inside this partial will be added after
# all the menus added in the `configure_menus`
# method.
def render_raw_menus_partial
# No-op
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Sidebars
module Projects
class Panel < ::Sidebars::Panel
override :render_raw_menus_partial
def render_raw_scope_menu_partial
'layouts/nav/sidebar/project_scope_menu'
end
override :render_raw_menus_partial
def render_raw_menus_partial
'layouts/nav/sidebar/project_menus'
end
override :aria_label
def aria_label
_('Project navigation')
end
end
end
end

View File

@ -250,13 +250,17 @@ module Projects
def make_secure_tmp_dir(tmp_path)
FileUtils.mkdir_p(tmp_path)
path = Dir.mktmpdir(nil, tmp_path)
path = Dir.mktmpdir(tmp_dir_prefix, tmp_path)
begin
yield(path)
ensure
FileUtils.remove_entry_secure(path)
end
end
def tmp_dir_prefix
"project-#{project.id}-build-#{build.id}-"
end
end
end

View File

@ -1,469 +1,3 @@
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(@project), 'aria-label': _('Project navigation') }
.nav-sidebar-inner-scroll
.context-header
= link_to project_path(@project), title: @project.name do
.avatar-container.rect-avatar.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
.sidebar-context-title
= @project.name
%ul.sidebar-top-level-items.qa-project-sidebar
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Project overview')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
= _('Project overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
%span= _('Details')
= nav_link(path: 'projects#activity') do
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
%span= _('Activity')
- if project_nav_tab?(:releases)
= nav_link(controller: :releases) do
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
%span= _('Releases')
- if project_nav_tab? :learn_gitlab
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
= link_to project_learn_gitlab_path(@project) do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Learn GitLab')
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
= link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
.nav-icon-container
= sprite_icon('doc-text')
%span.nav-item-name#js-onboarding-repo-link
= _('Repository')
%ul.sidebar-sub-level-items
= nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
= _('Repository')
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
= _('Files')
= nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
= _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
= _('Branches')
= nav_link(controller: [:tags]) do
= link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
= _('Tags')
= nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do
= _('Contributors')
= nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do
= _('Graph')
= nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
= _('Compare')
= render_if_exists 'projects/sidebar/repository_locked_files'
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name#js-onboarding-issues-link
= _('Issues')
- if @project.issues_enabled?
%span.badge.badge-pill.count.issue_counter
= number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
= _('Issues')
- if @project.issues_enabled?
%span.badge.badge-pill.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count(current_user))
%li.divider.fly-out-top-item
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: _('Issues') do
%span
= _('List')
= nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
%span
= boards_link_text
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
%span
= _('Labels')
= render 'projects/sidebar/issues_service_desk'
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
%span
= _('Milestones')
= render_if_exists 'layouts/nav/sidebar/project_iterations_link'
- if project_nav_tab?(:external_issue_tracker)
- issue_tracker = @project.external_issue_tracker
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
- else
= nav_link do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= issue_tracker.title
- if (project_nav_tab? :labels) && !@project.issues_enabled?
= nav_link(controller: [:labels]) do
= link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
.nav-icon-container
= sprite_icon('label')
%span.nav-item-name#js-onboarding-labels-link
= _('Labels')
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
= link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name#js-onboarding-mr-link
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
= render_if_exists "layouts/nav/requirements_link", project: @project
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
.nav-icon-container
= sprite_icon('rocket')
%span.nav-item-name#js-onboarding-pipelines-link
= _('CI/CD')
%ul.sidebar-sub-level-items
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
= _('CI/CD')
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
%span
= _('Pipelines')
- if can_view_pipeline_editor?(@project)
= nav_link(controller: :pipeline_editor, action: :show) do
= link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
%span
= s_('Pipelines|Editor')
- if project_nav_tab? :builds
= nav_link(controller: :jobs) do
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
%span
= _('Jobs')
- if Feature.enabled?(:artifacts_management_page, @project)
= nav_link(controller: :artifacts, action: :index) do
= link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
%span
= _('Artifacts')
- if project_nav_tab?(:pipelines)
= nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
%span
= _('Schedules')
= render_if_exists "layouts/nav/test_cases_link", project: @project
- if project_nav_tab? :security_and_compliance
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
- if project_nav_tab? :operations
= nav_link(controller: sidebar_operations_paths) do
= link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Operations')
%ul.sidebar-sub-level-items
= nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to sidebar_operations_link_path do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
- if project_nav_tab? :metrics_dashboards
= nav_link(controller: :metrics_dashboard, action: [:show]) do
= link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
%span
= _('Metrics')
- if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
= nav_link(controller: :logs, action: [:index]) do
= link_to project_logs_path(@project), title: _('Logs') do
%span
= _('Logs')
- if project_nav_tab? :environments
= render "layouts/nav/sidebar/tracing_link"
- if project_nav_tab?(:error_tracking)
= nav_link(controller: :error_tracking) do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
%span
= _('Error Tracking')
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
= link_to project_alert_management_index_path(@project), title: _('Alerts') do
%span
= _('Alerts')
- if project_nav_tab?(:incidents)
= nav_link(controller: :incidents) do
= link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
%span
= _('Incidents')
= render_if_exists 'projects/sidebar/oncall_schedules'
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
%span
= _('Serverless')
- if project_nav_tab? :terraform
= nav_link(controller: :terraform) do
= link_to project_terraform_index_path(@project), title: _('Terraform') do
%span
= _('Terraform')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:cluster_agents, :clusters]) do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
.js-feature-highlight{ disabled: true,
data: { trigger: 'manual',
container: 'body',
placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: user_callouts_path,
auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
- if project_nav_tab? :environments
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
%span
= _('Environments')
- if project_nav_tab? :feature_flags
= nav_link(controller: :feature_flags) do
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
%span
= _('Feature Flags')
- if project_nav_tab?(:product_analytics)
= nav_link(controller: :product_analytics) do
= link_to project_product_analytics_path(@project), title: _('Product Analytics') do
%span
= _('Product Analytics')
= render_if_exists 'layouts/nav/sidebar/project_packages_link'
- if project_nav_tab? :analytics
= render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
- if project_nav_tab?(:confluence)
- confluence_url = project_wikis_confluence_path(@project)
= nav_link do
= link_to confluence_url, class: 'shortcuts-confluence' do
.nav-icon-container
= image_tag 'confluence.svg', alt: _('Confluence')
%span.nav-item-name
= _('Confluence')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= _('Confluence')
- if project_nav_tab? :wiki
= render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
- if project_nav_tab?(:external_wiki)
- external_wiki_url = @project.external_wiki.external_wiki_url
= nav_link do
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= s_('ExternalWikiService|External wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to external_wiki_url do
%strong.fly-out-top-item-name
= s_('ExternalWikiService|External wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
= _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
= _('Snippets')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
= _('Members')
- if project_nav_tab? :settings
= nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project) do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item#js-onboarding-settings-link
= _('Settings')
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
= nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
%span
= _('General')
- if can_edit
= nav_link(controller: [:integrations, :services]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
%span
= _('Integrations')
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
%span
= _('Webhooks')
- if can?(current_user, :read_resource_access_tokens, @project)
= nav_link(controller: [:access_tokens]) do
= link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
%span
= _('Access Tokens')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: _('Repository') do
%span
= _('Repository')
- if !@project.archived? && @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
%span
= _('CI/CD')
- if settings_operations_available?
= nav_link(controller: [:operations]) do
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
= _('Operations')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
%span
= _('Pages')
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span
= _('Activity')
-# Shortcut to Repository > Graph (formerly, Network)
- if project_nav_tab? :network
%li.hidden
= link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
= _('Graph')
-# Shortcut to Issues > New Issue
- if project_nav_tab?(:issues)
%li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
= _('Create a new issue')
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
%li.hidden
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
= _('Jobs')
-# Shortcut to commits page
- if project_nav_tab? :commits
%li.hidden
= link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
= _('Commits')
-# Shortcut to issue boards
- if project_nav_tab?(:issues)
%li.hidden
= link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'
= render 'shared/sidebar_toggle_button'
-# We're migration the project sidebar to a logical model based structure. If you need to update
-# any of the existing menus, you can find them in app/views/layouts/nav/sidebar/_project_menus.html.haml.
= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user))

View File

@ -0,0 +1,458 @@
= nav_link(path: sidebar_projects_paths, html_options: { class: 'home' }) do
= link_to project_path(@project), class: 'shortcuts-project rspec-project-link', data: { qa_selector: 'project_link' } do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Project overview')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
= _('Project overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
%span= _('Details')
= nav_link(path: 'projects#activity') do
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity', data: { qa_selector: 'activity_link' } do
%span= _('Activity')
- if project_nav_tab?(:releases)
= nav_link(controller: :releases) do
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
%span= _('Releases')
- if project_nav_tab? :learn_gitlab
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
= link_to project_learn_gitlab_path(@project) do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Learn GitLab')
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
= link_to project_tree_path(@project), class: 'shortcuts-tree', data: { qa_selector: "repository_link" } do
.nav-icon-container
= sprite_icon('doc-text')
%span.nav-item-name#js-onboarding-repo-link
= _('Repository')
%ul.sidebar-sub-level-items
= nav_link(controller: sidebar_repository_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to project_tree_path(@project) do
%strong.fly-out-top-item-name
= _('Repository')
%li.divider.fly-out-top-item
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_tree_path(@project) do
= _('Files')
= nav_link(controller: [:commit, :commits]) do
= link_to project_commits_path(@project, current_ref), id: 'js-onboarding-commits-link' do
= _('Commits')
= nav_link(html_options: {class: branches_tab_class}) do
= link_to project_branches_path(@project), data: { qa_selector: "branches_link" }, id: 'js-onboarding-branches-link' do
= _('Branches')
= nav_link(controller: [:tags]) do
= link_to project_tags_path(@project), data: { qa_selector: "tags_link" } do
= _('Tags')
= nav_link(path: 'graphs#show') do
= link_to project_graph_path(@project, current_ref) do
= _('Contributors')
= nav_link(controller: %w(network)) do
= link_to project_network_path(@project, current_ref) do
= _('Graph')
= nav_link(controller: :compare) do
= link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do
= _('Compare')
= render_if_exists 'projects/sidebar/repository_locked_files'
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards, :iterations] : 'projects/issues') do
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
.nav-icon-container
= sprite_icon('issues')
%span.nav-item-name#js-onboarding-issues-link
= _('Issues')
- if @project.issues_enabled?
%span.badge.badge-pill.count.issue_counter
= number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
= _('Issues')
- if @project.issues_enabled?
%span.badge.badge-pill.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count(current_user))
%li.divider.fly-out-top-item
= nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: _('Issues') do
%span
= _('List')
= nav_link(controller: :boards) do
= link_to project_boards_path(@project), title: boards_link_text, data: { qa_selector: "issue_boards_link" } do
%span
= boards_link_text
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do
%span
= _('Labels')
= render 'projects/sidebar/issues_service_desk'
= nav_link(controller: :milestones) do
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
%span
= _('Milestones')
= render_if_exists 'layouts/nav/sidebar/project_iterations_link'
- if project_nav_tab?(:external_issue_tracker)
- issue_tracker = @project.external_issue_tracker
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
- else
= nav_link do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= issue_tracker.title
- if (project_nav_tab? :labels) && !@project.issues_enabled?
= nav_link(controller: [:labels]) do
= link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
.nav-icon-container
= sprite_icon('label')
%span.nav-item-name#js-onboarding-labels-link
= _('Labels')
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
= link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
.nav-icon-container
= sprite_icon('git-merge')
%span.nav-item-name#js-onboarding-mr-link
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter
= number_with_delimiter(@project.open_merge_requests_count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
= link_to project_merge_requests_path(@project) do
%strong.fly-out-top-item-name
= _('Merge requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge
= number_with_delimiter(@project.open_merge_requests_count)
= render_if_exists "layouts/nav/requirements_link", project: @project
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], unless: -> { current_path?('projects/pipelines#charts') }) do
= link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
.nav-icon-container
= sprite_icon('rocket')
%span.nav-item-name#js-onboarding-pipelines-link
= _('CI/CD')
%ul.sidebar-sub-level-items
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts, :test_cases, :pipeline_editor], html_options: { class: "fly-out-top-item" }) do
= link_to project_pipelines_path(@project) do
%strong.fly-out-top-item-name
= _('CI/CD')
%li.divider.fly-out-top-item
- if project_nav_tab? :pipelines
= nav_link(path: ['pipelines#index', 'pipelines#show']) do
= link_to project_pipelines_path(@project), title: _('Pipelines'), class: 'shortcuts-pipelines' do
%span
= _('Pipelines')
- if can_view_pipeline_editor?(@project)
= nav_link(controller: :pipeline_editor, action: :show) do
= link_to project_ci_pipeline_editor_path(@project), title: s_('Pipelines|Editor') do
%span
= s_('Pipelines|Editor')
- if project_nav_tab? :builds
= nav_link(controller: :jobs) do
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
%span
= _('Jobs')
- if Feature.enabled?(:artifacts_management_page, @project)
= nav_link(controller: :artifacts, action: :index) do
= link_to project_artifacts_path(@project), title: _('Artifacts'), class: 'shortcuts-builds' do
%span
= _('Artifacts')
- if project_nav_tab?(:pipelines)
= nav_link(controller: :pipeline_schedules) do
= link_to pipeline_schedules_path(@project), title: _('Schedules'), class: 'shortcuts-builds' do
%span
= _('Schedules')
= render_if_exists "layouts/nav/test_cases_link", project: @project
- if project_nav_tab? :security_and_compliance
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
- if project_nav_tab? :operations
= nav_link(controller: sidebar_operations_paths) do
= link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
= _('Operations')
%ul.sidebar-sub-level-items
= nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to sidebar_operations_link_path do
%strong.fly-out-top-item-name
= _('Operations')
%li.divider.fly-out-top-item
- if project_nav_tab? :metrics_dashboards
= nav_link(controller: :metrics_dashboard, action: [:show]) do
= link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
%span
= _('Metrics')
- if project_nav_tab?(:environments) && can?(current_user, :read_pod_logs, @project)
= nav_link(controller: :logs, action: [:index]) do
= link_to project_logs_path(@project), title: _('Logs') do
%span
= _('Logs')
- if project_nav_tab? :environments
= render "layouts/nav/sidebar/tracing_link"
- if project_nav_tab?(:error_tracking)
= nav_link(controller: :error_tracking) do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
%span
= _('Error Tracking')
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
= link_to project_alert_management_index_path(@project), title: _('Alerts') do
%span
= _('Alerts')
- if project_nav_tab?(:incidents)
= nav_link(controller: :incidents) do
= link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
%span
= _('Incidents')
= render_if_exists 'projects/sidebar/oncall_schedules'
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
%span
= _('Serverless')
- if project_nav_tab? :terraform
= nav_link(controller: :terraform) do
= link_to project_terraform_index_path(@project), title: _('Terraform') do
%span
= _('Terraform')
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:cluster_agents, :clusters]) do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-kubernetes' do
%span
= _('Kubernetes')
- if show_cluster_hint
.js-feature-highlight{ disabled: true,
data: { trigger: 'manual',
container: 'body',
placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: user_callouts_path,
auto_devops_help_path: help_page_path('topics/autodevops/index.md') } }
- if project_nav_tab? :environments
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
%span
= _('Environments')
- if project_nav_tab? :feature_flags
= nav_link(controller: :feature_flags) do
= link_to project_feature_flags_path(@project), title: _('Feature Flags'), class: 'shortcuts-feature-flags' do
%span
= _('Feature Flags')
- if project_nav_tab?(:product_analytics)
= nav_link(controller: :product_analytics) do
= link_to project_product_analytics_path(@project), title: _('Product Analytics') do
%span
= _('Product Analytics')
= render_if_exists 'layouts/nav/sidebar/project_packages_link'
- if project_nav_tab? :analytics
= render 'layouts/nav/sidebar/analytics_links', links: project_analytics_navbar_links(@project, current_user)
- if project_nav_tab?(:confluence)
- confluence_url = project_wikis_confluence_path(@project)
= nav_link do
= link_to confluence_url, class: 'shortcuts-confluence' do
.nav-icon-container
= image_tag 'confluence.svg', alt: _('Confluence')
%span.nav-item-name
= _('Confluence')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: 'fly-out-top-item' } ) do
= link_to confluence_url, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= _('Confluence')
- if project_nav_tab? :wiki
= render 'layouts/nav/sidebar/wiki_link', wiki_url: wiki_path(@project.wiki)
- if project_nav_tab?(:external_wiki)
- external_wiki_url = @project.external_wiki.external_wiki_url
= nav_link do
= link_to external_wiki_url, class: 'shortcuts-external_wiki' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= s_('ExternalWikiService|External wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to external_wiki_url do
%strong.fly-out-top-item-name
= s_('ExternalWikiService|External wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
= _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
= _('Snippets')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
= _('Members')
- if project_nav_tab? :settings
= nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project) do
.nav-icon-container
= sprite_icon('settings')
%span.nav-item-name.qa-settings-item#js-onboarding-settings-link
= _('Settings')
%ul.sidebar-sub-level-items
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
= nav_link(path: sidebar_settings_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to edit_project_path(@project) do
%strong.fly-out-top-item-name
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: %w[projects#edit]) do
= link_to edit_project_path(@project), title: _('General'), class: 'qa-general-settings-link' do
%span
= _('General')
- if can_edit
= nav_link(controller: [:integrations, :services]) do
= link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
%span
= _('Integrations')
= nav_link(controller: [:hooks, :hook_logs]) do
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
%span
= _('Webhooks')
- if can?(current_user, :read_resource_access_tokens, @project)
= nav_link(controller: [:access_tokens]) do
= link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
%span
= _('Access Tokens')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: _('Repository') do
%span
= _('Repository')
- if !@project.archived? && @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to project_settings_ci_cd_path(@project), title: _('CI/CD') do
%span
= _('CI/CD')
- if settings_operations_available?
= nav_link(controller: [:operations]) do
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
= _('Operations')
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
%span
= _('Pages')
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do
%span
= _('Activity')
-# Shortcut to Repository > Graph (formerly, Network)
- if project_nav_tab? :network
%li.hidden
= link_to project_network_path(@project, current_ref), title: _('Network'), class: 'shortcuts-network' do
= _('Graph')
-# Shortcut to Issues > New Issue
- if project_nav_tab?(:issues)
%li.hidden
= link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
= _('Create a new issue')
-# Shortcut to Pipelines > Jobs
- if project_nav_tab? :builds
%li.hidden
= link_to project_jobs_path(@project), title: _('Jobs'), class: 'shortcuts-builds' do
= _('Jobs')
-# Shortcut to commits page
- if project_nav_tab? :commits
%li.hidden
= link_to project_commits_path(@project), title: _('Commits'), class: 'shortcuts-commits' do
= _('Commits')
-# Shortcut to issue boards
- if project_nav_tab?(:issues)
%li.hidden
= link_to _('Issue Boards'), project_boards_path(@project), title: _('Issue Boards'), class: 'shortcuts-issue-boards'

View File

@ -0,0 +1,6 @@
.context-header
= link_to project_path(@project), title: @project.name do
.avatar-container.rect-avatar.s40.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile', width: 40, height: 40)
.sidebar-context-title
= @project.name

View File

@ -15,11 +15,12 @@
.col.form-group
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
= f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
%p.form-text.text-muted{ data: { qa_selector: 'key_expiry_date_field_description' } }= ssh_key_expires_field_description
.js-add-ssh-key-validation-warning.hide
.bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
%strong= _('Oops, are you sure?')
%p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible.")
%p= s_("Profiles|Publicly visible private SSH keys can compromise your system.")
%button.btn.gl-button.btn-confirm.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")

View File

@ -1,3 +1,5 @@
- icon_classes = 'settings-list-icon gl-display-none gl-sm-display-block'
%li.key-list-item
.gl-display-flex.gl-align-items-flex-start
.key-list-item-info.gl-w-full.float-none
@ -5,15 +7,11 @@
= key.title
.gl-display-flex.gl-align-items-center.gl-mt-2
- if key.valid?
- if key.expired?
%span.gl-display-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') }
= sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
- else
= sprite_icon('key', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
- if key.valid? && !key.expired?
= sprite_icon('key', css_class: icon_classes)
- else
%span.gl-display-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') }
= sprite_icon('warning-solid', css_class: 'settings-list-icon gl-display-none gl-sm-display-block')
%span.gl-display-inline-block.has-tooltip{ title: ssh_key_expiration_tooltip(key) }
= sprite_icon('warning-solid', css_class: icon_classes)
%span.gl-text-truncate.gl-sm-ml-3
= key.fingerprint

View File

@ -49,7 +49,7 @@
= render_if_exists 'projects/commits/mirror_status'
.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json) } }
.js-branch-list{ data: { diverging_counts_endpoint: diverging_commit_counts_namespace_project_branches_path(@project.namespace, @project, format: :json), default_branch: @project.default_branch } }
- if can?(current_user, :admin_project, @project)
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
.row-content-block

View File

@ -1,20 +0,0 @@
.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
= html_escape(_('%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}')) % { label_name: tag.strong(label.name), subject_name: label.subject_name, span_open: '<span>'.html_safe, span_close: '</span>'.html_safe }
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }= _('Cancel')
= link_to _('Delete label'),
label.destroy_path,
title: _('Delete'),
method: :delete,
class: 'gl-button btn btn-danger'

View File

@ -36,10 +36,10 @@
label_text_color: label.text_color,
group_name: label.project.group.name } }
= _('Promote to group label')
- if can?(current_user, :admin_label, label)
%li
%span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
%button.text-danger.remove-row{ type: 'button' }= _('Delete')
%li
%span
%button.text-danger.remove-row.js-delete-label-modal-button{ type: 'button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }
= _('Delete')
- if current_user
%li.inline.label-subscription
- if label.can_subscribe_to_label_in_different_levels?
@ -61,5 +61,3 @@
- else
%button.gl-button.js-subscribe-button.label-subscribe-button.btn.btn-default.gl-ml-3{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
= render 'shared/delete_label_modal', label: label

View File

@ -0,0 +1,10 @@
%aside.nav-sidebar{ class: ('sidebar-collapsed-desktop' if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(sidebar.container), 'aria-label': sidebar.aria_label }
.nav-sidebar-inner-scroll
- if sidebar.render_raw_scope_menu_partial
= render sidebar.render_raw_scope_menu_partial
%ul.sidebar-top-level-items.qa-project-sidebar
- if sidebar.render_raw_menus_partial
= render sidebar.render_raw_menus_partial
= render 'shared/sidebar_toggle_button'

View File

@ -24,7 +24,13 @@ module EachShardWorker
end
def healthy_ready_shards
ready_shards.select(&:success)
success_checks, failed_checks = ready_shards.partition(&:success)
if failed_checks.any?
::Gitlab::AppLogger.error(message: 'Excluding unhealthy shards', failed_checks: failed_checks.map(&:payload), class: self.class.name)
end
success_checks
end
def ready_shards

View File

@ -0,0 +1,5 @@
---
title: Make blobs directly accessible through the graphql repository
merge_request: 58677
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update default branch in divergence graph
merge_request: 58871
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add search functionality to Jira Connect App namespaces
merge_request: 57669
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix N+1 queries to find or initialize services
merge_request: 58879
author:
type: performance

View File

@ -3,5 +3,3 @@ title: Update GIicon size in geo_node_header.vue
merge_request: 57952
author: singhanshuman
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix loading pipelines by commit SHA for GraphQL
merge_request: 59110
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Ensure a project iid is set before transitioning on pipeline error
merge_request: 57783
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Expose timelogs against issues and merge requests in GraphQL
merge_request: 57321
author: Lee Tickett @leetickett
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/lib/gitlab/hook_data
merge_request: 58262
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/groups
merge_request: 58423
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/ide
merge_request: 58424
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/services/issues
merge_request: 58425
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Include project and build ID in Pages tmp directory
merge_request: 59106
author:
type: changed

View File

@ -0,0 +1,8 @@
---
name: ci_pipeline_ensure_iid_on_drop
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57783
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326886
milestone: '13.11'
type: development
group: group::code review
default_enabled: false

View File

@ -2905,6 +2905,7 @@ Relationship between an epic and an issue.
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
| `title` | [`String!`](#string) | Title of the issue. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
@ -3468,6 +3469,7 @@ An edge in a connection.
| `subscribed` | [`Boolean!`](#boolean) | Indicates the currently logged in user is subscribed to the issue. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Task completion status of the issue. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the issue. |
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the issue. |
| `title` | [`String!`](#string) | Title of the issue. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the issue. |
@ -3980,6 +3982,7 @@ An edge in a connection.
| `targetProjectId` | [`Int!`](#int) | ID of the merge request target project. |
| `taskCompletionStatus` | [`TaskCompletionStatus!`](#taskcompletionstatus) | Completion status of tasks. |
| `timeEstimate` | [`Int!`](#int) | Time estimate of the merge request. |
| `timelogs` | [`TimelogConnection!`](#timelogconnection) | Timelogs on the merge request. |
| `title` | [`String!`](#string) | Title of the merge request. |
| `titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| `totalTimeSpent` | [`Int!`](#int) | Total time reported as spent on the merge request. |
@ -5440,6 +5443,7 @@ Autogenerated return type of RepositionImageDiffNote.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. |
| `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. |
| `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. |
| `rootRef` | [`String`](#string) | Default branch of the repository. |

View File

@ -12,7 +12,17 @@ module Gitlab
end
pipeline.add_error_message(message)
pipeline.drop!(drop_reason) if drop_reason && persist_pipeline?
if drop_reason && persist_pipeline?
if Feature.enabled?(:ci_pipeline_ensure_iid_on_drop, pipeline.project, default_enabled: :yaml)
# Project iid must be called outside a transaction, so we ensure it is set here
# otherwise it may be set within the state transition transaction of the drop! call
# which it will lock the InternalId row for the whole transaction
pipeline.ensure_project_iid!
end
pipeline.drop!(drop_reason)
end
# TODO: consider not to rely on AR errors directly as they can be
# polluted with other unrelated errors (e.g. state machine)

View File

@ -623,9 +623,6 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
msgid "%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}"
msgstr ""
msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
msgstr ""
@ -10229,7 +10226,7 @@ msgstr ""
msgid "Delete label"
msgstr ""
msgid "Delete label: %{label_name} ?"
msgid "Delete label: %{labelName}"
msgstr ""
msgid "Delete pipeline"
@ -23895,6 +23892,9 @@ msgstr ""
msgid "Profiles|Enter your name, so people you know can recognize you"
msgstr ""
msgid "Profiles|Expired key is not valid."
msgstr ""
msgid "Profiles|Expires at"
msgstr ""
@ -23925,6 +23925,9 @@ msgstr ""
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
msgstr ""
msgid "Profiles|Invalid key."
msgstr ""
msgid "Profiles|Invalid password"
msgstr ""
@ -23934,6 +23937,15 @@ msgstr ""
msgid "Profiles|Key"
msgstr ""
msgid "Profiles|Key can still be used after expiration."
msgstr ""
msgid "Profiles|Key usable beyond expiration date."
msgstr ""
msgid "Profiles|Key will be deleted on this date."
msgstr ""
msgid "Profiles|Last used:"
msgstr ""
@ -23979,6 +23991,9 @@ msgstr ""
msgid "Profiles|Public email"
msgstr ""
msgid "Profiles|Publicly visible private SSH keys can compromise your system."
msgstr ""
msgid "Profiles|Remove avatar"
msgstr ""
@ -24003,9 +24018,6 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it? It will be publicly visible."
msgstr ""
msgid "Profiles|This email will be displayed on your public profile"
msgstr ""
@ -24093,9 +24105,6 @@ msgstr ""
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
msgstr ""
msgid "Profiles|Your key has expired"
msgstr ""
msgid "Profiles|Your location was automatically set based on your %{provider_label} account"
msgstr ""

View File

@ -13,7 +13,7 @@ module QA
include SubMenus::Settings
include SubMenus::Packages
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :activity_link
element :merge_requests_link
element :snippets_link

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :link_pipelines
end
end

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :issue_boards_link
element :issues_item
element :labels_link

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :operations_link
element :operations_environments_link
element :operations_metrics_link

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :project_link
end
end

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :repository_link
element :branches_link
element :tags_link

View File

@ -13,7 +13,7 @@ module QA
base.class_eval do
include QA::Page::Project::SubMenus::Common
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :settings_item
element :general_settings_link
element :integrations_settings_link

View File

@ -1,11 +1,22 @@
# frozen_string_literal: true
# Read about factories at https://github.com/thoughtbot/factory_bot
FactoryBot.define do
factory :timelog do
time_spent { 3600 }
issue
user { issue.project.creator }
for_issue
factory :issue_timelog, traits: [:for_issue]
factory :merge_request_timelog, traits: [:for_merge_request]
trait :for_issue do
issue
user { issue.author }
end
trait :for_merge_request do
merge_request
issue { nil }
user { merge_request.author }
end
end
end

View File

@ -18,18 +18,18 @@ RSpec.describe "User removes labels" do
visit(project_labels_path(project))
end
it "removes label" do
it "removes label", :js do
page.within(".other-labels") do
page.first(".label-list-item") do
first('.js-label-options-dropdown').click
first(".remove-row").click
end
expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
first(:link, "Delete label").click
end
expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
first(:link, "Delete label").click
expect(page).to have_content("Label was removed").and have_no_content(label.title)
end
end

View File

@ -0,0 +1,83 @@
import { TEST_HOST } from 'helpers/test_constants';
import initDeleteLabelModal from '~/delete_label_modal';
describe('DeleteLabelModal', () => {
const buttons = [
{
labelName: 'label 1',
subjectName: 'GitLab Org',
destroyPath: `${TEST_HOST}/1`,
},
{
labelName: 'label 2',
subjectName: 'GitLab Org',
destroyPath: `${TEST_HOST}/2`,
},
];
beforeEach(() => {
const buttonContainer = document.createElement('div');
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
button.setAttribute('data-label-name', x.labelName);
button.setAttribute('data-subject-name', x.subjectName);
button.setAttribute('data-destroy-path', x.destroyPath);
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});
document.body.appendChild(buttonContainer);
});
afterEach(() => {
document.body.innerHTML = '';
});
const findJsHooks = () => document.querySelectorAll('.js-delete-label-modal-button');
const findModal = () => document.querySelector('.gl-modal');
it('starts with only js-containers', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
expect(findModal()).not.toExist();
});
describe('when first button clicked', () => {
beforeEach(() => {
initDeleteLabelModal();
findJsHooks().item(0).click();
});
it('does not replace js-containers with GlModal', () => {
expect(findJsHooks()).toHaveLength(buttons.length);
});
it('renders GlModal', () => {
expect(findModal()).toExist();
});
});
describe.each`
index
${0}
${1}
`(`when multiple buttons exist`, ({ index }) => {
beforeEach(() => {
initDeleteLabelModal();
findJsHooks().item(index).click();
});
it('correct props are passed to gl-modal', () => {
expect(findModal().querySelector('.modal-title').innerHTML).toContain(
buttons[index].labelName,
);
expect(findModal().querySelector('.modal-body').innerHTML).toContain(
buttons[index].subjectName,
);
expect(findModal().querySelector('.modal-footer .btn-danger').href).toContain(
buttons[index].destroyPath,
);
});
});
});

View File

@ -1,7 +1,7 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/api';
import GroupsList from '~/jira_connect/components/groups_list.vue';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
@ -12,20 +12,27 @@ jest.mock('~/jira_connect/api', () => {
fetchGroups: jest.fn(),
};
});
const mockGroupsPath = '/groups';
describe('GroupsList', () => {
let wrapper;
const mockEmptyResponse = { data: [] };
const createComponent = (options = {}) => {
wrapper = shallowMount(GroupsList, {
...options,
});
wrapper = extendedWrapper(
shallowMount(GroupsList, {
provide: {
groupsPath: mockGroupsPath,
},
...options,
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGlAlert = () => wrapper.find(GlAlert);
@ -33,56 +40,72 @@ describe('GroupsList', () => {
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
const findSecondItem = () => findAllItems().at(1);
const findSearchBox = () => wrapper.find(GlSearchBoxByType);
const findGroupsList = () => wrapper.findByTestId('groups-list');
describe('isLoading is true', () => {
describe('when groups are loading', () => {
it('renders loading icon', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
fetchGroups.mockReturnValue(new Promise(() => {}));
createComponent();
wrapper.setData({ isLoading: true });
await wrapper.vm.$nextTick();
expect(findGlLoadingIcon().exists()).toBe(true);
});
});
describe('error fetching groups', () => {
describe('when groups fetch fails', () => {
it('renders error message', async () => {
fetchGroups.mockRejectedValue();
createComponent();
await waitForPromises();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
});
});
describe('no groups returned', () => {
describe('with no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
createComponent();
await waitForPromises();
expect(findGlLoadingIcon().exists()).toBe(false);
expect(wrapper.text()).toContain('No available namespaces');
});
});
describe('with groups returned', () => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
fetchGroups.mockResolvedValue({
headers: { 'X-PAGE': 1, 'X-TOTAL': 2 },
data: [mockGroup1, mockGroup2],
});
createComponent();
await waitForPromises();
});
it('renders groups list', () => {
expect(findAllItems().length).toBe(2);
expect(findAllItems()).toHaveLength(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
it('sets GroupListItem `disabled` prop to `false`', () => {
findAllItems().wrappers.forEach((groupListItem) => {
expect(groupListItem.props('disabled')).toBe(false);
});
});
it('does not set opacity of the groups list', () => {
expect(findGroupsList().classes()).not.toContain('gl-opacity-5');
});
it('shows error message on $emit from item', async () => {
const errorMessage = 'error message';
@ -93,5 +116,55 @@ describe('GroupsList', () => {
expect(findGlAlert().exists()).toBe(true);
expect(findGlAlert().text()).toContain(errorMessage);
});
describe('when searching groups', () => {
const mockSearchTeam = 'mock search term';
describe('while groups are loading', () => {
beforeEach(async () => {
fetchGroups.mockClear();
fetchGroups.mockReturnValue(new Promise(() => {}));
findSearchBox().vm.$emit('input', mockSearchTeam);
await wrapper.vm.$nextTick();
});
it('calls `fetchGroups` with search term', () => {
expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
page: 1,
perPage: 10,
search: mockSearchTeam,
});
});
it('disables GroupListItems', async () => {
findAllItems().wrappers.forEach((groupListItem) => {
expect(groupListItem.props('disabled')).toBe(true);
});
});
it('sets opacity of the groups list', () => {
expect(findGroupsList().classes()).toContain('gl-opacity-5');
});
it('sets loading prop of ths search box', () => {
expect(findSearchBox().props('isLoading')).toBe(true);
});
});
describe('when group search finishes loading', () => {
beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1] });
findSearchBox().vm.$emit('input');
await waitForPromises();
});
it('renders new groups list', () => {
expect(findAllItems()).toHaveLength(1);
expect(findFirstItem().props('group')).toBe(mockGroup1);
});
});
});
});
});

View File

@ -0,0 +1,64 @@
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DeleteLabelModal from '~/vue_shared/components/delete_label_modal.vue';
const MOCK_MODAL_DATA = {
labelName: 'label 1',
subjectName: 'GitLab Org',
destroyPath: `${TEST_HOST}/1`,
};
describe('vue_shared/components/delete_label_modal', () => {
let wrapper;
const createComponent = () => {
wrapper = extendedWrapper(
mount(DeleteLabelModal, {
propsData: {
selector: '.js-test-btn',
},
stubs: {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findModal = () => wrapper.find(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
describe('template', () => {
describe('when modal data is set', () => {
beforeEach(() => {
createComponent();
wrapper.vm.labelName = MOCK_MODAL_DATA.labelName;
wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName;
wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath;
});
it('renders GlModal', () => {
expect(findModal().exists()).toBe(true);
});
it('displays the label name and subject name', () => {
expect(findModal().text()).toContain(
`${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`,
);
});
it('passes the destroyPath to the button', () => {
expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath);
});
});
});
});

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::BlobsResolver do
include GraphqlHelpers
describe '.resolver_complexity' do
it 'adds one per path being resolved' do
control = described_class.resolver_complexity({}, child_complexity: 1)
expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1))
.to eq(control + 3)
end
end
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:args) { { paths: paths, ref: ref } }
let(:paths) { [] }
let(:ref) { nil }
subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) }
context 'when unauthorized' do
it 'raises an exception' do
expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when authorized' do
before do
project.add_developer(user)
end
context 'using no filter' do
it 'returns nothing' do
is_expected.to be_empty
end
end
context 'using paths filter' do
let(:paths) { ['README.md'] }
it 'returns the specified blobs for HEAD' do
is_expected.to contain_exactly(have_attributes(path: 'README.md'))
end
context 'specifying a non-existent blob' do
let(:paths) { ['non-existent'] }
it 'returns nothing' do
is_expected.to be_empty
end
end
context 'specifying a different ref' do
let(:ref) { 'add-pdf-file' }
let(:paths) { ['files/pdf/test.pdf', 'README.md'] }
it 'returns the specified blobs for that ref' do
is_expected.to contain_exactly(
have_attributes(path: 'files/pdf/test.pdf'),
have_attributes(path: 'README.md')
)
end
end
end
end
end
end

View File

@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') }
let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') }
let_it_be(:other_pipeline) { create(:ci_pipeline) }
let(:current_user) { create(:user) }
@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed iid' do
expect(Ci::PipelinesFinder)
.to receive(:new)
.with(project, current_user, iids: ['1234'])
.and_call_original
result = batch_sync do
resolve_pipeline(project, { iid: '1234' })
end
@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed sha' do
expect(Ci::PipelinesFinder)
.to receive(:new)
.with(project, current_user, sha: ['sha'])
.and_call_original
result = batch_sync do
resolve_pipeline(project, { sha: 'sha' })
end
@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for iid' do
create(:ci_pipeline, project: project, iid: '1235')
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
end
@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for sha' do
create(:ci_pipeline, project: project, sha: 'sha2')
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { sha: 'sha' }) }
end

View File

@ -9,51 +9,57 @@ RSpec.describe Resolvers::TimelogResolver do
expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
end
context "within a group" do
context "with a group" do
let_it_be(:current_user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :public, group: group) }
before do
before_all do
group.add_developer(current_user)
project.add_developer(current_user)
end
before do
group.clear_memoization(:timelogs)
end
describe '#resolve' do
let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:issue2) { create(:issue, project: project) }
let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) }
it 'finds all timelogs within given dates' do
timelogs = resolve_timelogs(args)
timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1)
end
it 'return nothing when user has insufficient permissions' do
user = create(:user)
group.add_guest(current_user)
expect(resolve_timelogs(args)).to be_empty
expect(resolve_timelogs(user: user, **args)).to be_empty
end
context 'when start_time and end_date are present' do
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(args)
timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
context 'finds timelogs until the time specified on end_time' do
context 'when start_date and end_time are present' do
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
it 'finds all timelogs within start_date and end_time' do
timelogs = resolve_timelogs(args)
timelogs = resolve_timelogs(**args)
expect(timelogs).to contain_exactly(timelog1)
end
@ -66,7 +72,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { {} }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start and End arguments must be present/)
end
end
@ -75,7 +81,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -84,7 +90,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -93,7 +99,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: 6.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -102,7 +108,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_date: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -111,7 +117,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -120,7 +126,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
@ -129,7 +135,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Only Time or Date arguments must be present/)
end
end
@ -138,7 +144,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /Start argument must be before End argument/)
end
end
@ -147,7 +153,7 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
expect { resolve_timelogs(**args) }
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
end
end
@ -155,7 +161,8 @@ RSpec.describe Resolvers::TimelogResolver do
end
end
def resolve_timelogs(args = {}, context = { current_user: current_user })
def resolve_timelogs(user: current_user, **args)
context = { current_user: user }
resolve(described_class, obj: group, args: args, ctx: context)
end
end

View File

@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
create_note_email]
create_note_email timelogs]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)

View File

@ -12,4 +12,6 @@ RSpec.describe GitlabSchema.types['Repository'] do
specify { expect(described_class).to have_graphql_field(:tree) }
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
specify { expect(described_class).to have_graphql_field(:blobs) }
end

View File

@ -112,6 +112,46 @@ RSpec.describe ProfilesHelper do
end
end
describe "#ssh_key_expiration_tooltip" do
using RSpec::Parameterized::TableSyntax
before do
allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
end
error_message = 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
where(:error, :expired, :result) do
false | false | nil
true | false | error_message
false | true | 'Key usable beyond expiration date.'
true | true | error_message
end
with_them do
let_it_be(:key) do
build(:personal_key)
end
it do
key.expires_at = expired ? 2.days.ago : 2.days.from_now
key.errors.add(:base, error_message) if error
expect(helper.ssh_key_expiration_tooltip(key)).to eq(result)
end
end
end
describe "#ssh_key_expires_field_description" do
before do
allow(Key).to receive(:enforce_ssh_key_expiration_feature_available?).and_return(false)
end
it 'returns the description' do
expect(helper.ssh_key_expires_field_description).to eq('Key can still be used after expiration.')
end
end
def stub_cas_omniauth_provider
provider = OpenStruct.new(
'name' => 'cas3',

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do
let(:helper_class) do
Class.new do
include Gitlab::Ci::Pipeline::Chain::Helpers
attr_accessor :pipeline, :command
def initialize(pipeline, command)
self.pipeline = pipeline
self.command = command
end
end
end
subject(:helper) { helper_class.new(pipeline, command) }
let(:pipeline) { build(:ci_empty_pipeline) }
let(:command) { double(save_incompleted: true) }
let(:message) { 'message' }
describe '.error' do
shared_examples 'error function' do
specify do
expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original
expect(pipeline).to receive(:add_error_message).with(message).and_call_original
expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original
subject.error(message, config_error: config_error, drop_reason: drop_reason)
expect(pipeline.yaml_errors).to eq(yaml_error)
expect(pipeline.errors[:base]).to include(message)
end
end
context 'when given a drop reason' do
context 'when config error is true' do
context 'sets the yaml error and overrides the drop reason' do
let(:drop_reason) { :config_error }
let(:config_error) { true }
let(:yaml_error) { message }
it_behaves_like "error function"
end
end
context 'when config error is false' do
context 'does not set the yaml error or override the drop reason' do
let(:drop_reason) { :size_limit_exceeded }
let(:config_error) { false }
let(:yaml_error) { nil }
it_behaves_like "error function"
end
end
end
context 'when the ci_pipeline_ensure_iid_on_drop feature flag is false' do
it 'does not ensure the project iid' do
stub_feature_flags(ci_pipeline_ensure_iid_on_drop: false)
expect(pipeline).to receive(:ensure_project_iid!).once
subject.error(message, config_error: true)
end
end
end
end

View File

@ -407,13 +407,13 @@ RSpec.describe Gitlab::Database do
expect(described_class.db_read_only?).to be_truthy
end
it 'detects a read write database' do
it 'detects a read-write database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
expect(described_class.db_read_only?).to be_falsey
end
it 'detects a read write database' do
it 'detects a read-write database' do
allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }])
expect(described_class.db_read_only?).to be_falsey

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::IssueBuilder do
let_it_be(:label) { create(:label) }
let_it_be(:issue) { create(:labeled_issue, labels: [label], project: label.project) }
let(:builder) { described_class.new(issue) }
describe '#build' do

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::MergeRequestBuilder do
let_it_be(:merge_request) { create(:merge_request) }
let(:builder) { described_class.new(merge_request) }
describe '#build' do

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::HookData::ReleaseBuilder do
let_it_be(:project) { create(:project, :public, :repository) }
let(:release) { create(:release, project: project) }
let(:builder) { described_class.new(release) }

View File

@ -32,18 +32,16 @@ RSpec.describe HasTimelogsReport do
end
describe '#user_can_access_group_timelogs?' do
before do
group.add_developer(user)
end
it 'returns true if user can access group timelogs' do
expect(group.user_can_access_group_timelogs?(user)).to be_truthy
group.add_developer(user)
expect(group).to be_user_can_access_group_timelogs(user)
end
it 'returns false if user has insufficient permissions' do
group.add_guest(user)
expect(group.user_can_access_group_timelogs?(user)).to be_falsey
expect(group).not_to be_user_can_access_group_timelogs(user)
end
end

View File

@ -5795,16 +5795,34 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#find_or_initialize_services' do
before do
allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
let_it_be(:subject) { create(:project) }
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }.count
expect(control_count).to be <= 4
end
it 'returns only enabled services' do
services = subject.find_or_initialize_services
it 'avoids N+1 database queries with more available services' do
allow(Service).to receive(:available_services_names).and_return(%w[pushover])
control_count = ActiveRecord::QueryRecorder.new { subject.find_or_initialize_services }
expect(services.count).to eq(2)
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
allow(Service).to receive(:available_services_names).and_call_original
expect { subject.find_or_initialize_services }.not_to exceed_query_limit(control_count)
end
context 'with disabled services' do
before do
allow(Service).to receive(:available_services_names).and_return(%w[prometheus pushover teamcity])
allow(subject).to receive(:disabled_services).and_return(%w[prometheus])
end
it 'returns only enabled services sorted' do
services = subject.find_or_initialize_services
expect(services.size).to eq(2)
expect(services.map(&:title)).to eq(['JetBrains TeamCity CI', 'Pushover'])
end
end
end

View File

@ -56,9 +56,9 @@ RSpec.describe Timelog do
group = create(:group)
subgroup = create(:group, parent: group)
create(:timelog, issue: create(:issue, project: create(:project)))
timelog1 = create(:timelog, issue: create(:issue, project: create(:project, group: group)))
timelog2 = create(:timelog, issue: create(:issue, project: create(:project, group: subgroup)))
create(:issue_timelog)
timelog1 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: group)))
timelog2 = create(:issue_timelog, issue: create(:issue, project: create(:project, group: subgroup)))
expect(described_class.for_issues_in_group(group)).to contain_exactly(timelog1, timelog2)
end
@ -66,9 +66,9 @@ RSpec.describe Timelog do
describe 'between_times' do
it 'returns collection of timelogs within given times' do
create(:timelog, spent_at: 65.days.ago)
timelog1 = create(:timelog, spent_at: 15.days.ago)
timelog2 = create(:timelog, spent_at: 5.days.ago)
create(:issue_timelog, spent_at: 65.days.ago)
timelog1 = create(:issue_timelog, spent_at: 15.days.ago)
timelog2 = create(:issue_timelog, spent_at: 5.days.ago)
timelogs = described_class.between_times(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2)

View File

@ -14,6 +14,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
before do
@ -34,11 +35,11 @@ RSpec.describe 'Timelogs through GroupQuery' do
end
it 'contains correct data', :aggregate_failures do
username = timelog_array.map {|data| data['user']['username'] }
username = timelog_array.map { |data| data['user']['username'] }
spent_at = timelog_array.map { |data| data['spentAt'].to_time }
time_spent = timelog_array.map { |data| data['timeSpent'] }
issue_title = timelog_array.map {|data| data['issue']['title'] }
milestone_title = timelog_array.map {|data| data['issue']['milestone']['title'] }
issue_title = timelog_array.map { |data| data['issue']['title'] }
milestone_title = timelog_array.map { |data| data['issue']['milestone']['title'] }
expect(username).to eq([user.username])
expect(spent_at.first).to be_like_time(timelog1.spent_at)
@ -50,7 +51,7 @@ RSpec.describe 'Timelogs through GroupQuery' do
context 'when arguments with no time are present' do
let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }}
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' } }
it 'sets times as start of day and end of day' do
expect(response).to have_gitlab_http_status(:ok)
@ -111,12 +112,10 @@ RSpec.describe 'Timelogs through GroupQuery' do
}
NODE
graphql_query_for("group", { "fullPath" => group.full_path },
[query_graphql_field(
"timelogs",
timelog_params,
timelog_nodes
)]
graphql_query_for(
:group,
{ full_path: group.full_path },
query_graphql_field(:timelogs, timelog_params, timelog_nodes)
)
end
end

View File

@ -5,14 +5,14 @@ require 'spec_helper'
RSpec.describe 'getting an issue list for a project' do
include GraphqlHelpers
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let(:fields) do
<<~QUERY
edges {
@ -76,7 +76,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
context 'no limit is provided' do
context 'when no limit is provided' do
let(:issue_limit) { nil }
it 'returns all issues' do
@ -143,13 +143,15 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:data_path) { [:project, :issues] }
def pagination_query(params)
graphql_query_for(:project, { full_path: sort_project.full_path },
graphql_query_for(
:project,
{ full_path: sort_project.full_path },
query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
)
end
def pagination_results_data(data)
data.map { |issue| issue.dig('iid').to_i }
data.map { |issue| issue['iid'].to_i }
end
context 'when sorting by due date' do
@ -189,27 +191,38 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
let(:expected_results) do
[
relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
relative_issue4.iid, relative_issue2.iid
]
end
end
end
end
context 'when sorting by priority' do
let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) }
let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) }
let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) }
let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) }
let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
let_it_be(:priority_issue4) { create(:issue, project: sort_project) }
let_it_be(:on_project) { { project: sort_project } }
let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
let_it_be(:priority_1) { create(:label, **on_project, priority: 1) }
let_it_be(:priority_2) { create(:label, **on_project, priority: 5) }
let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) }
let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) }
let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) }
let_it_be(:priority_issue4) { create(:issue, **on_project) }
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_ASC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
let(:expected_results) do
[
priority_issue3.iid, priority_issue1.iid,
priority_issue2.iid, priority_issue4.iid
]
end
end
end
@ -217,7 +230,9 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_DESC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
let(:expected_results) do
[priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid]
end
end
end
end
@ -275,7 +290,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
context 'fetching alert management alert' do
context 'when fetching alert management alert' do
let(:fields) do
<<~QUERY
edges {
@ -297,7 +312,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
create(:alert_management_alert, :with_issue, project: project )
create(:alert_management_alert, :with_issue, project: project)
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
end
@ -312,7 +327,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
context 'fetching labels' do
context 'when fetching labels' do
let(:fields) do
<<~QUERY
edges {
@ -362,7 +377,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
context 'fetching assignees' do
context 'when fetching assignees' do
let(:fields) do
<<~QUERY
edges {
@ -420,9 +435,10 @@ RSpec.describe 'getting an issue list for a project' do
query = graphql_query_for(
:project,
{ full_path: project.full_path },
query_graphql_field(:issues, search_params, [
query_graphql_field(
:issues, search_params,
query_graphql_field(:nodes, nil, requested_fields)
])
)
)
post_graphql(query, current_user: current_user)
end
@ -448,5 +464,16 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check'
end
context 'when requesting `timelogs`' do
let(:requested_fields) { 'timelogs { nodes { timeSpent } }' }
before do
create_list(:issue_timelog, 2, issue: issue_a)
create(:issue_timelog, issue: issue_b)
end
include_examples 'N+1 query check'
end
end
end

View File

@ -299,6 +299,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
reviewers { nodes { username } }
participants { nodes { username } }
headPipeline { status }
timelogs { nodes { timeSpent } }
SELECT
end
@ -307,7 +308,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
query($first: Int) {
project(fullPath: "#{project.full_path}") {
mergeRequests(first: $first) {
nodes { #{mr_fields} }
nodes { iid #{mr_fields} }
}
}
}
@ -324,6 +325,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
mr.assignees << current_user
mr.reviewers << create(:user)
mr.reviewers << current_user
mr.timelogs << create(:merge_request_timelog, merge_request: mr)
end
end
@ -345,7 +347,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
def user_collection
{ 'nodes' => all(match(a_hash_including('username' => be_present))) }
{ 'nodes' => be_present.and(all(match(a_hash_including('username' => be_present)))) }
end
it 'returns appropriate results' do
@ -358,7 +360,8 @@ RSpec.describe 'getting merge request listings nested in a project' do
'assignees' => user_collection,
'reviewers' => user_collection,
'participants' => user_collection,
'headPipeline' => { 'status' => be_present }
'headPipeline' => { 'status' => be_present },
'timelogs' => { 'nodes' => be_one }
)))
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Groups::AutoDevopsService, '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let(:group_params) { { auto_devops_enabled: '0' } }
let(:service) { described_class.new(group, user, group_params) }

View File

@ -8,6 +8,7 @@ RSpec.describe Groups::GroupLinks::UpdateService, '#execute' do
let_it_be(:group) { create(:group, :private) }
let_it_be(:shared_group) { create(:group, :private) }
let_it_be(:project) { create(:project, group: shared_group) }
let(:group_member_user) { create(:user) }
let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }

View File

@ -5,12 +5,14 @@ require 'spec_helper'
RSpec.describe Groups::TransferService do
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
let(:transfer_service) { described_class.new(group, user) }
context 'handling packages' do
let_it_be(:group) { create(:group, :public) }
let_it_be(:new_group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
before do
@ -272,6 +274,7 @@ RSpec.describe Groups::TransferService do
context 'with a group integration' do
let_it_be(:instance_integration) { create(:slack_service, :instance, webhook: 'http://project.slack.com') }
let(:new_created_integration) { Service.find_by(group: group) }
context 'with an inherited integration' do

View File

@ -59,6 +59,7 @@ RSpec.describe Groups::UpdateSharedRunnersService do
context 'disable shared Runners' do
let_it_be(:group) { create(:group) }
let(:params) { { shared_runners_setting: 'disabled_and_unoverridable' } }
it 'receives correct method and succeeds' do

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::BaseConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:sha) { 'sha' }
describe '#execute' do

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::SchemasConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:filename) { 'sample.yml' }
let(:schema_content) { double(body: '{"title":"Sample schema"}') }

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Ide::TerminalConfigService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:sha) { 'sha' }
describe '#execute' do

View File

@ -6,6 +6,7 @@ RSpec.describe Issues::BuildService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:user) { developer }
before_all do

View File

@ -242,6 +242,7 @@ RSpec.describe Issues::CloneService do
context 'issue with a design', :clean_gitlab_redis_shared_state do
let_it_be(:new_project) { create(:project) }
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
let(:subject) { clone_service.execute(old_issue, new_project) }

View File

@ -11,6 +11,7 @@ RSpec.describe Issues::CreateService do
describe '#execute' do
let_it_be(:assignee) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:issue) { described_class.new(project, user, opts).execute }
context 'when params are valid' do

View File

@ -8,6 +8,7 @@ RSpec.describe Issues::ExportCsvService do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:issue) { create(:issue, project: project, author: user) }
let_it_be(:bad_issue) { create(:issue, project: project, author: user) }
subject { described_class.new(Issue.all, project) }
it 'renders csv to string' do

View File

@ -206,6 +206,7 @@ RSpec.describe Issues::MoveService do
context 'issue with a design', :clean_gitlab_redis_shared_state do
let_it_be(:new_project) { create(:project) }
let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
let(:subject) { move_service.execute(old_issue, new_project) }

View File

@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe Issues::RelatedBranchesService do
let_it_be(:developer) { create(:user) }
let_it_be(:issue) { create(:issue) }
let(:user) { developer }
subject { described_class.new(issue.project, user) }

View File

@ -724,9 +724,7 @@ RSpec.describe Projects::CreateService, '#execute' do
it 'cleans invalid record and logs warning', :aggregate_failures do
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
allow_next_instance_of(Project) do |instance|
allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
end
allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) }))
project = create_project(user, opts)

View File

@ -55,6 +55,12 @@ RSpec.describe Projects::UpdatePagesService do
end
end
it 'creates a temporary directory with the project and build ID' do
expect(Dir).to receive(:mktmpdir).with("project-#{project.id}-build-#{build.id}-", anything).and_call_original
subject.execute
end
it "doesn't deploy to legacy storage if it's disabled" do
allow(Settings.pages.local_store).to receive(:enabled).and_return(false)

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'profiles/keys/_form.html.haml' do
let_it_be(:key) { Key.new }
let(:page) { Capybara::Node::Simple.new(rendered) }
before do
assign(:key, key)
end
context 'when the form partial is used' do
before do
allow(view).to receive(:ssh_key_expires_field_description).and_return('Key can still be used after expiration.')
render
end
it 'renders the form with the correct action' do
expect(page.find('form')['action']).to eq('/-/profile/keys')
end
it 'has the key field', :aggregate_failures do
expect(rendered).to have_field('Key', type: 'textarea', placeholder: 'Typically starts with "ssh-ed25519 …" or "ssh-rsa …"')
expect(rendered).to have_text("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity.")
end
it 'has the title field', :aggregate_failures do
expect(rendered).to have_field('Title', type: 'text', placeholder: 'e.g. My MacBook key')
expect(rendered).to have_text('Give your individual key a title.')
end
it 'has the expires at field', :aggregate_failures do
expect(rendered).to have_field('Expires at', type: 'date')
expect(page.find_field('Expires at')['min']).to eq(l(1.day.from_now, format: "%Y-%m-%d"))
expect(rendered).to have_text('Key can still be used after expiration.')
end
it 'has the validation warning', :aggregate_failures do
expect(rendered).to have_text("Oops, are you sure? Publicly visible private SSH keys can compromise your system.")
expect(rendered).to have_button('Yes, add it')
end
it 'has the submit button' do
expect(rendered).to have_button('Add key')
end
end
end

View File

@ -0,0 +1,109 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'profiles/keys/_key.html.haml' do
let_it_be(:user) { create(:user) }
before do
allow(view).to receive(:key).and_return(key)
allow(view).to receive(:is_admin).and_return(false)
end
context 'when the key partial is used' do
let_it_be(:key) do
create(:personal_key,
user: user,
last_used_at: 7.days.ago,
expires_at: 2.days.from_now)
end
it 'displays the correct values', :aggregate_failures do
render
expect(rendered).to have_text(key.title)
expect(rendered).to have_css('[data-testid="key-icon"]')
expect(rendered).to have_text(key.fingerprint)
expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
expect(rendered).to have_text(l(key.created_at, format: "%b %d, %Y"))
expect(rendered).to have_text(key.expires_at.to_date)
expect(response).to render_template(partial: 'shared/ssh_keys/_key_delete')
end
context 'when the key has not been used' do
let_it_be(:key) do
create(:personal_key,
user: user,
last_used_at: nil)
end
it 'renders "Never" for last used' do
render
expect(rendered).to have_text('Last used: Never')
end
end
context 'when the key does not have an expiration date' do
let_it_be(:key) do
create(:personal_key,
user: user,
expires_at: nil)
end
it 'renders "Never" for expires' do
render
expect(rendered).to have_text('Expires: Never')
end
end
context 'when the key is not deletable' do
# Turns out key.can_delete? is only false for LDAP keys
# but LDAP keys don't exist outside EE
before do
allow(key).to receive(:can_delete?).and_return(false)
end
it 'does not render the partial' do
render
expect(response).not_to render_template(partial: 'shared/ssh_keys/_key_delete')
end
end
context 'icon tooltip' do
using RSpec::Parameterized::TableSyntax
where(:valid, :expiry, :result) do
false | 2.days.from_now | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
false | 2.days.ago | 'Key type is forbidden. Must be DSA, ECDSA, or ED25519'
true | 2.days.ago | 'Key usable beyond expiration date.'
true | 2.days.from_now | ''
end
with_them do
let_it_be(:key) do
create(:personal_key, user: user)
end
it 'renders the correct icon', :aggregate_failures do
unless valid
stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE)
end
key.expires_at = expiry
render
if result.empty?
expect(rendered).to have_css('[data-testid="key-icon"]')
else
expect(rendered).to have_css('[data-testid="warning-solid-icon"]')
expect(rendered).to have_selector("span.has-tooltip[title='#{result}']")
end
end
end
end
end
end

View File

@ -64,10 +64,7 @@ RSpec.describe Projects::PostCreationWorker do
it 'cleans invalid record and logs warning', :aggregate_failures do
invalid_service_record = build(:prometheus_service, properties: { api_url: nil, manual_configuration: true }.to_json)
allow_next_found_instance_of(Project) do |instance|
allow(instance).to receive(:build_prometheus_service).and_return(invalid_service_record)
end
allow(PrometheusService).to receive(:new).and_return(invalid_service_record)
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice
subject

Some files were not shown because too many files have changed in this diff Show More