Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-04 12:10:55 +00:00
parent 9f0d276489
commit f2fd07aa1c
90 changed files with 1064 additions and 512 deletions

View File

@ -308,6 +308,10 @@ Rails/RakeEnvironment:
# Context on why it's disabled: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93419#note_1048223982 # Context on why it's disabled: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93419#note_1048223982
Enabled: false Enabled: false
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96675#note_1094403693
Rails/WhereExists:
Enabled: false
# GitLab ################################################################### # GitLab ###################################################################
Gitlab/ModuleWithInstanceVariables: Gitlab/ModuleWithInstanceVariables:

View File

@ -3920,7 +3920,6 @@ Layout/LineLength:
- 'spec/features/groups/settings/repository_spec.rb' - 'spec/features/groups/settings/repository_spec.rb'
- 'spec/features/groups_spec.rb' - 'spec/features/groups_spec.rb'
- 'spec/features/ide/static_object_external_storage_csp_spec.rb' - 'spec/features/ide/static_object_external_storage_csp_spec.rb'
- 'spec/features/incidents/user_views_incident_spec.rb'
- 'spec/features/invites_spec.rb' - 'spec/features/invites_spec.rb'
- 'spec/features/issuables/issuable_list_spec.rb' - 'spec/features/issuables/issuable_list_spec.rb'
- 'spec/features/issuables/markdown_references/internal_references_spec.rb' - 'spec/features/issuables/markdown_references/internal_references_spec.rb'

View File

@ -1,30 +1,28 @@
--- ---
# Cop supports --auto-correct. # Cop supports --auto-correct.
Rails/NegateInclude: Rails/NegateInclude:
# Offense count: 65 Details: grace period
# Temporarily disabled due to too many offenses
Enabled: false
Exclude: Exclude:
- 'app/finders/projects_finder.rb' - 'app/finders/projects_finder.rb'
- 'app/helpers/application_settings_helper.rb' - 'app/helpers/application_settings_helper.rb'
- 'app/helpers/projects_helper.rb' - 'app/helpers/projects_helper.rb'
- 'app/helpers/tree_helper.rb' - 'app/helpers/tree_helper.rb'
- 'app/models/concerns/timebox.rb'
- 'app/models/integrations/chat_message/pipeline_message.rb' - 'app/models/integrations/chat_message/pipeline_message.rb'
- 'app/models/integrations/field.rb'
- 'app/models/label.rb' - 'app/models/label.rb'
- 'app/models/merge_request.rb' - 'app/models/merge_request.rb'
- 'app/models/milestone.rb'
- 'app/services/todo_service.rb' - 'app/services/todo_service.rb'
- 'app/services/work_items/parent_links/create_service.rb'
- 'config/application.rb' - 'config/application.rb'
- 'config/initializers/1_settings.rb' - 'config/initializers/1_settings.rb'
- 'danger/roulette/Dangerfile' - 'danger/roulette/Dangerfile'
- 'ee/app/finders/security/pipeline_vulnerabilities_finder.rb' - 'ee/app/finders/security/pipeline_vulnerabilities_finder.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/vulnerability.rb' - 'ee/app/models/ee/vulnerability.rb'
- 'ee/app/services/epic_issues/create_service.rb' - 'ee/app/services/epic_issues/create_service.rb'
- 'ee/app/services/security/ingestion/tasks/ingest_remediations.rb' - 'ee/app/services/security/ingestion/tasks/ingest_remediations.rb'
- 'ee/app/services/security/security_orchestration_policies/validate_policy_service.rb' - 'ee/app/services/security/security_orchestration_policies/validate_policy_service.rb'
- 'lib/api/maven_packages.rb' - 'lib/api/maven_packages.rb'
- 'lib/generators/gitlab/usage_metric_generator.rb'
- 'lib/gitlab/background_migration/legacy_upload_mover.rb' - 'lib/gitlab/background_migration/legacy_upload_mover.rb'
- 'lib/gitlab/ci/build/rules/rule/clause/exists.rb' - 'lib/gitlab/ci/build/rules/rule/clause/exists.rb'
- 'lib/gitlab/ci/parsers/coverage/sax_document.rb' - 'lib/gitlab/ci/parsers/coverage/sax_document.rb'
@ -38,11 +36,10 @@ Rails/NegateInclude:
- 'lib/gitlab/task_helpers.rb' - 'lib/gitlab/task_helpers.rb'
- 'lib/gitlab/url_blocker.rb' - 'lib/gitlab/url_blocker.rb'
- 'lib/gitlab_edition.rb' - 'lib/gitlab_edition.rb'
- 'qa/qa/page/merge_request/show.rb'
- 'qa/qa/runtime/ip_address.rb' - 'qa/qa/runtime/ip_address.rb'
- 'qa/qa/support/run.rb' - 'qa/qa/support/run.rb'
- 'qa/qa/tools/delete_test_users.rb' - 'qa/qa/tools/delete_test_users.rb'
- 'qa/qa/vendor/jenkins/page/configure_job.rb'
- 'qa/qa/vendor/jenkins/page/last_job_console.rb'
- 'rubocop/cop/gitlab/feature_available_usage.rb' - 'rubocop/cop/gitlab/feature_available_usage.rb'
- 'rubocop/cop/graphql/id_type.rb' - 'rubocop/cop/graphql/id_type.rb'
- 'rubocop/cop/migration/add_reference.rb' - 'rubocop/cop/migration/add_reference.rb'
@ -56,3 +53,4 @@ Rails/NegateInclude:
- 'spec/support/matchers/pushed_frontend_feature_flags_matcher.rb' - 'spec/support/matchers/pushed_frontend_feature_flags_matcher.rb'
- 'spec/support/shared_contexts/markdown_golden_master_shared_examples.rb' - 'spec/support/shared_contexts/markdown_golden_master_shared_examples.rb'
- 'spec/uploaders/object_storage_spec.rb' - 'spec/uploaders/object_storage_spec.rb'
- 'tooling/danger/specs.rb'

View File

@ -1,44 +0,0 @@
---
# Cop supports --auto-correct.
Rails/WhereExists:
# Offense count: 48
# Temporarily disabled due to too many offenses
Enabled: false
Exclude:
- 'app/models/application_setting/term.rb'
- 'app/models/ci/pipeline_artifact.rb'
- 'app/models/ci/ref.rb'
- 'app/models/clusters/agent.rb'
- 'app/models/concerns/has_wiki.rb'
- 'app/models/concerns/noteable.rb'
- 'app/models/container_repository.rb'
- 'app/models/design_management/design.rb'
- 'app/models/group.rb'
- 'app/models/group_deploy_token.rb'
- 'app/models/label.rb'
- 'app/models/lfs_object.rb'
- 'app/models/merge_request_diff.rb'
- 'app/models/namespace.rb'
- 'app/models/project.rb'
- 'app/models/protected_branch/push_access_level.rb'
- 'app/services/projects/transfer_service.rb'
- 'app/services/todos/destroy/unauthorized_features_service.rb'
- 'db/migrate/20210422195929_create_elastic_reindexing_slices.rb'
- 'ee/app/models/approval_merge_request_rule_source.rb'
- 'ee/app/models/concerns/ee/protected_ref_access.rb'
- 'ee/app/models/ee/epic.rb'
- 'ee/app/models/ee/group_member.rb'
- 'ee/app/models/ee/milestone_release.rb'
- 'ee/app/models/geo_node.rb'
- 'ee/app/models/merge_requests/external_status_check.rb'
- 'ee/app/models/merge_train.rb'
- 'ee/app/workers/concerns/elastic/indexing_control.rb'
- 'lib/gitlab/auth.rb'
- 'lib/gitlab/checks/matching_merge_request.rb'
- 'lib/gitlab/database/partitioning/detached_partition_dropper.rb'
- 'spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb'
- 'spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_features_spec.rb'
- 'spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/services/clusters/cleanup/service_account_service_spec.rb'
- 'spec/services/clusters/destroy_service_spec.rb'

View File

@ -1 +1 @@
b55578ec476e8bc8ecd9775ee7e9960b52e0f6e0 f75740430e51520d3edcd22065285cec050d2b74

View File

@ -11,7 +11,7 @@ export default {
BoardSettingsSidebar, BoardSettingsSidebar,
BoardTopBar, BoardTopBar,
}, },
inject: ['disabled'], inject: ['disabled', 'fullBoardId'],
computed: { computed: {
...mapGetters(['isSidebarOpen']), ...mapGetters(['isSidebarOpen']),
}, },
@ -27,7 +27,7 @@ export default {
<template> <template>
<div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }"> <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
<board-top-bar /> <board-top-bar />
<board-content :disabled="disabled" /> <board-content :disabled="disabled" :board-id="fullBoardId" />
<board-settings-sidebar /> <board-settings-sidebar />
</div> </div>
</template> </template>

View File

