From 1f3baf00bfdff196b43ade455d8268ce10ff13aa Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 24 Nov 2021 21:12:47 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../details_page/details_header.vue | 1 - ...et_container_repository_tags.query.graphql | 1 + .../explorer/pages/details.vue | 6 +- .../components/attention_requested_toggle.vue | 2 +- ...test_wal_locations_for_idempotent_jobs.yml | 2 +- config/initializers/action_cable.rb | 2 + doc/api/projects.md | 2 +- doc/api/users.md | 1 + doc/ci/large_repositories/index.md | 4 +- doc/ci/runners/index.md | 6 +- .../runner_cloud/linux_runner_cloud.md | 6 +- .../runners/runner_cloud/macos/environment.md | 4 +- .../runner_cloud/macos_runner_cloud.md | 17 +- .../runner_cloud/windows_runner_cloud.md | 4 +- doc/development/index.md | 5 +- doc/development/sidekiq_style_guide.md | 13 +- doc/install/requirements.md | 3 +- doc/topics/plan_and_track.md | 3 +- doc/user/gitlab_com/index.md | 6 +- doc/user/group/epics/index.md | 2 + .../epic-view-ancestors-in-sidebar_v14_6.png | Bin 0 -> 24780 bytes ...ssue-view-parent-epic-in-sidebar_v14_6.png | Bin 0 -> 25077 bytes doc/user/group/planning_hierarchy/index.md | 59 +++++++ lib/gitlab/database/migration_helpers.rb | 1 + .../background_migration_helpers.rb | 100 ----------- .../batched_background_migration_helpers.rb | 118 +++++++++++++ ...n_cable_subscription_adapter_identifier.rb | 15 ++ .../container_registry/explorer/mock_data.js | 3 +- .../explorer/pages/details_spec.js | 38 ++-- ....js => attention_requested_toggle_spec.js} | 4 +- ...le_subscription_adapter_identifier_spec.rb | 24 +++ .../background_migration_helpers_spec.rb | 155 ----------------- ...tched_background_migration_helpers_spec.rb | 164 ++++++++++++++++++ 33 files changed, 461 insertions(+), 310 deletions(-) create mode 100644 doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png create mode 100644 doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png create mode 100644 doc/user/group/planning_hierarchy/index.md create mode 100644 lib/gitlab/database/migrations/batched_background_migration_helpers.rb create mode 100644 lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb rename spec/frontend/sidebar/components/{attention_required_toggle_spec.js => attention_requested_toggle_spec.js} (96%) create mode 100644 spec/initializers/action_cable_subscription_adapter_identifier_spec.rb create mode 100644 spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue index e9e36151fe6..d988ad8d8ca 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue @@ -46,7 +46,6 @@ export default { data() { return { containerRepository: {}, - fetchTagsCount: false, }; }, apollo: { diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql index a703c2dd0ac..502382010f9 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql @@ -9,6 +9,7 @@ query getContainerRepositoryTags( ) { containerRepository(id: $id) { id + tagsCount tags(after: $after, before: $before, first: $first, last: $last) { nodes { digest diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue index feabc4f770b..bc6e3091f0e 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue @@ -25,9 +25,11 @@ import { UNFINISHED_STATUS, MISSING_OR_DELETED_IMAGE_BREADCRUMB, ROOT_IMAGE_TEXT, + GRAPHQL_PAGE_SIZE, } from '../constants/index'; import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql'; import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql'; +import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql'; export default { name: 'RegistryDetailsPage', @@ -133,8 +135,8 @@ export default { awaitRefetchQueries: true, refetchQueries: [ { - query: getContainerRepositoryDetailsQuery, - variables: this.queryVariables, + query: getContainerRepositoryTagsQuery, + variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE }, }, ], }); diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue index 38ba468d197..42e56906e2c 100644 --- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue +++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue @@ -64,7 +64,7 @@ export default { " \ diff --git a/doc/api/users.md b/doc/api/users.md index da33b71aa35..292dc411e5b 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -1623,6 +1623,7 @@ Returns: - `201 Created` on success. - `404 User Not Found` if user cannot be found. - `403 Forbidden` if the user cannot be approved because they are blocked by an administrator or by LDAP synchronization. +- `409 Conflict` if the user has been deactivated. Example Responses: diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md index 76e34df1f8c..fe4af9421db 100644 --- a/doc/ci/large_repositories/index.md +++ b/doc/ci/large_repositories/index.md @@ -259,5 +259,5 @@ For very active repositories with a large number of references and files, you ca must be configured per-repository. The pack-objects cache also automatically works for forks. On GitLab.com, where the pack-objects cache is enabled on all Gitaly servers, we found that we no longer need a pre-clone step for `gitlab-org/gitlab` development. - Optimize your CI/CD jobs by seeding repository data in a pre-clone step with the - [`pre_clone_script`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) of GitLab Runner. See the - [Runner Cloud for Linux](../runners/runner_cloud/linux_runner_cloud.md#pre-clone-script) for more details. + [`pre_clone_script`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) of GitLab Runner. See + [SaaS runners on Linux](../runners/runner_cloud/linux_runner_cloud.md#pre-clone-script) for details. diff --git a/doc/ci/runners/index.md b/doc/ci/runners/index.md index d408bc46609..b4e9fe818cf 100644 --- a/doc/ci/runners/index.md +++ b/doc/ci/runners/index.md @@ -5,12 +5,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: reference --- -# GitLab Runner Cloud **(FREE)** +# Runner SaaS **(FREE SAAS)** -If you are using self-managed GitLab or you want to use your own runners on GitLab.com, you can +If you are using self-managed GitLab or you use GitLab.com but want to use your own runners, you can [install and configure your own runners](https://docs.gitlab.com/runner/install/). -If you are using GitLab SaaS (GitLab.com), your CI jobs automatically run on runners in the GitLab Runner Cloud. +If you are using GitLab SaaS (GitLab.com), your CI jobs automatically run on runners provided by GitLab. No configuration is required. Your jobs can run on: - [Linux runners](build_cloud/linux_build_cloud.md). diff --git a/doc/ci/runners/runner_cloud/linux_runner_cloud.md b/doc/ci/runners/runner_cloud/linux_runner_cloud.md index d0fedfcabb2..bda495009e2 100644 --- a/doc/ci/runners/runner_cloud/linux_runner_cloud.md +++ b/doc/ci/runners/runner_cloud/linux_runner_cloud.md @@ -4,9 +4,9 @@ group: Runner info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Runner Cloud for Linux **(FREE)** +# SaaS runners on Linux **(FREE SAAS)** -Runner Cloud runners for Linux run in autoscale mode and are powered by Google Cloud Platform. +SaaS runners on Linux are autoscaled ephemeral Google Cloud Platform virtual machines. Autoscaling means reduced queue times to spin up CI/CD jobs, and isolated VMs for each job, thus maximizing security. These shared runners are available on GitLab.com. @@ -38,7 +38,7 @@ These runners share a [distributed cache](https://docs.gitlab.com/runner/configu ## Pre-clone script -Cloud runners for Linux provide a way to run commands in a CI +With SaaS runners on Linux, you can run commands in a CI job before the runner attempts to run `git init` and `git fetch` to download a GitLab repository. The [`pre_clone_script`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) diff --git a/doc/ci/runners/runner_cloud/macos/environment.md b/doc/ci/runners/runner_cloud/macos/environment.md index ddefad775c1..3332eab9b44 100644 --- a/doc/ci/runners/runner_cloud/macos/environment.md +++ b/doc/ci/runners/runner_cloud/macos/environment.md @@ -4,9 +4,9 @@ group: Runner info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# VM instances and images for Runner Cloud for macOS **(FREE)** +# VM instances and images for SaaS runners on macOS **(FREE SAAS)** -When you use the Runner Cloud for macOS: +When you use SaaS runners on macOS: - Each of your jobs runs in a newly provisioned VM, which is dedicated to the specific job. - The VM is active only for the duration of the job and immediately deleted. diff --git a/doc/ci/runners/runner_cloud/macos_runner_cloud.md b/doc/ci/runners/runner_cloud/macos_runner_cloud.md index 332284fa8c1..40c4deb51aa 100644 --- a/doc/ci/runners/runner_cloud/macos_runner_cloud.md +++ b/doc/ci/runners/runner_cloud/macos_runner_cloud.md @@ -4,19 +4,20 @@ group: Runner info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Runner Cloud for macOS (Beta) **(FREE SAAS)** +# SaaS runners on macOS (Beta) **(FREE SAAS)** -The Runner Cloud for macOS Beta provides on-demand runners integrated with GitLab SaaS [CI/CD](../../../ci/index.md). +SaaS runners on macOS provide an on-demand macOS build environment integrated with +GitLab SaaS [CI/CD](../../../ci/index.md). Use these runners to build, test, and deploy apps for the Apple ecosystem (macOS, iOS, tvOS). You can take advantage of all the capabilities of the GitLab single DevOps platform and not have to manage or operate a build environment. -Cloud runners for macOS are in [Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) +SaaS runners on macOS are in [Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) and shouldn't be relied upon for mission-critical production jobs. ## Quickstart -To start using Runner Cloud for macOS Beta, you must submit an access request [issue](https://gitlab.com/gitlab-com/macos-buildcloud-runners-beta/-/issues/new?issuable_template=beta_access_request). After your +To start using SaaS runners on macOS, you must submit an access request [issue](https://gitlab.com/gitlab-com/macos-buildcloud-runners-beta/-/issues/new?issuable_template=beta_access_request). After your access has been granted and your build environment configured, you must configure your `.gitlab-ci.yml` pipeline file: @@ -28,10 +29,10 @@ The runners automatically run your build. ## Example `.gitlab-ci.yml` file -The following sample `.gitlab-ci.yml` file shows how to start using the runners for macOS: +The following sample `.gitlab-ci.yml` file shows how to start using the SaaS runners on macOS: ```yaml -.macos_buildcloud_runners: +.macos_saas_runners: tags: - shared-macos-amd64 image: macos-11-xcode-12 @@ -45,14 +46,14 @@ before_script: build: extends: - - .macos_buildcloud_runners + - .macos_saas_runners stage: build script: - echo "running scripts in the build job" test: extends: - - .macos_buildcloud_runners + - .macos_saas_runners stage: test script: - echo "running scripts in the test job" diff --git a/doc/ci/runners/runner_cloud/windows_runner_cloud.md b/doc/ci/runners/runner_cloud/windows_runner_cloud.md index ef4d4076c91..87ee542fb14 100644 --- a/doc/ci/runners/runner_cloud/windows_runner_cloud.md +++ b/doc/ci/runners/runner_cloud/windows_runner_cloud.md @@ -4,9 +4,9 @@ group: Runner info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Runner Cloud for Windows (beta) **(FREE)** +# SaaS runners on Windows (beta) **(FREE SAAS)** -Runner Cloud runners for Windows are in [beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) +SaaS runners on Windows are in [beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta) and shouldn't be used for production workloads. During this beta period, the [shared runner pipeline quota](../../../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota) diff --git a/doc/development/index.md b/doc/development/index.md index fa49d43d46c..1398104abda 100644 --- a/doc/development/index.md +++ b/doc/development/index.md @@ -139,8 +139,9 @@ In these cases, use the following workflow: and approval from the VP of Development, the DRI for Development Guidelines, @clefelhocz1. -1. After all approvals are complete, assign the merge request to the - Technical Writer for [Development Guidelines](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines) +1. After all approvals are complete, review the page's metadata to + [find a Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments) + who can help you merge the changes. for final content review and merge. The Technical Writer may ask for additional approvals as previously suggested before merging the MR. diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index e6d865d56e9..f4fe80ad15e 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -342,6 +342,7 @@ end > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69372) in GitLab 14.3. > - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/338350) in GitLab 14.4. +> - [Enabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338350) in GitLab 14.6. The deduplication always take into account the latest binary replication pointer, not the first one. This happens because we drop the same job scheduled for the second time and the Write-Ahead Log (WAL) is lost. @@ -353,15 +354,11 @@ This way we are always comparing the latest binary replication pointer, making sure that we read from the replica that is fully caught up. FLAG: -On self-managed GitLab, by default this feature is not available. -To make it available, -ask an administrator to [enable the preserve_latest_wal_locations_for_idempotent_jobs flag](../administration/feature_flags.md). -FLAG: -On self-managed GitLab, by default this feature is not available. -To make it available, -ask an administrator to [enable the `preserve_latest_wal_locations_for_idempotent_jobs` flag](../administration/feature_flags.md). +On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to +[disable the feature flag](../administration/feature_flags.md) named preserve_latest_wal_locations_for_idempotent_jobs flag. + This feature flag is related to GitLab development and is not intended to be used by GitLab administrators, though. -On GitLab.com, this feature is available but can be configured by GitLab.com administrators only. +On GitLab.com, this feature is available. ## Limited capacity worker diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 9f435f04429..8aa7ca511c4 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -302,7 +302,8 @@ The GitLab Runner server requirements depend on: Since the nature of the jobs varies for each use case, you need to experiment by adjusting the job concurrency to get the optimum setting. -For reference, the GitLab.com Runner Cloud [auto-scaling runner for Linux](../ci/runners/build_cloud/linux_build_cloud.md) is configured so that a **single job** runs in a **single instance** with: +For reference, the [SaaS runners on Linux](../ci/runners/build_cloud/linux_build_cloud.md) +are configured so that a **single job** runs in a **single instance** with: - 1 vCPU. - 3.75 GB of RAM. diff --git a/doc/topics/plan_and_track.md b/doc/topics/plan_and_track.md index d4d69959b6a..c70c896e411 100644 --- a/doc/topics/plan_and_track.md +++ b/doc/topics/plan_and_track.md @@ -42,5 +42,6 @@ Align your work across teams. - [Epics](../user/group/epics/index.md) - [Multi-level epics](../user/group/epics/manage_epics.md#multi-level-child-epics) - [Epic boards](../user/group/epics/epic_boards.md) - - [View heath status](../user/project/issues/managing_issues.md#health-status) + - [View health status](../user/project/issues/managing_issues.md#health-status) - [Roadmaps](../user/group/roadmap/index.md) +- [Planning hierarchies](../user/group/planning_hierarchy/index.md) diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index bf0f03128d9..900490053d6 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -200,11 +200,11 @@ The following limits apply for [Webhooks](../project/integrations/webhooks.md): | [Number of webhooks](../../administration/instance_limits.md#number-of-webhooks) | `100` per project, `50` per group | `100` per project, `50` per group | | Maximum payload size | 25 MB | 25 MB | -## Shared Runner Cloud runners +## Runner SaaS -GitLab has shared runners on GitLab.com that you can use to run your CI jobs. +Runner SaaS is the hosted, secure, and managed build environment you can use to run CI/CD jobs for your GitLab.com hosted project. -For more information, see [GitLab Runner Cloud runners](../../ci/runners/index.md). +For more information, see [Runner SaaS](../../ci/runners/index.md). ## Sidekiq diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md index 9c3cc3508da..998b8bf18ab 100644 --- a/doc/user/group/epics/index.md +++ b/doc/user/group/epics/index.md @@ -41,6 +41,8 @@ graph TD Child_epic --> Issue2 ``` +Also, read more about possible [planning hierarchies](../planning_hierarchy/index.md). + ## Roadmap in epics **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7327) in GitLab 11.10. diff --git a/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png b/doc/user/group/planning_hierarchy/img/epic-view-ancestors-in-sidebar_v14_6.png new file mode 100644 index 0000000000000000000000000000000000000000..373b861239bca5d4d986755a378a75f8fd027ec8 GIT binary patch literal 24780 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 T3heMcpwxED9Tr9A%$R=xYA#C3 literal 0 HcmV?d00001 diff --git a/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png b/doc/user/group/planning_hierarchy/img/issue-view-parent-epic-in-sidebar_v14_6.png new file mode 100644 index 0000000000000000000000000000000000000000..95a5777674a4c931dc7564889003a881063ba84b GIT binary patch literal 25077 zcmeFZbyQW|+CIDym6Q}zIz$1byQQU55D>{tcQ;B1D1t~R-6f46pdc;XDH2MD($ewW z+tcrPf8Y3>F~0A=qvK(0*4k^%x$b$#bzS%L2~$;;y@E}KjX)r-$laGxM&}f~CL!|Jsd|^VGgI=YoaY{Gt;IPqCSIVrSW} zB)0^%Dizv!Qk|%;&G;Pngr3>^d1ZcIk7cLvs1L-6+YlWNL5iP0T0R+a@8jvF4?2$E zscq@sy&B$LUHwQ_bZLt;-A?Ri?(_kzpYn!oMWC-D`^mv~!O_p2n`>g!xpvb_5f9A2 zrBdyl9WlL`_CI>1FqTo~AFW`|7nCY_T47MKv5^=_UA?>VY*X~}xrJI+U+PKZ{zhQ0 z+gxsk|NW=VS|aYf-5G6)k9M5Z?KNB?i3DDRv^dyz`H-A3__6L6Y(3aIx=DLYjlxH1 zEa~=an{?G9_cq%qTrox2k4MW@Y_(oua@Xp#vE+8R81*tfPi`?(lu9tXxFta9F(#(S zp%!7YZ#_N0OwZPqJ-x-A+j89d=5RgOz&AM)+fDg1_nex^_(H7D+w}Gop*Mtt?kUPj zV~zF(sptF;{Aq$#KGtwH+O^FQ($`Sekl3Z6k1;%^zW=@RV_5n<>E>}P6ULTh8#mgT zpEg^c`9tXMpsl`Lm)s4+td2Qpx=$_H-jd^WrH7@p+Pg^QY)e#C;ierSVIrwu<8zKE zF$OKCyxdpD@-f0 zRZQrvC36U=tWQoBtkiOyVA4l$Z!=~_@E+atHA}Kw)GNi_; z%eG|RYIC1vB389}o$u4+u4RTW znn`ub%C_LpI^3+#?zVB|@RC;M$dpwh=`b->w~5ne2{oMTJ#e9L7d76ecRHuwi{7!H zTRdlQl1V?NrrW3Dd*8)nXCP(!`Xryflce)wF_3gRg z5AG`Sdrgb;@|Yu$vp>6gDk~Bwo2Q=1>+i4LI;G1pu2G?C)Z?%r&ip2P6=Q+eG=&9O zB3-j&GL}-Xt}YZ8o;~VG^ue~?tby{n@_7o=-gJ0yh5_{hG0Xe`qvX7_WEC$XvLb7!RzM_yQd0QGK#ahvdUr&+w~ z)y;}byI~zt1nk@wJt-ywWlxoF|I};<;mMolsSYfSA={zAQhjIMHqlIm-Ml5W+pRC$ zSNN<2fx-U6)K021T6rZub4 %7<0>EMKpb2wdV`PeUI==zby)9$FKn0=6-5Y`eU983;D(Tr0q$WJp%U- zYG@>s;tTe=YV_heUlk~uD7`2nF`Q&tGVaF7KSSW(l+L;o^E528;+Gl4ypz?b4JU79 z-Io;lZuvDA7X{`wDbv`8wid!YBORXZM$2ynKDjte(o zen8%qPSAn--4Sskr8dZJRC%~FVvN?2W*} z+rUHYr89c*&s*FH7K!Y?M!qQ8ogL84^Ca&^b$b>$!{l|w#|}7^QIDlYwF;j*1_Tf5 z-dl^Aa$aDT*=fQ1$evcf_);$F3EB%~@lD=We)Bd77X(D*_iz#d%0#cP_CDQdL7EKE zjs(omczwaLL`UbQdez{{o-6(!fH>q*Xw#OrfX_?O_!n_koNrmLmKNDNkt$M@TCrj# z-d4RkOW#1j8%3km7_!&BEdN7&^O?_YM>5QZVfp%0vcvnAHQT>q)6+fV2~I4$YI9j# z@4^$!nv|;v_cm6qbYhxfG#9Sisu&n=vr%ojjM43KWjy&rQ6oTiBE9(G%{DFvl7>6) ziip!I)HCO#CJ1$w+S6Be-_1sC;OI|b56X*$v2t`cZB`Mz8Z@?y)MWIy$J@-?BRb!d zUe$EQj&p=F)hzaYLzI|7=o6VKOT-;ZO)45GzQJedL07s7i(QsEo=3Lq4YNq!i+mr= z+K(XbCs;DcX<;-?*ef$fZ^?gt|6J7B%E*vYcg6cHF8U=JOuk2#uD&P47DQ(h7T%Pk zZ(z$5Y*fCv=G&Yde^lLQ%Yey{zPcES3}St%%a;5!I3k9rQLB{ORzlw)=imwPRKuGO z{O`IhV=6Y?;4$O9H!f84rf)>3d^COB?s|{!NK2VHQs7bDC{`@8275MV#DQ&AH^T zned+)yH(`LU15_!mSbj8PRbk05gC=T-O}{h+-1(C!d+=t?XnV|VQ8UFJIHn{75w-_ z`Z;%QSF1A{njOxG6PA^fdJ~nj%Jx&5S3aK-Ndxt%S_)NVamh`H*$^S~1$S`FE9Ipr zUq5-3TcjnYBzB{d>Ia_v70u+!!&Jk;ntcYHH_Cpw6rsPq94957R;RtZWu2H`o^H3Y z*HWw@WVG)mME-E|I$v`m)tr~iu6-a8{coBy^o}P%4FS5MBGewDvu_EAaEphDvQ##t zq8_jl?nw_l)$J1+-!;RP-nBd&vcqWV6WaIG4Zz96Jv4ZE?*K`g+>~xVZM~ZO-gDx# zaaD!)+?3PyIItt(EOB75XLT*Qg?6H3IE7J32fh-rh8W}cOA89dj4R1rA zgm(JqFGW)!w`(&jCuv;FQ`>1<%L48ROt^N|)^7k+AJ2PE@#ELPJxIMrj&SDUsg?_QuGo zCuNIsysuj+x0-+ZY~)RhYVL)7Pb-V5e;CK}{f(hvz+vUIz`#Qd&E z38?}BI{&Gbpv%Nl)wbBpZKAX*8??eAm}5frFs z4^=cxzcA-OH@s%|8R~^VCvGFb>3QeBaSgJd8p7j^o=t(6$-|)nzPBCRu{gkWEORHa5 z%~?L0fp(?1u)V_U#V}#OjSKis$%ope5Fe9oSv{SfdZk5|*MrX17W{hAjO`iFnbW%%iDt}$ zcD+wD8l=stZmndy%5#oMJ6{jd<4V%^{zg2#QOS_U5M?Y9z+}av7{G1m+WjDif_s`Y z0TGPNhHk0Bbt&|okDBlOOE-mGNYgDYy@|Si1?$2uv3nv>L0NRfW4EL;tzHGblpnYt z&!OvgsSM+1-9y}f0dy|B*hR@FZPAGqBu1AG`nXNS8!pHbx@zKQ#1VuTH82v?sF8Zo zx#BpyoRwOkSG)bQQiIK6e?jfK1?K(p_ENJVb1TYnOG#owf9- zM^j(MSFohEE>TVMsF2Dh@ecc%m$tYi?pE6HP&a!#^ zop&YSm5Q`i3pMJrSAE_Lj+NhL;;KB0j}rQjZFb17EUMACd6M3mwUj9tNL|wo07G4JP5tOmv++uQ|bRy7URv_`Y{Hot&aPkGD2n-N_+!U4{yyLcBf zOQtBq?qyj|&1qwpTGHcLz-Zm+TguMgs$ges!OBw_7@rt2J7LCY>S|8mOkc2oe4|$0WqokKkC&bc7&7Wtpas}7EyWVl%{ zqkba(0p?F%Q8lq|1H6G3PgmC)YkNaQVzLkI=BA~pX;9VF>di#Nf`Y>z$ua}qeTjf(bN7dd5W`5nxAsM;! z0d8=GIw61TPuD$3L;%CuP3iOv7U&S*u8hridwMxp=5Agak)ifGO0m8Z=|w4 zy)RLALiB+0gl?X_JKy6^5uFW<36#svAFVY63w=r=N_k>Paz0K+tP*wc-U?5$EW5+2 z^g#Mj4-aIwyApo>bN+4x@9&dI8v!j6LxiQ=l|tm-4OH7K$CRtH`o~ezJN8c7S7r;ZCWeB8d**Ik3_DLL+Kc} z6ZpDCCcT(LbyAg!q&?2B=rN*N-rqXrVeoiFb61z&!el;(h*tE+Ra^1qe!BUimgLGi zD@Lta=DBXEp&0Xwybm&o%^3^$D-Xit9bHFTXy_*-dfVO&<(9f>TR&&Cr5p%R=W@I# zFnP{;Gx&1&?uyI1ro9{4Zw3sOKc%66o46udAxYSrCx{VRjz(Zg^P<_yN{QiZW8$QE2InQ2M3o?vfV|eZ*{AdF!n8*5sxlkmt9(vg59TLZi zq?PDBUEM+2jPy*RbhXBq-TtS>D=~_-m1`+h{a@!w-}EMh_xP_(;W8d#-DC0B^*I-z zi&wdA6@0&)Zfr|$WL9FLuC0(>VB#^+Ys_PrjT_GSZ1WX(wTQ~d?4E$d9?!{WR)z>t zPA?}BI->PPLP|Zoi!WYJ>z?Wc{~+76s=Cbcx`FD_4c9najEpMA_m)g8_ubzLmJ<@T z@&x6fOW|DXx8fX))Y2Xx+6wokqc#26;}F!$mEN{7)c@(YER zu-;i}&}fa1D>@ZayGjV#rY**MEnD6)eTe=JU-hZ(G_hLPd|oT7F4+ZNji1Q*!u5xQ z4~V;SmL~=WrL$t6tT*n3*E@CcnwI8HP0WqPv=`@RlC0g7NTU>cXJGi#WgMrGs&>?* z|1dB&=Hu^A{OTcn$pefdv4r|qudHQ5SO;8mPtyE3NII!lByihxyx&~?B9oHQ?vO`{ z>5Q&Rp5bZ9@#$^@+LZKzi`ydfYk5KFllI}<%N$qz7SN<$U>Y7`^Ha9I9Z~xIjq#4G z0rA`9_GjMyi309Xmu7_sg2LRg(0?Tq=i02ir7@z@_&E^$+$UqmmZ>t6OA%d`C4J0k z_qphpgOzXMv_%fKZ*jbaw1Pe5U4!uSMHN|!c~0W=hIpdQB)J61dPUu^zKP@4f!_)7 z>SV~}HEn2U1Ovm2>$rlh_s^5j{)(}eH#HIw+>9L>E$dF(=?D^_-*vu{qo*?0#Z8n< zpj8mg@j-a*#;%4_oL@nR>iWc}YH{&)t5eY7s8{O--lz}%1>;27TphTWg zc8w)Ei01k7**g}zD!zs|R~pW$U-%8uoJ;qyFMG`|>h-T@`(Y{GA-|DBKyz6)+ySBD z(s*~qcSkn-o)Cd%ZSU%!M;f7^$2=)keZ_g9@sl2s+?NzjTt23Y4Z zpZyTaAd@WLA`~x`pT-hBGve}cc`&mg#l-v@Jl2F1X9v;mAC9wrShP3}Qda0$RDPDj zm8WL-N_M}b=+rHjKJn_OJl&$H%2Gwk{c7hCWBTuxJY81q)jYc<jSgz%`x5GCRZ3e~h31_-zU+G=hRHB_&nmBqjgW_knIt zl5dpA{pNcVtp*y|86+4lLitVe@QlSZTht0vgRs-JEJhmV2l3P8Bwx?YGFFpPoIETP ztma=^{mzUTKuu$3r~I5%efIpd#Ea|r5yxBW({t;TG{f?W?l&X3(=~&Im?-(zW z=r2oNrwwoK<}_nq!kXD~jYJr17z=luPo`$-_2}SYhPH5Zv)j(P&gLt9`(CR;VI-}? zAbOjMXC9mSSSjUDYgoN@UYwF(Oy?D&w>|(!b%i!n}HWC7MixXH2@d z+v#`z%};uF#5mR&g6HlXznJsEMN8LDd+eIDVbYeLifAhQ{8LF3Pm?j#o-wV*|K@eW*OdGVMDr)K%SEdN-zLLY<3$FX5y7Gy)@}!QpB7 zn9uaoz{IzDbF$M@gm#Wo61`qnE&_pf*%BIY50w-JP3&!1jZE#0k*prJ4$!DWAcVy{ z9E?n?kuEgGNOMa&5xR{}^>j3rrXqCOJWA|J4w6U<%llqVNDVJ#O%pF`69H2?F;Q$` z4?!5f7U^O{<6&!K=Pc+ULicA}LHLZi%tlA^=PNGOB6JUxRB0sboscx#tlX^ZEYcp9 zZk%+Y*fhdUre=cbQZj!X0)7*rvv6^75M*O>cXwxX=VG;YGH2ru5D;Kv=VasLWPxw6 zID6W;7MZ|k#lP*VE)@OI9B zwF20K&BMrnjf0h)&DNIfpPz7ck#>VY{<@(5^$BN9=x4I2Bc1JColKC@Zb&;9`hOn6 z)a38aJGeU8{COQy6E>s`(iXnz46}0l%ao|j=-;3Cml;ua{w(sJhnJO8QvLhaQ5G|| zv~~D13GB_k4D4cQ_J0@{b?4s(hR6Rt?!PSKZ>IXQ3iy_ilAx5mi7V=DaUyyDX(%~6XBQ(o z6C`RV7@XA-#zAuPm~n6jn6Ypnk(?~t#+cNV7jfnVJa7*gM%8 z!R}ky8kr;69PG^h+(7MupsJ+@(&nL*CA_pVIGhL_2Olrj|G2I5Pq%sbIRD&5?Utap zs+&N_p_;U#>wa zh0M(M_tgBCF;Tnt|L32-`ojO8Q_#@-cas0ezW+7Xf6etDS>QkV@_)VSzvlXnEbt$F z`M=)v|Czb4|3U7Mc7Vd%fx0bewu%DnyKJl=D}^{m{Y|UOjfN{&4)=AO5eOV&)PHD* zgrw_m5yM4JNg87ThZGl^gLxS98eAfBk$&j%SLB2E2aq`?EP%71Gzd8qOxl}L!2}W# zn*a16C@7CW;(jPL?Z1ARUTdPP7Y@sAI*w9G*SL>Vy3zkHC8*nQC1(IK9#9`)vp z_PX!(_I7(mho|M(tE8mviArlZC8bzCDNL+^`UsLcTAS;iHaK0DRj0i-MD{jj42n<9 zdJ?3^iuFBrmj@JTBF4ubUbt`}^u>#4K55i5&%S>*94*vo9?sYJT4hh&{On5e*w`DK zYpgLJ4P<1V5q${3qx51L8?3a}-dmqiXp5j+cuDB5NA~jBdz)h8S#UkIqEl&kEf9Gso$Z{7^*?^jJqO2Wj(rek4wNhRXN?zwI1;_8Z4?eOEm z`SHv-zw;vQuZc>N_E)qntHW|Nqo>FF@4ff!X6NQsCTqAdWnN~$Vi8%nxjR$tGma+* zj=R5quZ$Lv6W>xyb6V)!ZX{&u?&>NSAB~HQ#1FiDg&Eohp_s)6^~(F}Q#CFtL}+Me z6_!KKA}EE3u3eM$@eyWaWmP9WeZR1To~C6$78DWoCw2MMY(?H(9~SiPOOE5E;K#US57*QBm_q$Gj(@_3ZTchNusJ zrfj6y>CtZrF~3JETuKTGbGs`;K|w+6R>O=*$;n?o`wFS4sUdFOy2T0B15b_@^%eZ` z<%{B5cJZ1~czAYgP2t-I+pWF5TcV;=_V)I}Ry+uWxVyB>%+GIdnQgA*rk4Ak`;8Xs z-=U`13i-V{^1^j()bf;HKp@uIz}wrqi1b^&M()GUJ~hKq1|eBlHxX8&h541WT1C3q z&z9lk1l-pt=Gr22!KM{fBhLdd@V8;m3l6_mA9QzjJGi<^tEk{BC@9#?d{*-F^UGgL ziA_pU^zh(+Z{DYM7F=0g-ZV8;*Z%wN!_p^J(_T2&u3b}35$@;{I}d;P@(L|2ZSCpa zH=mszzU8&Gp-fqP*hT?|?+Dl|LoiHrO%2o7RE@LwqeqVpevcWHRaGs`w!Uy#?tfaW z|LH=1juLtK`}Yfh^k=0ILME1$I1(QVqhkdrQT#y zQ`7H@i_hM?xdwjohT!!5RbB-yF0RTrb2BsbZ24HC{X0K#c(}ReM~n2hEeC_Hkn^&; z{d@=>2y@?NWW)sh1hxi#Y3uC7rld?(9}`(S`n`szu%CIXV`gL|UZh)D@qvqj<3~}| zw7Or_hYySEQ=jCE_&7MCq+gIStdbPp5#5e{5{@ zy(H&b27gsnR_3-DR~6--jm5^oGTNGL#l6891j_=;QuU^Y6u3wtkx0j-UP^AVE8u?f zKbk`*gxqlL?KkL((ual8EouBy@E!3axkt-!|Gr6UnocL%JKf+2I!N+zu z+O_=o^Ji&UnbF#45rweFD~JSMoAD>_-*fFO_S|uudXIbM3R+B#>yMhDp`jO3$}6Ap zH_z+%f{#b@)FgElXJ;kAuCe}Kd%C-y7urwP)(pCjz^33)3brivBp&_UMpw`6>a?^Yx?ao#NtR#S~#3%g^}N+0(}7Dl7&9zPuzSCLxiA zXv`Kk7eskoa|r$&%8B^+cqu8V{#=z*)BQHlD5rl%W)t))CTU|WT~IQ;p(4}pn;GlV@-Ub{c-{ph1h z*g&T2ovy*bK}4QrJ`wDB%h#_u*1k?oc^z7N|xVUGjsnn2}=vY}J=I2e< zCu;)J(`j$rx|Ln*tpz^%R?w9>k=M4VtLug1+?O(ofegBDv0nxTqL!B}PEJp2T-Qi% ztE6Pv&wRF-tR{RiUFZGY>$epi1%E@bko&<_8?~dOBV-8dmokB$J_#B4Y+ZtNP1gIT z9`A1`I6tnvy12N=&(Cjhdi}fX4U_Q_`bB%R;cqHY6yD6moGAw##md-*RNkU zgoc2kBCd$nE&}_rG%+QmQnH|{{Nx7mY{AgcWjob>_xM_tdVXnXsRy!c%z}IJlRG{a z*L&mU6=_o>GAcS+!ZZ9jM*!3+bj77rvVP_pY1AHSj&P$B!fBrlfJZsN(`*z6BpElv) z;hmMfZf?9@US6#qa&AhUHv$^+IGtaq{eOW`zAw4Sl){FHEV#+eF3;c{PeI88#DTxH zwRL{b7ouCV*zgp+vw=mG9q*Tl*s11x*DSy)>Xo&Udb5cNOV~0?mU{1f)>PlU8-OT| z+G6FryabJmjFN;sx2ew$?<*-GXFmJhynP$}RbpaSl7KVjl`FoM#8;p5rJj5>zXADU zyxNhTUN$18Deb$8lzCr@(#X39+3YFK`|J8nPEKz*k=R$rxL;j}A|O#^fBXaXM8N+{ z81Tp)$4~Is=1!m3&gKuv?w%e;SJ&(Z1zL#0$8}`%^z`7a92^|rBLfB6Y*0XAPvK%% zc=$~Lfq0phWIxu|Bl7cCsO{`7U%KRYxQzsOm}gKgO6`AA#)&EUBg>nKiAl3S>k5<~ zK$!F%@l~&wj zAM9t8U0k?pYHA+p>TUxxym|wUgbZZJ}1F)Q5TH0yG7ju9jmuAR52S6k`K0Yilk(|eB*bu5_=P*6&5fa{; z+iDn-kB^T^*rNdeg6Glh3iZc52w3MIO_v_&>%(p^KmmpgLAyl1i*fn#V6CUKOaw(( zem>J3?c$p(EJeev!9C+m*7#ShFhFpSvgopVY#KtjkTo^E31+#BeP(vJGL#F21zlEF zmV%NJLO{7pLvC$t&BV&86BZtW1l&s}Dw+bNFFH21)qTdd4VJJn^@&galiLDdJ4jKN z8LywN!Z^C8<5rakkf$y5yI_{G%F1jMF8(~Kl*sEi+k*Acbxo!(MFfSv3badDoqoYc z+5s~oBM)x8Q@XFDsmeYgYh!a4(7MrJmYnox$g^jIs1x2 z7Xw9LcEEP@KXk1YTz=ydk=ALvAQXDb^LZ4d7YiqrN7x2hq2bDyW2Y9zJAjb{WZ2 zL-+Od1!^Jn_%RiP#Z3kVrpguxEG#T(3C$v{9l)8^EY>hanWWE1i9sshEh17<8OS8p z*dA++^4LyXh38S&9zc`jSTW`3=%{Al!?I!5zRu20pe;()W?vwB2CE%SpjVzr zw0~}bL`nx^lY~Bho}pJ|ce35Z_E1kRv!Fl$7zQj~{>cT1m2%%>HzFdU&Qm{8AtN2j zlHUMp5rFE#0qdYMR!#&3UBr^7?Kzyuv>eKDSsQ)%>sytz(qnCYWaExx|AWGZYS!34 zmaH9vC7nnjKI+#vAyL)ngd3pQcnI;)(Gehj%Tu5?Ipfx6z%dVI&d=Ha)aPjxC5%?5 zLx{JHjR8i#_WJc}R4O#^{Y_$NX$ipNOgC*gJg?PNQdsD_vt@G>%oW9JJF(ftRu8G? z3&8D|f!+7--@o{&=P_yC^|7!dMZ`ON@UiueD$Z%YW7`E^R7`^tzrnDUu*zUFZg_$w zB`bT!*CCj%4qRib#GtLWSGgw4ezIDv_{SkGE-pJSZw>^GhldAamxj7JKr{6++SS!n z-9<7CL%*M+b*{;DjwLQb78OALyOb0S4aO-(PA8Bi1Z_q!X`SDIR!PN=AlR$@fVj zpFK0rDU?RW)4Jfwo+cy%QxOF=HLe?QPtX3CH*a#och65Q2EZ$26#6^v?jFoXt31EE z0aO%#>Mg)Nuw^K`tFHcfa6>B&z@FpJaSl}aw68yvv9MqScF0gxR>tO26g~N?OR~xmm2^ZhDAm$ zy)p1pI}%notEfPDFB|8w7fz(AD2V()0@K%#wA=T+Ha4LgdsrJQshARcgk`KqqlsXh`=_g^Tg zot7`o5c!vdzl!>0kOIsJ5W-`R>2&y%oqtqr*IphG|K^RXjEqd#&fVA##r?1?Amn8G zpPxa#eU+U25;O)#5B?USiLeJL;79Dvi`_(tKR$l^*s5Io1|IhEI@(}vD}QWlXU7T@ zhch?kRl;&XaphT5qN1$qQnpwrh7xO@-Vl#kf$Oj2#U|=9CE6dQt zBrrDiK|WVd9LbUE>Tn~lo)tVo(7?`LE!Gmurpl)D;`|TA>yHNZuZkB+10(43^z|>B zzZuLH`C5*u!{}9bW&1-e$!l*pUrXIJddi1Fr-U&K5_H>)DK216D4p2%@SbypX%Aai*bLh{9lQ2f0p(?#W>*0fJ~(Q0AqDlo`WlZ+Vnc$>MY5$GsrCv z_!m%`Tmp~veL1x?>{_QyIzj~TP zOe0EG^6qVG%Wd&yg}S&lRx(uQ?GF4z3S7%}q9PrbX-jJ>AvJa6ix)3~me+tX284u! zjFtfP7>lXshtZq}p+}^$qLV$yZ=hAnGb%+*8iCx!su!C`NajnHW zo12DE;ZII>`cQ=uR2vQu4S)Xng@%cbztl$UZ@SPKixBoXcmT zH?jr>2H3ixPLs|EadB}}#_-->A1E;p1DG4*T4g_DHro<*aD4pU_8YI;`lOtU%w=F< z9-f{9_5SB?{OSg8A7M<^dPWO4FRE5rJ;+f>jfjdOjJ~b1h$>kizY$YYh*L|0XYFr?YH3G{rQy` z0|NuN#OB^!4sa56VqZ#-CmO))6>+xHwecVopc|T-w?Y02i-^Dj$pod5yn00dq$I0S z1t2J7R-37hvGBwN)p|eQ2x>9b;JYnc7tqlGaSrQ#Kz5Om@!53)A_0hq+@9}{-|>aY z)Jl~LVhy+58#Z1i2Ltr`tq*~A*!=N@_6X=Dw1B9=3 z;c7!i2Np;JOs2FcKj!D9fN)gC#U~`ZPE1sI^oT<88LnC!`HdS!CMG+%qAx*$N(QzN zE806NJd>JVQlbKYDe4!fS{&TmF&9WODE5;A10|yt01F`iWvE%#Jbl_|UH5o}_%x99 zac$o3<6xX?7NGq`{Q{4bF*c?HNYlv~E8hwVSr$PsBL--)FauD0wK$M42`MS{SN-}# zJzgg)TRQ-)$;im4h=avP!s8b%QBDD!M`^df4vmeCuk%=GM$J`Nk45NKT5XMJ8*msk zT?8%zNh}>e1n6^e;y7v%-n}Cuu}_{L5_oOp|4L^8PBHjhdIts*_B(kC3eMliEA5fF zkjpXw2-1VPi*s|$K$!J|(|jvl_8$e{V8}oM>Qp?JIUY()>xjOsS)?0_ zP13ne3v_}B61Mb_TM)rQbab>N>`~46D+VDUauJa_^$nHN6zTes$J)CbGr2alPCkZynrU^R0h)!6hRvl+!4mwzRo$!Cr zU^wu8&Gzk$hLsgFV4Q3~DVpyz<*?_k?Ch>+NJvW7_#X3uz>#6p{0t;{6x%Rv%ee{^ zS;Yw$1*q>8IgY=k>x8(>x*NvF4`P$~*w`W|h1^uLA0pO-BdOgllbyc`u31T>8-65CS`pT zT2pfnWiBh~h?A3(14vspggw~dNfSt^H*el_ zm~SV9{IdyZ4H#UZUR8vs?@_7Ulwg`+%rW@ekLBg|JhjXUGluTIK6A(_9$sFv%gdp; zxwnC}qJQsA7Q!Xx#eD*pOITPKav%nFj(!TCJ(cg#Zew$EAn*g`pJ5sIqSs zg02Y2sdlGV2qpKyG(?1ipvXSuOk}*O$YN|`(>;`{atI1k4GwjJ*?1C6WLE4SD;8Or{aY^miZEKE&#WN81) zfCnZ(BdR}J;Tq(tcBoS^{oU>D-+?59DkD{s0}`j*bS)H^XIfv2fFNsVXuOV&{#s#4 z49S}zuK7Jsfr=axOUQ9$nQsgL*@N~IBN>7>0Q41d5wI{5&`7~yGYSg2(j`NnuO^;3 z;IR2!9L7bptYZCb;8Pf&t4Rs~Hw%tyqbv(KJ}EH16Bh{o%w2poWvLGH-JJ7;z zfu9V*dSP&=c|c!h|Hv~;Ab(^zFZBX=c7S-Qt*Z-r{=5mQg3fz0N7x0(vhgC`yoeBd z>RYU=cx-HJP@gawmQwwdAx1!f*Nb8z>J)4P0-pUUI=UI$83pdiUdrUFl^E2g zT)sl)0Lv3S-PM3bNf;>cK=IKG0rrE$5qfaoF*!A*1H3NkhI$v#_3NP^V1VbD{Adb> zE>e<;=wa5llZO^$fw!;}pzgZgglCdwKwpH{IzL$wL)GBI!n;rqzo42>TxPhC%5k&I`~+exCz}_@z%y3(twHfYf1pA1^bx*BH~?7h<8a#F(^M0POc(MX6tog!z!UI#)jo&igUK8=ASoDLaGAaTf2{JaYqMVCkJ43YBloKCS+36$Y1K{xiP*y;T z)T#60f$u=yCJ0nk5SJe>Hot1pEvcyysI(eUw~`?xC2fr;G#C}YK34y|``ix~J1{GY zuo1^#W@l)G^z7_s#ek=$r|xM*pZDJNl2!332)7rtDNQ?ONTiXqbwTY6gs02KbTUx1 zGOH10V5@2cBd9*79WE`c?qeuIsNNU&U>PJu(690@HdW2|DS$*Z0~|Z-Juc`XPr;&> zaj&O^`}u<-fefAhxK0qPrIip5iaB&$W1ug4RX|x+H(~ocxDAzoK>^UZa0>p=+RO1^ zQ~15Tj5YBqE3#&`?c`jhi6NJp5pD12)PSx)%@_uur*# zDspnPf`Vj{l9H%yqEG4`(6>&32h1Zlb<}-v%`X^zihzK0T8Fh&4k8fO>QH9^SZr9h zZ6{(Ny$pdO*qbCE34#mY)Ldj&974V-_~px&lh>h??L$Kefc6W3?I^uUx5};u(kY<7 z9M~t9UlSxRWi)<%h6;X@pZ_h0;HWM-c;&-SZYq5+$c?|%6@*fnCLU}Q3eNH0Z zv#kT_Hp=e-z7s=|hwg^qPU6h(0OApu$AaKryx0rJ#~pAY~*Xo5;g*udeS%RxX! z_8caMLZZP`_M2Y?9e%nnHV4h{~69j_VW-4%?Yr{NgE$`XrB z)3>i*JAM{s9Q3_ajDI8wdx>iCI6Lpm_;O`uWvP}Kw;G!tDk>;wd^Xm2y>x7fb9?{* zBoEMTO+phgnVx~+lB8s_@$pSKMk1%IoCEZ`Raqc0HMI|1KMyiUS>sy(_JGKsKevF_ zyAAFQZHCtV{?hT!hFg8VGc;}~z_F1QYD_gtAk!_>H2-oeQ9D)f3ldk>GqbL5i?MDL{a<6?W!cv+~z6v7FznyHU zHU5K%{eNGBBMi_Ed*!ZyN&APw|Lttc|9Yh5pU3$tO&6BU97r3mkbjg4+6H7@1VC+6 z8!}e@QUe4FzvFDs)eM6Aos_Pg9+vHBI14i?`fsc|ioloHWn~c6+Itlruk+IjBbpX)%b+vMplbwE9{~Jk12NZ}kFxEIee*Fm)M-r?7 zh?{F){eWNk><-BC3ks6zKvPf>Rhb}nqofxgP|&5hGj##r*w1fOkw9OJY;AiWSF>|* znGENtp~O=Ci*UvQ#M~qRRGanZr;&h2`U`cqy!O|XKx#w7#KNNG=DrH-Gaw+~KJ+DF z6lkyC0Ii<0L^gCRA6mDdGRIYmGBSpORO$p;Bgl8M%E}St>wCYx0dget_xA@u{|i8A zQ0^K4(E^THpo&^NNzH=>Ma!2ja4IJEZdZ4=DX^v8^(j5;ZveN4tz4r_OwG;bfZYJ? zMs?u!_V!RsQeflRD;padkr3igxEP?}1>JC>Cy}B@p-`rcO-!1zak@EO{92U zTQcU@=fG$f*WQ9E_8E*3oaH5z!s>Z^>p&aF8?1m_M z*%-4nXsC4xe}P;BWZ4LsFY@niiijiwtN}98jN6ev+>;#~@{J5BhSD*?)ivTw(hy4o<3>^(N`8^A&Hd_yJC+ z_=p9k?VieHHh%f?43_K!FrAK$4pc1k-rnBsM81;ox%?3;X05`!%0hNkoO&mug+<(B+w8Q4xZ6$A8E91!w#A-B6wMHpav7!=|*As$h1Wtad5z}#~K|I=8M z%=-aOe!Qru@vO0GL+VEo*wg)8PDzL0y4NWA6_j4})M+vAjauw&!~7)h6-fz+3!mJ6 zfx%DRe_11uDDsBtq`5abnbh`}SzgH2il9Hz0Es0ah446^vc z3JXG%2t!ry4-KZi{8oLCg}_LmPBiqt42_K?V6!R{+8KW9ld?im6Fi4?L7Dq9G(-&N z*HC`3cyzEe$HB+fHZ-K+i3#U(=yrGEf9DBvNJC=*;>PLM1P>f&c?+>hNJt3!+eI-k zF*xb5wY|-}<&|RF`^Hs7qE;EY@W2~&*Os6Sj6fYXKtsO_#|^r`flzS@cNWcR2jwVDroy}dH<76qJJ<@ZyUQwu9fIoa76%c38F@85sYbbc=0P*6=pJFN*i zUi})Hh2wq%_fjyy?3MqdM8^TNuZrWfwU+ZcTDc7z6>#ej`MF@ap+(v3Gd#$X^VV%)48nj0Sx^#n7gk(UFyzDU^-s z<~Zk!-jsKkq?y#wSLp{-Hshlr17E&>KT)ec%&MjBZOjV2QE8!}p=tvLnA1TS^Vsfj zJKs98Q>~=Z^n!xpL1v%NFbsX?c`fdC>d)^r^Zq`*s>I1xF^qmEh%WWy4i>7x$~(b0 zs8{WyEO2WCc@_L9FZHr{?V$Qt;y{KmFt1?k#vu$>;zwJ2`k2y)RwiN(~0 z4z&_dPJ>g~r^Q$8ok_02DJvTqY%sK`1DH-X|Mtd#NC3wM^~=Jwwy_b~;n*=%EQqp)bDBYG<+gc1K@JG3DZjD0$d0ZA_uyXTg@z*B%o`$!coid{QT zBDkqdvr%%0LE**HA4HZ$h$yW{D*It)gZXubcuX~+3LLLzKYSRNmX@|=wntI8rKkr4 z_OWgy6Jk~*rytbvc|bIM^Y-ltao&!O!x+isSEujq+jI}!`IeWLw|9b%j4sM1JU7dG zm>)!0N?WChBx3!~^p%m?R~JR)K1*TM?mu{NN81Ny{H4Q3kIE}wI9?KErvyk@ATuk3 z2sy~OPXuW9yAdr=#T@jFZz&mJVzR7#YsI-AZ4C=MnN6NzOW^rV`$lV4^%s}GFOhXw z`8KiK;dUV#hpfdM6bk*cWJXXT3*FZUeIL0d>j)S_?*zx|rndU$9Oc6pqa;tu%1(rY zNNJEqHLhHHt2J{^=1u1eD{5J8uGB5}B()q9GT5;SUD2n1e;uAM*hM_(3tqb%<44To zeirPijYO!0g}u}?H1b7wSS~wX{q9oGtV)=+Av9VH#WiQRL9FJC=g-f>uH_sA{w`{A zdwjVsYDaiuqM1Ogh@r%#-We;=%gf8p-=sn$+YKeYxn-XlO5+jG9|2uH=8Tm|6z#O5 z?e#r8JQ&%kSV%%Y@}J$A&H_}`EtnAN>gt5>?@{*Vh}gaUN(*_>{>G}6Zyd#QCqgF^ z%qBdguo`Z9JX$h%;wl^Jf}j8UUwpb+N`w%^F`nd-8$9Gfq1ZUSN$H=nxF*5H;UNi> zfeOy(Qgn6kIFb3lm#~GdFf=-Ll&}$=yj2M;q>w+Mf3T|~R7RMY324v=jm^BAMW+x} zxog5OR)W~`JbZBR_4QApgrWn-Ad{UBoL+i&cYJYzz+2lZJ29LYaphO%2J1;%C24Uk zdB)@ewuYsrWipvVU`u<6Zl+maXVxN4dClCHNe*N;wUk@C7QI`KNJGJafwZn$G(K1Z%bx^q`7O z0f$m&A{khdi>if1KjxjH)MUlI9x${0KXlfXUQ4DkCH0xYJSanA1Fi$UoW00@KZx+ z23$B+vpQNqpvN_Nr$1H&nT0z9H%`lQhp z8e3W-=(4*!J;_*u2xHiAe!|hBw*x2$ND(7a!({t@QHjJ!k}YLZBzVyW=U%G7x5~PA zPY!R6jC2l9QO?TB%J#Urre*X2&cfL4|wD;~h1D z-}>&xtHt^s%7p_{1V|$m=}(c6Q$9=Hp%gfGHk%*RCJh>z3#joCd7~}HwT6aiXR~>$G@<(PC#JAe@KjTZ`unh%i|@FV#OP_eZrT*3pli-$2%nU` zq@$;2T_>S$8MK-_fByWG3u7g+h)g8f0GupvdjSb6K43a6PiJdL+k8g*aAHr#JZ#9E zhY!av=4;Om(9H`xb_^uH-A416_syIT!%5m%W3xr}lTJre5~n8I;3qSax^U zq~S9>!Z_7&Yjf!dq}%rMM$@Mc@uSJRj=upJ<Ov6hY>Zc%{7qpb<*bAQLzr_`>f{ETrNN#;Uw&@DGYM`v#Pp6rjcyDs3k zb8|;en>H9p8TQii@{kED!mw(aUL8uE`d<<$aeeruOY^<=I6dxaj(e~#qG_=G0cBh> zHBHSJuf~6}Z-Hb(E&K_)lOvU>yB%f zSh%{n{@T@nFlS+xruv!*fULY+2DLQC{y7Yb=C4sf5?t>v@GIjHm_ zwY4v1t`&4278-+0#ggLc(;=_lzpSarVkTCf;i0>s;9Lb=eDu_*0=>TE5KSPzjvj^J zk9Znsnntrb3y3dr5}zee_R>^Wirt^Av_QUH--SXZlzjg&w%kz6a5{b3t LPjg}{R{Q@O>2d}H literal 0 HcmV?d00001 diff --git a/doc/user/group/planning_hierarchy/index.md b/doc/user/group/planning_hierarchy/index.md new file mode 100644 index 00000000000..4af8ceca7f5 --- /dev/null +++ b/doc/user/group/planning_hierarchy/index.md @@ -0,0 +1,59 @@ +--- +type: reference +stage: Plan +group: Product Planning +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Planning hierarchies **(PREMIUM)** + +Planning hierarchies are an integral part of breaking down your work in GitLab. +To understand how you can use epics and issues together in hierarchies, remember the following: + +- [Epics](../epics/index.md) exist in groups. +- [Issues](../../project/issues/index.md) exist in projects. + +GitLab is not opinionated on how you structure your work and the hierarchy you can build with multi-level +epics. For example, you can use the hierarchy as a folder of issues for bigger initiatives. + +To learn about hierarchies in general, common frameworks, and using GitLab for +portfolio management, see +[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/). + +## Hierarchies with epics + +With epics, you can achieve the following hierarchy: + +```mermaid +graph TD + Group_epic --> Project1_Issue1 + Group_epic --> Project1_Issue2 + Group_epic --> Project2_Issue1 +``` + +### Hierarchies with multi-level epics **(ULTIMATE)** + +With the addition of [multi-level epics](../epics/manage_epics.md#multi-level-child-epics) and up to +seven levels of nested epics, you can achieve the following hierarchy: + +```mermaid +classDiagram + direction TD + class Epic + class Issue + + Epic *-- "0..7" Epic + Epic "1" *-- "0..*" Issue +``` + +## View ancestry of an epic + +In an epic, you can view the ancestors as parents in the right sidebar under **Ancestors**. + +![epics state dropdown](img/epic-view-ancestors-in-sidebar_v14_6.png) + +## View ancestry of an issue + +In an issue, you can view the parented epic above the issue in the right sidebar under **Epic**. + +![epics state dropdown](img/issue-view-parent-epic-in-sidebar_v14_6.png) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 7dce4fa0ce2..4245dd80714 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -4,6 +4,7 @@ module Gitlab module Database module MigrationHelpers include Migrations::BackgroundMigrationHelpers + include Migrations::BatchedBackgroundMigrationHelpers include DynamicModelHelpers include RenameTableHelpers include AsyncIndexes::MigrationHelpers diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb index 9bc68e9d7c9..50c8e35f9b3 100644 --- a/lib/gitlab/database/migrations/background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/background_migration_helpers.rb @@ -5,11 +5,7 @@ module Gitlab module Migrations module BackgroundMigrationHelpers BATCH_SIZE = 1_000 # Number of rows to process per job - SUB_BATCH_SIZE = 100 # Number of rows to process per sub-batch JOB_BUFFER_SIZE = 1_000 # Number of jobs to bulk queue at a time - BATCH_CLASS_NAME = 'PrimaryKeyBatchingStrategy' # Default batch class for batched migrations - BATCH_MIN_VALUE = 1 # Default minimum value for batched migrations - BATCH_MIN_DELAY = 2.minutes.freeze # Minimum delay between batched migrations # Bulk queues background migration jobs for an entire table, batched by ID range. # "Bulk" meaning many jobs will be pushed at a time for efficiency. @@ -170,102 +166,6 @@ module Gitlab duration end - # Creates a batched background migration for the given table. A batched migration runs one job - # at a time, computing the bounds of the next batch based on the current migration settings and the previous - # batch bounds. Each job's execution status is tracked in the database as the migration runs. The given job - # class must be present in the Gitlab::BackgroundMigration module, and the batch class (if specified) must be - # present in the Gitlab::BackgroundMigration::BatchingStrategies module. - # - # If migration with same job_class_name, table_name, column_name, and job_aruments already exists, this helper - # will log an warning and not create a new one. - # - # job_class_name - The background migration job class as a string - # batch_table_name - The name of the table the migration will batch over - # batch_column_name - The name of the column the migration will batch over - # job_arguments - Extra arguments to pass to the job instance when the migration runs - # job_interval - The pause interval between each job's execution, minimum of 2 minutes - # batch_min_value - The value in the column the batching will begin at - # batch_max_value - The value in the column the batching will end at, defaults to `SELECT MAX(batch_column)` - # batch_class_name - The name of the class that will be called to find the range of each next batch - # batch_size - The maximum number of rows per job - # sub_batch_size - The maximum number of rows processed per "iteration" within the job - # - # - # *Returns the created BatchedMigration record* - # - # Example: - # - # queue_batched_background_migration( - # 'CopyColumnUsingBackgroundMigrationJob', - # :events, - # :id, - # job_interval: 2.minutes, - # other_job_arguments: ['column1', 'column2']) - # - # Where the the background migration exists: - # - # class Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob - # def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, *other_args) - # # do something - # end - # end - def queue_batched_background_migration( # rubocop:disable Metrics/ParameterLists - job_class_name, - batch_table_name, - batch_column_name, - *job_arguments, - job_interval:, - batch_min_value: BATCH_MIN_VALUE, - batch_max_value: nil, - batch_class_name: BATCH_CLASS_NAME, - batch_size: BATCH_SIZE, - sub_batch_size: SUB_BATCH_SIZE - ) - - if Gitlab::Database::BackgroundMigration::BatchedMigration.for_configuration(job_class_name, batch_table_name, batch_column_name, job_arguments).exists? - Gitlab::AppLogger.warn "Batched background migration not enqueued because it already exists: " \ - "job_class_name: #{job_class_name}, table_name: #{batch_table_name}, column_name: #{batch_column_name}, " \ - "job_arguments: #{job_arguments.inspect}" - return - end - - job_interval = BATCH_MIN_DELAY if job_interval < BATCH_MIN_DELAY - - batch_max_value ||= connection.select_value(<<~SQL) - SELECT MAX(#{connection.quote_column_name(batch_column_name)}) - FROM #{connection.quote_table_name(batch_table_name)} - SQL - - migration_status = batch_max_value.nil? ? :finished : :active - batch_max_value ||= batch_min_value - - migration = Gitlab::Database::BackgroundMigration::BatchedMigration.create!( - job_class_name: job_class_name, - table_name: batch_table_name, - column_name: batch_column_name, - job_arguments: job_arguments, - interval: job_interval, - min_value: batch_min_value, - max_value: batch_max_value, - batch_class_name: batch_class_name, - batch_size: batch_size, - sub_batch_size: sub_batch_size, - status: migration_status) - - # This guard is necessary since #total_tuple_count was only introduced schema-wise, - # after this migration helper had been used for the first time. - return migration unless migration.respond_to?(:total_tuple_count) - - # We keep track of the estimated number of tuples to reason later - # about the overall progress of a migration. - migration.total_tuple_count = Gitlab::Database::SharedModel.using_connection(connection) do - Gitlab::Database::PgClass.for_table(batch_table_name)&.cardinality_estimate - end - migration.save! - - migration - end - # Force a background migration to complete. # # WARNING: This method will block the caller and move the background migration from an diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb new file mode 100644 index 00000000000..dcaf7fad05f --- /dev/null +++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module Gitlab + module Database + module Migrations + # BatchedBackgroundMigrations are a new approach to scheduling and executing background migrations, which uses + # persistent state in the database to track each migration. This avoids having to batch over an entire table and + # schedule a large number of sidekiq jobs upfront. It also provides for more flexibility as the migration runs, + # as it can be paused and restarted, and have configuration values like the batch size updated dynamically as the + # migration runs. + # + # For now, these migrations are not considered ready for general use, for more information see the tracking epic: + # https://gitlab.com/groups/gitlab-org/-/epics/6751 + module BatchedBackgroundMigrationHelpers + BATCH_SIZE = 1_000 # Number of rows to process per job + SUB_BATCH_SIZE = 100 # Number of rows to process per sub-batch + BATCH_CLASS_NAME = 'PrimaryKeyBatchingStrategy' # Default batch class for batched migrations + BATCH_MIN_VALUE = 1 # Default minimum value for batched migrations + BATCH_MIN_DELAY = 2.minutes.freeze # Minimum delay between batched migrations + + # Creates a batched background migration for the given table. A batched migration runs one job + # at a time, computing the bounds of the next batch based on the current migration settings and the previous + # batch bounds. Each job's execution status is tracked in the database as the migration runs. The given job + # class must be present in the Gitlab::BackgroundMigration module, and the batch class (if specified) must be + # present in the Gitlab::BackgroundMigration::BatchingStrategies module. + # + # If migration with same job_class_name, table_name, column_name, and job_aruments already exists, this helper + # will log an warning and not create a new one. + # + # job_class_name - The background migration job class as a string + # batch_table_name - The name of the table the migration will batch over + # batch_column_name - The name of the column the migration will batch over + # job_arguments - Extra arguments to pass to the job instance when the migration runs + # job_interval - The pause interval between each job's execution, minimum of 2 minutes + # batch_min_value - The value in the column the batching will begin at + # batch_max_value - The value in the column the batching will end at, defaults to `SELECT MAX(batch_column)` + # batch_class_name - The name of the class that will be called to find the range of each next batch + # batch_size - The maximum number of rows per job + # sub_batch_size - The maximum number of rows processed per "iteration" within the job + # + # *Returns the created BatchedMigration record* + # + # Example: + # + # queue_batched_background_migration( + # 'CopyColumnUsingBackgroundMigrationJob', + # :events, + # :id, + # job_interval: 2.minutes, + # other_job_arguments: ['column1', 'column2']) + # + # Where the the background migration exists: + # + # class Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJob + # def perform(start_id, end_id, batch_table, batch_column, sub_batch_size, *other_args) + # # do something + # end + # end + def queue_batched_background_migration( # rubocop:disable Metrics/ParameterLists + job_class_name, + batch_table_name, + batch_column_name, + *job_arguments, + job_interval:, + batch_min_value: BATCH_MIN_VALUE, + batch_max_value: nil, + batch_class_name: BATCH_CLASS_NAME, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + + if Gitlab::Database::BackgroundMigration::BatchedMigration.for_configuration(job_class_name, batch_table_name, batch_column_name, job_arguments).exists? + Gitlab::AppLogger.warn "Batched background migration not enqueued because it already exists: " \ + "job_class_name: #{job_class_name}, table_name: #{batch_table_name}, column_name: #{batch_column_name}, " \ + "job_arguments: #{job_arguments.inspect}" + return + end + + job_interval = BATCH_MIN_DELAY if job_interval < BATCH_MIN_DELAY + + batch_max_value ||= connection.select_value(<<~SQL) + SELECT MAX(#{connection.quote_column_name(batch_column_name)}) + FROM #{connection.quote_table_name(batch_table_name)} + SQL + + migration_status = batch_max_value.nil? ? :finished : :active + batch_max_value ||= batch_min_value + + migration = Gitlab::Database::BackgroundMigration::BatchedMigration.create!( + job_class_name: job_class_name, + table_name: batch_table_name, + column_name: batch_column_name, + job_arguments: job_arguments, + interval: job_interval, + min_value: batch_min_value, + max_value: batch_max_value, + batch_class_name: batch_class_name, + batch_size: batch_size, + sub_batch_size: sub_batch_size, + status: migration_status) + + # This guard is necessary since #total_tuple_count was only introduced schema-wise, + # after this migration helper had been used for the first time. + return migration unless migration.respond_to?(:total_tuple_count) + + # We keep track of the estimated number of tuples to reason later + # about the overall progress of a migration. + migration.total_tuple_count = Gitlab::Database::SharedModel.using_connection(connection) do + Gitlab::Database::PgClass.for_table(batch_table_name)&.cardinality_estimate + end + migration.save! + + migration + end + end + end + end +end diff --git a/lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb b/lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb new file mode 100644 index 00000000000..e7ac562d844 --- /dev/null +++ b/lib/gitlab/patch/action_cable_subscription_adapter_identifier.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Modifies https://github.com/rails/rails/blob/v6.1.4.1/actioncable/lib/action_cable/subscription_adapter/base.rb so +# that we do not overwrite an id that was explicitly set to `nil` in cable.yml. +# This is needed to support GCP Memorystore. See https://github.com/rails/rails/issues/38244. + +module Gitlab + module Patch + module ActionCableSubscriptionAdapterIdentifier + def identifier + @server.config.cable.has_key?(:id) ? @server.config.cable[:id] : super # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + end + end +end diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js index 6a835a28807..04348a01dde 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js @@ -167,6 +167,7 @@ export const imageTagsMock = (nodes = tagsMock) => ({ data: { containerRepository: { id: containerRepositoryMock.id, + tagsCount: nodes.length, tags: { nodes, pageInfo: { ...tagsPageInfo }, @@ -191,7 +192,7 @@ export const graphQLImageDetailsMock = (override) => ({ data: { containerRepository: { ...containerRepositoryMock, - + tagsCount: tagsMock.length, tags: { nodes: tagsMock, pageInfo: { ...tagsPageInfo }, diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js index adc9a64e5c9..9b821ba8ef3 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js @@ -1,6 +1,7 @@ import { GlKeysetPagination } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; +import { nextTick } from 'vue'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; @@ -22,6 +23,7 @@ import { } from '~/packages_and_registries/container_registry/explorer/constants'; import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; +import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql'; import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue'; import Tracking from '~/tracking'; @@ -32,6 +34,7 @@ import { containerRepositoryMock, graphQLEmptyImageDetailsMock, tagsMock, + imageTagsMock, } from '../mock_data'; import { DeleteModal } from '../stubs'; @@ -67,12 +70,13 @@ describe('Details Page', () => { const waitForApolloRequestRender = async () => { await waitForPromises(); - await wrapper.vm.$nextTick(); + await nextTick(); }; const mountComponent = ({ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)), options, config = {}, } = {}) => { @@ -81,6 +85,7 @@ describe('Details Page', () => { const requestHandlers = [ [getContainerRepositoryDetailsQuery, resolver], [deleteContainerRepositoryTagsMutation, mutationResolver], + [getContainerRepositoryTagsQuery, tagsResolver], ]; apolloProvider = createMockApollo(requestHandlers); @@ -242,38 +247,49 @@ describe('Details Page', () => { describe('confirmDelete event', () => { let mutationResolver; + let tagsResolver; beforeEach(() => { mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock); - mountComponent({ mutationResolver }); + tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock)); + mountComponent({ mutationResolver, tagsResolver }); return waitForApolloRequestRender(); }); + describe('when one item is selected to be deleted', () => { - it('calls apollo mutation with the right parameters', async () => { + it('calls apollo mutation with the right parameters and refetches the tags list query', async () => { findTagsList().vm.$emit('delete', [cleanTags[0]]); - await wrapper.vm.$nextTick(); + await nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: [cleanTags[0].name] }), ); + + await waitForPromises(); + + expect(tagsResolver).toHaveBeenCalled(); }); }); describe('when more than one item is selected to be deleted', () => { - it('calls apollo mutation with the right parameters', async () => { + it('calls apollo mutation with the right parameters and refetches the tags list query', async () => { findTagsList().vm.$emit('delete', tagsMock); - await wrapper.vm.$nextTick(); + await nextTick(); findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }), ); + + await waitForPromises(); + + expect(tagsResolver).toHaveBeenCalled(); }); }); }); @@ -382,7 +398,7 @@ describe('Details Page', () => { findPartialCleanupAlert().vm.$emit('dismiss'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, { feature_name: config.userCalloutId, @@ -472,7 +488,7 @@ describe('Details Page', () => { await waitForApolloRequestRender(); findDetailsHeader().vm.$emit('delete'); - await wrapper.vm.$nextTick(); + await nextTick(); }; it('on delete event it deletes the image', async () => { @@ -497,13 +513,13 @@ describe('Details Page', () => { findDeleteImage().vm.$emit('start'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findTagsLoader().exists()).toBe(true); findDeleteImage().vm.$emit('end'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findTagsLoader().exists()).toBe(false); }); @@ -513,7 +529,7 @@ describe('Details Page', () => { findDeleteImage().vm.$emit('error'); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE); }); diff --git a/spec/frontend/sidebar/components/attention_required_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js similarity index 96% rename from spec/frontend/sidebar/components/attention_required_toggle_spec.js rename to spec/frontend/sidebar/components/attention_requested_toggle_spec.js index 8555068cdd8..0939297a754 100644 --- a/spec/frontend/sidebar/components/attention_required_toggle_spec.js +++ b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js @@ -23,8 +23,8 @@ describe('Attention require toggle', () => { it.each` attentionRequested | icon - ${true} | ${'star'} - ${false} | ${'star-o'} + ${true} | ${'attention-solid'} + ${false} | ${'attention'} `( 'renders $icon icon when attention_requested is $attentionRequested', ({ attentionRequested, icon }) => { diff --git a/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb new file mode 100644 index 00000000000..12988b851ef --- /dev/null +++ b/spec/initializers/action_cable_subscription_adapter_identifier_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do + describe '#identifier' do + context 'when id key is nil on cable.yml' do + it 'does not override server config id with action cable pid' do + config = { + adapter: 'redis', + url: 'unix:/home/localuser/redis/redis.socket', + channel_prefix: 'test_', + id: nil + } + ::ActionCable::Server::Base.config.cable = config + + sub = ActionCable.server.pubsub.send(:redis_connection) + + expect(sub.connection[:id]).to eq('redis:///home/localuser/redis/redis.socket/0') + expect(ActionCable.server.config.cable[:id]).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb index e42a6c970ea..35c81393f39 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -354,161 +354,6 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end end - describe '#queue_batched_background_migration' do - let(:pgclass_info) { instance_double('Gitlab::Database::PgClass', cardinality_estimate: 42) } - - before do - allow(Gitlab::Database::PgClass).to receive(:for_table).and_call_original - end - - context 'when such migration already exists' do - it 'does not create duplicate migration' do - create( - :batched_background_migration, - job_class_name: 'MyJobClass', - table_name: :projects, - column_name: :id, - interval: 10.minutes, - min_value: 5, - max_value: 1005, - batch_class_name: 'MyBatchClass', - batch_size: 200, - sub_batch_size: 20, - job_arguments: [[:id], [:id_convert_to_bigint]] - ) - - expect do - model.queue_batched_background_migration( - 'MyJobClass', - :projects, - :id, - [:id], [:id_convert_to_bigint], - job_interval: 5.minutes, - batch_min_value: 5, - batch_max_value: 1000, - batch_class_name: 'MyBatchClass', - batch_size: 100, - sub_batch_size: 10) - end.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } - end - end - - it 'creates the database record for the migration' do - expect(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info) - - expect do - model.queue_batched_background_migration( - 'MyJobClass', - :projects, - :id, - job_interval: 5.minutes, - batch_min_value: 5, - batch_max_value: 1000, - batch_class_name: 'MyBatchClass', - batch_size: 100, - sub_batch_size: 10) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( - job_class_name: 'MyJobClass', - table_name: 'projects', - column_name: 'id', - interval: 300, - min_value: 5, - max_value: 1000, - batch_class_name: 'MyBatchClass', - batch_size: 100, - sub_batch_size: 10, - job_arguments: %w[], - status: 'active', - total_tuple_count: pgclass_info.cardinality_estimate) - end - - context 'when the job interval is lower than the minimum' do - let(:minimum_delay) { described_class::BATCH_MIN_DELAY } - - it 'sets the job interval to the minimum value' do - expect do - model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: minimum_delay - 1.minute) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last - - expect(created_migration.interval).to eq(minimum_delay) - end - end - - context 'when additional arguments are passed to the method' do - it 'saves the arguments on the database record' do - expect do - model.queue_batched_background_migration( - 'MyJobClass', - :projects, - :id, - 'my', - 'arguments', - job_interval: 5.minutes, - batch_max_value: 1000) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( - job_class_name: 'MyJobClass', - table_name: 'projects', - column_name: 'id', - interval: 300, - min_value: 1, - max_value: 1000, - job_arguments: %w[my arguments]) - end - end - - context 'when the max_value is not given' do - context 'when records exist in the database' do - let!(:event1) { create(:event) } - let!(:event2) { create(:event) } - let!(:event3) { create(:event) } - - it 'creates the record with the current max value' do - expect do - model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last - - expect(created_migration.max_value).to eq(event3.id) - end - - it 'creates the record with an active status' do - expect do - model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_active - end - end - - context 'when the database is empty' do - it 'sets the max value to the min value' do - expect do - model.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last - - expect(created_migration.max_value).to eq(created_migration.min_value) - end - - it 'creates the record with a finished status' do - expect do - model.queue_batched_background_migration('MyJobClass', :projects, :id, job_interval: 5.minutes) - end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) - - expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_finished - end - end - end - end - describe '#migrate_async' do it 'calls BackgroundMigrationWorker.perform_async' do expect(BackgroundMigrationWorker).to receive(:perform_async).with("Class", "hello", "world") diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb new file mode 100644 index 00000000000..c45149d67bf --- /dev/null +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers do + let(:migration) do + ActiveRecord::Migration.new.extend(described_class) + end + + describe '#queue_batched_background_migration' do + let(:pgclass_info) { instance_double('Gitlab::Database::PgClass', cardinality_estimate: 42) } + + before do + allow(Gitlab::Database::PgClass).to receive(:for_table).and_call_original + end + + context 'when such migration already exists' do + it 'does not create duplicate migration' do + create( + :batched_background_migration, + job_class_name: 'MyJobClass', + table_name: :projects, + column_name: :id, + interval: 10.minutes, + min_value: 5, + max_value: 1005, + batch_class_name: 'MyBatchClass', + batch_size: 200, + sub_batch_size: 20, + job_arguments: [[:id], [:id_convert_to_bigint]] + ) + + expect do + migration.queue_batched_background_migration( + 'MyJobClass', + :projects, + :id, + [:id], [:id_convert_to_bigint], + job_interval: 5.minutes, + batch_min_value: 5, + batch_max_value: 1000, + batch_class_name: 'MyBatchClass', + batch_size: 100, + sub_batch_size: 10) + end.not_to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count } + end + end + + it 'creates the database record for the migration' do + expect(Gitlab::Database::PgClass).to receive(:for_table).with(:projects).and_return(pgclass_info) + + expect do + migration.queue_batched_background_migration( + 'MyJobClass', + :projects, + :id, + job_interval: 5.minutes, + batch_min_value: 5, + batch_max_value: 1000, + batch_class_name: 'MyBatchClass', + batch_size: 100, + sub_batch_size: 10) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( + job_class_name: 'MyJobClass', + table_name: 'projects', + column_name: 'id', + interval: 300, + min_value: 5, + max_value: 1000, + batch_class_name: 'MyBatchClass', + batch_size: 100, + sub_batch_size: 10, + job_arguments: %w[], + status: 'active', + total_tuple_count: pgclass_info.cardinality_estimate) + end + + context 'when the job interval is lower than the minimum' do + let(:minimum_delay) { described_class::BATCH_MIN_DELAY } + + it 'sets the job interval to the minimum value' do + expect do + migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: minimum_delay - 1.minute) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last + + expect(created_migration.interval).to eq(minimum_delay) + end + end + + context 'when additional arguments are passed to the method' do + it 'saves the arguments on the database record' do + expect do + migration.queue_batched_background_migration( + 'MyJobClass', + :projects, + :id, + 'my', + 'arguments', + job_interval: 5.minutes, + batch_max_value: 1000) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to have_attributes( + job_class_name: 'MyJobClass', + table_name: 'projects', + column_name: 'id', + interval: 300, + min_value: 1, + max_value: 1000, + job_arguments: %w[my arguments]) + end + end + + context 'when the max_value is not given' do + context 'when records exist in the database' do + let!(:event1) { create(:event) } + let!(:event2) { create(:event) } + let!(:event3) { create(:event) } + + it 'creates the record with the current max value' do + expect do + migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last + + expect(created_migration.max_value).to eq(event3.id) + end + + it 'creates the record with an active status' do + expect do + migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_active + end + end + + context 'when the database is empty' do + it 'sets the max value to the min value' do + expect do + migration.queue_batched_background_migration('MyJobClass', :events, :id, job_interval: 5.minutes) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + created_migration = Gitlab::Database::BackgroundMigration::BatchedMigration.last + + expect(created_migration.max_value).to eq(created_migration.min_value) + end + + it 'creates the record with a finished status' do + expect do + migration.queue_batched_background_migration('MyJobClass', :projects, :id, job_interval: 5.minutes) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(1) + + expect(Gitlab::Database::BackgroundMigration::BatchedMigration.last).to be_finished + end + end + end + end +end