diff options
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= |