diff options
160 files changed, 1162 insertions, 1625 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 46258bf6b7b..0face29d47d 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -279,7 +279,7 @@ Dangerfile /ee/app/assets/javascripts/vue_shared/components/customizable_dashboard/panels_base.vue /ee/app/assets/javascripts/product_analytics/ -^[Analytics Instrumentation] @gitlab-org/analytics-section/product-intelligence/engineers +^[Analytics Instrumentation] @gitlab-org/analytics-section/analytics-instrumentation/engineers /ee/lib/gitlab/usage_data_counters/ /ee/lib/ee/gitlab/usage_data.rb /lib/gitlab/usage_data.rb diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 1576ddaee7d..44942fe8112 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -6,7 +6,7 @@ workflow: include: - project: gitlab-org/quality/pipeline-common - ref: 5.2.2 + ref: 5.3.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index bbf8493b12a..ccbdfd3c298 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -1,10 +1,5 @@ include: - - project: gitlab-org/quality/pipeline-common - ref: 5.1.1 - file: - - /ci/base.gitlab-ci.yml - - /ci/allure-report.yml - - /ci/knapsack-report.yml + - local: .gitlab/ci/qa-common/main.gitlab-ci.yml - template: Verify/Browser-Performance.gitlab-ci.yml .test-variables: @@ -21,7 +16,7 @@ include: .bundle-base: extends: - .qa-cache - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}:bundler-2.3 + - .ruby-image before_script: - cd qa && bundle install @@ -75,8 +70,6 @@ review-qa-smoke: - .rules:qa-smoke variables: QA_SCENARIO: Test::Instance::Smoke - QA_RUN_TYPE: review-qa-smoke - retry: 1 review-qa-blocking: extends: @@ -84,7 +77,6 @@ review-qa-blocking: - .rules:qa-blocking variables: QA_SCENARIO: Test::Instance::ReviewBlocking - QA_RUN_TYPE: review-qa-blocking retry: 1 review-qa-blocking-parallel: extends: @@ -98,7 +90,6 @@ review-qa-non-blocking: - .rules:qa-non-blocking variables: QA_SCENARIO: Test::Instance::ReviewNonBlocking - QA_RUN_TYPE: review-qa-non-blocking when: manual allow_failure: true review-qa-non-blocking-parallel: @@ -118,18 +109,12 @@ browser_performance: e2e-test-report: extends: - - .generate-allure-report-base + - .e2e-test-report - .rules:prepare-report stage: post-qa variables: - ALLURE_JOB_NAME: e2e-review-qa - ALLURE_PROJECT_PATH: $CI_PROJECT_PATH ALLURE_RESULTS_GLOB: qa/tmp/allure-results - ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID - GITLAB_AUTH_TOKEN: $PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE - GIT_STRATEGY: none - allow_failure: true - when: always + ALLURE_JOB_NAME: $QA_RUN_TYPE upload-knapsack-report: extends: @@ -145,38 +130,29 @@ delete-test-resources: - .rules:prepare-report stage: post-qa variables: - QA_TEST_RESOURCES_FILE_PATTERN: $CI_PROJECT_DIR/qa/tmp/test-resources-*.json GITLAB_QA_ACCESS_TOKEN: $REVIEW_APPS_ROOT_TOKEN script: - export GITLAB_ADDRESS="$QA_GITLAB_URL" - - bundle exec rake "test_resources:delete[$QA_TEST_RESOURCES_FILE_PATTERN]" + - bundle exec rake "test_resources:delete[$CI_PROJECT_DIR/qa/tmp/test-resources-*.json]" allow_failure: true - when: always notify-slack: extends: - - .notify-slack-qa - - .qa-cache + - .notify-slack - .rules:main-run stage: post-qa variables: + QA_RSPEC_XML_FILE_PATTERN: ${CI_PROJECT_DIR}/qa/tmp/rspec-*.xml RUN_WITH_BUNDLE: "true" - QA_PATH: qa - ALLURE_JOB_NAME: e2e-review-qa - SLACK_ICON_EMOJI: ci_failing - STATUS_SYM: ☠️ - STATUS: failed - TYPE: "(review-app) " + ALLURE_JOB_NAME: $QA_RUN_TYPE when: on_failure - script: - - bundle exec prepare-stage-reports --input-files "${CI_PROJECT_DIR}/qa/tmp/rspec-*.xml" - - !reference [.notify-slack-qa, script] export-test-metrics: extends: + - .export-test-metrics - .bundle-base - .rules:main-run stage: post-qa + variables: + QA_METRICS_REPORT_FILE_PATTERN: tmp/test-metrics-*.json when: always - script: - - bundle exec rake "ci:export_test_metrics[tmp/test-metrics-*.json]" diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 9d1d86b2696..c3f14011c5b 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -101,36 +101,33 @@ start-review-app-pipeline: SCHEDULE_TYPE: $SCHEDULE_TYPE DAST_RUN: $DAST_RUN SKIP_MESSAGE: Skipping review-app due to mr containing only quarantine changes! + QA_RUN_TYPE: e2e-review-qa trigger: strategy: depend include: - artifact: review-app-pipeline.yml job: e2e-test-pipeline-generate +include: + - project: gitlab-org/quality/pipeline-common + ref: 5.2.2 + file: + - /ci/danger-review.yml + danger-review: extends: - .default-retry - .ruby-node-cache - .review:rules:danger - stage: test - needs: [] + image: "${DEFAULT_CI_IMAGE}" before_script: - source scripts/utils.sh - bundle_install_script "--with danger" - yarn_install_script - script: - # ${DANGER_DANGERFILE} is used by Jihulab for customizing danger support: https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/.gitlab-ci.yml - - > - if [ -z "$DANGER_GITLAB_API_TOKEN" ]; then - run_timed_command danger_as_local - else - danger_id=$(echo -n ${DANGER_GITLAB_API_TOKEN} | md5sum | awk '{print $1}' | cut -c5-10) - run_timed_command "bundle exec danger --fail-on-errors=true --verbose --danger_id=\"${danger_id}\" --dangerfile=\"${DANGER_DANGERFILE:-Dangerfile}\"" - fi danger-review-local: - extends: - - danger-review - - .review:rules:danger-local - script: - - run_timed_command danger_as_local + extends: danger-review + before_script: + - !reference ["danger-review", "before_script"] + # We unset DANGER_GITLAB_API_TOKEN so that Danger will run as local from `danger-review:script` + - unset DANGER_GITLAB_API_TOKEN diff --git a/.gitlab/issue_templates/Service Ping reporting and monitoring.md b/.gitlab/issue_templates/Service Ping reporting and monitoring.md index baa384a8aa2..d8d93ee73c4 100644 --- a/.gitlab/issue_templates/Service Ping reporting and monitoring.md +++ b/.gitlab/issue_templates/Service Ping reporting and monitoring.md @@ -1,6 +1,6 @@ -<!-- This issue template is used by https://about.gitlab.com/handbook/engineering/development/analytics-section/product-intelligence/ for tracking effort around Service Ping reporting for GitLab.com --> +<!-- This issue template is used by https://about.gitlab.com/handbook/engineering/development/analytics-section/analytics-instrumentation/ for tracking effort around Service Ping reporting for GitLab.com --> -The [Product Intelligence group](https://about.gitlab.com/handbook/engineering/development/analytics/product-intelligence/) runs manual reporting of ServicePing for GitLab.com on a weekly basis. This issue: +The [Analytics Instrumentation group](https://about.gitlab.com/handbook/engineering/development/analytics/analytics-instrumentation/) runs manual reporting of ServicePing for GitLab.com on a weekly basis. This issue: - Captures the work required to complete the reporting process,. - Captures the follow-up tasks that are focused on metrics performance verification. @@ -123,7 +123,7 @@ If you get mentioned, check the failing metric and open an optimization issue. <!-- Do not edit below this line --> /confidential -/label ~"group::product intelligence" ~"devops::analytics" ~backend ~"section::analytics" ~"Category:Service Ping" +/label ~"group::analytics instrumentation" ~"devops::analytics" ~backend ~"section::analytics" ~"Category:Service Ping" /epic https://gitlab.com/groups/gitlab-org/-/epics/6000 /weight 5 /title Monitor and Generate GitLab.com Service Ping diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index b74cb450320..765de6db997 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -528,18 +528,6 @@ Layout/ArgumentAlignment: - 'app/models/pages_domain_acme_order.rb' - 'app/models/project.rb' - 'app/models/project_feature.rb' - - 'app/models/project_import_data.rb' - - 'app/models/project_team.rb' - - 'app/models/prometheus_alert.rb' - - 'app/models/release.rb' - - 'app/models/releases/source.rb' - - 'app/models/remote_mirror.rb' - - 'app/models/repository.rb' - - 'app/models/resource_timebox_event.rb' - - 'app/models/service_desk_setting.rb' - - 'app/models/terraform/state.rb' - - 'app/models/time_tracking/timelog_category.rb' - - 'app/models/u2f_registration.rb' - 'app/services/compare_service.rb' - 'app/services/concerns/rate_limited_service.rb' - 'app/services/design_management/copy_design_collection/copy_service.rb' diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index a3eb5a03fa6..0edfba1c6f4 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -14.20.0 +14.21.0 @@ -362,7 +362,7 @@ gem 'snowplow-tracker', '~> 0.8.0' # Metrics gem 'webrick', '~> 1.8.1', require: false -gem 'prometheus-client-mmap', '~> 0.23', require: 'prometheus/client' +gem 'prometheus-client-mmap', '~> 0.24', require: 'prometheus/client' gem 'warning', '~> 1.3.0' diff --git a/Gemfile.checksum b/Gemfile.checksum index 63e4d83f50c..f6f50484232 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -452,11 +452,11 @@ {"name":"premailer","version":"1.16.0","platform":"ruby","checksum":"03e4402c448e6bae13fb5f6301a8bde4f3508e1bff90ae7c0972c7be94694786"}, {"name":"premailer-rails","version":"1.10.3","platform":"ruby","checksum":"7cdcb97027866f7a81c490c6d15ada7f39666b5f6375f0821b7e97e0483b112f"}, {"name":"proc_to_ast","version":"0.1.0","platform":"ruby","checksum":"92a73fa66e2250a83f8589f818b0751bcf227c68f85916202df7af85082f8691"}, -{"name":"prometheus-client-mmap","version":"0.23.1","platform":"aarch64-linux","checksum":"4091121090d1d44747b3d09f2dbd5fdd61e274d557b8ed98b06c65cdd006d174"}, -{"name":"prometheus-client-mmap","version":"0.23.1","platform":"arm64-darwin","checksum":"fa54f230631852392b38cba1ad396c0472cb9f088eef563d0c381b19b1333855"}, -{"name":"prometheus-client-mmap","version":"0.23.1","platform":"ruby","checksum":"48545f23217a5e85ca79fa8c2563711e319debdae46ddbd6348ff37f48029c40"}, -{"name":"prometheus-client-mmap","version":"0.23.1","platform":"x86_64-darwin","checksum":"99b56f4017f0a1a062914da253c613b9957bfabf5b38af5012e3d8515ed49555"}, -{"name":"prometheus-client-mmap","version":"0.23.1","platform":"x86_64-linux","checksum":"624da747dbb97e0d88be1f2ba5ae5253941fc85dea875845f5b4c7a2c95ee032"}, +{"name":"prometheus-client-mmap","version":"0.24.3","platform":"aarch64-linux","checksum":"e9e05922724a1caa2788dd32567ec9e3e95607fb32f8eb95bb95884061a3042e"}, +{"name":"prometheus-client-mmap","version":"0.24.3","platform":"arm64-darwin","checksum":"6b618db676f7286de80c1ffdc52d931fc474e470c34d652e38d74b46953955b8"}, +{"name":"prometheus-client-mmap","version":"0.24.3","platform":"ruby","checksum":"014542ebeb7b9187427c0d431fe5bcab9b07d293a25e61182a92e79cc95dde34"}, +{"name":"prometheus-client-mmap","version":"0.24.3","platform":"x86_64-darwin","checksum":"4fde644acbd01e149edda9c47746dca0dc9610951fc793a22989a069670cfe5c"}, +{"name":"prometheus-client-mmap","version":"0.24.3","platform":"x86_64-linux","checksum":"981dbaadfccb8652feac7fce7a129464274140dbb7603f5e5060b8536387eaa1"}, {"name":"pry","version":"0.14.2","platform":"java","checksum":"fd780670977ba04ff7ee32dabd4d02fe4bf02e977afe8809832d5dca1412862e"}, {"name":"pry","version":"0.14.2","platform":"ruby","checksum":"c4fe54efedaca1d351280b45b8849af363184696fcac1c72e0415f9bdac4334d"}, {"name":"pry-byebug","version":"3.10.1","platform":"ruby","checksum":"c8f975c32255bfdb29e151f5532130be64ff3d0042dc858d0907e849125581f8"}, diff --git a/Gemfile.lock b/Gemfile.lock index 857f0fe3a03..fe45768cdf9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1145,7 +1145,7 @@ GEM coderay parser unparser - prometheus-client-mmap (0.23.1) + prometheus-client-mmap (0.24.3) rb_sys (~> 0.9) pry (0.14.2) coderay (~> 1.1) @@ -1859,7 +1859,7 @@ DEPENDENCIES pg_query (~> 2.2, >= 2.2.1) png_quantizator (~> 0.2.1) premailer-rails (~> 1.10.3) - prometheus-client-mmap (~> 0.23) + prometheus-client-mmap (~> 0.24) pry-byebug pry-rails (~> 0.3.9) pry-shell (~> 0.6.1) diff --git a/app/assets/javascripts/blob/components/table_contents.vue b/app/assets/javascripts/blob/components/table_contents.vue index 28e81b83713..ee8bd23f844 100644 --- a/app/assets/javascripts/blob/components/table_contents.vue +++ b/app/assets/javascripts/blob/components/table_contents.vue @@ -42,9 +42,6 @@ export default { } }, methods: { - close() { - this.$refs.disclosureDropdown?.close(); - }, generateHeaders() { const BASE_PADDING = 16; const headers = [...this.blobViewer.querySelectorAll('h1,h2,h3,h4,h5,h6')]; @@ -72,10 +69,8 @@ export default { <template> <gl-disclosure-dropdown v-if="!isHidden && items.length" - ref="disclosureDropdown" icon="list-bulleted" class="gl-mr-2" :items="items" - @action="close" /> </template> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index 1b711feb686..8e8d397549e 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -258,9 +258,6 @@ export default { }, methods: { ...mapActions(['updateList', 'setActiveId', 'toggleListCollapsed']), - closeListActions() { - this.$refs.headerListActions?.close(); - }, openSidebarSettings() { if (this.activeId === inactiveId) { sidebarEventHub.$emit('sidebar.closeAll'); @@ -277,8 +274,6 @@ export default { } this.track('click_button', { label: 'list_settings' }); - - this.closeListActions(); }, showScopedLabels(label) { return this.scopedLabelsAvailable && isScopedLabel(label); @@ -292,13 +287,9 @@ export default { } else { eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`); } - - this.closeListActions(); }, showNewEpicForm() { eventHub.$emit(`${toggleFormEventPrefix.epic}${this.list.id}`); - - this.closeListActions(); }, toggleExpanded() { const collapsed = !this.list.collapsed; @@ -532,7 +523,6 @@ export default { </div> <gl-disclosure-dropdown v-if="showListHeaderActions" - ref="headerListActions" v-gl-tooltip.hover.top="{ title: $options.i18n.listActions, boundary: 'viewport', diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue index 365e0384d87..75850cbb108 100644 --- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue +++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue @@ -58,8 +58,6 @@ export default { selectAgent(agent) { this.$emit('agentSelected', agent); this.selectedAgent = agent; - - this.$refs.dropdown.closeAndFocus(); }, onKeyEnter() { if (!this.searchTerm?.length) { @@ -76,7 +74,6 @@ export default { <template> <div @keydown.enter.stop.prevent="onKeyEnter"> <gl-collapsible-listbox - ref="dropdown" v-model="selectedAgent" class="gl-w-full" toggle-class="select-agent-dropdown" diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue index 4fd90130284..7474f8f3298 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue @@ -105,7 +105,7 @@ export default { */ this.$nextTick(() => { if (!this.noteUpdateDirty) { - this.autosaveDiscussion.reset(); + this.autosaveDiscussion?.reset(); } }); }, diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 64a7c047cb4..596381f1de4 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -187,6 +187,9 @@ export default { 'file.file_hash': { handler: function hashChangeWatch(newHash, oldHash) { if ( + this.viewDiffsFileByFile && + !this.isCollapsed && + !this.glFeatures.singleFileFileByFile && newHash && oldHash && !this.hasDiff && diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index 43ba527dad8..9ddf5b51c9a 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -240,6 +240,7 @@ export default { :show-suggest-popover="showSuggestPopover" :save-button-title="__('Comment')" :autosave-key="autosaveKey" + :autofocus="false" class="diff-comment-form gl-mt-3" @handleFormUpdateAddToReview="addToReview" @cancelForm="handleCancelCommentForm" diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 30c34e9a5be..9fd816b971b 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -461,6 +461,26 @@ export const setParallelDiffViewType = ({ commit }) => { export const showCommentForm = ({ commit }, { lineCode, fileHash }) => { commit(types.TOGGLE_LINE_HAS_FORM, { lineCode, fileHash, hasForm: true }); + + // The comment form for diffs gets focussed differently due to the way the virtual scroller + // works. If we focus the comment form on mount and the comment form gets removed and then + // added again the page will scroll in unexpected ways + setTimeout(() => { + const el = document.querySelector(`[data-line-code="${lineCode}"] textarea`); + + if (!el) return; + + const { bottom } = el.getBoundingClientRect(); + const overflowBottom = bottom - window.innerHeight; + + // Prevent the browser scrolling for us + // We handle the scrolling to not break the diffs virtual scroller + el.focus({ preventScroll: true }); + + if (overflowBottom > 0) { + window.scrollBy(0, Math.floor(Math.abs(overflowBottom)) + 150); + } + }); }; export const cancelCommentForm = ({ commit }, { lineCode, fileHash }) => { diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue index 91145db10e2..c835cab6175 100644 --- a/app/assets/javascripts/environments/components/edit_environment.vue +++ b/app/assets/javascripts/environments/components/edit_environment.vue @@ -3,7 +3,9 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getEnvironment from '../graphql/queries/environment.query.graphql'; +import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql'; import EnvironmentForm from './environment_form.vue'; export default { @@ -11,6 +13,7 @@ export default { GlLoadingIcon, EnvironmentForm, }, + mixins: [glFeatureFlagsMixin()], inject: ['projectEnvironmentsPath', 'updateEnvironmentPath', 'projectPath', 'environmentName'], apollo: { environment: { @@ -42,6 +45,44 @@ export default { this.formEnvironment = environment; }, onSubmit() { + if (this.glFeatures?.environmentSettingsToGraphql) { + this.updateWithGraphql(); + } else { + this.updateWithAxios(); + } + }, + async updateWithGraphql() { + this.loading = true; + try { + const { data } = await this.$apollo.mutate({ + mutation: updateEnvironment, + variables: { + input: { + id: this.formEnvironment.id, + externalUrl: this.formEnvironment.externalUrl, + }, + }, + }); + + const { errors } = data.environmentUpdate; + + if (errors.length > 0) { + throw new Error(errors[0]?.message ?? errors[0]); + } + + const { path } = data.environmentUpdate.environment; + + if (path) { + visitUrl(path); + } + } catch (error) { + const { message } = error; + createAlert({ message }); + } finally { + this.loading = false; + } + }, + updateWithAxios() { this.loading = true; axios .put(this.updateEnvironmentPath, { diff --git a/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql new file mode 100644 index 00000000000..9ea0e3609cb --- /dev/null +++ b/app/assets/javascripts/environments/graphql/mutations/update_environment.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateEnvironment($input: EnvironmentUpdateInput!) { + environmentUpdate(input: $input) { + environment { + id + path + } + errors + } +} diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue deleted file mode 100644 index 3911201457f..00000000000 --- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue +++ /dev/null @@ -1,131 +0,0 @@ -<script> -import { - GlButton, - GlFormGroup, - GlFormInput, - GlFormCheckbox, - GlIcon, - GlLink, - GlSprintf, -} from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import { helpPagePath } from '~/helpers/help_page_helper'; - -export default { - components: { - GlButton, - GlFormCheckbox, - GlFormGroup, - GlFormInput, - GlIcon, - GlLink, - GlSprintf, - }, - data() { - return { - helpUrl: helpPagePath('operations/metrics/embed_grafana', { - anchor: 'use-integration-with-grafana-api', - }), - placeholderUrl: 'https://my-grafana.example.com/', - }; - }, - computed: { - ...mapState(['operationsSettingsEndpoint', 'grafanaToken', 'grafanaUrl', 'grafanaEnabled']), - integrationEnabled: { - get() { - return this.grafanaEnabled; - }, - set(grafanaEnabled) { - this.setGrafanaEnabled(grafanaEnabled); - }, - }, - localGrafanaToken: { - get() { - return this.grafanaToken; - }, - set(token) { - this.setGrafanaToken(token); - }, - }, - localGrafanaUrl: { - get() { - return this.grafanaUrl; - }, - set(url) { - this.setGrafanaUrl(url); - }, - }, - }, - methods: { - ...mapActions([ - 'setGrafanaUrl', - 'setGrafanaToken', - 'setGrafanaEnabled', - 'updateGrafanaIntegration', - ]), - }, -}; -</script> - -<template> - <section id="grafana" class="settings no-animate js-grafana-integration"> - <div class="settings-header"> - <h4 - class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only" - > - {{ s__('GrafanaIntegration|Grafana authentication') }} - </h4> - <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> - <p class="js-section-sub-header"> - {{ - s__( - 'GrafanaIntegration|Set up Grafana authentication to embed Grafana panels in GitLab Flavored Markdown.', - ) - }} - <gl-link :href="helpUrl">{{ __('Learn more.') }}</gl-link> - </p> - </div> - <div class="settings-content"> - <form> - <gl-form-group :label="__('Enable authentication')" label-for="grafana-integration-enabled"> - <gl-form-checkbox id="grafana-integration-enabled" v-model="integrationEnabled"> - {{ s__('GrafanaIntegration|Active') }} - </gl-form-checkbox> - </gl-form-group> - <gl-form-group - :label="s__('GrafanaIntegration|Grafana URL')" - label-for="grafana-url" - :description="s__('GrafanaIntegration|Enter the base URL of the Grafana instance.')" - > - <gl-form-input id="grafana-url" v-model="localGrafanaUrl" :placeholder="placeholderUrl" /> - </gl-form-group> - <gl-form-group :label="s__('GrafanaIntegration|API token')" label-for="grafana-token"> - <gl-form-input id="grafana-token" v-model="localGrafanaToken" /> - <p class="form-text text-muted"> - <gl-sprintf - :message=" - s__('GrafanaIntegration|Enter the %{docLinkStart}Grafana API token%{docLinkEnd}.') - " - > - <template #docLink="{ content }"> - <gl-link - href="https://grafana.com/docs/http_api/auth/#create-api-token" - target="_blank" - >{{ content }} <gl-icon name="external-link" class="gl-vertical-align-middle" - /></gl-link> - </template> - </gl-sprintf> - </p> - </gl-form-group> - <gl-button - variant="confirm" - category="primary" - data-testid="save-grafana-settings-button" - @click="updateGrafanaIntegration" - > - {{ __('Save changes') }} - </gl-button> - </form> - </div> - </section> -</template> diff --git a/app/assets/javascripts/grafana_integration/index.js b/app/assets/javascripts/grafana_integration/index.js deleted file mode 100644 index 9ade29dae69..00000000000 --- a/app/assets/javascripts/grafana_integration/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import Vue from 'vue'; -import GrafanaIntegration from './components/grafana_integration.vue'; -import store from './store'; - -export default () => { - const el = document.querySelector('.js-grafana-integration'); - - if (!el) return false; - - return new Vue({ - el, - store: store(el.dataset), - render(createElement) { - return createElement(GrafanaIntegration); - }, - }); -}; diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js deleted file mode 100644 index 76e21f09719..00000000000 --- a/app/assets/javascripts/grafana_integration/store/actions.js +++ /dev/null @@ -1,44 +0,0 @@ -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import * as mutationTypes from './mutation_types'; - -export const setGrafanaUrl = ({ commit }, url) => commit(mutationTypes.SET_GRAFANA_URL, url); - -export const setGrafanaToken = ({ commit }, token) => - commit(mutationTypes.SET_GRAFANA_TOKEN, token); - -export const setGrafanaEnabled = ({ commit }, enabled) => - commit(mutationTypes.SET_GRAFANA_ENABLED, enabled); - -export const updateGrafanaIntegration = ({ state, dispatch }) => - axios - .patch(state.operationsSettingsEndpoint, { - project: { - grafana_integration_attributes: { - grafana_url: state.grafanaUrl, - token: state.grafanaToken, - enabled: state.grafanaEnabled, - }, - }, - }) - .then(() => dispatch('receiveGrafanaIntegrationUpdateSuccess')) - .catch((error) => dispatch('receiveGrafanaIntegrationUpdateError', error)); - -export const receiveGrafanaIntegrationUpdateSuccess = () => { - /** - * The operations_controller currently handles successful requests - * by creating an alert banner message to notify the user. - */ - refreshCurrentPage(); -}; - -export const receiveGrafanaIntegrationUpdateError = (_, error) => { - const { response } = error; - const message = response.data && response.data.message ? response.data.message : ''; - - createAlert({ - message: `${__('There was an error saving your changes.')} ${message}`, - }); -}; diff --git a/app/assets/javascripts/grafana_integration/store/index.js b/app/assets/javascripts/grafana_integration/store/index.js deleted file mode 100644 index a11bd8089fd..00000000000 --- a/app/assets/javascripts/grafana_integration/store/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import * as actions from './actions'; -import mutations from './mutations'; -import createState from './state'; - -Vue.use(Vuex); - -export const createStore = (initialState) => - new Vuex.Store({ - state: createState(initialState), - actions, - mutations, - }); - -export default createStore; diff --git a/app/assets/javascripts/grafana_integration/store/mutation_types.js b/app/assets/javascripts/grafana_integration/store/mutation_types.js deleted file mode 100644 index 314c3a4039a..00000000000 --- a/app/assets/javascripts/grafana_integration/store/mutation_types.js +++ /dev/null @@ -1,3 +0,0 @@ -export const SET_GRAFANA_URL = 'SET_GRAFANA_URL'; -export const SET_GRAFANA_TOKEN = 'SET_GRAFANA_TOKEN'; -export const SET_GRAFANA_ENABLED = 'SET_GRAFANA_ENABLED'; diff --git a/app/assets/javascripts/grafana_integration/store/mutations.js b/app/assets/javascripts/grafana_integration/store/mutations.js deleted file mode 100644 index 0992030d404..00000000000 --- a/app/assets/javascripts/grafana_integration/store/mutations.js +++ /dev/null @@ -1,13 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.SET_GRAFANA_URL](state, url) { - state.grafanaUrl = url; - }, - [types.SET_GRAFANA_TOKEN](state, token) { - state.grafanaToken = token; - }, - [types.SET_GRAFANA_ENABLED](state, enabled) { - state.grafanaEnabled = enabled; - }, -}; diff --git a/app/assets/javascripts/grafana_integration/store/state.js b/app/assets/javascripts/grafana_integration/store/state.js deleted file mode 100644 index a912eb58327..00000000000 --- a/app/assets/javascripts/grafana_integration/store/state.js +++ /dev/null @@ -1,8 +0,0 @@ -import { parseBoolean } from '~/lib/utils/common_utils'; - -export default (initialState = {}) => ({ - operationsSettingsEndpoint: initialState.operationsSettingsEndpoint, - grafanaToken: initialState.grafanaIntegrationToken || '', - grafanaUrl: initialState.grafanaIntegrationUrl || '', - grafanaEnabled: parseBoolean(initialState.grafanaIntegrationEnabled) || false, -}); diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue index 5160903c762..64b916caddb 100644 --- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue +++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue @@ -17,14 +17,9 @@ export default { methods: { convertToTask() { eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos); - this.closeDropdown(); }, deleteTaskListItem() { eventHub.$emit('delete-task-list-item', this.$el.closest('li').dataset.sourcepos); - this.closeDropdown(); - }, - closeDropdown() { - this.$refs.dropdown.close(); }, }, }; @@ -33,7 +28,6 @@ export default { <template> <gl-disclosure-dropdown v-if="canUpdate" - ref="dropdown" class="task-list-item-actions-wrapper" category="tertiary" icon="ellipsis_v" diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 27fb116d213..47e0ace1ea7 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -214,22 +214,18 @@ export default { methods: { ...mapActions(['toggleAwardRequest', 'promoteCommentToTimelineEvent']), onEdit() { - this.closeMoreActionsDropdown(); this.$emit('handleEdit'); }, onDelete() { - this.closeMoreActionsDropdown(); this.$emit('handleDelete'); }, onResolve() { this.$emit('handleResolve'); }, onAbuse() { - this.closeMoreActionsDropdown(); this.toggleReportAbuseDrawer(true); }, onCopyUrl() { - this.closeMoreActionsDropdown(); this.$toast.show(__('Link copied to clipboard.')); }, handleAssigneeUpdate(assignees) { @@ -241,8 +237,6 @@ export default { let { assignees } = this; const { project_id, iid } = this.getNoteableData; - this.closeMoreActionsDropdown(); - if (this.isUserAssigned) { assignees = assignees.filter((assignee) => assignee.id !== this.author.id); } else { @@ -271,11 +265,6 @@ export default { toggleReportAbuseDrawer(isOpen) { this.isReportAbuseDrawerOpen = isOpen; }, - closeMoreActionsDropdown() { - if (this.shouldShowActionsDropdown && this.$refs.moreActionsDropdown) { - this.$refs.moreActionsDropdown.close(); - } - }, }, }; </script> @@ -374,7 +363,6 @@ export default { /> <div v-else-if="shouldShowActionsDropdown" class="more-actions dropdown"> <gl-disclosure-dropdown - ref="moreActionsDropdown" v-gl-tooltip :title="$options.i18n.moreActionsLabel" :aria-label="$options.i18n.moreActionsLabel" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 2cf6e9bb180..fe7967f1ed0 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -94,6 +94,11 @@ export default { required: false, default: false, }, + autofocus: { + type: Boolean, + required: false, + default: true, + }, }, data() { return { @@ -359,7 +364,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :disabled="isSubmitting" supports-quick-actions - autofocus + :autofocus="autofocus" @keydown.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" @keydown.exact.up="editMyLastNote()" diff --git a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue b/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue deleted file mode 100644 index 7120ad511d3..00000000000 --- a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue +++ /dev/null @@ -1,60 +0,0 @@ -<script> -import { GlFormGroup, GlFormSelect } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import { s__ } from '~/locale'; -import { timezones } from '~/monitoring/format_date'; - -export default { - components: { - GlFormGroup, - GlFormSelect, - }, - computed: { - ...mapState(['dashboardTimezone']), - dashboardTimezoneModel: { - get() { - return this.dashboardTimezone.selected; - }, - set(selected) { - this.setDashboardTimezone(selected); - }, - }, - options() { - return [ - { - value: timezones.LOCAL, - text: s__("MetricsSettings|User's local timezone"), - }, - { - value: timezones.UTC, - text: s__('MetricsSettings|UTC (Coordinated Universal Time)'), - }, - ]; - }, - }, - methods: { - ...mapActions(['setDashboardTimezone']), - }, -}; -</script> - -<template> - <gl-form-group - :label="s__('MetricsSettings|Dashboard timezone')" - label-for="dashboard-timezone-setting" - > - <template #description> - {{ - s__( - "MetricsSettings|Choose whether to display dashboard metrics in UTC or the user's local timezone.", - ) - }} - </template> - - <gl-form-select - id="dashboard-timezone-setting" - v-model="dashboardTimezoneModel" - :options="options" - /> - </gl-form-group> -</template> diff --git a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue deleted file mode 100644 index 2ea5b4e01b1..00000000000 --- a/app/assets/javascripts/operation_settings/components/form_group/external_dashboard.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -import { GlFormGroup, GlFormInput } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; - -export default { - components: { - GlFormGroup, - GlFormInput, - }, - computed: { - ...mapState(['externalDashboard']), - userDashboardUrl: { - get() { - return this.externalDashboard.url; - }, - set(url) { - this.setExternalDashboardUrl(url); - }, - }, - }, - methods: { - ...mapActions(['setExternalDashboardUrl']), - }, -}; -</script> - -<template> - <gl-form-group - :label="s__('MetricsSettings|External dashboard URL')" - label-for="external-dashboard-url" - > - <template #description> - {{ - s__( - 'MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard.', - ) - }} - </template> - <!-- placeholder with a url is a false positive --> - <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings --> - <gl-form-input - id="external-dashboard-url" - v-model="userDashboardUrl" - placeholder="https://my-org.gitlab.io/my-dashboards" - /> - <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings --> - </gl-form-group> -</template> diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue deleted file mode 100644 index 959fffa2629..00000000000 --- a/app/assets/javascripts/operation_settings/components/metrics_settings.vue +++ /dev/null @@ -1,55 +0,0 @@ -<script> -import { GlButton, GlLink } from '@gitlab/ui'; -import { mapState, mapActions } from 'vuex'; -import DashboardTimezone from './form_group/dashboard_timezone.vue'; -import ExternalDashboard from './form_group/external_dashboard.vue'; - -export default { - components: { - GlButton, - GlLink, - ExternalDashboard, - DashboardTimezone, - }, - computed: { - ...mapState(['helpPage']), - userDashboardUrl: { - get() { - return this.externalDashboard.url; - }, - set(url) { - this.setExternalDashboardUrl(url); - }, - }, - }, - methods: { - ...mapActions(['saveChanges']), - }, -}; -</script> - -<template> - <section class="settings no-animate"> - <div class="settings-header"> - <h4 - class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only" - > - {{ s__('MetricsSettings|Metrics') }} - </h4> - <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> - <p class="js-section-sub-header"> - {{ s__('MetricsSettings|Manage metrics dashboard settings.') }} - <gl-link :href="helpPage">{{ __('Learn more.') }}</gl-link> - </p> - </div> - <div class="settings-content"> - <form> - <dashboard-timezone /> - <external-dashboard /> - <gl-button variant="confirm" category="primary" @click="saveChanges"> - {{ __('Save Changes') }} - </gl-button> - </form> - </div> - </section> -</template> diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js deleted file mode 100644 index e56583963ad..00000000000 --- a/app/assets/javascripts/operation_settings/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import Vue from 'vue'; -import MetricsSettingsForm from './components/metrics_settings.vue'; -import store from './store'; - -export default () => { - const el = document.querySelector('.js-operation-settings'); - - if (!el) return false; - - return new Vue({ - el, - store: store(el.dataset), - render(createElement) { - return createElement(MetricsSettingsForm); - }, - }); -}; diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js deleted file mode 100644 index 7fa79da59c4..00000000000 --- a/app/assets/javascripts/operation_settings/store/actions.js +++ /dev/null @@ -1,41 +0,0 @@ -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import * as mutationTypes from './mutation_types'; - -export const setExternalDashboardUrl = ({ commit }, url) => - commit(mutationTypes.SET_EXTERNAL_DASHBOARD_URL, url); - -export const setDashboardTimezone = ({ commit }, selected) => - commit(mutationTypes.SET_DASHBOARD_TIMEZONE, selected); - -export const saveChanges = ({ state, dispatch }) => - axios - .patch(state.operationsSettingsEndpoint, { - project: { - metrics_setting_attributes: { - dashboard_timezone: state.dashboardTimezone.selected, - external_dashboard_url: state.externalDashboard.url, - }, - }, - }) - .then(() => dispatch('receiveSaveChangesSuccess')) - .catch((error) => dispatch('receiveSaveChangesError', error)); - -export const receiveSaveChangesSuccess = () => { - /** - * The operations_controller currently handles successful requests - * by creating an alert banner message to notify the user. - */ - refreshCurrentPage(); -}; - -export const receiveSaveChangesError = (_, error) => { - const { response = {} } = error; - const message = response.data && response.data.message ? response.data.message : ''; - - createAlert({ - message: `${__('There was an error saving your changes.')} ${message}`, - }); -}; diff --git a/app/assets/javascripts/operation_settings/store/index.js b/app/assets/javascripts/operation_settings/store/index.js deleted file mode 100644 index a11bd8089fd..00000000000 --- a/app/assets/javascripts/operation_settings/store/index.js +++ /dev/null @@ -1,16 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import * as actions from './actions'; -import mutations from './mutations'; -import createState from './state'; - -Vue.use(Vuex); - -export const createStore = (initialState) => - new Vuex.Store({ - state: createState(initialState), - actions, - mutations, - }); - -export default createStore; diff --git a/app/assets/javascripts/operation_settings/store/mutation_types.js b/app/assets/javascripts/operation_settings/store/mutation_types.js deleted file mode 100644 index 92543fd7f03..00000000000 --- a/app/assets/javascripts/operation_settings/store/mutation_types.js +++ /dev/null @@ -1,2 +0,0 @@ -export const SET_EXTERNAL_DASHBOARD_URL = 'SET_EXTERNAL_DASHBOARD_URL'; -export const SET_DASHBOARD_TIMEZONE = 'SET_DASHBOARD_TIMEZONE'; diff --git a/app/assets/javascripts/operation_settings/store/mutations.js b/app/assets/javascripts/operation_settings/store/mutations.js deleted file mode 100644 index f55717f6c98..00000000000 --- a/app/assets/javascripts/operation_settings/store/mutations.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.SET_EXTERNAL_DASHBOARD_URL](state, url) { - state.externalDashboard.url = url; - }, - [types.SET_DASHBOARD_TIMEZONE](state, selected) { - state.dashboardTimezone.selected = selected; - }, -}; diff --git a/app/assets/javascripts/operation_settings/store/state.js b/app/assets/javascripts/operation_settings/store/state.js deleted file mode 100644 index c0eca580848..00000000000 --- a/app/assets/javascripts/operation_settings/store/state.js +++ /dev/null @@ -1,10 +0,0 @@ -export default (initialState = {}) => ({ - operationsSettingsEndpoint: initialState.operationsSettingsEndpoint, - helpPage: initialState.helpPage, - externalDashboard: { - url: initialState.externalDashboardUrl, - }, - dashboardTimezone: { - selected: initialState.dashboardTimezoneSetting, - }, -}); diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index 3a46241e2eb..1b8657c5ec7 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -1,14 +1,10 @@ import mountAlertsSettings from '~/alerts_settings'; import mountErrorTrackingForm from '~/error_tracking_settings'; -import mountGrafanaIntegration from '~/grafana_integration'; import initIncidentsSettings from '~/incidents_settings'; -import mountOperationSettings from '~/operation_settings'; import initSettingsPanels from '~/settings_panels'; initIncidentsSettings(); mountErrorTrackingForm(); -mountOperationSettings(); -mountGrafanaIntegration(); if (!IS_EE) { initSettingsPanels(); } diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue index fa6056aff5e..8e5aeba1ec2 100644 --- a/app/assets/javascripts/super_sidebar/components/create_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue @@ -42,9 +42,6 @@ export default { isInvitedMembers(groupItem) { return groupItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT; }, - closeAndFocus() { - this.$refs.dropdown.closeAndFocus(); - }, }, toggleId: 'create-menu-toggle', popperOptions: { @@ -64,7 +61,6 @@ export default { <template> <div> <gl-disclosure-dropdown - ref="dropdown" category="tertiary" icon="plus" no-caret @@ -89,7 +85,6 @@ export default { :key="`${groupItem.text}-trigger`" trigger-source="top-nav" :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN" - @modal-opened="closeAndFocus" /> <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" /> </template> diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 4de17ffa8b6..03f6a9c2122 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -132,7 +132,7 @@ export default { items: [ { text: this.$options.i18n.shortcuts, - action: this.showKeyboardShortcuts, + action: () => {}, extraAttrs: { class: 'js-shortcuts-modal-trigger', 'data-track-action': 'click_button', @@ -172,18 +172,11 @@ export default { return true; }, - showKeyboardShortcuts() { - this.$refs.dropdown.close(); - }, - showTanukiBotChat() { - this.$refs.dropdown.close(); - this.helpCenterState.showTanukiBotChatDrawer = true; }, async showWhatsNew() { - this.$refs.dropdown.close(); this.showWhatsNewNotification = false; if (!this.toggleWhatsNewDrawer) { @@ -226,7 +219,6 @@ export default { <template> <gl-disclosure-dropdown - ref="dropdown" :popper-options="$options.popperOptions" @shown="trackDropdownToggle(true)" @hidden="trackDropdownToggle(false)" diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index cd5a83c86cc..e08fb630fb7 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -241,6 +241,7 @@ export default { :popper-options="$options.popperOptions" data-testid="user-dropdown" data-qa-selector="user_menu" + :auto-close="false" @shown="onShow" > <template #toggle> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 295063a3ef1..af0b34f1389 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -267,6 +267,7 @@ export default { :css-classes="['diff-suggest-popover']" placement="bottom" :show="suggestPopoverVisible" + triggers="" > <strong>{{ __('New! Suggest changes directly') }}</strong> <p class="mb-2"> diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue index 91f87be1233..948258fbee6 100644 --- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue +++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue @@ -33,6 +33,9 @@ export default { currentUserId() { return window.gon.current_user_id; }, + currentUserFullName() { + return window.gon.current_user_fullname; + }, /** * Parse and convert award emoji list to a format that AwardsList can understand */ @@ -42,15 +45,18 @@ export default { name: emoji.name, user: { id: getIdFromGraphQLId(emoji.user.id), + name: emoji.user.name, }, })); }, }, methods: { handleAward(name) { - // Decide action based on emoji is already present + // Decide action based on emoji given by current user. const action = - this.awards.findIndex((emoji) => emoji.name === name) > -1 + this.awards.findIndex( + (emoji) => emoji.name === name && emoji.user.id === this.currentUserId, + ) > -1 ? EMOJI_ACTION_REMOVE : EMOJI_ACTION_ADD; const inputVariables = { @@ -96,13 +102,19 @@ export default { __typename: 'AwardEmoji', user: { id: convertToGraphQLId(TYPENAME_USER, this.currentUserId), + name: this.currentUserFullName, __typename: 'UserCore', }, }, ]; // Exclude the award emoji node in case of remove action if (action === EMOJI_ACTION_REMOVE) { - awardEmojiNodes = [...this.awardEmoji.nodes.filter((emoji) => emoji.name !== name)]; + awardEmojiNodes = [ + ...this.awardEmoji.nodes.filter( + (emoji) => + !(emoji.name === name && getIdFromGraphQLId(emoji.user.id) === this.currentUserId), + ), + ]; } return { workItemUpdate: { diff --git a/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql b/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql index 85b88990cd6..bed09974ef5 100644 --- a/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/award_emoji.fragment.graphql @@ -2,5 +2,6 @@ fragment AwardEmojiFragment on AwardEmoji { name user { id + name } } diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index 6c40781670a..192cb82aaab 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -1,6 +1,6 @@ // Common .diff-file { - margin-bottom: $gl-padding; + padding-bottom: $gl-padding; &.has-body { .file-title { diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 45a7901b2c4..8d2146cddc5 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -56,7 +56,7 @@ class Admin::UsersController < Admin::ApplicationController log_impersonation_event - flash[:alert] = format(_("You are now impersonating %{username}"), username: user.username) + flash[:notice] = format(_("You are now impersonating %{username}"), username: user.username) redirect_to root_path else diff --git a/app/controllers/concerns/web_ide_csp.rb b/app/controllers/concerns/web_ide_csp.rb index c2d66abb538..0327020a0c2 100644 --- a/app/controllers/concerns/web_ide_csp.rb +++ b/app/controllers/concerns/web_ide_csp.rb @@ -5,25 +5,27 @@ module WebIdeCSP included do before_action :include_web_ide_csp + end - # We want to include frames from `/assets/webpack` of the request's host to - # support URL flexibility with the Web IDE. - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118875 - def include_web_ide_csp - return if request.content_security_policy.directives.blank? + # We want to include frames from `/assets/webpack` of the request's host to + # support URL flexibility with the Web IDE. + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118875 + def include_web_ide_csp + return if request.content_security_policy.directives.blank? - base_uri = URI(request.url) - base_uri.path = ::Gitlab.config.gitlab.relative_url_root || '/' - # `.path +=` handles combining `x/` and `/foo` - base_uri.path += '/assets/webpack/' - webpack_url = base_uri.to_s + base_uri = URI(request.url) + base_uri.path = ::Gitlab.config.gitlab.relative_url_root || '/' + # `.path +=` handles combining `x/` and `/foo` + base_uri.path += '/assets/webpack/' + webpack_url = base_uri.to_s - default_src = Array(request.content_security_policy.directives['default-src'] || []) - request.content_security_policy.directives['frame-src'] ||= default_src - request.content_security_policy.directives['frame-src'].concat([webpack_url, 'https://*.vscode-cdn.net/']) + default_src = Array(request.content_security_policy.directives['default-src'] || []) + request.content_security_policy.directives['frame-src'] ||= default_src + request.content_security_policy.directives['frame-src'].concat([webpack_url, 'https://*.vscode-cdn.net/']) - request.content_security_policy.directives['worker-src'] ||= default_src - request.content_security_policy.directives['worker-src'].concat([webpack_url]) - end + request.content_security_policy.directives['worker-src'] ||= default_src + request.content_security_policy.directives['worker-src'].concat([webpack_url]) end end + +WebIdeCSP.prepend_mod_with('WebIdeCSP') diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index f91ec55573d..0db26c544fa 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -25,6 +25,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:kas_user_access_project, @project) end + before_action only: [:edit, :new] do + push_frontend_feature_flag(:environment_settings_to_graphql, @project) + end + before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect] before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_stop_environment!, only: [:stop] diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 55b1aff51da..236eaf92546 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -12,13 +12,15 @@ class AbuseReport < ApplicationRecord cache_markdown_field :message, pipeline: :single_line - belongs_to :reporter, class_name: 'User' - belongs_to :user + belongs_to :reporter, class_name: 'User', inverse_of: :reported_abuse_reports + belongs_to :user, inverse_of: :abuse_reports + belongs_to :resolved_by, class_name: 'User', inverse_of: :resolved_abuse_reports + belongs_to :assignee, class_name: 'User', inverse_of: :assigned_abuse_reports has_many :events, class_name: 'ResourceEvents::AbuseReportEvent', inverse_of: :abuse_report - validates :reporter, presence: true - validates :user, presence: true + validates :reporter, presence: true, on: :create + validates :user, presence: true, on: :create validates :message, presence: true validates :category, presence: true validates :user_id, @@ -27,7 +29,7 @@ class AbuseReport < ApplicationRecord message: ->(object, data) do _('You have already reported this user') end - } + }, on: :create validates :reported_from_url, allow_blank: true, @@ -45,6 +47,9 @@ class AbuseReport < ApplicationRecord message: N_("exceeds the limit of %{count} links") } + validates :mitigation_steps, length: { maximum: 1000 }, allow_blank: true + validates :evidence, json_schema: { filename: 'abuse_report_evidence' }, allow_blank: true + before_validation :filter_empty_strings_from_links_to_spam validate :links_to_spam_contains_valid_urls diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 3b514d5c5ff..7e0722ab68c 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -7,12 +7,12 @@ class ProjectImportData < ApplicationRecord belongs_to :project, inverse_of: :import_data attr_encrypted :credentials, - key: Settings.attr_encrypted_db_key_base, - marshal: true, - encode: true, - mode: :per_attribute_iv_and_salt, - insecure_mode: true, - algorithm: 'aes-256-cbc' + key: Settings.attr_encrypted_db_key_base, + marshal: true, + encode: true, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + algorithm: 'aes-256-cbc' # NOTE # We are serializing a project as `data` in an "unsafe" way here diff --git a/app/models/project_team.rb b/app/models/project_team.rb index dd200aec807..631797ee812 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -179,9 +179,11 @@ class ProjectTeam # # Returns a Hash mapping user ID -> maximum access level. def max_member_access_for_user_ids(user_ids) - Gitlab::SafeRequestLoader.execute(resource_key: project.max_member_access_for_resource_key(User), - resource_ids: user_ids, - default_value: Gitlab::Access::NO_ACCESS) do |user_ids| + Gitlab::SafeRequestLoader.execute( + resource_key: project.max_member_access_for_resource_key(User), + resource_ids: user_ids, + default_value: Gitlab::Access::NO_ACCESS + ) do |user_ids| project.project_authorizations .where(user: user_ids) .group(:user_id) @@ -202,9 +204,11 @@ class ProjectTeam end def contribution_check_for_user_ids(user_ids) - Gitlab::SafeRequestLoader.execute(resource_key: "contribution_check_for_users:#{project.id}", - resource_ids: user_ids, - default_value: false) do |user_ids| + Gitlab::SafeRequestLoader.execute( + resource_key: "contribution_check_for_users:#{project.id}", + resource_ids: user_ids, + default_value: false + ) do |user_ids| project.merge_requests .merged .where(author_id: user_ids, target_branch: project.default_branch.to_s) diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb index 59440947d71..52fc0a9d1bb 100644 --- a/app/models/prometheus_alert.rb +++ b/app/models/prometheus_alert.rb @@ -25,7 +25,7 @@ class PrometheusAlert < ApplicationRecord validates :environment, :project, :prometheus_metric, :threshold, :operator, presence: true validates :runbook_url, length: { maximum: 255 }, allow_blank: true, - addressable_url: { enforce_sanitization: true, ascii_only: true } + addressable_url: { enforce_sanitization: true, ascii_only: true } validate :require_valid_environment_project! validate :require_valid_metric_project! diff --git a/app/models/release.rb b/app/models/release.rb index 0f00732b62e..10eb022b08b 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -35,8 +35,10 @@ class Release < ApplicationRecord scope :sorted, -> { order(released_at: :desc) } scope :preloaded, -> { - includes(:author, :evidences, :milestones, :links, :sorted_links, - project: [:project_feature, :route, { namespace: :route }]) + includes( + :author, :evidences, :milestones, :links, :sorted_links, + project: [:project_feature, :route, { namespace: :route }] + ) } scope :with_milestones, -> { joins(:milestone_releases) } scope :with_group_milestones, -> { joins(:milestones).where.not(milestones: { group_id: nil }) } diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb index 44760541290..3ad7efcfcec 100644 --- a/app/models/releases/source.rb +++ b/app/models/releases/source.rb @@ -9,9 +9,7 @@ module Releases class << self def all(project, tag_name) Gitlab::Workhorse::ARCHIVE_FORMATS.map do |format| - Releases::Source.new(project: project, - tag_name: tag_name, - format: format) + Releases::Source.new(project: project, tag_name: tag_name, format: format) end end end @@ -19,9 +17,7 @@ module Releases def url Gitlab::Routing .url_helpers - .project_archive_url(project, - id: File.join(tag_name, archive_prefix), - format: format) + .project_archive_url(project, id: File.join(tag_name, archive_prefix), format: format) end def hook_attrs diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index b830cf313af..8b2f3bdcedf 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -11,12 +11,12 @@ class RemoteMirror < ApplicationRecord UNPROTECTED_BACKOFF_DELAY = 5.minutes attr_encrypted :credentials, - key: Settings.attr_encrypted_db_key_base, - marshal: true, - encode: true, - mode: :per_attribute_iv_and_salt, - insecure_mode: true, - algorithm: 'aes-256-cbc' + key: Settings.attr_encrypted_db_key_base, + marshal: true, + encode: true, + mode: :per_attribute_iv_and_salt, + insecure_mode: true, + algorithm: 'aes-256-cbc' belongs_to :project, inverse_of: :remote_mirrors @@ -31,10 +31,8 @@ class RemoteMirror < ApplicationRecord scope :stuck, -> do started - .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)', - MAX_INCREMENTAL_RUNTIME.ago) - .or(where('(last_update_started_at < ? AND last_update_at IS NULL)', - MAX_FIRST_RUNTIME.ago)) + .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)', MAX_INCREMENTAL_RUNTIME.ago) + .or(where('(last_update_started_at < ? AND last_update_at IS NULL)', MAX_FIRST_RUNTIME.ago)) end state_machine :update_status, initial: :none do diff --git a/app/models/repository.rb b/app/models/repository.rb index e942157993b..acb795f174d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -880,10 +880,12 @@ class Repository end def merge(user, source_sha, merge_request, message) - merge_to_branch(user, - source_sha: source_sha, - target_branch: merge_request.target_branch, - message: message) do |commit_id| + merge_to_branch( + user, + source_sha: source_sha, + target_branch: merge_request.target_branch, + message: message + ) do |commit_id| merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id) nil # Return value does not matter. end @@ -1136,10 +1138,13 @@ class Repository end def squash(user, merge_request, message) - raw.squash(user, start_sha: merge_request.diff_start_sha, - end_sha: merge_request.diff_head_sha, - author: merge_request.author, - message: message) + raw.squash( + user, + start_sha: merge_request.diff_start_sha, + end_sha: merge_request.diff_head_sha, + author: merge_request.author, + message: message + ) end def submodule_links @@ -1271,11 +1276,13 @@ class Repository end def initialize_raw_repository - Gitlab::Git::Repository.new(shard, - disk_path + '.git', - repo_type.identifier_for_container(container), - container.full_path, - container: container) + Gitlab::Git::Repository.new( + shard, + disk_path + '.git', + repo_type.identifier_for_container(container), + container.full_path, + container: container + ) end end diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb index dddd4d0fe84..1cc77501d8d 100644 --- a/app/models/resource_timebox_event.rb +++ b/app/models/resource_timebox_event.rb @@ -34,8 +34,9 @@ class ResourceTimeboxEvent < ResourceEvent case self when ResourceMilestoneEvent - Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user, - project: issue.project) + Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action( + author: user, project: issue.project + ) else # no-op end diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index 93c128c989c..ecd3e27a9c4 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -28,7 +28,7 @@ module Terraform validates :project_id, :name, presence: true validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, - format: { with: HEX_REGEXP, message: 'only allows hex characters' } + format: { with: HEX_REGEXP, message: 'only allows hex characters' } attribute :uuid, default: -> { SecureRandom.hex(UUID_LENGTH / 2) } diff --git a/app/models/time_tracking/timelog_category.rb b/app/models/time_tracking/timelog_category.rb index 246e78f31cb..67565039acd 100644 --- a/app/models/time_tracking/timelog_category.rb +++ b/app/models/time_tracking/timelog_category.rb @@ -18,9 +18,9 @@ module TimeTracking validates :description, length: { maximum: 1024 } validates :color, color: true, allow_blank: false, length: { maximum: 7 } validates :billing_rate, - if: :billable?, - presence: true, - numericality: { greater_than: 0 } + if: :billable?, + presence: true, + numericality: { greater_than: 0 } DEFAULT_COLOR = ::Gitlab::Color.of('#6699cc') diff --git a/app/models/user.rb b/app/models/user.rb index 1bccb5466dd..87e31f84839 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -216,8 +216,10 @@ class User < ApplicationRecord has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_many :abuse_reports, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent - has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent + has_many :abuse_reports, dependent: :nullify, foreign_key: :user_id, inverse_of: :user # rubocop:disable Cop/ActiveRecordDependent + has_many :reported_abuse_reports, dependent: :nullify, foreign_key: :reporter_id, class_name: "AbuseReport", inverse_of: :reporter # rubocop:disable Cop/ActiveRecordDependent + has_many :assigned_abuse_reports, foreign_key: :assignee_id, class_name: "AbuseReport", inverse_of: :assignee + has_many :resolved_abuse_reports, foreign_key: :resolved_by_id, class_name: "AbuseReport", inverse_of: :resolved_by has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :abuse_trust_scores, class_name: 'Abuse::TrustScore', foreign_key: :user_id has_many :builds, class_name: 'Ci::Build' diff --git a/app/validators/json_schemas/abuse_report_evidence.json b/app/validators/json_schemas/abuse_report_evidence.json new file mode 100644 index 00000000000..e00628d5704 --- /dev/null +++ b/app/validators/json_schemas/abuse_report_evidence.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Evidence to support an abuse report", + "type": "object", + "properties": { + "issues": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "description" + ] + } + }, + "snippets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "content": { + "type": "string" + } + }, + "required": [ + "id", + "content" + ] + } + }, + "notes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "content": { + "type": "string" + } + }, + "required": [ + "id", + "content" + ] + } + }, + "user": { + "type": "object", + "properties": { + "login_count": { + "type": "integer" + }, + "account_age": { + "type": "integer" + }, + "spam_score": { + "type": "number" + }, + "telesign_score": { + "type": "number" + }, + "arkos_score": { + "type": "number" + }, + "pvs_score": { + "type": "number" + }, + "product_coverage": { + "type": "number" + }, + "virus_total_score": { + "type": "number" + } + }, + "required": [ + "login_count", + "account_age", + "spam_score", + "telesign_score", + "arkos_score", + "pvs_score", + "product_coverage", + "virus_total_score" + ] + } + }, + "required": [ + "user" + ] +} diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index 6461b71b10d..7d5d41c2851 100644 --- a/app/views/clusters/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml @@ -8,12 +8,12 @@ = render Pajamas::AlertComponent.new(variant: :warning, alert_options: { class: 'hidden js-cluster-api-unreachable' }) do |c| - = c.body do + - c.with_body do = s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.') = render Pajamas::AlertComponent.new(variant: :warning, alert_options: { class: 'hidden js-cluster-authentication-failure js-cluster-api-unreachable' }) do |c| - = c.body do + - c.with_body do = s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.') .hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } diff --git a/app/views/clusters/clusters/_deprecation_alert.html.haml b/app/views/clusters/clusters/_deprecation_alert.html.haml index 0318c0f7dfa..4f35ba78cc6 100644 --- a/app/views/clusters/clusters/_deprecation_alert.html.haml +++ b/app/views/clusters/clusters/_deprecation_alert.html.haml @@ -1,5 +1,5 @@ = render Pajamas::AlertComponent.new(variant: :warning, dismissible: false, alert_options: { class: 'gl-mt-6 gl-mb-3' }) do |c| - = c.body do + - c.with_body do - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe - issue_link_start = link_start % { url: 'https://gitlab.com/gitlab-org/configure/general/-/issues/199' } - docs_link_start = link_start % { url: help_page_path('user/clusters/agent/index.md') } diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index 40632e27fa7..08badbb4963 100644 --- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml @@ -4,8 +4,8 @@ alert_options: { class: 'gcp-signup-offer', data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}, close_button_options: { data: { track_action: 'click_dismiss', track_label: 'gcp_signup_offer_banner' }}) do |c| - = c.body do + - c.with_body do = s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } - = c.actions do + - c.with_actions do = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer', data: { track_action: 'click_button', track_label: 'gcp_signup_offer_banner' } }) do = s_("ClusterIntegration|Apply for credit") diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml index c5fdc31a775..2b13e2ba9a5 100644 --- a/app/views/dashboard/projects/_blank_state_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml @@ -12,7 +12,7 @@ = _('Projects are where you store your code, access issues, wiki and other features of GitLab.') - else = render Pajamas::AlertComponent.new(variant: :info, alert_options: { class: 'gl-mb-5 gl-w-full' }) do |c| - = c.body do + - c.with_body do = _("You see projects here when you're added to a group or project.").html_safe - if current_user.can_create_group? diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index fb5a57b509c..5fa9b3272f0 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -12,7 +12,7 @@ - if render_remember_me .gl-px-5 = render Pajamas::CheckboxTagComponent.new(name: 'remember_me') do |c| - = c.label do + - c.with_label do = _('Remember me') .submit-container.move-submit-down.gl-px-5.gl-pb-5 diff --git a/app/views/devise/shared/_error_messages.html.haml b/app/views/devise/shared/_error_messages.html.haml index b7589a4460e..caebda72b9c 100644 --- a/app/views/devise/shared/_error_messages.html.haml +++ b/app/views/devise/shared/_error_messages.html.haml @@ -3,7 +3,7 @@ variant: :danger, dismissible: false, alert_options: { id: 'error_explanation', class: 'gl-mb-3'}) do |c| - = c.body do + - c.with_body do %ul.gl-pl-4 - resource.errors.full_messages.each do |message| %li= message diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 14a9bde2d9e..8f2c2c58790 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -18,5 +18,5 @@ = label_for_provider(provider) - if render_remember_me = render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c| - = c.label do + - c.with_label do = _('Remember me') diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index e49b3eb7781..88d57ed7e33 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -3,5 +3,5 @@ - if current_user - unless has_label - %span.gl-float-left= _("Visibility:") + %span.gl-float-left.gl-white-space-nowrap= _("Visibility:") = gl_redirect_listbox_tag(projects_filter_items, selected, class: 'gl-ml-3', data: { placement: 'right' }) diff --git a/app/views/import/shared/_errors.html.haml b/app/views/import/shared/_errors.html.haml index 2dbb54a9a0e..760715b56ea 100644 --- a/app/views/import/shared/_errors.html.haml +++ b/app/views/import/shared/_errors.html.haml @@ -2,6 +2,6 @@ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c| - = c.body do + - c.with_body do - @errors.each do |error| = error diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml index 5c70136a932..ee4644e9ff0 100644 --- a/app/views/layouts/header/_registration_enabled_callout.html.haml +++ b/app/views/layouts/header/_registration_enabled_callout.html.haml @@ -6,9 +6,9 @@ data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: callouts_path }}, close_button_options: { data: { testid: 'close-registration-enabled-callout' }}) do |c| - = c.body do + - c.with_body do = _("Your GitLab instance allows anyone to register for an account, which is a security risk on public-facing GitLab instances. You should deactivate new sign ups if public users aren't expected to register for an account.") - = c.actions do + - c.with_actions do = render Pajamas::ButtonComponent.new(variant: :confirm, href: general_admin_application_settings_path(anchor: 'js-signup-settings')) do = _('Deactivate') = render Pajamas::ButtonComponent.new(button_options: { class: 'js-close gl-ml-3'}) do diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index 71c622d7a62..9a50e3e2eb2 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -17,9 +17,9 @@ %div{ class: "#{container_class} limit-container-width" } .content{ id: "content-body" } = render Pajamas::CardComponent.new do |c| - = c.header do + - c.with_header do = brand_header_logo({add_gitlab_black_text: true}) - = c.body do + - c.with_body do - if header_link?(:user_dropdown) .navbar-collapse %ul.nav.navbar-nav diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index ea8d6b7fda2..91a54f15305 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -4,14 +4,14 @@ - if current_user.ldap_user? = render Pajamas::AlertComponent.new(alert_options: { class: 'gl-my-5' }, dismissible: false) do |c| - = c.body do + - c.with_body do = s_('Profiles|Some options are unavailable for LDAP accounts') - if params[:two_factor_auth_enabled_successfully] = render Pajamas::AlertComponent.new(variant: :success, alert_options: { class: 'gl-my-5' }, close_button_options: { class: 'js-close-2fa-enabled-success-alert' }) do |c| - = c.body do + - c.with_body do = html_escape(_('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can %{anchorOpen}use that key to generate additional recovery codes%{anchorClose}.')) % { anchorOpen: '<a href="%{href}">'.html_safe % { href: help_page_path('user/profile/account/two_factor_authentication', anchor: 'generate-new-recovery-codes-using-ssh') }, anchorClose: '</a>'.html_safe } .row.gl-mt-3.js-search-settings-section diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml index 54736153223..1952655937e 100644 --- a/app/views/profiles/active_sessions/index.html.haml +++ b/app/views/profiles/active_sessions/index.html.haml @@ -11,6 +11,6 @@ .gl-mb-3 = render Pajamas::CardComponent.new(card_options: { class: 'gl-border-0' }, body_options: { class: 'gl-p-0' }) do |c| - - c.body do + - c.with_body do %ul.list-group.list-group-flush = render partial: 'profiles/active_sessions/active_session', collection: @sessions diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml index bc30ccc5821..b0a694f9bc6 100644 --- a/app/views/profiles/chat_names/new.html.haml +++ b/app/views/profiles/chat_names/new.html.haml @@ -3,9 +3,9 @@ %main{ role: 'main' } .gl-max-w-80.gl-mx-auto.gl-mt-6 = render Pajamas::CardComponent.new do |c| - - c.header do + - c.with_header do %h4.gl-m-0= sprintf(s_('Integrations|Authorize %{integration_name} (%{user}) to use your account?'), { user: @chat_name_params[:chat_name], integration_name: @integration_name }) - - c.body do + - c.with_body do %p = sprintf(s_('Integrations|An application called %{integration_name} is requesting access to your GitLab account. This application was created by GitLab Inc.'), { integration_name: @integration_name }) %p @@ -16,7 +16,7 @@ %li= s_('SlackIntegration|Run ChatOps jobs.') %p.gl-mb-0 = s_("SlackIntegration|You don't have to reauthorize this application if the permission scope changes in future releases.") - - c.footer do + - c.with_footer do .gl-display-flex = form_tag profile_chat_names_path, method: :post do = hidden_field_tag :token, @chat_name_token.token diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 3c05502be57..4f3d97fb90c 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -2,9 +2,9 @@ .row.gl-mt-3 .col-md-4 = render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0'}) do |c| - - c.header do + - c.with_header do = _('SSH Key') - - c.body do + - c.with_body do %ul.content-list %li %span.light= _('Title:') @@ -27,9 +27,9 @@ %pre.well-pre = @key.key = render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0'}) do |c| - - c.header do + - c.with_header do = _('Fingerprints') - - c.body do + - c.with_body do %ul.content-list %li %span.light= 'MD5:' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index a632c450eda..06d37787d2e 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -5,7 +5,7 @@ %div - if @user.errors.any? = render Pajamas::AlertComponent.new(variant: :danger) do |c| - = c.body do + - c.with_body do %ul - @user.errors.full_messages.each do |msg| %li= msg diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 9cc7f6bdd49..461164e1ae9 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -24,7 +24,7 @@ = raw @qr_code .col-md-8 = render Pajamas::CardComponent.new do |c| - - c.body do + - c.with_body do %p.gl-mt-0.gl-mb-3.gl-font-weight-bold = _("Can't scan the code?") %p.gl-mt-0.gl-mb-3 @@ -42,7 +42,7 @@ variant: :danger, alert_options: { class: 'gl-mb-3' }, dismissible: false) do |c| - = c.body do + - c.with_body do = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer' - if current_password_required? @@ -130,7 +130,7 @@ variant: :danger, alert_options: { class: 'gl-mb-3' }, dismissible: false) do |c| - = c.body do + - c.with_body do = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer' .js-manage-two-factor-form{ data: { current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } } - else diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml index b9db9898d49..49b0f8c39c8 100644 --- a/app/views/projects/mirrors/_branch_filter.html.haml +++ b/app/views/projects/mirrors/_branch_filter.html.haml @@ -1,6 +1,9 @@ -.form-check.gl-mb-3 - = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' - = label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label' - .form-text.text-muted - = _('If enabled, only protected branches will be mirrored.') - = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' +.form-group + = render Pajamas::CheckboxTagComponent.new(name: :only_protected_branches, + checkbox_options: { class: 'js-mirror-protected' }, + label_options: { class: 'gl-mb-0!' }) do |c| + = c.label do + = _('Mirror only protected branches') + = c.help_text do + = _('If enabled, only protected branches will be mirrored.') + = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/settings/operations/_grafana_integration.html.haml b/app/views/projects/settings/operations/_grafana_integration.html.haml deleted file mode 100644 index 69e42a6c4fb..00000000000 --- a/app/views/projects/settings/operations/_grafana_integration.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project), - grafana_integration: { url: grafana_integration_url, token: grafana_integration_masked_token, enabled: grafana_integration_enabled?.to_s } } } diff --git a/app/views/projects/settings/operations/_metrics_dashboard.html.haml b/app/views/projects/settings/operations/_metrics_dashboard.html.haml deleted file mode 100644 index 056d3e8102b..00000000000 --- a/app/views/projects/settings/operations/_metrics_dashboard.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project), - help_page: help_page_path('operations/metrics/dashboards/settings'), - external_dashboard: { url: metrics_external_dashboard_url, - help_page: help_page_path('operations/metrics/dashboards/settings') }, - dashboard_timezone: { setting: metrics_dashboard_timezone.upcase } } } diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml index d44ebf1eb83..93ab98c1472 100644 --- a/app/views/projects/settings/operations/show.html.haml +++ b/app/views/projects/settings/operations/show.html.haml @@ -2,14 +2,7 @@ - breadcrumb_title _('Monitor Settings') - @force_desktop_expanded_sidebar = true -- if Feature.disabled?(:remove_monitor_metrics) - = render 'projects/settings/operations/metrics_dashboard' - = render 'projects/settings/operations/error_tracking' = render 'projects/settings/operations/alert_management' = render 'projects/settings/operations/incidents' - -- if Feature.disabled?(:remove_monitor_metrics) - = render 'projects/settings/operations/grafana_integration' - = render_if_exists 'projects/settings/operations/status_page' diff --git a/app/views/protected_branches/shared/_create_protected_branch.html.haml b/app/views/protected_branches/shared/_create_protected_branch.html.haml index 9bc224b2e78..62cf81a59d4 100644 --- a/app/views/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/protected_branches/shared/_create_protected_branch.html.haml @@ -1,9 +1,9 @@ = gitlab_ui_form_for [protected_branch_entity, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' } = render Pajamas::CardComponent.new(card_options: { class: "gl-mb-5" }) do |c| - - c.header do + - c.with_header do = s_("ProtectedBranch|Protect a branch") - - c.body do + - c.with_body do = form_errors(@protected_branch) .form-group.row = f.label :name, s_('ProtectedBranch|Branch:'), class: 'col-sm-12' @@ -38,7 +38,7 @@ - force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url } = (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe = render_if_exists 'protected_branches/ee/code_owner_approval_form', f: f, protected_branch_entity: protected_branch_entity - - c.footer do + - c.with_footer do = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true .js-alert-protected-branch-created-container.gl-mb-5 diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml index 6c8ab5654a0..d59ad342972 100644 --- a/app/views/registrations/welcome/show.html.haml +++ b/app/views/registrations/welcome/show.html.haml @@ -23,7 +23,7 @@ 'aria-live' => 'assertive', data: { testid: 'welcome-form' } }) do |f| = render Pajamas::CardComponent.new do |c| - - c.body do + - c.with_body do .devise-errors = render 'devise/shared/error_messages', resource: current_user .row diff --git a/config/feature_flags/development/fix_dora_deployment_frequency_calculation.yml b/config/feature_flags/development/add_validation_for_push_rules.yml index efc68c28eab..c1cb4b4f601 100644 --- a/config/feature_flags/development/fix_dora_deployment_frequency_calculation.yml +++ b/config/feature_flags/development/add_validation_for_push_rules.yml @@ -1,8 +1,8 @@ --- -name: fix_dora_deployment_frequency_calculation -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116644 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/404565 -milestone: '15.11' +name: add_validation_for_push_rules +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121030 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411901 +milestone: '16.1' type: development -group: group::optimize -default_enabled: true +group: group::source code +default_enabled: false diff --git a/config/feature_flags/development/environment_settings_to_graphql.yml b/config/feature_flags/development/environment_settings_to_graphql.yml new file mode 100644 index 00000000000..89da0c73324 --- /dev/null +++ b/config/feature_flags/development/environment_settings_to_graphql.yml @@ -0,0 +1,8 @@ +--- +name: environment_settings_to_graphql +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121091 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/412332 +milestone: '16.1' +type: development +group: group::environments +default_enabled: false diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 5b865498651..27c33a0c144 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -55,7 +55,7 @@ end OPTIONAL_REVIEW_TEMPLATE = '%{role} review is optional for %{category}' NOT_AVAILABLE_TEMPLATES = { default: 'No %{role} available', - product_intelligence: group_not_available_template('#g_analyze_analytics_instrumentation', '@gitlab-org/analytics-section/product-intelligence/engineers'), + analytics_instrumentation: group_not_available_template('#g_analyze_analytics_instrumentation', '@gitlab-org/analytics-section/analytics-instrumentation/engineers'), import_integrate_be: group_not_available_template('#g_manage_import_and_integrate', '@gitlab-org/manage/import-and-integrate'), import_integrate_fe: group_not_available_template('#g_manage_import_and_integrate', '@gitlab-org/manage/import-and-integrate') }.freeze diff --git a/db/migrate/20230517182802_add_fields_to_abuse_reports.rb b/db/migrate/20230517182802_add_fields_to_abuse_reports.rb new file mode 100644 index 00000000000..dc83e4a1879 --- /dev/null +++ b/db/migrate/20230517182802_add_fields_to_abuse_reports.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AddFieldsToAbuseReports < Gitlab::Database::Migration[2.1] + RESOLVED_BY_INDEX = 'index_abuse_reports_on_resolved_by_id' + ASSIGNEE_INDEX = 'index_abuse_reports_on_assignee_id' + + disable_ddl_transaction! + + def up + with_lock_retries do + add_column :abuse_reports, :resolved_by_id, :int, null: true + add_column :abuse_reports, :assignee_id, :int, null: true + add_column :abuse_reports, :mitigation_steps, :text, null: true + add_column :abuse_reports, :evidence, :jsonb, null: true + end + + add_text_limit :abuse_reports, :mitigation_steps, 1000 + add_concurrent_index :abuse_reports, :resolved_by_id, name: RESOLVED_BY_INDEX + add_concurrent_index :abuse_reports, :assignee_id, name: ASSIGNEE_INDEX + end + + def down + change_table :abuse_reports do |t| + t.remove :resolved_by_id + t.remove :assignee_id + t.remove :mitigation_steps + t.remove :evidence + end + end +end diff --git a/db/migrate/20230517182958_add_foreign_key_constraints_to_abuse_reports.rb b/db/migrate/20230517182958_add_foreign_key_constraints_to_abuse_reports.rb new file mode 100644 index 00000000000..17507c3ad46 --- /dev/null +++ b/db/migrate/20230517182958_add_foreign_key_constraints_to_abuse_reports.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddForeignKeyConstraintsToAbuseReports < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + return if foreign_key_exists?(:abuse_reports, column: :resolved_by_id) + + add_concurrent_foreign_key :abuse_reports, :users, + column: :resolved_by_id, + null: true, + on_delete: :nullify + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :abuse_reports, column: :resolved_by_id + end + end +end diff --git a/db/migrate/20230517183403_add_foreign_key_to_abuse_reports_for_assignee.rb b/db/migrate/20230517183403_add_foreign_key_to_abuse_reports_for_assignee.rb new file mode 100644 index 00000000000..d30133fb3a6 --- /dev/null +++ b/db/migrate/20230517183403_add_foreign_key_to_abuse_reports_for_assignee.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddForeignKeyToAbuseReportsForAssignee < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + return if foreign_key_exists?(:abuse_reports, column: :assignee_id) + + add_concurrent_foreign_key :abuse_reports, :users, + column: :assignee_id, + null: true, + on_delete: :nullify + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :abuse_reports, column: :assignee_id + end + end +end diff --git a/db/migrate/20230523073455_add_new_async_index_table_name_length_constraint.rb b/db/migrate/20230523073455_add_new_async_index_table_name_length_constraint.rb new file mode 100644 index 00000000000..96f3cc7372b --- /dev/null +++ b/db/migrate/20230523073455_add_new_async_index_table_name_length_constraint.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddNewAsyncIndexTableNameLengthConstraint < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + CONSTRAINT_NAME = 'check_schema_and_name_length' + + def up + add_text_limit :postgres_async_indexes, :table_name, 127, constraint_name: CONSTRAINT_NAME, validate: false + end + + def down + remove_text_limit :postgres_async_indexes, :table_name, constraint_name: CONSTRAINT_NAME + end +end diff --git a/db/migrate/20230523074248_validate_async_index_table_name_length_constraint.rb b/db/migrate/20230523074248_validate_async_index_table_name_length_constraint.rb new file mode 100644 index 00000000000..5b7d74d7db9 --- /dev/null +++ b/db/migrate/20230523074248_validate_async_index_table_name_length_constraint.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ValidateAsyncIndexTableNameLengthConstraint < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + validate_text_limit :postgres_async_indexes, :table_name, constraint_name: 'check_schema_and_name_length' + end + + def down + # no-op because we cannot invalidate a constraint + end +end diff --git a/db/migrate/20230523074517_remove_old_async_index_table_name_length_constraint.rb b/db/migrate/20230523074517_remove_old_async_index_table_name_length_constraint.rb new file mode 100644 index 00000000000..4d43cbdd032 --- /dev/null +++ b/db/migrate/20230523074517_remove_old_async_index_table_name_length_constraint.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class RemoveOldAsyncIndexTableNameLengthConstraint < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + OLD_LENGTH_LIMIT = 63 + + def up + remove_text_limit :postgres_async_indexes, :table_name + end + + def down + # Remove items that might break the old length validation. (unlikely to happen) + define_batchable_model('postgres_async_indexes').each_batch do |indexes| + indexes.where('CHAR_LENGTH(table_name) > ?', OLD_LENGTH_LIMIT).delete_all + end + + add_text_limit :postgres_async_indexes, :table_name, OLD_LENGTH_LIMIT + end +end diff --git a/db/schema_migrations/20230517182802 b/db/schema_migrations/20230517182802 new file mode 100644 index 00000000000..e9ef71cf8bc --- /dev/null +++ b/db/schema_migrations/20230517182802 @@ -0,0 +1 @@ +8cd0b7369ad654e72fb12b9ae0d54cb38ada4b24a20358541dc5ab18ae60605c
\ No newline at end of file diff --git a/db/schema_migrations/20230517182958 b/db/schema_migrations/20230517182958 new file mode 100644 index 00000000000..769fceffb75 --- /dev/null +++ b/db/schema_migrations/20230517182958 @@ -0,0 +1 @@ +7a34b874349603c65c5ad8d71033a8ec23e7253c51bef20314a5c45d8da9903f
\ No newline at end of file diff --git a/db/schema_migrations/20230517183403 b/db/schema_migrations/20230517183403 new file mode 100644 index 00000000000..1a4eed8c669 --- /dev/null +++ b/db/schema_migrations/20230517183403 @@ -0,0 +1 @@ +7169d207128db503be38e1106f63a7c2e752ceacbfba93142a731267ce0fa0fe
\ No newline at end of file diff --git a/db/schema_migrations/20230523073455 b/db/schema_migrations/20230523073455 new file mode 100644 index 00000000000..b1f55a3744b --- /dev/null +++ b/db/schema_migrations/20230523073455 @@ -0,0 +1 @@ +45f116be064d575bba2acab71458a89dbe5332c3cfb4544eb51638bf86081123
\ No newline at end of file diff --git a/db/schema_migrations/20230523074248 b/db/schema_migrations/20230523074248 new file mode 100644 index 00000000000..29b21d2fcf7 --- /dev/null +++ b/db/schema_migrations/20230523074248 @@ -0,0 +1 @@ +a9408102327280918366975b599985f3e382ebd493267fd010ad2dcbe6140eb3
\ No newline at end of file diff --git a/db/schema_migrations/20230523074517 b/db/schema_migrations/20230523074517 new file mode 100644 index 00000000000..e6017c55df3 --- /dev/null +++ b/db/schema_migrations/20230523074517 @@ -0,0 +1 @@ +662b78b3f796cec08f325ec71389bdaf9efa0695b6bfe224b18295175f1383bf
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c117d095de4..7fee4c30bcc 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10793,9 +10793,14 @@ CREATE TABLE abuse_reports ( status smallint DEFAULT 1 NOT NULL, resolved_at timestamp with time zone, screenshot text, + resolved_by_id integer, + assignee_id integer, + mitigation_steps text, + evidence jsonb, CONSTRAINT abuse_reports_links_to_spam_length_check CHECK ((cardinality(links_to_spam) <= 20)), CONSTRAINT check_4b0a5120e0 CHECK ((char_length(screenshot) <= 255)), - CONSTRAINT check_ab1260fa6c CHECK ((char_length(reported_from_url) <= 512)) + CONSTRAINT check_ab1260fa6c CHECK ((char_length(reported_from_url) <= 512)), + CONSTRAINT check_f3c0947a2d CHECK ((char_length(mitigation_steps) <= 1000)) ); CREATE SEQUENCE abuse_reports_id_seq @@ -20324,7 +20329,7 @@ CREATE TABLE postgres_async_indexes ( CONSTRAINT check_083b21157b CHECK ((char_length(definition) <= 2048)), CONSTRAINT check_45dc23c315 CHECK ((char_length(last_error) <= 10000)), CONSTRAINT check_b732c6cd1d CHECK ((char_length(name) <= 63)), - CONSTRAINT check_e64ff4359e CHECK ((char_length(table_name) <= 63)) + CONSTRAINT check_schema_and_name_length CHECK ((char_length(table_name) <= 127)) ); CREATE SEQUENCE postgres_async_indexes_id_seq @@ -29645,6 +29650,10 @@ CREATE INDEX index_abuse_report_events_on_abuse_report_id ON abuse_report_events CREATE INDEX index_abuse_report_events_on_user_id ON abuse_report_events USING btree (user_id); +CREATE INDEX index_abuse_reports_on_assignee_id ON abuse_reports USING btree (assignee_id); + +CREATE INDEX index_abuse_reports_on_resolved_by_id ON abuse_reports USING btree (resolved_by_id); + CREATE INDEX index_abuse_reports_on_status_and_created_at ON abuse_reports USING btree (status, created_at); CREATE INDEX index_abuse_reports_on_status_and_id ON abuse_reports USING btree (status, id); @@ -34902,6 +34911,9 @@ ALTER TABLE ONLY bulk_import_export_uploads ALTER TABLE ONLY ci_pipelines ADD CONSTRAINT fk_3d34ab2e06 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE SET NULL; +ALTER TABLE ONLY abuse_reports + ADD CONSTRAINT fk_3fe6467b93 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -35616,6 +35628,9 @@ ALTER TABLE ONLY vulnerability_external_issue_links ALTER TABLE ONLY epics ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY abuse_reports + ADD CONSTRAINT fk_f10de8b524 FOREIGN KEY (resolved_by_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY boards ADD CONSTRAINT fk_f15266b5f9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index f92b408323c..f8cab0c605e 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -147,9 +147,9 @@ The following metrics are available: | `service_desk_thank_you_email` | Counter | 14.0 | Total number of email responses to new Service Desk emails | | | `service_desk_new_note_email` | Counter | 14.0 | Total number of email notifications on new Service Desk comment | | | `email_receiver_error` | Counter | 14.1 | Total number of errors when processing incoming emails | | -| `gitlab_snowplow_events_total` | Counter | 14.1 | Total number of GitLab Snowplow product intelligence events emitted | | -| `gitlab_snowplow_failed_events_total` | Counter | 14.1 | Total number of GitLab Snowplow product intelligence events emission failures | | -| `gitlab_snowplow_successful_events_total` | Counter | 14.1 | Total number of GitLab Snowplow product intelligence events emission successes | | +| `gitlab_snowplow_events_total` | Counter | 14.1 | Total number of GitLab Snowplow Analytics Instrumentation events emitted | | +| `gitlab_snowplow_failed_events_total` | Counter | 14.1 | Total number of GitLab Snowplow Analytics Instrumentation events emission failures | | +| `gitlab_snowplow_successful_events_total` | Counter | 14.1 | Total number of GitLab Snowplow Analytics Instrumentation events emission successes | | | `gitlab_ci_build_trace_errors_total` | Counter | 14.4 | Total amount of different error types on a build trace | `error_reason` | | `gitlab_presentable_object_cacheless_render_real_duration_seconds` | Histogram | 15.3 | Duration of real time spent caching and representing specific web request objects | `controller`, `action` | | `cached_object_operations_total` | Counter | 15.3 | Total number of objects cached for specific web requests | `controller`, `action` | diff --git a/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md index 94714e7b245..6645f390fd1 100644 --- a/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md +++ b/doc/architecture/blueprints/clickhouse_ingestion_pipeline/index.md @@ -95,9 +95,9 @@ The following case-studies describe how each group intends to solve the underlyi - In addition, [ClickHouse: Investigate client-side buffering to batch writes into ClickHouse](https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2044) talks about their experimentation with using application-local queueing/batching to work around the problems mentioned above. -- ~"group::product intelligence" has been working on building our analytics offering and recently looking at building and/or improving parts of the system. +- ~"group::analytics instrumentation" has been working on building our analytics offering and recently looking at building and/or improving parts of the system. - - [Product Analytics Collector Component](https://gitlab.com/groups/gitlab-org/-/epics/9346) talks about replacing Jitsu with Snowplow for collecting and processing tracking events. For more details of the proposal, see [Jitsu replacement](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/proposals/-/blob/62d332baf5701810d9e7a0b2c00df18431e82f22/doc/jitsu_replacement.md). + - [Product Analytics Collector Component](https://gitlab.com/groups/gitlab-org/-/epics/9346) talks about replacing Jitsu with Snowplow for collecting and processing tracking events. For more details of the proposal, see [Jitsu replacement](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/proposals/-/blob/62d332baf5701810d9e7a0b2c00df18431e82f22/doc/jitsu_replacement.md). - The initial design was prototyped with [Snowplow as Jitsu Replacement PoC](https://gitlab.com/gitlab-org/analytics-section/product-analytics/devkit/-/merge_requests/37). diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 02f187d62c8..97b112c19d9 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -166,7 +166,7 @@ with [domain expertise](#domain-experts). | End-to-end **and** non-end-to-end changes (*4*) | [Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors). | | Only End-to-end changes (*4*) **or** if the MR author is a [Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors) | [Quality maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_qa). | | A new or updated [application limit](https://about.gitlab.com/handbook/product/product-processes/#introducing-application-limits) | [Product manager](https://about.gitlab.com/company/team/). | -| Analytics Instrumentation (telemetry or analytics) changes | [Analytics Instrumentation engineer](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/engineers). | +| Analytics Instrumentation (telemetry or analytics) changes | [Analytics Instrumentation engineer](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/engineers). | | An addition of, or changes to a [Feature spec](testing_guide/testing_levels.md#frontend-feature-tests) | [Quality maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_qa) or [Quality reviewer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_reviewers_qa). | | A new service to GitLab (Puma, Sidekiq, Gitaly are examples) | [Product manager](https://about.gitlab.com/company/team/). See the [process for adding a service component to GitLab](adding_service_component.md) for details. | | Changes related to authentication or authorization | [Manage:Authentication and Authorization team member](https://about.gitlab.com/company/team/). Check the [code review section on the group page](https://about.gitlab.com/handbook/engineering/development/dev/manage/authentication-and-authorization/#additional-considerations) for more details. Patterns for files known to require review from the team are listed in the in the `Authentication and Authorization` section of the [`CODEOWNERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/CODEOWNERS) file, and the team will be listed in the approvers section of all merge requests that modify these files. | diff --git a/doc/development/database_review.md b/doc/development/database_review.md index 0e34e550098..76e9add215b 100644 --- a/doc/development/database_review.md +++ b/doc/development/database_review.md @@ -27,7 +27,7 @@ A database review is required for: database review. - Changes in Service Data metrics that use `count`, `distinct_count`, `estimate_batch_distinct_count` and `sum`. These metrics could have complex queries over large tables. - See the [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) + See the [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) for implementation details. A database reviewer is expected to look out for overly complex diff --git a/doc/development/documentation/topic_types/index.md b/doc/development/documentation/topic_types/index.md index 37ed876fc59..381f70914c6 100644 --- a/doc/development/documentation/topic_types/index.md +++ b/doc/development/documentation/topic_types/index.md @@ -23,7 +23,7 @@ The acronym refers to the first letter of each topic type. In addition to the four primary topic types, you can use the following: -- Page types: [Tutorials](tutorial.md) and [Get started](#get-started) +- Page type: [Tutorials](tutorial.md) - Topic type: [Related topics](#related-topics) - Page or topic type: [Glossaries](glossary.md) @@ -36,6 +36,7 @@ You should avoid: - Topics that have one or two sentences only. In these cases: - Incorporate the information in another topic. - If the sentence links to another page, use a [Related topics](#related-topics) link instead. +- Get started topics. To document a procedure for a single feature, use a [task](task.md). For a set of steps, use a [tutorial](tutorial.md). ## Topic title guidelines @@ -63,27 +64,3 @@ full sentences, and so should not end in a period. - [CI/CD variables](link-to-topic) - [Environment variables](link-to-topic) ``` - -## Get started - -A get started page is a set of steps to help a user get set up -quickly to use a single GitLab feature or tool. -It consists of more than one task. - -Get started pages should be in this format: - -```markdown -# Title ("Get started with <feature>") - -Complete the following steps to ... . - -1. First step. -1. Another step. -1. Another step. - -If you need to add more than one task, -consider using subsections for each distinct task. -``` - -In the left nav, use `Get started` as the text. On the page itself, spell out -the full name. For example, `Get started with application security`. diff --git a/doc/development/feature_development.md b/doc/development/feature_development.md index 15058134092..c4a5c996ab1 100644 --- a/doc/development/feature_development.md +++ b/doc/development/feature_development.md @@ -160,9 +160,9 @@ The following integration guides are internal. Some integrations require access - [Externalization](i18n/externalization.md) - [Translation](i18n/translation.md) -## Product Intelligence guides +## Analytics Instrumentation guides -- [Product Intelligence guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) +- [Analytics Instrumentation guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) - [Service Ping guide](service_ping/index.md) - [Snowplow guide](snowplow/index.md) diff --git a/doc/development/service_ping/implement.md b/doc/development/service_ping/implement.md index 73b74feb239..2cabd6c40e1 100644 --- a/doc/development/service_ping/implement.md +++ b/doc/development/service_ping/implement.md @@ -695,11 +695,11 @@ Create a merge request for the new Service Ping metric, and do the following: ## Verify your metric -On GitLab.com, the Product Intelligence team regularly [monitors Service Ping](https://gitlab.com/groups/gitlab-org/-/epics/6000). +On GitLab.com, the Analytics Instrumentation team regularly [monitors Service Ping](https://gitlab.com/groups/gitlab-org/-/epics/6000). They may alert you that your metrics need further optimization to run quicker and with greater success. The Service Ping JSON payload for GitLab.com is shared in the -[#g_product_intelligence](https://gitlab.slack.com/archives/CL3A7GFPF) Slack channel every week. +[#g_analyze_analytics_instrumentation](https://gitlab.slack.com/archives/CL3A7GFPF) Slack channel every week. You may also use the [Service Ping QA dashboard](https://app.periscopedata.com/app/gitlab/632033/Usage-Ping-QA) to check how well your metric performs. The dashboard allows filtering by GitLab version, by "Self-managed" and "SaaS", and shows you how many failures have occurred for each metric. Whenever you notice a high failure rate, you can re-optimize your metric. diff --git a/doc/development/service_ping/index.md b/doc/development/service_ping/index.md index 3504a730eed..01772b194a4 100644 --- a/doc/development/service_ping/index.md +++ b/doc/development/service_ping/index.md @@ -497,13 +497,13 @@ GitlabServicePingWorker.new.perform('triggered_from_cron' => false, 'skip_db_wri ## Monitoring -Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health). +Service Ping reporting process state is monitored with [internal SiSense dashboard](https://app.periscopedata.com/app/gitlab/968489). ## Related topics -- [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) +- [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) - [Snowplow Guide](../snowplow/index.md) -- [Product Intelligence Direction](https://about.gitlab.com/direction/analytics/product-intelligence/) +- [Analytics Instrumentation Direction](https://about.gitlab.com/direction/analytics/analytics-instrumentation/) - [Data Analysis Process](https://about.gitlab.com/handbook/business-technology/data-team/#data-analysis-process/) - [Data for Product Managers](https://about.gitlab.com/handbook/business-technology/data-team/programs/data-for-product-managers/) - [Data Infrastructure](https://about.gitlab.com/handbook/business-technology/data-team/platform/infrastructure/) diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md index f36a97bcf6b..debe1cca37e 100644 --- a/doc/development/service_ping/metrics_dictionary.md +++ b/doc/development/service_ping/metrics_dictionary.md @@ -219,7 +219,7 @@ key_path: uuid description: GitLab instance unique identifier product_section: analytics product_stage: analytics -product_group: product_intelligence +product_group: analytics_instrumentation value_type: string status: active milestone: 9.1 @@ -313,7 +313,7 @@ bundle exec rails generate gitlab:usage_metric_definition:redis_hll issues users ## Metrics Dictionary -[Metrics Dictionary is a separate application](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/metric-dictionary). +[Metrics Dictionary is a separate application](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/metric-dictionary). All metrics available in Service Ping are in the [Metrics Dictionary](https://metrics.gitlab.com/). diff --git a/doc/development/service_ping/metrics_instrumentation.md b/doc/development/service_ping/metrics_instrumentation.md index 7441a2d1bd4..b6ca773a572 100644 --- a/doc/development/service_ping/metrics_instrumentation.md +++ b/doc/development/service_ping/metrics_instrumentation.md @@ -29,7 +29,7 @@ A metric definition has the [`instrumentation_class`](metrics_dictionary.md) fie The defined instrumentation class should inherit one of the existing metric classes: `DatabaseMetric`, `RedisMetric`, `RedisHLLMetric`, `NumbersMetric` or `GenericMetric`. -The current convention is that a single instrumentation class corresponds to a single metric. On rare occasions, there are exceptions to that convention like [Redis metrics](#redis-metrics). To use a single instrumentation class for more than one metric, please reach out to one of the `@gitlab-org/analytics-section/product-intelligence/engineers` members to consult about your case. +The current convention is that a single instrumentation class corresponds to a single metric. On rare occasions, there are exceptions to that convention like [Redis metrics](#redis-metrics). To use a single instrumentation class for more than one metric, please reach out to one of the `@gitlab-org/analytics-section/analytics-instrumentation/engineers` members to consult about your case. Using the instrumentation classes ensures that metrics can fail safe individually, without breaking the entire process of Service Ping generation. diff --git a/doc/development/service_ping/metrics_lifecycle.md b/doc/development/service_ping/metrics_lifecycle.md index 318db6895fb..cc56863690c 100644 --- a/doc/development/service_ping/metrics_lifecycle.md +++ b/doc/development/service_ping/metrics_lifecycle.md @@ -34,11 +34,11 @@ Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automa WARNING: If a metric is not used in Sisense or any other system after 6 months, the -Product Intelligence team marks it as inactive and assigns it to the group owner for review. +Analytics Instrumentation team marks it as inactive and assigns it to the group owner for review. We are working on automating this process. See [this epic](https://gitlab.com/groups/gitlab-org/-/epics/8988) for details. -Product Intelligence removes metrics from Service Ping if they are not used in any Sisense dashboard. +Analytics Instrumentation removes metrics from Service Ping if they are not used in any Sisense dashboard. For an example of the metric removal process, see this [example issue](https://gitlab.com/gitlab-org/gitlab/-/issues/388236). @@ -88,7 +88,7 @@ To remove a metric: Do not remove the metric's YAML definition altogether. Some self-managed instances might not immediately update to the latest version of GitLab, and - therefore continue to report the removed metric. The Product Intelligence team + therefore continue to report the removed metric. The Analytics Instrumentation team requires a record of all removed metrics to identify and filter them. For example please take a look at this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60149/diffs#b01f429a54843feb22265100c0e4fec1b7da1240_10_10). diff --git a/doc/development/service_ping/performance_indicator_metrics.md b/doc/development/service_ping/performance_indicator_metrics.md index 0ca663ce09a..d7811c52bb1 100644 --- a/doc/development/service_ping/performance_indicator_metrics.md +++ b/doc/development/service_ping/performance_indicator_metrics.md @@ -6,11 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Performance Indicator Metrics guide -This guide describes how to use metrics definitions to define [performance indicator](https://about.gitlab.com/handbook/product/product-intelligence-guide/#implementing-product-performance-indicators) metrics. +This guide describes how to use metrics definitions to define [performance indicator](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/#implementing-product-performance-indicators) metrics. To use a metric definition to manage a performance indicator: 1. Create a merge request that includes related changes. -1. Use labels `~"product intelligence"`, `"~Data Warehouse::Impact Check"`. +1. Use labels `~"analytics instrumentation"`, `"~Data Warehouse::Impact Check"`. 1. Update the metric definition `performance_indicator_type` [field](metrics_dictionary.md#metrics-definition-and-validation). 1. Create an issue in GitLab Product Data Insights project with the [PI Chart Help template](https://gitlab.com/gitlab-data/product-analytics/-/issues/new?issuable_template=PI%20Chart%20Help) to have the new metric visualized. diff --git a/doc/development/service_ping/review_guidelines.md b/doc/development/service_ping/review_guidelines.md index 8128f1e371b..feb85a491ad 100644 --- a/doc/development/service_ping/review_guidelines.md +++ b/doc/development/service_ping/review_guidelines.md @@ -42,7 +42,7 @@ are regular backend changes. - Assign both the `~backend` and `~analytics instrumentation` reviews to another Analytics Instrumentation team member. - Assign the maintainer review to someone outside of the Analytics Instrumentation group. - Assign an - [engineer](https://gitlab.com/groups/gitlab-org/analytics-section/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) from the Analytics Instrumentation team for a review. + [engineer](https://gitlab.com/groups/gitlab-org/analytics-section/analytics-instrumentation/engineers/-/group_members?with_inherited_permissions=exclude) from the Analytics Instrumentation team for a review. - Set the correct attributes in the metric's YAML definition: - `product_section`, `product_stage`, `product_group` - Provide a clear description of the metric. @@ -76,7 +76,7 @@ are regular backend changes. [Danger bot](../dangerbot.md) adds the list of changed Analytics Instrumentation files and pings the -[`@gitlab-org/analytics-section/product-intelligence/engineers`](https://gitlab.com/groups/gitlab-org/analytics-section/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) group for merge requests +[`@gitlab-org/analytics-section/analytics-instrumentation/engineers`](https://gitlab.com/groups/gitlab-org/analytics-section/analytics-instrumentation/engineers/-/group_members?with_inherited_permissions=exclude) group for merge requests that are not drafts. Any of the Analytics Instrumentation engineers can be assigned for the Analytics Instrumentation review. diff --git a/doc/development/service_ping/troubleshooting.md b/doc/development/service_ping/troubleshooting.md index 1b896efb726..e69557c7f1b 100644 --- a/doc/development/service_ping/troubleshooting.md +++ b/doc/development/service_ping/troubleshooting.md @@ -10,13 +10,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w ### Symptoms -You will be alerted by a [Sisense alert](https://app.periscopedata.com/app/gitlab/alert/Service-ping-payload-drop-Alert/5a5b8766b8af43b4a75d6e9c684a365f/edit) that is sent to `#g_product_intelligence` Slack channel +You will be alerted by a [Sisense alert](https://app.periscopedata.com/app/gitlab/alert/Service-ping-payload-drop-Alert/5a5b8766b8af43b4a75d6e9c684a365f/edit) that is sent to `#g_analyze_analytics_instrumentation` Slack channel ### Locating the problem First you need to identify at which stage in Service Ping data pipeline the drop is occurring. -Start at [Service Ping Health Dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health) on Sisense. +Start at [Service Ping Health Dashboard](https://app.periscopedata.com/app/gitlab/968489) on Sisense. The alert compares the current daily value with the daily value from previous week, excluding the last 48 hours as this is the interval where we export the data in GCP and get it into DWH. @@ -30,7 +30,7 @@ For results about an investigation conducted into an unexpected drop in Service Check if the [export jobs](https://gitlab.com/gitlab-services/version-gitlab-com#data-export-using-pipeline-schedules) are successful. -Check [Service Ping errors](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health?widget=14609989&udv=0) in the [Service Ping Health Dashboard](https://app.periscopedata.com/app/gitlab/968489/Product-Intelligence---Service-Ping-Health). +Check [Service Ping errors](https://app.periscopedata.com/app/gitlab/968489?widget=14609989&udv=0) in the [Service Ping Health Dashboard](https://app.periscopedata.com/app/gitlab/968489). ### Troubleshoot Google Storage layer diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md index 4ccb90c22a6..ca74761dbda 100644 --- a/doc/development/snowplow/index.md +++ b/doc/development/snowplow/index.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Snowplow development guidelines -Snowplow is an enterprise-grade marketing and Product Intelligence platform that tracks how users engage with our website and application. +Snowplow is an enterprise-grade marketing and Analytics Instrumentation platform that tracks how users engage with our website and application. [Snowplow](https://snowplow.io/) consists of several loosely-coupled sub-systems: @@ -194,9 +194,9 @@ Snowplow JavaScript adds [web-specific parameters](https://docs.snowplow.io/docs - [Snowplow data structure](https://docs.snowplow.io/docs/understanding-your-pipeline/canonical-event/) - [Our Iglu schema registry](https://gitlab.com/gitlab-org/iglu) - [List of events used in our codebase (Event Dictionary)](https://metrics.gitlab.com/snowplow/) -- [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) +- [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) - [Service Ping Guide](../service_ping/index.md) -- [Product Intelligence Direction](https://about.gitlab.com/direction/analytics/product-intelligence/) +- [Analytics Instrumentation Direction](https://about.gitlab.com/direction/analytics/analytics-instrumentation/) - [Data Analysis Process](https://about.gitlab.com/handbook/business-technology/data-team/#data-analysis-process/) - [Data for Product Managers](https://about.gitlab.com/handbook/business-technology/data-team/programs/data-for-product-managers/) - [Data Infrastructure](https://about.gitlab.com/handbook/business-technology/data-team/platform/infrastructure/) diff --git a/doc/development/snowplow/infrastructure.md b/doc/development/snowplow/infrastructure.md index ac146542630..9679abac6b7 100644 --- a/doc/development/snowplow/infrastructure.md +++ b/doc/development/snowplow/infrastructure.md @@ -50,11 +50,11 @@ See [Snowplow technology 101](https://github.com/snowplow/snowplow/#snowplow-tec ### Pseudonymization -In contrast to a typical Snowplow pipeline, after enrichment, GitLab Snowplow events go through a [pseudonymization service](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/snowplow-pseudonymization) in the form of an AWS Lambda service before they are stored in S3 storage. +In contrast to a typical Snowplow pipeline, after enrichment, GitLab Snowplow events go through a [pseudonymization service](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/snowplow-pseudonymization) in the form of an AWS Lambda service before they are stored in S3 storage. #### Why events need to be pseudonymized -GitLab is bound by its [obligations to community](https://about.gitlab.com/handbook/product/product-intelligence-guide/service-usage-data-commitment/) +GitLab is bound by its [obligations to community](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/service-usage-data-commitment/) and by [legal regulations](https://about.gitlab.com/handbook/legal/privacy/services-usage-data/) to protect the privacy of its users. GitLab must provide valuable insights for business decisions, and there is a need @@ -81,11 +81,11 @@ See [Data team's Snowplow Overview](https://about.gitlab.com/handbook/business-t There are several tools that monitor Snowplow events tracking in different stages of the processing pipeline: -- [Product Intelligence Grafana dashboard](https://dashboards.gitlab.net/d/product-intelligence-main/product-intelligence-product-intelligence?orgId=1) monitors backend events sent from a GitLab.com instance to a collectors fleet. This dashboard provides information about: +- [Analytics Instrumentation Grafana dashboard](https://dashboards.gitlab.net/d/product-intelligence-main/product-intelligence-product-intelligence?orgId=1) monitors backend events sent from a GitLab.com instance to a collectors fleet. This dashboard provides information about: - The number of events that successfully reach Snowplow collectors. - The number of events that failed to reach Snowplow collectors. - The number of backend events that were sent. -- [AWS CloudWatch dashboard](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=SnowPlow;start=P3D) monitors the state of the events in a processing pipeline. The pipeline starts from Snowplow collectors, goes through to enrichers and pseudonymization, and then up to persistence in an S3 bucket. From S3, the events are imported into the Snowflake Data Warehouse. You must have AWS access rights to view this dashboard. For more information, see [monitoring](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/snowplow-pseudonymization#monitoring) in the Snowplow Events pseudonymization service documentation. +- [AWS CloudWatch dashboard](https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=SnowPlow;start=P3D) monitors the state of the events in a processing pipeline. The pipeline starts from Snowplow collectors, goes through to enrichers and pseudonymization, and then up to persistence in an S3 bucket. From S3, the events are imported into the Snowflake Data Warehouse. You must have AWS access rights to view this dashboard. For more information, see [monitoring](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/snowplow-pseudonymization#monitoring) in the Snowplow Events pseudonymization service documentation. - [Sisense dashboard](https://app.periscopedata.com/app/gitlab/417669/Snowplow-Summary-Dashboard) provides information about the number of good and bad events imported into the Data Warehouse, in addition to the total number of imported Snowplow events. For more information, see this [video walk-through](https://www.youtube.com/watch?v=NxPS0aKa_oU). @@ -93,8 +93,8 @@ For more information, see this [video walk-through](https://www.youtube.com/watc ## Related topics - [Snowplow technology 101](https://github.com/snowplow/snowplow/#snowplow-technology-101) -- [Snowplow pseudonymization AWS Lambda project](https://gitlab.com/gitlab-org/analytics-section/product-intelligence/snowplow-pseudonymization) -- [Product Intelligence Guide](https://about.gitlab.com/handbook/product/product-intelligence-guide/) +- [Snowplow pseudonymization AWS Lambda project](https://gitlab.com/gitlab-org/analytics-section/analytics-instrumentation/snowplow-pseudonymization) +- [Analytics Instrumentation Guide](https://about.gitlab.com/handbook/product/analytics-instrumentation-guide/) - [Data Infrastructure](https://about.gitlab.com/handbook/business-technology/data-team/platform/infrastructure/) - [Snowplow architecture overview (internal)](https://www.youtube.com/watch?v=eVYJjzspsLU) - [Snowplow architecture overview slide deck (internal)](https://docs.google.com/presentation/d/16gQEO5CAg8Tx4NBtfnZj-GF4juFI6HfEPWcZgH4Rn14/edit?usp=sharing) diff --git a/doc/development/snowplow/review_guidelines.md b/doc/development/snowplow/review_guidelines.md index d5432cc9075..07b25f95e13 100644 --- a/doc/development/snowplow/review_guidelines.md +++ b/doc/development/snowplow/review_guidelines.md @@ -40,5 +40,5 @@ events or touches Snowplow related files. - Check that an event definition file was created or updated in accordance with the [Event Dictionary Guide](event_dictionary_guide.md). - If needed, check that the events are firing locally using one of the [testing tools](implementation.md#develop-and-test-snowplow) available. -- Approve the MR, and relabel the MR with `~"product intelligence::approved"`. +- Approve the MR, and relabel the MR with `~"analytics instrumentation::approved"`. - If the snowplow event mirrors a RedisHLL event, then tag @mdrussell to review if the payload is usable for this purpose. diff --git a/doc/development/snowplow/troubleshooting.md b/doc/development/snowplow/troubleshooting.md index 92267dfcb0c..885f4e0c16f 100644 --- a/doc/development/snowplow/troubleshooting.md +++ b/doc/development/snowplow/troubleshooting.md @@ -31,14 +31,14 @@ While on CloudWatch dashboard set time range to last 4 weeks, to get better pict ### Troubleshooting GitLab application layer -Drop occurring at application layer can be symptom of some issue, but it might be also a result of normal application lifecycle, intended changes done to product intelligence or experiments tracking +Drop occurring at application layer can be symptom of some issue, but it might be also a result of normal application lifecycle, intended changes done to analytics instrumentation or experiments tracking or even a result of a public holiday in some regions of the world with a larger user-base. To verify if there is an underlying problem to solve, you can check following things: 1. Check `about.gitlab.com` website traffic on [Google Analytics](https://analytics.google.com/analytics/web/) to verify if some public holiday might impact overall use of GitLab system 1. You may require to open an access request for Google Analytics access first, for example: [access request internal issue](https://gitlab.com/gitlab-com/team-member-epics/access-requests/-/issues/1772) 1. Plot `select date(dvce_created_tstamp) , event , count(*) from legacy.snowplow_unnested_events_90 where dvce_created_tstamp > '2021-06-15' and dvce_created_tstamp < '2021-07-10' group by 1 , 2 order by 1 , 2` in SiSense to see what type of events was responsible for drop 1. Plot `select date(dvce_created_tstamp) ,se_category , count(*) from legacy.snowplow_unnested_events_90 where dvce_created_tstamp > '2021-06-15' and dvce_created_tstamp < '2021-07-31' and event = 'struct' group by 1 , 2 order by 1, 2` what events recorded the biggest drops in suspected category -1. Check if there was any MR merged that might cause reduction in reported events, pay an attention to ~"product intelligence" and ~"growth experiment" labeled MRs +1. Check if there was any MR merged that might cause reduction in reported events, pay an attention to ~"analytics instrumentation" and ~"growth experiment" labeled MRs 1. Check (via [Grafana explore tab](https://dashboards.gitlab.net/explore) ) following Prometheus counters `gitlab_snowplow_events_total`, `gitlab_snowplow_failed_events_total` and `gitlab_snowplow_successful_events_total` to see how many events were fired correctly from GitLab.com. Example query to use `sum(rate(gitlab_snowplow_successful_events_total{env="gprd"}[5m])) / sum(rate(gitlab_snowplow_events_total{env="gprd"}[5m]))` would chart rate at which number of good events rose in comparison to events sent in total. If number drops from 1 it means that problem might be in communication between GitLab and AWS collectors fleet. 1. Check [logs in Kibana](https://log.gprd.gitlab.net/app/discover#) and filter with `{ "query": { "match_phrase": { "json.message": "failed to be reported to collector at" } } }` if there are some failed events logged diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index ba226e0f05b..ef34bfbc151 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -211,7 +211,7 @@ If there are problems with the manual upload: 1. Open a confidential issue in the [security fork of version app project](https://gitlab.com/gitlab-org/security/version.gitlab.com). 1. Attach the JSON payload if possible. -1. Tag `@gitlab-org/analytics-section/product-intelligence` who will triage the issue. +1. Tag `@gitlab-org/analytics-section/analytics-instrumentation` who will triage the issue. <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md index 17d4e7fc3df..2bb57c02f81 100644 --- a/doc/user/group/manage.md +++ b/doc/user/group/manage.md @@ -670,7 +670,7 @@ To enable Code Suggestions for a group: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/404856) in GitLab 16.0. -You can give all users in the group access to Experiment features. +You can give all users in a top-level group access to Experiment features. WARNING: [Experiment features](../../policy/alpha-beta-support.md#experiment) may produce unexpected results @@ -680,7 +680,7 @@ and might include insecure code or failed pipelines). This setting [cascades to all projects](../project/merge_requests/approvals/settings.md#settings-cascading) that belong to the group. -To enable Experiment features for a group: +To enable Experiment features for a top-level group: 1. On the top bar, select **Main menu > Groups** and find your group. 1. On the left sidebar, select **Settings > General**. diff --git a/lib/gitlab/database/async_indexes/postgres_async_index.rb b/lib/gitlab/database/async_indexes/postgres_async_index.rb index 9f5f39613ed..a3c600a4519 100644 --- a/lib/gitlab/database/async_indexes/postgres_async_index.rb +++ b/lib/gitlab/database/async_indexes/postgres_async_index.rb @@ -8,13 +8,17 @@ module Gitlab self.table_name = 'postgres_async_indexes' + # schema_name + . + table_name + MAX_TABLE_NAME_LENGTH = (Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH * 2) + 1 MAX_IDENTIFIER_LENGTH = Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH MAX_DEFINITION_LENGTH = 2048 validates :name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH } - validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH } + validates :table_name, presence: true, length: { maximum: MAX_TABLE_NAME_LENGTH } validates :definition, presence: true, length: { maximum: MAX_DEFINITION_LENGTH } + validate :ensure_correct_schema_and_table_name + scope :to_create, -> { where("definition ILIKE 'CREATE%'") } scope :to_drop, -> { where("definition ILIKE 'DROP%'") } scope :ordered, -> { order(attempts: :asc, id: :asc) } @@ -22,6 +26,24 @@ module Gitlab def to_s definition end + + private + + def ensure_correct_schema_and_table_name + return unless table_name + + schema, table, *rest = table_name.split('.') + + too_long = (table.nil? && schema.length > MAX_DEFINITION_LENGTH) || # no schema given + # both schema and table given + (schema.length > MAX_IDENTIFIER_LENGTH || (table && table.length > MAX_IDENTIFIER_LENGTH)) + + if too_long + errors.add(:table_name, :too_long) + elsif rest.any? + errors.add(:table_name, :invalid) + end + end end end end diff --git a/lib/gitlab/database/load_balancing/host.rb b/lib/gitlab/database/load_balancing/host.rb index 156e033d183..bdbb80d6f31 100644 --- a/lib/gitlab/database/load_balancing/host.rb +++ b/lib/gitlab/database/load_balancing/host.rb @@ -142,25 +142,11 @@ module Gitlab # primary. # # This method will return nil if no lag size could be calculated. - def replication_lag_size(location = primary_write_location) - location = connection.quote(location) - - # The following is necessary to handle a mix of logical and physical replicas. We assume that if they have - # pg_replication_origin_status then they are a logical replica. In a logical replica we need to use - # `remote_lsn` rather than `pg_last_wal_replay_lsn` in order for our LSN to be comparable to the source - # cluster. This logic would be broken if we have 2 logical subscriptions or if we have a logical subscription - # in the source primary cluster. Read more at https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121208 + def replication_lag_size + location = connection.quote(primary_write_location) row = query_and_release(<<-SQL.squish) - SELECT pg_wal_lsn_diff(#{location}, ( - CASE - WHEN (SELECT TRUE FROM pg_replication_origin_status) THEN - (SELECT remote_lsn FROM pg_replication_origin_status) - WHEN pg_is_in_recovery() THEN - pg_last_wal_replay_lsn() - ELSE - pg_current_wal_insert_lsn() - END - ))::float AS diff + SELECT pg_wal_lsn_diff(#{location}, pg_last_wal_replay_lsn())::float + AS diff SQL row['diff'].to_i if row.any? @@ -187,8 +173,22 @@ module Gitlab # # location - The transaction write location as reported by a primary. def caught_up?(location) - lag = replication_lag_size(location) - lag.present? && lag.to_i <= 0 + string = connection.quote(location) + + # In case the host is a primary pg_last_wal_replay_lsn/pg_last_xlog_replay_location() returns + # NULL. The recovery check ensures we treat the host as up-to-date in + # such a case. + query = <<-SQL.squish + SELECT NOT pg_is_in_recovery() + OR pg_wal_lsn_diff(pg_last_wal_replay_lsn(), #{string}) >= 0 + AS result + SQL + + row = query_and_release(query) + + ::Gitlab::Utils.to_boolean(row['result']) + rescue *CONNECTION_ERRORS + false end def query_and_release(sql) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index accf2afca30..7041b5a4e9b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9472,6 +9472,9 @@ msgstr "" msgid "CiCatalog|There was an error fetching CI/CD Catalog resources." msgstr "" +msgid "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!" +msgstr "" + msgid "CiCdAnalytics|Date range: %{range}" msgstr "" @@ -16647,9 +16650,6 @@ msgstr "" msgid "Enable authenticated web request rate limit" msgstr "" -msgid "Enable authentication" -msgstr "" - msgid "Enable automatic repository housekeeping" msgstr "" @@ -20474,6 +20474,9 @@ msgstr "" msgid "Gitpod|https://gitpod.example.com" msgstr "" +msgid "Give us some feedback" +msgstr "" + msgid "Given access %{time_ago}" msgstr "" @@ -20876,27 +20879,6 @@ msgstr "" msgid "Grafana response contains invalid json" msgstr "" -msgid "GrafanaIntegration|API token" -msgstr "" - -msgid "GrafanaIntegration|Active" -msgstr "" - -msgid "GrafanaIntegration|Enter the %{docLinkStart}Grafana API token%{docLinkEnd}." -msgstr "" - -msgid "GrafanaIntegration|Enter the base URL of the Grafana instance." -msgstr "" - -msgid "GrafanaIntegration|Grafana URL" -msgstr "" - -msgid "GrafanaIntegration|Grafana authentication" -msgstr "" - -msgid "GrafanaIntegration|Set up Grafana authentication to embed Grafana panels in GitLab Flavored Markdown." -msgstr "" - msgid "Grant access" msgstr "" @@ -28425,30 +28407,6 @@ msgstr "" msgid "MetricsDashboardAnnotation|can't be before starting_at time" msgstr "" -msgid "MetricsSettings|Add a button to the metrics dashboard linking directly to your existing external dashboard." -msgstr "" - -msgid "MetricsSettings|Choose whether to display dashboard metrics in UTC or the user's local timezone." -msgstr "" - -msgid "MetricsSettings|Dashboard timezone" -msgstr "" - -msgid "MetricsSettings|External dashboard URL" -msgstr "" - -msgid "MetricsSettings|Manage metrics dashboard settings." -msgstr "" - -msgid "MetricsSettings|Metrics" -msgstr "" - -msgid "MetricsSettings|UTC (Coordinated Universal Time)" -msgstr "" - -msgid "MetricsSettings|User's local timezone" -msgstr "" - msgid "MetricsUsersStarredDashboards|Dashboard with requested path can not be found" msgstr "" @@ -46706,6 +46664,9 @@ msgstr "" msgid "Time spent must be formatted correctly. For example: 1h 30m." msgstr "" +msgid "Time to Merge" +msgstr "" + msgid "Time to Restore Service" msgstr "" @@ -49397,6 +49358,9 @@ msgstr "" msgid "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed." msgstr "" +msgid "ValueStreamAnalytics|Merge request analytics" +msgstr "" + msgid "ValueStreamAnalytics|New Value Stream" msgstr "" @@ -52420,6 +52384,9 @@ msgstr "" msgid "Your device was successfully set up! Give it a name and register it with the GitLab server." msgstr "" +msgid "Your feedback is important to us 👋" +msgstr "" + msgid "Your file must contain a column named %{codeStart}title%{codeEnd}. A %{codeStart}description%{codeEnd} column is optional. The maximum file size allowed is 10 MB." msgstr "" diff --git a/package.json b/package.json index dcf1a59fec8..04aa2c93382 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.2.0", "@gitlab/svgs": "3.47.0", - "@gitlab/ui": "62.10.0", + "@gitlab/ui": "62.12.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "0.0.1-dev-20230511143809", "@mattiasbuelens/web-streams-adapter": "^0.1.0", diff --git a/scripts/utils.sh b/scripts/utils.sh index b41bc18deff..66512798a11 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -290,13 +290,6 @@ function fail_pipeline_early() { fi } -function danger_as_local() { - # Force danger to skip CI source GitLab and fallback to "local only git repo". - unset GITLAB_CI - # We need to base SHA to help danger determine the base commit for this shallow clone. - bundle exec danger dry_run --fail-on-errors=true --verbose --base="${CI_MERGE_REQUEST_DIFF_BASE_SHA}" --head="${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}" --dangerfile="${DANGER_DANGERFILE:-Dangerfile}" -} - # We're inlining this function in `.gitlab/ci/package-and-test/main.gitlab-ci.yml` so make sure to reflect any changes there function assets_image_tag() { local cache_assets_hash_file="cached-assets-hash.txt" diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 9b00451de30..a53d200b1e1 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -904,7 +904,7 @@ RSpec.describe Admin::UsersController do it "shows a notice" do post :impersonate, params: { id: user.username } - expect(flash[:alert]).to eq("You are now impersonating #{user.username}") + expect(flash[:notice]).to eq("You are now impersonating #{user.username}") end it 'clears token session keys' do diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index 699da744fab..14a44d1108a 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -10,10 +10,32 @@ FactoryBot.define do trait :closed do status { 'closed' } + resolved_by factory: :user end trait :with_screenshot do screenshot { fixture_file_upload('spec/fixtures/dk.png') } end + + trait :with_assignee do + assignee factory: :user + end + + trait :with_evidence do + evidence do + { + "user" => { + "login_count" => rand(0..1000), + "account_age" => rand(0..1000), + "spam_score" => rand(0.0..1.0), + "telesign_score" => rand(0.0..1.0), + "arkos_score" => rand(0.0..1.0), + "pvs_score" => rand(0.0..1.0), + "product_coverage" => rand(0.0..1.0), + "virus_total_score" => rand(0.0..1.0) + } + } + end + end end end diff --git a/spec/features/issues/user_sorts_issue_comments_spec.rb b/spec/features/issues/user_sorts_issue_comments_spec.rb index 153066343f2..8ca9d2003ee 100644 --- a/spec/features/issues/user_sorts_issue_comments_spec.rb +++ b/spec/features/issues/user_sorts_issue_comments_spec.rb @@ -17,7 +17,6 @@ RSpec.describe 'Comment sort direction', feature_category: :team_planning do # open dropdown, and select 'Newest first' page.within('.issuable-details') do click_button('Sort or filter') - click_button('Oldest first') click_button('Newest first') end diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb index 89fee1cdb49..a79d04d85d0 100644 --- a/spec/features/projects/settings/monitor_settings_spec.rb +++ b/spec/features/projects/settings/monitor_settings_spec.rb @@ -11,7 +11,6 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego before do sign_in(user) - stub_feature_flags(remove_monitor_metrics: false) end describe 'Sidebar > Monitor' do @@ -192,30 +191,5 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego end end end - - describe 'grafana integration settings form' do - it 'successfully fills and completes the form' do - visit project_settings_operations_path(project) - - wait_for_requests - - within '.js-grafana-integration' do - click_button('Expand') - end - - expect(page).to have_content('Grafana URL') - expect(page).to have_content('API token') - expect(page).to have_button('Save changes') - - fill_in('grafana-url', with: 'http://gitlab-test.grafana.net') - fill_in('grafana-token', with: 'token') - - click_button('Save changes') - - wait_for_requests - - assert_text('Your changes have been saved') - end - end end end diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb index b706a624fc5..6117ed4e9f6 100644 --- a/spec/features/projects/work_items/work_item_spec.rb +++ b/spec/features/projects/work_items/work_item_spec.rb @@ -4,9 +4,11 @@ require 'spec_helper' RSpec.describe 'Work item', :js, feature_category: :team_planning do let_it_be_with_reload(:user) { create(:user) } + let_it_be_with_reload(:user2) { create(:user, name: 'John') } let_it_be(:project) { create(:project, :public) } let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:emoji_upvote) { create(:award_emoji, :upvote, awardable: work_item, user: user2) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestones) { create_list(:milestone, 25, project: project) } let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) } diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js index 09b6b1edc44..bc648e52fad 100644 --- a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js +++ b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js @@ -2,7 +2,7 @@ import { nextTick } from 'vue'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated @@ -25,7 +25,7 @@ describe('AbuseReportActions', () => { const report = mockAbuseReports[0]; const createComponent = (props = {}) => { - wrapper = shallowMountExtended(AbuseReportActions, { + wrapper = mountExtended(AbuseReportActions, { propsData: { report, ...props, diff --git a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js index 02b455d0b61..1ec8764705c 100644 --- a/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js +++ b/spec/frontend/clusters_list/components/available_agents_dropwdown_spec.js @@ -20,7 +20,6 @@ describe('AvailableAgentsDropdown', () => { propsData, stubs: { GlCollapsibleListbox }, }); - wrapper.vm.$refs.dropdown.closeAndFocus = jest.fn(); }; describe('there are agents available', () => { diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap index e6369c39816..30793c95cbd 100644 --- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap +++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap @@ -1,15 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Design note component should match the snapshot 1`] = ` -<timeline-entry-item-stub +<timelineentryitem-stub class="design-note note-form" id="note_123" > - <gl-avatar-link-stub + <glavatarlink-stub class="gl-float-left gl-mr-3" href="https://gitlab.com/user" > - <gl-avatar-stub + <glavatar-stub alt="avatar" entityid="0" entityname="foo-bar" @@ -17,13 +17,13 @@ exports[`Design note component should match the snapshot 1`] = ` size="32" src="https://gitlab.com/avatar" /> - </gl-avatar-link-stub> + </glavatarlink-stub> <div class="gl-display-flex gl-justify-content-space-between" > <div> - <gl-link-stub + <gllink-stub class="js-user-link" data-testid="user-link" data-user-id="1" @@ -43,7 +43,7 @@ exports[`Design note component should match the snapshot 1`] = ` > @foo-bar </span> - </gl-link-stub> + </gllink-stub> <span class="note-headline-light note-headline-meta" @@ -52,17 +52,17 @@ exports[`Design note component should match the snapshot 1`] = ` class="system-note-message" /> - <gl-link-stub + <gllink-stub class="note-timestamp system-note-separator gl-display-block gl-mb-2" href="#note_123" > - <time-ago-tooltip-stub + <timeagotooltip-stub cssclass="" datetimeformat="DATE_WITH_TIME_FORMAT" time="2019-07-26T15:02:20Z" tooltipplacement="bottom" /> - </gl-link-stub> + </gllink-stub> </span> </div> @@ -82,5 +82,5 @@ exports[`Design note component should match the snapshot 1`] = ` data-testid="note-text" /> -</timeline-entry-item-stub> +</timelineentryitem-stub> `; diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js index fc8309c3e28..661d1ac4087 100644 --- a/spec/frontend/design_management/components/design_notes/design_note_spec.js +++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js @@ -1,7 +1,7 @@ import { ApolloMutation } from 'vue-apollo'; import { nextTick } from 'vue'; import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import DesignNote from '~/design_management/components/design_notes/design_note.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -44,7 +44,7 @@ describe('Design note component', () => { const findDeleteDropdownItem = () => findDropdownItems().at(1); function createComponent(props = {}, data = { isEditing: false }) { - wrapper = shallowMountExtended(DesignNote, { + wrapper = mountExtended(DesignNote, { propsData: { note: {}, noteableId: 'gid://gitlab/DesignManagement::Design/6', @@ -65,6 +65,11 @@ describe('Design note component', () => { ApolloMutation, GlDisclosureDropdown, GlDisclosureDropdownItem, + TimelineEntryItem: true, + TimeAgoTooltip: true, + GlAvatarLink: true, + GlAvatar: true, + GlLink: true, }, }); } diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js index cc28e12788b..853eb185786 100644 --- a/spec/frontend/environments/edit_environment_spec.js +++ b/spec/frontend/environments/edit_environment_spec.js @@ -10,14 +10,21 @@ import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import getEnvironment from '~/environments/graphql/queries/environment.query.graphql'; +import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql'; import { __ } from '~/locale'; import createMockApollo from '../__helpers__/mock_apollo_helper'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/alert'); +const newExternalUrl = 'https://google.ca'; const environment = { id: '1', name: 'foo', externalUrl: 'https://foo.example.com' }; const resolvedEnvironment = { project: { id: '1', environment } }; +const environmentUpdate = { environment: { id: '1', path: 'path/to/environment' }, errors: [] }; +const environmentUpdateError = { + environment: null, + errors: [{ message: 'uh oh!' }], +}; const provide = { projectEnvironmentsPath: '/projects/environments', @@ -28,25 +35,40 @@ const provide = { }; describe('~/environments/components/edit.vue', () => { - Vue.use(VueApollo); - let wrapper; let mock; - const createWrapper = () => { - const mockApollo = createMockApollo([ - [getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })], - ]); + const createMockApolloProvider = (mutationResult, environmentSettingsToGraphql) => { + Vue.use(VueApollo); - return mountExtended(EditEnvironment, { - provide, - apolloProvider: mockApollo, - }); + const mocks = [[getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })]]; + + if (environmentSettingsToGraphql) { + mocks.push([ + updateEnvironment, + jest.fn().mockResolvedValue({ data: { environmentUpdate: mutationResult } }), + ]); + } + + return createMockApollo(mocks); }; - afterEach(() => { - mock.restore(); - }); + const createWrapper = async ({ + mutationResult = environmentUpdate, + environmentSettingsToGraphql = false, + } = {}) => { + wrapper = mountExtended(EditEnvironment, { + provide: { + ...provide, + glFeatures: { + environmentSettingsToGraphql, + }, + }, + apolloProvider: createMockApolloProvider(mutationResult, environmentSettingsToGraphql), + }); + + await waitForPromises(); + }; const findNameInput = () => wrapper.findByLabelText(__('Name')); const findExternalUrlInput = () => wrapper.findByLabelText(__('External URL')); @@ -54,24 +76,14 @@ describe('~/environments/components/edit.vue', () => { const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists(); - const submitForm = async (expected, response) => { - mock - .onPut(provide.updateEnvironmentPath, { - external_url: expected.url, - id: '1', - }) - .reply(...response); - await findExternalUrlInput().setValue(expected.url); - + const submitForm = async () => { + await findExternalUrlInput().setValue(newExternalUrl); await findForm().trigger('submit'); - await waitForPromises(); }; describe('default', () => { beforeEach(async () => { - mock = new MockAdapter(axios); - wrapper = createWrapper(); - await waitForPromises(); + await createWrapper(); }); it('sets the title to Edit environment', () => { @@ -79,50 +91,118 @@ describe('~/environments/components/edit.vue', () => { expect(header.exists()).toBe(true); }); - it('shows loader after form is submitted', async () => { - const expected = { url: 'https://google.ca' }; + it('renders a disabled "Name" field', () => { + const nameInput = findNameInput(); - expect(showsLoading()).toBe(false); + expect(nameInput.attributes().disabled).toBe('disabled'); + expect(nameInput.element.value).toBe(environment.name); + }); - await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]); + it('renders an "External URL" field', () => { + const urlInput = findExternalUrlInput(); - expect(showsLoading()).toBe(true); + expect(urlInput.element.value).toBe(environment.externalUrl); }); + }); - it('submits the updated environment on submit', async () => { - const expected = { url: 'https://google.ca' }; + describe('when environmentSettingsToGraphql feature is enabled', () => { + describe('when mutation successful', () => { + beforeEach(async () => { + await createWrapper({ environmentSettingsToGraphql: true }); + }); - await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]); + it('shows loader after form is submitted', async () => { + expect(showsLoading()).toBe(false); - expect(visitUrl).toHaveBeenCalledWith('/test'); + await submitForm(); + + expect(showsLoading()).toBe(true); + }); + + it('submits the updated environment on submit', async () => { + await submitForm(); + await waitForPromises(); + + expect(visitUrl).toHaveBeenCalledWith(environmentUpdate.environment.path); + }); }); - it('shows errors on error', async () => { - const expected = { url: 'https://google.ca' }; + describe('when mutation failed', () => { + beforeEach(async () => { + await createWrapper({ + mutationResult: environmentUpdateError, + environmentSettingsToGraphql: true, + }); + }); + + it('shows errors on error', async () => { + await submitForm(); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + expect(showsLoading()).toBe(false); + }); + }); + }); - await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]); + describe('when environmentSettingsToGraphql feature is disabled', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + await createWrapper(); + }); - expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + afterEach(() => { + mock.restore(); + }); + + it('shows loader after form is submitted', async () => { expect(showsLoading()).toBe(false); + + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_OK, { path: '/test' }]); + + await submitForm(); + + expect(showsLoading()).toBe(true); }); - it('renders a disabled "Name" field', () => { - const nameInput = findNameInput(); + it('submits the updated environment on submit', async () => { + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_OK, { path: '/test' }]); + + await submitForm(); + await waitForPromises(); - expect(nameInput.attributes().disabled).toBe('disabled'); - expect(nameInput.element.value).toBe(environment.name); + expect(visitUrl).toHaveBeenCalledWith('/test'); }); - it('renders an "External URL" field', () => { - const urlInput = findExternalUrlInput(); + it('shows errors on error', async () => { + mock + .onPut(provide.updateEnvironmentPath, { + external_url: newExternalUrl, + id: environment.id, + }) + .reply(...[HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]); + + await submitForm(); + await waitForPromises(); - expect(urlInput.element.value).toBe(environment.externalUrl); + expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' }); + expect(showsLoading()).toBe(false); }); }); describe('when environment query is loading', () => { beforeEach(() => { - wrapper = createWrapper(); + createWrapper(); }); it('renders loading icon', () => { diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap deleted file mode 100644 index 9447e7daba8..00000000000 --- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`grafana integration component default state to match the default snapshot 1`] = ` -<section - class="settings no-animate js-grafana-integration" - id="grafana" -> - <div - class="settings-header" - > - <h4 - class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only" - > - - Grafana authentication - - </h4> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-settings-toggle" - icon="" - size="medium" - variant="default" - > - Expand - </gl-button-stub> - - <p - class="js-section-sub-header" - > - - Set up Grafana authentication to embed Grafana panels in GitLab Flavored Markdown. - - <gl-link-stub> - Learn more. - </gl-link-stub> - </p> - </div> - - <div - class="settings-content" - > - <form> - <gl-form-group-stub - label="Enable authentication" - label-for="grafana-integration-enabled" - labeldescription="" - optionaltext="(optional)" - > - <gl-form-checkbox-stub - id="grafana-integration-enabled" - > - - Active - - </gl-form-checkbox-stub> - </gl-form-group-stub> - - <gl-form-group-stub - description="Enter the base URL of the Grafana instance." - label="Grafana URL" - label-for="grafana-url" - labeldescription="" - optionaltext="(optional)" - > - <gl-form-input-stub - id="grafana-url" - placeholder="https://my-grafana.example.com/" - value="http://test.host" - /> - </gl-form-group-stub> - - <gl-form-group-stub - label="API token" - label-for="grafana-token" - labeldescription="" - optionaltext="(optional)" - > - <gl-form-input-stub - id="grafana-token" - value="someToken" - /> - - <p - class="form-text text-muted" - > - <gl-sprintf-stub - message="Enter the %{docLinkStart}Grafana API token%{docLinkEnd}." - /> - </p> - </gl-form-group-stub> - - <gl-button-stub - buttontextclasses="" - category="primary" - data-testid="save-grafana-settings-button" - icon="" - size="medium" - variant="confirm" - > - - Save changes - - </gl-button-stub> - </form> - </div> -</section> -`; diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js deleted file mode 100644 index 540fc597aa9..00000000000 --- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { TEST_HOST } from 'helpers/test_constants'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { createAlert } from '~/alert'; -import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue'; -import { createStore } from '~/grafana_integration/store'; -import axios from '~/lib/utils/axios_utils'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; - -jest.mock('~/lib/utils/url_utility'); -jest.mock('~/alert'); - -describe('grafana integration component', () => { - let wrapper; - let store; - const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`; - const grafanaIntegrationUrl = `${TEST_HOST}`; - const grafanaIntegrationToken = 'someToken'; - - beforeEach(() => { - store = createStore({ - operationsSettingsEndpoint, - grafanaIntegrationUrl, - grafanaIntegrationToken, - }); - }); - - afterEach(() => { - createAlert.mockReset(); - refreshCurrentPage.mockReset(); - }); - - describe('default state', () => { - it('to match the default snapshot', () => { - wrapper = shallowMount(GrafanaIntegration, { store }); - - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders header text', () => { - wrapper = shallowMount(GrafanaIntegration, { store }); - - expect(wrapper.find('.js-section-header').text()).toBe('Grafana authentication'); - }); - - describe('expand/collapse button', () => { - it('renders as an expand button by default', () => { - wrapper = shallowMount(GrafanaIntegration, { store }); - - const button = wrapper.findComponent(GlButton); - expect(button.text()).toBe('Expand'); - }); - }); - - describe('sub-header', () => { - it('renders descriptive text', () => { - wrapper = shallowMount(GrafanaIntegration, { store }); - - expect(wrapper.find('.js-section-sub-header').text()).toContain( - 'Set up Grafana authentication to embed Grafana panels in GitLab Flavored Markdown.\n Learn more.', - ); - }); - }); - - describe('form', () => { - beforeEach(() => { - jest.spyOn(axios, 'patch').mockImplementation(); - wrapper = mountExtended(GrafanaIntegration, { store }); - }); - - afterEach(() => { - axios.patch.mockReset(); - }); - - describe('submit button', () => { - const findSubmitButton = () => wrapper.findByTestId('save-grafana-settings-button'); - - const endpointRequest = [ - operationsSettingsEndpoint, - { - project: { - grafana_integration_attributes: { - grafana_url: grafanaIntegrationUrl, - token: grafanaIntegrationToken, - enabled: false, - }, - }, - }, - ]; - - it('submits form on click', async () => { - axios.patch.mockResolvedValue(); - findSubmitButton(wrapper).trigger('click'); - - expect(axios.patch).toHaveBeenCalledWith(...endpointRequest); - await nextTick(); - expect(refreshCurrentPage).toHaveBeenCalled(); - }); - - it('creates alert banner on error', async () => { - const message = 'mockErrorMessage'; - axios.patch.mockRejectedValue({ response: { data: { message } } }); - - findSubmitButton().trigger('click'); - - expect(axios.patch).toHaveBeenCalledWith(...endpointRequest); - - await nextTick(); - await jest.runAllTicks(); - expect(createAlert).toHaveBeenCalledWith({ - message: `There was an error saving your changes. ${message}`, - }); - }); - }); - }); -}); diff --git a/spec/frontend/grafana_integration/store/mutations_spec.js b/spec/frontend/grafana_integration/store/mutations_spec.js deleted file mode 100644 index 18e87394189..00000000000 --- a/spec/frontend/grafana_integration/store/mutations_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import mutations from '~/grafana_integration/store/mutations'; -import createState from '~/grafana_integration/store/state'; - -describe('grafana integration mutations', () => { - let localState; - - beforeEach(() => { - localState = createState(); - }); - - describe('SET_GRAFANA_URL', () => { - it('sets grafanaUrl', () => { - const mockUrl = 'mockUrl'; - mutations.SET_GRAFANA_URL(localState, mockUrl); - - expect(localState.grafanaUrl).toBe(mockUrl); - }); - }); - - describe('SET_GRAFANA_TOKEN', () => { - it('sets grafanaToken', () => { - const mockToken = 'mockToken'; - mutations.SET_GRAFANA_TOKEN(localState, mockToken); - - expect(localState.grafanaToken).toBe(mockToken); - }); - }); - describe('SET_GRAFANA_ENABLED', () => { - it('updates grafanaEnabled for integration', () => { - mutations.SET_GRAFANA_ENABLED(localState, true); - - expect(localState.grafanaEnabled).toBe(true); - }); - }); -}); diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js index 7dacbefaeff..0b3ff0667b1 100644 --- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js +++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js @@ -6,7 +6,7 @@ import eventHub from '~/issues/show/event_hub'; describe('TaskListItemActions component', () => { let wrapper; - const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0); const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1); @@ -20,7 +20,6 @@ describe('TaskListItemActions component', () => { provide: { canUpdate: true }, attachTo: document.querySelector('div'), }); - wrapper.vm.$refs.dropdown.close = jest.fn(); }; beforeEach(() => { @@ -28,7 +27,7 @@ describe('TaskListItemActions component', () => { }); it('renders dropdown', () => { - expect(findGlDropdown().props()).toMatchObject({ + expect(findGlDisclosureDropdown().props()).toMatchObject({ category: 'tertiary', icon: 'ellipsis_v', placement: 'right', diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 879bada4aee..fc50afcb01d 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -175,11 +175,6 @@ describe('noteActions', () => { const { resolveButton } = wrapper.vm.$refs; expect(resolveButton.$el.getAttribute('title')).toBe(`Resolved by ${complexUnescapedName}`); }); - - it('closes the dropdown', () => { - findReportAbuseButton().vm.$emit('action'); - expect(mockCloseDropdown).toHaveBeenCalled(); - }); }); }); diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js deleted file mode 100644 index 5bccf4943ae..00000000000 --- a/spec/frontend/operation_settings/components/metrics_settings_spec.js +++ /dev/null @@ -1,214 +0,0 @@ -import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { TEST_HOST } from 'helpers/test_constants'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import { timezones } from '~/monitoring/format_date'; -import DashboardTimezone from '~/operation_settings/components/form_group/dashboard_timezone.vue'; -import ExternalDashboard from '~/operation_settings/components/form_group/external_dashboard.vue'; -import MetricsSettings from '~/operation_settings/components/metrics_settings.vue'; - -import store from '~/operation_settings/store'; - -jest.mock('~/lib/utils/url_utility'); -jest.mock('~/alert'); - -describe('operation settings external dashboard component', () => { - let wrapper; - - const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`; - const helpPage = `${TEST_HOST}/help/metrics/page/path`; - const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`; - const dashboardTimezoneSetting = timezones.LOCAL; - - const mountComponent = (shallow = true) => { - const config = [ - MetricsSettings, - { - store: store({ - operationsSettingsEndpoint, - helpPage, - externalDashboardUrl, - dashboardTimezoneSetting, - }), - stubs: { - ExternalDashboard, - DashboardTimezone, - }, - }, - ]; - wrapper = shallow ? shallowMount(...config) : mount(...config); - }; - - beforeEach(() => { - jest.spyOn(axios, 'patch').mockImplementation(); - }); - - afterEach(() => { - axios.patch.mockReset(); - refreshCurrentPage.mockReset(); - createAlert.mockReset(); - }); - - it('renders header text', () => { - mountComponent(); - expect(wrapper.find('.js-section-header').text()).toBe('Metrics'); - }); - - describe('expand/collapse button', () => { - it('renders as an expand button by default', () => { - mountComponent(); - const button = wrapper.findComponent(GlButton); - - expect(button.text()).toBe('Expand'); - }); - }); - - describe('sub-header', () => { - let subHeader; - - beforeEach(() => { - mountComponent(); - subHeader = wrapper.find('.js-section-sub-header'); - }); - - it('renders descriptive text', () => { - expect(subHeader.text()).toContain('Manage metrics dashboard settings.'); - }); - - it('renders help page link', () => { - const link = subHeader.findComponent(GlLink); - - expect(link.text()).toBe('Learn more.'); - expect(link.attributes().href).toBe(helpPage); - }); - }); - - describe('form', () => { - describe('dashboard timezone', () => { - describe('field label', () => { - let formGroup; - - beforeEach(() => { - mountComponent(false); - formGroup = wrapper.findComponent(DashboardTimezone).findComponent(GlFormGroup); - }); - - it('uses label text', () => { - expect(formGroup.find('label').text()).toBe('Dashboard timezone'); - }); - - it('uses description text', () => { - const description = formGroup.find('small'); - const expectedDescription = - "Choose whether to display dashboard metrics in UTC or the user's local timezone."; - - expect(description.text()).toBe(expectedDescription); - }); - }); - - describe('select field', () => { - let select; - - beforeEach(() => { - mountComponent(); - select = wrapper.findComponent(DashboardTimezone).findComponent(GlFormSelect); - }); - - it('defaults to externalDashboardUrl', () => { - expect(select.attributes('value')).toBe(dashboardTimezoneSetting); - }); - }); - }); - - describe('external dashboard', () => { - describe('input label', () => { - let formGroup; - - beforeEach(() => { - mountComponent(false); - formGroup = wrapper.findComponent(ExternalDashboard).findComponent(GlFormGroup); - }); - - it('uses label text', () => { - expect(formGroup.find('label').text()).toBe('External dashboard URL'); - }); - - it('uses description text', () => { - const description = formGroup.find('small'); - const expectedDescription = - 'Add a button to the metrics dashboard linking directly to your existing external dashboard.'; - - expect(description.text()).toBe(expectedDescription); - }); - }); - - describe('input field', () => { - let input; - - beforeEach(() => { - mountComponent(); - input = wrapper.findComponent(ExternalDashboard).findComponent(GlFormInput); - }); - - it('defaults to externalDashboardUrl', () => { - expect(input.attributes().value).toBe(externalDashboardUrl); - }); - - it('uses a placeholder', () => { - expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards'); - }); - }); - }); - - describe('submit button', () => { - const findSubmitButton = () => wrapper.find('.settings-content form').findComponent(GlButton); - - const endpointRequest = [ - operationsSettingsEndpoint, - { - project: { - metrics_setting_attributes: { - dashboard_timezone: dashboardTimezoneSetting, - external_dashboard_url: externalDashboardUrl, - }, - }, - }, - ]; - - it('renders button label', () => { - mountComponent(); - const submit = findSubmitButton(); - expect(submit.text()).toBe('Save Changes'); - }); - - it('submits form on click', async () => { - mountComponent(false); - axios.patch.mockResolvedValue(); - findSubmitButton().trigger('click'); - - expect(axios.patch).toHaveBeenCalledWith(...endpointRequest); - - await nextTick(); - expect(refreshCurrentPage).toHaveBeenCalled(); - }); - - it('creates an alert on error', async () => { - mountComponent(false); - const message = 'mockErrorMessage'; - axios.patch.mockRejectedValue({ response: { data: { message } } }); - findSubmitButton().trigger('click'); - - expect(axios.patch).toHaveBeenCalledWith(...endpointRequest); - - await nextTick(); - await jest.runAllTicks(); - expect(createAlert).toHaveBeenCalledWith({ - message: `There was an error saving your changes. ${message}`, - }); - }); - }); - }); -}); diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js deleted file mode 100644 index db6b54b503d..00000000000 --- a/spec/frontend/operation_settings/store/mutations_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import { timezones } from '~/monitoring/format_date'; -import mutations from '~/operation_settings/store/mutations'; -import createState from '~/operation_settings/store/state'; - -describe('operation settings mutations', () => { - let localState; - - beforeEach(() => { - localState = createState(); - }); - - describe('SET_EXTERNAL_DASHBOARD_URL', () => { - it('sets externalDashboardUrl', () => { - const mockUrl = 'mockUrl'; - mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl); - - expect(localState.externalDashboard.url).toBe(mockUrl); - }); - }); - - describe('SET_DASHBOARD_TIMEZONE', () => { - it('sets dashboardTimezoneSetting', () => { - mutations.SET_DASHBOARD_TIMEZONE(localState, timezones.LOCAL); - - expect(localState.dashboardTimezone.selected).not.toBeUndefined(); - expect(localState.dashboardTimezone.selected).toBe(timezones.LOCAL); - }); - }); -}); diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js index 456085e23da..6c26947f9ce 100644 --- a/spec/frontend/super_sidebar/components/create_menu_spec.js +++ b/spec/frontend/super_sidebar/components/create_menu_spec.js @@ -6,7 +6,6 @@ import { GlDisclosureDropdownItem, } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { stubComponent } from 'helpers/stub_component'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; import { __ } from '~/locale'; import CreateMenu from '~/super_sidebar/components/create_menu.vue'; @@ -21,8 +20,6 @@ describe('CreateMenu component', () => { const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); const findGlTooltip = () => wrapper.findComponent(GlTooltip); - const closeAndFocusMock = jest.fn(); - const createWrapper = () => { wrapper = shallowMountExtended(CreateMenu, { propsData: { @@ -30,9 +27,7 @@ describe('CreateMenu component', () => { }, stubs: { InviteMembersTrigger, - GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, { - methods: { closeAndFocus: closeAndFocusMock }, - }), + GlDisclosureDropdown, }, }); }; @@ -93,10 +88,5 @@ describe('CreateMenu component', () => { expect(findGlTooltip().exists()).toBe(true); }); - - it('closes the dropdown when invite members modal is opened', () => { - findInviteMembersTrigger().vm.$emit('modal-opened'); - expect(closeAndFocusMock).toHaveBeenCalled(); - }); }); }); diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index a727fe6c243..204a64dd3a0 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -100,7 +100,6 @@ describe('HelpCenter component', () => { describe('with show_tanuki_bot true', () => { beforeEach(() => { createWrapper({ ...sidebarData, show_tanuki_bot: true }); - jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); }); it('shows Ask GitLab Chat with the help items', () => { @@ -119,10 +118,6 @@ describe('HelpCenter component', () => { findButton('Ask GitLab Chat').click(); }); - it('closes the dropdown', () => { - expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); - }); - it('sets helpCenterState.showTanukiBotChatDrawer to true', () => { expect(helpCenterState.showTanukiBotChatDrawer).toBe(true); }); @@ -150,16 +145,9 @@ describe('HelpCenter component', () => { let button; beforeEach(() => { - jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); - button = findButton('Keyboard shortcuts ?'); }); - it('closes the dropdown', () => { - button.click(); - expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); - }); - it('shows the keyboard shortcuts modal', () => { // This relies on the event delegation set up by the Shortcuts class in // ~/behaviors/shortcuts/shortcuts.js. @@ -179,17 +167,12 @@ describe('HelpCenter component', () => { describe('showWhatsNew', () => { beforeEach(() => { - jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); beforeEach(() => { createWrapper({ ...sidebarData, show_version_check: true }); }); findButton("What's new 5").click(); }); - it('closes the dropdown', () => { - expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled(); - }); - it('shows the "What\'s new" slideout', () => { expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(expect.any(Object)); }); diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index 26a74036b10..b149a597fa2 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -120,17 +120,24 @@ describe('vue_shared/component/markdown/markdown_editor', () => { }); }); - it.each` - desc | supportsQuickActions - ${'passes render_quick_actions param to renderMarkdownPath if quick actions are enabled'} | ${true} - ${'does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled'} | ${false} - `('$desc', async ({ supportsQuickActions }) => { - buildWrapper({ propsData: { supportsQuickActions } }); + it('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => { + buildWrapper({ propsData: { supportsQuickActions: true } }); + + await enableContentEditor(); + + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toContain(`render_quick_actions=true`); + }); + + // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/411565 + // eslint-disable-next-line jest/no-disabled-tests + it.skip('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => { + buildWrapper({ propsData: { supportsQuickActions: false } }); await enableContentEditor(); expect(mock.history.post).toHaveLength(1); - expect(mock.history.post[0].url).toContain(`render_quick_actions=${supportsQuickActions}`); + expect(mock.history.post[0].url).toContain(`render_quick_actions=false`); }); it('enables content editor switcher when contentEditorEnabled prop is true', () => { @@ -178,7 +185,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined); }); - it('disables content editor when disabled prop is true', async () => { + // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/404734 + // eslint-disable-next-line jest/no-disabled-tests + it.skip('disables content editor when disabled prop is true', async () => { buildWrapper({ propsData: { disabled: true } }); await enableContentEditor(); diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js index f87c0e3f357..6abb941a834 100644 --- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js +++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js @@ -53,6 +53,15 @@ describe('WorkItemAwardEmoji component', () => { ); const workItemUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage)); const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0]; + const mockAwardEmojiDifferentUserThumbsUp = { + name: 'thumbsup', + __typename: 'AwardEmoji', + user: { + id: 'gid://gitlab/User/1', + name: 'John Doe', + __typename: 'UserCore', + }, + }; const createComponent = ({ mockWorkItemUpdateMutationHandler = [updateWorkItemMutation, workItemSuccessHandler], @@ -74,7 +83,8 @@ describe('WorkItemAwardEmoji component', () => { beforeEach(() => { isLoggedIn.mockReturnValue(true); window.gon = { - current_user_id: 1, + current_user_id: 5, + current_user_fullname: 'Dave Smith', }; createComponent(); @@ -85,7 +95,7 @@ describe('WorkItemAwardEmoji component', () => { expect(findAwardsList().props()).toEqual({ boundary: '', canAwardEmoji: true, - currentUserId: 1, + currentUserId: 5, defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN], selectedClass: 'selected', awards: [], @@ -101,6 +111,7 @@ describe('WorkItemAwardEmoji component', () => { name: EMOJI_THUMBSUP, user: { id: 5, + name: 'Dave Smith', }, }, { @@ -108,6 +119,35 @@ describe('WorkItemAwardEmoji component', () => { name: EMOJI_THUMBSDOWN, user: { id: 5, + name: 'Dave Smith', + }, + }, + ]); + }); + + it('renders awards list given by multiple users', () => { + createComponent({ + awardEmoji: { + ...mockAwardsWidget, + nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUserThumbsUp], + }, + }); + + expect(findAwardsList().props('awards')).toEqual([ + { + id: 1, + name: EMOJI_THUMBSUP, + user: { + id: 5, + name: 'Dave Smith', + }, + }, + { + id: 2, + name: EMOJI_THUMBSUP, + user: { + id: 1, + name: 'John Doe', }, }, ]); @@ -167,4 +207,49 @@ describe('WorkItemAwardEmoji component', () => { expect(findAwardsList().props('canAwardEmoji')).toBe(false); }); }); + + describe('when a different users awards same emoji', () => { + const awardEmojiDifferentUserSuccessHandler = jest.fn().mockResolvedValue( + updateWorkItemMutationResponseFactory({ + awardEmoji: { + ...mockAwardsWidget, + nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUserThumbsUp], + }, + }), + ); + + beforeEach(() => { + window.gon = { + current_user_id: 1, + current_user_fullname: 'John Doe', + }; + }); + + it('calls mutation succesfully', async () => { + createComponent({ + mockWorkItemUpdateMutationHandler: [ + updateWorkItemMutation, + awardEmojiDifferentUserSuccessHandler, + ], + awardEmoji: { + ...mockAwardsWidget, + nodes: [mockAwardEmojiThumbsUp], + }, + }); + + findAwardsList().vm.$emit('award', EMOJI_THUMBSUP); + + await waitForPromises(); + + expect(awardEmojiDifferentUserSuccessHandler).toHaveBeenCalledWith({ + input: { + id: mockWorkItem.id, + awardEmojiWidget: { + action: EMOJI_ACTION_ADD, + name: EMOJI_THUMBSUP, + }, + }, + }); + }); + }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index fd31074378b..96c14ef4dd1 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -51,6 +51,7 @@ export const mockAwardEmojiThumbsUp = { __typename: 'AwardEmoji', user: { id: 'gid://gitlab/User/5', + name: 'Dave Smith', __typename: 'UserCore', }, }; @@ -60,6 +61,7 @@ export const mockAwardEmojiThumbsDown = { __typename: 'AwardEmoji', user: { id: 'gid://gitlab/User/5', + name: 'Dave Smith', __typename: 'UserCore', }, }; diff --git a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb index 5e9d4f78a4a..9e37124ba28 100644 --- a/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb +++ b/spec/lib/gitlab/database/async_indexes/postgres_async_index_spec.rb @@ -6,6 +6,9 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model, it { is_expected.to be_a Gitlab::Database::SharedModel } describe 'validations' do + subject(:model) { build(:postgres_async_index) } + + let(:table_name_limit) { described_class::MAX_TABLE_NAME_LENGTH } let(:identifier_limit) { described_class::MAX_IDENTIFIER_LENGTH } let(:definition_limit) { described_class::MAX_DEFINITION_LENGTH } let(:last_error_limit) { described_class::MAX_LAST_ERROR_LENGTH } @@ -13,10 +16,45 @@ RSpec.describe Gitlab::Database::AsyncIndexes::PostgresAsyncIndex, type: :model, it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) } it { is_expected.to validate_presence_of(:table_name) } - it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) } + it { is_expected.to validate_length_of(:table_name).is_at_most(table_name_limit) } it { is_expected.to validate_presence_of(:definition) } it { is_expected.to validate_length_of(:definition).is_at_most(definition_limit) } it { is_expected.to validate_length_of(:last_error).is_at_most(last_error_limit) } + + shared_examples 'table_name is invalid' do + before do + model.table_name = table_name + end + + it 'is invalid' do + expect(model).to be_invalid + expect(model.errors).to have_key(:table_name) + end + end + + context 'when passing a long schema name' do + let(:table_name) { "#{'schema_name' * 10}.table_name" } + + it_behaves_like 'table_name is invalid' + end + + context 'when passing a long table name' do + let(:table_name) { "schema_name.#{'table_name' * 10}" } + + it_behaves_like 'table_name is invalid' + end + + context 'when passing a long table name and schema name' do + let(:table_name) { "#{'schema_name' * 10}.#{'table_name' * 10}" } + + it_behaves_like 'table_name is invalid' + end + + context 'when invalid table name is given' do + let(:table_name) { 'a.b.c' } + + it_behaves_like 'table_name is invalid' + end end describe 'scopes' do diff --git a/spec/lib/gitlab/database/load_balancing/host_spec.rb b/spec/lib/gitlab/database/load_balancing/host_spec.rb index d277514dea9..b040c7a76bd 100644 --- a/spec/lib/gitlab/database/load_balancing/host_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/host_spec.rb @@ -357,21 +357,28 @@ RSpec.describe Gitlab::Database::LoadBalancing::Host do it 'returns true when a host has caught up' do allow(host).to receive(:connection).and_return(connection) - expect(connection).to receive(:select_all).and_return([{ 'diff' => -1 }]) + expect(connection).to receive(:select_all).and_return([{ 'result' => 't' }]) expect(host.caught_up?('foo')).to eq(true) end - it 'returns false when diff query returns nothing' do + it 'returns true when a host has caught up' do + allow(host).to receive(:connection).and_return(connection) + expect(connection).to receive(:select_all).and_return([{ 'result' => true }]) + + expect(host.caught_up?('foo')).to eq(true) + end + + it 'returns false when a host has not caught up' do allow(host).to receive(:connection).and_return(connection) - expect(connection).to receive(:select_all).and_return([]) + expect(connection).to receive(:select_all).and_return([{ 'result' => 'f' }]) expect(host.caught_up?('foo')).to eq(false) end it 'returns false when a host has not caught up' do allow(host).to receive(:connection).and_return(connection) - expect(connection).to receive(:select_all).and_return([{ 'diff' => 123 }]) + expect(connection).to receive(:select_all).and_return([{ 'result' => false }]) expect(host.caught_up?('foo')).to eq(false) end diff --git a/spec/migrations/remove_old_async_index_table_name_length_constraint_spec.rb b/spec/migrations/remove_old_async_index_table_name_length_constraint_spec.rb new file mode 100644 index 00000000000..fdecf9a663b --- /dev/null +++ b/spec/migrations/remove_old_async_index_table_name_length_constraint_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RemoveOldAsyncIndexTableNameLengthConstraint, schema: 20230523074248, feature_category: :database do + let(:migration) { described_class.new } + let(:postgres_async_indexes) { table(:postgres_async_indexes) } + let(:old_length) { Gitlab::Database::MigrationHelpers::MAX_IDENTIFIER_NAME_LENGTH } + let(:long_table_name) { "#{'a' * old_length}.#{'b' * old_length}" } + + describe '.up' do + it 'allows inserting longer table names' do + migration.up + + expect do + postgres_async_indexes.create!( + name: 'some_index', + definition: '(id)', + table_name: long_table_name + ) + end.not_to raise_error + end + end + + describe '.down' do + it 'disallows inserting longer table names' do + migration.down + + expect do + postgres_async_indexes.create!( + name: 'some_index', + definition: '(id)', + table_name: long_table_name + ) + end.to raise_error(ActiveRecord::StatementInvalid) + end + + it 'cleans up records with too long table_name' do + migration.up + + # Delete + postgres_async_indexes.create!( + name: 'some_index', + definition: '(id)', + table_name: long_table_name + ) + + # Keep + postgres_async_indexes.create!( + name: 'other_index', + definition: '(id)', + table_name: 'short_name' + ) + + migration.down + + async_indexes = postgres_async_indexes.all + expect(async_indexes.size).to eq(1) + + expect(async_indexes.first.name).to eq('other_index') + end + end +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index edfac39728f..6192a271028 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -13,8 +13,10 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do it { expect(subject).to be_valid } describe 'associations' do - it { is_expected.to belong_to(:reporter).class_name('User') } - it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:reporter).class_name('User').inverse_of(:reported_abuse_reports) } + it { is_expected.to belong_to(:resolved_by).class_name('User').inverse_of(:resolved_abuse_reports) } + it { is_expected.to belong_to(:assignee).class_name('User').inverse_of(:assigned_abuse_reports) } + it { is_expected.to belong_to(:user).inverse_of(:abuse_reports) } it { is_expected.to have_many(:events).class_name('ResourceEvents::AbuseReportEvent').inverse_of(:abuse_report) } it "aliases reporter to author" do @@ -28,8 +30,8 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do let(:ftp) { 'ftp://example.com' } let(:javascript) { 'javascript:alert(window.opener.document.location)' } - it { is_expected.to validate_presence_of(:reporter) } - it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:reporter).on(:create) } + it { is_expected.to validate_presence_of(:user).on(:create) } it { is_expected.to validate_presence_of(:message) } it { is_expected.to validate_presence_of(:category) } @@ -47,6 +49,8 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do it { is_expected.to allow_value('http://localhost:9000').for(:reported_from_url) } it { is_expected.to allow_value('https://gitlab.com').for(:reported_from_url) } + it { is_expected.to validate_length_of(:mitigation_steps).is_at_most(1000).allow_blank } + it { is_expected.to allow_value([]).for(:links_to_spam) } it { is_expected.to allow_value(nil).for(:links_to_spam) } it { is_expected.to allow_value('').for(:links_to_spam) } @@ -82,6 +86,48 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do it { is_expected.to allow_value(nil).for(:screenshot) } it { is_expected.to allow_value('').for(:screenshot) } end + + describe 'evidence' do + it { is_expected.not_to allow_value("string").for(:evidence) } + it { is_expected.not_to allow_value(1.0).for(:evidence) } + + it { is_expected.to allow_value(nil).for(:evidence) } + + it { + is_expected.to allow_value( + { + issues: [ + { + id: 1, + title: "test issue title", + description: "test issue content" + } + ], + snippets: [ + { + id: 2, + content: "snippet content" + } + ], + notes: [ + { + id: 44, + content: "notes content" + } + ], + user: { + login_count: 1, + account_age: 3, + spam_score: 0.3, + telesign_score: 0.4, + arkos_score: 0.2, + pvs_score: 0.8, + product_coverage: 0.8, + virus_total_score: 0.2 + } + }).for(:evidence) + } + end end describe 'scopes' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a2d87d32d44..29e99dbfacb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -155,7 +155,10 @@ RSpec.describe User, feature_category: :user_profile do it { is_expected.to have_many(:chat_names).dependent(:destroy) } it { is_expected.to have_many(:saved_replies).class_name('::Users::SavedReply') } it { is_expected.to have_many(:uploads) } - it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } + it { is_expected.to have_many(:abuse_reports).dependent(:nullify).inverse_of(:user) } + it { is_expected.to have_many(:reported_abuse_reports).dependent(:nullify).class_name('AbuseReport').inverse_of(:reporter) } + it { is_expected.to have_many(:assigned_abuse_reports).class_name('AbuseReport').inverse_of(:assignee) } + it { is_expected.to have_many(:resolved_abuse_reports).class_name('AbuseReport').inverse_of(:resolved_by) } it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') } it { is_expected.to have_many(:releases).dependent(:nullify) } it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:user) } diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb index fe7210e4372..5b7da9ce84f 100644 --- a/spec/requests/ide_controller_spec.rb +++ b/spec/requests/ide_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe IdeController, feature_category: :web_ide do + include ContentSecurityPolicyHelpers using RSpec::Parameterized::TableSyntax let_it_be(:reporter) { create(:user) } @@ -20,18 +21,6 @@ RSpec.describe IdeController, feature_category: :web_ide do let(:user) { creator } - def find_csp_source(key) - csp = response.headers['Content-Security-Policy'] - - # Transform "default-src foo bar; connect-src foo bar; script-src ..." - # into array of values for a single directive based on the given key - csp.split(';') - .map(&:strip) - .find { |entry| entry.starts_with?(key) } - .split(' ') - .drop(1) - end - before do stub_feature_flags(vscode_web_ide: true) sign_in(user) @@ -198,8 +187,8 @@ RSpec.describe IdeController, feature_category: :web_ide do it 'updates the content security policy with the correct frame sources' do subject - expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") - expect(find_csp_source('worker-src')).to include("http://www.example.com/assets/webpack/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('worker-src')).to include("http://www.example.com/assets/webpack/") end it 'with relative_url_root, updates the content security policy with the correct frame sources' do @@ -207,8 +196,8 @@ RSpec.describe IdeController, feature_category: :web_ide do subject - expect(find_csp_source('frame-src')).to include("http://www.example.com/gitlab/assets/webpack/") - expect(find_csp_source('worker-src')).to include("http://www.example.com/gitlab/assets/webpack/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/gitlab/assets/webpack/") + expect(find_csp_directive('worker-src')).to include("http://www.example.com/gitlab/assets/webpack/") end end end diff --git a/spec/requests/web_ide/remote_ide_controller_spec.rb b/spec/requests/web_ide/remote_ide_controller_spec.rb index 9e9d3dfc703..62f5cb90e0a 100644 --- a/spec/requests/web_ide/remote_ide_controller_spec.rb +++ b/spec/requests/web_ide/remote_ide_controller_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_development do + include ContentSecurityPolicyHelpers using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } @@ -63,7 +64,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct connect sources" do - expect(find_csp_source('connect-src')).to include( + expect(find_csp_directive('connect-src')).to include( "ws://#{remote_host}", "wss://#{remote_host}", "http://#{remote_host}", @@ -72,7 +73,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct frame sources" do - expect(find_csp_source('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") + expect(find_csp_directive('frame-src')).to include("http://www.example.com/assets/webpack/", "https://*.vscode-cdn.net/") end end @@ -84,7 +85,7 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen end it "updates the content security policy with the correct remote_host" do - expect(find_csp_source('connect-src')).to include( + expect(find_csp_directive('connect-src')).to include( "ws://#{remote_host}", "wss://#{remote_host}", "http://#{remote_host}", @@ -122,18 +123,6 @@ RSpec.describe WebIde::RemoteIdeController, feature_category: :remote_developmen } end - def find_csp_source(key) - csp = response.headers['Content-Security-Policy'] - - # Transform "default-src foo bar; connect-src foo bar; script-src ..." - # into array of values for a single directive based on the given key - csp.split(';') - .map(&:strip) - .find { |entry| entry.starts_with?(key) } - .split(' ') - .drop(1) - end - def post_to_remote_ide params = { connection_token: connection_token, diff --git a/spec/support/helpers/content_security_policy_helpers.rb b/spec/support/helpers/content_security_policy_helpers.rb index 7e3de9fd219..50a1bb62bc5 100644 --- a/spec/support/helpers/content_security_policy_helpers.rb +++ b/spec/support/helpers/content_security_policy_helpers.rb @@ -17,4 +17,22 @@ any_time: false) end end end + + # Finds the given csp directive values as an array + # + # Example: + # ``` + # find_csp_directive('connect-src') + # ``` + def find_csp_directive(key) + csp = response.headers['Content-Security-Policy'] + + # Transform "default-src foo bar; connect-src foo bar; script-src ..." + # into array of values for a single directive based on the given key + csp.split(';') + .map(&:strip) + .find { |entry| entry.starts_with?(key) } + .split(' ') + .drop(1) + end end diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index a484452b9e9..46c8ebc01be 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -344,42 +344,52 @@ RSpec.shared_examples 'work items todos' do end RSpec.shared_examples 'work items award emoji' do - let(:award_section_selector) { '[data-testid="work-item-award-list"]' } - let(:award_action_selector) { '[data-testid="award-button"]' } - let(:selected_award_action_selector) { '[data-testid="award-button"].selected' } - let(:emoji_picker_action_selector) { '[data-testid="emoji-picker"]' } + let(:award_section_selector) { '.awards' } + let(:award_button_selector) { '[data-testid="award-button"]' } + let(:selected_award_button_selector) { '[data-testid="award-button"].selected' } + let(:emoji_picker_button_selector) { '[data-testid="emoji-picker"]' } let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' } + let(:tooltip_selector) { '.gl-tooltip' } def select_emoji - first(award_action_selector).click + page.within(award_section_selector) do + page.first(award_button_selector).click + end wait_for_requests end - it 'adds award to the work item' do + it 'adds award to the work item for current user' do + select_emoji + within(award_section_selector) do - select_emoji + expect(page).to have_selector(selected_award_button_selector) - expect(page).to have_selector(selected_award_action_selector) - expect(first(award_action_selector)).to have_content '1' + # As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2 + expect(first(award_button_selector)).to have_content '2' end + expect(page.find(tooltip_selector)).to have_content("You and John reacted with :thumbsup:") end - it 'removes award from work item' do - within(award_section_selector) do - select_emoji + it 'removes award from work item for current user' do + select_emoji - expect(first(award_action_selector)).to have_content '1' + page.within(award_section_selector) do + # As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2 + expect(first(award_button_selector)).to have_content '2' + end - select_emoji + select_emoji - expect(first(award_action_selector)).to have_content '0' + page.within(award_section_selector) do + # The emoji count will be back to 1 + expect(first(award_button_selector)).to have_content '1' end end - it 'add custom award to the work item' do + it 'add custom award to the work item for current user' do within(award_section_selector) do - find(emoji_picker_action_selector).click + find(emoji_picker_button_selector).click find(basketball_emoji_selector).click expect(page).to have_selector(basketball_emoji_selector) diff --git a/tooling/danger/analytics_instrumentation.rb b/tooling/danger/analytics_instrumentation.rb index ce5ee55e3ee..767ae8dfb4c 100644 --- a/tooling/danger/analytics_instrumentation.rb +++ b/tooling/danger/analytics_instrumentation.rb @@ -8,7 +8,7 @@ module Tooling APPROVED_LABEL = 'analytics instrumentation::approved' REVIEW_LABEL = 'analytics instrumentation::review pending' CHANGED_FILES_MESSAGE = <<~MSG - For the following files, a review from the [Data team and Analytics Instrumentation team](https://gitlab.com/groups/gitlab-org/analytics-section/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) is recommended + For the following files, a review from the [Data team and Analytics Instrumentation team](https://gitlab.com/groups/gitlab-org/analytics-section/analytics-instrumentation/engineers/-/group_members?with_inherited_permissions=exclude) is recommended Please check the ~"analytics instrumentation" [Service Ping guide](https://docs.gitlab.com/ee/development/service_ping/) or the [Snowplow guide](https://docs.gitlab.com/ee/development/snowplow/). For MR review guidelines, see the [Service Ping review guidelines](https://docs.gitlab.com/ee/development/service_ping/review_guidelines.html) or the [Snowplow review guidelines](https://docs.gitlab.com/ee/development/snowplow/review_guidelines.html). diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue index 2d354d605f8..d8f5023555c 100644 --- a/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue +++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/DynamicScroller.vue @@ -45,19 +45,21 @@ export default { provide () { if (typeof ResizeObserver !== 'undefined') { this.$_resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - if (entry.target) { - const event = new CustomEvent( - 'resize', - { - detail: { - contentRect: entry.contentRect, + requestAnimationFrame(() => { + for (const entry of entries) { + if (entry.target) { + const event = new CustomEvent( + 'resize', + { + detail: { + contentRect: entry.contentRect, + }, }, - }, - ) - entry.target.dispatchEvent(event) + ) + entry.target.dispatchEvent(event) + } } - } + }); }) } diff --git a/yarn.lock b/yarn.lock index 8b17d2637b6..703a30a9e61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1115,10 +1115,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.47.0.tgz#1a18f008aef1ecb5407688017c3bbdbc597b7ec1" integrity sha512-xP8AyuFYRFmlxtcBYRqCnLmBgMjrACa0mUliRk/hAKUWcXoz/U4vdK69T1DhWalVi4cpUqmi4+rrIWI6fBdzew== -"@gitlab/ui@62.10.0": - version "62.10.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.10.0.tgz#b93b4dca47a8ca0e4f6bf2ab1e49b783c348d27c" - integrity sha512-Ektyickh1tem5L7WTWFmmie5T5heQE3TaPj55xqs53nljl92ZHq8za703uINnvE2j2SSuMyRqtAhHjglB+wRmA== +"@gitlab/ui@62.12.0": + version "62.12.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.12.0.tgz#936f09121dcdf985281e622b1c102c1c343d22bf" + integrity sha512-a29tvQQBQATkMy1BxPrnxhmSJk8miuRNq5bK/Tt94nRzf+4tRQ3JVf9eJX/km1/V1WgqvAzDSPZV46ptT061XA== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.23.1" |