Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-11-08 18:11:30 +00:00
parent a34d7fd9a7
commit 2308cd5020
91 changed files with 1621 additions and 698 deletions

View File

@ -1 +1 @@
9bf28d2089501b82b40e2b9f6ad21cf80751f15f
c65b631d971809d9e0294356d7892860d4800cf3

View File

@ -1,7 +1,15 @@
<script>
import { GlDisclosureDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui';
import {
GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
GlFormCheckbox,
GlFormRadioGroup,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
@ -34,12 +42,14 @@ export default {
GlButton,
GlIcon,
GlForm,
GlFormRadioGroup,
GlFormCheckbox,
MarkdownEditor,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
@ -53,6 +63,7 @@ export default {
note: '',
approve: false,
approval_password: '',
reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
@ -74,6 +85,38 @@ export default {
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
radioGroupOptions() {
return [
{
html: [
__('Comment'),
`<p class="help-text">
${__('Submit general feedback without explicit approval.')}
</p>`,
].join('<br />'),
value: 'reviewed',
},
{
html: [
__('Approve'),
`<p class="help-text">
${__('Submit feedback and approve these changes.')}
</p>`,
].join('<br />'),
value: 'approved',
disabled: !this.userPermissions.canApprove,
},
{
html: [
__('Request changes'),
`<p class="help-text">
${__('Submit feedback that should be addressed before merging.')}
</p>`,
].join('<br />'),
value: 'requested_changes',
},
];
},
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
@ -208,7 +251,14 @@ export default {
@keydown.ctrl.enter="submitReview"
/>
</div>
<template v-if="userPermissions.canApprove">
<gl-form-radio-group
v-if="glFeatures.mrRequestChanges"
v-model="noteData.reviewer_state"
:options="radioGroupOptions"
class="gl-mt-4"
data-testid="reviewer_states"
/>
<template v-else-if="userPermissions.canApprove">
<gl-form-checkbox
v-model="noteData.approve"
data-testid="approve_merge_request"
@ -216,14 +266,14 @@ export default {
>
{{ __('Approve merge request') }}
</gl-form-checkbox>
<approval-password
v-if="getNoteableData.require_password_to_approve"
v-show="noteData.approve"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
</template>
<approval-password
v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
v-show="noteData.approve || noteData.reviewer_state === 'approved'"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
<div class="gl-display-flex gl-justify-content-start gl-mt-4">
<gl-button
:loading="isSubmitting"

View File

@ -4,12 +4,22 @@ import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
const defaultTitle = __('CI/CD Catalog');
const defaultDescription = s__(
'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
);
export default {
components: {
GlBanner,
GlLink,
},
inject: ['pageTitle', 'pageDescription'],
inject: {
pageTitle: { default: defaultTitle },
pageDescription: {
default: defaultDescription,
},
},
data() {
return {
isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
@ -50,7 +60,7 @@ export default {
</gl-banner>
<h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
<p>
<span>{{ pageDescription }}</span>
<span data-testid="description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>

View File

@ -0,0 +1,112 @@
<script>
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
export default {
components: {
CatalogHeader,
CatalogListSkeletonLoader,
CiResourcesList,
EmptyState,
},
data() {
return {
catalogResources: [],
currentPage: 1,
totalCount: 0,
pageInfo: {},
};
},
apollo: {
catalogResources: {
query: getCatalogResources,
variables() {
return {
first: ciCatalogResourcesItemsCount,
};
},
update(data) {
return data?.ciCatalogResources?.nodes || [];
},
result({ data }) {
const { pageInfo } = data?.ciCatalogResources || {};
this.pageInfo = pageInfo;
this.totalCount = data?.ciCatalogResources?.count || 0;
},
error(e) {
createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
},
},
},
computed: {
hasResources() {
return this.catalogResources.length > 0;
},
isLoading() {
return this.$apollo.queries.catalogResources.loading;
},
},
methods: {
async handlePrevPage() {
try {
await this.$apollo.queries.catalogResources.fetchMore({
variables: {
before: this.pageInfo.startCursor,
last: ciCatalogResourcesItemsCount,
first: null,
},
});
this.currentPage -= 1;
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
}
},
async handleNextPage() {
try {
await this.$apollo.queries.catalogResources.fetchMore({
variables: {
after: this.pageInfo.endCursor,
},
});
this.currentPage += 1;
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
}
},
},
i18n: {
fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
},
};
</script>
<template>
<div>
<catalog-header />
<catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
<empty-state v-else-if="!hasResources" />
<ci-resources-list
v-else
:current-page="currentPage"
:page-info="pageInfo"
:prev-text="__('Prev')"
:next-text="__('Next')"
:resources="catalogResources"
:total-count="totalCount"
@onPrevPage="handlePrevPage"
@onNextPage="handleNextPage"
/>
</div>
</template>

View File

@ -0,0 +1,10 @@
<script>
import CiCatalogHome from './components/ci_catalog_home.vue';
export default {
components: { CiCatalogHome },
};
</script>
<template>
<ci-catalog-home />
</template>

View File

@ -0,0 +1,16 @@
#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
count
nodes {
...CatalogResourceFields
}
}
}

View File

@ -0,0 +1,37 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
import GlobalCatalog from './global_catalog.vue';
import CiResourcesPage from './components/pages/ci_resources_page.vue';
import { createRouter } from './router';
export const initCatalog = (selector = '#js-ci-cd-catalog') => {
const el = document.querySelector(selector);
if (!el) {
return null;
}
const { dataset } = el;
const { ciCatalogPath } = dataset;
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, cacheConfig),
});
return new Vue({
el,
name: 'GlobalCatalog',
router: createRouter(ciCatalogPath, CiResourcesPage),
apolloProvider,
provide: {
ciCatalogPath,
},
render(h) {
return h(GlobalCatalog);
},
});
};

View File

@ -64,7 +64,7 @@ export default {
latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
mergeTrainBadgeText: s__('Pipelines|merge train'),
mergeTrainBadgeTooltip: s__(
'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch.',
),
invalidBadgeText: s__('Pipelines|yaml invalid'),
failedBadgeText: s__('Pipelines|error'),
@ -74,7 +74,11 @@ export default {
),
detachedBadgeText: s__('Pipelines|merge request'),
detachedBadgeTooltip: s__(
"Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
"Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.",
),
mergedResultsBadgeText: s__('Pipelines|merged results'),
mergedResultsBadgeTooltip: s__(
'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.',
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
@ -526,6 +530,15 @@ export default {
>
{{ $options.i18n.detachedBadgeText }}
</gl-badge>
<gl-badge
v-if="badges.mergedResultsPipeline"
v-gl-tooltip
:title="$options.i18n.mergedResultsBadgeTooltip"
variant="info"
size="sm"
>
{{ $options.i18n.mergedResultsBadgeText }}
</gl-badge>
<gl-badge
v-if="badges.stuck"
v-gl-tooltip

View File

@ -26,6 +26,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
child,
latest,
mergeTrainPipeline,
mergedResultsPipeline,
invalid,
failed,
autoDevops,
@ -62,6 +63,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
child: parseBoolean(child),
latest: parseBoolean(latest),
mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
invalid: parseBoolean(invalid),
failed: parseBoolean(failed),
autoDevops: parseBoolean(autoDevops),

View File

@ -25,12 +25,22 @@ export const EMOJI_VERSION = '3';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
if (
isLocalStorageAvailable &&
window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
window.localStorage.getItem(CACHE_KEY)
) {
return JSON.parse(window.localStorage.getItem(CACHE_KEY));
try {
window.localStorage.removeItem(CACHE_VERSION_KEY);
} catch {
// Cleanup after us and remove the old EMOJI_VERSION_KEY
}
try {
if (isLocalStorageAvailable) {
const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY));
if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) {
return parsed.data;
}
}
} catch {
// Maybe the stored data was corrupted or the version didn't match.
// Let's not error out.
}
// We load the JSON file direct from the server
@ -41,8 +51,7 @@ async function loadEmoji() {
);
try {
window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
} catch {
// Setting data in localstorage may fail when storage quota is exceeded.
// We should continue even when this fails.

View File

@ -0,0 +1,3 @@
import { initCatalog } from '~/ci/catalog/';
initCatalog();

View File

@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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';
@ -15,6 +16,7 @@ import {
SCOPE_NOTES,
SCOPE_COMMITS,
SCOPE_MILESTONES,
SCOPE_WIKI_BLOBS,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@ -24,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
import WikiBlobsFilters from './wiki_blobs_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@ -33,6 +36,7 @@ export default {
BlobsFilters,
ProjectsFilters,
NotesFilters,
WikiBlobsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
@ -41,6 +45,7 @@ export default {
CommitsFilters,
MilestonesFilters,
},
mixins: [glFeatureFlagsMixin()],
computed: {
// useSidebarNavigation refers to whether the new left sidebar navigation is enabled
...mapState(['useSidebarNavigation', 'searchType']),
@ -66,6 +71,12 @@ export default {
showMilestonesFilters() {
return this.currentScope === SCOPE_MILESTONES;
},
showWikiBlobsFilters() {
return (
this.currentScope === SCOPE_WIKI_BLOBS &&
this.glFeatures?.searchProjectWikisHideArchivedProjects
);
},
showScopeNavigation() {
// showScopeNavigation refers to whether the scope navigation should be shown
// while the legacy navigation is being used and there are no search results
@ -93,6 +104,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
<wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
@ -109,6 +121,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
<wiki-blobs-filters v-if="showWikiBlobsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@ -119,6 +132,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
<wiki-blobs-filters v-if="showWikiBlobsFilters" />
</small-screen-drawer-navigation>
</section>
</template>

View File

@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
const scopes = [
'projects',
'issues',
'merge_requests',
'notes',
'blobs',
'commits',
'milestones',
'wiki_blobs',
];
const filterParam = 'include_archived';

View File

@ -0,0 +1,18 @@
<script>
import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
export default {
name: 'WikiBlobsFilters',
components: {
ArchivedFilter,
FiltersTemplate,
},
};
</script>
<template>
<filters-template>
<archived-filter class="gl-mb-5" />
</filters-template>
</template>

View File

@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',

View File

@ -40,15 +40,14 @@ export default {
},
methods: {
getModalInfoCopyStr() {
const stateNameEncoded = this.stateName
? encodeURIComponent(this.stateName)
: '<YOUR-STATE-NAME>';
const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default';
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
-backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
-backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\
-backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\

View File

@ -26,6 +26,11 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
const ALLOWED_ICONS = ['issue-close'];
const ICON_COLORS = {
'issue-close': 'gl-bg-blue-100! gl-text-blue-700',
};
export default {
i18n: {
deleteButtonLabel: __('Remove description history'),
@ -66,6 +71,12 @@ export default {
noteAnchorId() {
return `note_${this.noteId}`;
},
getIconColor() {
return ICON_COLORS[this.note.systemNoteIconName] || '';
},
isAllowedIcon() {
return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
},
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@ -102,9 +113,16 @@ export default {
class="note system-note note-wrapper"
>
<div
class="gl-float-left gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600"
:class="[
getIconColor,
{
'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon,
'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
},
]"
class="gl-float-left gl--flex-center gl-rounded-full gl-relative"
>
<gl-icon :name="note.systemNoteIconName" />
<gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">

View File

@ -0,0 +1,59 @@
/**
Shared styles for system note dot and icon styles used for MR, Issue, Work Item
*/
.system-note-tiny-dot {
width: 8px;
height: 8px;
margin-top: 6px;
margin-left: 12px;
margin-right: 8px;
border: 2px solid var(--gray-50, $gray-50);
}
.system-note-icon {
width: 20px;
height: 20px;
margin-left: 6px;
&.gl-bg-green-100 {
--bg-color: var(--green-100, #{$green-100});
}
&.gl-bg-red-100 {
--bg-color: var(--red-100, #{$red-100});
}
&.gl-bg-blue-100 {
--bg-color: var(--blue-100, #{$blue-100});
}
}
.system-note-icon:not(.mr-system-note-empty)::before {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
bottom: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, transparent, var(--bg-color));
.system-note:first-child & {
display: none;
}
}
.system-note-icon:not(.mr-system-note-empty)::after {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
top: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, var(--bg-color), transparent);
.system-note:last-child & {
display: none;
}
}

View File

@ -1,4 +1,5 @@
@import 'mixins_and_variables_and_functions';
@import 'system_note_styles';
.issuable-details {
section {
@ -104,61 +105,3 @@
@include gl-font-weight-normal;
}
}
.system-note-tiny-dot {
width: 8px;
height: 8px;
margin-top: 6px;
margin-left: 12px;
margin-right: 8px;
border: 2px solid var(--gray-50, $gray-50);
}
.system-note-icon {
width: 20px;
height: 20px;
margin-left: 6px;
&.gl-bg-green-100 {
--bg-color: var(--green-100, #{$green-100});
}
&.gl-bg-red-100 {
--bg-color: var(--red-100, #{$red-100});
}
&.gl-bg-blue-100 {
--bg-color: var(--blue-100, #{$blue-100});
}
}
.system-note-icon:not(.mr-system-note-empty)::before {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
bottom: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, transparent, var(--bg-color));
.system-note:first-child & {
display: none;
}
}
.system-note-icon:not(.mr-system-note-empty)::after {
content: '';
display: block;
position: absolute;
left: calc(50% - 1px);
top: 100%;
width: 2px;
height: 20px;
background: linear-gradient(to bottom, var(--bg-color), transparent);
.system-note:last-child & {
display: none;
}
}

View File

@ -1,4 +1,5 @@
@import 'mixins_and_variables_and_functions';
@import 'system_note_styles';
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
$work-item-overview-right-sidebar-width: 23rem;

View File

@ -190,7 +190,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def update_reviewer_state
if reviewer_state_params[:reviewer_state] === 'approved'
::MergeRequests::ApprovalService
.new(project: @project, current_user: current_user)
.new(project: @project, current_user: current_user, params: approve_params)
.execute(merge_request)
else
::MergeRequests::UpdateReviewerStateService

View File

@ -46,6 +46,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_pipelines_graphql, project)
push_frontend_feature_flag(:notifications_todos_buttons, current_user)
push_frontend_feature_flag(:widget_pipeline_pass_subscription_update, project)
push_frontend_feature_flag(:mr_request_changes, current_user)
end
before_action only: [:edit] do

View File

@ -125,6 +125,13 @@ module Repositories
def log_user_activity
Users::ActivityService.new(author: user, project: project, namespace: project&.namespace).execute
end
def append_info_to_payload(payload)
super
payload[:metadata] ||= {}
payload[:metadata][:repository_storage] = project&.repository_storage
end
end
end

View File

@ -1,27 +0,0 @@
# frozen_string_literal: true
# Mocked data for data transfer
# Follow this epic for recent progress: https://gitlab.com/groups/gitlab-org/-/epics/9330
module DataTransfer
class MockedTransferFinder
def execute
start_date = Date.new(2023, 0o1, 0o1)
date_for_index = ->(i) { (start_date + i.months).strftime('%Y-%m-%d') }
0.upto(11).map do |i|
{
date: date_for_index.call(i),
repository_egress: rand(70000..550000),
artifacts_egress: rand(70000..550000),
packages_egress: rand(70000..550000),
registry_egress: rand(70000..550000)
}.tap do |hash|
hash[:total_egress] = hash
.slice(:repository_egress, :artifacts_egress, :packages_egress, :registry_egress)
.values
.sum
end
end
end
end
end

View File

@ -15,7 +15,7 @@ module Mutations
def resolve(project_path:)
project = authorized_find!(project_path: project_path)
response = ::Ci::Catalog::AddResourceService.new(project, current_user).execute
response = ::Ci::Catalog::Resources::CreateService.new(project, current_user).execute
errors = response.success? ? [] : [response.message]

View File

@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, group)
results = if Feature.enabled?(:data_transfer_monitoring_mock_data, group)
::DataTransfer::MockedTransferFinder.new.execute
else
::DataTransfer::GroupDataTransferFinder.new(
group: group,
from: args[:from],
to: args[:to],
user: current_user
).execute.map(&:attributes)
end
results = ::DataTransfer::GroupDataTransferFinder.new(
group: group,
from: args[:from],
to: args[:to],
user: current_user
).execute.map(&:attributes)
{ egress_nodes: results.to_a }
end

View File

@ -16,16 +16,12 @@ module Resolvers
def resolve(**args)
return { egress_nodes: [] } unless Feature.enabled?(:data_transfer_monitoring, project.group)
results = if Feature.enabled?(:data_transfer_monitoring_mock_data, project.group)
::DataTransfer::MockedTransferFinder.new.execute
else
::DataTransfer::ProjectDataTransferFinder.new(
project: project,
from: args[:from],
to: args[:to],
user: current_user
).execute
end
results = ::DataTransfer::ProjectDataTransferFinder.new(
project: project,
from: args[:from],
to: args[:to],
user: current_user
).execute
{ egress_nodes: results }
end

View File

@ -13,7 +13,6 @@ module Types
def total_egress(parent:)
return unless Feature.enabled?(:data_transfer_monitoring, parent.group)
return 40_000_000 if Feature.enabled?(:data_transfer_monitoring_mock_data, parent.group)
object[:egress_nodes].sum('repository_egress + artifacts_egress + packages_egress + registry_egress')
end

View File

@ -93,16 +93,11 @@ module AuthHelper
end
def saml_providers
auth_providers.select do |provider|
provider == :saml || auth_strategy_class(provider) == 'OmniAuth::Strategies::SAML'
providers = Gitlab.config.omniauth.providers.select do |provider|
provider.name == 'saml' || provider.dig('args', 'strategy_class') == 'OmniAuth::Strategies::SAML'
end
end
def auth_strategy_class(provider)
config = Gitlab::Auth::OAuth::Provider.config_for(provider)
return if config.nil? || config['args'].blank?
config.args['strategy_class']
providers.map(&:name).map(&:to_sym)
end
def any_form_based_providers_enabled?

View File

@ -40,6 +40,7 @@ module Projects
child: pipeline.child?.to_s,
latest: pipeline.latest?.to_s,
merge_train_pipeline: pipeline.merge_train_pipeline?.to_s,
merged_results_pipeline: (pipeline.merged_result_pipeline? && !pipeline.merge_train_pipeline?).to_s,
invalid: pipeline.has_yaml_errors?.to_s,
failed: pipeline.failure_reason?.to_s,
auto_devops: pipeline.auto_devops_source?.to_s,

View File

@ -1,41 +0,0 @@
# frozen_string_literal: true
module Ci
module Catalog
class AddResourceService
include Gitlab::Allowable
attr_reader :project, :current_user
def initialize(project, user)
@current_user = user
@project = project
end
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
validation_response = Ci::Catalog::Resources::ValidateService.new(project, project.default_branch).execute
if validation_response.success?
create_catalog_resource
else
ServiceResponse.error(message: validation_response.message)
end
end
private
def create_catalog_resource
catalog_resource = Ci::Catalog::Resource.new(project: project)
if catalog_resource.valid?
catalog_resource.save!
ServiceResponse.success(payload: catalog_resource)
else
ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
end
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Ci
module Catalog
module Resources
class CreateService
include Gitlab::Allowable
attr_reader :project, :current_user
def initialize(project, user)
@current_user = user
@project = project
end
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :add_catalog_resource, project)
catalog_resource = Ci::Catalog::Resource.new(project: project)
if catalog_resource.valid?
catalog_resource.save!
ServiceResponse.success(payload: catalog_resource)
else
ServiceResponse.error(message: catalog_resource.errors.full_messages.join(', '))
end
end
end
end
end
end

View File

@ -11,11 +11,14 @@ module Ci
end
def execute(&transition)
job.user = current_user
job.job_variables_attributes = variables if variables
transition ||= ->(job) { job.enqueue! }
Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition)
Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job') do |job|
job.user = current_user
job.job_variables_attributes = variables if variables
transition.call(job)
end
ResetSkippedJobsService.new(job.project, current_user).execute(job)

View File

@ -83,7 +83,7 @@
%ul.content-list.todos-list
= render @allowed_todos
= paginate @todos, theme: "gitlab"
.js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.col.js-nothing-here-container.gl-empty-state.gl-text-center.hidden
.svg-content.svg-150
= image_tag 'illustrations/empty-todos-all-done-md.svg'
.text-content.gl-text-center

View File

@ -1,3 +1,3 @@
- page_title _('CI/CD Catalog')
#js-ci-cd-catalog
#js-ci-cd-catalog{ data: { ci_catalog_path: explore_catalog_index_path } }

View File

@ -6,12 +6,12 @@
.form-group
= f.label :name, class: 'label-bold'
= f.text_field :name, class: 'form-control gl-form-input', data: { qa_selector: 'deploy_token_name_field' }, required: true
= f.text_field :name, class: 'form-control gl-form-input', data: { testid: 'deploy-token-name-field' }, required: true
.text-secondary= s_('DeployTokens|Enter a unique name for your deploy token.')
.form-group
= f.label :expires_at, _('Expiration date (optional)'), class: 'label-bold'
= f.gitlab_ui_datepicker :expires_at, data: { qa_selector: 'deploy_token_expires_at_field' }, value: f.object.expires_at
= f.gitlab_ui_datepicker :expires_at, data: { testid: 'deploy-token-expires-at-field' }, value: f.object.expires_at
.text-secondary= s_('DeployTokens|Enter an expiration date for your token. Defaults to never expire.')
.form-group
@ -22,15 +22,15 @@
.form-group
= f.label :scopes, _('Scopes (select at least one)'), class: 'label-bold'
= f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_repository_checkbox' } }
= f.gitlab_ui_checkbox_component :read_repository, 'read_repository', help_text: s_('DeployTokens|Allows read-only access to the repository.'), checkbox_options: { data: { testid: 'deploy-token-read-repository-checkbox' } }
- if container_registry_enabled?(group_or_project)
= f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_registry_checkbox' } }
= f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_registry_checkbox' } }
= f.gitlab_ui_checkbox_component :read_registry, 'read_registry', help_text: s_('DeployTokens|Allows read-only access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-read-registry-checkbox' } }
= f.gitlab_ui_checkbox_component :write_registry, 'write_registry', help_text: s_('DeployTokens|Allows write access to registry images.'), checkbox_options: { data: { testid: 'deploy-token-write-registry-checkbox' } }
- if packages_registry_enabled?(group_or_project)
= f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_read_package_registry_checkbox' } }
= f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } }
= f.gitlab_ui_checkbox_component :read_package_registry, 'read_package_registry', help_text: s_('DeployTokens|Allows read-only access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-read-package-registry-checkbox' } }
= f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { testid: 'deploy-token-write-package-registry-checkbox' } }
.gl-mt-3
= f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true
= f.submit s_('DeployTokens|Create deploy token'), data: { testid: 'create-deploy-token-button' }, pajamas_button: true

View File

@ -1,11 +1,11 @@
.created-deploy-token-container.info-well{ data: { qa_selector: 'created_deploy_token_container' } }
.created-deploy-token-container.info-well{ data: { testid: 'created-deploy-token-container' } }
.well-segment
%h5.gl-mt-0
= s_('DeployTokens|Your new Deploy Token username')
.form-group
.input-group
= text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_user_field' }
= text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-user-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-success
@ -15,7 +15,7 @@
.form-group
.input-group
= text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { qa_selector: 'deploy_token_field' }
= text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus', data: { testid: 'deploy-token-field' }
.input-group-append
= deprecated_clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token'), placement: 'left')
%span.deploy-token-help-block.gl-mt-2.text-danger

View File

@ -1,8 +0,0 @@
---
name: data_transfer_monitoring_mock_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113392
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/397693
milestone: '15.11'
type: development
group: group::source code
default_enabled: false

View File

@ -297,7 +297,8 @@ For example, if you start rolling out new code and:
## Expand and collapse job log sections
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/14664) in GitLab 12.0.
> - Support for output of multi-line command bash shell output [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/3486) in GitLab 16.5 behind the [GitLab Runner feature flag](https://docs.gitlab.com/runner/configuration/feature-flags.html), `FF_SCRIPT_SECTIONS`.
Job logs are divided into sections that can be collapsed or expanded. Each section displays
the duration.

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines"
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
# Development processes
@ -35,32 +35,12 @@ Complementary reads:
### Development guidelines review
When you submit a change to the GitLab development guidelines, who
you ask for reviews depends on the level of change.
For changes to development guidelines, request review and approval from an experienced GitLab Team Member.
#### Wording, style, or link changes
Not all changes require extensive review. For example, MRs that don't change the
content's meaning or function can be reviewed, approved, and merged by any
maintainer or Technical Writer. These can include:
- Typo fixes.
- Clarifying links, such as to external programming language documentation.
- Changes to comply with the [Documentation Style Guide](documentation/index.md)
that don't change the intent of the documentation page.
#### Specific changes
If the MR proposes changes that are limited to a particular stage, group, or team,
request a review and approval from an experienced GitLab Team Member in that
group. For example, if you're documenting a new internal API used exclusively by
For example, if you're documenting a new internal API used exclusively by
a given group, request an engineering review from one of the group's members.
After the engineering review is complete, assign the MR to the
[Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
in the modified documentation page's metadata.
If the page is not assigned to a specific group, follow the
[Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
Small fixes, like typos, can be merged by any user with at least the Maintainer role.
#### Broader changes
@ -85,7 +65,6 @@ In these cases, use the following workflow:
- [Quality](https://about.gitlab.com/handbook/engineering/quality/)
- [Engineering Productivity](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity/)
- [Infrastructure](https://about.gitlab.com/handbook/engineering/infrastructure/)
- [Technical Writing](https://about.gitlab.com/handbook/product/ux/technical-writing/)
You can skip this step for MRs authored by EMs or Staff Engineers responsible
for their area.
@ -97,15 +76,12 @@ In these cases, use the following workflow:
author / approver of the MR.
If this is a significant change across multiple areas, request final review
and approval from the VP of Development, the DRI for Development Guidelines,
@clefelhocz1.
and approval from the VP of Development, who is the DRI for development guidelines.
1. After all approvals are complete, assign the MR to the
[Technical Writer associated with the stage and group](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments)
in the modified documentation page's metadata.
If the page is not assigned to a specific group, follow the
[Technical Writing review process for development guidelines](https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-development-guidelines).
The Technical Writer may ask for additional approvals as previously suggested before merging the MR.
Any Maintainer can merge the MR.
If you would like a review by a technical writer, post a message in the #docs Slack channel.
Technical writers do not need to review the content, however, and any Maintainer
other than the MR author can merge.
### Reviewer values
@ -114,6 +90,8 @@ In these cases, use the following workflow:
As a reviewer or as a reviewee, make sure to familiarize yourself with
the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/reviewer-values/) we strive for at GitLab.
Also, any doc content should follow the [Documentation Style Guide](documentation/index.md).
## Language-specific guides
### Go guides

View File

@ -36,6 +36,13 @@ A member of the Technical Writing team adds these labels:
`docs::` prefix. For example, `~docs::improvement`.
- The [`~Technical Writing` team label](../labels/index.md#team-labels).
NOTE:
With the exception of `/doc/development/documentation`,
technical writers do not review content in the `doc/development` directory.
Any Maintainer can merge content in the `doc/development` directory.
If you would like a technical writer review of content in the `doc/development` directory,
ask in the `#docs` Slack channel.
## Post-merge reviews
If not assigned to a Technical Writer for review prior to merging, a review must be scheduled

View File

@ -14,6 +14,13 @@ when developing new features or instrumenting existing ones.
## Fundamental concepts
<div class="video-fallback">
See the video about <a href="https://www.youtube.com/watch?v=GtFNXbjygWo">the concepts of events and metrics.</a>
</div>
<figure class="video_container">
<iframe src="https://www.youtube-nocookie.com/embed/GtFNXbjygWo" frameborder="0" allowfullscreen="true"> </iframe>
</figure>
Events and metrics are the foundation of the internal analytics system.
Understanding the difference between the two concepts is vital to using the system.

View File

@ -0,0 +1,102 @@
---
stage: Create
group: Source Code
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Project Repository Storage Moves
This document was created to help contributors understand the code design of
[project repository storage moves](../../api/project_repository_storage_moves.md).
Read this document before making changes to the code for this feature.
This document is intentionally limited to an overview of how the code is
designed, as code can change often. To understand how a specific part of the
feature works, view the code and the specs. The details here explain how the
major components of the Code Owners feature work.
NOTE:
This document should be updated when parts of the codebase referenced in this
document are updated, removed, or new parts are added.
## Business logic
- `Projects::RepositoryStorageMove`: Tracks the move, includes state machine.
- Defined in `app/models/projects/repository_storage_move.rb`.
- `RepositoryStorageMovable`: Contains the state machine logic, validators, and some helper methods.
- Defined in `app/models/concerns/repository_storage_movable.rb`.
- `Project`: The project model.
- Defined in `app/models/project.rb`.
- `CanMoveRepositoryStorage`: Contains helper methods that are into `Project`.
- Defined in `app/models/concerns/can_move_repository_storage.rb`.
- `API::ProjectRepositoryStorageMoves`: API class for project repository storage moves.
- Defined in `lib/api/project_repository_storage_moves.rb`.
- `Entities::Projects::RepositoryStorageMove`: API entity for serializing the `Projects::RepositoryStorageMove` model.
- Defined in `lib/api/entities/projects/repository_storage_moves.rb`.
- `Projects::ScheduleBulkRepositoryShardMovesService`: Service to schedule bulk moves.
- Defined in `app/services/projects/schedule_bulk_repository_shard_moves_service.rb`.
- `ScheduleBulkRepositoryShardMovesMethods`: Generic methods for bulk moves.
- Defined in `app/services/concerns/schedule_bulk_repository_shard_moves_methods.rb`.
- `Projects::ScheduleBulkRepositoryShardMovesWorker`: Worker to handle bulk moves.
- Defined in `app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb`.
- `Projects::UpdateRepositoryStorageWorker`: Finds repository storage move and then calls the update storage service.
- Defined in `app/workers/projects/update_repository_storage_worker.rb`.
- `UpdateRepositoryStorageWorker`: Module containing generic logic for `Projects::UpdateRepositoryStorageWorker`.
- Defined in `app/workers/concerns/update_repository_storage_worker.rb`.
- `Projects::UpdateRepositoryStorageService`: Performs the move.
- Defined in `app/services/projects/update_repository_storage_service.rb`.
- `UpdateRepositoryStorageMethods`: Module with generic methods included in `Projects::UpdateRepositoryStorageService`.
- Defined in `app/services/concerns/update_repository_storage_methods.rb`.
- `Projects::UpdateService`: Schedules move if the passed parameters request a move.
- Defined in `app/services/projects/update_service.rb`.
- `PoolRepository`: Ruby object representing Gitaly `ObjectPool`.
- Defined in `app/models/pool_repository.rb`.
- `ObjectPool::CreateWorker`: Worker to create an `ObjectPool` via `Gitaly`.
- Defined in `app/workers/object_pool/create_worker.rb`.
- `ObjectPool::JoinWorker`: Worker to join an `ObjectPool` via `Gitaly`.
- Defined in `app/workers/object_pool/join_worker.rb`.
- `ObjectPool::ScheduleJoinWorker`: Worker to schedule an `ObjectPool::JoinWorker`.
- Defined in `app/workers/object_pool/schedule_join_worker.rb`.
- `ObjectPool::DestroyWorker`: Worker to destroy an `ObjectPool` via `Gitaly`.
- Defined in `app/workers/object_pool/destroy_worker.rb`.
- `ObjectPoolQueue`: Module to configure `ObjectPool` workers.
- Defined in `app/workers/concerns/object_pool_queue.rb`.
- `Repositories::ReplicateService`: Handles replication of data from one repository to another.
- Defined in `app/services/repositories/replicate_service.rb`.
## Flow
These flowcharts should help explain the flow from the endpoints down to the
models for different features.
### Schedule a repository storage move via the API
```mermaid
graph TD
A[<code>POST /api/:version/project_repository_storage_moves</code>] --> C
B[<code>POST /api/:version/projects/:id/repository_storage_moves</code>] --> D
C[Schedule move for each project in shard] --> D[Set state to scheduled]
D --> E[<code>after_transition callback</code>]
E --> F{<code>set_repository_read_only!</code>}
F -->|success| H[Schedule repository update worker]
F -->|error| G[Set state to failed]
```
### Moving the storage after being scheduled
```mermaid
graph TD
A[Repository update worker scheduled] --> B{State is scheduled?}
B -->|Yes| C[Set state to started]
B -->|No| D[Return success]
C --> E{Same filesystem?}
E -.-> G[Set project repo to writable]
E -->|Yes| F["Mirror repositories (project, wiki, design, & pool)"]
G --> H[Update repo storage value]
H --> I[Set state to finished]
I --> J[Associate project with new pool repository]
J --> K[Unlink old pool repository]
K --> L[Update project repository storage values]
L --> N[Remove old paths if same filesystem]
N --> M[Set state to finished]
```

View File

@ -11,8 +11,6 @@ type: index, reference
You can get AI generated support from GitLab Duo Chat about the following topics:
- How to use GitLab.
- Questions about an issue.
- How to use GitLab.
- Questions about an issue.
- Question about an epic.

View File

@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Organization
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/409913) in GitLab 16.1 [with a flag](../../administration/feature_flags.md) named `ui_for_organizations`. Disabled by default.
FLAG:
This feature is not ready for production use.
On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../../administration/feature_flags.md) named `ui_for_organizations`.
On GitLab.com, this feature is not available.
DISCLAIMER:
This page contains information related to upcoming products, features, and functionality.
It is important to note that the information presented is for informational purposes only.
@ -37,6 +44,37 @@ see [epic 9265](https://gitlab.com/groups/gitlab-org/-/epics/9265).
For a video introduction to the new hierarchy concept for groups and projects for epics, see
[Consolidating groups and projects update (August 2021)](https://www.youtube.com/watch?v=fE74lsG_8yM).
## View organizations
To view the organizations you have access to:
- On the left sidebar, select **Organizations** (**{organization}**).
## Create an organization
1. On the left sidebar, at the top, select **Create new** (**{plus}**) and **New organization**.
1. In the **Organization name** field, enter a name for the organization.
1. In the **Organization URL** field, enter a path for the organization.
1. Select **Create organization**.
## Edit an organization's name
1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to edit.
1. Select **Settings > General**.
1. Update the **Organization name** field.
1. Select **Save changes**.
## Manage groups and projects
1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
1. Select **Manage > Groups and projects**.
1. To switch between groups and projects, use the **Display** filter next to the search box.
## Manage users
1. On the left sidebar, select **Organizations** (**{organization}**) and find the organization you want to manage.
1. Select **Manage > Users**.
## Related topics
- [Organization developer documentation](../../development/organization/index.md)

View File

@ -82,6 +82,7 @@ or:
> - Filtering by `reviewer` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47605) in GitLab 13.7.
> - Filtering by potential approvers was moved to GitLab Premium in 13.9.
> - Filtering by `approved-by` moved to GitLab Premium in 13.9.
> - Filtering by `source-branch` [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134555) in GitLab 16.6.
To filter the list of merge requests:

View File

@ -1,11 +1,14 @@
# frozen_string_literal: true
# DEPRECATED. Consider using using Internal Events tracking framework
# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
require 'rails/generators'
module Gitlab
module UsageMetricDefinition
class RedisHllGenerator < Rails::Generators::Base
desc 'Generates a metric definition .yml file with defaults for Redis HLL.'
desc '[DEPRECATED] Generates a metric definition .yml file with defaults for Redis HLL.'
argument :category, type: :string, desc: "Category name"
argument :events, type: :array, desc: "Unique event names", banner: 'event_one event_two event_three'

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# DEPRECATED. Consider using using Internal Events tracking framework
# https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html
require 'rails/generators'
module Gitlab
@ -30,7 +33,7 @@ module Gitlab
source_root File.expand_path('../../../generator_templates/usage_metric_definition', __dir__)
desc 'Generates metric definitions yml files'
desc '[DEPRECATED] Generates metric definitions yml files'
class_option :ee, type: :boolean, optional: true, default: false, desc: 'Indicates if metric is for ee'
class_option :dir,
@ -40,6 +43,13 @@ module Gitlab
argument :key_paths, type: :array, desc: 'Unique JSON key paths for the metrics'
def create_metric_file
say("This generator is DEPRECATED. Use Internal Events tracking framework instead.")
# rubocop: disable Gitlab/DocUrl -- link for developers, not users
say("https://docs.gitlab.com/ee/development/internal_analytics/internal_event_instrumentation/quick_start.html")
# rubocop: enable Gitlab/DocUrl
desc = ask("Would you like to continue anyway? y/N") || 'n'
return unless desc.casecmp('y') == 0
validate!
key_paths.each do |key_path|

View File

@ -90,7 +90,7 @@ module Gitlab
result = ::Gitlab::Instrumentation::RedisClusterValidator.validate(commands)
return true if result.nil?
if !result[:valid] && !result[:allowed] && (Rails.env.development? || Rails.env.test?)
if !result[:valid] && !result[:allowed] && raise_cross_slot_validation_errors?
raise RedisClusterValidator::CrossSlotError, "Redis command #{result[:command_name]} arguments hash to different slots. See https://docs.gitlab.com/ee/development/redis.html#multi-key-commands"
end
@ -189,6 +189,10 @@ module Gitlab
redirection_type, _, target_node_key = err_msg.split
{ redirection_type: redirection_type, target_node_key: target_node_key }
end
def raise_cross_slot_validation_errors?
Rails.env.development? || Rails.env.test?
end
end
end
end

View File

@ -79,7 +79,7 @@ module Gitlab
end
def create_ci_catalog(project)
result = ::Ci::Catalog::AddResourceService.new(project, @current_user).execute
result = ::Ci::Catalog::Resources::CreateService.new(project, @current_user).execute
if result.success?
result.payload
else

View File

@ -10337,6 +10337,9 @@ msgstr ""
msgid "CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier."
msgstr ""
msgid "CiCatalog|Discover CI configuration resources for a seamless CI/CD experience."
msgstr ""
msgid "CiCatalog|Get started with the CI/CD Catalog"
msgstr ""
@ -12601,9 +12604,6 @@ msgstr ""
msgid "ComplianceReport|Remove framework from selected projects"
msgstr ""
msgid "ComplianceReport|Retrieving the compliance framework report failed. Refresh the page and try again."
msgstr ""
msgid "ComplianceReport|Search target branch"
msgstr ""
@ -35330,10 +35330,13 @@ msgstr ""
msgid "Pipelines|This pipeline is stuck"
msgstr ""
msgid "Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch."
msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of all other merge requests queued for merging into the target branch."
msgstr ""
msgid "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch."
msgid "Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch."
msgstr ""
msgid "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch."
msgstr ""
msgid "Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables."
@ -35417,6 +35420,9 @@ msgstr ""
msgid "Pipelines|merge train"
msgstr ""
msgid "Pipelines|merged results"
msgstr ""
msgid "Pipelines|stuck"
msgstr ""
@ -40429,6 +40435,9 @@ msgstr ""
msgid "Request a new one"
msgstr ""
msgid "Request changes"
msgstr ""
msgid "Request data is too large"
msgstr ""
@ -46503,6 +46512,15 @@ msgstr ""
msgid "Submit feedback"
msgstr ""
msgid "Submit feedback and approve these changes."
msgstr ""
msgid "Submit feedback that should be addressed before merging."
msgstr ""
msgid "Submit general feedback without explicit approval."
msgstr ""
msgid "Submit review"
msgstr ""

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
module QA
module Page
module Component
module DeployToken
extend QA::Page::PageConcern
def self.included(base)
super
base.view 'app/views/shared/deploy_tokens/_form.html.haml' do
element 'deploy-token-name-field'
element 'deploy-token-expires-at-field'
element 'deploy-token-read-repository-checkbox'
element 'deploy-token-read-package-registry-checkbox'
element 'deploy-token-write-package-registry-checkbox'
element 'deploy-token-read-registry-checkbox'
element 'deploy-token-write-registry-checkbox'
element 'create-deploy-token-button'
end
base.view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
element 'created-deploy-token-container'
element 'deploy-token-user-field'
element 'deploy-token-field'
end
end
def fill_token_name(name)
fill_element('deploy-token-name-field', name)
end
def fill_token_expires_at(expires_at)
fill_element('deploy-token-expires-at-field', "#{expires_at}\n")
end
def fill_scopes(scopes)
check_element('deploy-token-read-repository-checkbox', true) if scopes.include? :read_repository
check_element('deploy-token-read-package-registry-checkbox', true) if scopes.include? :read_package_registry
check_element('deploy-token-write-package-registry-checkbox', true) if scopes.include? :write_package_registry
check_element('deploy-token-read-registry-checkbox', true) if scopes.include? :read_registry
check_element('deploy-token-write-registry-checkbox', true) if scopes.include? :write_registry
end
def add_token
click_element('create-deploy-token-button')
end
def token_username
within_new_project_deploy_token do
find_element('deploy-token-user-field').value
end
end
def token_password
within_new_project_deploy_token do
find_element('deploy-token-field').value
end
end
private
def within_new_project_deploy_token(&block)
has_element?('created-deploy-token-container', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
within_element('created-deploy-token-container', &block)
end
end
end
end
end

View File

@ -5,60 +5,7 @@ module QA
module Group
module Settings
class GroupDeployTokens < Page::Base
view 'app/views/shared/deploy_tokens/_form.html.haml' do
element :deploy_token_name_field
element :deploy_token_expires_at_field
element :deploy_token_read_repository_checkbox
element :deploy_token_read_package_registry_checkbox
element :deploy_token_read_registry_checkbox
element :deploy_token_write_package_registry_checkbox
element :create_deploy_token_button
end
view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
element :created_deploy_token_container
element :deploy_token_user_field
element :deploy_token_field
end
def fill_token_name(name)
fill_element(:deploy_token_name_field, name)
end
def fill_token_expires_at(expires_at)
fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
end
def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false )
check_element(:deploy_token_read_repository_checkbox, true) if read_repository
check_element(:deploy_token_read_package_registry_checkbox, true) if read_package_registry
check_element(:deploy_token_read_registry_checkbox, true) if read_registry
check_element(:deploy_token_write_package_registry_checkbox, true) if write_package_registry
end
def add_token
click_element(:create_deploy_token_button)
end
def token_username
within_new_project_deploy_token do
find_element(:deploy_token_user_field).value
end
end
def token_password
within_new_project_deploy_token do
find_element(:deploy_token_field).value
end
end
private
def within_new_project_deploy_token(&block)
has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
within_element(:created_deploy_token_container, &block)
end
include Page::Component::DeployToken
end
end
end

View File

@ -1,84 +0,0 @@
# frozen_string_literal: true
module QA
module Page
module Project
module Settings
class DeployTokens < Page::Base
view 'app/views/shared/deploy_tokens/_form.html.haml' do
element :deploy_token_name_field
element :deploy_token_expires_at_field
element :deploy_token_read_repository_checkbox
element :deploy_token_read_package_registry_checkbox
element :deploy_token_write_package_registry_checkbox
element :deploy_token_read_registry_checkbox
element :deploy_token_write_registry_checkbox
element :create_deploy_token_button
end
view 'app/views/shared/deploy_tokens/_new_deploy_token.html.haml' do
element :created_deploy_token_container
element :deploy_token_user_field
element :deploy_token_field
end
def fill_token_name(name)
fill_element(:deploy_token_name_field, name)
end
def fill_token_expires_at(expires_at)
fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
end
def fill_scopes(scopes)
if scopes.include? :read_repository
check_element(:deploy_token_read_repository_checkbox, true)
end
if scopes.include? :read_package_registry
check_element(:deploy_token_read_package_registry_checkbox, true)
end
if scopes.include? :write_package_registry
check_element(:deploy_token_write_package_registry_checkbox, true)
end
if scopes.include? :read_registry
check_element(:deploy_token_read_registry_checkbox, true)
end
if scopes.include? :write_registry
check_element(:deploy_token_write_registry_checkbox, true)
end
end
def add_token
click_element(:create_deploy_token_button)
end
def token_username
within_new_project_deploy_token do
find_element(:deploy_token_user_field).value
end
end
def token_password
within_new_project_deploy_token do
find_element(:deploy_token_field).value
end
end
private
def within_new_project_deploy_token
has_element?(:created_deploy_token_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
within_element(:created_deploy_token_container) do
yield
end
end
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module QA
module Page
module Project
module Settings
class ProjectDeployTokens < Page::Base
include Page::Component::DeployToken
end
end
end
end
end

View File

@ -33,7 +33,7 @@ module QA
def expand_deploy_tokens(&block)
expand_content('deploy-tokens-settings-content') do
Settings::DeployTokens.perform(&block)
Settings::ProjectDeployTokens.perform(&block)
end
end

View File

@ -51,7 +51,7 @@ module QA
setting.expand_deploy_tokens do |page|
page.fill_token_name(name)
page.fill_token_expires_at(expires_at)
page.fill_scopes(read_repository: true, read_package_registry: true, write_package_registry: true)
page.fill_scopes(@scopes)
page.add_token
end

View File

@ -1,7 +1,11 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_security do
RSpec.describe 'Verify', :runner, product_group: :pipeline_security,
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422863',
type: :flaky
} do
describe 'Unlocking job artifacts across parent-child pipelines' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:project) { create(:project, name: 'unlock-job-artifacts-parent-child-project') }

View File

@ -200,4 +200,24 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
end
end
end
describe '#append_info_to_payload' do
let(:log_payload) { {} }
let(:container) { project.design_management_repository }
let(:repository_path) { "#{container.full_path}.git" }
let(:params) { { repository_path: repository_path, service: 'git-upload-pack' } }
let(:repository_storage) { "default" }
before do
allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *|
method.call(log_payload)
end
end
it 'appends metadata for logging' do
post :git_upload_pack, params: params
expect(controller).to have_received(:append_info_to_payload)
expect(log_payload.dig(:metadata, :repository_storage)).to eq(repository_storage)
end
end
end

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Global Catalog', :js, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:group) }
let_it_be(:user) { create(:user) }
before_all do
namespace.add_developer(user)
end
before do
sign_in(user)
end
describe 'GET explore/catalog' do
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:ci_resource_projects) do
create_list(
:project,
3,
:repository,
description: 'A simple component',
namespace: namespace
)
end
before do
ci_resource_projects.each do |current_project|
create(:ci_catalog_resource, project: current_project)
end
visit explore_catalog_index_path
wait_for_requests
end
it 'shows CI Catalog title and description', :aggregate_failures do
expect(page).to have_content('CI/CD Catalog')
expect(page).to have_content('Discover CI configuration resources for a seamless CI/CD experience.')
end
it 'renders CI Catalog resources list' do
expect(find_all('[data-testid="catalog-resource-item"]').length).to be(3)
end
context 'for a single CI/CD catalog resource' do
it 'renders resource details', :aggregate_failures do
within_testid('catalog-resource-item', match: :first) do
expect(page).to have_content(ci_resource_projects[2].name)
expect(page).to have_content(ci_resource_projects[2].description)
expect(page).to have_content(namespace.name)
end
end
context 'when clicked' do
before do
find_by_testid('ci-resource-link', match: :first).click
end
it 'navigate to the details page' do
expect(page).to have_content('Go to the project')
end
end
end
end
describe 'GET explore/catalog/:id' do
let_it_be(:project) { create(:project, :repository, namespace: namespace) }
let_it_be(:new_ci_resource) { create(:ci_catalog_resource, project: project) }
before do
visit explore_catalog_path(id: new_ci_resource["id"])
end
it 'navigates to the details page' do
expect(page).to have_content('Go to the project')
end
end
end

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DataTransfer::MockedTransferFinder, feature_category: :source_code_management do
describe '#execute' do
subject(:execute) { described_class.new.execute }
it 'returns mock data' do
expect(execute.first).to include(
date: '2023-01-01',
repository_egress: be_a(Integer),
artifacts_egress: be_a(Integer),
packages_egress: be_a(Integer),
registry_egress: be_a(Integer),
total_egress: be_a(Integer)
)
expect(execute.size).to eq(12)
end
end
end

View File

@ -1,5 +1,5 @@
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
import { CACHE_KEY } from '~/emoji/constants';
export const validEmoji = {
atom: {
@ -105,9 +105,8 @@ export function clearEmojiMock() {
initEmojiMap.promise = null;
}
export async function initEmojiMock(mockData = mockEmojiData) {
export async function initEmojiMock(data = mockEmojiData) {
clearEmojiMock();
localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
await initEmojiMap();
}

View File

@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => {
let storage = {};
return {
get length() {
return Object.keys(storage).length;
},
clear: jest.fn(() => {
storage = {};
}),

View File

@ -19,7 +19,11 @@ let wrapper;
let publishReview;
let trackingSpy;
function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
function factory({
canApprove = true,
shouldAnimateReviewButton = false,
mrRequestChanges = false,
} = {}) {
publishReview = jest.fn();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
const requestHandlers = [
@ -75,6 +79,9 @@ function factory({ canApprove = true, shouldAnimateReviewButton = false } = {})
wrapper = mountExtended(SubmitDropdown, {
store,
apolloProvider,
provide: {
glFeatures: { mrRequestChanges },
},
});
}
@ -101,6 +108,7 @@ describe('Batch comments submit dropdown', () => {
note: 'Hello world',
approve: false,
approval_password: '',
reviewer_state: 'reviewed',
});
});
@ -171,4 +179,52 @@ describe('Batch comments submit dropdown', () => {
);
},
);
describe('when mrRequestChanges feature flag is enabled', () => {
it('renders a radio group with review state options', async () => {
factory({ mrRequestChanges: true });
await waitForPromises();
expect(wrapper.findAll('.gl-form-radio').length).toBe(3);
});
it('renders disabled approve radio button when user can not approve', async () => {
factory({ mrRequestChanges: true, canApprove: false });
wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
await waitForPromises();
expect(wrapper.find('.custom-control-input[value="approved"]').attributes('disabled')).toBe(
'disabled',
);
});
it.each`
value
${'approved'}
${'reviewed'}
${'requested_changes'}
`('sends $value review state to api when submitting', async ({ value }) => {
factory({ mrRequestChanges: true });
wrapper.findComponent(GlDisclosureDropdown).vm.$emit('shown');
await waitForPromises();
await wrapper.find(`.custom-control-input[value="${value}"]`).trigger('change');
findForm().vm.$emit('submit', { preventDefault: jest.fn() });
expect(publishReview).toHaveBeenCalledWith(expect.anything(), {
noteable_type: 'merge_request',
noteable_id: 1,
note: 'Hello world',
approve: false,
approval_password: '',
reviewer_state: value,
});
});
});
});

View File

@ -10,36 +10,53 @@ describe('CatalogHeader', () => {
let wrapper;
const defaultProps = {};
const defaultProvide = {
const customProvide = {
pageTitle: 'Catalog page',
pageDescription: 'This is a nice catalog page',
};
const findBanner = () => wrapper.findComponent(GlBanner);
const findFeedbackButton = () => findBanner().findComponent(GlButton);
const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
const findTitle = () => wrapper.find('h1');
const findDescription = () => wrapper.findByTestId('description');
const createComponent = ({ props = {}, stubs = {} } = {}) => {
const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
wrapper = shallowMountExtended(CatalogHeader, {
propsData: {
...defaultProps,
...props,
},
provide: defaultProvide,
provide,
stubs: {
...stubs,
},
});
};
it('renders the Catalog title and description', () => {
createComponent();
describe('title and description', () => {
describe('when there are no values provided', () => {
beforeEach(() => {
createComponent();
});
expect(findTitle().exists()).toBe(true);
expect(findDescription().exists()).toBe(true);
it('renders the default values', () => {
expect(findTitle().text()).toBe('CI/CD Catalog');
expect(findDescription().text()).toBe(
'Discover CI configuration resources for a seamless CI/CD experience.',
);
});
});
describe('when custom values are provided', () => {
beforeEach(() => {
createComponent({ provide: customProvide });
});
it('renders the custom values', () => {
expect(findTitle().text()).toBe(customProvide.pageTitle);
expect(findDescription().text()).toBe(customProvide.pageDescription);
});
});
});
describe('Feedback banner', () => {
describe('when user has never dismissed', () => {
beforeEach(() => {

View File

@ -0,0 +1,211 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/alert';
import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
import { cacheConfig } from '~/ci/catalog/graphql/settings';
import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql';
import { emptyCatalogResponseBody, catalogResponseBody } from '../../mock';
Vue.use(VueApollo);
jest.mock('~/alert');
describe('CiResourcesPage', () => {
let wrapper;
let catalogResourcesResponse;
const createComponent = () => {
const handlers = [[getCatalogResources, catalogResourcesResponse]];
const mockApollo = createMockApollo(handlers, {}, cacheConfig);
wrapper = shallowMountExtended(ciResourcesPage, {
apolloProvider: mockApollo,
});
return waitForPromises();
};
const findCatalogHeader = () => wrapper.findComponent(CatalogHeader);
const findCiResourcesList = () => wrapper.findComponent(CiResourcesList);
const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader);
const findEmptyState = () => wrapper.findComponent(EmptyState);
beforeEach(() => {
catalogResourcesResponse = jest.fn();
});
describe('when initial queries are loading', () => {
beforeEach(() => {
createComponent();
});
it('shows a loading icon and no list', () => {
expect(findLoadingState().exists()).toBe(true);
expect(findEmptyState().exists()).toBe(false);
expect(findCiResourcesList().exists()).toBe(false);
});
});
describe('when queries have loaded', () => {
it('renders the Catalog Header', async () => {
await createComponent();
expect(findCatalogHeader().exists()).toBe(true);
});
describe('and there are no resources', () => {
beforeEach(async () => {
catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody);
await createComponent();
});
it('renders the empty state', () => {
expect(findLoadingState().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
expect(findCiResourcesList().exists()).toBe(false);
});
});
describe('and there are resources', () => {
const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
beforeEach(async () => {
catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
await createComponent();
});
it('renders the resources list', () => {
expect(findLoadingState().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(false);
expect(findCiResourcesList().exists()).toBe(true);
});
it('passes down props to the resources list', () => {
expect(findCiResourcesList().props()).toMatchObject({
currentPage: 1,
resources: nodes,
pageInfo,
totalCount: count,
});
});
});
});
describe('pagination', () => {
it.each`
eventName
${'onPrevPage'}
${'onNextPage'}
`('refetch query with new params when receiving $eventName', async ({ eventName }) => {
const { pageInfo } = catalogResponseBody.data.ciCatalogResources;
catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
await createComponent();
expect(catalogResourcesResponse).toHaveBeenCalledTimes(1);
await findCiResourcesList().vm.$emit(eventName);
expect(catalogResourcesResponse).toHaveBeenCalledTimes(2);
if (eventName === 'onNextPage') {
expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
after: pageInfo.endCursor,
first: 20,
});
} else {
expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({
before: pageInfo.startCursor,
last: 20,
first: null,
});
}
});
});
describe('pages count', () => {
describe('when the fetchMore call suceeds', () => {
beforeEach(async () => {
catalogResourcesResponse.mockResolvedValue(catalogResponseBody);
await createComponent();
});
it('increments and drecrements the page count correctly', async () => {
expect(findCiResourcesList().props().currentPage).toBe(1);
findCiResourcesList().vm.$emit('onNextPage');
await waitForPromises();
expect(findCiResourcesList().props().currentPage).toBe(2);
await findCiResourcesList().vm.$emit('onPrevPage');
await waitForPromises();
expect(findCiResourcesList().props().currentPage).toBe(1);
});
});
describe('when the fetchMore call fails', () => {
const errorMessage = 'there was an error';
describe('for next page', () => {
beforeEach(async () => {
catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
await createComponent();
});
it('does not increment the page and calls createAlert', async () => {
expect(findCiResourcesList().props().currentPage).toBe(1);
findCiResourcesList().vm.$emit('onNextPage');
await waitForPromises();
expect(findCiResourcesList().props().currentPage).toBe(1);
expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
});
});
describe('for previous page', () => {
beforeEach(async () => {
// Initial query
catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
// When clicking on next
catalogResourcesResponse.mockResolvedValueOnce(catalogResponseBody);
// when clicking on previous
catalogResourcesResponse.mockRejectedValue({ message: errorMessage });
await createComponent();
});
it('does not decrement the page and calls createAlert', async () => {
expect(findCiResourcesList().props().currentPage).toBe(1);
findCiResourcesList().vm.$emit('onNextPage');
await waitForPromises();
expect(findCiResourcesList().props().currentPage).toBe(2);
findCiResourcesList().vm.$emit('onPrevPage');
await waitForPromises();
expect(findCiResourcesList().props().currentPage).toBe(2);
expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
});
});
});
});
});

View File

@ -0,0 +1,17 @@
import { shallowMount } from '@vue/test-utils';
import GlobalCatalog from '~/ci/catalog/global_catalog.vue';
import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
describe('GlobalCatalog', () => {
let wrapper;
const findHomeComponent = () => wrapper.findComponent(CiCatalogHome);
beforeEach(() => {
wrapper = shallowMount(GlobalCatalog);
});
it('renders the catalog home component', () => {
expect(findHomeComponent().exists()).toBe(true);
});
});

View File

@ -0,0 +1,48 @@
import Vue from 'vue';
import { initCatalog } from '~/ci/catalog/';
import * as Router from '~/ci/catalog/router';
import CiResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue';
describe('~/ci/catalog/index', () => {
describe('initCatalog', () => {
const SELECTOR = 'SELECTOR';
let el;
let component;
const baseRoute = '/explore/catalog';
const createElement = () => {
el = document.createElement('div');
el.id = SELECTOR;
el.dataset.ciCatalogPath = baseRoute;
document.body.appendChild(el);
};
afterEach(() => {
el = null;
});
describe('when the element exists', () => {
beforeEach(() => {
createElement();
jest.spyOn(Router, 'createRouter');
component = initCatalog(`#${SELECTOR}`);
});
it('returns a Vue Instance', () => {
expect(component).toBeInstanceOf(Vue);
});
it('creates a router with the received base path and component', () => {
expect(Router.createRouter).toHaveBeenCalledTimes(1);
expect(Router.createRouter).toHaveBeenCalledWith(baseRoute, CiResourcesPage);
});
});
describe('When the element does not exist', () => {
it('returns `null`', () => {
expect(initCatalog('foo')).toBe(null);
});
});
});
});

View File

@ -1,5 +1,23 @@
import { componentsMockData } from '~/ci/catalog/constants';
export const emptyCatalogResponseBody = {
data: {
ciCatalogResources: {
pageInfo: {
startCursor:
'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
endCursor:
'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
hasNextPage: false,
hasPreviousPage: false,
__typename: 'PageInfo',
},
count: 0,
nodes: [],
},
},
};
export const catalogResponseBody = {
data: {
ciCatalogResources: {

View File

@ -97,6 +97,7 @@ describe('Pipeline details header', () => {
child: false,
latest: true,
mergeTrainPipeline: false,
mergedResultsPipeline: false,
invalid: false,
failed: false,
autoDevops: false,

View File

@ -1,9 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import {
emojiFixtureMap,
initEmojiMock,
validEmoji,
invalidEmoji,
clearEmojiMock,
mockEmojiData,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
import { createMockClient } from 'helpers/mock_apollo_helper';
@ -16,6 +18,7 @@ import {
getEmojiMap,
emojiFallbackImageSrc,
loadCustomEmojiWithNames,
EMOJI_VERSION,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@ -26,8 +29,11 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useLocalStorageSpy } from 'jest/__helpers__/local_storage_helper';
let mockClient;
jest.mock('~/lib/graphql', () => {
@ -74,6 +80,195 @@ function createMockEmojiClient() {
document.body.dataset.groupFullPath = 'test-group';
}
describe('retrieval of emojis.json', () => {
useLocalStorageSpy();
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(/emojis\.json$/).reply(HTTP_STATUS_OK, mockEmojiData);
initEmojiMap.promise = null;
});
afterEach(() => {
mock.restore();
});
const assertCorrectLocalStorage = () => {
expect(localStorage.length).toBe(1);
expect(localStorage.getItem(CACHE_KEY)).toBe(
JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
);
};
const assertEmojiBeingLoadedCorrectly = () => {
expect(Object.keys(getEmojiMap())).toEqual(Object.keys(validEmoji));
};
it('should remove the old `CACHE_VERSION_KEY`', async () => {
localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
await initEmojiMap();
expect(localStorage.getItem(CACHE_VERSION_KEY)).toBe(null);
});
describe('when the localStorage is empty', () => {
it('should call the API and store results in localStorage', async () => {
await initEmojiMap();
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(1);
assertCorrectLocalStorage();
});
});
describe('when the localStorage stores the correct version', () => {
beforeEach(async () => {
localStorage.setItem(CACHE_KEY, JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }));
localStorage.setItem.mockClear();
await initEmojiMap();
});
it('should not call the API and not mutate the localStorage', () => {
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(0);
expect(localStorage.setItem).not.toHaveBeenCalled();
assertCorrectLocalStorage();
});
});
describe('when the localStorage stores an incorrect version', () => {
beforeEach(async () => {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ data: mockEmojiData, EMOJI_VERSION: `${EMOJI_VERSION}-different` }),
);
localStorage.setItem.mockClear();
await initEmojiMap();
});
it('should call the API and store results in localStorage', () => {
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(1);
assertCorrectLocalStorage();
});
});
describe('when the localStorage stores corrupted data', () => {
beforeEach(async () => {
localStorage.setItem(CACHE_KEY, "[invalid: 'INVALID_JSON");
localStorage.setItem.mockClear();
await initEmojiMap();
});
it('should call the API and store results in localStorage', () => {
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(1);
assertCorrectLocalStorage();
});
});
describe('when the localStorage stores data in a different format', () => {
beforeEach(async () => {
localStorage.setItem(CACHE_KEY, JSON.stringify([]));
localStorage.setItem.mockClear();
await initEmojiMap();
});
it('should call the API and store results in localStorage', () => {
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(1);
assertCorrectLocalStorage();
});
});
describe('when the localStorage is full', () => {
beforeEach(async () => {
const oldSetItem = localStorage.setItem;
localStorage.setItem = jest.fn().mockImplementationOnce((key, value) => {
if (key === CACHE_KEY) {
throw new Error('Storage Full');
}
oldSetItem(key, value);
});
await initEmojiMap();
});
it('should call API but not store the results', () => {
assertEmojiBeingLoadedCorrectly();
expect(mock.history.get.length).toBe(1);
expect(localStorage.length).toBe(0);
expect(localStorage.setItem).toHaveBeenCalledTimes(1);
expect(localStorage.setItem).toHaveBeenCalledWith(
CACHE_KEY,
JSON.stringify({ data: mockEmojiData, EMOJI_VERSION }),
);
});
});
describe('backwards compatibility', () => {
// As per: https://gitlab.com/gitlab-org/gitlab/-/blob/62b66abd3bb7801e7c85b4e42a1bbd51fbb37c1b/app/assets/javascripts/emoji/index.js#L27-52
async function prevImplementation() {
if (
window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
window.localStorage.getItem(CACHE_KEY)
) {
return JSON.parse(window.localStorage.getItem(CACHE_KEY));
}
// We load the JSON file direct from the server
// because it can't be loaded from a CDN due to
// cross domain problems with JSON
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
try {
window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
} catch {
// Setting data in localstorage may fail when storage quota is exceeded.
// We should continue even when this fails.
}
return data;
}
it('Old -> New -> Old should not break', async () => {
// The follow steps simulate a multi-version deployment. e.g.
// Hitting a page on "regular" .com, then canary, and then "regular" again
// Load emoji the old way to pre-populate the cache
let res = await prevImplementation();
expect(res).toEqual(mockEmojiData);
expect(mock.history.get.length).toBe(1);
localStorage.setItem.mockClear();
// Load emoji the new way
await initEmojiMap();
expect(mock.history.get.length).toBe(2);
assertEmojiBeingLoadedCorrectly();
assertCorrectLocalStorage();
localStorage.setItem.mockClear();
// Load emoji the old way to pre-populate the cache
res = await prevImplementation();
expect(res).toEqual(mockEmojiData);
expect(mock.history.get.length).toBe(3);
expect(localStorage.setItem.mock.calls).toEqual([
[CACHE_VERSION_KEY, EMOJI_VERSION],
[CACHE_KEY, JSON.stringify(mockEmojiData)],
]);
// Load emoji the old way should work again (and be taken from the cache)
res = await prevImplementation();
expect(res).toEqual(mockEmojiData);
expect(mock.history.get.length).toBe(3);
});
});
});
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();

View File

@ -17,6 +17,7 @@ import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
import WikiBlobsFilters from '~/search/sidebar/components/wiki_blobs_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';
@ -44,6 +45,11 @@ describe('GlobalSearchSidebar', () => {
wrapper = shallowMount(GlobalSearchSidebar, {
store,
provide: {
glFeatures: {
searchProjectWikisHideArchivedProjects: true,
},
},
});
};
@ -55,6 +61,7 @@ describe('GlobalSearchSidebar', () => {
const findNotesFilters = () => wrapper.findComponent(NotesFilters);
const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters);
const findWikiBlobsFilters = () => wrapper.findComponent(WikiBlobsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
@ -85,6 +92,8 @@ describe('GlobalSearchSidebar', () => {
${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'wiki_blobs'} | ${findWikiBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
`('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);

View File

@ -8,13 +8,13 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'aws/eu-central-1';
const stateNamePlaceholder = '<YOUR-STATE-NAME>';
const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
-backend-config="address=${terraformApiUrl}/${stateNameEncoded}" \\
-backend-config="lock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="unlock_address=${terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME" \\
-backend-config="lock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="unlock_address=${terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
@ -67,7 +67,7 @@ describe('InitCommandModal', () => {
describe('init command', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
`-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
`-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
it('includes correct username', () => {
@ -94,7 +94,7 @@ describe('InitCommandModal', () => {
describe('on rendering', () => {
it('includes correct address', () => {
expect(findInitCommand().text()).toContain(
`-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`,
`-backend-config="address=${terraformApiUrl}/$TF_STATE_NAME"`,
);
});
});

View File

@ -40,8 +40,14 @@ describe('Work Items system note component', () => {
);
});
it('should render svg icon', () => {
expect(findTimelineIcon().exists()).toBe(true);
it('should render svg icon only for allowed icons', () => {
expect(findTimelineIcon().exists()).toBe(false);
const ALLOWED_ICONS = ['issue-close'];
ALLOWED_ICONS.forEach((icon) => {
createComponent({ note: { ...workItemSystemNoteWithMetadata, systemNoteIconName: icon } });
expect(findTimelineIcon().exists()).toBe(true);
});
});
it('should not show compare previous version for FOSS', () => {

View File

@ -10,6 +10,7 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
let(:finder_results) do
[
build(:project_data_transfer, date: to, repository_egress: 250000)
@ -41,21 +42,12 @@ RSpec.describe Resolvers::DataTransfer::GroupDataTransferResolver, feature_categ
include_examples 'Data transfer resolver'
context 'when data_transfer_monitoring_mock_data is disabled' do
let(:finder) { instance_double(::DataTransfer::GroupDataTransferFinder) }
it 'calls GroupDataTransferFinder with expected arguments' do
expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
group: group, from: from, to: to, user: current_user).once.and_return(finder)
allow(finder).to receive(:execute).once.and_return(finder_results)
before do
stub_feature_flags(data_transfer_monitoring_mock_data: false)
end
it 'calls GroupDataTransferFinder with expected arguments' do
expect(::DataTransfer::GroupDataTransferFinder).to receive(:new).with(
group: group, from: from, to: to, user: current_user
).once.and_return(finder)
allow(finder).to receive(:execute).once.and_return(finder_results)
expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
end
expect(resolve_egress).to eq({ egress_nodes: finder_results.map(&:attributes) })
end
end

View File

@ -10,6 +10,9 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
let(:finder_results) do
[
{
@ -44,21 +47,12 @@ RSpec.describe Resolvers::DataTransfer::ProjectDataTransferResolver, feature_cat
include_examples 'Data transfer resolver'
context 'when data_transfer_monitoring_mock_data is disabled' do
let(:finder) { instance_double(::DataTransfer::ProjectDataTransferFinder) }
it 'calls ProjectDataTransferFinder with expected arguments' do
expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
project: project, from: from, to: to, user: current_user).once.and_return(finder)
allow(finder).to receive(:execute).once.and_return(finder_results)
before do
stub_feature_flags(data_transfer_monitoring_mock_data: false)
end
it 'calls ProjectDataTransferFinder with expected arguments' do
expect(::DataTransfer::ProjectDataTransferFinder).to receive(:new).with(
project: project, from: from, to: to, user: current_user
).once.and_return(finder)
allow(finder).to receive(:execute).once.and_return(finder_results)
expect(resolve_egress).to eq({ egress_nodes: finder_results })
end
expect(resolve_egress).to eq({ egress_nodes: finder_results })
end
end

View File

@ -14,25 +14,15 @@ RSpec.describe GitlabSchema.types['ProjectDataTransfer'], feature_category: :sou
let_it_be(:project) { create(:project) }
let(:from) { Date.new(2022, 1, 1) }
let(:to) { Date.new(2023, 1, 1) }
let(:finder_result) { 40_000_000 }
let(:relation) { instance_double(ActiveRecord::Relation) }
it 'returns mock data' do
expect(resolve_field(:total_egress, { from: from, to: to }, extras: { parent: project },
arg_style: :internal)).to eq(finder_result)
before do
allow(relation).to receive(:sum).and_return(10)
end
context 'when data_transfer_monitoring_mock_data is disabled' do
let(:relation) { instance_double(ActiveRecord::Relation) }
before do
allow(relation).to receive(:sum).and_return(10)
stub_feature_flags(data_transfer_monitoring_mock_data: false)
end
it 'calls sum on active record relation' do
expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
arg_style: :internal)).to eq(10)
end
it 'calls sum on active record relation' do
expect(resolve_field(:total_egress, { egress_nodes: relation }, extras: { parent: project },
arg_style: :internal)).to eq(10)
end
end
end

View File

@ -2,7 +2,9 @@
require "spec_helper"
RSpec.describe AuthHelper do
RSpec.describe AuthHelper, feature_category: :system_access do
include LoginHelpers
describe "button_based_providers" do
it 'returns all enabled providers from devise' do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
@ -310,88 +312,16 @@ RSpec.describe AuthHelper do
end
end
describe '#auth_strategy_class' do
subject(:auth_strategy_class) { helper.auth_strategy_class(name) }
context 'when configuration specifies no provider' do
let(:name) { 'does_not_exist' }
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
context 'when configuration specifies a provider with args but without strategy_class' do
let(:name) { 'google_oauth2' }
let(:provider) do
Struct.new(:name, :args).new(
name,
'app_id' => 'YOUR_APP_ID'
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
context 'when configuration specifies a provider with args and strategy_class' do
let(:name) { 'provider1' }
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
let(:provider) do
Struct.new(:name, :args).new(
name,
'strategy_class' => strategy
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns the class' do
expect(auth_strategy_class).to eq(strategy)
end
end
context 'when configuration specifies another provider with args and another strategy_class' do
let(:name) { 'provider1' }
let(:strategy) { 'OmniAuth::Strategies::LDAP' }
let(:provider) do
Struct.new(:name, :args).new(
'another_name',
'strategy_class' => strategy
)
end
before do
allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
it 'returns false' do
expect(auth_strategy_class).to be_falsey
end
end
end
describe '#saml_providers' do
subject(:saml_providers) { helper.saml_providers }
let(:saml_strategy) { 'OmniAuth::Strategies::SAML' }
let(:saml_provider_1_name) { 'saml_provider_1' }
let(:saml_provider_1_name) { 'saml' }
let(:saml_provider_1) do
Struct.new(:name, :args).new(
saml_provider_1_name,
'strategy_class' => saml_strategy
{}
)
end
@ -422,7 +352,7 @@ RSpec.describe AuthHelper do
context 'when SAML is enabled without specifying a strategy class' do
before do
allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the saml provider' do
@ -432,8 +362,7 @@ RSpec.describe AuthHelper do
context 'when configuration specifies no provider' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
stub_omniauth_config(providers: [])
end
it 'returns an empty list' do
@ -443,30 +372,27 @@ RSpec.describe AuthHelper do
context 'when configuration specifies a provider with a SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1])
stub_omniauth_config(providers: [saml_provider_1])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name])
expect(saml_providers).to match_array([saml_provider_1_name.to_sym])
end
end
context 'when configuration specifies two providers with a SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, saml_provider_2_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, saml_provider_2])
stub_omniauth_config(providers: [saml_provider_1, saml_provider_2])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
context 'when configuration specifies a provider with a non-SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([ldap_provider_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([ldap_provider])
stub_omniauth_config(providers: [ldap_provider])
end
it 'returns an empty list' do
@ -476,12 +402,11 @@ RSpec.describe AuthHelper do
context 'when configuration specifies four providers but only two with SAML strategy_class' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([saml_provider_1_name, ldap_provider_name, saml_provider_2_name, google_oauth2_provider_name])
allow(Gitlab.config.omniauth).to receive(:providers).and_return([saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
stub_omniauth_config(providers: [saml_provider_1, ldap_provider, saml_provider_2, google_oauth2_provider])
end
it 'returns the provider' do
expect(saml_providers).to match_array([saml_provider_1_name, saml_provider_2_name])
expect(saml_providers).to match_array([saml_provider_1_name.to_sym, saml_provider_2_name.to_sym])
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout do
RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:category) { 'test_category' }
@ -16,6 +16,10 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
stub_const("#{Gitlab::UsageMetricDefinitionGenerator}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
allow_next_instance_of(Gitlab::UsageMetricDefinitionGenerator) do |instance|
allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
end
end
after do

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_category: :service_ping do
include UsageDataHelpers
let(:key_path) { 'counts_weekly.test_metric' }
@ -14,6 +14,10 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir)
# Stub Prometheus requests from Gitlab::Utils::UsageData
stub_prometheus_queries
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:ask).and_return('y') # confirm deprecation warning
end
end
after do
@ -100,4 +104,19 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout do
expect(files.count).to eq(2)
end
end
['n', 'N', 'random word', nil].each do |answer|
context "when user agreed with deprecation warning by typing: #{answer}" do
it 'does not create definition file' do
allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:ask).and_return(answer)
end
described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name }).invoke_all
files = Dir.glob(File.join(temp_dir, 'metrics/counts_7d/*_metric.yml'))
expect(files.count).to eq(0)
end
end
end
end

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Saml::Config do
include LoginHelpers
describe '.enabled?' do
subject { described_class.enabled? }
@ -10,7 +12,7 @@ RSpec.describe Gitlab::Auth::Saml::Config do
context 'when SAML is enabled' do
before do
allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:saml])
stub_basic_saml_config
end
it { is_expected.to eq(true) }

View File

@ -42,15 +42,19 @@ RSpec.describe Gitlab::InstrumentationHelper, :clean_gitlab_redis_repository_cac
context 'when Redis calls are made' do
let_it_be(:redis_store_class) { define_helper_redis_store_class }
before do # init redis connection with `test` env details
before do
redis_store_class.with(&:ping)
Gitlab::Redis::Queues.with(&:ping)
RequestStore.clear!
end
it 'adds Redis data and omits Gitaly data' do
stub_rails_env('staging') # to avoid raising CrossSlotError
it 'adds Redis data including cross slot calls' do
expect(Gitlab::Instrumentation::RedisBase)
.to receive(:raise_cross_slot_validation_errors?)
.once.and_return(false)
redis_store_class.with { |redis| redis.mset('test-cache', 123, 'test-cache2', 123) }
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis_store_class.with { |redis| redis.mget('cache-test', 'cache-test-2') }
end

View File

@ -53,7 +53,7 @@ RSpec.describe ::Gitlab::Seeders::Ci::Catalog::ResourceSeeder, feature_category:
context 'when ci resource creation fails' do
before do
allow_next_instance_of(::Ci::Catalog::AddResourceService) do |service|
allow_next_instance_of(::Ci::Catalog::Resources::CreateService) do |service|
allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error'))
end
end

View File

@ -71,45 +71,21 @@ RSpec.describe 'group data transfers', feature_category: :source_code_management
context 'when user has enough permissions' do
before do
group.add_owner(current_user)
subject
end
context 'when data_transfer_monitoring_mock_data is NOT enabled' do
before do
stub_feature_flags(data_transfer_monitoring_mock_data: false)
subject
end
it 'returns real results' do
expect(response).to have_gitlab_http_status(:ok)
it 'returns real results' do
expect(response).to have_gitlab_http_status(:ok)
expect(egress_data.count).to eq(2)
expect(egress_data.count).to eq(2)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
end
it_behaves_like 'a working graphql query'
expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 6])
end
context 'when data_transfer_monitoring_mock_data is enabled' do
before do
stub_feature_flags(data_transfer_monitoring_mock_data: true)
subject
end
it 'returns mock results' do
expect(response).to have_gitlab_http_status(:ok)
expect(egress_data.count).to eq(12)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
end
it_behaves_like 'a working graphql query'
end
it_behaves_like 'a working graphql query'
end
end

View File

@ -36,19 +36,5 @@ RSpec.describe 'CatalogResourcesCreate', feature_category: :pipeline_composition
expect(response).to have_gitlab_http_status(:success)
end
end
context 'with an invalid project' do
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_owner(current_user)
end
it 'returns an error' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_mutation_response(:catalog_resources_create)['errors']).not_to be_empty
end
end
end
end

View File

@ -68,45 +68,21 @@ RSpec.describe 'project data transfers', feature_category: :source_code_manageme
context 'when user has enough permissions' do
before do
project.add_owner(current_user)
subject
end
context 'when data_transfer_monitoring_mock_data is NOT enabled' do
before do
stub_feature_flags(data_transfer_monitoring_mock_data: false)
subject
end
it 'returns real results' do
expect(response).to have_gitlab_http_status(:ok)
it 'returns real results' do
expect(response).to have_gitlab_http_status(:ok)
expect(egress_data.count).to eq(2)
expect(egress_data.count).to eq(2)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
end
it_behaves_like 'a working graphql query'
expect(egress_data.pluck('repositoryEgress')).to match_array(%w[1 2])
end
context 'when data_transfer_monitoring_mock_data is enabled' do
before do
stub_feature_flags(data_transfer_monitoring_mock_data: true)
subject
end
it 'returns mock results' do
expect(response).to have_gitlab_http_status(:ok)
expect(egress_data.count).to eq(12)
expect(egress_data.first.keys).to match_array(
%w[date totalEgress repositoryEgress artifactsEgress packagesEgress registryEgress]
)
end
it_behaves_like 'a working graphql query'
end
it_behaves_like 'a working graphql query'
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_composition do
RSpec.describe Ci::Catalog::Resources::CreateService, feature_category: :pipeline_composition do
let_it_be(:project) { create(:project, :catalog_resource_with_components) }
let_it_be(:user) { create(:user) }
@ -32,20 +32,6 @@ RSpec.describe Ci::Catalog::AddResourceService, feature_category: :pipeline_comp
end
end
context 'with an invalid project' do
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_owner(user)
end
it 'does not create a catalog resource' do
response = service.execute
expect(response.message).to eq('Project must have a description, Project must contain components')
end
end
context 'with an invalid catalog resource' do
it 'does not save the catalog resource' do
catalog_resource = instance_double(::Ci::Catalog::Resource,

View File

@ -78,4 +78,33 @@ RSpec.describe Ci::EnqueueJobService, '#execute', feature_category: :continuous_
execute
end
end
context 'when the job is manually triggered another user' do
let(:job_variables) do
[{ key: 'third', secret_value: 'third' },
{ key: 'fourth', secret_value: 'fourth' }]
end
let(:service) do
described_class.new(build, current_user: user, variables: job_variables)
end
it 'assigns the user and variables to the job', :aggregate_failures do
called = false
service.execute do
unless called
called = true
raise ActiveRecord::StaleObjectError
end
build.enqueue!
end
build.reload
expect(called).to be true # ensure we actually entered the failure path
expect(build.user).to eq(user)
expect(build.job_variables.map(&:key)).to contain_exactly('third', 'fourth')
end
end
end

View File

@ -70,4 +70,3 @@
- UploaderFinder
- UserGroupNotificationSettingsFinder
- UserGroupsCounter
- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed

View File

@ -262,19 +262,15 @@ module LoginHelpers
end
def stub_omniauth_config(messages)
allow(Gitlab.config.omniauth).to receive_messages(messages)
allow(Gitlab.config.omniauth).to receive_messages(GitlabSettings::Options.build(messages))
end
def stub_basic_saml_config
allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
allow(config).to receive_messages({ options: { name: 'saml', args: {} } })
end
stub_omniauth_config(providers: [{ name: 'saml', args: {} }])
end
def stub_saml_group_config(groups)
allow_next_instance_of(Gitlab::Auth::Saml::Config) do |config|
allow(config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
end
stub_omniauth_config(providers: [{ name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} }])
end
end

View File

@ -1,16 +1,6 @@
# frozen_string_literal: true
RSpec.shared_examples 'Data transfer resolver' do
it 'returns mock data' do |_query_object|
mocked_data = ['mocked_data']
allow_next_instance_of(DataTransfer::MockedTransferFinder) do |instance|
allow(instance).to receive(:execute).and_return(mocked_data)
end
expect(resolve_egress[:egress_nodes]).to eq(mocked_data)
end
context 'when data_transfer_monitoring is disabled' do
before do
stub_feature_flags(data_transfer_monitoring: false)