@ -3,12 +3,24 @@ import { GlAlert } from '@gitlab/ui';
import { sortBy, throttle } from 'lodash'; import { sortBy, throttle } from 'lodash';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { s__ } from '~/locale';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants'; import { defaultSortableOptions } from '~/sortable/constants';
import { DraggableItemTypes } from '../constants'; import {
DraggableItemTypes,
issuableTypes,
BoardType,
listsQuery,
} from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue'; import BoardColumn from './board_column.vue';
export default { export default {
i18n: {
fetchError: s__(
'Boards|An error occurred while fetching the board lists. Please reload the page.',
),
},
draggableItemTypes: DraggableItemTypes, draggableItemTypes: DraggableItemTypes,
components: { components: {
BoardAddNewColumn, BoardAddNewColumn,
@ -19,26 +31,76 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'), EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert, GlAlert,
}, },
inject: ['canAdminList'], inject: ['canAdminList', 'boardType', 'fullPath', 'issuableType', 'isApolloBoard'],
props: { props: {
disabled: { disabled: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
boardId: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
boardHeight: null, boardHeight: null,
boardListsApollo: {},
apolloError: null,
updatedBoardId: this.boardId,
}; };
}, },
apollo: {
boardListsApollo: {
query() {
return listsQuery[this.issuableType].query;
},
variables() {
return this.queryVariables;
},
skip() {
return !this.isApolloBoard;
},
update(data) {
const { lists } = data[this.boardType].board;
return formatBoardLists(lists);
},
result() {
// this allows us to delay fetching lists when we switch a board to fetch the actual board lists
// instead of fetching lists for the "previous" board
this.updatedBoardId = this.boardId;
},
error() {
this.apolloError = this.$options.i18n.fetchError;
},
},
},
computed: { computed: {
...mapState(['boardLists', 'error', 'addColumnForm']), ...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn', 'isEpicBoard', 'isIssueBoard']), ...mapGetters(['isSwimlanesOn']),
isIssueBoard() {
return this.issuableType === issuableTypes.issue;
},
isEpicBoard() {
return this.issuableType === issuableTypes.epic;
},
addColumnFormVisible() { addColumnFormVisible() {
return this.addColumnForm?.visible; return this.addColumnForm?.visible;
}, },
queryVariables() {
return {
...(this.isIssueBoard && {
isGroup: this.boardType === BoardType.group,
isProject: this.boardType === BoardType.project,
}),
fullPath: this.fullPath,
boardId: this.boardId,
filterParams: this.filterParams,
};
},
boardListsToUse() { boardListsToUse() {
return sortBy([...Object.values(this.boardLists)], 'position'); const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
return sortBy([...Object.values(lists)], 'position');
}, },
canDragColumns() { canDragColumns() {
return this.canAdminList; return this.canAdminList;
@ -59,6 +121,9 @@ export default {
return this.canDragColumns ? options : {}; return this.canDragColumns ? options : {};
}, },
errorToDisplay() {
return this.isApolloBoard ? this.apolloError : this.error;
},
}, },
mounted() { mounted() {
this.setBoardHeight(); this.setBoardHeight();
@ -88,8 +153,8 @@ export default {
<template> <template>
<div v-cloak data-qa-selector="boards_list"> <div v-cloak data-qa-selector="boards_list">
<gl-alert v-if="error" variant="danger" :dismissible="true" @dismiss="unsetError"> <gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="unsetError">
{{ error }} {{ errorToDisplay }}
</gl-alert> </gl-alert>
<component <component
:is="boardColumnWrapper" :is="boardColumnWrapper"

View File

@ -53,6 +53,8 @@ function mountBoardApp(el) {
store, store,
apolloProvider, apolloProvider,
provide: { provide: {
isApolloBoard: window.gon?.features?.apolloBoards,
fullBoardId: fullBoardId(boardId),
disabled: parseBoolean(el.dataset.disabled), disabled: parseBoolean(el.dataset.disabled),
groupId: Number(groupId), groupId: Number(groupId),
rootPath, rootPath,

View File

@ -3,7 +3,7 @@
import $ from 'jquery'; import $ from 'jquery';
import issuableEventHub from '~/issues/list/eventhub'; import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select'; import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select'; import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
@ -55,7 +55,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() { initDropdowns() {
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); mountMilestoneDropdown();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy // Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at // the import/no-unresolved lint rule when FOSS_ONLY=1, even though at

View File

@ -29,7 +29,7 @@ export default {
dagDocPath: { dagDocPath: {
default: null, default: null,
}, },
emptySvgPath: { emptyDagSvgPath: {
default: '', default: '',
}, },
pipelineIid: { pipelineIid: {
@ -213,7 +213,7 @@ export default {
/> />
<gl-empty-state <gl-empty-state
v-else-if="hasNoDependentJobs" v-else-if="hasNoDependentJobs"
:svg-path="emptySvgPath" :svg-path="emptyDagSvgPath"
:title="$options.emptyStateTexts.title" :title="$options.emptyStateTexts.title"
> >
<template #description> <template #description>

View File

@ -1,12 +1,13 @@
<script> <script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui'; import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { failedJobsTabName, jobsTabName, needsTabName, testReportTabName } from '../constants'; import {
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue'; failedJobsTabName,
import Dag from './dag/dag.vue'; jobsTabName,
import FailedJobsApp from './jobs/failed_jobs_app.vue'; needsTabName,
import JobsApp from './jobs/jobs_app.vue'; pipelineTabName,
import TestReports from './test_reports/test_reports.vue'; testReportTabName,
} from '../constants';
export default { export default {
i18n: { i18n: {
@ -19,20 +20,16 @@ export default {
}, },
}, },
tabNames: { tabNames: {
pipeline: pipelineTabName,
needs: needsTabName, needs: needsTabName,
jobs: jobsTabName, jobs: jobsTabName,
failures: failedJobsTabName, failures: failedJobsTabName,
tests: testReportTabName, tests: testReportTabName,
}, },
components: { components: {
Dag,
GlBadge, GlBadge,
GlTab, GlTab,
GlTabs, GlTabs,
JobsApp,
FailedJobsApp,
PipelineGraphWrapper,
TestReports,
}, },
inject: [ inject: [
'defaultTabValue', 'defaultTabValue',
@ -41,14 +38,27 @@ export default {
'totalJobCount', 'totalJobCount',
'testsCount', 'testsCount',
], ],
data() {
return {
activeTab: this.defaultTabValue,
};
},
computed: { computed: {
showFailedJobsTab() { showFailedJobsTab() {
return this.failedJobsCount > 0; return this.failedJobsCount > 0;
}, },
}, },
watch: {
$route(to) {
this.activeTab = to.name;
},
},
methods: { methods: {
isActive(tabName) { isActive(tabName) {
return tabName === this.defaultTabValue; return tabName === this.activeTab;
},
navigateTo(tabName) {
this.$router.push({ name: tabName });
}, },
}, },
}; };
@ -59,10 +69,12 @@ export default {
<gl-tab <gl-tab
ref="pipelineTab" ref="pipelineTab"
:title="$options.i18n.tabs.pipelineTitle" :title="$options.i18n.tabs.pipelineTitle"
:active="isActive($options.tabNames.pipeline)"
data-testid="pipeline-tab" data-testid="pipeline-tab"
lazy lazy
@click="navigateTo($options.tabNames.pipeline)"
> >
<pipeline-graph-wrapper /> <router-view />
</gl-tab> </gl-tab>
<gl-tab <gl-tab
ref="dagTab" ref="dagTab"
@ -70,15 +82,21 @@ export default {
:active="isActive($options.tabNames.needs)" :active="isActive($options.tabNames.needs)"
data-testid="dag-tab" data-testid="dag-tab"
lazy lazy
@click="navigateTo($options.tabNames.needs)"
> >
<dag /> <router-view />
</gl-tab> </gl-tab>
<gl-tab :active="isActive($options.tabNames.jobs)" data-testid="jobs-tab" lazy> <gl-tab
:active="isActive($options.tabNames.jobs)"
data-testid="jobs-tab"
lazy
@click="navigateTo($options.tabNames.jobs)"
>
<template #title> <template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.jobsTitle }}</span> <span class="gl-mr-2">{{ $options.i18n.tabs.jobsTitle }}</span>
<gl-badge size="sm" data-testid="builds-counter">{{ totalJobCount }}</gl-badge> <gl-badge size="sm" data-testid="builds-counter">{{ totalJobCount }}</gl-badge>
</template> </template>
<jobs-app /> <router-view />
</gl-tab> </gl-tab>
<gl-tab <gl-tab
v-if="showFailedJobsTab" v-if="showFailedJobsTab"
@ -86,19 +104,25 @@ export default {
:active="isActive($options.tabNames.failures)" :active="isActive($options.tabNames.failures)"
data-testid="failed-jobs-tab" data-testid="failed-jobs-tab"
lazy lazy
@click="navigateTo($options.tabNames.failures)"
> >
<template #title> <template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.failedJobsTitle }}</span> <span class="gl-mr-2">{{ $options.i18n.tabs.failedJobsTitle }}</span>
<gl-badge size="sm" data-testid="failed-builds-counter">{{ failedJobsCount }}</gl-badge> <gl-badge size="sm" data-testid="failed-builds-counter">{{ failedJobsCount }}</gl-badge>
</template> </template>
<failed-jobs-app :failed-jobs-summary="failedJobsSummary" /> <router-view :failed-jobs-summary="failedJobsSummary" />
</gl-tab> </gl-tab>
<gl-tab :active="isActive($options.tabNames.tests)" data-testid="tests-tab" lazy> <gl-tab
:active="isActive($options.tabNames.tests)"
data-testid="tests-tab"
lazy
@click="navigateTo($options.tabNames.tests)"
>
<template #title> <template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.testsTitle }}</span> <span class="gl-mr-2">{{ $options.i18n.tabs.testsTitle }}</span>
<gl-badge size="sm" data-testid="tests-counter">{{ testsCount }}</gl-badge> <gl-badge size="sm" data-testid="tests-counter">{{ testsCount }}</gl-badge>
</template> </template>
<test-reports /> <router-view />
</gl-tab> </gl-tab>
<slot></slot> <slot></slot>
</gl-tabs> </gl-tabs>

View File

@ -47,8 +47,7 @@ export const CHILD_VIEW = 'child';
// Pipeline tabs // Pipeline tabs
export const TAB_QUERY_PARAM = 'tab'; export const pipelineTabName = 'graph';
export const needsTabName = 'dag'; export const needsTabName = 'dag';
export const jobsTabName = 'builds'; export const jobsTabName = 'builds';
export const failedJobsTabName = 'failures'; export const failedJobsTabName = 'failures';

View File

@ -1,3 +1,4 @@
import VueRouter from 'vue-router';
import { createAlert } from '~/flash'; import { createAlert } from '~/flash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag'; import createDagApp from './pipeline_details_dag';
@ -32,9 +33,16 @@ export default async function initPipelineDetailsBundle() {
if (gon.features?.pipelineTabsVue) { if (gon.features?.pipelineTabsVue) {
const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs'); const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs'); const { createPipelineTabs } = await import('./pipeline_tabs');
const { routes } = await import('ee_else_ce/pipelines/routes');
const router = new VueRouter({
mode: 'history',
base: dataset.pipelinePath,
routes,
});
try { try {
const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider); const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider, router);
createPipelineTabs(appOptions); createPipelineTabs(appOptions);
} catch { } catch {
createAlert({ createAlert({

View File

@ -1,17 +1,17 @@
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue'; import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
import { removeParams, updateHistory } from '~/lib/utils/url_utility';
import { TAB_QUERY_PARAM } from '~/pipelines/constants';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import createTestReportsStore from './stores/test_reports'; import createTestReportsStore from './stores/test_reports';
import { getPipelineDefaultTab, reportToSentry } from './utils'; import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo); Vue.use(VueApollo);
Vue.use(VueRouter);
Vue.use(Vuex); Vue.use(Vuex);
export const createAppOptions = (selector, apolloProvider) => { export const createAppOptions = (selector, apolloProvider, router) => {
const el = document.querySelector(selector); const el = document.querySelector(selector);
if (!el) return null; if (!el) return null;
@ -40,6 +40,7 @@ export const createAppOptions = (selector, apolloProvider) => {
suiteEndpoint, suiteEndpoint,
blobPath, blobPath,
hasTestReport, hasTestReport,
emptyDagSvgPath,
emptyStateImagePath, emptyStateImagePath,
artifactsExpiredImagePath, artifactsExpiredImagePath,
isFullCodequalityReportAvailable, isFullCodequalityReportAvailable,
@ -65,6 +66,7 @@ export const createAppOptions = (selector, apolloProvider) => {
}), }),
}, },
}), }),
router,
provide: { provide: {
canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports), canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports),
codequalityReportDownloadPath, codequalityReportDownloadPath,
@ -91,6 +93,7 @@ export const createAppOptions = (selector, apolloProvider) => {
suiteEndpoint, suiteEndpoint,
blobPath, blobPath,
hasTestReport, hasTestReport,
emptyDagSvgPath,
emptyStateImagePath, emptyStateImagePath,
artifactsExpiredImagePath, artifactsExpiredImagePath,
testsCount, testsCount,
@ -107,12 +110,6 @@ export const createAppOptions = (selector, apolloProvider) => {
export const createPipelineTabs = (options) => { export const createPipelineTabs = (options) => {
if (!options) return; if (!options) return;
updateHistory({
url: removeParams([TAB_QUERY_PARAM]),
title: document.title,
replace: true,
});
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue(options); new Vue(options);
}; };

View File

@ -0,0 +1,20 @@
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import Dag from './components/dag/dag.vue';
import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
import JobsApp from './components/jobs/jobs_app.vue';
import TestReports from './components/test_reports/test_reports.vue';
import {
pipelineTabName,
needsTabName,
jobsTabName,
failedJobsTabName,
testReportTabName,
} from './constants';
export const routes = [
{ name: pipelineTabName, path: '/', component: PipelineGraphWrapper },
{ name: needsTabName, path: '/dag', component: Dag },
{ name: jobsTabName, path: '/builds', component: JobsApp },
{ name: failedJobsTabName, path: '/failures', component: FailedJobsApp },
{ name: testReportTabName, path: '/test_report', component: TestReports },
];

View File

@ -1,12 +1,7 @@
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import { getParameterValues } from '~/lib/utils/url_utility'; import { parseUrlPathname } from '~/lib/utils/url_utility';
import { import { NEEDS_PROPERTY, SUPPORTED_FILTER_PARAMETERS, validPipelineTabNames } from './constants';
NEEDS_PROPERTY,
SUPPORTED_FILTER_PARAMETERS,
TAB_QUERY_PARAM,
validPipelineTabNames,
} from './constants';
/* /*
The following functions are the main engine in transforming the data as The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects. received from the endpoint into the format the d3 graph expects.
@ -145,10 +140,12 @@ export const reportMessageToSentry = (component, message, context) => {
}; };
export const getPipelineDefaultTab = (url) => { export const getPipelineDefaultTab = (url) => {
const [tabQueryValue] = getParameterValues(TAB_QUERY_PARAM, url); const strippedUrl = parseUrlPathname(url);
const regexp = /\w*$/;
const [tabName] = strippedUrl.match(regexp);
if (tabQueryValue && validPipelineTabNames.includes(tabQueryValue)) { if (tabName && validPipelineTabNames.includes(tabName)) {
return tabQueryValue; return tabName;
} }
return null; return null;

View File

@ -0,0 +1,75 @@
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale';
import { IssuableAttributeType } from '../../constants';
import SidebarDropdown from '../sidebar_dropdown.vue';
const noMilestone = {
id: 0,
title: __('No milestone'),
};
const placeholderMilestone = {
id: -1,
title: __('Select milestone'),
};
export default {
issuableAttribute: IssuableAttributeType.Milestone,
components: {
SidebarDropdown,
},
props: {
attrWorkspacePath: {
required: true,
type: String,
},
issuableType: {
type: String,
required: true,
validator(value) {
return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
},
},
workspaceType: {
type: String,
required: true,
validator(value) {
return [WorkspaceType.group, WorkspaceType.project].includes(value);
},
},
},
data() {
return {
milestone: placeholderMilestone,
};
},
computed: {
value() {
return this.milestone.id === placeholderMilestone.id
? undefined
: getIdFromGraphQLId(this.milestone.id);
},
},
methods: {
handleChange(milestone) {
this.milestone = milestone.id === null ? noMilestone : milestone;
},
},
};
</script>
<template>
<div>
<input type="hidden" name="update[milestone_id]" :value="value" />
<sidebar-dropdown
:attr-workspace-path="attrWorkspacePath"
:current-attribute="milestone"
:issuable-attribute="$options.issuableAttribute"
:issuable-type="issuableType"
:workspace-type="workspaceType"
@change="handleChange"
/>
</div>
</template>

View File

@ -8,7 +8,7 @@ import {
GlSearchBoxByType, GlSearchBoxByType,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash'; import { kebabCase, snakeCase } from 'lodash';
import { IssuableType } from '~/issues/constants'; import { IssuableType, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { import {
defaultEpicSort, defaultEpicSort,
@ -73,6 +73,14 @@ export default {
return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
}, },
}, },
workspaceType: {
type: String,
required: false,
default: WorkspaceType.project,
validator(value) {
return [WorkspaceType.group, WorkspaceType.project].includes(value);
},
},
}, },
data() { data() {
return { return {
@ -86,7 +94,7 @@ export default {
query() { query() {
const { list } = this.issuableAttributeQuery; const { list } = this.issuableAttributeQuery;
const { query } = list[this.issuableType]; const { query } = list[this.issuableType];
return query; return query[this.workspaceType] || query;
}, },
variables() { variables() {
if (!this.isEpic) { if (!this.isEpic) {

View File

@ -53,6 +53,7 @@ import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/querie
import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql'; import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql';
import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.mutation.graphql'; import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.mutation.graphql';
import groupMilestonesQuery from './queries/group_milestones.query.graphql';
import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql'; import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql'; import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
import projectMilestonesQuery from './queries/project_milestones.query.graphql'; import projectMilestonesQuery from './queries/project_milestones.query.graphql';
@ -241,10 +242,16 @@ export const issuableMilestoneQueries = {
export const milestonesQueries = { export const milestonesQueries = {
[IssuableType.Issue]: { [IssuableType.Issue]: {
query: projectMilestonesQuery, query: {
[WorkspaceType.group]: groupMilestonesQuery,
[WorkspaceType.project]: projectMilestonesQuery,
},
}, },
[IssuableType.MergeRequest]: { [IssuableType.MergeRequest]: {
query: projectMilestonesQuery, query: {
[WorkspaceType.group]: groupMilestonesQuery,
[WorkspaceType.project]: projectMilestonesQuery,
},
}, },
}; };

View File

@ -18,6 +18,7 @@ import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assi
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import BulkUpdateMilestoneDropdown from '~/sidebar/components/milestone/bulk_update_milestone_dropdown.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue'; import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
@ -289,6 +290,31 @@ function mountMilestoneSelect() {
}); });
} }
export function mountMilestoneDropdown() {
const el = document.querySelector('.js-milestone-dropdown');
if (!el) {
return null;
}
Vue.use(VueApollo);
return new Vue({
el,
name: 'BulkUpdateMilestoneDropdownRoot',
apolloProvider,
render(createElement) {
return createElement(BulkUpdateMilestoneDropdown, {
props: {
attrWorkspacePath: el.dataset.fullPath,
issuableType: isInIssuePage() ? IssuableType.Issue : IssuableType.MergeRequest,
workspaceType: el.dataset.workspaceType,
},
});
},
});
}
export function mountSidebarLabels() { export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels'); const el = document.querySelector('.js-sidebar-labels');

View File

@ -7,6 +7,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:board_multi_select, group) push_frontend_feature_flag(:board_multi_select, group)
push_frontend_feature_flag(:apollo_boards, group)
push_frontend_feature_flag(:realtime_labels, group) push_frontend_feature_flag(:realtime_labels, group)
experiment(:prominent_create_board_btn, subject: current_user) do |e| experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.control {} e.control {}

View File

@ -7,6 +7,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available! before_action :check_issues_available!
before_action do before_action do
push_frontend_feature_flag(:board_multi_select, project) push_frontend_feature_flag(:board_multi_select, project)
push_frontend_feature_flag(:apollo_boards, project)
push_frontend_feature_flag(:realtime_labels, project&.group) push_frontend_feature_flag(:realtime_labels, project&.group)
experiment(:prominent_create_board_btn, subject: current_user) do |e| experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.control {} e.control {}

View File

@ -140,21 +140,13 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def builds def builds
if Feature.enabled?(:pipeline_tabs_vue, project) render_show
redirect_to pipeline_path(@pipeline, tab: 'builds')
else
render_show
end
end end
def dag def dag
respond_to do |format| respond_to do |format|
format.html do format.html do
if Feature.enabled?(:pipeline_tabs_vue, project) render_show
redirect_to pipeline_path(@pipeline, tab: 'dag')
else
render_show
end
end end
format.json do format.json do
render json: Ci::DagPipelineSerializer render json: Ci::DagPipelineSerializer
@ -165,9 +157,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def failures def failures
if Feature.enabled?(:pipeline_tabs_vue, project) if @pipeline.failed_builds.present?
redirect_to pipeline_path(@pipeline, tab: 'failures')
elsif @pipeline.failed_builds.present?
render_show render_show
else else
redirect_to pipeline_path(@pipeline) redirect_to pipeline_path(@pipeline)
@ -222,11 +212,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def test_report def test_report
respond_to do |format| respond_to do |format|
format.html do format.html do
if Feature.enabled?(:pipeline_tabs_vue, project) render_show
redirect_to pipeline_path(@pipeline, tab: 'test_report')
else
render_show
end
end end
format.json do format.json do
render json: TestReportSerializer render json: TestReportSerializer

View File

@ -19,6 +19,7 @@ module Projects
blob_path: project_blob_path(project, pipeline.sha), blob_path: project_blob_path(project, pipeline.sha),
has_test_report: pipeline.has_test_reports?, has_test_report: pipeline.has_test_reports?,
empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'), empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
empty_dag_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'),
artifacts_expired_image_path: image_path('illustrations/pipeline.svg'), artifacts_expired_image_path: image_path('illustrations/pipeline.svg'),
tests_count: pipeline.test_report_summary.total[:count] tests_count: pipeline.test_report_summary.total[:count]
} }

View File

@ -13,8 +13,7 @@ module AlertManagement
key: Settings.attr_encrypted_db_key_base_32, key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm' algorithm: 'aes-256-gcm'
default_value_for(:endpoint_identifier, allows_nil: false) { SecureRandom.hex(8) } attribute :endpoint_identifier, default: -> { SecureRandom.hex(8) }
default_value_for(:token) { generate_token }
validates :project, presence: true validates :project, presence: true
validates :active, inclusion: { in: [true, false] } validates :active, inclusion: { in: [true, false] }

View File

@ -639,14 +639,6 @@ class ApplicationSetting < ApplicationRecord
validates :inactive_projects_send_warning_email_after_months, validates :inactive_projects_send_warning_email_after_months,
numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months } numericality: { only_integer: true, greater_than: 0, less_than: :inactive_projects_delete_after_months }
validates :cube_api_base_url,
addressable_url: { allow_localhost: true, allow_local_network: false },
allow_blank: true
validates :product_analytics_enabled,
presence: true,
allow_blank: true
attr_encrypted :asset_proxy_secret_key, attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv, mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,

View File

@ -3,11 +3,13 @@
class ProjectImportEntity < ProjectEntity class ProjectImportEntity < ProjectEntity
include ImportHelper include ImportHelper
expose :import_source expose :import_source, documentation: { type: 'string', example: 'source/source-repo' }
expose :import_status expose :import_status, documentation: {
expose :human_import_status_name type: 'string', example: 'scheduled', values: %w[scheduled started finished failed canceled]
}
expose :human_import_status_name, documentation: { type: 'string', example: 'canceled' }
expose :provider_link do |project, options| expose :provider_link, documentation: { type: 'string', example: '/source/source-repo' } do |project, options|
provider_project_link_url(options[:provider_url], project[:import_source]) provider_project_link_url(options[:provider_url], project[:import_source])
end end
end end

View File

@ -119,6 +119,7 @@
= render_if_exists 'admin/application_settings/feishu_integration' = render_if_exists 'admin/application_settings/feishu_integration'
= render 'admin/application_settings/third_party_offers' = render 'admin/application_settings/third_party_offers'
= render 'admin/application_settings/snowplow' = render 'admin/application_settings/snowplow'
= render_if_exists 'admin/application_settings/product_analytics' if Feature.enabled?(:cube_api_proxy)
= render 'admin/application_settings/error_tracking' if Feature.enabled?(:gitlab_error_tracking) = render 'admin/application_settings/error_tracking' if Feature.enabled?(:gitlab_error_tracking)
= render 'admin/application_settings/eks' = render 'admin/application_settings/eks'
= render 'admin/application_settings/floc' = render 'admin/application_settings/floc'

View File

@ -24,7 +24,7 @@
= c.body do = c.body do
- enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics') - enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')
- enable_service_ping_link = '<a href="%{url}">'.html_safe % { url: enable_service_ping_link_url } - enable_service_ping_link = '<a href="%{url}">'.html_safe % { url: enable_service_ping_link_url }
- generate_manually_link_url = help_page_path('administration/troubleshooting/gitlab_rails_cheat_sheet', anchor: 'generate-service-ping') - generate_manually_link_url = help_page_path('development/service_ping/troubleshooting', anchor: 'generate-service-ping')
- generate_manually_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_manually_link_url } - generate_manually_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_manually_link_url }
= html_escape(s_('%{enable_service_ping_link_start}Enable%{link_end} or %{generate_manually_link_start}generate%{link_end} Service Ping to preview and download service usage data payload.')) % { enable_service_ping_link_start: enable_service_ping_link, generate_manually_link_start: generate_manually_link, link_end: '</a>'.html_safe } = html_escape(s_('%{enable_service_ping_link_start}Enable%{link_end} or %{generate_manually_link_start}generate%{link_end} Service Ping to preview and download service usage data payload.')) % { enable_service_ping_link_start: enable_service_ping_link, generate_manually_link_start: generate_manually_link, link_end: '</a>'.html_safe }

View File

@ -7,7 +7,7 @@
= gl_tab_link_to s_('Branches|Overview'), project_branches_path(@project), { item_active: @mode == 'overview', title: s_('Branches|Show overview of the branches') } = gl_tab_link_to s_('Branches|Overview'), project_branches_path(@project), { item_active: @mode == 'overview', title: s_('Branches|Show overview of the branches') }
= gl_tab_link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), { title: s_('Branches|Show active branches') } = gl_tab_link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), { title: s_('Branches|Show active branches') }
= gl_tab_link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), { title: s_('Branches|Show stale branches') } = gl_tab_link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), { title: s_('Branches|Show stale branches') }
= gl_tab_link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), { item_active: !%w[overview active stale].include?(@mode), title: s_('Branches|Show all branches') } = gl_tab_link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), { item_active: %w[overview active stale].exclude?(@mode), title: s_('Branches|Show all branches') }
.nav-controls .nav-controls
#js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } } #js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } }

View File

@ -30,4 +30,4 @@
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline, @current_user) } #js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline, @current_user) }
- else - else
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } } .js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline), pipeline_path: pipeline_path(@pipeline) } }

View File

@ -34,7 +34,7 @@
.title .title
= _('Milestone') = _('Milestone')
.filter-item .filter-item
= dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, use_id: true, default_label: _("Milestone") } }) .js-milestone-dropdown{ data: { full_path: @project.full_path, workspace_type: Namespaces::ProjectNamespace.sti_name.downcase } }
- if is_issue - if is_issue
= render_if_exists 'shared/issuable/iterations_dropdown', parent: @project.group = render_if_exists 'shared/issuable/iterations_dropdown', parent: @project.group
- if is_issue - if is_issue

View File

@ -0,0 +1,8 @@
---
name: apollo_boards
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102719
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381210
milestone: '15.6'
type: development
group: group::product planning
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: enable_new_sentry_clientside_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102650
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344832
milestone: '15.6'
type: development
group: group::runner
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: enable_old_sentry_clientside_integration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102650
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344832
milestone: '15.6'
type: development
group: group::runner
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: search_index_curation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98809
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/375274
milestone: '15.6'
type: development
group: group::global search
default_enabled: false

View File

@ -45,6 +45,8 @@ metadata:
description: Operations related to project hooks description: Operations related to project hooks
- name: project_import_bitbucket - name: project_import_bitbucket
description: Operations related to import BitBucket projects description: Operations related to import BitBucket projects
- name: project_import_github
description: Operations related to import GitHub projects
- name: release_links - name: release_links
description: Operations related to release assets (links) description: Operations related to release assets (links)
- name: releases - name: releases

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveUsersForeignKeyToProjects < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key_if_exists :projects, column: :creator_id
end
end
def down
add_concurrent_foreign_key :projects, :users, column: :creator_id, on_delete: :nullify, validate: false
end
end

View File

@ -0,0 +1 @@
7ddb85c1acfd3fbeddbe96857d329ad09cd21210e6765ff36d4b9f516a7c10be

View File

@ -32626,9 +32626,6 @@ ALTER TABLE ONLY service_desk_settings
ALTER TABLE ONLY design_management_designs_versions ALTER TABLE ONLY design_management_designs_versions
ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE; ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE;
ALTER TABLE ONLY projects
ADD CONSTRAINT fk_03ec10b0d3 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL NOT VALID;
ALTER TABLE ONLY issues ALTER TABLE ONLY issues
ADD CONSTRAINT fk_05f1e72feb FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; ADD CONSTRAINT fk_05f1e72feb FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -232,12 +232,9 @@ who are aware of the risks.
- [Troubleshooting Kubernetes](https://docs.gitlab.com/charts/troubleshooting/kubernetes_cheat_sheet.html) - [Troubleshooting Kubernetes](https://docs.gitlab.com/charts/troubleshooting/kubernetes_cheat_sheet.html)
- [Troubleshooting PostgreSQL](troubleshooting/postgresql.md) - [Troubleshooting PostgreSQL](troubleshooting/postgresql.md)
- [Guide to test environments](troubleshooting/test_environments.md) (for Support Engineers) - [Guide to test environments](troubleshooting/test_environments.md) (for Support Engineers)
- [GitLab Rails console commands](troubleshooting/gitlab_rails_cheat_sheet.md) (for Support Engineers)
- [Troubleshooting SSL](troubleshooting/ssl.md) - [Troubleshooting SSL](troubleshooting/ssl.md)
- Related links: - Related links:
- [GitLab Developer Documentation](../development/index.md) - [GitLab Developer Documentation](../development/index.md)
- [Repairing and recovering broken Git repositories](https://git.seveas.net/repairing-and-recovering-broken-git-repositories.html) - [Repairing and recovering broken Git repositories](https://git.seveas.net/repairing-and-recovering-broken-git-repositories.html)
- [Testing with OpenSSL](https://www.feistyduck.com/library/openssl-cookbook/online/testing-with-openssl/index.html) - [Testing with OpenSSL](https://www.feistyduck.com/library/openssl-cookbook/online/testing-with-openssl/index.html)
- [`strace` zine](https://wizardzines.com/zines/strace/) - [`strace` zine](https://wizardzines.com/zines/strace/)
- GitLab.com-specific resources:
- [Example group SAML and SCIM configurations](../user/group/saml_sso/example_saml_config.md)

View File

@ -1,91 +1,11 @@
--- ---
stage: Systems redirect_to: 'index.md'
group: Distribution remove_date: '2023-02-01'
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
--- ---
# GitLab Rails Console Cheat Sheet **(FREE SELF)** This document was moved to [another location](index.md).
This is the GitLab Support Team's collection of information regarding the GitLab Rails <!-- This redirect file can be deleted after 2023-02-01. -->
console, for use while troubleshooting. It is listed here for transparency, <!-- Redirects that point to other docs in the same project expire in three months. -->
and for users with experience with these tools. If you are currently <!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
having an issue with GitLab, it is highly recommended that you first check <!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
our guide on [our Rails console](../operations/rails_console.md),
and your [support options](https://about.gitlab.com/support/), before attempting to use
this information.
WARNING:
Some of these scripts could be damaging if not run correctly,
or under the right conditions. We highly recommend running them under the
guidance of a Support Engineer, or running them in a test environment with a
backup of the instance ready to be restored, just in case.
WARNING:
As GitLab changes, changes to the code are inevitable,
and so some scripts may not work as they once used to. These are not kept
up-to-date as these scripts/commands were added as they were found/needed. As
mentioned above, we recommend running these scripts under the supervision of a
Support Engineer, who can also verify that they continue to work as they
should and, if needed, update the script for the latest version of GitLab.
## Mirrors
### Find mirrors with "bad decrypt" errors
This content has been converted to a Rake task, see [verify database values can be decrypted using the current secrets](../raketasks/check.md#verify-database-values-can-be-decrypted-using-the-current-secrets).
### Transfer mirror users and tokens to a single service account
This content has been moved to [Troubleshooting Repository mirroring](../../user/project/repository/mirror/index.md#transfer-mirror-users-and-tokens-to-a-single-service-account-in-rails-console).
## Merge requests
## CI
This content has been moved to [Troubleshooting CI/CD](../../ci/troubleshooting.md).
## License
This content has been moved to [Activate GitLab EE with a license file or key](../../user/admin_area/license_file.md).
## Registry
### Registry Disk Space Usage by Project
Find this content in the [Container Registry troubleshooting documentation](../packages/container_registry.md#registry-disk-space-usage-by-project).
### Run the Cleanup policy now
Find this content in the [Container Registry troubleshooting documentation](../packages/container_registry.md#run-the-cleanup-policy-now).
## Sidekiq
This content has been moved to [Troubleshooting Sidekiq](../sidekiq/sidekiq_troubleshooting.md).
## Geo
### Reverify all uploads (or any SSF data type which is verified)
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#reverify-all-uploads-or-any-ssf-data-type-which-is-verified).
### Artifacts
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#find-failed-artifacts).
### Repository verification failures
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#find-repository-verification-failures).
### Resync repositories
Moved to [Geo replication troubleshooting - Resync repository types except for project or project wiki repositories](../geo/replication/troubleshooting.md#repository-types-except-for-project-or-project-wiki-repositories).
Moved to [Geo replication troubleshooting - Resync project and project wiki repositories](../geo/replication/troubleshooting.md#resync-project-and-project-wiki-repositories).
### Blob types
Moved to [Geo replication troubleshooting](../geo/replication/troubleshooting.md#blob-types).
## Generate Service Ping
This content has been moved to [Service Ping Troubleshooting](../../development/service_ping/troubleshooting.md).

View File

@ -9,19 +9,21 @@ info: To determine the technical writer assigned to the Stage/Group associated w
This page documents a collection of resources to help you troubleshoot a GitLab This page documents a collection of resources to help you troubleshoot a GitLab
installation. installation.
This list is not necessarily comprehensive. If you don't find what you're looking
for in this list, you should search the documentation.
## Troubleshooting guides ## Troubleshooting guides
- [SSL](ssl.md) - [SSL](ssl.md)
- [Geo](../geo/replication/troubleshooting.md) - [Geo](../geo/replication/troubleshooting.md)
- [GitLab Rails console cheat sheet](gitlab_rails_cheat_sheet.md) - [SAML](../../user/group/saml_sso/troubleshooting.md)
- [Example group SAML and SCIM configurations](../../user/group/saml_sso/example_saml_config.md)
- [Kubernetes cheat sheet](https://docs.gitlab.com/charts/troubleshooting/kubernetes_cheat_sheet.html) - [Kubernetes cheat sheet](https://docs.gitlab.com/charts/troubleshooting/kubernetes_cheat_sheet.html)
- [Linux cheat sheet](linux_cheat_sheet.md) - [Linux cheat sheet](linux_cheat_sheet.md)
- [Parsing GitLab logs with `jq`](../logs/log_parsing.md) - [Parsing GitLab logs with `jq`](../logs/log_parsing.md)
- [Diagnostics tools](diagnostics_tools.md) - [Diagnostics tools](diagnostics_tools.md)
Some feature documentation pages also have a troubleshooting section at the end Some feature documentation pages also have a troubleshooting section at the end
that you can check for feature-specific help. that you can check for feature-specific help, including helpful Rails commands.
If you need a testing environment to troubleshoot, see the If you need a testing environment to troubleshoot, see the
[apps for a testing environment](test_environments.md). [apps for a testing environment](test_environments.md).

View File

@ -190,6 +190,30 @@ If you add a member to a group by using the [share a group with another group](.
- Remove the member from the shared group. You must be a group owner to do this. - Remove the member from the shared group. You must be a group owner to do this.
- From the group's membership page, remove access from the entire shared group. - From the group's membership page, remove access from the entire shared group.
## Seat usage alerts
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348481) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `seat_flag_alerts`.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/362041) in GitLab 15.4. Feature flag `seat_flag_alerts` removed.
If you have the Owner role of the top-level group, an alert notifies you
of your total seat usage.
The alert displays on group, subgroup, and project
pages, and only for top-level groups linked to subscriptions enrolled
in [quarterly subscription reconciliations](../quarterly_reconciliation.md).
After you dismiss the alert, it doesn't display until another seat is used.
The alert displays based on the following seat usage. You cannot configure the
amounts at which the alert displays.
| Seats in subscription | Seat usage |
|-----------------------|------------------------------------------------------------------------|
| 0-15 | One seat remaining in the subscription. |
| 16-25 | Two seats remaining in the subscription. |
| 26-99 | 10% of seats have been used. |
| 100-999 | 8% of seats have been used. |
| 1000+ | 5% of seats have been used |
## Upgrade your GitLab SaaS subscription tier ## Upgrade your GitLab SaaS subscription tier
To upgrade your [GitLab tier](https://about.gitlab.com/pricing/): To upgrade your [GitLab tier](https://about.gitlab.com/pricing/):

View File

@ -187,6 +187,7 @@ module API
mount ::API::FreezePeriods mount ::API::FreezePeriods
mount ::API::Keys mount ::API::Keys
mount ::API::ImportBitbucketServer mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Metadata mount ::API::Metadata
mount ::API::MergeRequestDiffs mount ::API::MergeRequestDiffs
mount ::API::ProjectHooks mount ::API::ProjectHooks
@ -261,7 +262,6 @@ module API
mount ::API::GroupVariables mount ::API::GroupVariables
mount ::API::Groups mount ::API::Groups
mount ::API::HelmPackages mount ::API::HelmPackages
mount ::API::ImportGithub
mount ::API::Integrations mount ::API::Integrations
mount ::API::Integrations::JiraConnect::Subscriptions mount ::API::Integrations::JiraConnect::Subscriptions
mount ::API::Invitations mount ::API::Invitations

View File

@ -6,15 +6,18 @@ module API
include ::API::ProjectsRelationBuilder include ::API::ProjectsRelationBuilder
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
expose :default_branch_or_main, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :default_branch_or_main, documentation: { type: 'string', example: 'main' }, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
# Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
expose :topic_names, as: :tag_list expose :topic_names, as: :tag_list, documentation: { type: 'string', is_array: true, example: 'tag' }
expose :topic_names, as: :topics expose :topic_names, as: :topics, documentation: { type: 'string', is_array: true, example: 'topic' }
expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url expose :ssh_url_to_repo, documentation: { type: 'string', example: 'git@gitlab.example.com:gitlab/gitlab.git' }
expose :http_url_to_repo, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab.git' }
expose :web_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab' }
expose :readme_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab/blob/master/README.md' }
expose :license_url, if: :license do |project| expose :license_url, if: :license, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab/blob/master/LICENCE' } do |project|
license = project.repository.license_blob license = project.repository.license_blob
if license if license
@ -26,13 +29,13 @@ module API
project.repository.license project.repository.license
end end
expose :avatar_url do |project, options| expose :avatar_url, documentation: { type: 'string', example: 'http://example.com/uploads/project/avatar/3/uploads/avatar.png' } do |project, options|
project.avatar_url(only_path: false) project.avatar_url(only_path: false)
end end
expose :forks_count expose :forks_count, documentation: { type: 'integer', example: 1 }
expose :star_count expose :star_count, documentation: { type: 'integer', example: 1 }
expose :last_activity_at expose :last_activity_at, documentation: { type: 'dateTime', example: '2013-09-30T13:46:02Z' }
expose :namespace, using: 'API::Entities::NamespaceBasic' expose :namespace, using: 'API::Entities::NamespaceBasic'
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes

View File

@ -3,11 +3,11 @@
module API module API
module Entities module Entities
class BasicRepositoryStorageMove < Grape::Entity class BasicRepositoryStorageMove < Grape::Entity
expose :id expose :id, documentation: { type: 'integer', example: 1 }
expose :created_at expose :created_at, documentation: { type: 'dateTime', example: '2020-05-07T04:27:17.234Z' }
expose :human_state_name, as: :state expose :human_state_name, as: :state, documentation: { type: 'string', example: 'scheduled' }
expose :source_storage_name expose :source_storage_name, documentation: { type: 'string', example: 'default' }
expose :destination_storage_name expose :destination_storage_name, documentation: { type: 'string', example: 'storage1' }
end end
end end
end end

View File

@ -5,148 +5,148 @@ module API
class Project < BasicProjectDetails class Project < BasicProjectDetails
include ::API::Helpers::RelatedResourcesHelpers include ::API::Helpers::RelatedResourcesHelpers
expose :container_registry_url, as: :container_registry_image_prefix, if: -> (_, _) { Gitlab.config.registry.enabled } expose :container_registry_url, as: :container_registry_image_prefix, documentation: { type: 'string', example: 'registry.gitlab.example.com/gitlab/gitlab-client' }, if: -> (_, _) { Gitlab.config.registry.enabled }
expose :_links do expose :_links do
expose :self do |project| expose :self, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4' } do |project|
expose_url(api_v4_projects_path(id: project.id)) expose_url(api_v4_projects_path(id: project.id))
end end
expose :issues, if: -> (project, options) { issues_available?(project, options) } do |project| expose :issues, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/issues' }, if: -> (project, options) { issues_available?(project, options) } do |project|
expose_url(api_v4_projects_issues_path(id: project.id)) expose_url(api_v4_projects_issues_path(id: project.id))
end end
expose :merge_requests, if: -> (project, options) { mrs_available?(project, options) } do |project| expose :merge_requests, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/merge_requests' }, if: -> (project, options) { mrs_available?(project, options) } do |project|
expose_url(api_v4_projects_merge_requests_path(id: project.id)) expose_url(api_v4_projects_merge_requests_path(id: project.id))
end end
expose :repo_branches do |project| expose :repo_branches, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/repository/branches' } do |project|
expose_url(api_v4_projects_repository_branches_path(id: project.id)) expose_url(api_v4_projects_repository_branches_path(id: project.id))
end end
expose :labels do |project| expose :labels, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/labels' } do |project|
expose_url(api_v4_projects_labels_path(id: project.id)) expose_url(api_v4_projects_labels_path(id: project.id))
end end
expose :events do |project| expose :events, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/events' } do |project|
expose_url(api_v4_projects_events_path(id: project.id)) expose_url(api_v4_projects_events_path(id: project.id))
end end
expose :members do |project| expose :members, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/members' } do |project|
expose_url(api_v4_projects_members_path(id: project.id)) expose_url(api_v4_projects_members_path(id: project.id))
end end
expose :cluster_agents do |project| expose :cluster_agents, documentation: { type: 'string', example: 'https://gitlab.example.com/api/v4/projects/4/cluster_agents' } do |project|
expose_url(api_v4_projects_cluster_agents_path(id: project.id)) expose_url(api_v4_projects_cluster_agents_path(id: project.id))
end end
end end
expose :packages_enabled expose :packages_enabled, documentation: { type: 'boolean' }
expose :empty_repo?, as: :empty_repo expose :empty_repo?, as: :empty_repo, documentation: { type: 'boolean' }
expose :archived?, as: :archived expose :archived?, as: :archived, documentation: { type: 'boolean' }
expose :visibility expose :visibility, documentation: { type: 'string', example: 'public' }
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :resolve_outdated_diff_discussions expose :resolve_outdated_diff_discussions, documentation: { type: 'boolean' }
expose :container_expiration_policy, expose :container_expiration_policy,
using: Entities::ContainerExpirationPolicy, using: Entities::ContainerExpirationPolicy,
if: -> (project, _) { project.container_expiration_policy } if: -> (project, _) { project.container_expiration_policy }
# Expose old field names with the new permissions methods to keep API compatible # Expose old field names with the new permissions methods to keep API compatible
# TODO: remove in API v5, replaced by *_access_level # TODO: remove in API v5, replaced by *_access_level
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } expose(:issues_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } expose(:merge_requests_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } expose(:wiki_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:jobs_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } expose(:snippets_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose(:container_registry_enabled) { |project, options| project.feature_available?(:container_registry, options[:current_user]) } expose(:container_registry_enabled, documentation: { type: 'boolean' }) { |project, options| project.feature_available?(:container_registry, options[:current_user]) }
expose :service_desk_enabled expose :service_desk_enabled, documentation: { type: 'boolean' }
expose :service_desk_address, if: -> (project, options) do expose :service_desk_address, documentation: { type: 'string', example: 'address@example.com' }, if: -> (project, options) do
Ability.allowed?(options[:current_user], :admin_issue, project) Ability.allowed?(options[:current_user], :admin_issue, project)
end end
expose(:can_create_merge_request_in) do |project, options| expose(:can_create_merge_request_in, documentation: { type: 'boolean' }) do |project, options|
Ability.allowed?(options[:current_user], :create_merge_request_in, project) Ability.allowed?(options[:current_user], :create_merge_request_in, project)
end end
expose(:issues_access_level) { |project, options| project_feature_string_access_level(project, :issues) } expose(:issues_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :issues) }
expose(:repository_access_level) { |project, options| project_feature_string_access_level(project, :repository) } expose(:repository_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :repository) }
expose(:merge_requests_access_level) { |project, options| project_feature_string_access_level(project, :merge_requests) } expose(:merge_requests_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :merge_requests) }
expose(:forking_access_level) { |project, options| project_feature_string_access_level(project, :forking) } expose(:forking_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :forking) }
expose(:wiki_access_level) { |project, options| project_feature_string_access_level(project, :wiki) } expose(:wiki_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :wiki) }
expose(:builds_access_level) { |project, options| project_feature_string_access_level(project, :builds) } expose(:builds_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :builds) }
expose(:snippets_access_level) { |project, options| project_feature_string_access_level(project, :snippets) } expose(:snippets_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :snippets) }
expose(:pages_access_level) { |project, options| project_feature_string_access_level(project, :pages) } expose(:pages_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :pages) }
expose(:operations_access_level) { |project, options| project_feature_string_access_level(project, :operations) } expose(:operations_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :operations) }
expose(:analytics_access_level) { |project, options| project_feature_string_access_level(project, :analytics) } expose(:analytics_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :analytics) }
expose(:container_registry_access_level) { |project, options| project_feature_string_access_level(project, :container_registry) } expose(:container_registry_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :container_registry) }
expose(:security_and_compliance_access_level) { |project, options| project_feature_string_access_level(project, :security_and_compliance) } expose(:security_and_compliance_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :security_and_compliance) }
expose(:releases_access_level) { |project, options| project_feature_string_access_level(project, :releases) } expose(:releases_access_level, documentation: { type: 'string', example: 'enabled' }) { |project, options| project_feature_string_access_level(project, :releases) }
expose :emails_disabled expose :emails_disabled, documentation: { type: 'boolean' }
expose :shared_runners_enabled expose :shared_runners_enabled, documentation: { type: 'boolean' }
expose :lfs_enabled?, as: :lfs_enabled expose :lfs_enabled?, as: :lfs_enabled, documentation: { type: 'boolean' }
expose :creator_id expose :creator_id, documentation: { type: 'integer', example: 1 }
expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do expose :forked_from_project, using: Entities::BasicProjectDetails, if: ->(project, options) do
project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project) project.forked? && Ability.allowed?(options[:current_user], :read_project, project.forked_from_project)
end end
expose :mr_default_target_self, if: -> (project) { project.forked? } expose :mr_default_target_self, if: -> (project) { project.forked? }, documentation: { type: 'boolean' }
expose :import_url, if: -> (project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } do |project| expose :import_url, documentation: { type: 'string', example: 'https://gitlab.com/gitlab/gitlab.git' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } do |project|
project[:import_url] project[:import_url]
end end
expose :import_type, if: -> (project, options) { Ability.allowed?(options[:current_user], :admin_project, project) } expose :import_type, documentation: { type: 'string', example: 'git' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :admin_project, project) }
expose :import_status expose :import_status, documentation: { type: 'string', example: 'none' }
expose :import_error, if: lambda { |_project, options| options[:user_can_admin_project] } do |project| expose :import_error, documentation: { type: 'string', example: 'Import error' }, if: lambda { |_project, options| options[:user_can_admin_project] } do |project|
project.import_state&.last_error project.import_state&.last_error
end end
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :open_issues_count, documentation: { type: 'integer', example: 1 }, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, documentation: { type: 'string', example: 'b8547b1dc37721d05889db52fa2f02' }, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :ci_default_git_depth expose :ci_default_git_depth, documentation: { type: 'integer', example: 20 }
expose :ci_forward_deployment_enabled expose :ci_forward_deployment_enabled, documentation: { type: 'boolean' }
expose(:ci_job_token_scope_enabled) { |p, _| p.ci_outbound_job_token_scope_enabled? } expose(:ci_job_token_scope_enabled, documentation: { type: 'boolean' }) { |p, _| p.ci_outbound_job_token_scope_enabled? }
expose :ci_separated_caches expose :ci_separated_caches, documentation: { type: 'boolean' }
expose :ci_opt_in_jwt expose :ci_opt_in_jwt, documentation: { type: 'boolean' }
expose :ci_allow_fork_pipelines_to_run_in_parent_project expose :ci_allow_fork_pipelines_to_run_in_parent_project, documentation: { type: 'boolean' }
expose :public_builds, as: :public_jobs expose :public_builds, as: :public_jobs, documentation: { type: 'boolean' }
expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| expose :build_git_strategy, documentation: { type: 'string', example: 'fetch' }, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options|
project.build_allow_git_fetch ? 'fetch' : 'clone' project.build_allow_git_fetch ? 'fetch' : 'clone'
end end
expose :build_timeout expose :build_timeout, documentation: { type: 'integer', example: 3600 }
expose :auto_cancel_pending_pipelines expose :auto_cancel_pending_pipelines, documentation: { type: 'string', example: 'enabled' }
expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :ci_config_path, documentation: { type: 'string', example: '' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
expose :shared_with_groups do |project, options| expose :shared_with_groups, documentation: { is_array: true } do |project, options|
user = options[:current_user] user = options[:current_user]
SharedGroupWithProject.represent(project.visible_group_links(for_user: user), options) SharedGroupWithProject.represent(project.visible_group_links(for_user: user), options)
end end
expose :only_allow_merge_if_pipeline_succeeds expose :only_allow_merge_if_pipeline_succeeds, documentation: { type: 'boolean' }
expose :allow_merge_on_skipped_pipeline expose :allow_merge_on_skipped_pipeline, documentation: { type: 'boolean' }
expose :restrict_user_defined_variables expose :restrict_user_defined_variables, documentation: { type: 'boolean' }
expose :request_access_enabled expose :request_access_enabled, documentation: { type: 'boolean' }
expose :only_allow_merge_if_all_discussions_are_resolved expose :only_allow_merge_if_all_discussions_are_resolved, documentation: { type: 'boolean' }
expose :remove_source_branch_after_merge expose :remove_source_branch_after_merge, documentation: { type: 'boolean' }
expose :printing_merge_request_link_enabled expose :printing_merge_request_link_enabled, documentation: { type: 'boolean' }
expose :merge_method expose :merge_method, documentation: { type: 'string', example: 'merge' }
expose :squash_option expose :squash_option, documentation: { type: 'string', example: 'default_off' }
expose :enforce_auth_checks_on_uploads expose :enforce_auth_checks_on_uploads, documentation: { type: 'boolean' }
expose :suggestion_commit_message expose :suggestion_commit_message, documentation: { type: 'string', example: 'Suggestion message' }
expose :merge_commit_template expose :merge_commit_template, documentation: { type: 'string', example: '%(title)' }
expose :squash_commit_template expose :squash_commit_template, documentation: { type: 'string', example: '%(source_branch)' }
expose :issue_branch_template expose :issue_branch_template, documentation: { type: 'string', example: '%(title)' }
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
} }
expose :auto_devops_enabled?, as: :auto_devops_enabled expose :auto_devops_enabled?, as: :auto_devops_enabled, documentation: { type: 'boolean' }
expose :auto_devops_deploy_strategy do |project, options| expose :auto_devops_deploy_strategy, documentation: { type: 'string', example: 'continuous' } do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end end
expose :autoclose_referenced_issues expose :autoclose_referenced_issues, documentation: { type: 'boolean' }
expose :repository_storage, if: ->(project, options) { expose :repository_storage, documentation: { type: 'string', example: 'default' }, if: ->(project, options) {
Ability.allowed?(options[:current_user], :change_repository_storage, project) Ability.allowed?(options[:current_user], :change_repository_storage, project)
} }
expose :keep_latest_artifacts_available?, as: :keep_latest_artifact expose :keep_latest_artifacts_available?, as: :keep_latest_artifact, documentation: { type: 'boolean' }
expose :runner_token_expiration_interval expose :runner_token_expiration_interval, documentation: { type: 'integer', example: 3600 }
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def self.preload_resource(project) def self.preload_resource(project)

View File

@ -3,7 +3,11 @@
module API module API
module Entities module Entities
class ProjectGroupLink < Grape::Entity class ProjectGroupLink < Grape::Entity
expose :id, :project_id, :group_id, :group_access, :expires_at expose :id, documentation: { type: 'integer', example: 1 }
expose :project_id, documentation: { type: 'integer', example: 1 }
expose :group_id, documentation: { type: 'integer', example: 1 }
expose :group_access, documentation: { type: 'integer', example: 10 }
expose :expires_at, documentation: { type: 'date', example: '2016-09-26' }
end end
end end
end end

View File

@ -3,10 +3,13 @@
module API module API
module Entities module Entities
class ProjectIdentity < Grape::Entity class ProjectIdentity < Grape::Entity
expose :id, :description expose :id, documentation: { type: 'integer', example: 1 }
expose :name, :name_with_namespace expose :description, documentation: { type: 'string', example: 'desc' }
expose :path, :path_with_namespace expose :name, documentation: { type: 'string', example: 'project1' }
expose :created_at expose :name_with_namespace, documentation: { type: 'string', example: 'John Doe / project1' }
expose :path, documentation: { type: 'string', example: 'project1' }
expose :path_with_namespace, documentation: { type: 'string', example: 'namespace1/project1' }
expose :created_at, documentation: { type: 'dateTime', example: '2020-05-07T04:27:17.016Z' }
end end
end end
end end

View File

@ -5,12 +5,16 @@ module API
class ProjectRepositoryStorage < Grape::Entity class ProjectRepositoryStorage < Grape::Entity
include Gitlab::Routing include Gitlab::Routing
expose :disk_path do |project| expose :disk_path, documentation: {
type: 'string',
example: '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b'
} do |project|
project.repository.disk_path project.repository.disk_path
end end
expose :id, as: :project_id expose :id, as: :project_id, documentation: { type: 'integer', example: 1 }
expose :repository_storage, :created_at expose :repository_storage, documentation: { type: 'string', example: 'default' }
expose :created_at, documentation: { type: 'dateTime', example: '2012-10-12T17:04:47Z' }
end end
end end
end end

View File

@ -37,7 +37,15 @@ module API
desc 'Import a GitHub project' do desc 'Import a GitHub project' do
detail 'This feature was introduced in GitLab 11.3.4.' detail 'This feature was introduced in GitLab 11.3.4.'
success ::ProjectEntity success code: 201, model: ::ProjectEntity
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 422, message: 'Unprocessable entity' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_import_github']
end end
params do params do
requires :personal_access_token, type: String, desc: 'GitHub personal access token' requires :personal_access_token, type: String, desc: 'GitHub personal access token'
@ -58,6 +66,18 @@ module API
end end
end end
desc 'Cancel GitHub project import' do
detail 'This feature was introduced in GitLab 15.5'
success code: 200, model: ProjectImportEntity
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 503, message: 'Service unavailable' }
]
tags ['project_import_github']
end
params do params do
requires :project_id, type: Integer, desc: 'ID of importing project to be canceled' requires :project_id, type: Integer, desc: 'ID of importing project to be canceled'
end end

View File

@ -147,6 +147,12 @@ module Gitlab
'in the body of your migration class' 'in the body of your migration class'
end end
if !options.delete(:allow_partition) && partition?(table_name)
raise ArgumentError, 'add_concurrent_index can not be used on a partitioned ' \
'table. Please use add_concurrent_partitioned_index on the partitioned table ' \
'as we need to create indexes on each partition and an index on the parent table'
end
options = options.merge({ algorithm: :concurrently }) options = options.merge({ algorithm: :concurrently })
if index_exists?(table_name, column_name, **options) if index_exists?(table_name, column_name, **options)
@ -1284,6 +1290,14 @@ into similar problems in the future (e.g. when new tables are created).
end end
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity # rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
def partition?(table_name)
if view_exists?(:postgres_partitions)
Gitlab::Database::PostgresPartition.partition_exists?(table_name)
else
Gitlab::Database::PostgresPartition.legacy_partition_exists?(table_name)
end
end
private private
def multiple_columns(columns, separator: ', ') def multiple_columns(columns, separator: ', ')

View File

@ -40,7 +40,7 @@ module Gitlab
partitioned_table.postgres_partitions.order(:name).each do |partition| partitioned_table.postgres_partitions.order(:name).each do |partition|
partition_index_name = generated_index_name(partition.identifier, options[:name]) partition_index_name = generated_index_name(partition.identifier, options[:name])
partition_options = options.merge(name: partition_index_name) partition_options = options.merge(name: partition_index_name, allow_partition: true)
add_concurrent_index(partition.identifier, column_names, partition_options) add_concurrent_index(partition.identifier, column_names, partition_options)
end end

View File

@ -19,6 +19,20 @@ module Gitlab
scope :for_parent_table, ->(name) { where("parent_identifier = concat(current_schema(), '.', ?)", name).order(:name) } scope :for_parent_table, ->(name) { where("parent_identifier = concat(current_schema(), '.', ?)", name).order(:name) }
def self.partition_exists?(table_name)
where("identifier = concat(current_schema(), '.', ?)", table_name).exists?
end
def self.legacy_partition_exists?(table_name)
result = connection.select_value(<<~SQL)
SELECT true FROM pg_class
WHERE relname = '#{table_name}'
AND relispartition = true;
SQL
!!result
end
def to_s def to_s
name name
end end

View File

@ -17,11 +17,18 @@ module Gitlab
gon.markdown_surround_selection = current_user&.markdown_surround_selection gon.markdown_surround_selection = current_user&.markdown_surround_selection
gon.markdown_automatic_lists = current_user&.markdown_automatic_lists gon.markdown_automatic_lists = current_user&.markdown_automatic_lists
if Gitlab.config.sentry.enabled # Support for Sentry setup via configuration will be removed in 16.0
# in favor of Gitlab::CurrentSettings.
if Feature.enabled?(:enable_old_sentry_clientside_integration) && Gitlab.config.sentry.enabled
gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
gon.sentry_environment = Gitlab.config.sentry.environment gon.sentry_environment = Gitlab.config.sentry.environment
end end
if Feature.enabled?(:enable_new_sentry_clientside_integration) && Gitlab::CurrentSettings.sentry_enabled
gon.sentry_dsn = Gitlab::CurrentSettings.sentry_clientside_dsn
gon.sentry_environment = Gitlab::CurrentSettings.sentry_environment
end
gon.recaptcha_api_server_url = ::Recaptcha.configuration.api_server_url gon.recaptcha_api_server_url = ::Recaptcha.configuration.api_server_url
gon.recaptcha_sitekey = Gitlab::CurrentSettings.recaptcha_site_key gon.recaptcha_sitekey = Gitlab::CurrentSettings.recaptcha_site_key
gon.gitlab_url = Gitlab.config.gitlab.url gon.gitlab_url = Gitlab.config.gitlab.url

View File

@ -2734,15 +2734,27 @@ msgstr ""
msgid "AdminSettings|CI/CD limits" msgid "AdminSettings|CI/CD limits"
msgstr "" msgstr ""
msgid "AdminSettings|Clickhouse URL"
msgstr ""
msgid "AdminSettings|Configure Let's Encrypt" msgid "AdminSettings|Configure Let's Encrypt"
msgstr "" msgstr ""
msgid "AdminSettings|Configure limits on the number of repositories users can download in a given time." msgid "AdminSettings|Configure limits on the number of repositories users can download in a given time."
msgstr "" msgstr ""
msgid "AdminSettings|Configure product analytics to track events within your project applications."
msgstr ""
msgid "AdminSettings|Configure when inactive projects should be automatically deleted. %{linkStart}What are inactive projects?%{linkEnd}" msgid "AdminSettings|Configure when inactive projects should be automatically deleted. %{linkStart}What are inactive projects?%{linkEnd}"
msgstr "" msgstr ""
msgid "AdminSettings|Cube API URL"
msgstr ""
msgid "AdminSettings|Cube API key"
msgstr ""
msgid "AdminSettings|Delete inactive projects" msgid "AdminSettings|Delete inactive projects"
msgstr "" msgstr ""
@ -2791,6 +2803,9 @@ msgstr ""
msgid "AdminSettings|Enable pipeline suggestion banner" msgid "AdminSettings|Enable pipeline suggestion banner"
msgstr "" msgstr ""
msgid "AdminSettings|Enable product analytics"
msgstr ""
msgid "AdminSettings|Enable shared runners for new projects" msgid "AdminSettings|Enable shared runners for new projects"
msgstr "" msgstr ""
@ -2836,6 +2851,18 @@ msgstr ""
msgid "AdminSettings|Instance runners expiration" msgid "AdminSettings|Instance runners expiration"
msgstr "" msgstr ""
msgid "AdminSettings|Jitsu administrator email"
msgstr ""
msgid "AdminSettings|Jitsu administrator password"
msgstr ""
msgid "AdminSettings|Jitsu host"
msgstr ""
msgid "AdminSettings|Jitsu project ID"
msgstr ""
msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines" msgid "AdminSettings|Keep the latest artifacts for all jobs in the latest successful pipelines"
msgstr "" msgstr ""
@ -2974,9 +3001,18 @@ msgstr ""
msgid "AdminSettings|Size and domain settings for Pages static sites." msgid "AdminSettings|Size and domain settings for Pages static sites."
msgstr "" msgstr ""
msgid "AdminSettings|The ID of the project in Jitsu. The project contains all analytics instances."
msgstr ""
msgid "AdminSettings|The URL of your Cube instance."
msgstr ""
msgid "AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects." msgid "AdminSettings|The default domain to use for Auto Review Apps and Auto Deploy stages in all projects."
msgstr "" msgstr ""
msgid "AdminSettings|The host of your Jitsu instance."
msgstr ""
msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire." msgid "AdminSettings|The latest artifacts for all jobs in the most recent successful pipelines in each project are stored and do not expire."
msgstr "" msgstr ""
@ -3001,6 +3037,15 @@ msgstr ""
msgid "AdminSettings|Use AWS OpenSearch Service with IAM credentials" msgid "AdminSettings|Use AWS OpenSearch Service with IAM credentials"
msgstr "" msgstr ""
msgid "AdminSettings|Used to connect Jitsu to the Clickhouse instance."
msgstr ""
msgid "AdminSettings|Used to generate short-lived API access tokens."
msgstr ""
msgid "AdminSettings|Used to retrieve dashboard data from the Cube instance."
msgstr ""
msgid "AdminSettings|Users and groups must accept the invitation before they're added to a group or project." msgid "AdminSettings|Users and groups must accept the invitation before they're added to a group or project."
msgstr "" msgstr ""
@ -5332,9 +5377,6 @@ msgstr ""
msgid "Assign labels" msgid "Assign labels"
msgstr "" msgstr ""
msgid "Assign milestone"
msgstr ""
msgid "Assign myself" msgid "Assign myself"
msgstr "" msgstr ""
@ -30890,6 +30932,9 @@ msgstr ""
msgid "Proceed" msgid "Proceed"
msgstr "" msgstr ""
msgid "Product analytics"
msgstr ""
msgid "ProductAnalytics|Audience" msgid "ProductAnalytics|Audience"
msgstr "" msgstr ""
@ -36144,10 +36189,10 @@ msgstr ""
msgid "SecurityOrchestration|%{agent} for %{namespaces}" msgid "SecurityOrchestration|%{agent} for %{namespaces}"
msgstr "" msgstr ""
msgid "SecurityOrchestration|%{branches} %{plural}" msgid "SecurityOrchestration|%{branches} and %{lastBranch} branches"
msgstr "" msgstr ""
msgid "SecurityOrchestration|%{branches} and %{lastBranch} %{plural}" msgid "SecurityOrchestration|%{branches} branch"
msgstr "" msgstr ""
msgid "SecurityOrchestration|%{scanners}" msgid "SecurityOrchestration|%{scanners}"
@ -36504,10 +36549,10 @@ msgstr ""
msgid "SecurityOrchestration|the %{branches}" msgid "SecurityOrchestration|the %{branches}"
msgstr "" msgstr ""
msgid "SecurityOrchestration|the %{namespaces} %{plural}" msgid "SecurityOrchestration|the %{namespaces} and %{lastNamespace} namespaces"
msgstr "" msgstr ""
msgid "SecurityOrchestration|the %{namespaces} and %{lastNamespace} %{plural}" msgid "SecurityOrchestration|the %{namespaces} namespace"
msgstr "" msgstr ""
msgid "SecurityOrchestration|vulnerabilities" msgid "SecurityOrchestration|vulnerabilities"
@ -48842,11 +48887,6 @@ msgstr ""
msgid "my-topic" msgid "my-topic"
msgstr "" msgstr ""
msgid "namespace"
msgid_plural "namespaces"
msgstr[0] ""
msgstr[1] ""
msgid "needs to be between 10 minutes and 1 month" msgid "needs to be between 10 minutes and 1 month"
msgstr "" msgstr ""

View File

@ -98,12 +98,17 @@ module QA
let(:mrs) { fetch_mrs(imported_project, target_api_client) } let(:mrs) { fetch_mrs(imported_project, target_api_client) }
let(:issues) { fetch_issues(imported_project, target_api_client) } let(:issues) { fetch_issues(imported_project, target_api_client) }
let(:import_failures) { imported_group.import_details.sum([]) { |details| details[:failures] } }
before do before do
destination_group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) destination_group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end end
# rubocop:disable RSpec/InstanceVariable # rubocop:disable RSpec/InstanceVariable
after do |example| after do |example|
# Log failures for easier debugging
Runtime::Logger.error("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
next unless defined?(@import_time) next unless defined?(@import_time)
# save data for comparison notification creation # save data for comparison notification creation
@ -112,7 +117,7 @@ module QA
{ {
importer: :gitlab, importer: :gitlab,
import_time: @import_time, import_time: @import_time,
errors: imported_group.import_details.sum([]) { |details| details[:failures] }, errors: import_failures,
source: { source: {
name: "GitLab Source", name: "GitLab Source",
project_name: source_project.path_with_namespace, project_name: source_project.path_with_namespace,
@ -154,7 +159,7 @@ module QA
end end
# rubocop:enable RSpec/InstanceVariable # rubocop:enable RSpec/InstanceVariable
it "migrates large gitlab group via api", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/358842' do it "migrates large gitlab group via api", testcase: "https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/358842" do
start = Time.now start = Time.now
# trigger import and log imported group path # trigger import and log imported group path
@ -165,7 +170,11 @@ module QA
# wait for import to finish and save import time # wait for import to finish and save import time
logger.info("== Waiting for import to be finished ==") logger.info("== Waiting for import to be finished ==")
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) expect { imported_group.import_status }.not_to eventually_eq("started").within(import_wait_duration)
# finished status actually means success, don't wait for finished status explicitly
# because test would wait full duration if returned status is "failed"
expect(imported_group.import_status).to eq("finished")
@import_time = Time.now - start @import_time = Time.now - start
aggregate_failures do aggregate_failures do

View File

@ -20,23 +20,11 @@ RSpec.describe Projects::PipelinesController do
end end
shared_examples 'the show page' do |param| shared_examples 'the show page' do |param|
it 'redirects to pipeline path with param' do it 'renders the show template' do
get param, params: { namespace_id: project.namespace, project_id: project, id: pipeline } get param, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
expect(response).to redirect_to(pipeline_path(pipeline, tab: param)) expect(response).to have_gitlab_http_status(:ok)
end expect(response).to render_template :show
context 'when the FF pipeline_tabs_vue is disabled' do
before do
stub_feature_flags(pipeline_tabs_vue: false)
end
it 'renders the show template' do
get param, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template :show
end
end end
end end
@ -710,37 +698,25 @@ RSpec.describe Projects::PipelinesController do
describe 'GET failures' do describe 'GET failures' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
context 'with ff `pipeline_tabs_vue` disabled' do context 'with failed jobs' do
before do before do
stub_feature_flags(pipeline_tabs_vue: false) create(:ci_build, :failed, pipeline: pipeline, name: 'hello')
end end
context 'with failed jobs' do it 'shows the page' do
before do get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
create(:ci_build, :failed, pipeline: pipeline, name: 'hello')
end
it 'shows the page' do expect(response).to have_gitlab_http_status(:ok)
get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline } expect(response).to render_template :show
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template :show
end
end
context 'without failed jobs' do
it 'redirects to the main pipeline page' do
get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
expect(response).to redirect_to(pipeline_path(pipeline))
end
end end
end end
it 'redirects to the pipeline page with `failures` query param' do context 'without failed jobs' do
get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline } it 'redirects to the main pipeline page' do
get :failures, params: { namespace_id: project.namespace, project_id: project, id: pipeline }
expect(response).to redirect_to(pipeline_path(pipeline, tab: 'failures')) expect(response).to redirect_to(pipeline_path(pipeline))
end
end end
end end

View File

@ -80,7 +80,7 @@ RSpec.describe 'Database schema' do
project_error_tracking_settings: %w[sentry_project_id], project_error_tracking_settings: %w[sentry_project_id],
project_group_links: %w[group_id], project_group_links: %w[group_id],
project_statistics: %w[namespace_id], project_statistics: %w[namespace_id],
projects: %w[ci_id mirror_user_id], projects: %w[creator_id ci_id mirror_user_id],
redirect_routes: %w[source_id], redirect_routes: %w[source_id],
repository_languages: %w[programming_language_id], repository_languages: %w[programming_language_id],
routes: %w[source_id], routes: %w[source_id],

View File

@ -22,6 +22,7 @@ RSpec.describe 'Issue board filters', :js do
let(:filter_submit) { find('.gl-search-box-by-click-search-button') } let(:filter_submit) { find('.gl-search-box-by-click-search-button') }
before do before do
stub_feature_flags(apollo_boards: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View File

@ -19,6 +19,7 @@ RSpec.describe 'Project issue boards', :js do
context 'signed in user' do context 'signed in user' do
before do before do
stub_feature_flags(apollo_boards: false)
project.add_maintainer(user) project.add_maintainer(user)
project.add_maintainer(user2) project.add_maintainer(user2)
@ -29,7 +30,7 @@ RSpec.describe 'Project issue boards', :js do
context 'no lists' do context 'no lists' do
before do before do
visit_project_board_path_without_query_limit(project, board) visit_project_board(project, board)
end end
it 'creates default lists' do it 'creates default lists' do
@ -73,7 +74,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) }
before do before do
visit_project_board_path_without_query_limit(project, board) visit_project_board(project, board)
end end
it 'shows description tooltip on list title', :quarantine do it 'shows description tooltip on list title', :quarantine do
@ -124,7 +125,7 @@ RSpec.describe 'Project issue boards', :js do
it 'infinite scrolls list' do it 'infinite scrolls list' do
create_list(:labeled_issue, 30, project: project, labels: [planning]) create_list(:labeled_issue, 30, project: project, labels: [planning])
visit_project_board_path_without_query_limit(project, board) visit_project_board(project, board)
page.within(find('.board:nth-child(2)')) do page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('38') expect(page.find('.board-header')).to have_content('38')
@ -203,7 +204,7 @@ RSpec.describe 'Project issue boards', :js do
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
# Make sure list positions are preserved after a reload # Make sure list positions are preserved after a reload
visit_project_board_path_without_query_limit(project, board) visit_project_board(project, board)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title)
@ -215,15 +216,19 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
it 'changes position of list' do it 'changes position of list' do
visit_project_board_path_without_query_limit(project, board) inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
visit_project_board(project, board)
end
drag(list_from_index: 0, list_to_index: 1, selector: '.board-header') drag(list_from_index: 0, list_to_index: 1, selector: '.board-header')
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
# Make sure list positions are preserved after a reload inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
visit_project_board_path_without_query_limit(project, board) # Make sure list positions are preserved after a reload
visit_project_board(project, board)
end
expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title)
expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title)
@ -234,7 +239,9 @@ RSpec.describe 'Project issue boards', :js do
selector = '.board:not(.is-ghost) .board-header' selector = '.board:not(.is-ghost) .board-header'
expect(page).to have_selector(selector, text: development.title, count: 1) expect(page).to have_selector(selector, text: development.title, count: 1)
drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false) inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false)
end
expect(page).to have_selector(selector, text: development.title, count: 1) expect(page).to have_selector(selector, text: development.title, count: 1)
end end
@ -492,7 +499,7 @@ RSpec.describe 'Project issue boards', :js do
context 'keyboard shortcuts' do context 'keyboard shortcuts' do
before do before do
visit_project_board_path_without_query_limit(project, board) visit_project_board(project, board)
wait_for_requests wait_for_requests
end end
@ -505,6 +512,7 @@ RSpec.describe 'Project issue boards', :js do
context 'signed out user' do context 'signed out user' do
before do before do
stub_feature_flags(apollo_boards: false)
visit project_board_path(project, board) visit project_board_path(project, board)
wait_for_requests wait_for_requests
end end
@ -526,6 +534,7 @@ RSpec.describe 'Project issue boards', :js do
let_it_be(:user_guest) { create(:user) } let_it_be(:user_guest) { create(:user) }
before do before do
stub_feature_flags(apollo_boards: false)
project.add_guest(user_guest) project.add_guest(user_guest)
sign_in(user_guest) sign_in(user_guest)
visit project_board_path(project, board) visit project_board_path(project, board)
@ -587,11 +596,9 @@ RSpec.describe 'Project issue boards', :js do
end end
end end
def visit_project_board_path_without_query_limit(project, board) def visit_project_board(project, board)
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do visit project_board_path(project, board)
visit project_board_path(project, board)
wait_for_requests wait_for_requests
end
end end
end end

View File

@ -15,6 +15,7 @@ RSpec.describe 'Issue Boards', :js do
let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) } let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
before do before do
stub_feature_flags(apollo_boards: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View File

@ -20,6 +20,7 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') } let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do before do
stub_feature_flags(apollo_boards: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View File

@ -15,6 +15,7 @@ RSpec.describe 'Project issue boards sidebar', :js do
let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) } let_it_be(:issue, reload: true) { create(:issue, project: project, relative_position: 1) }
before do before do
stub_feature_flags(apollo_boards: false)
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)

View File

@ -31,6 +31,7 @@ RSpec.describe 'User adds lists', :js do
with_them do with_them do
before do before do
stub_feature_flags(apollo_boards: false)
sign_in(user) sign_in(user)
set_cookie('sidebar_collapsed', 'true') set_cookie('sidebar_collapsed', 'true')

View File

@ -44,6 +44,7 @@ RSpec.describe 'User visits issue boards', :js do
with_them do with_them do
before do before do
stub_feature_flags(apollo_boards: false)
visit board_path visit board_path
wait_for_requests wait_for_requests
@ -59,6 +60,7 @@ RSpec.describe 'User visits issue boards', :js do
end end
context "project boards" do context "project boards" do
stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, project: project) } let_it_be(:board) { create_default(:board, project: project) }
let_it_be(:backlog_list) { create_default(:backlog_list, board: board) } let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }
@ -68,6 +70,7 @@ RSpec.describe 'User visits issue boards', :js do
end end
context "group boards" do context "group boards" do
stub_feature_flags(apollo_boards: false)
let_it_be(:board) { create_default(:board, group: group) } let_it_be(:board) { create_default(:board, group: group) }
let_it_be(:backlog_list) { create_default(:backlog_list, board: board) } let_it_be(:backlog_list) { create_default(:backlog_list, board: board) }

View File

@ -19,6 +19,7 @@ RSpec.describe 'Group Issue Boards', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') } let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do before do
stub_feature_flags(apollo_boards: false)
sign_in(user) sign_in(user)
visit group_board_path(group, board) visit group_board_path(group, board)

View File

@ -4,12 +4,16 @@ require "spec_helper"
RSpec.describe "User views incident" do RSpec.describe "User views incident" do
let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) } let_it_be(:developer) { create(:user) }
let_it_be(:note) { create(:note, noteable: incident, project: project, author: user) } let_it_be(:user) { developer }
let(:author) { developer }
let(:description) { "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)" }
let(:incident) { create(:incident, project: project, description: description, author: author) }
before_all do before_all do
project.add_developer(user) project.add_developer(developer)
project.add_guest(guest)
end end
before do before do
@ -18,57 +22,61 @@ RSpec.describe "User views incident" do
visit(project_issues_incident_path(project, incident)) visit(project_issues_incident_path(project, incident))
end end
it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") } specify do
expect(page).to have_header_with_correct_id_and_link(1, 'Description header', 'description-header')
end
it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet' it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
describe 'user actions' do describe 'user actions' do
it 'shows the merge request and incident actions', :js, :aggregate_failures do it 'shows the merge request and incident actions', :js, :aggregate_failures do
expected_href = new_project_issue_path(project,
issuable_template: 'incident',
issue: { issue_type: 'incident' },
add_related_issue: incident.iid)
click_button 'Incident actions' click_button 'Incident actions'
expect(page).to have_link('New related incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' }, add_related_issue: incident.iid })) expect(page).to have_link('New related incident', href: expected_href)
expect(page).to have_button('Create merge request') expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident') expect(page).to have_button('Close incident')
end end
context 'when user is a guest' do context 'when user is guest' do
before do let(:user) { guest }
project.add_guest(user)
login_as(user) context 'and author' do
let(:author) { guest }
visit(project_issues_incident_path(project, incident)) it 'does not show the incident actions', :js do
expect(page).not_to have_button('Incident actions')
end
end end
it 'does not show the incident actions', :js, :aggregate_failures do context 'and not author' do
expect(page).not_to have_button('Incident actions') it 'shows incident actions', :js do
click_button 'Incident actions'
expect(page).to have_link 'Report abuse'
end
end end
end end
end end
context 'when the project is archived' do context 'when the project is archived' do
before do before_all do
project.update!(archived: true) project.update!(archived: true)
visit(project_issues_incident_path(project, incident))
end end
it 'hides the merge request and incident actions', :aggregate_failures do it 'does not show the incident actions', :js do
expect(page).not_to have_link('New incident') expect(page).not_to have_button('Incident actions')
expect(page).not_to have_button('Create merge request')
expect(page).not_to have_link('Close incident')
end end
end end
describe 'user status' do describe 'user status' do
subject { visit(project_issues_incident_path(project, incident)) }
context 'when showing status of the author of the incident' do context 'when showing status of the author of the incident' do
it_behaves_like 'showing user status' do subject { visit(project_issues_incident_path(project, incident)) }
let(:user_with_status) { user }
end
end
context 'when showing status of a user who commented on an incident', :js do
it_behaves_like 'showing user status' do it_behaves_like 'showing user status' do
let(:user_with_status) { user } let(:user_with_status) { user }
end end

View File

@ -417,7 +417,7 @@ RSpec.describe 'Issues > Labels bulk assignment' do
click_button 'Select milestone' click_button 'Select milestone'
wait_for_requests wait_for_requests
items.map do |item| items.map do |item|
click_link item click_button item
end end
end end

View File

@ -80,7 +80,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
click_button 'Edit issues' click_button 'Edit issues'
check 'Select all' check 'Select all'
click_button 'Select milestone' click_button 'Select milestone'
click_link milestone.title click_button milestone.title
click_update_issues_button click_update_issues_button
expect(page.find('.issue')).to have_content milestone.title expect(page.find('.issue')).to have_content milestone.title
@ -97,7 +97,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js do
click_button 'Edit issues' click_button 'Edit issues'
check 'Select all' check 'Select all'
click_button 'Select milestone' click_button 'Select milestone'
click_link 'No milestone' click_button 'No milestone'
click_update_issues_button click_update_issues_button
expect(find('.issue:first-of-type')).not_to have_text milestone.title expect(find('.issue:first-of-type')).not_to have_text milestone.title

View File

@ -130,7 +130,7 @@ RSpec.describe 'Merge requests > User mass updates', :js do
click_button 'Edit merge requests' click_button 'Edit merge requests'
check 'Select all' check 'Select all'
click_button 'Select milestone' click_button 'Select milestone'
click_link text click_button text
click_update_merge_requests_button click_update_merge_requests_button
end end

View File

@ -28,6 +28,7 @@ describe('BoardApp', () => {
store, store,
provide: { provide: {
...provide, ...provide,
fullBoardId: 'gid://gitlab/Board/1',
}, },
}); });
}; };

View File

@ -1,15 +1,20 @@
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import Vuex from 'vuex'; import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters'; import getters from 'ee_else_ce/boards/stores/getters';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import BoardColumn from '~/boards/components/board_column.vue'; import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue'; import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { mockLists } from '../mock_data'; import { mockLists, boardListsQueryResponse } from '../mock_data';
Vue.use(VueApollo);
Vue.use(Vuex); Vue.use(Vuex);
const actions = { const actions = {
@ -18,6 +23,7 @@ const actions = {
describe('BoardContent', () => { describe('BoardContent', () => {
let wrapper; let wrapper;
let fakeApollo;
window.gon = {}; window.gon = {};
const defaultState = { const defaultState = {
@ -35,19 +41,33 @@ describe('BoardContent', () => {
}); });
}; };
const createComponent = ({ state, props = {}, canAdminList = true } = {}) => { const createComponent = ({
state,
props = {},
canAdminList = true,
isApolloBoard = false,
issuableType = 'issue',
boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
} = {}) => {
fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
const store = createStore({ const store = createStore({
...defaultState, ...defaultState,
...state, ...state,
}); });
wrapper = shallowMount(BoardContent, { wrapper = shallowMount(BoardContent, {
apolloProvider: fakeApollo,
propsData: { propsData: {
lists: mockLists,
disabled: false, disabled: false,
boardId: 'gid://gitlab/Board/1',
...props, ...props,
}, },
provide: { provide: {
canAdminList, canAdminList,
boardType: 'group',
fullPath: 'gitlab-org/gitlab',
issuableType,
isApolloBoard,
}, },
store, store,
}); });
@ -78,6 +98,7 @@ describe('BoardContent', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
fakeApollo = null;
}); });
describe('default', () => { describe('default', () => {
@ -112,7 +133,7 @@ describe('BoardContent', () => {
describe('when issuableType is not issue', () => { describe('when issuableType is not issue', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ state: { issuableType: 'foo' } }); createComponent({ issuableType: 'foo' });
}); });
it('does not render BoardContentSidebar', () => { it('does not render BoardContentSidebar', () => {
@ -139,4 +160,19 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(Draggable).exists()).toBe(false); expect(wrapper.findComponent(Draggable).exists()).toBe(false);
}); });
}); });
describe('when Apollo boards FF is on', () => {
beforeEach(async () => {
createComponent({ isApolloBoard: true });
await waitForPromises();
});
it('renders a BoardColumn component per list', () => {
expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
});
it('renders BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
});
}); });

View File

@ -433,8 +433,11 @@ export const mockList = {
label: null, label: null,
assignee: null, assignee: null,
milestone: null, milestone: null,
iteration: null,
loading: false, loading: false,
issuesCount: 1, issuesCount: 1,
maxIssueCount: 0,
__typename: 'BoardList',
}; };
export const mockLabelList = { export const mockLabelList = {
@ -449,11 +452,15 @@ export const mockLabelList = {
color: '#F0AD4E', color: '#F0AD4E',
textColor: '#FFFFFF', textColor: '#FFFFFF',
description: null, description: null,
descriptionHtml: null,
}, },
assignee: null, assignee: null,
milestone: null, milestone: null,
iteration: null,
loading: false, loading: false,
issuesCount: 0, issuesCount: 0,
maxIssueCount: 0,
__typename: 'BoardList',
}; };
export const mockMilestoneList = { export const mockMilestoneList = {
@ -844,6 +851,22 @@ export const mockGroupLabelsResponse = {
}, },
}; };
export const boardListsQueryResponse = {
data: {
group: {
id: 'gid://gitlab/Group/1',
board: {
id: 'gid://gitlab/Board/1',
hideBacklogList: false,
lists: {
nodes: mockLists,
},
},
__typename: 'Group',
},
},
};
export const boardListQueryResponse = (issuesCount = 20) => ({ export const boardListQueryResponse = (issuesCount = 20) => ({
data: { data: {
boardList: { boardList: {

View File

@ -2,10 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { GlTab } from '@gitlab/ui'; import { GlTab } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue'; import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import Dag from '~/pipelines/components/dag/dag.vue';
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
describe('The Pipeline Tabs', () => { describe('The Pipeline Tabs', () => {
let wrapper; let wrapper;
@ -16,12 +12,6 @@ describe('The Pipeline Tabs', () => {
const findPipelineTab = () => wrapper.findByTestId('pipeline-tab'); const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
const findTestsTab = () => wrapper.findByTestId('tests-tab'); const findTestsTab = () => wrapper.findByTestId('tests-tab');
const findDagApp = () => wrapper.findComponent(Dag);
const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
const findJobsApp = () => wrapper.findComponent(JobsApp);
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
const findTestsApp = () => wrapper.findComponent(TestReports);
const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter'); const findFailedJobsBadge = () => wrapper.findByTestId('failed-builds-counter');
const findJobsBadge = () => wrapper.findByTestId('builds-counter'); const findJobsBadge = () => wrapper.findByTestId('builds-counter');
const findTestsBadge = () => wrapper.findByTestId('tests-counter'); const findTestsBadge = () => wrapper.findByTestId('tests-counter');
@ -43,6 +33,7 @@ describe('The Pipeline Tabs', () => {
}, },
stubs: { stubs: {
GlTab, GlTab,
RouterView: true,
}, },
}), }),
); );
@ -54,17 +45,16 @@ describe('The Pipeline Tabs', () => {
describe('Tabs', () => { describe('Tabs', () => {
it.each` it.each`
tabName | tabComponent | appComponent tabName | tabComponent
${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp} ${'Pipeline'} | ${findPipelineTab}
${'Dag'} | ${findDagTab} | ${findDagApp} ${'Dag'} | ${findDagTab}
${'Jobs'} | ${findJobsTab} | ${findJobsApp} ${'Jobs'} | ${findJobsTab}
${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp} ${'Failed Jobs'} | ${findFailedJobsTab}
${'Tests'} | ${findTestsTab} | ${findTestsApp} ${'Tests'} | ${findTestsTab}
`('shows $tabName tab with its associated component', ({ appComponent, tabComponent }) => { `('shows $tabName tab', ({ tabComponent }) => {
createComponent(); createComponent();
expect(tabComponent().exists()).toBe(true); expect(tabComponent().exists()).toBe(true);
expect(appComponent().exists()).toBe(true);
}); });
describe('with no failed jobs', () => { describe('with no failed jobs', () => {

View File

@ -1,5 +1,5 @@
import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils'; import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
import { TAB_QUERY_PARAM, validPipelineTabNames } from '~/pipelines/constants'; import { validPipelineTabNames } from '~/pipelines/constants';
describe('utils functions', () => { describe('utils functions', () => {
const jobName1 = 'build_1'; const jobName1 = 'build_1';
@ -173,18 +173,25 @@ describe('utils functions', () => {
describe('getPipelineDefaultTab', () => { describe('getPipelineDefaultTab', () => {
const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/'; const baseUrl = 'http://gitlab.com/user/multi-projects-small/-/pipelines/332/';
it('returns null if there was no `tab` params', () => { it('returns null if there is only the base url', () => {
expect(getPipelineDefaultTab(baseUrl)).toBe(null); expect(getPipelineDefaultTab(baseUrl)).toBe(null);
}); });
it('returns null if there was no valid tab param', () => { it('returns null if there was no valid last url part', () => {
expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=invalid`)).toBe(null); expect(getPipelineDefaultTab(`${baseUrl}something`)).toBe(null);
}); });
it('returns the correct tab name if present', () => { it('returns the correct tab name if present', () => {
validPipelineTabNames.forEach((tabName) => { validPipelineTabNames.forEach((tabName) => {
expect(getPipelineDefaultTab(`${baseUrl}?${TAB_QUERY_PARAM}=${tabName}`)).toBe(tabName); expect(getPipelineDefaultTab(`${baseUrl}${tabName}`)).toBe(tabName);
}); });
}); });
it('returns the right value even with query params', () => {
const [tabName] = validPipelineTabNames;
expect(getPipelineDefaultTab(`${baseUrl}${tabName}?query="something"&query2="else"`)).toBe(
tabName,
);
});
}); });
}); });

View File

@ -1,9 +1,7 @@
import { createAppOptions, createPipelineTabs } from '~/pipelines/pipeline_tabs'; import { createAppOptions } from '~/pipelines/pipeline_tabs';
import { updateHistory } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
removeParams: () => 'gitlab.com', removeParams: () => 'gitlab.com',
updateHistory: jest.fn(),
joinPaths: () => {}, joinPaths: () => {},
setUrlFragment: () => {}, setUrlFragment: () => {},
})); }));
@ -64,32 +62,4 @@ describe('~/pipelines/pipeline_tabs.js', () => {
expect(createAppOptions('foo', null)).toBe(null); expect(createAppOptions('foo', null)).toBe(null);
}); });
}); });
describe('createPipelineTabs', () => {
const title = 'Pipeline Tabs';
beforeAll(() => {
document.title = title;
});
afterAll(() => {
document.title = '';
});
it('calls `updateHistory` with correct params', () => {
createPipelineTabs({});
expect(updateHistory).toHaveBeenCalledWith({
title,
url: 'gitlab.com',
replace: true,
});
});
it("returns early if options aren't provided", () => {
createPipelineTabs();
expect(updateHistory).not.toHaveBeenCalled();
});
});
}); });

View File

@ -0,0 +1,74 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale';
import BulkUpdateMilestoneDropdown from '~/sidebar/components/milestone/bulk_update_milestone_dropdown.vue';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
describe('BulkUpdateMilestoneDropdown component', () => {
let wrapper;
const propsData = {
attrWorkspacePath: 'full/path',
issuableType: IssuableType.Issue,
workspaceType: WorkspaceType.project,
};
const findHiddenInput = () => wrapper.find('input');
const findSidebarDropdown = () => wrapper.findComponent(SidebarDropdown);
const createComponent = () => {
wrapper = shallowMount(BulkUpdateMilestoneDropdown, { propsData });
};
beforeEach(() => {
createComponent();
});
it('renders SidebarDropdown', () => {
expect(findSidebarDropdown().props()).toMatchObject({
attrWorkspacePath: propsData.attrWorkspacePath,
issuableAttribute: BulkUpdateMilestoneDropdown.issuableAttribute,
issuableType: propsData.issuableType,
workspaceType: propsData.workspaceType,
});
});
it('renders hidden input', () => {
expect(findHiddenInput().attributes()).toEqual({
type: 'hidden',
name: 'update[milestone_id]',
value: undefined,
});
});
describe('when SidebarDropdown emits `change` event', () => {
describe('when valid milestone is emitted', () => {
it('updates the hidden input value', async () => {
const milestone = {
id: 'gid://gitlab/Milestone/52',
title: __('Milestone 52'),
};
findSidebarDropdown().vm.$emit('change', milestone);
await nextTick();
expect(findHiddenInput().attributes('value')).toBe(
getIdFromGraphQLId(milestone.id).toString(),
);
});
});
describe('when null milestone is emitted', () => {
it('updates the hidden input value to `0`', async () => {
const milestone = { id: null };
findSidebarDropdown().vm.$emit('change', milestone);
await nextTick();
expect(findHiddenInput().attributes('value')).toBe('0');
});
});
});
});

View File

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

View File

@ -389,6 +389,40 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo) model.add_concurrent_index(:users, :foo)
end end
context 'when targeting a partition table' do
let(:schema) { 'public' }
let(:name) { '_test_partition_01' }
let(:identifier) { "#{schema}.#{name}" }
before do
model.execute(<<~SQL)
CREATE TABLE public._test_partitioned_table (
id serial NOT NULL,
partition_id serial NOT NULL,
PRIMARY KEY (id, partition_id)
) PARTITION BY LIST(partition_id);
CREATE TABLE #{identifier} PARTITION OF public._test_partitioned_table
FOR VALUES IN (1);
SQL
end
context 'when allow_partition is true' do
it 'creates the index concurrently' do
expect(model).to receive(:add_index).with(:_test_partition_01, :foo, algorithm: :concurrently)
model.add_concurrent_index(:_test_partition_01, :foo, allow_partition: true)
end
end
context 'when allow_partition is not provided' do
it 'raises ArgumentError' do
expect { model.add_concurrent_index(:_test_partition_01, :foo) }
.to raise_error(ArgumentError, /use add_concurrent_partitioned_index/)
end
end
end
end end
context 'inside a transaction' do context 'inside a transaction' do
@ -2889,4 +2923,36 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
model.add_sequence(:test_table, :test_column, :test_table_id_seq, 1) model.add_sequence(:test_table, :test_column, :test_table_id_seq, 1)
end end
end end
describe "#partition?" do
subject { model.partition?(table_name) }
let(:table_name) { 'ci_builds_metadata' }
context "when a partition table exist" do
context 'when the view postgres_partitions exists' do
it 'calls the view', :aggregate_failures do
expect(Gitlab::Database::PostgresPartition).to receive(:partition_exists?).with(table_name).and_call_original
expect(subject).to be_truthy
end
end
context 'when the view postgres_partitions does not exist' do
before do
allow(model).to receive(:view_exists?).and_return(false)
end
it 'does not call the view', :aggregate_failures do
expect(Gitlab::Database::PostgresPartition).to receive(:legacy_partition_exists?).with(table_name).and_call_original
expect(subject).to be_truthy
end
end
end
context "when a partition table does not exist" do
let(:table_name) { 'partition_does_not_exist' }
it { is_expected.to be_falsey }
end
end
end end

View File

@ -65,8 +65,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
end end
def expect_add_concurrent_index_and_call_original(table, column, index) def expect_add_concurrent_index_and_call_original(table, column, index)
expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, { name: index }) expect(migration).to receive(:add_concurrent_index).ordered.with(table, column, { name: index, allow_partition: true })
.and_wrap_original { |_, table, column, options| connection.add_index(table, column, **options) } .and_wrap_original do |_, table, column, options|
options.delete(:allow_partition)
connection.add_index(table, column, **options)
end
end end
end end
@ -91,7 +94,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::IndexHelpers do
it 'forwards them to the index helper methods', :aggregate_failures do it 'forwards them to the index helper methods', :aggregate_failures do
expect(migration).to receive(:add_concurrent_index) expect(migration).to receive(:add_concurrent_index)
.with(partition1_identifier, column_name, { name: partition1_index, where: 'x > 0', unique: true }) .with(partition1_identifier, column_name, { name: partition1_index, where: 'x > 0', unique: true, allow_partition: true })
expect(migration).to receive(:add_index) expect(migration).to receive(:add_index)
.with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true }) .with(table_name, column_name, { name: index_name, where: 'x > 0', unique: true })

View File

@ -72,4 +72,36 @@ RSpec.describe Gitlab::Database::PostgresPartition, type: :model do
expect(find(identifier).condition).to eq("FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')") expect(find(identifier).condition).to eq("FOR VALUES FROM ('2020-01-01 00:00:00+00') TO ('2020-02-01 00:00:00+00')")
end end
end end
describe '.partition_exists?' do
subject { described_class.partition_exists?(table_name) }
context 'when the partition exists' do
let(:table_name) { "ci_builds_metadata" }
it { is_expected.to be_truthy }
end
context 'when the partition does not exist' do
let(:table_name) { 'partition_does_not_exist' }
it { is_expected.to be_falsey }
end
end
describe '.legacy_partition_exists?' do
subject { described_class.legacy_partition_exists?(table_name) }
context 'when the partition exists' do
let(:table_name) { "ci_builds_metadata" }
it { is_expected.to be_truthy }
end
context 'when the partition does not exist' do
let(:table_name) { 'partition_does_not_exist' }
it { is_expected.to be_falsey }
end
end
end end

View File

@ -39,6 +39,72 @@ RSpec.describe Gitlab::GonHelper do
helper.add_gon_variables helper.add_gon_variables
end end
end end
describe 'sentry configuration' do
let(:legacy_clientside_dsn) { 'https://xxx@sentry-legacy.example.com/1' }
let(:clientside_dsn) { 'https://xxx@sentry.example.com/1' }
let(:environment) { 'production' }
context 'with enable_old_sentry_clientside_integration enabled' do
before do
stub_feature_flags(
enable_old_sentry_clientside_integration: true,
enable_new_sentry_clientside_integration: false
)
stub_config(sentry: { enabled: true, clientside_dsn: legacy_clientside_dsn, environment: environment })
end
it 'sets sentry dsn and environment from config' do
expect(gon).to receive(:sentry_dsn=).with(legacy_clientside_dsn)
expect(gon).to receive(:sentry_environment=).with(environment)
helper.add_gon_variables
end
end
context 'with enable_new_sentry_clientside_integration enabled' do
before do
stub_feature_flags(
enable_old_sentry_clientside_integration: false,
enable_new_sentry_clientside_integration: true
)
stub_application_setting(sentry_enabled: true)
stub_application_setting(sentry_clientside_dsn: clientside_dsn)
stub_application_setting(sentry_environment: environment)
end
it 'sets sentry dsn and environment from application settings' do
expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
expect(gon).to receive(:sentry_environment=).with(environment)
helper.add_gon_variables
end
end
context 'with enable_old_sentry_clientside_integration and enable_new_sentry_clientside_integration enabled' do
before do
stub_feature_flags(
enable_old_sentry_clientside_integration: true,
enable_new_sentry_clientside_integration: true
)
stub_config(sentry: { enabled: true, clientside_dsn: legacy_clientside_dsn, environment: environment })
stub_application_setting(sentry_enabled: true)
stub_application_setting(sentry_clientside_dsn: clientside_dsn)
stub_application_setting(sentry_environment: environment)
end
it 'sets sentry dsn and environment from application settings' do
expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
expect(gon).to receive(:sentry_environment=).with(environment)
helper.add_gon_variables
end
end
end
end end
describe '#push_frontend_feature_flag' do describe '#push_frontend_feature_flag' do

View File

@ -13,6 +13,11 @@ RSpec.describe AlertManagement::HttpIntegration do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
end end
describe 'default values' do
it { expect(described_class.new.endpoint_identifier).to be_present }
it { expect(described_class.new(endpoint_identifier: 'test').endpoint_identifier).to eq('test') }
end
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
@ -124,10 +129,6 @@ RSpec.describe AlertManagement::HttpIntegration do
end end
context 'when unsaved' do context 'when unsaved' do
context 'when unassigned' do
it_behaves_like 'valid token'
end
context 'when assigned' do context 'when assigned' do
include_context 'assign token', 'random_token' include_context 'assign token', 'random_token'

View File

@ -4089,7 +4089,6 @@
- './spec/features/incidents/user_creates_new_incident_spec.rb' - './spec/features/incidents/user_creates_new_incident_spec.rb'
- './spec/features/incidents/user_filters_incidents_by_status_spec.rb' - './spec/features/incidents/user_filters_incidents_by_status_spec.rb'
- './spec/features/incidents/user_searches_incidents_spec.rb' - './spec/features/incidents/user_searches_incidents_spec.rb'
- './spec/features/incidents/user_views_incident_spec.rb'
- './spec/features/invites_spec.rb' - './spec/features/invites_spec.rb'
- './spec/features/issuables/issuable_list_spec.rb' - './spec/features/issuables/issuable_list_spec.rb'
- './spec/features/issuables/markdown_references/internal_references_spec.rb' - './spec/features/issuables/markdown_references/internal_references_spec.rb'

View File

@ -3,6 +3,7 @@
RSpec.shared_examples 'multiple issue boards' do RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do context 'authorized user' do
before do before do
stub_feature_flags(apollo_boards: false)
parent.add_maintainer(user) parent.add_maintainer(user)
login_as(user) login_as(user)
@ -121,6 +122,7 @@ RSpec.shared_examples 'multiple issue boards' do
context 'unauthorized user' do context 'unauthorized user' do
before do before do
stub_feature_flags(apollo_boards: false)
visit boards_path visit boards_path
wait_for_requests wait_for_requests
end end

View File

@ -5,6 +5,7 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do context 'multiple issue boards' do
before do before do
stub_feature_flags(apollo_boards: false)
board_parent.add_reporter(user) board_parent.add_reporter(user)
stub_licensed_features(multiple_group_issue_boards: true) stub_licensed_features(multiple_group_issue_boards: true)
end end

View File

@ -52,6 +52,7 @@ const (
geoGitProjectPattern = `^/[^-].+\.git/` // Prevent matching routes like /-/push_from_secondary geoGitProjectPattern = `^/[^-].+\.git/` // Prevent matching routes like /-/push_from_secondary
projectPattern = `^/([^/]+/){1,}[^/]+/` projectPattern = `^/([^/]+/){1,}[^/]+/`
apiProjectPattern = apiPattern + `v4/projects/[^/]+` // API: Projects can be encoded via group%2Fsubgroup%2Fproject apiProjectPattern = apiPattern + `v4/projects/[^/]+` // API: Projects can be encoded via group%2Fsubgroup%2Fproject
apiGroupPattern = apiPattern + `v4/groups/[^/]+`
apiTopicPattern = apiPattern + `v4/topics` apiTopicPattern = apiPattern + `v4/topics`
snippetUploadPattern = `^/uploads/personal_snippet` snippetUploadPattern = `^/uploads/personal_snippet`
userUploadPattern = `^/uploads/user` userUploadPattern = `^/uploads/user`
@ -303,6 +304,7 @@ func configureRoutes(u *upstream) {
// we need to declare each routes until we have fixed all the routes on the rails codebase. // we need to declare each routes until we have fixed all the routes on the rails codebase.
// Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status // Overall status can be seen at https://gitlab.com/groups/gitlab-org/-/epics/1802#current-status
u.route("POST", apiProjectPattern+`/wikis/attachments\z`, tempfileMultipartProxy), u.route("POST", apiProjectPattern+`/wikis/attachments\z`, tempfileMultipartProxy),
u.route("POST", apiGroupPattern+`/wikis/attachments\z`, tempfileMultipartProxy),
u.route("POST", apiPattern+`graphql\z`, tempfileMultipartProxy), u.route("POST", apiPattern+`graphql\z`, tempfileMultipartProxy),
u.route("POST", apiTopicPattern, tempfileMultipartProxy), u.route("POST", apiTopicPattern, tempfileMultipartProxy),
u.route("PUT", apiTopicPattern, tempfileMultipartProxy), u.route("PUT", apiTopicPattern, tempfileMultipartProxy),

View File

@ -138,6 +138,8 @@ func TestAcceleratedUpload(t *testing.T) {
{"POST", `/api/v4/groups`, false}, {"POST", `/api/v4/groups`, false},
{"PUT", `/api/v4/groups/5`, false}, {"PUT", `/api/v4/groups/5`, false},
{"PUT", `/api/v4/groups/group%2Fsubgroup`, false}, {"PUT", `/api/v4/groups/group%2Fsubgroup`, false},
{"POST", `/api/v4/groups/1/wikis/attachments`, false},
{"POST", `/api/v4/groups/my%2Fsubgroup/wikis/attachments`, false},
{"POST", `/api/v4/users`, false}, {"POST", `/api/v4/users`, false},
{"PUT", `/api/v4/users/42`, false}, {"PUT", `/api/v4/users/42`, false},
{"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true}, {"PUT", "/api/v4/projects/9001/packages/nuget/v1/files", true},