Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
fab43fda65
commit
0127158127
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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] ?? []);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const TRACKING_LABEL_CHECKBOX = 'checkbox';
|
|||
|
||||
const scopes = {
|
||||
PROJECTS: 'projects',
|
||||
ISSUES: 'issues',
|
||||
};
|
||||
|
||||
const filterParam = 'include_archived';
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 } }) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class JobAnnotationEntity < Grape::Entity
|
||||
expose :name
|
||||
expose :data
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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}" }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -33,7 +33,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.providers
|
||||
Devise.omniauth_providers
|
||||
::Devise.omniauth_providers
|
||||
end
|
||||
|
||||
def self.enabled?(name)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/');
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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/',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -989,6 +989,7 @@ export default {
|
|||
},
|
||||
erase_path: '/root/ci-mock/-/jobs/4757/erase',
|
||||
artifacts: [null],
|
||||
annotations: [],
|
||||
runner: {
|
||||
id: 1,
|
||||
short_sha: 'ABCDEFGH',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue