summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml20
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue1
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/app.vue11
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue37
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/constants.js1
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue2
-rw-r--r--app/assets/javascripts/diffs/store/getters.js6
-rw-r--r--app/assets/javascripts/related_issues/components/related_issuable_input.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue75
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue63
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue29
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue14
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue4
-rw-r--r--app/assets/javascripts/work_items/router/index.js2
-rw-r--r--app/assets/stylesheets/framework/forms.scss19
-rw-r--r--app/assets/stylesheets/framework/timeline.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss13
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/groups/children_controller.rb24
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/views/admin/broadcast_messages/_preview.html.haml3
-rw-r--r--app/views/projects/commit/diff_files.html.haml6
-rw-r--r--app/views/projects/diffs/_diffs.html.haml4
-rw-r--r--config/application.rb1
-rw-r--r--config/feature_flags/development/runner_machine_heartbeat.yml8
-rw-r--r--doc/user/application_security/dast/proxy-based.md6
-rw-r--r--doc/user/application_security/secret_detection/index.md10
-rw-r--r--doc/user/discussions/index.md2
-rw-r--r--doc/user/group/saml_sso/index.md2
-rw-r--r--doc/user/group/saml_sso/scim_setup.md4
-rw-r--r--doc/user/okrs.md20
-rw-r--r--doc/user/tasks.md20
-rw-r--r--lib/api/ci/helpers/runner.rb2
-rw-r--r--lib/gitlab/background_migration/issues_internal_id_scope_updater.rb17
-rw-r--r--lib/gitlab/multi_collection_paginator.rb2
-rw-r--r--locale/gitlab.pot9
-rw-r--r--spec/controllers/groups/children_controller_spec.rb12
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb221
-rw-r--r--spec/features/admin/admin_runners_spec.rb23
-rw-r--r--spec/features/commit_spec.rb29
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb14
-rw-r--r--spec/frontend/admin/abuse_reports/components/app_spec.js13
-rw-r--r--spec/frontend/diffs/store/getters_spec.js13
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js45
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js126
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js1
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js15
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js22
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js1
-rw-r--r--spec/frontend/work_items/router_spec.js1
-rw-r--r--spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb21
-rw-r--r--spec/lib/gitlab/multi_collection_paginator_spec.rb7
-rw-r--r--spec/models/commit_status_spec.rb1
-rw-r--r--spec/requests/admin/broadcast_messages_controller_spec.rb5
-rw-r--r--spec/requests/api/ci/runner/jobs_put_spec.rb11
-rw-r--r--spec/services/ci/unlock_artifacts_service_spec.rb6
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb11
-rw-r--r--workhorse/go.mod2
-rw-r--r--workhorse/go.sum3
68 files changed, 776 insertions, 304 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 59758d2aec7..c5d992cab63 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -49,6 +49,18 @@ compile-production-assets:
after_script:
- rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
+compile-production-assets-esbuild:
+ allow_failure: true
+ extends:
+ - .compile-assets-base
+ - .frontend:rules:compile-production-assets
+ variables:
+ NODE_ENV: "production"
+ RAILS_ENV: "production"
+ WEBPACK_USE_ESBUILD_LOADER: "true"
+ after_script:
+ - rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
+
compile-test-assets:
extends:
- .compile-assets-base
@@ -61,6 +73,14 @@ compile-test-assets:
- "${WEBPACK_COMPILE_LOG_PATH}"
when: always
+compile-test-assets-esbuild:
+ allow_failure: true
+ extends:
+ - .compile-assets-base
+ - .frontend:rules:compile-test-assets
+ variables:
+ WEBPACK_USE_ESBUILD_LOADER: "true"
+
compile-test-assets as-if-foss:
extends:
- compile-test-assets
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 3275b63d742..99aa62a1bca 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -648,10 +648,10 @@ e2e-test-report:
- .rules:report:allure-report
stage: report
variables:
+ ALLURE_JOB_NAME: e2e-package-and-test
GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE
ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
- GIT_STRATEGY: none
# Temporary separate test report for super-sidebar test job
# TODO: remove once super-sidebar is on by default and enabled in tests
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 97f8820a511..d935fecba01 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -69,7 +69,6 @@ e2e:package-and-test-ee:
GITLAB_QA_IMAGE: "${CI_REGISTRY_IMAGE}/gitlab-ee-qa:${CI_COMMIT_SHA}"
RUN_WITH_BUNDLE: "true" # instructs pipeline to install and run gitlab-qa gem via bundler
QA_PATH: qa # sets the optional path for bundler to run from
- ALLURE_JOB_NAME: e2e-package-and-test
QA_RUN_TYPE: e2e-package-and-test
inherit:
variables:
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
index f7e3675c983..a4211002f71 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
@@ -29,7 +29,7 @@ export default {
</script>
<template>
- <list-item>
+ <list-item data-testid="abuse-report-row">
<template #left-primary>
<div class="gl-font-weight-normal" data-testid="title">{{ title }}</div>
</template>
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue
index 8726cd2f6fa..b60fe3ae9b8 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue
@@ -102,6 +102,7 @@ export default {
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
:sort-options="$options.sortOptions"
+ data-testid="abuse-reports-filtered-search-bar"
@onFilter="handleFilter"
@onSort="handleSort"
/>
diff --git a/app/assets/javascripts/admin/abuse_reports/components/app.vue b/app/assets/javascripts/admin/abuse_reports/components/app.vue
index a3e04b16ff9..e1e75a4f8d0 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/app.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlPagination } from '@gitlab/ui';
+import { GlEmptyState, GlPagination } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from './abuse_reports_filtered_search_bar.vue';
import AbuseReportRow from './abuse_report_row.vue';
@@ -9,6 +9,7 @@ export default {
components: {
AbuseReportRow,
FilteredSearchBar,
+ GlEmptyState,
GlPagination,
},
props: {
@@ -37,7 +38,13 @@ export default {
<div>
<filtered-search-bar />
- <abuse-report-row v-for="(report, index) in abuseReports" :key="index" :report="report" />
+ <gl-empty-state v-if="abuseReports.length == 0" :title="s__('AbuseReports|No reports found')" />
+ <abuse-report-row
+ v-for="(report, index) in abuseReports"
+ v-else
+ :key="index"
+ :report="report"
+ />
<gl-pagination
v-if="showPagination"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
index b52b7f6cdeb..65aa4cba074 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -15,13 +15,22 @@ import { s__ } from '~/locale';
import { createAlert, VARIANT_DANGER } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import {
+ BROADCAST_MESSAGES_PATH,
+ MESSAGES_PREVIEW_PATH,
+ THEMES,
+ TYPES,
+ TYPE_BANNER,
+} from '../constants';
import MessageFormGroup from './message_form_group.vue';
import DatetimePicker from './datetime_picker.vue';
const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } };
export default {
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
name: 'MessageForm',
components: {
DatetimePicker,
@@ -36,6 +45,9 @@ export default {
GlFormTextarea,
MessageFormGroup,
},
+ directives: {
+ SafeHtml,
+ },
mixins: [glFeatureFlagsMixin()],
inject: ['targetAccessLevelOptions'],
i18n: {
@@ -81,6 +93,7 @@ export default {
})),
startsAt: new Date(this.broadcastMessage.startsAt.getTime()),
endsAt: new Date(this.broadcastMessage.endsAt.getTime()),
+ renderedMessage: '',
};
},
computed: {
@@ -91,7 +104,7 @@ export default {
return this.message.trim() === '';
},
messagePreview() {
- return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message;
+ return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.renderedMessage;
},
isAddForm() {
return !this.broadcastMessage.id;
@@ -114,6 +127,11 @@ export default {
});
},
},
+ watch: {
+ message() {
+ this.renderPreview();
+ },
+ },
methods: {
async onSubmit() {
this.loading = true;
@@ -140,13 +158,25 @@ export default {
}
return true;
},
+
+ async renderPreview() {
+ try {
+ const res = await axios.post(MESSAGES_PREVIEW_PATH, this.formPayload, FORM_HEADERS);
+ this.renderedMessage = res.data;
+ } catch (e) {
+ this.renderedMessage = '';
+ }
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'],
},
};
</script>
<template>
<gl-form @submit.prevent="onSubmit">
<gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable">
- {{ messagePreview }}
+ <div v-safe-html:[$options.safeHtmlConfig]="messagePreview"></div>
</gl-broadcast-message>
<message-form-group :label="$options.i18n.message" label-for="message-textarea">
@@ -154,6 +184,7 @@ export default {
id="message-textarea"
v-model="message"
size="sm"
+ :debounce="$options.DEFAULT_DEBOUNCE_AND_THROTTLE_MS"
:placeholder="$options.i18n.messagePlaceholder"
/>
</message-form-group>
diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js
index 6250d5a943d..323ac6857f6 100644
--- a/app/assets/javascripts/admin/broadcast_messages/constants.js
+++ b/app/assets/javascripts/admin/broadcast_messages/constants.js
@@ -1,6 +1,7 @@
import { s__ } from '~/locale';
export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages';
+export const MESSAGES_PREVIEW_PATH = '/admin/broadcast_messages/preview';
export const TYPE_BANNER = 'banner';
export const TYPE_NOTIFICATION = 'notification';
diff --git a/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue
index 694ab7b526e..ff182c61ccf 100644
--- a/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/platforms_drawer.vue
@@ -88,6 +88,7 @@ export default {
:open="open"
:header-height="drawerHeightOffset"
:z-index="$options.DRAWER_Z_INDEX"
+ data-testid="runner-platforms-drawer"
@close="onClose"
>
<template #title>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index c2b7b9aea8a..ee9ca5dc08c 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -97,7 +97,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link @click="toggleDrawer">{{ content }}</gl-link>
+ <gl-link data-testid="runner-install-link" @click="toggleDrawer">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index f530c05ae1e..10a6a872fe4 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -90,6 +90,12 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
export const getDiffFileByHash = (state) => (fileHash) =>
state.diffFiles.find((file) => file.file_hash === fileHash);
+export function isTreePathLoaded(state) {
+ return (path) => {
+ return Boolean(state.treeEntries[path]?.diffLoaded);
+ };
+}
+
export const flatBlobsList = (state) =>
Object.values(state.treeEntries).filter((f) => f.type === 'blob');
diff --git a/app/assets/javascripts/related_issues/components/related_issuable_input.vue b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
index 8d6a3110f35..1846b9cf8f4 100644
--- a/app/assets/javascripts/related_issues/components/related_issuable_input.vue
+++ b/app/assets/javascripts/related_issues/components/related_issuable_input.vue
@@ -182,7 +182,7 @@ export default {
<div
ref="issuableFormWrapper"
:class="{ focus: isInputFocused }"
- class="add-issuable-form-input-wrapper form-control gl-field-error-outline gl-h-auto gl-p-3 gl-pb-2"
+ class="add-issuable-form-input-wrapper form-control gl-field-error-outline gl-h-auto gl-px-3 gl-pt-2 gl-pb-0"
role="button"
@click="onIssuableFormWrapperClick"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index dbfbd35b9b6..18503720814 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -2,6 +2,11 @@ import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { stateToComponentMap as classStateMap, stateKey } from './stores/state_maps';
+export const FOUR_MINUTES_IN_MS = 1000 * 60 * 4;
+
+export const STATE_QUERY_POLLING_INTERVAL_DEFAULT = 5000;
+export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 2;
+
export const SUCCESS = 'success';
export const WARNING = 'warning';
export const INFO = 'info';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 8867478654a..bbad2c13220 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,5 +1,5 @@
<script>
-import { isEmpty } from 'lodash';
+import { isEmpty, clamp } from 'lodash';
import {
registerExtension,
registeredExtensions,
@@ -10,7 +10,6 @@ import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_wid
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import { stateToComponentMap as classState } from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { createAlert } from '~/alert';
-import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
@@ -44,7 +43,13 @@ import UnresolvedDiscussionsState from './components/states/unresolved_discussio
import WorkInProgressState from './components/states/work_in_progress.vue';
import ExtensionsContainer from './components/extensions/container';
import WidgetContainer from './components/widget/app.vue';
-import { STATE_MACHINE, stateToComponentMap } from './constants';
+import {
+ STATE_MACHINE,
+ stateToComponentMap,
+ STATE_QUERY_POLLING_INTERVAL_DEFAULT,
+ STATE_QUERY_POLLING_INTERVAL_BACKOFF,
+ FOUR_MINUTES_IN_MS,
+} from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
@@ -99,6 +104,7 @@ export default {
apollo: {
state: {
query: getStateQuery,
+ notifyOnNetworkStatusChange: true,
manual: true,
skip() {
return !this.mr;
@@ -106,10 +112,19 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- result({ data: { project } }) {
- if (project) {
- this.mr.setGraphqlData(project);
- this.loading = false;
+ pollInterval() {
+ return this.pollInterval;
+ },
+ result(response) {
+ if (!response.loading) {
+ this.pollInterval = this.apolloStateQueryPollingInterval;
+
+ if (response.data?.project) {
+ this.mr.setGraphqlData(response.data.project);
+ this.loading = false;
+ }
+ } else {
+ this.checkStatus(undefined, undefined, false);
}
},
subscribeToMore: {
@@ -158,9 +173,27 @@ export default {
loading: true,
recomputeComponentName: 0,
issuableId: false,
+ startingPollInterval: STATE_QUERY_POLLING_INTERVAL_DEFAULT,
+ pollInterval: STATE_QUERY_POLLING_INTERVAL_DEFAULT,
};
},
computed: {
+ apolloStateQueryMaxPollingInterval() {
+ return this.startingPollInterval + FOUR_MINUTES_IN_MS;
+ },
+ apolloStateQueryPollingInterval() {
+ if (this.startingPollInterval < 0) {
+ return 0;
+ }
+
+ const unboundedInterval = STATE_QUERY_POLLING_INTERVAL_BACKOFF * this.pollInterval;
+
+ return clamp(
+ unboundedInterval,
+ this.startingPollInterval,
+ this.apolloStateQueryMaxPollingInterval,
+ );
+ },
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
@@ -284,7 +317,8 @@ export default {
mounted() {
MRWidgetService.fetchInitialData()
.then(({ data, headers }) => {
- this.startingPollInterval = Number(headers['POLL-INTERVAL']);
+ this.startingPollInterval =
+ Number(headers['POLL-INTERVAL']) || STATE_QUERY_POLLING_INTERVAL_DEFAULT;
this.initWidget(data);
})
.catch(() =>
@@ -295,9 +329,6 @@ export default {
},
beforeDestroy() {
eventHub.$off('mr.discussion.updated', this.checkStatus);
- if (this.pollingInterval) {
- this.pollingInterval.destroy();
- }
if (this.deploymentsInterval) {
this.deploymentsInterval.destroy();
@@ -332,7 +363,6 @@ export default {
this.initPostMergeDeploymentsPolling();
}
- this.initPolling();
this.bindEventHubListeners();
eventHub.$on('mr.discussion.updated', this.checkStatus);
@@ -363,8 +393,10 @@ export default {
createService(store) {
return new MRWidgetService(this.getServiceEndpoints(store));
},
- checkStatus(cb, isRebased) {
- this.$apollo.queries.state.refetch();
+ checkStatus(cb, isRebased, refetch = true) {
+ if (refetch) {
+ this.$apollo.queries.state.refetch();
+ }
return this.service
.checkStatus()
@@ -389,17 +421,6 @@ export default {
}
return Promise.resolve();
},
- initPolling() {
- if (this.startingPollInterval <= 0) return;
-
- this.pollingInterval = new SmartInterval({
- callback: this.checkStatus,
- startingInterval: this.startingPollInterval,
- maxInterval: this.startingPollInterval + secondsToMilliseconds(4 * 60),
- hiddenInterval: secondsToMilliseconds(6 * 60),
- incrementByFactorOf: 2,
- });
- },
initDeploymentsPolling() {
this.deploymentsInterval = this.deploymentsPoll(this.fetchPreMergeDeployments);
},
@@ -476,10 +497,10 @@ export default {
notify.notifyMe(title, message, this.mr.gitlabLogo);
},
resumePolling() {
- this.pollingInterval?.resume();
+ this.$apollo.queries.state.startPolling(this.pollInterval);
},
stopPolling() {
- this.pollingInterval?.stopTimer();
+ this.$apollo.queries.state.stopPolling();
},
bindEventHubListeners() {
eventHub.$on('MRWidgetUpdateRequested', (cb) => {
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index f010fb1b5ce..1396d19d679 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -96,30 +96,41 @@ export default {
</script>
<template>
- <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
- <markdown-editor
- :value="commentText"
- :render-markdown-path="markdownPreviewPath"
- :markdown-docs-path="$options.constantOptions.markdownDocsPath"
- :form-field-props="formFieldProps"
- data-testid="work-item-add-comment"
- class="gl-mb-3"
- autofocus
- @input="setCommentText"
- @keydown.meta.enter="$emit('submitForm', commentText)"
- @keydown.ctrl.enter="$emit('submitForm', commentText)"
- @keydown.esc.stop="cancelEditing"
- />
- <gl-button
- category="primary"
- variant="confirm"
- data-testid="confirm-button"
- :loading="isSubmitting"
- @click="$emit('submitForm', commentText)"
- >{{ commentButtonText }}
- </gl-button>
- <gl-button data-testid="cancel-button" category="primary" class="gl-ml-3" @click="cancelEditing"
- >{{ __('Cancel') }}
- </gl-button>
- </form>
+ <div class="timeline-content">
+ <div class="timeline-discussion-body">
+ <div class="note-body">
+ <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
+ <markdown-editor
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :form-field-props="formFieldProps"
+ data-testid="work-item-add-comment"
+ class="gl-mb-3"
+ autofocus
+ use-bottom-toolbar
+ @input="setCommentText"
+ @keydown.meta.enter="$emit('submitForm', commentText)"
+ @keydown.ctrl.enter="$emit('submitForm', commentText)"
+ @keydown.esc.stop="cancelEditing"
+ />
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="confirm-button"
+ :loading="isSubmitting"
+ @click="$emit('submitForm', commentText)"
+ >{{ commentButtonText }}
+ </gl-button>
+ <gl-button
+ data-testid="cancel-button"
+ category="primary"
+ class="gl-ml-3"
+ @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </form>
+ </div>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index ba1470f97cd..db36b4e1bbe 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -73,7 +73,7 @@ export default {
<div
v-if="isOpen"
class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
- :class="{ 'gl-px-5 gl-py-4': !error }"
+ :class="{ 'gl-p-3': !error }"
data-testid="widget-body"
>
<slot name="body"></slot>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 3f5e1c74c31..ad7a54aaf16 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -115,11 +115,6 @@ export default {
required: false,
default: null,
},
- modal: {
- type: Object,
- required: false,
- default: null,
- },
},
data() {
const workItemId = getParameterByName('work_item_id');
@@ -707,20 +702,16 @@ export default {
@removeChild="removeChild"
@show-modal="openInModal"
/>
- <template v-if="workItemsMvcEnabled">
- <work-item-notes
- v-if="workItemNotes"
- :work-item-id="workItem.id"
- :query-variables="queryVariables"
- :full-path="fullPath"
- :fetch-by-iid="fetchByIid"
- :work-item-type="workItemType"
- :is-modal="isModal"
- class="gl-pt-5"
- @error="updateError = $event"
- @has-notes="updateHasNotes"
- />
- </template>
+ <work-item-notes
+ v-if="workItemNotes"
+ :work-item-id="workItem.id"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ :work-item-type="workItemType"
+ class="gl-pt-5"
+ @error="updateError = $event"
+ />
<gl-empty-state
v-if="error"
:title="$options.i18n.fetchErrorTitle"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 620d86bb363..d119cdc2785 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -109,7 +109,9 @@ export default {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
- return `/${this.projectPath}/-/work_items/${this.childItem.iid}?iid_path=true`;
+ return `${gon?.relative_url_root || ''}/${this.projectPath}/-/work_items/${
+ this.childItem.iid
+ }?iid_path=true`;
},
hasChildren() {
return this.getWidgetByType(this.childItem, WIDGET_TYPE_HIERARCHY)?.hasChildren;
@@ -171,7 +173,7 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-flex-start gl-mb-3"
+ class="gl-display-flex gl-align-items-flex-start"
:class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
>
<gl-button
@@ -181,18 +183,20 @@ export default {
:aria-label="chevronTooltip"
:icon="chevronType"
category="tertiary"
+ size="small"
:loading="isLoadingChildren"
class="gl-px-0! gl-py-3! gl-mr-3"
data-testid="expand-child"
@click="toggleItem"
/>
<div
- class="gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-bg-white gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
+ class="work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-rounded-base"
+ :class="[hasMetadata ? 'gl-py-3' : 'gl-py-0']"
data-testid="links-child"
>
<span
:id="`stateIcon-${childItem.id}`"
- class="gl-mr-3"
+ class="gl-cursor-help gl-mr-3 gl-line-height-32"
:class="{ 'gl-display-flex': hasMetadata }"
data-testid="item-status-icon"
>
@@ -239,7 +243,7 @@ export default {
<work-item-link-child-metadata
v-if="hasMetadata"
:metadata-widgets="metadataWidgets"
- class="gl-mt-3"
+ class="gl-mt-1"
/>
</div>
<div
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index d7e54ea1eb0..8f0e429234f 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -334,11 +334,11 @@ export default {
</gl-dropdown>
</template>
<template #body>
- <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-3" />
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
<template v-else>
<div v-if="isChildrenEmpty && !isShownAddForm && !error" data-testid="links-empty">
- <p class="gl-mb-0 gl-text-gray-500">
+ <p class="gl-px-3 gl-py-2 gl-mb-0 gl-text-gray-500">
{{ $options.i18n.emptyStateMessage }}
</p>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 5169a77dd33..af475496075 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -340,7 +340,7 @@ export default {
<template>
<gl-form
- class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base"
+ class="gl-bg-white gl-mt-1 gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base"
@submit.prevent="addOrCreateMethod"
>
<gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
index 1aa4a433a58..fb3ed7af736 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
@@ -11,8 +11,8 @@ export default {
</script>
<template>
- <span class="gl-ml-2">
- <gl-dropdown category="tertiary" toggle-class="btn-icon" :right="true">
+ <span class="gl-ml-5">
+ <gl-dropdown category="tertiary" toggle-class="btn-icon btn-sm" :right="true">
<template #button-content>
<gl-icon name="ellipsis_v" :size="14" />
</template>
diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js
index 777badeb5be..8d67bcaf84f 100644
--- a/app/assets/javascripts/work_items/router/index.js
+++ b/app/assets/javascripts/work_items/router/index.js
@@ -11,6 +11,6 @@ export function createRouter(fullPath) {
return new VueRouter({
routes: routes(),
mode: 'history',
- base: joinPaths(fullPath, '-', 'work_items'),
+ base: joinPaths(gon?.relative_url_root, fullPath, '-', 'work_items'),
});
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index b685a27b216..b6ac4939a9c 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -289,3 +289,22 @@ label {
.input-group-text {
max-height: $input-height;
}
+
+.add-issuable-form-input-wrapper {
+ &.focus {
+ border-color: var(--gray-700, $gray-700);
+
+ input {
+ @include gl-shadow-none;
+ }
+ }
+
+ .gl-show-field-errors &.form-control:not(textarea) {
+ height: auto;
+ }
+}
+
+.add-issuable-form-input-wrapper.focus,
+.issue-token-remove-button:focus {
+ @include gl-focus;
+}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 32e9bba8712..699693bd354 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -31,6 +31,7 @@
&:not(.note-form).internal-note .timeline-content,
&:not(.note-form).draft-note .timeline-content {
background-color: $orange-50 !important;
+ border-radius: 3px;
}
.timeline-entry-inner {
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 7321da1526d..6ec6a282329 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -139,21 +139,6 @@
}
}
-.add-issuable-form-input-wrapper {
- &.focus {
- border-color: var(--gray-700, $gray-700);
- @include gl-focus;
-
- input {
- @include gl-shadow-none;
- }
- }
-
- .gl-show-field-errors &.form-control:not(textarea) {
- height: auto;
- }
-}
-
/*
* Following overrides are done to prevent
* legacy dropdown styles from influencing
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 5f6883623b2..00c86c46ac8 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -87,6 +87,19 @@
}
}
+.work-item-link-child {
+ @include gl-border-1;
+ @include gl-border-solid;
+ @include gl-border-transparent;
+ @include gl-rounded-base;
+
+ &:hover,
+ &:focus-within {
+ @include gl-bg-white;
+ @include gl-border-gray-50;
+ }
+}
+
// sticky error placement for errors in modals , by default it is 83px for full view
#work-item-detail-modal {
.flash-container.flash-container-page.sticky {
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index d641a26c9fb..654b8309937 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -72,7 +72,7 @@ module Admin
def preview
@broadcast_message = BroadcastMessage.new(broadcast_message_params)
- render partial: 'admin/broadcast_messages/preview'
+ render plain: render_broadcast_message(@broadcast_message), status: :ok
end
protected
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
index fb3a86d5219..ca3be1542aa 100644
--- a/app/controllers/groups/children_controller.rb
+++ b/app/controllers/groups/children_controller.rb
@@ -5,6 +5,8 @@ module Groups
extend ::Gitlab::Utils::Override
before_action :group
+ before_action :validate_per_page
+
skip_cross_project_access_check :index
feature_category :subgroups
@@ -44,7 +46,7 @@ module Groups
@children = GroupDescendantsFinder.new(
current_user: current_user,
parent_group: parent,
- params: params.to_unsafe_h
+ params: group_descendants_params
).execute.page(params[:page])
end
@@ -54,5 +56,25 @@ module Groups
def has_project_list?
true
end
+
+ def group_descendants_params
+ @group_descendants_params ||= params.to_unsafe_h.compact
+ end
+
+ def validate_per_page
+ return unless group_descendants_params.key?(:per_page)
+
+ per_page = begin
+ Integer(group_descendants_params[:per_page])
+ rescue ArgumentError, TypeError
+ 0
+ end
+
+ respond_to do |format|
+ format.json do
+ render status: :bad_request, json: { message: 'per_page does not have a valid value' } if per_page < 1
+ end
+ end
+ end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index a5f938dc1c7..604ca7e3a1a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -10,6 +10,7 @@ class CommitStatus < Ci::ApplicationRecord
include TaggableQueries
self.table_name = 'ci_builds'
+ self.primary_key = :id
partitionable scope: :pipeline
belongs_to :user
diff --git a/app/views/admin/broadcast_messages/_preview.html.haml b/app/views/admin/broadcast_messages/_preview.html.haml
deleted file mode 100644
index 56168926a6e..00000000000
--- a/app/views/admin/broadcast_messages/_preview.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.js-broadcast-banner-message-preview
- = render "shared/broadcast_message", { message: @broadcast_message, preview: true } do
- = _('Your message here')
diff --git a/app/views/projects/commit/diff_files.html.haml b/app/views/projects/commit/diff_files.html.haml
index 0c52c1a15a4..7287d10a109 100644
--- a/app/views/projects/commit/diff_files.html.haml
+++ b/app/views/projects/commit/diff_files.html.haml
@@ -1 +1,5 @@
-= render partial: 'projects/diffs/file', collection: diffs.diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: 'is-commit' }
+- diff_files = conditionally_paginate_diff_files(diffs, paginate: true, page: params[:page], per: Projects::CommitController::COMMIT_DIFFS_PER_PAGE)
+
+= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: 'is-commit' }
+
+= paginate(diff_files, theme: "gitlab", params: { action: :show })
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 8ff6d348d95..03e26fd4456 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -3,7 +3,7 @@
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
- load_diff_files_async = Feature.enabled?(:async_commit_diff_files, @project) && diff_page_context == "is-commit"
-- paginate_diffs = local_assigns.fetch(:paginate_diffs, false) && !load_diff_files_async
+- paginate_diffs = local_assigns.fetch(:paginate_diffs, false)
- paginate_diffs_per_page = local_assigns.fetch(:paginate_diffs_per_page, nil)
- page = local_assigns.fetch(:page, nil)
- diff_files = conditionally_paginate_diff_files(diffs, paginate: paginate_diffs, page: page, per: paginate_diffs_per_page)
@@ -32,7 +32,7 @@
.files{ data: { can_create_note: can_create_note } }
- if load_diff_files_async
- - url = url_for(safe_params.merge(action: 'diff_files'))
+ - url = url_for(safe_params.merge(action: 'diff_files', page: page))
.js-diffs-batch{ data: { diff_files_path: url } }
= gl_loading_icon(size: "md", css_class: "gl-mt-4")
- else
diff --git a/config/application.rb b/config/application.rb
index 96ae34d430d..781b6e042b1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -32,7 +32,6 @@ module Gitlab
# Rails 6.1
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
ActiveSupport.utc_to_local_returns_utc_offset_times = false
- config.action_controller.urlsafe_csrf_tokens = false
config.action_view.preload_links_header = false
# Rails 5.2
diff --git a/config/feature_flags/development/runner_machine_heartbeat.yml b/config/feature_flags/development/runner_machine_heartbeat.yml
new file mode 100644
index 00000000000..6f00fa47821
--- /dev/null
+++ b/config/feature_flags/development/runner_machine_heartbeat.yml
@@ -0,0 +1,8 @@
+---
+name: runner_machine_heartbeat
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114859
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390261
+milestone: '15.10'
+type: development
+group: group::runner
+default_enabled: false
diff --git a/doc/user/application_security/dast/proxy-based.md b/doc/user/application_security/dast/proxy-based.md
index f70afac4c26..ae2d04ae4bb 100644
--- a/doc/user/application_security/dast/proxy-based.md
+++ b/doc/user/application_security/dast/proxy-based.md
@@ -80,10 +80,10 @@ To enable DAST to run automatically, either:
- Enable [Auto DAST](../../../topics/autodevops/stages.md#auto-dast) (provided
by [Auto DevOps](../../../topics/autodevops/index.md)).
-- [Edit the `.gitlab.ci.yml` file manually](#edit-the-gitlabciyml-file-manually).
-- [Use an automatically configured merge request](#configure-dast-using-the-ui).
+- [Edit the `.gitlab.ci.yml` file manually](#edit-the-gitlab-ciyml-file-manually).
+- [Configure DAST using the UI](#configure-dast-using-the-ui).
-#### Edit the `.gitlab.ci.yml` file manually
+#### Edit the `.gitlab-ci.yml` file manually
In this method you manually edit the existing `.gitlab-ci.yml` file. Use this method if your GitLab CI/CD configuration file is complex.
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index 93d5ed0aa58..6dd20ea094e 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -141,12 +141,12 @@ To enable Secret Detection, either:
- Enable [Auto DevOps](../../../topics/autodevops/index.md), which includes [Auto Secret Detection](../../../topics/autodevops/stages.md#auto-secret-detection).
-- [Edit the `.gitlab.ci.yml` file manually](#edit-the-gitlabciyml-file-manually). Use this method if
- your `.gitlab-ci.yml` file is complex.
+- [Edit the `.gitlab.ci.yml` file manually](#edit-the-gitlab-ciyml-file-manually). Use this method
+ if your `.gitlab-ci.yml` file is complex.
- [Use an automatically configured merge request](#use-an-automatically-configured-merge-request).
-### Edit the `.gitlab.ci.yml` file manually
+### Edit the `.gitlab-ci.yml` file manually
This method requires you to manually edit the existing `.gitlab-ci.yml` file. Use this method if
your GitLab CI/CD configuration file is complex.
@@ -180,9 +180,9 @@ the `.gitlab-ci.yml` file. You then merge the merge request to enable Secret Det
NOTE:
This method works best with no existing `.gitlab-ci.yml` file, or with a minimal configuration
file. If you have a complex GitLab configuration file it may not be parsed successfully, and an
-error may occur. In that case, use the [manual](#edit-the-gitlabciyml-file-manually) method instead.
+error may occur. In that case, use the [manual](#edit-the-gitlab-ciyml-file-manually) method instead.
-To enable Secret Detection automatically:
+To enable Secret Detection:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Security and Compliance > Security configuration**.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index e168a140a21..ad88d7c3ea0 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -36,6 +36,8 @@ You can create comments in places like:
- Issues
- Merge requests
- Snippets
+- Tasks
+- OKRs
Each object can have as many as 5,000 comments.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 355e069a438..430a00a839a 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -240,7 +240,7 @@ On GitLab.com, SSO is enforced:
- When SAML SSO is enabled.
- For users with an existing SAML identity when accessing groups and projects in the organization's
- group hierarchy. Users can view other groups and projects without SSO sign in.
+ group hierarchy. Users can view other groups and projects as well as their user settings without SSO sign in by using their GitLab.com credentials.
A user has a SAML identity if one or both of the following are true:
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 95cfe90943e..a61615104c1 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -200,6 +200,10 @@ On subsequent visits, new and existing users can access groups either:
For role information, see the [Group SAML](index.md#user-access-and-management) page.
+### Passwords for users created through SCIM for GitLab groups
+
+GitLab requires passwords for all user accounts. For more information on how GitLab generates passwords for users created through SCIM for GitLab groups, see [generated passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md).
+
### Link SCIM and SAML identities
If [group SAML](index.md) is configured and you have an existing GitLab.com account, users can link their SCIM and SAML
diff --git a/doc/user/okrs.md b/doc/user/okrs.md
index 7ca102402cc..7025dc916ac 100644
--- a/doc/user/okrs.md
+++ b/doc/user/okrs.md
@@ -115,6 +115,26 @@ To edit an OKR:
1. Optional. To edit the description, select the edit icon (**{pencil}**), make your changes, and
select **Save**.
+## View OKR system notes
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) to feature flag named `work_items_mvc` in GitLab 15.8. Disabled by default.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
+> - Changing activity sort order [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.8.
+> - Filtering activity [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389971) in GitLab 15.10.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project.
+
+You can view all the system notes related to the task. By default they are sorted by **Oldest first**.
+You can always change the sorting order to **Newest first**, which is remembered across sessions.
+
+## Comments and threads
+
+You can add [comments](discussions/index.md) and reply to threads in tasks.
+
## Assign users
To show who is responsible for an OKR, you can assign users to it.
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 0fc4c7571ab..eb0184da929 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -289,3 +289,23 @@ To add a task to an iteration:
The task window opens.
1. Next to **Iteration**, select **Add to iteration**.
1. From the dropdown list, select the iteration to be associated with the task.
+
+## View task system notes
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) to feature flag named `work_items_mvc` in GitLab 15.8. Disabled by default.
+> - Changing activity sort order [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.8.
+> - Filtering activity [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389971) in GitLab 15.10.
+> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project.
+
+You can view all the system notes related to the task. By default they are sorted by **Oldest first**.
+You can always change the sorting order to **Newest first**, which is remembered across sessions.
+You can also filter activity by **Comments only** and **History only** in addition to the default **All activity** which is remembered across sessions.
+
+## Comments and threads
+
+You can add [comments](discussions/index.md) and reply to threads in tasks.
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 833ce5e32fa..738c5bb3789 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -96,7 +96,7 @@ module API
# the heartbeat should be triggered.
if heartbeat_runner
job.runner&.heartbeat(get_runner_ip)
- job.runner_machine&.heartbeat(get_runner_ip)
+ job.runner_machine&.heartbeat(get_runner_ip) if Feature.enabled?(:runner_machine_heartbeat)
end
job
diff --git a/lib/gitlab/background_migration/issues_internal_id_scope_updater.rb b/lib/gitlab/background_migration/issues_internal_id_scope_updater.rb
index 485fb28405d..21ca4392003 100644
--- a/lib/gitlab/background_migration/issues_internal_id_scope_updater.rb
+++ b/lib/gitlab/background_migration/issues_internal_id_scope_updater.rb
@@ -32,16 +32,16 @@ module Gitlab
connection.execute(
<<~SQL
- DELETE FROM internal_ids WHERE id IN (#{sub_batch.select(:id).to_sql})
- SQL
+ DELETE FROM internal_ids WHERE id IN (#{sub_batch.select(:id).to_sql})
+ SQL
)
end
def create_namespace_scoped_records(sub_batch)
# Creates a corresponding namespace scoped record for every `issues` usage scoped to a project.
- # On conflict there is nothing to do as it means the record was already created when
- # a new issue is created with the newlly namespace scoped Issue model, see Issue#has_internal_id
- # definition.
+ # On conflict it means the record was already created when a new issue is created with the
+ # newly namespace scoped Issue model, see Issue#has_internal_id definition. In which case to
+ # make sure we have the namespace_id scoped record set to the greatest of the two last_values.
created_records_ids = connection.execute(
<<~SQL
INSERT INTO internal_ids (usage, last_value, namespace_id)
@@ -49,12 +49,13 @@ module Gitlab
FROM internal_ids
INNER JOIN projects ON projects.id = internal_ids.project_id
WHERE internal_ids.id IN(#{sub_batch.select(:id).to_sql})
- ON CONFLICT (usage, namespace_id) WHERE namespace_id IS NOT NULL DO NOTHING
+ ON CONFLICT (usage, namespace_id) WHERE namespace_id IS NOT NULL
+ DO UPDATE SET last_value = GREATEST(EXCLUDED.last_value, internal_ids.last_value)
RETURNING id;
- SQL
+ SQL
)
- log_info("Created internal_ids records", ids: created_records_ids.field_values('id'))
+ log_info("Created/updated internal_ids records", ids: created_records_ids.field_values('id'))
end
def log_info(message, **extra)
diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb
index 87cc0a0d3d2..e122f0b9317 100644
--- a/lib/gitlab/multi_collection_paginator.rb
+++ b/lib/gitlab/multi_collection_paginator.rb
@@ -9,6 +9,8 @@ module Gitlab
@per_page = (per_page || Kaminari.config.default_per_page).to_i
@first_collection, @second_collection = collections
+
+ raise ArgumentError, 'Page size must be at least 1' if @per_page < 1
end
def paginate(page)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9a6bef1b5ea..7617e278c6e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1986,6 +1986,9 @@ msgstr ""
msgid "Abuse reports notification email"
msgstr ""
+msgid "AbuseReports|No reports found"
+msgstr ""
+
msgid "Accept invitation"
msgstr ""
@@ -50428,9 +50431,6 @@ msgstr ""
msgid "Your membership in %{group} no longer expires."
msgstr ""
-msgid "Your message here"
-msgstr ""
-
msgid "Your name"
msgstr ""
@@ -51224,6 +51224,9 @@ msgstr ""
msgid "ciReport|There was an error reverting the dismissal. Please try again."
msgstr ""
+msgid "ciReport|There was an error reverting the dismissal: %{error}"
+msgstr ""
+
msgid "ciReport|This report contains all Code Quality issues in the source branch."
msgstr ""
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index d0656ee47ce..2e37ed95c1c 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -275,6 +275,18 @@ RSpec.describe Groups::ChildrenController, feature_category: :subgroups do
allow(Kaminari.config).to receive(:default_per_page).and_return(per_page)
end
+ it 'rejects negative per_page parameter' do
+ get :index, params: { group_id: group.to_param, per_page: -1 }, format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'rejects non-numeric per_page parameter' do
+ get :index, params: { group_id: group.to_param, per_page: 'abc' }, format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
context 'with only projects' do
let!(:other_project) { create(:project, :public, namespace: group) }
let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) }
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 8ea4f8c959f..9fe72b981f1 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -3,80 +3,209 @@
require 'spec_helper'
RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
context 'as an admin' do
- before do
- stub_feature_flags(abuse_reports_list: false)
+ describe 'displayed reports' do
+ include FilteredSearchHelpers
- admin = create(:admin)
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
- end
+ let_it_be(:open_report) { create(:abuse_report, created_at: 5.days.ago, updated_at: 2.days.ago) }
+ let_it_be(:open_report2) { create(:abuse_report, created_at: 4.days.ago, updated_at: 3.days.ago, category: 'phishing') }
+ let_it_be(:closed_report) { create(:abuse_report, :closed) }
- describe 'if a user has been reported for abuse' do
- let!(:abuse_report) { create(:abuse_report, user: user) }
+ let(:abuse_report_row_selector) { '[data-testid="abuse-report-row"]' }
- describe 'in the abuse report view' do
- it 'presents information about abuse report' do
- visit admin_abuse_reports_path
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
- expect(page).to have_content('Abuse Reports')
- expect(page).to have_content(abuse_report.message)
- expect(page).to have_link(user.name, href: user_path(user))
- expect(page).to have_link('Remove user')
- end
+ visit admin_abuse_reports_path
end
- describe 'in the profile page of the user' do
- it 'shows a link to the admin view of the user' do
- visit user_path(user)
+ it 'only includes open reports by default' do
+ expect_displayed_reports_count(2)
+
+ expect_report_shown(open_report, open_report2)
- expect(page).to have_link '', href: admin_user_path(user)
+ within '[data-testid="abuse-reports-filtered-search-bar"]' do
+ expect(page).to have_content 'Status = Open'
end
end
- end
- describe 'if a many users have been reported for abuse' do
- let(:report_count) { AbuseReport.default_per_page + 3 }
+ it 'can be filtered by status, user, reporter, and category', :aggregate_failures do
+ # filter by status
+ filter %w[Status Closed]
+ expect_displayed_reports_count(1)
+ expect_report_shown(closed_report)
+ expect_report_not_shown(open_report, open_report2)
- before do
- report_count.times do
- create(:abuse_report, user: create(:user))
+ filter %w[Status Open]
+ expect_displayed_reports_count(2)
+ expect_report_shown(open_report, open_report2)
+ expect_report_not_shown(closed_report)
+
+ # filter by user
+ filter(['User', open_report2.user.username])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report2)
+ expect_report_not_shown(open_report, closed_report)
+
+ # filter by reporter
+ filter(['Reporter', open_report.reporter.username])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report)
+ expect_report_not_shown(open_report2, closed_report)
+
+ # filter by category
+ filter(['Category', open_report2.category])
+
+ expect_displayed_reports_count(1)
+ expect_report_shown(open_report2)
+ expect_report_not_shown(open_report, closed_report)
+ end
+
+ it 'can be sorted by created_at and updated_at in desc and asc order', :aggregate_failures do
+ # created_at desc (default)
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(report_text(open_report))
+
+ # created_at asc
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(report_text(open_report))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+
+ # updated_at ascending
+ sort_by 'Updated date'
+
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(report_text(open_report))
+
+ # updated_at descending
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(report_text(open_report))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+ end
+
+ def report_rows
+ page.all(abuse_report_row_selector)
+ end
+
+ def report_text(report)
+ "#{report.user.name} reported for #{report.category}"
+ end
+
+ def expect_report_shown(*reports)
+ reports.each do |r|
+ expect(page).to have_content(report_text(r))
end
end
- describe 'in the abuse report view' do
- it 'presents information about abuse report' do
- visit admin_abuse_reports_path
+ def expect_report_not_shown(*reports)
+ reports.each do |r|
+ expect(page).not_to have_content(report_text(r))
+ end
+ end
- expect(page).to have_selector('.pagination')
- expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ def expect_displayed_reports_count(count)
+ expect(page).to have_css(abuse_report_row_selector, count: count)
+ end
+
+ def filter(tokens)
+ # remove all existing filters first
+ page.find_all('.gl-token-close').each(&:click)
+
+ select_tokens(*tokens, submit: true, input_text: 'Filter reports')
+ end
+
+ def sort_by(sort)
+ page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do
+ page.find('.gl-dropdown-toggle').click
+
+ page.within('.dropdown-menu') do
+ click_button sort
+ wait_for_requests
+ end
end
end
end
- describe 'filtering by user' do
- let!(:user2) { create(:user) }
- let!(:abuse_report) { create(:abuse_report, user: user) }
- let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+ context 'when abuse_reports_list feature flag is disabled' do
+ before do
+ stub_feature_flags(abuse_reports_list: false)
- it 'shows only single user report' do
- visit admin_abuse_reports_path
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ describe 'if a user has been reported for abuse' do
+ let!(:abuse_report) { create(:abuse_report, user: user) }
- page.within '.filter-form' do
- click_button 'User'
- wait_for_requests
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
- page.within '.dropdown-menu-user' do
- click_link user2.name
+ expect(page).to have_content('Abuse Reports')
+ expect(page).to have_content(abuse_report.message)
+ expect(page).to have_link(user.name, href: user_path(user))
+ expect(page).to have_link('Remove user')
end
+ end
+
+ describe 'in the profile page of the user' do
+ it 'shows a link to the admin view of the user' do
+ visit user_path(user)
- wait_for_requests
+ expect(page).to have_link '', href: admin_user_path(user)
+ end
end
+ end
- expect(page).to have_content(user2.name)
- expect(page).not_to have_content(user.name)
+ describe 'if a many users have been reported for abuse' do
+ let(:report_count) { AbuseReport.default_per_page + 3 }
+
+ before do
+ report_count.times do
+ create(:abuse_report, user: create(:user))
+ end
+ end
+
+ describe 'in the abuse report view' do
+ it 'presents information about abuse report' do
+ visit admin_abuse_reports_path
+
+ expect(page).to have_selector('.pagination')
+ expect(page).to have_selector('.pagination .js-pagination-page', count: (report_count.to_f / AbuseReport.default_per_page).ceil)
+ end
+ end
+ end
+
+ describe 'filtering by user' do
+ let!(:user2) { create(:user) }
+ let!(:abuse_report) { create(:abuse_report, user: user) }
+ let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+
+ it 'shows only single user report' do
+ visit admin_abuse_reports_path
+
+ page.within '.filter-form' do
+ click_button 'User'
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ wait_for_requests
+ end
+
+ expect(page).to have_content(user2.name)
+ expect(page).not_to have_content(user.name)
+ end
end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c22517538cc..d9867c2e704 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -491,6 +491,29 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
end
end
+ describe "Runner create page", :js do
+ before do
+ visit new_admin_runner_path
+ end
+
+ context 'when runner is saved' do
+ before do
+ fill_in s_('Runners|Runner description'), with: 'runner-foo'
+ fill_in s_('Runners|Tags'), with: 'tag1'
+ click_on _('Submit')
+ wait_for_requests
+ end
+
+ it 'navigates to registration page and opens install instructions drawer' do
+ expect(page.find('[data-testid="alert-success"]')).to have_content(s_('Runners|Runner created.'))
+ expect(current_url).to match(register_admin_runner_path(Ci::Runner.last))
+
+ click_on 'How do I install GitLab Runner?'
+ expect(page.find('[data-testid="runner-platforms-drawer"]')).to have_content('gitlab-runner install')
+ end
+ end
+ end
+
describe "Runner show page", :js do
let_it_be(:runner) do
create(
diff --git a/spec/features/commit_spec.rb b/spec/features/commit_spec.rb
index a3208ca6d37..dd96b763e55 100644
--- a/spec/features/commit_spec.rb
+++ b/spec/features/commit_spec.rb
@@ -16,7 +16,6 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
let(:files) { commit.diffs.diff_files.to_a }
before do
- stub_feature_flags(async_commit_diff_files: false)
project.add_maintainer(user)
sign_in(user)
end
@@ -28,15 +27,9 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
visit project_commit_path(project, commit)
end
- it "shows the short commit message" do
+ it "shows the short commit message, number of total changes and stats", :js, :aggregate_failures do
expect(page).to have_content(commit.title)
- end
-
- it "reports the correct number of total changes" do
expect(page).to have_content("Changes #{commit.diffs.size}")
- end
-
- it 'renders diff stats', :js do
expect(page).to have_selector(".diff-stats")
end
@@ -50,22 +43,24 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
visit project_commit_path(project, commit)
end
- it "shows an adjusted count for changed files on this page", :js do
- expect(page).to have_content("Showing 1 changed file")
+ def diff_files_on_page
+ page.all('.files .diff-file').pluck(:id)
end
- it "shows only the first diff on the first page" do
- expect(page).to have_selector(".files ##{files[0].file_hash}")
- expect(page).not_to have_selector(".files ##{files[1].file_hash}")
- end
+ it "shows paginated content and controls to navigate", :js, :aggregate_failures do
+ expect(page).to have_content("Showing 1 changed file")
+
+ wait_for_requests
+
+ expect(diff_files_on_page).to eq([files[0].file_hash])
- it "can navigate to the second page" do
within(".files .gl-pagination") do
click_on("2")
end
- expect(page).not_to have_selector(".files ##{files[0].file_hash}")
- expect(page).to have_selector(".files ##{files[1].file_hash}")
+ wait_for_requests
+
+ expect(diff_files_on_page).to eq([files[1].file_hash])
end
end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 1f09b01ddec..43dd80187ce 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -19,6 +19,8 @@ RSpec.describe 'Expand and collapse diffs', :js, feature_category: :source_code_
# Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch)
visit project_commit_path(project, project.commit(branch))
+
+ wait_for_requests
end
def file_container(filename)
@@ -222,10 +224,16 @@ RSpec.describe 'Expand and collapse diffs', :js, feature_category: :source_code_
let(:branch) { 'expand-collapse-files' }
# safe-files -> 100 | safe-lines -> 5000 | commit-files -> 105
- it 'does collapsing from the safe number of files to the end on small files' do
- expect(page).to have_link('Expand all')
+ it 'does collapsing from the safe number of files to the end on small files', :aggregate_failures do
+ expect(page).not_to have_link('Expand all')
+ expect(page).to have_selector('.diff-content', count: 20)
+ expect(page).to have_selector('.diff-collapsed', count: 0)
- expect(page).to have_selector('.diff-content', count: 105)
+ visit project_commit_path(project, project.commit(branch), page: 6)
+ wait_for_requests
+
+ expect(page).to have_link('Expand all')
+ expect(page).to have_selector('.diff-content', count: 5)
expect(page).to have_selector('.diff-collapsed', count: 5)
%w(file-95.txt file-96.txt file-97.txt file-98.txt file-99.txt).each do |filename|
diff --git a/spec/frontend/admin/abuse_reports/components/app_spec.js b/spec/frontend/admin/abuse_reports/components/app_spec.js
index 6af666ffe50..41728baaf33 100644
--- a/spec/frontend/admin/abuse_reports/components/app_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/app_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlPagination } from '@gitlab/ui';
+import { GlEmptyState, GlPagination } from '@gitlab/ui';
import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
import AbuseReportsApp from '~/admin/abuse_reports/components/app.vue';
@@ -11,6 +11,7 @@ describe('AbuseReportsApp', () => {
let wrapper;
const findFilteredSearchBar = () => wrapper.findComponent(AbuseReportsFilteredSearchBar);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findAbuseReportRows = () => wrapper.findAllComponents(AbuseReportRow);
const findPagination = () => wrapper.findComponent(GlPagination);
@@ -33,9 +34,19 @@ describe('AbuseReportsApp', () => {
it('renders one AbuseReportRow for each abuse report', () => {
createComponent();
+ expect(findEmptyState().exists()).toBe(false);
expect(findAbuseReportRows().length).toBe(mockAbuseReports.length);
});
+ it('renders empty state when there are no reports', () => {
+ createComponent({
+ abuseReports: [],
+ pagination: { currentPage: 1, perPage: 20, totalItems: 0 },
+ });
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
describe('pagination', () => {
const pagination = {
currentPage: 1,
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 70e0689786a..ed7b6699e2c 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -288,6 +288,19 @@ describe('Diffs Module Getters', () => {
});
});
+ describe('isTreePathLoaded', () => {
+ it.each`
+ desc | loaded | path | bool
+ ${'the file exists and has been loaded'} | ${true} | ${'path/tofile'} | ${true}
+ ${'the file exists and has not been loaded'} | ${false} | ${'path/tofile'} | ${false}
+ ${'the file does not exist'} | ${false} | ${'tofile/path'} | ${false}
+ `('returns $bool when $desc', ({ loaded, path, bool }) => {
+ localState.treeEntries['path/tofile'] = { diffLoaded: loaded };
+
+ expect(getters.isTreePathLoaded(localState)(path)).toBe(bool);
+ });
+ });
+
describe('allBlobs', () => {
it('returns an array of blobs', () => {
localState.treeEntries = {
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index 2d5557b65f1..5e5f46ff34e 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
@@ -129,9 +129,7 @@ describe('Global Search Searchable Dropdown', () => {
describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
beforeEach(() => {
createComponent({}, { frequentItems });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchText });
+ findGlDropdownSearch().vm.$emit('input', searchText);
});
it(`should${length ? '' : ' not'} render frequent dropdown items`, () => {
@@ -187,28 +185,33 @@ describe('Global Search Searchable Dropdown', () => {
});
describe('opening the dropdown', () => {
- describe('for the first time', () => {
- beforeEach(() => {
- findGlDropdown().vm.$emit('show');
- });
+ beforeEach(() => {
+ findGlDropdown().vm.$emit('show');
+ });
- it('$emits @search and @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
- });
+ it('$emits @search and @first-open on the first open', async () => {
+ expect(wrapper.emitted('search')[0]).toStrictEqual(['']);
+ expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
});
- describe('not for the first time', () => {
- beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasBeenOpened: true });
- findGlDropdown().vm.$emit('show');
+ describe('when the dropdown has been opened', () => {
+ it('$emits @search with the searchText', async () => {
+ const searchText = 'foo';
+
+ findGlDropdownSearch().vm.$emit('input', searchText);
+ await nextTick();
+
+ expect(wrapper.emitted('search')[1]).toStrictEqual([searchText]);
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
- it('$emits @search and not @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')).toBeUndefined();
+ it('does not emit @first-open again', async () => {
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
+
+ findGlDropdownSearch().vm.$emit('input');
+ await nextTick();
+
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 171ebbe8279..43ce9b77cd3 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -1,5 +1,5 @@
import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -21,6 +21,7 @@ import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget/constants';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
@@ -62,6 +63,8 @@ jest.mock('@sentry/browser', () => ({
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
+ let stateQueryHandler;
+ let queryResponse;
let wrapper;
let mock;
@@ -91,32 +94,36 @@ describe('MrWidgetOptions', () => {
gon.features = {};
});
- const createComponent = (mrData = mockData, options = {}) => {
- wrapper = mount(MrWidgetOptions, {
+ const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
+ const mounting = fullMount ? mount : shallowMount;
+
+ queryResponse = {
+ data: {
+ project: {
+ ...getStateQueryResponse.data.project,
+ mergeRequest: {
+ ...getStateQueryResponse.data.project.mergeRequest,
+ mergeError: mrData.mergeError || null,
+ },
+ },
+ },
+ };
+ stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
+ wrapper = mounting(MrWidgetOptions, {
propsData: {
mrData: { ...mrData },
},
data() {
- return { loading: false };
+ return {
+ loading: false,
+ ...data,
+ };
},
...options,
apolloProvider: createMockApollo([
[approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
- [
- getStateQuery,
- jest.fn().mockResolvedValue({
- data: {
- project: {
- ...getStateQueryResponse.data.project,
- mergeRequest: {
- ...getStateQueryResponse.data.project.mergeRequest,
- mergeError: mrData.mergeError || null,
- },
- },
- },
- }),
- ],
+ [getStateQuery, stateQueryHandler],
[readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)],
[
userPermissionsQuery,
@@ -354,18 +361,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('initPolling', () => {
- it('should call SmartInterval', () => {
- wrapper.vm.initPolling();
-
- expect(SmartInterval).toHaveBeenCalledWith(
- expect.objectContaining({
- callback: wrapper.vm.checkStatus,
- }),
- );
- });
- });
-
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
wrapper.vm.initDeploymentsPolling();
@@ -532,23 +527,64 @@ describe('MrWidgetOptions', () => {
});
});
- describe('resumePolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'resume').mockImplementation(() => {});
+ describe('Apollo query', () => {
+ const interval = 5;
+ const data = 'foo';
+ const mockCheckStatus = jest.fn().mockResolvedValue({ data });
+ const mockSetGraphqlData = jest.fn();
+ const mockSetData = jest.fn();
- wrapper.vm.resumePolling();
+ beforeEach(() => {
+ wrapper.destroy();
+
+ return createComponent(
+ mockData,
+ {},
+ {
+ pollInterval: interval,
+ startingPollInterval: interval,
+ mr: {
+ setData: mockSetData,
+ setGraphqlData: mockSetGraphqlData,
+ },
+ service: {
+ checkStatus: mockCheckStatus,
+ },
+ },
+ false,
+ );
+ });
- expect(wrapper.vm.pollingInterval.resume).toHaveBeenCalled();
+ describe('normal polling behavior', () => {
+ it('responds to the GraphQL query finishing', () => {
+ expect(mockSetGraphqlData).toHaveBeenCalledWith(queryResponse.data.project);
+ expect(mockCheckStatus).toHaveBeenCalled();
+ expect(mockSetData).toHaveBeenCalledWith(data, undefined);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+ });
});
- });
- describe('stopPolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'stopTimer').mockImplementation(() => {});
+ describe('external event control', () => {
+ describe('enablePolling', () => {
+ it('enables the Apollo query polling using the event hub', () => {
+ eventHub.$emit('EnablePolling');
- wrapper.vm.stopPolling();
+ expect(stateQueryHandler).toHaveBeenCalled();
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+ });
+ });
- expect(wrapper.vm.pollingInterval.stopTimer).toHaveBeenCalled();
+ describe('disablePolling', () => {
+ it('disables the Apollo query polling using the event hub', () => {
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+
+ eventHub.$emit('DisablePolling');
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
+
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1); // no additional polling after a real interval timeout
+ });
+ });
});
});
});
@@ -893,11 +929,7 @@ describe('MrWidgetOptions', () => {
});
describe('mock extension', () => {
- let pollRequest;
-
beforeEach(() => {
- pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
-
registerExtension(workingExtension());
createComponent();
@@ -948,10 +980,6 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.findComponent(GlButton).exists()).toBe(true);
expect(collapsedSection.findComponent(GlButton).text()).toBe('Full report');
});
-
- it('extension polling is not called if enablePolling flag is not passed', () => {
- expect(pollRequest).toHaveBeenCalledTimes(0);
- });
});
describe('expansion', () => {
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 4f7ae4059ee..1bdf5d1c840 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -90,7 +90,6 @@ describe('WorkItemDetailModal component', () => {
workItemId: defaultPropsData.workItemId,
workItemParentId: defaultPropsData.issueGid,
workItemIid: null,
- modal: null,
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index d20e5f8d1b3..fe7556f8ec6 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -99,7 +99,6 @@ describe('WorkItemDetail component', () => {
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
- workItemsMvcEnabled = false,
workItemsMvc2Enabled = false,
} = {}) => {
const handlers = [
@@ -123,7 +122,6 @@ describe('WorkItemDetail component', () => {
},
provide: {
glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
workItemsMvc2: workItemsMvc2Enabled,
},
hasIssueWeightsFeature: true,
@@ -746,21 +744,10 @@ describe('WorkItemDetail component', () => {
});
describe('notes widget', () => {
- it('does not render notes by default', async () => {
+ it('renders notes by default', async () => {
createComponent();
await waitForPromises();
- expect(findNotesWidget().exists()).toBe(false);
- });
-
- it('renders notes when the work_items_mvc flag is on', async () => {
- const notesWorkItem = workItemResponseFactory({
- notesWidgetPresent: true,
- });
- const handler = jest.fn().mockResolvedValue(notesWorkItem);
- createComponent({ workItemsMvcEnabled: true, handler });
- await waitForPromises();
-
expect(findNotesWidget().exists()).toBe(true);
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index ca181c346b5..721436e217e 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -111,6 +111,24 @@ describe('WorkItemLinkChild', () => {
expect(titleEl.text()).toBe(workItemTask.title);
});
+ describe('renders item title correctly for relative instance', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/test' };
+ createComponent();
+ titleEl = wrapper.findByTestId('item-title');
+ });
+
+ it('renders item title with correct href', () => {
+ expect(titleEl.attributes('href')).toBe(
+ '/test/gitlab-org/gitlab-test/-/work_items/4?iid_path=true',
+ );
+ });
+
+ it('renders item title with correct text', () => {
+ expect(titleEl.text()).toBe(workItemTask.title);
+ });
+ });
+
it.each`
action | event | emittedEvent
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
@@ -147,6 +165,8 @@ describe('WorkItemLinkChild', () => {
expect(metadataEl.props()).toMatchObject({
metadataWidgets: workItemObjectiveMetadataWidgets,
});
+
+ expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-3');
});
it('does not render item metadata component when item has no metadata present', () => {
@@ -156,6 +176,8 @@ describe('WorkItemLinkChild', () => {
});
expect(findMetadataComponent().exists()).toBe(false);
+
+ expect(wrapper.find('[data-testid="links-child"]').classes()).toContain('gl-py-0');
});
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 39b5e523667..37326910e13 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -52,7 +52,6 @@ describe('Work items root component', () => {
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
workItemIid: '1',
- modal: null,
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 8ebf76d40c8..5dad7f7c43f 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -75,6 +75,7 @@ describe('Work items router', () => {
WorkItemWeight: true,
WorkItemIteration: true,
WorkItemHealthStatus: true,
+ WorkItemNotes: true,
},
});
};
diff --git a/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb b/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb
index 8830af52730..1adff322b41 100644
--- a/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb
+++ b/spec/lib/gitlab/background_migration/issues_internal_id_scope_updater_spec.rb
@@ -2,6 +2,7 @@
require 'spec_helper'
# this needs the schema to be before we introduce the not null constraint on routes#namespace_id
+# rubocop:disable RSpec/MultipleMemoizedHelpers
RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, feature_category: :team_planning do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
@@ -15,6 +16,7 @@ RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, featur
let(:pr_nmsp3) { namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: gr2.id) }
let(:pr_nmsp4) { namespaces.create!(name: 'proj4', path: 'proj4', type: 'Project', parent_id: gr2.id) }
let(:pr_nmsp5) { namespaces.create!(name: 'proj5', path: 'proj5', type: 'Project', parent_id: gr2.id) }
+ let(:pr_nmsp6) { namespaces.create!(name: 'proj6', path: 'proj6', type: 'Project', parent_id: gr2.id) }
# rubocop:disable Layout/LineLength
let(:p1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: gr1.id, project_namespace_id: pr_nmsp1.id) }
@@ -22,6 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, featur
let(:p3) { projects.create!(name: 'proj3', path: 'proj3', namespace_id: gr2.id, project_namespace_id: pr_nmsp3.id) }
let(:p4) { projects.create!(name: 'proj4', path: 'proj4', namespace_id: gr2.id, project_namespace_id: pr_nmsp4.id) }
let(:p5) { projects.create!(name: 'proj5', path: 'proj5', namespace_id: gr2.id, project_namespace_id: pr_nmsp5.id) }
+ let(:p6) { projects.create!(name: 'proj6', path: 'proj6', namespace_id: gr2.id, project_namespace_id: pr_nmsp6.id) }
# rubocop:enable Layout/LineLength
# a project that already is covered by a record for its namespace. This should result in no new record added and
@@ -44,6 +47,11 @@ RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, featur
# a record scoped to a group, should not affect anything.
let!(:issues_internal_ids_gr1) { internal_ids.create!(namespace_id: gr1.id, usage: 0, last_value: 600) }
+ # a project that is covered by a record for its namespace, but has a higher last_value, due to updates during rolling
+ # deploy for instance, see https://gitlab.com/gitlab-com/gl-infra/production/-/issues/8548
+ let!(:issues_internal_ids_p6) { internal_ids.create!(project_id: p6.id, usage: 0, last_value: 111) }
+ let!(:issues_internal_ids_pr_nmsp6) { internal_ids.create!(namespace_id: pr_nmsp6.id, usage: 0, last_value: 100) }
+
subject(:perform_migration) do
described_class.new(
start_id: internal_ids.minimum(:id),
@@ -59,15 +67,24 @@ RSpec.describe Gitlab::BackgroundMigration::IssuesInternalIdScopeUpdater, featur
it 'backfills internal_ids records and removes related project records', :aggregate_failures do
perform_migration
- expected_recs = [pr_nmsp1.id, pr_nmsp2.id, pr_nmsp3.id, pr_nmsp5.id, gr1.id]
+ expected_recs = [pr_nmsp1.id, pr_nmsp2.id, pr_nmsp3.id, pr_nmsp5.id, gr1.id, pr_nmsp6.id]
# all namespace scoped records for issues(0) usage
- expect(internal_ids.where.not(namespace_id: nil).where(usage: 0).count).to eq(5)
+ expect(internal_ids.where.not(namespace_id: nil).where(usage: 0).count).to eq(6)
# all namespace_ids for issues(0) usage
expect(internal_ids.where.not(namespace_id: nil).where(usage: 0).pluck(:namespace_id)).to match_array(expected_recs)
# this is the record with usage: 4
expect(internal_ids.where.not(project_id: nil).count).to eq(1)
# no project scoped records for issues usage left
expect(internal_ids.where.not(project_id: nil).where(usage: 0).count).to eq(0)
+
+ # the case when the project_id scoped record had the higher last_value,
+ # see `issues_internal_ids_p6` and issues_internal_ids_pr_nmsp6 definitions above
+ expect(internal_ids.where(namespace_id: pr_nmsp6.id).first.last_value).to eq(111)
+
+ # the case when the namespace_id scoped record had the higher last_value,
+ # see `issues_internal_ids_p1` and issues_internal_ids_pr_nmsp1 definitions above.
+ expect(internal_ids.where(namespace_id: pr_nmsp1.id).first.last_value).to eq(111)
end
end
+# rubocop:enable RSpec/MultipleMemoizedHelpers
diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb
index 080b3382684..25baa8913bf 100644
--- a/spec/lib/gitlab/multi_collection_paginator_spec.rb
+++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb
@@ -5,6 +5,13 @@ require 'spec_helper'
RSpec.describe Gitlab::MultiCollectionPaginator do
subject(:paginator) { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 3) }
+ it 'raises an error for invalid page size' do
+ expect { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: 0) }
+ .to raise_error(ArgumentError)
+ expect { described_class.new(Project.all.order(:id), Group.all.order(:id), per_page: -1) }
+ .to raise_error(ArgumentError)
+ end
+
it 'combines both collections' do
project = create(:project)
group = create(:group)
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2270fd6f5a9..6b2e33c4f83 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -33,6 +33,7 @@ RSpec.describe CommitStatus do
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
it { is_expected.not_to be_retried }
+ it { expect(described_class.primary_key).to eq('id') }
describe '#author' do
subject { commit_status.author }
diff --git a/spec/requests/admin/broadcast_messages_controller_spec.rb b/spec/requests/admin/broadcast_messages_controller_spec.rb
index 69b84d6d795..0143c9ce030 100644
--- a/spec/requests/admin/broadcast_messages_controller_spec.rb
+++ b/spec/requests/admin/broadcast_messages_controller_spec.rb
@@ -8,6 +8,7 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
let_it_be(:invalid_broadcast_message) { { broadcast_message: { message: '' } } }
let_it_be(:test_message) { 'you owe me a new acorn' }
+ let_it_be(:test_preview) { '<p>Hello, world!</p>' }
before do
sign_in(create(:admin))
@@ -23,11 +24,11 @@ RSpec.describe Admin::BroadcastMessagesController, :enable_admin_mode, feature_c
end
describe 'POST /preview' do
- it 'renders preview partial' do
+ it 'renders preview html' do
post preview_admin_broadcast_messages_path, params: { broadcast_message: { message: "Hello, world!" } }
expect(response).to have_gitlab_http_status(:ok)
- expect(response.body).to render_template(:_preview)
+ expect(response.body).to eq(test_preview)
end
end
diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb
index ef3b38e3fc4..bf28b25e0a6 100644
--- a/spec/requests/api/ci/runner/jobs_put_spec.rb
+++ b/spec/requests/api/ci/runner/jobs_put_spec.rb
@@ -43,6 +43,17 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
.and change { runner_machine.reload.contacted_at }
end
+ context 'when runner_machine_heartbeat is disabled' do
+ before do
+ stub_feature_flags(runner_machine_heartbeat: false)
+ end
+
+ it 'does not load runner machine' do
+ queries = ActiveRecord::QueryRecorder.new { update_job(state: 'success') }
+ expect(queries.log).not_to include(/ci_runner_machines/)
+ end
+ end
+
context 'when status is given' do
it 'marks job as succeeded' do
update_job(state: 'success')
diff --git a/spec/services/ci/unlock_artifacts_service_spec.rb b/spec/services/ci/unlock_artifacts_service_spec.rb
index 2746cfdbcd0..1921ea4bdba 100644
--- a/spec/services/ci/unlock_artifacts_service_spec.rb
+++ b/spec/services/ci/unlock_artifacts_service_spec.rb
@@ -201,8 +201,7 @@ RSpec.describe Ci::UnlockArtifactsService, feature_category: :continuous_integra
describe '#unlock_job_artifacts_query' do
subject { described_class.new(pipeline.project, pipeline.user).unlock_job_artifacts_query(pipeline_ids) }
- context 'when running on a ref before a pipeline' do
- let(:before_pipeline) { pipeline }
+ context 'when given a single pipeline ID' do
let(:pipeline_ids) { [older_pipeline.id] }
it 'produces the expected SQL string' do
@@ -226,8 +225,7 @@ RSpec.describe Ci::UnlockArtifactsService, feature_category: :continuous_integra
end
end
- context 'when running on just the ref' do
- let(:before_pipeline) { nil }
+ context 'when given multiple pipeline IDs' do
let(:pipeline_ids) { [older_pipeline.id, newer_pipeline.id, pipeline.id] }
it 'produces the expected SQL string' do
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 677cea7b804..b07f5dcf2e1 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -190,9 +190,9 @@ module FilteredSearchHelpers
##
# For use with gl-filtered-search
- def select_tokens(*args, submit: false)
+ def select_tokens(*args, submit: false, input_text: 'Search')
within '[data-testid="filtered-search-input"]' do
- find_field('Search').click
+ find_field(input_text).click
args.each do |token|
# Move mouse away to prevent invoking tooltips on usernames, which blocks the search input
@@ -230,6 +230,13 @@ module FilteredSearchHelpers
find('.gl-filtered-search-token-segment', text: value).click
end
+ def toggle_sort_direction
+ page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do
+ page.find("button[title^='Sort direction']").click
+ wait_for_requests
+ end
+ end
+
def expect_visible_suggestions_list
expect(page).to have_css('.gl-filtered-search-suggestion-list')
end
diff --git a/workhorse/go.mod b/workhorse/go.mod
index 28084e0b4b4..3553b197cd2 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -36,7 +36,7 @@ require (
golang.org/x/oauth2 v0.5.0
golang.org/x/tools v0.6.0
google.golang.org/grpc v1.53.0
- google.golang.org/protobuf v1.28.1
+ google.golang.org/protobuf v1.29.0
honnef.co/go/tools v0.3.3
)
diff --git a/workhorse/go.sum b/workhorse/go.sum
index a35e468037e..9fb8ea1524e 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -2844,8 +2844,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0=
+google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 h1:DkD0plWEVUB8v/Ru6kRBW30Hy/fRNBC8hPdcExuBZMc=
gopkg.in/DataDog/dd-trace-go.v1 v1.32.0/go.mod h1:wRKMf/tRASHwH/UOfPQ3IQmVFhTz2/1a1/mpXoIjF54=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=