Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a34d7fd9a7
commit
2308cd5020
|
|
@ -1 +1 @@
|
|||
9bf28d2089501b82b40e2b9f6ad21cf80751f15f
|
||||
c65b631d971809d9e0294356d7892860d4800cf3
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<script>
|
||||
import CiCatalogHome from './components/ci_catalog_home.vue';
|
||||
|
||||
export default {
|
||||
components: { CiCatalogHome },
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<ci-catalog-home />
|
||||
</template>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { initCatalog } from '~/ci/catalog/';
|
||||
|
||||
initCatalog();
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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" \\
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ export const createLocalStorageSpy = () => {
|
|||
let storage = {};
|
||||
|
||||
return {
|
||||
get length() {
|
||||
return Object.keys(storage).length;
|
||||
},
|
||||
clear: jest.fn(() => {
|
||||
storage = {};
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ describe('Pipeline details header', () => {
|
|||
child: false,
|
||||
latest: true,
|
||||
mergeTrainPipeline: false,
|
||||
mergedResultsPipeline: false,
|
||||
invalid: false,
|
||||
failed: false,
|
||||
autoDevops: false,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -70,4 +70,3 @@
|
|||
- UploaderFinder
|
||||
- UserGroupNotificationSettingsFinder
|
||||
- UserGroupsCounter
|
||||
- DataTransfer::MockedTransferFinder # Can be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/397693 is closed
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue