diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue index af4349083b6..be7c0b68b4c 100644 --- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue @@ -265,7 +265,6 @@ export default { {{ __('Mask variable') }} diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index ca4d8da2482..e1cca5adc73 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -8,6 +8,7 @@ import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin'; import PipelinesService from '~/pipelines/services/pipelines_service'; import PipelineStore from '~/pipelines/stores/pipelines_store'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -19,7 +20,7 @@ export default { TablePagination, SvgBlankState, }, - mixins: [PipelinesMixin], + mixins: [PipelinesMixin, glFeatureFlagMixin()], props: { endpoint: { type: String, @@ -90,6 +91,9 @@ export default { canRenderPipelineButton() { return this.latestPipelineDetachedFlag; }, + pipelineButtonClass() { + return !this.glFeatures.newPipelinesTable ? 'gl-md-display-none' : 'gl-lg-display-none'; + }, isForkMergeRequest() { return this.sourceProjectFullPath !== this.targetProjectFullPath; }, @@ -192,7 +196,8 @@ export default { diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js index fd75d476b86..fb9b7d61fd2 100644 --- a/app/assets/javascripts/packages/details/store/getters.js +++ b/app/assets/javascripts/packages/details/store/getters.js @@ -122,8 +122,8 @@ export const gradleGroovyInstalCommand = ({ packageEntity }) => { export const gradleGroovyAddSourceCommand = ({ mavenPath }) => // eslint-disable-next-line @gitlab/require-i18n-strings - `gitlab { - url "${mavenPath}" + `maven { + url '${mavenPath}' }`; export const groupExists = ({ groupListUrl }) => groupListUrl.length > 0; diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js index 1971323abac..cc9a9b6cc38 100644 --- a/app/assets/javascripts/pages/admin/projects/index/index.js +++ b/app/assets/javascripts/pages/admin/projects/index/index.js @@ -6,7 +6,7 @@ import Translate from '~/vue_shared/translate'; import deleteProjectModal from './components/delete_project_modal.vue'; -document.addEventListener('DOMContentLoaded', () => { +(() => { Vue.use(Translate); const deleteProjectModalEl = document.getElementById('delete-project-modal'); @@ -39,4 +39,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +})(); diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index fc0a8b07e7f..ec50253c939 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -106,8 +106,8 @@ export default { height: this.$refs[this.containerId].scrollHeight, }; }, - onError(errorType) { - this.$emit('error', errorType); + onError(payload) { + this.$emit('error', payload); }, setJob(jobName) { this.hoveredJobName = jobName; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index ed33a94af6e..962f2ca2a4c 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -73,7 +73,11 @@ export default { return unwrapPipelineData(this.pipelineProjectPath, data); }, error(err) { - this.reportFailure(LOAD_FAILURE, serializeLoadErrors(err)); + this.reportFailure({ type: LOAD_FAILURE, skipSentry: true }); + reportToSentry( + this.$options.name, + `type: ${LOAD_FAILURE}, info: ${serializeLoadErrors(err)}`, + ); }, result({ error }) { /* @@ -134,11 +138,15 @@ export default { refreshPipelineGraph() { this.$apollo.queries.pipeline.refetch(); }, - reportFailure(type, err = '') { + /* eslint-disable @gitlab/require-i18n-strings */ + reportFailure({ type, err = 'No error string passed.', skipSentry = false }) { this.showAlert = true; this.alertType = type; - reportToSentry(this.$options.name, `type: ${this.alertType}, info: ${err}`); + if (!skipSentry) { + reportToSentry(this.$options.name, `type: ${type}, info: ${err}`); + } }, + /* eslint-enable @gitlab/require-i18n-strings */ }, }; diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 939400eb1c3..b55a77a3c4f 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -111,14 +111,12 @@ export default { this.loadingPipelineId = null; this.$emit('scrollContainer'); }, - error(err, _vm, _key, type) { - this.$emit('error', LOAD_FAILURE); + error(err) { + this.$emit('error', { type: LOAD_FAILURE, skipSentry: true }); reportToSentry( 'linked_pipelines_column', - `error type: ${LOAD_FAILURE}, error: ${serializeLoadErrors( - err, - )}, apollo error type: ${type}`, + `error type: ${LOAD_FAILURE}, error: ${serializeLoadErrors(err)}`, ); }, }); diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue index 84ca0bf1443..f4215d67292 100644 --- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue +++ b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue @@ -170,7 +170,7 @@ export default { const parsedData = parseData(arrayOfJobs); this.links = generateLinksData(parsedData, this.containerId, `-${this.pipelineId}`); } catch (err) { - this.$emit('error', DRAW_FAILURE); + this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false }); reportToSentry(this.$options.name, err); } this.finishPerfMeasureAndSend(); diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue index 278089b6155..543bdf94307 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue @@ -48,6 +48,9 @@ export default { legacyTableMobileClass() { return !this.glFeatures.newPipelinesTable ? 'table-mobile-content' : ''; }, + showInProgress() { + return !this.duration && !this.finishedTime; + }, }, }; @@ -57,6 +60,11 @@ export default { {{ s__('Pipeline|Duration') }}
+ + + {{ s__('Pipeline|In progress') }} + +

{{ durationFormatted }} diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 73e7476b55d..b8c2255ab7e 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -18,7 +18,7 @@ module BoardsHelper time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s, recent_boards_endpoint: recent_boards_path, parent: current_board_parent.model_name.param_key, - group_id: @group&.id, + group_id: group_id, labels_filter_base_path: build_issue_link_base, labels_fetch_path: labels_fetch_path, labels_manage_path: labels_manage_path, @@ -26,6 +26,12 @@ module BoardsHelper } end + def group_id + return @group.id if board.group_board? + + @project&.group&.id + end + def full_path if board.group_board? @group.full_path diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index 21345e4d80e..8f2bdd109cb 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -4,8 +4,8 @@ %fieldset .form-group .form-check - = f.check_box :performance_bar_enabled, class: 'form-check-input' - = f.label :performance_bar_enabled, class: 'form-check-label qa-enable-performance-bar-checkbox' do + = f.check_box :performance_bar_enabled, class: 'form-check-input', data: { qa_selector: 'enable_performance_bar_checkbox'} + = f.label :performance_bar_enabled, class: 'form-check-label' do Enable access to the Performance Bar .form-group = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-bold' diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index f6b635173a5..8b1bf37ff10 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -55,7 +55,7 @@ %div{ :class => "col-sm-12" } .form-check - experiment(:new_project_readme, actor: current_user) do |e| - = check_box_tag 'project[initialize_with_readme]', '1', e.run, class: 'form-check-input qa-initialize-with-readme-checkbox', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme", track_value: "" } + = check_box_tag 'project[initialize_with_readme]', '1', e.run, class: 'form-check-input', data: { qa_selector: "initialize_with_readme_checkbox", track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme", track_value: "" } = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do .option-title %strong= s_('ProjectsNew|Initialize repository with a README') diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index f6e4442d4fb..56906eb6e66 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -21,8 +21,8 @@ .form-actions - if @milestone.new_record? - = f.submit _('Create milestone'), class: 'gl-button btn-success btn', data: { qa_selector: 'create_milestone_button' } - = link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-cancel' + = f.submit _('Create milestone'), class: 'gl-button btn-confirm btn', data: { qa_selector: 'create_milestone_button' } + = link_to _('Cancel'), project_milestones_path(@project), class: 'gl-button btn btn-default btn-cancel' - else - = f.submit _('Save changes'), class: 'gl-button btn-success btn' - = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'gl-button btn btn-cancel' + = f.submit _('Save changes'), class: 'gl-button btn-confirm btn' + = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'gl-button btn btn-default btn-cancel' diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index c0df986e1ca..059ef53c42b 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -8,7 +8,7 @@ = render 'shared/milestones/search_form' = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-success', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do + = link_to new_project_milestone_path(@project), class: 'gl-button btn btn-confirm', data: { qa_selector: "new_project_milestone_link" }, title: _('New milestone') do = _('New milestone') - if @milestones.blank? diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index bcdc2988e49..5a1e263141d 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -35,7 +35,7 @@ = link_to _('Learn more.'), help_page_path('user/project/repository/repository_mirroring', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' .panel-footer - = f.submit _('Mirror repository'), class: 'gl-button btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror + = f.submit _('Mirror repository'), class: 'gl-button btn btn-confirm js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror - else .gl-alert.gl-alert-info{ role: 'alert' } = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 904b3d6f483..58d125acc2d 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -56,7 +56,7 @@ %tr.build-state.responsive-table-border-start %td.responsive-table-cell.ci-status-icon-failed{ data: { column: _('Status')} } .d-none.d-md-block.build-icon - = custom_icon("icon_status_#{build.status}") + = sprite_icon("status_#{build.status}") .d-md-none.build-badge = render "ci/status/badge", link: false, status: job.detailed_status(current_user) %td.responsive-table-cell.build-name{ data: { column: _('Name')} } diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 32b1af50a01..0210ed8c9b3 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -3,7 +3,6 @@ - use_creator_avatar = false unless local_assigns[:use_creator_avatar] == true - stars = true unless local_assigns[:stars] == false - forks = true unless local_assigns[:forks] == false -- merge_requests = true unless local_assigns[:merge_requests] == false - pipeline_status = true unless local_assigns[:pipeline_status] == false - skip_namespace = false unless local_assigns[:skip_namespace] == true - user = local_assigns[:user] @@ -39,8 +38,9 @@ - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, use_creator_avatar: use_creator_avatar, - forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, merge_requests: merge_requests, - issues: project.issues_enabled?, pipeline_status: pipeline_status, compact_mode: compact_mode + forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user, + merge_requests: project.merge_requests_enabled?, issues: project.issues_enabled?, + pipeline_status: pipeline_status, compact_mode: compact_mode = paginate_collection(projects, remote: remote) unless skip_pagination - else - if @contributed_projects diff --git a/changelogs/unreleased/263532-instrument-usage-ping-count-number-of-enabled-integrations-per-pro.yml b/changelogs/unreleased/263532-instrument-usage-ping-count-number-of-enabled-integrations-per-pro.yml new file mode 100644 index 00000000000..d08f1a76fe2 --- /dev/null +++ b/changelogs/unreleased/263532-instrument-usage-ping-count-number-of-enabled-integrations-per-pro.yml @@ -0,0 +1,5 @@ +--- +title: 'Usage ping: Histogram for enabled integrations per project' +merge_request: 55782 +author: +type: added diff --git a/changelogs/unreleased/btn-confirm-project-milestones.yml b/changelogs/unreleased/btn-confirm-project-milestones.yml new file mode 100644 index 00000000000..b03e041f1a4 --- /dev/null +++ b/changelogs/unreleased/btn-confirm-project-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in milestones directory +merge_request: 56342 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/btn-confirm-project-mirrors.yml b/changelogs/unreleased/btn-confirm-project-mirrors.yml new file mode 100644 index 00000000000..9f847c6fdac --- /dev/null +++ b/changelogs/unreleased/btn-confirm-project-mirrors.yml @@ -0,0 +1,5 @@ +--- +title: Move from btn-success to btn-confirm in mirrors directory +merge_request: 56343 +author: Yogi (@yo) +type: changed diff --git a/changelogs/unreleased/cromefire_-master-patch-00495.yml b/changelogs/unreleased/cromefire_-master-patch-00495.yml new file mode 100644 index 00000000000..e9a9f52ae69 --- /dev/null +++ b/changelogs/unreleased/cromefire_-master-patch-00495.yml @@ -0,0 +1,5 @@ +--- +title: Correct generated maven repository instruction for Gradle Groovy DSL +merge_request: 56318 +author: Cromefire_ (@cromefire_) +type: fixed diff --git a/changelogs/unreleased/hide-mr-counts-in-project-list-where-mrs-disabled.yml b/changelogs/unreleased/hide-mr-counts-in-project-list-where-mrs-disabled.yml new file mode 100644 index 00000000000..be9cfbdfecd --- /dev/null +++ b/changelogs/unreleased/hide-mr-counts-in-project-list-where-mrs-disabled.yml @@ -0,0 +1,5 @@ +--- +title: Hide MR count and link in project list where MRs are disabled +merge_request: !56432 +author: Simon Stieger @sim0 +type: fixed diff --git a/changelogs/unreleased/pb-add-hourglass-icon-to-empty-duration-cell.yml b/changelogs/unreleased/pb-add-hourglass-icon-to-empty-duration-cell.yml new file mode 100644 index 00000000000..f7fe185d7e3 --- /dev/null +++ b/changelogs/unreleased/pb-add-hourglass-icon-to-empty-duration-cell.yml @@ -0,0 +1,5 @@ +--- +title: Display in progress for pipeline duration cell when pipeline has not finished running +merge_request: 56266 +author: +type: changed diff --git a/config/feature_flags/development/value_stream_analytics_extended_form.yml b/config/feature_flags/development/value_stream_analytics_extended_form.yml index 1cdb9111af4..f74c85309c1 100644 --- a/config/feature_flags/development/value_stream_analytics_extended_form.yml +++ b/config/feature_flags/development/value_stream_analytics_extended_form.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294190 milestone: '13.7' type: development group: group::optimize -default_enabled: false +default_enabled: true diff --git a/config/metrics/counts_all/20210309165717_projects_with_enabled_alert_integrations_histogram.yml b/config/metrics/counts_all/20210309165717_projects_with_enabled_alert_integrations_histogram.yml new file mode 100644 index 00000000000..6d393fad3a3 --- /dev/null +++ b/config/metrics/counts_all/20210309165717_projects_with_enabled_alert_integrations_histogram.yml @@ -0,0 +1,20 @@ +--- +key_path: usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram +description: Histogram (buckets 1 to 100) of projects with at least 1 enabled integration. +product_section: ops +product_stage: monitor +product_group: group::monitor +product_category: incident_management +value_type: object +status: data_available +milestone: "13.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55782 +time_frame: all +data_source: database +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/schema.json b/config/metrics/schema.json index 73a7ee65dec..cc1eafcf0ba 100644 --- a/config/metrics/schema.json +++ b/config/metrics/schema.json @@ -22,7 +22,7 @@ }, "value_type": { "type": "string", - "enum": ["string", "number", "boolean"] + "enum": ["string", "number", "boolean", "object"] }, "status": { "type": ["string"], diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index bd9a60e952c..122f9caebe7 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -286,8 +286,8 @@ The visual review tools retrieve the merge request ID from the `data-merge-reque data attribute included in the `script` HTML tag used to add the visual review tools to your review app. -​After determining the ID for the merge request to link to a visual review app, you -can supply the ID by either:​​ +After determining the ID for the merge request to link to a visual review app, you +can supply the ID by either: - Hard-coding it in the script tag via the data attribute `data-merge-request-id` of the app. - Dynamically adding the `data-merge-request-id` value during the build of the app. diff --git a/doc/development/cicd/templates.md b/doc/development/cicd/templates.md index 3e7ae39cb7d..ed0d217c247 100644 --- a/doc/development/cicd/templates.md +++ b/doc/development/cicd/templates.md @@ -72,6 +72,10 @@ Please read [versioning](#versioning) section for introducing breaking change sa When a root `.gitlab-ci.yml` [includes](../../ci/yaml/README.md#include) multiple templates, these global keywords could be overridden by the others and cause an unexpected behavior. +- Include [a changelog](../changelog.md) if your merge request introduces a user-facing change. +- Use [`$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH`](../../ci/variables/predefined_variables.md) + instead of a hardcoded `main` branch, and never use `master`. +- Use [`rules`](../../ci/yaml/README.md#rules) instead of [`only` or `except`](../../ci/yaml/README.md#onlyexcept-basic), if possible. ## Versioning diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 32790c44249..8860f3d1abf 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -1028,6 +1028,18 @@ Status: `data_available` Tiers: `premium`, `ultimate` +### `counts.geo_node_usage.git_fetch_event_count_weekly` + +Number of Git fetch events from Prometheus on the Geo secondary + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210309194425_git_fetch_event_count_weekly.yml) + +Group: `group::geo` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + ### `counts.geo_nodes` Total number of sites in a Geo deployment @@ -14840,6 +14852,18 @@ Status: `data_available` Tiers: `free` +### `usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram` + +Histogram (buckets 1 to 100) of projects with at least 1 enabled integration. + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210309165717_projects_with_enabled_alert_integrations_histogram.yml) + +Group: `group::monitor` + +Status: `data_available` + +Tiers: `free`, `premium`, `ultimate` + ### `usage_activity_by_stage.monitor.projects_with_error_tracking_enabled` Projects where error tracking is enabled diff --git a/doc/development/usage_ping/metrics_dictionary.md b/doc/development/usage_ping/metrics_dictionary.md index 3f43aaa916f..3c08fb0cc87 100644 --- a/doc/development/usage_ping/metrics_dictionary.md +++ b/doc/development/usage_ping/metrics_dictionary.md @@ -32,7 +32,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields: | `product_stage` | no | The [stage](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) for the metric. | | `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the metric. | | `product_category` | no | The [product category](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml) for the metric. | -| `value_type` | yes | `string`; one of `string`, `number`, `boolean`. | +| `value_type` | yes | `string`; one of `string`, `number`, `boolean`, `object`. | | `status` | yes | `string`; status of the metric, may be set to `data_available`, `planned`, `in_progress`, `implemented`, `not_used`, `deprecated` | | `time_frame` | yes | `string`; may be set to a value like `7d`, `28d`, `all`, `none`. | | `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `ruby`. | diff --git a/doc/development/value_stream_analytics.md b/doc/development/value_stream_analytics.md index b3692bd1d2c..6b2442f1c32 100644 --- a/doc/development/value_stream_analytics.md +++ b/doc/development/value_stream_analytics.md @@ -90,7 +90,7 @@ Some start/end event pairs are not "compatible" with each other. For example: - "Issue closed" to "Issue closed": Duration is always 0. The `StageEvents` module describes the allowed `start_event` and `end_event` pairings (`PAIRING_RULES` constant). If a new event is added, it needs to be registered in this module. -​To add a new event:​ +To add a new event: 1. Add an entry in `ENUM_MAPPING` with a unique number, which is used in the `Stage` model as `enum`. 1. Define which events are compatible with the event in the `PAIRING_RULES` hash. @@ -190,9 +190,9 @@ Currently supported parents: ### Default stages The [original implementation](https://gitlab.com/gitlab-org/gitlab/-/issues/847) of value stream analytics defined 7 stages. These stages are always available for each parent, however altering these stages is not possible. -​ + To make things efficient and reduce the number of records created, the default stages are expressed as in-memory objects (not persisted). When the user creates a custom stage for the first time, all the stages are persisted. This behavior is implemented in the value stream analytics service objects. -​ + The reason for this was that we'd like to add the abilities to hide and order stages later on. ## Data Collector diff --git a/doc/user/group/value_stream_analytics/img/delete_value_stream_v13.4.png b/doc/user/group/value_stream_analytics/img/delete_value_stream_v13_4.png similarity index 100% rename from doc/user/group/value_stream_analytics/img/delete_value_stream_v13.4.png rename to doc/user/group/value_stream_analytics/img/delete_value_stream_v13_4.png diff --git a/doc/user/group/value_stream_analytics/img/extended_value_stream_form_v13_10.png b/doc/user/group/value_stream_analytics/img/extended_value_stream_form_v13_10.png new file mode 100644 index 00000000000..26508787177 Binary files /dev/null and b/doc/user/group/value_stream_analytics/img/extended_value_stream_form_v13_10.png differ diff --git a/doc/user/group/value_stream_analytics/img/vsa_custom_stage_v13_10.png b/doc/user/group/value_stream_analytics/img/vsa_custom_stage_v13_10.png new file mode 100644 index 00000000000..77f4a26b880 Binary files /dev/null and b/doc/user/group/value_stream_analytics/img/vsa_custom_stage_v13_10.png differ diff --git a/doc/user/group/value_stream_analytics/img/vsa_default_stage_v13_10.png b/doc/user/group/value_stream_analytics/img/vsa_default_stage_v13_10.png new file mode 100644 index 00000000000..1adb114b025 Binary files /dev/null and b/doc/user/group/value_stream_analytics/img/vsa_default_stage_v13_10.png differ diff --git a/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13.3.png b/doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_3.png similarity index 100% rename from doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13.3.png rename to doc/user/group/value_stream_analytics/img/vsa_filter_bar_v13_3.png diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md index 4fcef07a04e..52cf51d85a4 100644 --- a/doc/user/group/value_stream_analytics/index.md +++ b/doc/user/group/value_stream_analytics/index.md @@ -59,7 +59,7 @@ To filter results: 1. Select a parameter to filter by. 1. Select a value from the autocompleted results, or type to refine the results. -![Value stream analytics filter bar](img/vsa_filter_bar_v13.3.png "Active filter bar for value stream analytics") +![Value stream analytics filter bar](img/vsa_filter_bar_v13_3.png "Active filter bar for value stream analytics") ### Date ranges @@ -299,10 +299,59 @@ To create a value stream: 1. Navigate to your group's **Analytics > Value Stream**. 1. Click the Value stream dropdown and select **Create new Value Stream** 1. Fill in a name for the new Value Stream + - You can [customize the stages](#creating-a-value-stream-with-stages) as the `value_stream_analytics_extended_form` feature flag is enabled. 1. Click the **Create Value Stream** button. ![New value stream](img/new_value_stream_v13_3.png "Creating a new value stream") +#### Creating a value stream with stages + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55572) in GitLab 13.10. +> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default. +> - It's enabled on GitLab.com. +> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](../../../administration/feature_flags.md). **(FREE SELF)** + +WARNING: +This feature might not be available to you. Check the **version history** note above for details. + +You can create value streams with stages, starting with a default or a blank template. You can +add stages as desired. + +To create a value stream with stages: + +1. Navigate to your group's **Analytics > Value Stream**. +1. Find and select the Value Stream dropdown. Select **Create new Value Stream**. +1. Select either **Create from default template** or **Create from no template**. + - Default stages in the value stream can be hidden or re-ordered + ![Default stage actions](img/vsa_default_stage_v13_10.png "Default stage actions") + - New stages can be added by clicking the 'Add another stage' button + - The name, start and end events for the stage can be selected + ![Custom stage actions](img/vsa_custom_stage_v13_10.png "Custom stage actions") +1. Select the **Create Value Stream** button to save the value stream. + +![Extended create value stream form](img/extended_value_stream_form_v13_10.png "Extended create value stream form") + +#### Enable or disable value stream with stages + +Value streams with stages is under development but ready for production use. +It is deployed behind a feature flag that is **enabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can opt to disable it. + +To enable it: + +```ruby +# For the instance +Feature.enable(:value_stream_analytics_extended_form) +``` + +To disable it: + +```ruby +# For the instance +Feature.disable(:value_stream_analytics_extended_form) +``` + ### Deleting a value stream > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221205) in GitLab 13.4. @@ -314,7 +363,7 @@ To delete a custom value stream: 1. Click the **Delete (name of value stream)**. 1. Click the **Delete** button to confirm. -![Delete value stream](img/delete_value_stream_v13.4.png "Deleting a custom value stream") +![Delete value stream](img/delete_value_stream_v13_4.png "Deleting a custom value stream") ## Days to completion chart diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index ce0b6fcaddd..ad2d2ac2a8e 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -95,9 +95,11 @@ Runners log in to the Dependency Proxy automatically. To pull through the Dependency Proxy, use the `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` [predefined CI/CD variable](../../../ci/variables/predefined_variables.md): +Example pulling the latest alpine image: + ```yaml # .gitlab-ci.yml -image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/node:latest +image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:latest ``` There are other additional predefined CI/CD variables you can also use: @@ -125,13 +127,20 @@ To store a Docker image in Dependency Proxy storage: 1. Go to your group's **Packages & Registries > Dependency Proxy**. 1. Copy the **Dependency Proxy URL**. 1. Use one of these commands. In these examples, the image is `alpine:latest`. +1. You can also pull images by digest to specify exactly which version of an image to pull. - - Add the URL to your [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image) file: + - Pull an image by tag by adding the image to your [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image) file: ```shell image: gitlab.example.com/groupname/dependency_proxy/containers/alpine:latest ``` + - Pull an image by digest by adding the image to your [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image) file: + + ```shell + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine@sha256:c9375e662992791e3f39e919b26f510e5254b42792519c180aad254e6b38f4dc + ``` + - Manually pull the Docker image: ```shell diff --git a/lib/api/groups.rb b/lib/api/groups.rb index a8b1cdab021..26fa00d6186 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -137,6 +137,10 @@ module API end end # rubocop: enable CodeReuse/ActiveRecord + + def authorize_group_creation! + authorize! :create_group + end end resource :groups do @@ -169,7 +173,7 @@ module API if parent_group authorize! :create_subgroup, parent_group else - authorize! :create_group + authorize_group_creation! end group = create_group diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 0d9b76f3d34..5dc3f71329d 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -629,6 +629,9 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def usage_activity_by_stage_monitor(time_period) + # Calculate histogram only for overall as other time periods aren't available/useful here. + integrations_histogram = time_period.empty? ? histogram(::AlertManagement::HttpIntegration.active, :project_id, buckets: 1..100) : nil + { clusters: distinct_count(::Clusters::Cluster.where(time_period), :user_id), clusters_applications_prometheus: cluster_applications_user_distinct_count(::Clusters::Applications::Prometheus, time_period), @@ -638,8 +641,9 @@ module Gitlab projects_with_tracing_enabled: distinct_count(::Project.with_tracing_enabled.where(time_period), :creator_id), projects_with_error_tracking_enabled: distinct_count(::Project.with_enabled_error_tracking.where(time_period), :creator_id), projects_with_incidents: distinct_count(::Issue.incident.where(time_period), :project_id), - projects_with_alert_incidents: distinct_count(::Issue.incident.with_alert_management_alerts.where(time_period), :project_id) - } + projects_with_alert_incidents: distinct_count(::Issue.incident.with_alert_management_alerts.where(time_period), :project_id), + projects_with_enabled_alert_integrations_histogram: integrations_histogram + }.compact end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/utils/usage_data.rb b/lib/gitlab/utils/usage_data.rb index 005da0f9642..854fc5c917d 100644 --- a/lib/gitlab/utils/usage_data.rb +++ b/lib/gitlab/utils/usage_data.rb @@ -39,10 +39,12 @@ module Gitlab extend self FALLBACK = -1 + HISTOGRAM_FALLBACK = { '-1' => -1 }.freeze DISTRIBUTED_HLL_FALLBACK = -2 ALL_TIME_TIME_FRAME_NAME = "all" SEVEN_DAYS_TIME_FRAME_NAME = "7d" TWENTY_EIGHT_DAYS_TIME_FRAME_NAME = "28d" + MAX_BUCKET_SIZE = 100 def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil) if batch @@ -87,6 +89,73 @@ module Gitlab FALLBACK end + # We don't support batching with histograms. + # Please avoid using this method on large tables. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/323949. + # + # rubocop: disable CodeReuse/ActiveRecord + def histogram(relation, column, buckets:, bucket_size: buckets.size) + # Using lambda to avoid exposing histogram specific methods + parameters_valid = lambda do + error_message = + if buckets.first == buckets.last + 'Lower bucket bound cannot equal to upper bucket bound' + elsif bucket_size == 0 + 'Bucket size cannot be zero' + elsif bucket_size > MAX_BUCKET_SIZE + "Bucket size #{bucket_size} exceeds the limit of #{MAX_BUCKET_SIZE}" + end + + return true unless error_message + + exception = ArgumentError.new(error_message) + exception.set_backtrace(caller) + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception) + + false + end + + return HISTOGRAM_FALLBACK unless parameters_valid.call + + count_grouped = relation.group(column).select(Arel.star.count.as('count_grouped')) + cte = Gitlab::SQL::CTE.new(:count_cte, count_grouped) + + # For example, 9 segements gives 10 buckets + bucket_segments = bucket_size - 1 + + width_bucket = Arel::Nodes::NamedFunction + .new('WIDTH_BUCKET', [cte.table[:count_grouped], buckets.first, buckets.last, bucket_segments]) + .as('buckets') + + query = cte + .table + .project(width_bucket, cte.table[:count]) + .group('buckets') + .order('buckets') + .with(cte.to_arel) + + # Return the histogram as a Hash because buckets are unique. + relation + .connection + .exec_query(query.to_sql) + .rows + .to_h + # Keys are converted to strings in Usage Ping JSON + .stringify_keys + rescue ActiveRecord::StatementInvalid => e + Gitlab::AppJsonLogger.error( + event: 'histogram', + relation: relation.table_name, + operation: 'histogram', + operation_args: [column, buckets.first, buckets.last, bucket_segments], + query: query.to_sql, + message: e.message + ) + + HISTOGRAM_FALLBACK + end + # rubocop: enable CodeReuse/ActiveRecord + def add(*args) return -1 if args.any?(&:negative?) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 53ba4f7ba29..be8ecd13ecb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9637,6 +9637,15 @@ msgstr "" msgid "DastProfiles|The maximum number of seconds allowed for the site under test to respond to a request." msgstr "" +msgid "DastProfiles|This profile is currently being used in a policy." +msgstr "" + +msgid "DastProfiles|This scanner profile is currently being used by a policy. To make edits you must remove it from the active policy." +msgstr "" + +msgid "DastProfiles|This site profile is currently being used by a policy. To make edits you must remove it from the active policy." +msgstr "" + msgid "DastProfiles|Turn on AJAX spider" msgstr "" @@ -22501,6 +22510,9 @@ msgstr "" msgid "Pipeline|Failed" msgstr "" +msgid "Pipeline|In progress" +msgstr "" + msgid "Pipeline|Key" msgstr "" diff --git a/package.json b/package.json index 56b54b74047..27d7d711fb8 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "deckar01-task_list": "^2.3.1", "diff": "^3.4.0", "document-register-element": "1.14.3", - "dompurify": "^2.2.6", + "dompurify": "^2.2.7", "dropzone": "^4.2.0", "editorconfig": "^0.15.3", "emoji-regex": "^7.0.3", diff --git a/qa/qa/page/admin/settings/component/ip_limits.rb b/qa/qa/page/admin/settings/component/ip_limits.rb index 9db2ae8ba58..1f9bd113cab 100644 --- a/qa/qa/page/admin/settings/component/ip_limits.rb +++ b/qa/qa/page/admin/settings/component/ip_limits.rb @@ -14,9 +14,9 @@ module QA end def enable_throttles - check_element :throttle_unauthenticated_checkbox - check_element :throttle_authenticated_api_checkbox - check_element :throttle_authenticated_web_checkbox + check_element(:throttle_unauthenticated_checkbox) + check_element(:throttle_authenticated_api_checkbox) + check_element(:throttle_authenticated_web_checkbox) end def save_settings diff --git a/qa/qa/page/admin/settings/component/outbound_requests.rb b/qa/qa/page/admin/settings/component/outbound_requests.rb index 248ea5b6715..c812c05f9c1 100644 --- a/qa/qa/page/admin/settings/component/outbound_requests.rb +++ b/qa/qa/page/admin/settings/component/outbound_requests.rb @@ -19,7 +19,7 @@ module QA private def check_allow_requests_to_local_network_from_services_checkbox - check_element :allow_requests_from_services_checkbox + check_element(:allow_requests_from_services_checkbox) end def click_save_changes_button diff --git a/qa/qa/page/admin/settings/component/performance_bar.rb b/qa/qa/page/admin/settings/component/performance_bar.rb index bc29efb64c0..9e92fa362fb 100644 --- a/qa/qa/page/admin/settings/component/performance_bar.rb +++ b/qa/qa/page/admin/settings/component/performance_bar.rb @@ -12,7 +12,7 @@ module QA end def enable_performance_bar - click_element :enable_performance_bar_checkbox + check_element(:enable_performance_bar_checkbox) Capybara.current_session.driver.browser.manage.add_cookie(name: 'perf_bar_enabled', value: 'true') end diff --git a/qa/qa/page/admin/settings/component/sign_up_restrictions.rb b/qa/qa/page/admin/settings/component/sign_up_restrictions.rb index 9526faf4126..9909155641f 100644 --- a/qa/qa/page/admin/settings/component/sign_up_restrictions.rb +++ b/qa/qa/page/admin/settings/component/sign_up_restrictions.rb @@ -13,13 +13,13 @@ module QA end def require_admin_approval_after_user_signup - check_element :require_admin_approval_after_user_signup_checkbox - click_element :save_changes_button + check_element(:require_admin_approval_after_user_signup_checkbox) + click_element(:save_changes_button) end def disable_signups - uncheck_element :signup_enabled_checkbox - click_element :save_changes_button + uncheck_element(:signup_enabled_checkbox) + click_element(:save_changes_button) end end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 99f73bbba48..d1b556b58fb 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -133,9 +133,15 @@ module QA end def check_element(name) + if find_element(name, visible: false).checked? + QA::Runtime::Logger.debug("#{name} is already checked") + + return + end + retry_until(sleep_interval: 1) do - find_element(name).set(true) - checked = find_element(name).checked? + find_element(name, visible: false).click + checked = find_element(name, visible: false).checked? QA::Runtime::Logger.debug(checked ? "#{name} was checked" : "#{name} was not checked") @@ -144,10 +150,19 @@ module QA end def uncheck_element(name) - retry_until(sleep_interval: 1) do - find_element(name).set(false) + unless find_element(name, visible: false).checked? + QA::Runtime::Logger.debug("#{name} is already unchecked") - !find_element(name).checked? + return + end + + retry_until(sleep_interval: 1) do + find_element(name, visible: false).click + unchecked = !find_element(name, visible: false).checked? + + QA::Runtime::Logger.debug(unchecked ? "#{name} was unchecked" : "#{name} was not unchecked") + + unchecked end end diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb index 8f5267c3362..ced8bd5c812 100644 --- a/qa/qa/page/group/settings/general.rb +++ b/qa/qa/page/group/settings/general.rb @@ -54,57 +54,57 @@ module QA end def set_lfs_enabled - expand_content :permission_lfs_2fa_content - check_element :lfs_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + check_element(:lfs_checkbox) + click_element(:save_permissions_changes_button) end def set_lfs_disabled - expand_content :permission_lfs_2fa_content - uncheck_element :lfs_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + uncheck_element(:lfs_checkbox) + click_element(:save_permissions_changes_button) end def set_request_access_enabled - expand_content :permission_lfs_2fa_content - check_element :request_access_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + check_element(:request_access_checkbox) + click_element(:save_permissions_changes_button) end def set_request_access_disabled - expand_content :permission_lfs_2fa_content - uncheck_element :request_access_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + uncheck_element(:request_access_checkbox) + click_element(:save_permissions_changes_button) end def set_require_2fa_enabled - expand_content :permission_lfs_2fa_content - check_element :require_2fa_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + check_element(:require_2fa_checkbox) + click_element(:save_permissions_changes_button) end def set_require_2fa_disabled - expand_content :permission_lfs_2fa_content - uncheck_element :require_2fa_checkbox - click_element :save_permissions_changes_button + expand_content(:permission_lfs_2fa_content) + uncheck_element(:require_2fa_checkbox) + click_element(:save_permissions_changes_button) end def set_project_creation_level(value) - expand_content :permission_lfs_2fa_content + expand_content(:permission_lfs_2fa_content) select_element(:project_creation_level_dropdown, value) - click_element :save_permissions_changes_button + click_element(:save_permissions_changes_button) end def toggle_request_access - expand_content :permission_lfs_2fa_content + expand_content(:permission_lfs_2fa_content) if find_element(:request_access_checkbox).checked? - uncheck_element :request_access_checkbox + uncheck_element(:request_access_checkbox) else - check_element :request_access_checkbox + check_element(:request_access_checkbox) end - click_element :save_permissions_changes_button + click_element(:save_permissions_changes_button) end end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 056942d0eaf..89b6effcad7 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -228,6 +228,7 @@ module QA !find_element(:squash_checkbox).disabled? end + # TODO: Fix workaround for data-qa-selector failure click_element(:squash_checkbox) end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 7e296528795..d1033dbfca9 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -68,7 +68,7 @@ module QA end def enable_initialize_with_readme - check_element :initialize_with_readme_checkbox + check_element(:initialize_with_readme_checkbox) end end end diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index 1b9a451c47d..59f59ca9966 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -36,7 +36,7 @@ module QA end def uncheck_rbac! - uncheck_element :rbac_checkbox + uncheck_element(:rbac_checkbox) end end end diff --git a/qa/qa/page/project/settings/auto_devops.rb b/qa/qa/page/project/settings/auto_devops.rb index 827d5b072c3..9dffa010805 100644 --- a/qa/qa/page/project/settings/auto_devops.rb +++ b/qa/qa/page/project/settings/auto_devops.rb @@ -11,8 +11,8 @@ module QA end def enable_autodevops - check_element :enable_autodevops_checkbox - click_element :save_changes_button + check_element(:enable_autodevops_checkbox) + click_element(:save_changes_button) end end end diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index f2ced668a60..2b8fad64afb 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -10,7 +10,6 @@ module QA view 'app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue' do element :ci_variable_key_field element :ci_variable_value_field - element :ci_variable_masked_checkbox element :ci_variable_save_button element :ci_variable_delete_button end diff --git a/qa/qa/page/project/settings/incidents.rb b/qa/qa/page/project/settings/incidents.rb index 9b523e2aa9e..610129851d9 100644 --- a/qa/qa/page/project/settings/incidents.rb +++ b/qa/qa/page/project/settings/incidents.rb @@ -13,7 +13,7 @@ module QA end def enable_issues_for_incidents - check_element :create_issue_checkbox + check_element(:create_issue_checkbox) end def select_issue_template(template) diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index 34754124931..fe5d629effe 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -29,7 +29,7 @@ module QA end def enable_merge_if_all_disscussions_are_resolved - click_element :allow_merge_if_all_discussions_are_resolved_checkbox + check_element(:allow_merge_if_all_discussions_are_resolved_checkbox) click_save_changes end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 45c46004790..fd68ac0de16 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -44,10 +44,6 @@ module QA element :commit_button end - view 'app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue' do - element :start_new_mr_checkbox - end - view 'app/assets/javascripts/ide/components/repo_editor.vue' do element :editor_container end diff --git a/rubocop/rubocop-usage-data.yml b/rubocop/rubocop-usage-data.yml index d0dd6a2e4da..a03f21e491a 100644 --- a/rubocop/rubocop-usage-data.yml +++ b/rubocop/rubocop-usage-data.yml @@ -34,6 +34,7 @@ UsageData/LargeTable: CountMethods: - :count - :distinct_count + - :histogram AllowedMethods: - :arel_table - :minimum diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh index c94faa8f6f4..4d0c1ba99d2 100755 --- a/scripts/lint-doc.sh +++ b/scripts/lint-doc.sh @@ -30,13 +30,13 @@ then ((ERRORCODE++)) fi -# Test for non-standard spaces (NBSP, NNBSP) in documentation. +# Test for non-standard spaces (NBSP, NNBSP, ZWSP) in documentation. echo '=> Checking for non-standard spaces...' echo -grep --extended-regexp --binary-file=without-match --recursive '[  ]' doc/ >/dev/null 2>&1 +grep --extended-regexp --binary-file=without-match --recursive '[  ​]' doc/ >/dev/null 2>&1 if [ $? -eq 0 ] then - echo '✖ ERROR: Non-standard spaces (NBSP, NNBSP) should not be used in documentation. + echo '✖ ERROR: Non-standard spaces (NBSP, NNBSP, ZWSP) should not be used in documentation. https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#spaces-between-words Replace with standard spaces:' >&2 # Find the spaces, then add color codes with sed to highlight each NBSP or NNBSP in the output. diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js index fa7123c813d..f12b75d3b70 100644 --- a/spec/frontend/packages/details/store/getters_spec.js +++ b/spec/frontend/packages/details/store/getters_spec.js @@ -250,8 +250,8 @@ describe('Getters PackageDetails Store', () => { setupState(); expect(gradleGroovyAddSourceCommand(state)).toMatchInlineSnapshot(` - "gitlab { - url \\"foo/registry\\" + "maven { + url 'foo/registry' }" `); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index edeab883e91..4c72dad735e 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -116,7 +116,7 @@ describe('Linked Pipelines Column', () => { it('emits the error', async () => { await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]); + expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); }); it('does not show the pipeline', async () => { @@ -167,7 +167,7 @@ describe('Linked Pipelines Column', () => { it('emits the error', async () => { await clickExpandButton(); - expect(wrapper.emitted().error).toEqual([[LOAD_FAILURE]]); + expect(wrapper.emitted().error).toEqual([[{ type: LOAD_FAILURE, skipSentry: true }]]); }); it('does not show the pipeline', async () => { diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index 4919efbb4c6..93aeb049434 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -29,6 +29,7 @@ describe('Timeago component', () => { const duration = () => wrapper.find('.duration'); const finishedAt = () => wrapper.find('.finished-at'); + const findInProgress = () => wrapper.find('[data-testid="pipeline-in-progress"]'); describe('with duration', () => { beforeEach(() => { @@ -77,4 +78,21 @@ describe('Timeago component', () => { expect(finishedAt().exists()).toBe(false); }); }); + + describe('in progress', () => { + it.each` + durationTime | finishedAtTime | shouldShow + ${10} | ${'2017-04-26T12:40:23.277Z'} | ${false} + ${10} | ${''} | ${false} + ${0} | ${'2017-04-26T12:40:23.277Z'} | ${false} + ${0} | ${''} | ${true} + `( + 'progress state shown: $shouldShow when pipeline duration is $durationTime and finished_at is $finishedAtTime', + ({ durationTime, finishedAtTime, shouldShow }) => { + createComponent({ duration: durationTime, finished_at: finishedAtTime }); + + expect(findInProgress().exists()).toBe(shouldShow); + }, + ); + }); }); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js index a33401c5ba9..a879b06e858 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -1,85 +1,88 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue'; +import { shallowMount } from '@vue/test-utils'; +import RelatedLinks from '~/vue_merge_request_widget/components/mr_widget_related_links.vue'; describe('MRWidgetRelatedLinks', () => { - let vm; + let wrapper; - const createComponent = (data) => { - const Component = Vue.extend(relatedLinksComponent); - - return mountComponent(Component, data); + const createComponent = (propsData = {}) => { + wrapper = shallowMount(RelatedLinks, { propsData }); }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('computed', () => { describe('closesText', () => { it('returns Closes text for open merge request', () => { - vm = createComponent({ state: 'open', relatedLinks: {} }); + createComponent({ state: 'open', relatedLinks: {} }); - expect(vm.closesText).toEqual('Closes'); + expect(wrapper.vm.closesText).toBe('Closes'); }); it('returns correct text for closed merge request', () => { - vm = createComponent({ state: 'closed', relatedLinks: {} }); + createComponent({ state: 'closed', relatedLinks: {} }); - expect(vm.closesText).toEqual('Did not close'); + expect(wrapper.vm.closesText).toBe('Did not close'); }); it('returns correct tense for merged request', () => { - vm = createComponent({ state: 'merged', relatedLinks: {} }); + createComponent({ state: 'merged', relatedLinks: {} }); - expect(vm.closesText).toEqual('Closed'); + expect(wrapper.vm.closesText).toBe('Closed'); }); }); }); it('should have only have closing issues text', () => { - vm = createComponent({ + createComponent({ relatedLinks: { closing: '#23 and #42', }, }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); + const content = wrapper + .text() + .replace(/\n(\s)+/g, ' ') + .trim(); expect(content).toContain('Closes #23 and #42'); expect(content).not.toContain('Mentions'); }); it('should have only have mentioned issues text', () => { - vm = createComponent({ + createComponent({ relatedLinks: { mentioned: '#7', }, }); - expect(vm.$el.innerText).toContain('Mentions #7'); - expect(vm.$el.innerText).not.toContain('Closes'); + expect(wrapper.text().trim()).toContain('Mentions #7'); + expect(wrapper.text().trim()).not.toContain('Closes'); }); it('should have closing and mentioned issues at the same time', () => { - vm = createComponent({ + createComponent({ relatedLinks: { closing: '#7', mentioned: '#23 and #42', }, }); - const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); + const content = wrapper + .text() + .replace(/\n(\s)+/g, ' ') + .trim(); expect(content).toContain('Closes #7'); expect(content).toContain('Mentions #23 and #42'); }); it('should have assing issues link', () => { - vm = createComponent({ + createComponent({ relatedLinks: { assignToMe: 'Assign yourself to these issues', }, }); - expect(vm.$el.innerText).toContain('Assign yourself to these issues'); + expect(wrapper.text().trim()).toContain('Assign yourself to these issues'); }); }); diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb index 83aad206547..b00ee19cea2 100644 --- a/spec/helpers/boards_helper_spec.rb +++ b/spec/helpers/boards_helper_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe BoardsHelper do let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } let_it_be(:base_group) { create(:group, path: 'base') } + let_it_be(:project) { create(:project, group: base_group) } let_it_be(:project_board) { create(:board, project: project) } let_it_be(:group_board) { create(:board, group: base_group) } @@ -82,6 +82,10 @@ RSpec.describe BoardsHelper do expect(helper.board_data[:labels_fetch_path]).to eq("/#{project.full_path}/-/labels.json?include_ancestor_groups=true") expect(helper.board_data[:labels_manage_path]).to eq("/#{project.full_path}/-/labels") end + + it 'returns the group id of a project' do + expect(helper.board_data[:group_id]).to eq(project.group.id) + end end context 'group board' do @@ -102,6 +106,10 @@ RSpec.describe BoardsHelper do expect(helper.board_data[:labels_fetch_path]).to eq("/groups/#{base_group.full_path}/-/labels.json?include_ancestor_groups=true&only_group_labels=true") expect(helper.board_data[:labels_manage_path]).to eq("/groups/#{base_group.full_path}/-/labels") end + + it 'returns the group id' do + expect(helper.board_data[:group_id]).to eq(base_group.id) + end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 97000856626..b1581bf02a6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -382,14 +382,15 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do describe 'usage_activity_by_stage_monitor' do it 'includes accurate usage_activity_by_stage data' do for_defined_days_back do - user = create(:user, dashboard: 'operations') + user = create(:user, dashboard: 'operations') cluster = create(:cluster, user: user) - create(:project, creator: user) + project = create(:project, creator: user) create(:clusters_applications_prometheus, :installed, cluster: cluster) create(:project_tracing_setting) create(:project_error_tracking_setting) create(:incident) create(:incident, alert_management_alert: create(:alert_management_alert)) + create(:alert_management_http_integration, :active, project: project) end expect(described_class.usage_activity_by_stage_monitor({})).to include( @@ -399,10 +400,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do projects_with_tracing_enabled: 2, projects_with_error_tracking_enabled: 2, projects_with_incidents: 4, - projects_with_alert_incidents: 2 + projects_with_alert_incidents: 2, + projects_with_enabled_alert_integrations_histogram: { '1' => 2 } ) - expect(described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period)).to include( + data_28_days = described_class.usage_activity_by_stage_monitor(described_class.last_28_days_time_period) + expect(data_28_days).to include( clusters: 1, clusters_applications_prometheus: 1, operations_dashboard_default_dashboard: 1, @@ -411,6 +414,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do projects_with_incidents: 2, projects_with_alert_incidents: 1 ) + + expect(data_28_days).not_to include(:projects_with_enabled_alert_integrations_histogram) end end @@ -528,14 +533,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(subject.keys).to include(*UsageDataHelpers::USAGE_DATA_KEYS) end - it 'gathers usage counts' do + it 'gathers usage counts', :aggregate_failures do count_data = subject[:counts] expect(count_data[:boards]).to eq(1) expect(count_data[:projects]).to eq(4) - expect(count_data.values_at(*UsageDataHelpers::SMAU_KEYS)).to all(be_an(Integer)) expect(count_data.keys).to include(*UsageDataHelpers::COUNTS_KEYS) expect(UsageDataHelpers::COUNTS_KEYS - count_data.keys).to be_empty + expect(count_data.values).to all(be_a_kind_of(Integer)) end it 'gathers usage counts correctly' do diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 45e57428141..6e1904c43e1 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Utils::UsageData do + include Database::DatabaseHelpers + describe '#count' do let(:relation) { double(:relation) } @@ -183,6 +185,102 @@ RSpec.describe Gitlab::Utils::UsageData do end end + describe '#histogram' do + let_it_be(:projects) { create_list(:project, 3) } + let(:project1) { projects.first } + let(:project2) { projects.second } + let(:project3) { projects.third } + + let(:fallback) { described_class::HISTOGRAM_FALLBACK } + let(:relation) { AlertManagement::HttpIntegration.active } + let(:column) { :project_id } + + def expect_error(exception, message, &block) + expect(Gitlab::ErrorTracking) + .to receive(:track_and_raise_for_dev_exception) + .with(instance_of(exception)) + .and_call_original + + expect(&block).to raise_error( + an_instance_of(exception).and( + having_attributes(message: message, backtrace: be_kind_of(Array))) + ) + end + + it 'checks bucket bounds to be not equal' do + expect_error(ArgumentError, 'Lower bucket bound cannot equal to upper bucket bound') do + described_class.histogram(relation, column, buckets: 1..1) + end + end + + it 'checks bucket_size being non-zero' do + expect_error(ArgumentError, 'Bucket size cannot be zero') do + described_class.histogram(relation, column, buckets: 1..2, bucket_size: 0) + end + end + + it 'limits the amount of buckets without providing bucket_size argument' do + expect_error(ArgumentError, 'Bucket size 101 exceeds the limit of 100') do + described_class.histogram(relation, column, buckets: 1..101) + end + end + + it 'limits the amount of buckets when providing bucket_size argument' do + expect_error(ArgumentError, 'Bucket size 101 exceeds the limit of 100') do + described_class.histogram(relation, column, buckets: 1..2, bucket_size: 101) + end + end + + it 'without data' do + histogram = described_class.histogram(relation, column, buckets: 1..100) + + expect(histogram).to eq({}) + end + + it 'aggregates properly within bounds' do + create(:alert_management_http_integration, :active, project: project1) + create(:alert_management_http_integration, :inactive, project: project1) + + create(:alert_management_http_integration, :active, project: project2) + create(:alert_management_http_integration, :active, project: project2) + create(:alert_management_http_integration, :inactive, project: project2) + + create(:alert_management_http_integration, :active, project: project3) + create(:alert_management_http_integration, :inactive, project: project3) + + histogram = described_class.histogram(relation, column, buckets: 1..100) + + expect(histogram).to eq('1' => 2, '2' => 1) + end + + it 'aggregates properly out of bounds' do + create_list(:alert_management_http_integration, 3, :active, project: project1) + histogram = described_class.histogram(relation, column, buckets: 1..2) + + expect(histogram).to eq('2' => 1) + end + + it 'returns fallback and logs canceled queries' do + create(:alert_management_http_integration, :active, project: project1) + + expect(Gitlab::AppJsonLogger).to receive(:error).with( + event: 'histogram', + relation: relation.table_name, + operation: 'histogram', + operation_args: [column, 1, 100, 99], + query: kind_of(String), + message: /PG::QueryCanceled/ + ) + + with_statement_timeout(0.001) do + relation = AlertManagement::HttpIntegration.select('pg_sleep(0.002)') + histogram = described_class.histogram(relation, column, buckets: 1..100) + + expect(histogram).to eq(fallback) + end + end + end + describe '#add' do it 'adds given values' do expect(described_class.add(1, 3)).to eq(4) diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb index b8d7ea3662f..db093bcef85 100644 --- a/spec/support/helpers/database/database_helpers.rb +++ b/spec/support/helpers/database/database_helpers.rb @@ -5,11 +5,65 @@ module Database # In order to directly work with views using factories, # we can swapout the view for a table of identical structure. def swapout_view_for_table(view) - ActiveRecord::Base.connection.execute(<<~SQL) + ActiveRecord::Base.connection.execute(<<~SQL.squish) CREATE TABLE #{view}_copy (LIKE #{view}); DROP VIEW #{view}; ALTER TABLE #{view}_copy RENAME TO #{view}; SQL end + + # Set statement timeout temporarily. + # Useful when testing query timeouts. + # + # Note that this method cannot restore the timeout if a query + # was canceled due to e.g. a statement timeout. + # Refrain from using this transaction in these situations. + # + # @param timeout - Statement timeout in seconds + # + # Example: + # + # with_statement_timeout(0.1) do + # model.select('pg_sleep(0.11)') + # end + def with_statement_timeout(timeout) + # Force a positive value and a minimum of 1ms for very small values. + timeout = (timeout * 1000).abs.ceil + + raise ArgumentError, 'Using a timeout of `0` means to disable statement timeout.' if timeout == 0 + + previous_timeout = ActiveRecord::Base.connection + .exec_query('SHOW statement_timeout')[0].fetch('statement_timeout') + + set_statement_timeout("#{timeout}ms") + + yield + ensure + begin + set_statement_timeout(previous_timeout) + rescue ActiveRecord::StatementInvalid + # After a transaction was canceled/aborted due to e.g. a statement + # timeout commands are ignored and will raise in PG::InFailedSqlTransaction. + # We can safely ignore this error because the statement timeout was set + # for the currrent transaction which will be closed anyway. + end + end + + # Set statement timeout for the current transaction. + # + # Note, that it does not restore the previous statement timeout. + # Use `with_statement_timeout` instead. + # + # @param timeout - Statement timeout in seconds + # + # Example: + # + # set_statement_timeout(0.1) + # model.select('pg_sleep(0.11)') + def set_statement_timeout(timeout) + ActiveRecord::Base.connection.execute( + format(%(SET LOCAL statement_timeout = '%s'), timeout) + ) + end end end diff --git a/yarn.lock b/yarn.lock index 2d02295121b..dd63db5131d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4308,10 +4308,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -dompurify@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.6.tgz#54945dc5c0b45ce5ae228705777e8e59d7b2edc4" - integrity sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ== +dompurify@^2.2.6, dompurify@^2.2.7: + version "2.2.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.7.tgz#a5f055a2a471638680e779bd08fc334962d11fd8" + integrity sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg== domutils@^1.5.1: version "1.7.0"