Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-09-12 12:11:33 +00:00
parent fab43fda65
commit 0127158127
84 changed files with 1332 additions and 479 deletions

View File

@ -3393,12 +3393,9 @@ RSpec/MissingFeatureCategory:
- 'spec/lib/gitlab/database/migrations/test_background_runner_spec.rb'
- 'spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb'
- 'spec/lib/gitlab/database/partitioning/detached_partition_dropper_spec.rb'
- 'spec/lib/gitlab/database/partitioning/monthly_strategy_spec.rb'
- 'spec/lib/gitlab/database/partitioning/partition_manager_spec.rb'
- 'spec/lib/gitlab/database/partitioning/partition_monitoring_spec.rb'
- 'spec/lib/gitlab/database/partitioning/replace_table_spec.rb'
- 'spec/lib/gitlab/database/partitioning/single_numeric_list_partition_spec.rb'
- 'spec/lib/gitlab/database/partitioning/sliding_list_strategy_spec.rb'
- 'spec/lib/gitlab/database/partitioning/time_partition_spec.rb'
- 'spec/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table_spec.rb'
- 'spec/lib/gitlab/database/partitioning_migration_helpers/index_helpers_spec.rb'

View File

@ -0,0 +1,34 @@
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlLink,
},
props: {
externalLinks: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div>
<div class="title gl-font-weight-bold">{{ s__('Job|External links') }}</div>
<ul class="gl-list-style-none gl-p-0 gl-m-0">
<li v-for="(externalLink, index) in externalLinks" :key="index">
<gl-link
:href="externalLink.url"
target="_blank"
rel="noopener noreferrer nofollow"
class="gl-text-blue-600!"
>
<gl-icon name="external-link" class="flex-shrink-0" />
{{ externalLink.label }}
</gl-link>
</li>
</ul>
</div>
</template>

View File

@ -3,8 +3,10 @@ import { isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { forwardDeploymentFailureModalId } from '~/ci/constants';
import { filterAnnotations } from '~/ci/job_details/utils';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
import ExternalLinksBlock from './external_links_block.vue';
import JobsContainer from './jobs_container.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
@ -25,6 +27,7 @@ export default {
SidebarHeader,
StagesDropdown,
TriggerBlock,
ExternalLinksBlock,
},
props: {
artifactHelpUrl: {
@ -40,6 +43,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
hasExternalLinks() {
return this.externalLinks.length > 0;
},
hasTriggers() {
return !isEmpty(this.job.trigger);
},
@ -52,6 +58,9 @@ export default {
shouldShowJobRetryForwardDeploymentModal() {
return this.job.retry_path && this.hasForwardDeploymentFailure;
},
externalLinks() {
return filterAnnotations(this.job.annotations, 'external_link');
},
},
watch: {
job(value, oldValue) {
@ -88,6 +97,13 @@ export default {
:help-url="artifactHelpUrl"
/>
<external-links-block
v-if="hasExternalLinks"
class="gl-py-4"
:class="$options.borderTopClass"
:external-links="externalLinks"
/>
<trigger-block
v-if="hasTriggers"
class="gl-py-4"

View File

@ -20,3 +20,10 @@ export const compactJobLog = (jobLog) => {
return compactedLog;
};
export const filterAnnotations = (annotations, type) => {
return [...annotations]
.sort((a, b) => a.name.localeCompare(b.name))
.flatMap((annotationList) => annotationList.data)
.flatMap((annotation) => annotation[type] ?? []);
};

View File

@ -54,6 +54,7 @@ export default {
:title="emptyStateText.title"
:description="emptyStateText.description"
:svg-path="emptyStateImagePath"
:svg-height="150"
:primary-button-link="testReportDocPath"
:primary-button-text="emptyStateText.button"
/>

View File

@ -18,6 +18,15 @@ export const initAccessDropdown = (el, options) => {
return new Vue({
el,
name: 'AccessDropdownRoot',
data() {
return { preselected };
},
methods: {
setPreselectedItems(items) {
this.preselected = items;
},
},
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
@ -25,7 +34,7 @@ export const initAccessDropdown = (el, options) => {
label,
disabled,
accessLevelsData: accessLevelsData.roles,
preselectedItems: preselected,
preselectedItems: this.preselected,
...props,
},
on: {
@ -35,6 +44,9 @@ export const initAccessDropdown = (el, options) => {
shown() {
vm.$emit('shown');
},
hidden() {
vm.$emit('hidden');
},
},
});
},

View File

@ -2,28 +2,23 @@ import { find } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
`.${ACCESS_LEVELS.MERGE}-container`,
);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
`.${ACCESS_LEVELS.PUSH}-container`,
);
this.selectedItems = {
[ACCESS_LEVELS.PUSH]: [],
[ACCESS_LEVELS.MERGE]: [],
};
this.initDropdowns();
this.buildDropdowns();
this.initToggles();
}
@ -67,6 +62,66 @@ export default class ProtectedBranchEdit {
}
}
initDropdowns() {
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown(
'js-allowed-to-merge',
ACCESS_LEVELS.MERGE,
gon.merge_access_levels,
'protected-branch-allowed-to-merge',
);
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown(
'js-allowed-to-push',
ACCESS_LEVELS.PUSH,
gon.push_access_levels,
'protected-branch-allowed-to-push',
);
}
buildDropdown(selector, accessLevel, accessLevelsData, testId) {
const [el] = this.$wrap.find(`.${selector}`);
if (!el) return undefined;
const projectId = gon.current_project_id;
const dropdown = initAccessDropdown(el, {
toggleClass: selector,
hasLicense: this.hasLicense,
searchEnabled: el.dataset.filter !== undefined,
showUsers: projectId !== undefined,
block: true,
accessLevel,
accessLevelsData,
testId,
});
dropdown.$on('select', (selected) => this.onSelectItems(accessLevel, selected));
dropdown.$on('hidden', () => this.onDropdownHide());
this.initSelectedItems(dropdown, accessLevel);
return dropdown;
}
initSelectedItems(dropdown, accessLevel) {
this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
if (item.type === LEVEL_TYPES.GROUP) return { id: item.id, group_id: item.group_id };
return { id: item.id, deploy_key_id: item.deploy_key_id };
});
}
onSelectItems(accessLevel, selected) {
this.selectedItems[accessLevel] = selected;
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) return;
this.updatePermissions();
}
updateProtectedBranch(formData, callback) {
axios
.patch(this.$wrap.data('url'), {
@ -78,79 +133,25 @@ export default class ProtectedBranchEdit {
});
}
buildDropdowns() {
// Allowed to merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
hasLicense: this.hasLicense,
});
// Allowed to push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
hasLicense: this.hasLicense,
});
}
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
const formData = Object.values(ACCESS_LEVELS).reduce((acc, level) => {
acc[`${level}_attributes`] = this.selectedItems[level];
return acc;
}, {});
axios
.patch(this.$wrap.data('url'), {
protected_branch: formData,
})
.then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
});
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
})
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
createAlert({ message: __('Failed to update branch!') });
this.updateProtectedBranch(formData, ({ data }) => {
this.hasChanges = false;
Object.values(ACCESS_LEVELS).forEach((level) => {
this.setSelectedItemsToDropdown(data[level], level);
});
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
setSelectedItemsToDropdown(items = [], accessLevel) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const selectedItems = this.selectedItems[accessLevel];
const currentSelectedItem = find(selectedItems, {
user_id: currentItem.user_id,
});
@ -182,6 +183,7 @@ export default class ProtectedBranchEdit {
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
this.selectedItems[accessLevel] = itemsToAdd;
this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
}
}

View File

@ -3,8 +3,11 @@
import { mapState, mapGetters } from 'vuex';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import {
SCOPE_ISSUES,
SCOPE_MERGE_REQUESTS,
@ -21,12 +24,14 @@ export default {
name: 'GlobalSearchSidebar',
components: {
IssuesFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
MergeRequestsFilters,
BlobsFilters,
ProjectsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
DomElementListener,
SmallScreenDrawerNavigation,
},
mixins: [glFeatureFlagsMixin()],
computed: {
@ -53,11 +58,17 @@ export default {
return Boolean(this.currentScope);
},
},
methods: {
toggleFiltersFromSidebar() {
toggleSuperSidebarCollapsed();
},
},
};
</script>
<template>
<section v-if="useSidebarNavigation">
<dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" />
<sidebar-portal>
<scope-sidebar-navigation />
<issues-filters v-if="showIssuesFilters" />
@ -66,14 +77,24 @@ export default {
<projects-filters v-if="showProjectsFilters" />
</sidebar-portal>
</section>
<section
v-else-if="showScopeNavigation"
class="search-sidebar gl-display-flex gl-flex-direction-column gl-md-mr-5 gl-mb-6 gl-mt-5"
class="gl-display-flex gl-flex-direction-column gl-lg-mr-0 gl-md-mr-5 gl-lg-mb-6 gl-lg-mt-5"
>
<scope-legacy-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
<blobs-filters v-if="showBlobFilters" />
<projects-filters v-if="showProjectsFilters" />
<div class="search-sidebar gl-display-none gl-lg-display-block">
<scope-legacy-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
<blobs-filters v-if="showBlobFilters" />
<projects-filters v-if="showProjectsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
<blobs-filters v-if="showBlobFilters" />
<projects-filters v-if="showProjectsFilters" />
</small-screen-drawer-navigation>
</section>
</template>

View File

@ -7,6 +7,7 @@ export const TRACKING_LABEL_CHECKBOX = 'checkbox';
const scopes = {
PROJECTS: 'projects',
ISSUES: 'issues',
};
const filterParam = 'include_archived';

View File

@ -14,7 +14,7 @@ export default {
GlFormCheckbox,
},
computed: {
...mapState(['urlQuery']),
...mapState(['urlQuery', 'useSidebarNavigation']),
selectedFilter: {
get() {
return [parseBoolean(this.urlQuery?.include_archived)];
@ -41,7 +41,9 @@ export default {
<template>
<gl-form-checkbox-group v-model="selectedFilter">
<h5>{{ $options.archivedFilterData.headerLabel }}</h5>
<h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ $options.archivedFilterData.headerLabel }}
</h5>
<gl-form-checkbox
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
:class="$options.LABEL_DEFAULT_CLASSES"

View File

@ -21,9 +21,6 @@ export default {
computed: {
...mapState(['sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
},
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@ -40,14 +37,15 @@ export default {
this.resetQuery();
},
},
HR_DEFAULT_CLASSES,
};
</script>
<template>
<gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
<hr v-if="!useSidebarNavigation" :class="hrClasses" />
<hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<slot></slot>
<hr v-if="!useSidebarNavigation" :class="hrClasses" />
<hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}

View File

@ -2,13 +2,15 @@
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HR_DEFAULT_CLASSES } from '../constants/index';
import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
import { confidentialFilterData } from './confidentiality_filter/data';
import { statusFilterData } from './status_filter/data';
import ConfidentialityFilter from './confidentiality_filter/index.vue';
import { labelFilterData } from './label_filter/data';
import { archivedFilterData } from './archived_filter/data';
import LabelFilter from './label_filter/index.vue';
import StatusFilter from './status_filter/index.vue';
import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@ -19,11 +21,12 @@ export default {
ConfidentialityFilter,
LabelFilter,
FiltersTemplate,
ArchivedFilter,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation']),
...mapState(['useSidebarNavigation', 'searchType']),
showConfidentialityFilter() {
return Object.values(confidentialFilterData.scopes).includes(this.currentScope);
},
@ -33,7 +36,15 @@ export default {
showLabelFilter() {
return (
Object.values(labelFilterData.scopes).includes(this.currentScope) &&
this.glFeatures.searchIssueLabelAggregation
this.glFeatures.searchIssueLabelAggregation &&
this.searchType === SEARCH_TYPE_ADVANCED
);
},
showArchivedFilter() {
return (
Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
this.glFeatures.searchIssuesHideArchivedProjects &&
this.searchType === SEARCH_TYPE_ADVANCED
);
},
showDivider() {
@ -52,6 +63,8 @@ export default {
<hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
<hr v-if="showLabelFilter && showDivider" :class="hrClasses" />
<label-filter v-if="showLabelFilter" />
<label-filter v-if="showLabelFilter" class="gl-mb-5" />
<hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
<archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>

View File

@ -57,7 +57,7 @@ export default {
</script>
<template>
<nav data-testid="search-filter">
<nav data-testid="search-filter" class="gl-border-none">
<gl-nav vertical pills>
<gl-nav-item
v-for="(item, scope) in navigation"
@ -81,6 +81,5 @@ export default {
</span>
</gl-nav-item>
</gl-nav>
<hr class="gl-mt-5 gl-mx-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
</nav>
</template>

View File

@ -0,0 +1,61 @@
<script>
import { GlDrawer } from '@gitlab/ui';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import { s__ } from '~/locale';
export default {
name: 'SmallScreenDrawerNavigation',
components: {
GlDrawer,
DomElementListener,
},
i18n: {
smallScreenFiltersDrawerHeader: s__('GlobalSearch|Filters'),
},
data() {
return {
openSmallScreenFilters: false,
};
},
computed: {
getDrawerHeaderHeight() {
if (!this.openSmallScreenFilters) return '0';
return getContentWrapperHeight();
},
},
methods: {
closeSmallScreenFilters() {
this.openSmallScreenFilters = false;
},
toggleSmallScreenFilters() {
this.openSmallScreenFilters = !this.openSmallScreenFilters;
},
},
DRAWER_Z_INDEX,
};
</script>
<template>
<dom-element-listener selector="#js-open-mobile-filters" @click="toggleSmallScreenFilters">
<gl-drawer
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
variant="sidebar"
class="small-screen-drawer-navigation"
:open="openSmallScreenFilters"
@close="closeSmallScreenFilters"
>
<template #title>
<h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
{{ $options.i18n.smallScreenFiltersDrawerHeader }}
</h2>
</template>
<template #default>
<div>
<slot></slot>
</div>
</template>
</gl-drawer>
</dom-element-listener>
</template>

View File

@ -138,7 +138,7 @@ export const setLabelFilterSearch = ({ commit }, { value }) => {
export const fetchSidebarCount = ({ commit, state }) => {
const promises = Object.values(state.navigation).map((navItem) => {
// active nav item has count already so we skip it
if (!navItem.active) {
if (!navItem.active && navItem.count_link) {
return axios
.get(navItem.count_link)
.then(({ data: { count } }) => {

View File

@ -33,7 +33,7 @@ export default {
state.frequentItems[key] = data;
},
[types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) {
const item = { ...state.navigation[key], count };
const item = { ...state.navigation[key], count, count_link: null };
state.navigation = { ...state.navigation, [key]: item };
},
[types.REQUEST_AGGREGATIONS](state) {

View File

@ -4,6 +4,7 @@
@import './pages/groups';
@import './pages/hierarchy';
@import './pages/issues';
@import './pages/labels';
@import './pages/note_form';
@import './pages/notes';
@import './pages/pipelines';

View File

@ -1,5 +1,3 @@
@import 'mixins_and_variables_and_functions';
.suggest-colors {
padding-top: 3px;
@ -31,19 +29,19 @@
margin-bottom: -5px;
&:first-of-type {
border-top-left-radius: $gl-border-radius-base;
border-top-left-radius: $border-radius-base;
}
&:nth-of-type(7) {
border-top-right-radius: $gl-border-radius-base;
border-top-right-radius: $border-radius-base;
}
&:nth-last-child(7) {
border-bottom-left-radius: $gl-border-radius-base;
border-bottom-left-radius: $border-radius-base;
}
&:last-of-type {
border-bottom-right-radius: $gl-border-radius-base;
border-bottom-right-radius: $border-radius-base;
}
}
}

View File

@ -5,21 +5,13 @@ module Harbor
extend ActiveSupport::Concern
included do
before_action :harbor_registry_enabled!
before_action :authorize_read_harbor_registry!
before_action do
push_frontend_feature_flag(:harbor_registry_integration)
end
feature_category :integrations
end
private
def harbor_registry_enabled!
render_404 unless Feature.enabled?(:harbor_registry_integration, defined?(group) ? group : project)
end
def authorize_read_harbor_registry!
raise NotImplementedError
end

View File

@ -18,7 +18,7 @@ module Projects
suite_endpoint: project_pipeline_test_path(project, pipeline, suite_name: 'suite', format: :json),
blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.has_test_reports?,
empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
empty_state_image_path: image_path('illustrations/empty-todos-md.svg'),
empty_dag_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'),
artifacts_expired_image_path: image_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count]

View File

@ -107,7 +107,10 @@ module Ci
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: proc { false },
detach_partition_if: proc { false }
detach_partition_if: proc { false },
# Most of the db tasks are run in a weekly basis, e.g. execute_batched_migrations.
# Therefore, let's start with 1.week and see how it'd go.
analyze_interval: 1.week
end
end
end

View File

@ -7,10 +7,7 @@ module RequireEmailVerification
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
# This value is twice the amount we want it to be, because due to a bug in the devise-two-factor
# gem every failed login attempt increments the value of failed_attempts by 2 instead of 1.
# See: https://github.com/tinfoil/devise-two-factor/issues/127
MAXIMUM_ATTEMPTS = 3 * 2
MAXIMUM_ATTEMPTS = 3
UNLOCK_IN = 24.hours
included do

View File

@ -57,6 +57,10 @@ class BuildDetailsEntity < Ci::JobEntity
using: JobArtifactReportEntity,
if: -> (*) { can?(current_user, :read_build, build) }
expose :job_annotations,
as: :annotations,
using: Ci::JobAnnotationEntity
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module Ci
class JobAnnotationEntity < Grape::Entity
expose :name
expose :data
end
end

View File

@ -1,5 +1,4 @@
- page_title _("Labels")
- add_page_specific_style 'page_bundles/labels'
= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card labels other-labels js-toggle-container js-admin-labels-container' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
- c.with_header do

View File

@ -1,6 +1,4 @@
- page_title _("New Label")
- add_page_specific_style 'page_bundles/labels'
%h1.page-title.gl-font-size-h-display
= _('New Label')
= render 'shared/labels/form', url: admin_labels_path, back_path: admin_labels_path

View File

@ -3,7 +3,6 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
- add_page_specific_style 'page_bundles/labels'
- if labels_or_filters
#js-promote-label-modal

View File

@ -1,7 +1,6 @@
- add_to_breadcrumbs _("Labels"), group_labels_path(@group)
- breadcrumb_title _("New")
- page_title _("New Label")
- add_page_specific_style 'page_bundles/labels'
%h1.page-title.gl-font-size-h-display
= _('New Label')

View File

@ -1,5 +1,4 @@
- add_page_specific_style 'page_bundles/merge_request'
- add_page_specific_style 'page_bundles/labels'
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title _("New")
- page_title _("New Issue")

View File

@ -3,7 +3,6 @@
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present?
- add_page_specific_style 'page_bundles/labels'
- if labels_or_filters
#js-promote-label-modal

View File

@ -1,7 +1,6 @@
- add_to_breadcrumbs _("Labels"), project_labels_path(@project)
- breadcrumb_title _("New")
- page_title _("New Label")
- add_page_specific_style 'page_bundles/labels'
%h1.page-title.gl-font-size-h-display
= _('New Label')

View File

@ -4,7 +4,6 @@
- add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/ci_status'
- add_page_specific_style 'page_bundles/merge_request'
- add_page_specific_style 'page_bundles/labels'
- conflicting_mr = @merge_request.existing_mrs_targeting_same_branch.first

View File

@ -3,17 +3,13 @@
%td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
= dropdown_tag((merge_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
.js-allowed-to-merge{ data: { preselected_items: access_levels_data(merge_access_levels).to_json } }
= render_if_exists 'protected_branches/shared/user_merge_access_levels', protected_branch: protected_branch
= render_if_exists 'protected_branches/shared/group_merge_access_levels', protected_branch: protected_branch
%td.push_access_levels-container
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
= dropdown_tag((push_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
.js-allowed-to-push{ data: { preselected_items: access_levels_data(push_access_levels).to_json } }
= render_if_exists 'protected_branches/shared/user_push_access_levels', protected_branch: protected_branch
= render_if_exists 'protected_branches/shared/group_push_access_levels', protected_branch: protected_branch

View File

@ -1,3 +1,3 @@
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
= render partial: 'search/results_status' unless @search_objects.to_a.empty?
= render partial: 'search/results_status'
= render partial: 'search/results_list'

View File

@ -8,7 +8,7 @@
- elsif @search_objects.blank?
= render partial: "search/results/empty"
- else
- statusBarClass = !show_super_sidebar? ? 'gl-md-pl-5' : ''
- statusBarClass = !show_super_sidebar? ? 'gl-lg-pl-5' : ''
.section{ class: statusBarClass }
- if @scope == 'commits'

View File

@ -1,28 +1,33 @@
- return unless @search_service_presenter.show_results_status?
- statusBarClass = !show_super_sidebar? ? 'gl-md-pl-5' : ''
- statusBarClass = !show_super_sidebar? ? 'gl-lg-pl-5' : ''
- statusBarClass = statusBarClass + ' gl-lg-display-none' if @search_objects.to_a.empty?
.section{ class: statusBarClass }
.search-results-status
.gl-display-flex.gl-flex-direction-column
.gl-p-5.gl-display-flex
.gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1.gl-white-space-nowrap.gl-max-w-full
- unless @search_service_presenter.without_count?
.gl-text-truncate
= search_entries_info(@search_objects, @scope, @search_term)
- unless @search_service_presenter.show_snippets?
- if @project
- link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1 gl-text-truncate search-wrap-f-md-down')
- if @scope == 'blobs'
= _("in")
.mx-md-1
#js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } }
= s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
- else
= _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
- elsif @group
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
- if @search_service_presenter.show_sort_dropdown?
.gl-md-display-flex.gl-flex-direction-column
#js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
%hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full
.gl-p-5.gl-display-flex.gl-flex-wrap
- unless @search_objects.to_a.empty?
.gl-display-flex.gl-text-left.gl-flex-grow-1.gl-flex-shrink-1.gl-white-space-nowrap.gl-flex-wrap.gl-sm-w-half
%p.gl-text-truncate.gl-my-auto
- unless @search_service_presenter.without_count?
= search_entries_info(@search_objects, @scope, @search_term)
- unless @search_service_presenter.show_snippets?
- if @project
- link_to_project = link_to(@project.full_name, @project, class: 'search-wrap-f-md-down')
- if @scope == 'blobs'
= _("in")
.mx-md-1.gl-my-auto
#js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } }
%p.gl-text-truncate.gl-my-auto
= s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
- else
= _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
- elsif @group
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
.gl-display-flex.gl-my-3.gl-flex-grow-1.gl-flex-shrink-1.gl-justify-content-end
= render Pajamas::ButtonComponent.new(category: 'primary', icon: 'filter', button_options: {id: 'js-open-mobile-filters', class: 'gl-lg-display-none'}) do
= s_('GlobalSearch|Filters')
- if @search_service_presenter.show_sort_dropdown? && !@search_objects.to_a.empty?
.gl-ml-3
#js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
%hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full

View File

@ -22,7 +22,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @search_service_presenter.advanced_search_enabled?.to_s, "default-branch-name": @project&.default_branch } }
.results.gl-md-display-flex.gl-mt-0
.results.gl-lg-display-flex.gl-mt-0
#js-search-sidebar{ class: search_bar_classes, data: { navigation_json: search_navigation_json, search_type: search_service.search_type } }
- if @search_term
= render 'search/results'

View File

@ -15,6 +15,5 @@
- page_title("#{board.name}", _("Boards"))
- add_page_specific_style 'page_bundles/boards'
- add_page_specific_style 'page_bundles/labels'
#js-issuable-board-app{ data: board_data }

View File

@ -11,7 +11,6 @@
- is_merge_request = issuable_type === 'merge_request'
- moved_sidebar_enabled = moved_mr_sidebar_enabled?
- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
- add_page_specific_style 'page_bundles/labels'
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { always_show_toggle: true, signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }

View File

@ -361,7 +361,6 @@ module Gitlab
config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/work_items.css"
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/labels.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "lazy_bundles/gridstack.css"
config.assets.precompile << "performance_bar.css"

View File

@ -1,8 +1,8 @@
---
name: harbor_registry_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81593
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353595
milestone: '14.9'
name: database_analyze_on_partitioned_tables
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130599
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/423959
milestone: '16.4'
type: development
group: group::container registry
group: group::database
default_enabled: false

View File

@ -1,11 +1,15 @@
# frozen_string_literal: true
require_dependency 'gitlab/auth/devise/strategies/combined_two_factor_authenticatable'
# Use this hook to configure devise mailer, warden hooks and so forth. The first
# four configuration values can also be set straight in your models.
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(scope: :user).unshift :two_factor_authenticatable
manager.default_strategies(scope: :user).unshift :two_factor_backupable
user_scoped_strategies = manager.default_strategies(scope: :user)
user_scoped_strategies.delete :two_factor_backupable
user_scoped_strategies.delete :two_factor_authenticatable
user_scoped_strategies.unshift :combined_two_factor_authenticatable
end
# This is the default. This makes it explicit that Devise loads routes

View File

@ -29,7 +29,7 @@ supported by many email clients.
## Favicon
By default, the favicon (used by the browser as the tab icon, as well as the CI status icon)
By default, the favicon (used by the browser as the tab icon and the CI status icon)
uses the GitLab logo. This can be customized with any icon desired. It must be a
32x32 `.png` or `.ico` image.
@ -42,7 +42,7 @@ of the page to activate it in the GitLab instance.
You can add a small header message, a small footer message, or both, to the interface
of your GitLab instance. These messages appear on all projects and pages of the
instance, including the sign in / sign up page. The default color is white text on
instance, including the sign-in/sign-up page. The default color is white text on
an orange background, but this can be customized by selecting **Customize colors**.
Limited [Markdown](../user/markdown.md) is supported, such as bold, italics, and links, for
@ -55,9 +55,9 @@ the header and footer added to all emails sent by the GitLab instance.
After you add a message, select **Update appearance settings** at the bottom of the page
to activate it in the GitLab instance.
## Sign in / Sign up pages
## Sign-in / Sign-up pages
You can replace the default message on the sign in / sign up page with your own message
You can replace the default message on the sign-in/sign-up page with your own message
and logo. You can make full use of [Markdown](../user/markdown.md) in the description.
The optimal size for the logo is 128 x 128 pixels, but any image can be used (below 1 MB)
@ -69,7 +69,7 @@ to activate it in the GitLab instance. You can also select **Sign-in page**,
to review the saved appearance settings:
NOTE:
You can add also add a [customized hcelp message](settings/help_page.md) below the sign in message or add [a Sign in text message](settings/sign_in_restrictions.md#sign-in-information).
You can add also add a [customized help message](settings/help_page.md) below the sign-in message or add [a Sign-in text message](settings/sign_in_restrictions.md#sign-in-information).
## Progressive Web App

View File

@ -477,6 +477,7 @@ To work around the issue, give these users the Guest role or higher to any proje
> - The ability for a custom role to view a vulnerability report [introduced](https://gitlab.com/groups/gitlab-org/-/epics/10160) in GitLab 16.1 [with a flag](../administration/feature_flags.md) named `custom_roles_vulnerability`.
> - Ability to view a vulnerability report [enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123835) in GitLab 16.1.
> - [Feature flag `custom_roles_vulnerability` removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124049) in GitLab 16.2.
> - Ability to create and remove a custom role with the UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/393235) in GitLab 16.4.
Custom roles allow group members who are assigned the Owner role to create roles
specific to the needs of their organization.
@ -502,12 +503,48 @@ This does not apply to the Guest+1 custom role because the `view_code` ability i
### Create a custom role
To enable custom roles for your group, a group member with the Owner role:
Prerequisites:
1. Makes sure that there is at least one private project in this group or one of
its subgroups, so that you can see the effect of giving a Guest a custom role.
1. Creates a personal access token with the API scope.
1. Uses [the API](../api/member_roles.md#add-a-member-role-to-a-group) to create a custom role for the root group.
- You must be an administrator for the self-managed instance, or have the Owner
role in the group you are creating the custom role in.
- The group must be in the Ultimate tier.
- You must have:
- At least one private project so that you can see the effect of giving a
user with the Guest role a custom role. The project can be in the group itself
or one of that group's subgroups.
- A [personal access token with the API scope](profile/personal_access_tokens.md#create-a-personal-access-token).
#### GitLab SaaS
Prerequisite:
- You must have the Owner role in the group you are creating the custom role in.
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Settings > Roles and Permissions**.
1. Select **Add new role**.
1. In **Base role to use as template**, select **Guest**.
1. In **Role name**, enter the custom role's title.
1. Select the **Permissions** for the new custom role.
1. Select **Create new role**.
#### GitLab.com
Prerequisite:
- You must be an administrator for the self-managed instance you are creating the custom role in.
1. On the left sidebar, select **Search or go to**.
1. Select **Admin Area**.
1. Select **Settings > Roles and Permissions**.
1. From the top dropdown list, select the group you want to create a custom role in.
1. Select **Add new role**.
1. In **Base role to use as template**, select **Guest**.
1. In **Role name**, enter the custom role's title.
1. Select the **Permissions** for the new custom role.
1. Select **Create new role**.
To create a custom role, you can also [use the API](../api/member_roles.md#add-a-member-role-to-a-group).
#### Custom role requirements
@ -567,16 +604,45 @@ Now the user is a regular Guest.
### Remove a custom role
Removing a custom role also removes all members with that custom role from
the group. If you decide to delete a custom role, you must re-add any users with that custom
role to the group.
Prerequisite:
To remove a custom role from a group, a group member with
the Owner role:
- You must have the Owner role in the group you are removing the custom role from.
- No group members have that custom role.
1. Optional. If the Owner does not know the `ID` of a custom
role, finds that `ID` by making an [API request](../api/member_roles.md#list-all-member-roles-of-a-group).
1. Uses [the API](../api/member_roles.md#remove-member-role-of-a-group) to delete the custom role.
You cannot remove a custom role from a group until there are no group members with
that custom role.
To do this, you can either remove the custom role from all group members with that
custom role, or remove those members from the group. You complete both of these actions
from the group members page:
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Manage > Members**.
To remove a custom role from a group member with that custom role:
1. On the member row you want to remove, in the **Max role** column, select the
dropdown list to change the role for the group member.
To remove a member with a custom role from the group:
1. On the member row you want to remove, select the vertical ellipsis
(**{ellipsis_v}**) and select **Remove member**.
1. In the **Remove member** confirmation dialog, do not select any checkboxes.
1. Select **Remove member**.
After you have made sure no group members have that custom role, delete the
custom role.
1. On the left sidebar, select **Search or go to**.
1. GitLab.com only. Select **Admin Area**.
1. Select **Settings > Roles and Permissions**.
1. Select **Custom Roles**.
1. In the **Actions** column, select **Delete role** (**{remove}**) and confirm.
To delete a custom role, you can also [use the API](../api/member_roles.md#remove-member-role-of-a-group).
To use the API, you must know the `ID` of the custom role. If you do not know this
`ID`, find it by making an [API request](../api/member_roles.md#list-all-member-roles-of-a-group).
### Known issues

View File

@ -0,0 +1,119 @@
# frozen_string_literal: true
module API
module Helpers
module Kubernetes
module AgentHelpers
include Gitlab::Utils::StrongMemoize
def authenticate_gitlab_kas_request!
render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers)
end
def agent_token
cluster_agent_token_from_authorization_token
end
strong_memoize_attr :agent_token
def agent
agent_token.agent
end
strong_memoize_attr :agent
def gitaly_info(project)
gitaly_features = Feature::Gitaly.server_feature_flags
Gitlab::GitalyClient.connection_data(project.repository_storage).merge(features: gitaly_features)
end
def gitaly_repository(project)
project.repository.gitaly_repository.to_h
end
def check_feature_enabled
not_found!('Internal API not found') unless Feature.enabled?(:kubernetes_agent_internal_api, type: :ops)
end
def check_agent_token
unauthorized! unless agent_token
::Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
end
def agent_has_access_to_project?(project)
Guest.can?(:download_code, project) || agent.has_access_to?(project)
end
def increment_unique_events
events = params[:unique_counters]&.slice(
:agent_users_using_ci_tunnel,
:k8s_api_proxy_requests_unique_users_via_ci_access, :k8s_api_proxy_requests_unique_agents_via_ci_access,
:k8s_api_proxy_requests_unique_users_via_user_access, :k8s_api_proxy_requests_unique_agents_via_user_access,
:k8s_api_proxy_requests_unique_users_via_pat_access, :k8s_api_proxy_requests_unique_agents_via_pat_access,
:flux_git_push_notified_unique_projects
)
events&.each do |event, entity_ids|
increment_unique_values(event, entity_ids)
end
end
def increment_count_events
events = params[:counters]&.slice(
:gitops_sync, :k8s_api_proxy_request, :flux_git_push_notifications_total,
:k8s_api_proxy_requests_via_ci_access, :k8s_api_proxy_requests_via_user_access,
:k8s_api_proxy_requests_via_pat_access
)
Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_event_counts(events)
end
def update_configuration(agent:, config:)
::Clusters::Agents::Authorizations::CiAccess::RefreshService.new(agent, config: config).execute
::Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: config).execute
end
def retrieve_user_from_session_cookie
# Load session
public_session_id_string =
begin
Gitlab::Kas::UserAccess.decrypt_public_session_id(params[:access_key])
rescue StandardError
bad_request!('Invalid access_key')
end
session_id = Rack::Session::SessionId.new(public_session_id_string)
session = ActiveSession.sessions_from_ids([session_id.private_id]).first
unauthorized!('Invalid session') unless session
# CSRF check
unless ::Gitlab::Kas::UserAccess.valid_authenticity_token?(session.symbolize_keys, params[:csrf_token])
unauthorized!('CSRF token does not match')
end
# Load user
user = Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
unauthorized!('Invalid user in session') unless user
user
end
def retrieve_user_from_personal_access_token
return unless access_token.present?
validate_access_token!(scopes: [Gitlab::Auth::K8S_PROXY_SCOPE])
::PersonalAccessTokens::LastUsedService.new(access_token).execute
access_token.user || raise(UnauthorizedError)
end
def access_token
return unless params[:access_key].present?
PersonalAccessToken.find_by_token(params[:access_key])
end
strong_memoize_attr :access_token
end
end
end
end

View File

@ -9,119 +9,7 @@ module API
authenticate_gitlab_kas_request!
end
helpers do
include Gitlab::Utils::StrongMemoize
def authenticate_gitlab_kas_request!
render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers)
end
def agent_token
@agent_token ||= cluster_agent_token_from_authorization_token
end
def agent
@agent ||= agent_token.agent
end
def repo_type
Gitlab::GlRepository::PROJECT
end
def gitaly_info(project)
gitaly_features = Feature::Gitaly.server_feature_flags
Gitlab::GitalyClient.connection_data(project.repository_storage).merge(features: gitaly_features)
end
def gitaly_repository(project)
project.repository.gitaly_repository.to_h
end
def check_feature_enabled
not_found!('Internal API not found') unless Feature.enabled?(:kubernetes_agent_internal_api, type: :ops)
end
def check_agent_token
unauthorized! unless agent_token
::Clusters::AgentTokens::TrackUsageService.new(agent_token).execute
end
def agent_has_access_to_project?(project)
Guest.can?(:download_code, project) || agent.has_access_to?(project)
end
def increment_unique_events
events = params[:unique_counters]&.slice(
:agent_users_using_ci_tunnel,
:k8s_api_proxy_requests_unique_users_via_ci_access, :k8s_api_proxy_requests_unique_agents_via_ci_access,
:k8s_api_proxy_requests_unique_users_via_user_access, :k8s_api_proxy_requests_unique_agents_via_user_access,
:k8s_api_proxy_requests_unique_users_via_pat_access, :k8s_api_proxy_requests_unique_agents_via_pat_access,
:flux_git_push_notified_unique_projects
)
events&.each do |event, entity_ids|
increment_unique_values(event, entity_ids)
end
end
def increment_count_events
events = params[:counters]&.slice(
:gitops_sync, :k8s_api_proxy_request, :flux_git_push_notifications_total,
:k8s_api_proxy_requests_via_ci_access, :k8s_api_proxy_requests_via_user_access,
:k8s_api_proxy_requests_via_pat_access
)
Gitlab::UsageDataCounters::KubernetesAgentCounter.increment_event_counts(events)
end
def update_configuration(agent:, config:)
::Clusters::Agents::Authorizations::CiAccess::RefreshService.new(agent, config: config).execute
::Clusters::Agents::Authorizations::UserAccess::RefreshService.new(agent, config: config).execute
end
def retrieve_user_from_session_cookie
# Load session
public_session_id_string =
begin
Gitlab::Kas::UserAccess.decrypt_public_session_id(params[:access_key])
rescue StandardError
bad_request!('Invalid access_key')
end
session_id = Rack::Session::SessionId.new(public_session_id_string)
session = ActiveSession.sessions_from_ids([session_id.private_id]).first
unauthorized!('Invalid session') unless session
# CSRF check
unless ::Gitlab::Kas::UserAccess.valid_authenticity_token?(session.symbolize_keys, params[:csrf_token])
unauthorized!('CSRF token does not match')
end
# Load user
user = Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
unauthorized!('Invalid user in session') unless user
user
end
def retrieve_user_from_personal_access_token
return unless access_token.present?
validate_access_token!(scopes: [Gitlab::Auth::K8S_PROXY_SCOPE])
::PersonalAccessTokens::LastUsedService.new(access_token).execute
access_token.user || raise(UnauthorizedError)
end
def access_token
return unless params[:access_key].present?
PersonalAccessToken.find_by_token(params[:access_key])
end
strong_memoize_attr :access_token
end
helpers ::API::Helpers::Kubernetes::AgentHelpers
namespace 'internal' do
namespace 'kubernetes' do

View File

@ -40,11 +40,14 @@ module BulkImports
private
def mapped_usernames
@mapped_usernames ||= ::BulkImports::UsersMapper.new(context: context).map_usernames
@mapped_usernames ||= ::BulkImports::UsersMapper.new(context: context)
.map_usernames.transform_keys { |key| "@#{key}" }
.transform_values { |value| "@#{value}" }
end
def username_regex(mapped_usernames)
@username_regex ||= Regexp.new(mapped_usernames.keys.map { |x| Regexp.escape(x) }.join('|'))
@username_regex ||= Regexp.new(mapped_usernames.keys.sort_by(&:length)
.reverse.map { |x| Regexp.escape(x) }.join('|'))
end
def add_matching_objects(collection, enum)

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
module Gitlab
module Auth
module Devise
module Strategies
# This strategy combines the following strategies from
# devise_two_factor gem:
# - TwoFactorAuthenticatable: https://github.com/devise-two-factor/devise-two-factor/blob/v4.0.2/lib/devise_two_factor/strategies/two_factor_authenticatable.rb
# - TwoFactorBackupable: https://github.com/devise-two-factor/devise-two-factor/blob/v4.0.2/lib/devise_two_factor/strategies/two_factor_backupable.rb
# to avoid double incrementing failed login attempts counter by each
# strategy in case an incorrect password is provided.
class CombinedTwoFactorAuthenticatable < ::Devise::Strategies::DatabaseAuthenticatable
def authenticate!
resource = mapping.to.find_for_database_authentication(authentication_hash)
# We check the OTP / backup code, then defer to DatabaseAuthenticatable
is_valid = validate(resource) do
validate_otp(resource) || resource.invalidate_otp_backup_code!(params[scope]['otp_attempt'])
end
if is_valid
# Devise fails to authenticate invalidated resources, but if we've
# gotten here, the object changed (Since we deleted a recovery code)
resource.save!
super
end
fail(::Devise.paranoid ? :invalid : :not_found_in_database) unless resource # rubocop: disable Style/SignalException
# We want to cascade to the next strategy if this one fails,
# but database authenticatable automatically halts on a bad password
@halted = false if @result == :failure
end
def validate_otp(resource)
return true unless resource.otp_required_for_login
return if params[scope]['otp_attempt'].nil?
resource.validate_and_consume_otp!(params[scope]['otp_attempt'])
end
end
end
end
end
end
Warden::Strategies.add(
:combined_two_factor_authenticatable,
Gitlab::Auth::Devise::Strategies::CombinedTwoFactorAuthenticatable)

View File

@ -33,7 +33,7 @@ module Gitlab
end
def self.providers
Devise.omniauth_providers
::Devise.omniauth_providers
end
def self.enabled?(name)

View File

@ -4,18 +4,21 @@ module Gitlab
module Database
module Partitioning
class MonthlyStrategy
attr_reader :model, :partitioning_key, :retain_for, :retain_non_empty_partitions
attr_reader :model, :partitioning_key, :retain_for, :retain_non_empty_partitions, :analyze_interval
# We create this many partitions in the future
HEADROOM = 6.months
delegate :table_name, to: :model
def initialize(model, partitioning_key, retain_for: nil, retain_non_empty_partitions: false)
def initialize(
model, partitioning_key, retain_for: nil, retain_non_empty_partitions: false,
analyze_interval: nil)
@model = model
@partitioning_key = partitioning_key
@retain_for = retain_for
@retain_non_empty_partitions = retain_non_empty_partitions
@analyze_interval = analyze_interval
end
def current_partitions

View File

@ -4,9 +4,12 @@ module Gitlab
module Database
module Partitioning
class PartitionManager
include ::Gitlab::Utils::StrongMemoize
UnsafeToDetachPartitionError = Class.new(StandardError)
LEASE_TIMEOUT = 1.minute
LEASE_TIMEOUT = 1.hour
STATEMENT_TIMEOUT = 1.hour
MANAGEMENT_LEASE_KEY = 'database_partition_management_%s'
RETAIN_DETACHED_PARTITIONS_FOR = 1.week
@ -33,6 +36,8 @@ module Gitlab
create(partitions_to_create) unless partitions_to_create.empty?
detach(partitions_to_detach) unless partitions_to_detach.empty?
run_analyze_on_partitioned_table
end
rescue ArgumentError => e
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
@ -146,6 +151,50 @@ module Gitlab
connection_name: @connection_name
)
end
def run_analyze_on_partitioned_table
return if Feature.disabled?(:database_analyze_on_partitioned_tables)
return if ineligible_for_analyzing?
set_analyze_statement_timeout do
# Running ANALYZE on partitioned table will go through itself and its partitions
connection.execute("ANALYZE VERBOSE #{model.quoted_table_name}")
end
end
def ineligible_for_analyzing?
first_model_partition.blank? || analyze_interval.blank? || last_analyzed_at_within_interval?
end
def last_analyzed_at_within_interval?
table_to_query = first_model_partition.identifier
# We don't need to get the last_analyze_time from partitioned table,
# because it's not supported and always returns NULL for PG version below 14
# Therefore, we can always get the last_analyze_time from the first partition
last_analyzed_at = connection.select_value(
"SELECT pg_stat_get_last_analyze_time('#{table_to_query}'::regclass)"
)
last_analyzed_at.present? && last_analyzed_at >= Time.current - analyze_interval
end
def first_model_partition
Gitlab::Database::SharedModel.using_connection(connection) do
Gitlab::Database::PostgresPartition.for_parent_table(model.table_name).first
end
end
strong_memoize_attr :first_model_partition
def analyze_interval
model.partitioning_strategy.analyze_interval
end
def set_analyze_statement_timeout
connection.execute(format("SET statement_timeout TO '%ds'", STATEMENT_TIMEOUT))
yield
ensure
connection.execute('RESET statement_timeout')
end
end
end
end

View File

@ -4,15 +4,16 @@ module Gitlab
module Database
module Partitioning
class SlidingListStrategy
attr_reader :model, :partitioning_key, :next_partition_if, :detach_partition_if
attr_reader :model, :partitioning_key, :next_partition_if, :detach_partition_if, :analyze_interval
delegate :table_name, to: :model
def initialize(model, partitioning_key, next_partition_if:, detach_partition_if:)
def initialize(model, partitioning_key, next_partition_if:, detach_partition_if:, analyze_interval: nil)
@model = model
@partitioning_key = partitioning_key
@next_partition_if = next_partition_if
@detach_partition_if = detach_partition_if
@analyze_interval = analyze_interval
ensure_partitioning_column_ignored_or_readonly!
end

View File

@ -57,8 +57,7 @@ module Sidebars
end
def harbor_registry_menu_item
if Feature.disabled?(:harbor_registry_integration) ||
context.group.harbor_integration.nil? ||
if context.group.harbor_integration.nil? ||
!context.group.harbor_integration.activated?
return nil_menu_item(:harbor_registry)
end

View File

@ -75,8 +75,7 @@ module Sidebars
end
def harbor_registry_menu_item
if Feature.disabled?(:harbor_registry_integration, context.project) ||
context.project.harbor_integration.nil? ||
if context.project.harbor_integration.nil? ||
!context.project.harbor_integration.activated?
return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry)
end

View File

@ -1969,12 +1969,24 @@ msgstr ""
msgid "AI|GitLab Duo"
msgstr ""
msgid "AI|Give feedback on AI content"
msgstr ""
msgid "AI|Give feedback to improve this answer."
msgstr ""
msgid "AI|Has no support and might not be documented"
msgstr ""
msgid "AI|Helpful"
msgstr ""
msgid "AI|How could the content be improved?"
msgstr ""
msgid "AI|How was the AI content?"
msgstr ""
msgid "AI|I don't see how I can help. Please give better instructions!"
msgstr ""
@ -2002,6 +2014,9 @@ msgstr ""
msgid "AI|Something went wrong. Please try again later"
msgstr ""
msgid "AI|Thank you for your feedback."
msgstr ""
msgid "AI|The container element wasn't found, stopping AI Genie."
msgstr ""
@ -2017,6 +2032,9 @@ msgstr ""
msgid "AI|Third-party AI services"
msgstr ""
msgid "AI|To help improve the quality of the content, send your feedback to GitLab team members."
msgstr ""
msgid "AI|Unhelpful"
msgstr ""
@ -2422,6 +2440,9 @@ msgstr ""
msgid "AbuseReport|View screenshot"
msgstr ""
msgid "Abusive or offensive"
msgstr ""
msgid "Accept invitation"
msgstr ""
@ -19549,6 +19570,9 @@ msgstr ""
msgid "Facebook"
msgstr ""
msgid "Factually incorrect"
msgstr ""
msgid "Fail"
msgstr ""
@ -21838,6 +21862,9 @@ msgstr ""
msgid "GlobalSearch|Fetching aggregations error."
msgstr ""
msgid "GlobalSearch|Filters"
msgstr ""
msgid "GlobalSearch|Group"
msgstr ""
@ -23396,6 +23423,9 @@ msgstr ""
msgid "Help translate to your language"
msgstr ""
msgid "Helpful"
msgstr ""
msgid "Helps prevent bots from brute-force attacks."
msgstr ""
@ -26903,6 +26933,9 @@ msgstr ""
msgid "Job|Erase job log and artifacts"
msgstr ""
msgid "Job|External links"
msgstr ""
msgid "Job|Failed"
msgstr ""
@ -39719,9 +39752,6 @@ msgstr ""
msgid "Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / Pipeline Artifacts: %{counter_pipeline_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets} / Packages: %{counter_packages} / Uploads: %{counter_uploads}"
msgstr ""
msgid "RepositorySettingsAccessLevel|Select"
msgstr ""
msgid "Request"
msgstr ""
@ -44668,6 +44698,9 @@ msgstr ""
msgid "Someone, hopefully you, has requested to reset the password for your GitLab account on %{link_to_gitlab}."
msgstr ""
msgid "Something else"
msgstr ""
msgid "Something went wrong"
msgstr ""
@ -49477,6 +49510,9 @@ msgstr ""
msgid "Tomorrow"
msgstr ""
msgid "Too long"
msgstr ""
msgid "Too many namespaces enabled. Manage them through the console or the API."
msgstr ""
@ -50249,6 +50285,9 @@ msgstr ""
msgid "Unhappy?"
msgstr ""
msgid "Unhelpful or irrelevant"
msgstr ""
msgid "Units|d"
msgstr ""

View File

@ -18,7 +18,6 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
stub_config(dependency_proxy: { enabled: false })
stub_config(registry: { enabled: false })
stub_feature_flags(harbor_registry_integration: false)
stub_feature_flags(observability_group_tab: false)
stub_group_wikis(false)
group.add_maintainer(user)
@ -87,8 +86,6 @@ RSpec.describe 'Group navbar', :with_license, feature_category: :navigation do
before do
group.update!(harbor_integration: harbor_integration)
stub_feature_flags(harbor_registry_integration: true)
insert_harbor_registry_nav(_('Package Registry'))
visit group_path(group)

View File

@ -15,7 +15,6 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
sign_in(user)
stub_config(registry: { enabled: false })
stub_feature_flags(harbor_registry_integration: false)
stub_feature_flags(ml_experiment_tracking: false)
insert_package_nav(_('Deployments'))
insert_infrastructure_registry_nav
@ -87,8 +86,6 @@ RSpec.describe 'Project navbar', :with_license, feature_category: :groups_and_pr
let_it_be(:harbor_integration) { create(:harbor_integration, project: project) }
before do
stub_feature_flags(harbor_registry_integration: true)
insert_harbor_registry_nav(_('Terraform modules'))
visit project_path(project)

View File

@ -234,8 +234,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
describe 'when failing to login the maximum allowed number of times' do
before do
# See comment in RequireEmailVerification::MAXIMUM_ATTEMPTS on why this is divided by 2
(RequireEmailVerification::MAXIMUM_ATTEMPTS / 2).times do
RequireEmailVerification::MAXIMUM_ATTEMPTS.times do
gitlab_sign_in(user, password: 'wrong_password')
end
end
@ -345,7 +344,7 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting,
before do
perform_enqueued_jobs do
(User.maximum_attempts / 2).times do
User.maximum_attempts.times do
gitlab_sign_in(user, password: 'wrong_password')
end
end

View File

@ -286,6 +286,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
enter_code(code, only_two_factor_webauthn_enabled: only_two_factor_webauthn_enabled)
expect(page).to have_content('Invalid two-factor code.')
expect(user.reload.failed_attempts).to eq(1)
end
end
end
@ -576,7 +577,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
end
end
context 'with invalid username and password' do
context 'with correct username and invalid password' do
let(:user) { create(:user, :no_super_sidebar) }
it 'blocks invalid login' do
@ -588,6 +589,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_
expect_single_session_with_short_ttl
expect(page).to have_content('Invalid login or password.')
expect(user.reload.failed_attempts).to eq(1)
end
end
end

View File

@ -0,0 +1,49 @@
import { GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ExternalLinksBlock from '~/ci/job_details/components/sidebar/external_links_block.vue';
describe('External links block', () => {
let wrapper;
const createWrapper = (propsData) => {
wrapper = mountExtended(ExternalLinksBlock, {
propsData: {
...propsData,
},
});
};
const findAllLinks = () => wrapper.findAllComponents(GlLink);
const findLink = () => findAllLinks().at(0);
it('renders a list of links', () => {
createWrapper({
externalLinks: [
{
label: 'URL 1',
url: 'https://url1.example.com/',
},
{
label: 'URL 2',
url: 'https://url2.example.com/',
},
],
});
expect(findAllLinks()).toHaveLength(2);
});
it('renders a link', () => {
createWrapper({
externalLinks: [
{
label: 'Example URL',
url: 'https://example.com/',
},
],
});
expect(findLink().text()).toBe('Example URL');
expect(findLink().attributes('href')).toBe('https://example.com/');
});
});

View File

@ -5,6 +5,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsBlock from '~/ci/job_details/components/sidebar/artifacts_block.vue';
import ExternalLinksBlock from '~/ci/job_details/components/sidebar/external_links_block.vue';
import JobRetryForwardDeploymentModal from '~/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue';
import JobsContainer from '~/ci/job_details/components/sidebar/jobs_container.vue';
import Sidebar from '~/ci/job_details/components/sidebar/sidebar.vue';
@ -20,6 +21,7 @@ describe('Sidebar details block', () => {
const forwardDeploymentFailure = 'forward_deployment_failure';
const findModal = () => wrapper.findComponent(JobRetryForwardDeploymentModal);
const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock);
const findExternalLinksBlock = () => wrapper.findComponent(ExternalLinksBlock);
const findJobStagesDropdown = () => wrapper.findComponent(StagesDropdown);
const findJobsContainer = () => wrapper.findComponent(JobsContainer);
@ -181,4 +183,40 @@ describe('Sidebar details block', () => {
expect(findArtifactsBlock().exists()).toBe(true);
});
});
describe('external links', () => {
beforeEach(() => {
createWrapper();
});
it('external links block is not shown if there are no external links', () => {
expect(findExternalLinksBlock().exists()).toBe(false);
});
it('external links block is shown if there are external links', async () => {
store.state.job.annotations = [
{
name: 'external_links',
data: [
{
external_link: {
label: 'URL 1',
url: 'https://url1.example.com/',
},
},
{
external_link: {
label: 'URL 2',
url: 'https://url2.example.com/',
},
},
],
},
];
await nextTick();
expect(findExternalLinksBlock().exists()).toBe(true);
});
});
});

View File

@ -1,4 +1,4 @@
import { compactJobLog } from '~/ci/job_details/utils';
import { compactJobLog, filterAnnotations } from '~/ci/job_details/utils';
import { mockJobLog } from 'jest/ci/jobs_mock_data';
describe('Job utils', () => {
@ -219,4 +219,47 @@ describe('Job utils', () => {
expect(compactJobLog(mockJobLog)).toStrictEqual(expectedResults);
});
});
describe('filterAnnotations', () => {
it('filters annotations by type', () => {
const data = [
{
name: 'b',
data: [
{
dummy: {},
},
{
external_link: {
label: 'URL 2',
url: 'https://url2.example.com/',
},
},
],
},
{
name: 'a',
data: [
{
external_link: {
label: 'URL 1',
url: 'https://url1.example.com/',
},
},
],
},
];
expect(filterAnnotations(data, 'external_link')).toEqual([
{
label: 'URL 1',
url: 'https://url1.example.com/',
},
{
label: 'URL 2',
url: 'https://url2.example.com/',
},
]);
});
});
});

View File

@ -989,6 +989,7 @@ export default {
},
erase_path: '/root/ci-mock/-/jobs/4757/erase',
artifacts: [null],
annotations: [],
runner: {
id: 1,
short_sha: 'ABCDEFGH',

View File

@ -20,7 +20,7 @@ describe('ProtectedBranchEdit', () => {
let mock;
beforeEach(() => {
jest.spyOn(ProtectedBranchEdit.prototype, 'buildDropdowns').mockImplementation();
jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
mock = new MockAdapter(axios);
});

View File

@ -194,7 +194,7 @@ export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = {
label: 'Projects',
scope: 'projects',
link: '/search?scope=projects&search=et',
count_link: '/search/count?scope=projects&search=et',
count_link: null,
},
};

View File

@ -4,13 +4,18 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { SEARCH_TYPE_ZOEKT, SEARCH_TYPE_ADVANCED } from '~/search/sidebar/constants';
import { MOCK_QUERY } from 'jest/search/mock_data';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import IssuesFilters from '~/search/sidebar/components/issues_filters.vue';
import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue';
import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue';
import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
Vue.use(Vuex);
@ -41,13 +46,16 @@ describe('GlobalSearchSidebar', () => {
const findBlobsFilters = () => wrapper.findComponent(BlobsFilters);
const findProjectsFilters = () => wrapper.findComponent(ProjectsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
const findDomElementListener = () => wrapper.findComponent(DomElementListener);
describe('renders properly', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it(`shows section`, () => {
expect(findSidebarSection().exists()).toBe(true);
});
@ -104,6 +112,7 @@ describe('GlobalSearchSidebar', () => {
it(`${!legacyNavShown ? 'hides' : 'shows'} the legacy navigation`, () => {
expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown);
expect(findSmallScreenDrawerNavigation().exists()).toBe(legacyNavShown);
});
it(`${!sidebarNavShown ? 'hides' : 'shows'} the sidebar navigation`, () => {
@ -111,4 +120,21 @@ describe('GlobalSearchSidebar', () => {
});
});
});
describe('when useSidebarNavigation=true', () => {
beforeEach(() => {
createComponent({ useSidebarNavigation: true });
});
it('toggles super sidebar when button is clicked', () => {
const elListener = findDomElementListener();
expect(toggleSuperSidebarCollapsed).not.toHaveBeenCalled();
elListener.vm.$emit('click');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
expect(elListener.props('selector')).toBe('#js-open-mobile-filters');
});
});
});

View File

@ -7,6 +7,8 @@ import IssuesFilters from '~/search/sidebar/components/issues_filters.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter/index.vue';
import StatusFilter from '~/search/sidebar/components/status_filter/index.vue';
import LabelFilter from '~/search/sidebar/components/label_filter/index.vue';
import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
import { SEARCH_TYPE_ADVANCED, SEARCH_TYPE_BASIC } from '~/search/sidebar/constants';
Vue.use(Vuex);
@ -17,10 +19,16 @@ describe('GlobalSearch IssuesFilters', () => {
currentScope: () => 'issues',
};
const createComponent = (initialState, ff = true) => {
const createComponent = ({
initialState = {},
searchIssueLabelAggregation = true,
searchIssuesHideArchivedProjects = true,
} = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
useSidebarNavigation: false,
searchType: SEARCH_TYPE_ADVANCED,
...initialState,
},
getters: defaultGetters,
@ -30,7 +38,8 @@ describe('GlobalSearch IssuesFilters', () => {
store,
provide: {
glFeatures: {
searchIssueLabelAggregation: ff,
searchIssueLabelAggregation,
searchIssuesHideArchivedProjects,
},
},
});
@ -39,11 +48,87 @@ describe('GlobalSearch IssuesFilters', () => {
const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
const findLabelFilter = () => wrapper.findComponent(LabelFilter);
const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
describe('Renders correctly with FF enabled', () => {
describe.each`
description | searchIssueLabelAggregation | searchIssuesHideArchivedProjects
${'Renders correctly with Label Filter disabled'} | ${false} | ${true}
${'Renders correctly with Archived Filter disabled'} | ${true} | ${false}
${'Renders correctly with Archived Filter and Label Filter disabled'} | ${false} | ${false}
${'Renders correctly with Archived Filter and Label Filter enabled'} | ${true} | ${true}
`('$description', ({ searchIssueLabelAggregation, searchIssuesHideArchivedProjects }) => {
beforeEach(() => {
createComponent({ urlQuery: MOCK_QUERY });
createComponent({
searchIssueLabelAggregation,
searchIssuesHideArchivedProjects,
});
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
it('renders ConfidentialityFilter', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
});
it(`renders correctly ArchivedFilter when searchIssuesHideArchivedProjects is ${searchIssuesHideArchivedProjects}`, () => {
expect(findArchivedFilter().exists()).toBe(searchIssuesHideArchivedProjects);
});
it('renders divider correctly', () => {
// one divider can't be disabled
let dividersCount = 1;
if (searchIssueLabelAggregation) {
dividersCount += 1;
}
if (searchIssuesHideArchivedProjects) {
dividersCount += 1;
}
expect(findDividers()).toHaveLength(dividersCount);
});
});
describe('Renders correctly with basic search', () => {
beforeEach(() => {
createComponent({ initialState: { searchType: SEARCH_TYPE_BASIC } });
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
it('renders ConfidentialityFilter', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
it("doesn't render LabelFilter", () => {
expect(findLabelFilter().exists()).toBe(false);
});
it("doesn't render ArchivedFilter", () => {
expect(findArchivedFilter().exists()).toBe(false);
});
it('renders 1 divider', () => {
expect(findDividers()).toHaveLength(1);
});
});
describe('Renders correctly in new nav', () => {
beforeEach(() => {
createComponent({
initialState: {
searchType: SEARCH_TYPE_ADVANCED,
useSidebarNavigation: true,
},
searchIssueLabelAggregation: true,
searchIssuesHideArchivedProjects: true,
});
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
@ -57,36 +142,19 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findLabelFilter().exists()).toBe(true);
});
it('renders dividers correctly', () => {
expect(findDividers()).toHaveLength(2);
});
});
describe('Renders correctly with FF disabled', () => {
beforeEach(() => {
createComponent({ urlQuery: MOCK_QUERY }, false);
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
it('renders ArchivedFilter', () => {
expect(findArchivedFilter().exists()).toBe(true);
});
it('renders ConfidentialityFilter', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
it("doesn't render LabelFilter", () => {
expect(findLabelFilter().exists()).toBe(false);
});
it('renders divider correctly', () => {
expect(findDividers()).toHaveLength(1);
it("doesn't render dividers", () => {
expect(findDividers()).toHaveLength(0);
});
});
describe('Renders correctly with wrong scope', () => {
beforeEach(() => {
defaultGetters.currentScope = () => 'blobs';
createComponent({ urlQuery: MOCK_QUERY });
createComponent();
});
it("doesn't render StatusFilter", () => {
expect(findStatusFilter().exists()).toBe(false);
@ -100,6 +168,10 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findLabelFilter().exists()).toBe(false);
});
it("doesn't render ArchivedFilter", () => {
expect(findArchivedFilter().exists()).toBe(false);
});
it("doesn't render dividers", () => {
expect(findDividers()).toHaveLength(0);
});

View File

@ -0,0 +1,68 @@
import { nextTick } from 'vue';
import { GlDrawer } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
describe('ScopeLegacyNavigation', () => {
let wrapper;
let closeSpy;
let toggleSpy;
const createComponent = () => {
wrapper = shallowMountExtended(SmallScreenDrawerNavigation, {
slots: {
default: '<div data-testid="default-slot-content">test</div>',
},
});
};
const findGlDrawer = () => wrapper.findComponent(GlDrawer);
const findTitle = () => wrapper.findComponent('h2');
const findSlot = () => wrapper.findByTestId('default-slot-content');
const findDomElementListener = () => wrapper.findComponent(DomElementListener);
describe('small screen navigation', () => {
beforeEach(() => {
createComponent();
});
it('renders drawer', () => {
expect(findGlDrawer().exists()).toBe(true);
expect(findGlDrawer().attributes('zindex')).toBe(DRAWER_Z_INDEX.toString());
expect(findGlDrawer().attributes('headerheight')).toBe('0');
});
it('renders title', () => {
expect(findTitle().exists()).toBe(true);
});
it('renders slots', () => {
expect(findSlot().exists()).toBe(true);
});
});
describe('actions', () => {
beforeEach(() => {
closeSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'closeSmallScreenFilters');
toggleSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'toggleSmallScreenFilters');
createComponent();
});
it('calls onClose', () => {
findGlDrawer().vm.$emit('close');
expect(closeSpy).toHaveBeenCalled();
});
it('calls toggleSmallScreenFilters', async () => {
expect(findGlDrawer().props('open')).toBe(false);
findDomElementListener().vm.$emit('click');
await nextTick();
expect(toggleSpy).toHaveBeenCalled();
expect(findGlDrawer().props('open')).toBe(true);
});
});
});

View File

@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import { mapValues } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/alert';
@ -312,6 +313,21 @@ describe('Global Search Store Actions', () => {
});
});
describe('fetchSidebarCount with no count_link', () => {
beforeEach(() => {
state.navigation = mapValues(MOCK_NAVIGATION_DATA, (navItem) => ({
...navItem,
count_link: null,
}));
});
it('should not request anything', async () => {
await testAction({ action: actions.fetchSidebarCount, state, expectedMutations: [] });
expect(mock.history.get.length).toBe(0);
});
});
describe.each`
action | axiosMock | type | expectedMutations | errorLogs
${actions.fetchAllAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION} | ${0}

View File

@ -32,7 +32,7 @@ RSpec.describe Projects::PipelineHelper do
blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)),
empty_dag_svg_path: match_asset_path('illustrations/empty-state/empty-dag-md.svg'),
empty_state_image_path: match_asset_path('illustrations/empty-state/empty-test-cases-lg.svg'),
empty_state_image_path: match_asset_path('illustrations/empty-todos-md.svg'),
artifacts_expired_image_path: match_asset_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count]
})

View File

@ -51,7 +51,7 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline, feature_cat
:note,
project: project,
noteable: mr,
note: '@manuelgrabowski-admin'
note: '@manuelgrabowski-admin, @boaty-mc-boatface'
)
end
@ -91,8 +91,10 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline, feature_cat
'source_username' => 'destination_username',
'bob' => 'alice-gdk',
'alice' => 'bob-gdk',
'manuelgrabowski' => 'manuelgrabowski-admin',
'manuelgrabowski-admin' => 'manuelgrabowski',
'manuelgrabowski' => 'manuelgrabowski-admin'
'boaty-mc-boatface' => 'boatymcboatface',
'boatymcboatface' => 'boaty-mc-boatface'
})
end
@ -179,7 +181,9 @@ RSpec.describe BulkImports::Projects::Pipelines::ReferencesPipeline, feature_cat
transformed_interchanged_usernames = subject.transform(context, interchanged_usernames)
expect(transformed_interchanged_usernames.note).to include("@manuelgrabowski")
expect(transformed_interchanged_usernames.note).to include("@boatymcboatface")
expect(transformed_interchanged_usernames.note).not_to include("@manuelgrabowski-admin")
expect(transformed_interchanged_usernames.note).not_to include("@boaty-mc-boatface")
end
context 'when object does not have reference or username' do

View File

@ -175,4 +175,30 @@ RSpec.describe Gitlab::Database::Partitioning::CiSlidingListStrategy, feature_ca
end.not_to raise_error
end
end
describe 'attributes' do
let(:partitioning_key) { :partition }
let(:next_partition_if) { -> { true } }
let(:detach_partition_if) { -> { false } }
let(:analyze_interval) { 1.week }
subject(:strategy) do
described_class.new(
model, partitioning_key,
next_partition_if: next_partition_if,
detach_partition_if: detach_partition_if,
analyze_interval: analyze_interval
)
end
specify do
expect(strategy).to have_attributes({
model: model,
partitioning_key: partitioning_key,
next_partition_if: next_partition_if,
detach_partition_if: detach_partition_if,
analyze_interval: analyze_interval
})
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy, feature_category: :database do
let(:connection) { ActiveRecord::Base.connection }
describe '#current_partitions' do
@ -273,4 +273,32 @@ RSpec.describe Gitlab::Database::Partitioning::MonthlyStrategy do
end
end
end
describe 'attributes' do
let(:partitioning_key) { :partition }
let(:retain_non_empty_partitions) { true }
let(:retain_for) { 12.months }
let(:analyze_interval) { 1.week }
let(:model) { class_double(ApplicationRecord, table_name: table_name, connection: connection) }
let(:table_name) { :_test_partitioned_test }
subject(:strategy) do
described_class.new(
model, partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
)
end
specify do
expect(strategy).to have_attributes({
model: model,
partitioning_key: partitioning_key,
retain_for: retain_for,
retain_non_empty_partitions: retain_non_empty_partitions,
analyze_interval: analyze_interval
})
end
end
end

View File

@ -2,7 +2,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
RSpec.describe Gitlab::Database::Partitioning::PartitionManager, feature_category: :database do
include ActiveSupport::Testing::TimeHelpers
include Database::PartitioningHelpers
include ExclusiveLeaseHelpers
@ -256,6 +257,141 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
end
end
describe 'analyze partitioned table' do
let(:analyze_table) { partitioned_table_name }
let(:analyze_partition) { "#{partitioned_table_name}_1" }
let(:analyze_regex) { /ANALYZE VERBOSE "#{analyze_table}"/ }
let(:analyze_interval) { 1.week }
let(:connection) { my_model.connection }
let(:create_partition) { true }
let(:my_model) do
interval = analyze_interval
Class.new(ApplicationRecord) do
include PartitionedTable
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: proc { false },
detach_partition_if: proc { false },
analyze_interval: interval
end
end
shared_examples_for 'run only once analyze within interval' do
it 'runs only once analyze within interval' do
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).to include(analyze_regex)
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).not_to include(analyze_regex)
travel_to((analyze_interval * 1.1).since) do
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).to include(analyze_regex)
end
end
end
shared_examples_for 'not to run the analyze at all' do
it 'does not run the analyze at all' do
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).not_to include(analyze_regex)
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).not_to include(analyze_regex)
travel_to((analyze_interval * 2).since) do
control = ActiveRecord::QueryRecorder.new { described_class.new(my_model, connection: connection).sync_partitions }
expect(control.occurrences).not_to include(analyze_regex)
end
end
end
before do
my_model.table_name = partitioned_table_name
connection.execute(<<~SQL)
CREATE TABLE #{analyze_table}(id serial) PARTITION BY LIST (id);
SQL
connection.execute(<<~SQL) if create_partition
CREATE TABLE IF NOT EXISTS #{analyze_partition} PARTITION OF #{analyze_table} FOR VALUES IN (1);
SQL
allow(connection).to receive(:select_value).and_return(nil, Time.current, Time.current)
end
context 'when feature flag database_analyze_on_partitioned_tables is enabled' do
before do
stub_feature_flags(database_analyze_on_partitioned_tables: true)
end
it_behaves_like 'run only once analyze within interval'
context 'when model does not set analyze_interval' do
let(:my_model) do
Class.new(ApplicationRecord) do
include PartitionedTable
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: proc { false },
detach_partition_if: proc { false }
end
end
it_behaves_like 'not to run the analyze at all'
end
context 'when no partition is created' do
let(:create_partition) { false }
it_behaves_like 'run only once analyze within interval'
end
end
context 'when feature flag database_analyze_on_partitioned_tables is disabled' do
before do
stub_feature_flags(database_analyze_on_partitioned_tables: false)
end
it_behaves_like 'not to run the analyze at all'
context 'when model does not set analyze_interval' do
let(:my_model) do
Class.new(ApplicationRecord) do
include PartitionedTable
partitioned_by :partition_id,
strategy: :ci_sliding_list,
next_partition_if: proc { false },
detach_partition_if: proc { false }
end
end
it_behaves_like 'not to run the analyze at all'
end
context 'when no partition is created' do
let(:create_partition) { false }
it_behaves_like 'not to run the analyze at all'
end
end
end
describe 'strategies that support analyze_interval' do
[
::Gitlab::Database::Partitioning::MonthlyStrategy,
::Gitlab::Database::Partitioning::SlidingListStrategy,
::Gitlab::Database::Partitioning::CiSlidingListStrategy
].each do |klass|
specify "#{klass} supports analyze_interval" do
expect(klass).to be_method_defined(:analyze_interval)
end
end
end
context 'creating and then detaching partitions for a table' do
let(:connection) { ActiveRecord::Base.connection }
let(:my_model) do

View File

@ -290,4 +290,30 @@ RSpec.describe Gitlab::Database::Partitioning::SlidingListStrategy, feature_cate
expect(partition_3_model.partition).to eq(3)
end
end
describe 'attributes' do
let(:partitioning_key) { :partition }
let(:next_partition_if) { -> { puts "next_partition_if" } }
let(:detach_partition_if) { -> { puts "detach_partition_if" } }
let(:analyze_interval) { 1.week }
subject(:strategy) do
described_class.new(
model, partitioning_key,
next_partition_if: next_partition_if,
detach_partition_if: detach_partition_if,
analyze_interval: analyze_interval
)
end
specify do
expect(strategy).to have_attributes({
model: model,
partitioning_key: partitioning_key,
next_partition_if: next_partition_if,
detach_partition_if: detach_partition_if,
analyze_interval: analyze_interval
})
end
end
end

View File

@ -24,29 +24,16 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu, feature_category
expect(menu.render?).to eq true
end
end
context 'when menu does not have any menu item to show' do
it 'returns false' do
stub_feature_flags(harbor_registry_integration: false)
stub_container_registry_config(enabled: false)
stub_config(packages: { enabled: false })
stub_config(dependency_proxy: { enabled: false })
expect(menu.render?).to eq false
end
end
end
describe '#link' do
let(:registry_enabled) { true }
let(:packages_enabled) { true }
let(:harbor_registry_integration) { true }
before do
stub_container_registry_config(enabled: registry_enabled)
stub_config(packages: { enabled: packages_enabled })
stub_config(dependency_proxy: { enabled: true })
stub_feature_flags(harbor_registry_integration: harbor_registry_integration)
end
subject { menu.link }
@ -70,14 +57,6 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu, feature_category
it 'menu link points to Harbor Registry page' do
expect(subject).to eq find_menu(menu, :harbor_registry).link
end
context 'when Harbor Registry is not visible' do
let(:harbor_registry_integration) { false }
it 'menu link points to Dependency Proxy page' do
expect(subject).to eq find_menu(menu, :dependency_proxy).link
end
end
end
end
end
@ -194,29 +173,13 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu, feature_category
describe 'Harbor Registry' do
let(:item_id) { :harbor_registry }
before do
stub_feature_flags(harbor_registry_integration: harbor_registry_enabled)
end
context 'when config harbor registry setting is disabled' do
let(:harbor_registry_enabled) { false }
it_behaves_like 'the menu entry is not available'
end
context 'when config harbor registry setting is enabled' do
let(:harbor_registry_enabled) { true }
it_behaves_like 'the menu entry is available'
end
it_behaves_like 'the menu entry is available'
context 'when config harbor registry setting is not activated' do
before do
harbor_integration.update!(active: false)
end
let(:harbor_registry_enabled) { true }
it_behaves_like 'the menu entry is not available'
end
end

View File

@ -39,7 +39,7 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_catego
before do
stub_container_registry_config(enabled: registry_enabled)
stub_config(packages: { enabled: packages_enabled })
stub_feature_flags(harbor_registry_integration: false, ml_experiment_tracking: false)
stub_feature_flags(ml_experiment_tracking: false)
end
context 'when Packages Registry is visible' do
@ -58,8 +58,8 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_catego
context 'when Container Registry is not visible' do
let(:registry_enabled) { false }
it 'does not display menu link' do
expect(subject.render?).to eq false
it 'displays menu link' do
expect(subject.render?).to eq true
end
end
end
@ -155,26 +155,13 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu, feature_catego
describe 'Harbor Registry' do
let(:item_id) { :harbor_registry }
context 'when config harbor registry setting is disabled' do
it 'does not add the menu item to the list' do
stub_feature_flags(harbor_registry_integration: false)
is_expected.to be_nil
end
end
context 'when config harbor registry setting is enabled' do
it 'the menu item is added to list of menu items' do
stub_feature_flags(harbor_registry_integration: true)
is_expected.not_to be_nil
expect(subject.active_routes[:controller]).to eq('projects/harbor/repositories')
end
it 'the menu item is added to list of menu items' do
is_expected.not_to be_nil
expect(subject.active_routes[:controller]).to eq('projects/harbor/repositories')
end
context 'when config harbor registry setting is not activated' do
it 'does not add the menu item to the list' do
stub_feature_flags(harbor_registry_integration: true)
project.harbor_integration.update!(active: false)
is_expected.to be_nil

View File

@ -51,7 +51,7 @@ RSpec.describe RequireEmailVerification, feature_category: :insider_threat do
context 'when failed_attempts is LT overridden amount' do
before do
instance.failed_attempts = 5
instance.failed_attempts = 2
end
it { is_expected.to eq(false) }

View File

@ -298,5 +298,21 @@ RSpec.describe BuildDetailsEntity do
end
end
end
context 'when the build has annotations' do
let!(:build) { create(:ci_build) }
let!(:annotation) { create(:ci_job_annotation, job: build, name: 'external_links', data: [{ external_link: { label: 'URL', url: 'https://example.com/' } }]) }
it 'exposes job URLs' do
expect(subject[:annotations].count).to eq(1)
expect(subject[:annotations].first[:name]).to eq('external_links')
expect(subject[:annotations].first[:data]).to include(a_hash_including(
'external_link' => a_hash_including(
'label' => 'URL',
'url' => 'https://example.com/'
)
))
end
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobAnnotationEntity, feature_category: :build_artifacts do
let(:entity) { described_class.new(annotation) }
let(:job) { build(:ci_build) }
let(:annotation) do
build(:ci_job_annotation, job: job, name: 'external_links', data:
[{ external_link: { label: 'URL', url: 'https://example.com/' } }])
end
describe '#as_json' do
subject { entity.as_json }
it 'contains valid name' do
expect(subject[:name]).to eq 'external_links'
end
it 'contains external links' do
expect(subject[:data]).to include(a_hash_including(
'external_link' => a_hash_including(
'label' => 'URL',
'url' => 'https://example.com/'
)
))
end
end
end

View File

@ -2,6 +2,7 @@
RSpec.shared_examples "protected branches > access control > CE" do
let(:no_one) { ProtectedRef::AccessLevel.humanize(::Gitlab::Access::NO_ACCESS) }
let_it_be(:edit_form) { '.js-protected-branch-edit-form' }
ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do
@ -41,12 +42,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
within_select(".js-allowed-to-push") do
click_on(access_type_name)
end
end
set_allowed_to('push', access_type_name, form: edit_form)
wait_for_requests
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
@ -63,12 +59,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
expect(ProtectedBranch.count).to eq(1)
within(".protected-branches-list") do
within_select(".js-allowed-to-merge") do
click_on(access_type_name)
end
end
set_allowed_to('merge', access_type_name, form: edit_form)
wait_for_requests
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)

View File

@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor artifacts controller' do |args|
get harbor_artifact_url(container, repository_id), headers: json_header
end
context 'with harbor registry feature flag enabled' do
it_behaves_like 'responds with 200 status with json'
end
context 'with harbor registry feature flag disabled' do
before do
stub_feature_flags(harbor_registry_integration: false)
end
it_behaves_like 'responds with 404 status'
end
it_behaves_like 'responds with 200 status with json'
context 'with anonymous user' do
before do

View File

@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args|
get harbor_repository_url(container)
end
context 'with harbor registry feature flag enabled' do
it_behaves_like 'responds with 200 status with html'
end
context 'with harbor registry feature flag disabled' do
before do
stub_feature_flags(harbor_registry_integration: false)
end
it_behaves_like 'responds with 404 status'
end
it_behaves_like 'responds with 200 status with html'
context 'with anonymous user' do
before do
@ -121,17 +111,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args|
get harbor_repository_url(container), headers: json_header
end
context 'with harbor registry feature flag enabled' do
it_behaves_like 'responds with 200 status with json'
end
context 'with harbor registry feature flag disabled' do
before do
stub_feature_flags(harbor_registry_integration: false)
end
it_behaves_like 'responds with 404 status'
end
it_behaves_like 'responds with 200 status with json'
context 'with valid params' do
context 'with valid page params' do

View File

@ -76,17 +76,7 @@ RSpec.shared_examples 'a harbor tags controller' do |args|
headers: json_header)
end
context 'with harbor registry feature flag enabled' do
it_behaves_like 'responds with 200 status with json'
end
context 'with harbor registry feature flag disabled' do
before do
stub_feature_flags(harbor_registry_integration: false)
end
it_behaves_like 'responds with 404 status'
end
it_behaves_like 'responds with 200 status with json'
context 'with anonymous user' do
before do