Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-11 18:10:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-11 18:10:20 +0300
commite3042fc5ced749e693ccef81b3f5838c55d5480c (patch)
treee004dca26da0ec413d5c9ebf174962a008fde0bb
parentc33a9adb709ffb40f816e66eb0c98cc750d6cd43 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue1
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue9
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue67
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue35
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue75
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue24
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue25
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue17
-rw-r--r--app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue43
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js14
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue58
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql1
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql2
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql6
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue19
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss1
-rw-r--r--app/controllers/concerns/boards_actions.rb6
-rw-r--r--app/controllers/groups/group_members_controller.rb11
-rw-r--r--app/finders/concerns/packages/finder_helper.rb6
-rw-r--r--app/finders/packages/composer/packages_finder.rb2
-rw-r--r--app/finders/packages/conan/package_finder.rb2
-rw-r--r--app/finders/packages/generic/package_finder.rb1
-rw-r--r--app/finders/packages/go/package_finder.rb1
-rw-r--r--app/finders/packages/group_packages_finder.rb4
-rw-r--r--app/finders/packages/maven/package_finder.rb21
-rw-r--r--app/finders/packages/npm/package_finder.rb1
-rw-r--r--app/finders/packages/nuget/package_finder.rb2
-rw-r--r--app/finders/packages/package_finder.rb1
-rw-r--r--app/graphql/queries/epic/epic_children.query.graphql4
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb5
-rw-r--r--app/models/board_group_recent_visit.rb20
-rw-r--r--app/models/board_project_recent_visit.rb20
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/concerns/board_recent_visit.rb34
-rw-r--r--app/models/packages/package.rb2
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/services/boards/visits/create_service.rb16
-rw-r--r--app/services/packages/nuget/search_service.rb1
-rw-r--r--app/views/groups/group_members/index.html.haml13
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/bulk_import_worker.rb1
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb33
-rw-r--r--changelogs/unreleased/321625-epic_boards-redirect.yml5
-rw-r--r--changelogs/unreleased/325508-set-traversal_ids-for-every-namespace.yml5
-rw-r--r--changelogs/unreleased/326229-package-displayable.yml5
-rw-r--r--changelogs/unreleased/329778-fine-tune-a-few-queries-found-in-groupmembers-index.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-add-export-request-worker.yml5
-rw-r--r--changelogs/unreleased/jira-form-copy-updates.yml5
-rw-r--r--changelogs/unreleased/make-comment-actions-larger.yml5
-rw-r--r--changelogs/unreleased/review-app-button-styles.yml6
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20210511104929_add_epic_board_recent_visits_table.rb24
-rw-r--r--db/migrate/20210511104930_add_index_to_epic_board_recent_visits.rb20
-rw-r--r--db/post_migrate/20210506065000_schedule_backfill_traversal_ids.rb38
-rw-r--r--db/schema_migrations/202105060650001
-rw-r--r--db/schema_migrations/202105111049291
-rw-r--r--db/schema_migrations/202105111049301
-rw-r--r--db/structure.sql40
-rw-r--r--doc/api/members.md9
-rw-r--r--lib/banzai/cross_project_reference.rb7
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb141
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb8
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb11
-rw-r--r--lib/banzai/filter/references/issuable_reference_filter.rb6
-rw-r--r--lib/banzai/filter/references/label_reference_filter.rb4
-rw-r--r--lib/banzai/filter/references/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/references/reference_cache.rb178
-rw-r--r--lib/banzai/filter/references/reference_filter.rb24
-rw-r--r--lib/bulk_imports/clients/http.rb11
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb76
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb42
-rw-r--r--locale/gitlab.pot84
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb15
-rw-r--r--spec/finders/concerns/packages/finder_helper_spec.rb25
-rw-r--r--spec/finders/packages/composer/packages_finder_spec.rb25
-rw-r--r--spec/finders/packages/conan/package_finder_spec.rb3
-rw-r--r--spec/finders/packages/generic/package_finder_spec.rb7
-rw-r--r--spec/finders/packages/go/package_finder_spec.rb13
-rw-r--r--spec/finders/packages/maven/package_finder_spec.rb12
-rw-r--r--spec/finders/packages/npm/package_finder_spec.rb10
-rw-r--r--spec/finders/packages/nuget/package_finder_spec.rb10
-rw-r--r--spec/finders/packages/package_finder_spec.rb12
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js47
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js26
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js51
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js26
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js25
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js27
-rw-r--r--spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js31
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_header_spec.js101
-rw-r--r--spec/frontend/registry/explorer/mock_data.js11
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js1
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js40
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb10
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb15
-rw-r--r--spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb80
-rw-r--r--spec/lib/banzai/filter/references/issue_reference_filter_spec.rb18
-rw-r--r--spec/lib/banzai/filter/references/reference_cache_spec.rb143
-rw-r--r--spec/lib/bulk_imports/clients/http_spec.rb102
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb21
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb21
-rw-r--r--spec/models/board_group_recent_visit_spec.rb60
-rw-r--r--spec/models/board_project_recent_visit_spec.rb60
-rw-r--r--spec/models/bulk_imports/entity_spec.rb9
-rw-r--r--spec/models/packages/package_spec.rb34
-rw-r--r--spec/services/boards/visits/create_service_spec.rb43
-rw-r--r--spec/services/packages/nuget/search_service_spec.rb12
-rw-r--r--spec/support/shared_examples/finders/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb65
-rw-r--r--spec/support/shared_examples/services/boards/create_service_shared_examples.rb25
-rw-r--r--spec/workers/bulk_import_worker_spec.rb3
-rw-r--r--spec/workers/bulk_imports/export_request_worker_spec.rb30
117 files changed, 2041 insertions, 669 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5f653fbfc26..0bf6709a0ba 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-4f0cd9404f31511f5051e49b363adc06aa3ec365
+30ae36f781ee979330b1f170d81c97c319c2fff1
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 71cabe80529..e08d294b8c5 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -79,6 +79,7 @@ export default {
:toggle-class="toggleClass"
:boundary="getBoundaryElement()"
menu-class="dropdown-extended-height"
+ category="tertiary"
no-flip
right
lazy
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index b0f19e5b585..93d8bcc4c19 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -16,13 +16,13 @@ const commentDetailOptions = [
{
value: 'standard',
label: s__('Integrations|Standard'),
- help: s__('Integrations|Includes commit title and branch'),
+ help: s__('Integrations|Includes commit title and branch.'),
},
{
value: 'all_details',
label: s__('Integrations|All details'),
help: s__(
- 'Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs',
+ 'Integrations|Includes Standard, plus the entire commit message, commit hash, and issue IDs',
),
},
];
@@ -144,7 +144,7 @@ export default {
label-for="service[trigger]"
:description="
s__(
- 'Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) is created.',
+ 'Integrations|When you mention a Jira issue in a commit or merge request, GitLab creates a remote link and comment (if enabled).',
)
"
>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 16c76e048bd..0cc818c6d0e 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -278,7 +278,6 @@ export default {
v-if="canResolve"
ref="resolveButton"
v-gl-tooltip
- size="small"
category="tertiary"
:variant="resolveVariant"
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
@@ -292,7 +291,7 @@ export default {
<template v-if="canAwardEmoji">
<emoji-picker
v-if="glFeatures.improvedEmojiPicker"
- toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-2 gl-p-0! gl-shadow-none! gl-bg-transparent!"
+ toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
@click="setAwardEmoji"
>
<template #button-content>
@@ -305,10 +304,9 @@ export default {
v-else
v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
- class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
+ class="note-action-button note-emoji-button add-reaction-button btn-icon js-add-award js-note-emoji"
category="tertiary"
variant="default"
- size="small"
:title="$options.i18n.addReactionLabel"
:aria-label="$options.i18n.addReactionLabel"
data-position="right"
@@ -336,7 +334,6 @@ export default {
:title="$options.i18n.editCommentLabel"
:aria-label="$options.i18n.editCommentLabel"
icon="pencil"
- size="small"
category="tertiary"
class="note-action-button js-note-edit"
data-qa-selector="note_edit_button"
@@ -347,7 +344,6 @@ export default {
v-gl-tooltip
:title="$options.i18n.deleteCommentLabel"
:aria-label="$options.i18n.deleteCommentLabel"
- size="small"
icon="remove"
category="tertiary"
class="note-action-button js-note-delete"
@@ -360,7 +356,6 @@ export default {
:title="$options.i18n.moreActionsLabel"
:aria-label="$options.i18n.moreActionsLabel"
icon="ellipsis_v"
- size="small"
category="tertiary"
class="note-action-button more-actions-toggle"
data-toggle="dropdown"
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index 5ce03091504..0cd2afcf8a0 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -22,7 +22,6 @@ export default {
data-track-event="click_button"
data-track-label="reply_comment_button"
category="tertiary"
- size="small"
icon="comment"
:title="$options.i18n.buttonText"
:aria-label="$options.i18n.buttonText"
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
new file mode 100644
index 00000000000..22c1563350d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineVisualReference from '../ui/pipeline_visual_reference.vue';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|🚀 Run your first pipeline'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|A typical GitLab pipeline consists of three stages: build, test and deploy. Each stage can have one or more jobs.',
+ ),
+ secondParagraph: s__(
+ 'PipelineEditorTutorial|In the example below, %{codeStart}build%{codeEnd} and %{codeStart}deploy%{codeEnd} each contain one job, and %{codeStart}test%{codeEnd} contains two jobs. Your scripts run in jobs like these.',
+ ),
+ thirdParagraph: s__(
+ 'PipelineEditorTutorial|You can use %{linkStart}CI/CD examples and templates%{linkEnd} to get your first %{codeStart}.gitlab-ci.yml%{codeEnd} configuration file started. Your first pipeline runs when you commit the changes.',
+ ),
+ note: s__(
+ 'PipelineEditorTutorial|If you’re using a self-managed GitLab instance, %{linkStart}make sure your instance has runners available.%{linkEnd}',
+ ),
+ },
+ components: {
+ GlCard,
+ GlLink,
+ GlSprintf,
+ PipelineVisualReference,
+ },
+ inject: ['ciExamplesHelpPagePath', 'runnerHelpPagePath'],
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <p class="gl-mb-3">
+ <gl-sprintf :message="$options.i18n.secondParagraph">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <pipeline-visual-reference />
+ <p class="gl-my-3">
+ <gl-sprintf :message="$options.i18n.thirdParagraph">
+ <template #link="{ content }">
+ <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.note">
+ <template #link="{ content }">
+ <gl-link :href="runnerHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
new file mode 100644
index 00000000000..3da535f5f94
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlCard, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|Get started with GitLab CI/CD'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|GitLab CI/CD can automatically build, test, and deploy your application.',
+ ),
+ secondParagraph: s__(
+ 'PipelineEditorTutorial|The pipeline stages and jobs are defined in a %{codeStart}.gitlab-ci.yml%{codeEnd} file. You can edit, visualize and validate the syntax in this file by using the Pipeline Editor.',
+ ),
+ },
+ components: {
+ GlCard,
+ GlSprintf,
+ },
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <p class="gl-mb-0">
+ <gl-sprintf :message="$options.i18n.secondParagraph">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
new file mode 100644
index 00000000000..f714f6411f1
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlCard, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|⚙️ Pipeline configuration reference'),
+ firstParagraph: s__('PipelineEditorTutorial|Resources to help with your CI/CD configuration:'),
+ browseExamples: s__(
+ 'PipelineEditorTutorial|Browse %{linkStart}CI/CD examples and templates%{linkEnd}',
+ ),
+ viewSyntaxRef: s__(
+ 'PipelineEditorTutorial|View %{linkStart}.gitlab-ci.yml syntax reference%{linkEnd}',
+ ),
+ learnMore: s__(
+ 'PipelineEditorTutorial|Learn more about %{linkStart}GitLab CI/CD concepts%{linkEnd}',
+ ),
+ needs: s__(
+ 'PipelineEditorTutorial|Make your pipeline more efficient with the %{linkStart}Needs keyword%{linkEnd}',
+ ),
+ },
+ components: {
+ GlCard,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['ciExamplesHelpPagePath', 'ciHelpPagePath', 'needsHelpPagePath', 'ymlHelpPagePath'],
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-3">{{ $options.i18n.firstParagraph }}</p>
+ <ul>
+ <li>
+ <gl-sprintf :message="$options.i18n.browseExamples">
+ <template #link="{ content }">
+ <gl-link :href="ciExamplesHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.viewSyntaxRef">
+ <template #link="{ content }">
+ <gl-link :href="ymlHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.learnMore">
+ <template #link="{ content }">
+ <gl-link :href="ciHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.needs">
+ <template #link="{ content }">
+ <gl-link :href="needsHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
new file mode 100644
index 00000000000..512414f0246
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlCard } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('PipelineEditorTutorial|💡 Tip: Visualize and validate your pipeline'),
+ firstParagraph: s__(
+ 'PipelineEditorTutorial|Use the Visualize and Lint tabs in the Pipeline Editor to visualize your pipeline and check for any errors or warnings before committing your changes.',
+ ),
+ },
+ components: {
+ GlCard,
+ },
+};
+</script>
+<template>
+ <gl-card>
+ <template #default>
+ <h4 class="gl-font-lg gl-mt-0">{{ $options.i18n.title }}</h4>
+ <p class="gl-mb-0">{{ $options.i18n.firstParagraph }}</p>
+ </template>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index ef5be8abf9a..e1f38b4332b 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
@@ -1,6 +1,10 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
+import FirstPipelineCard from './cards/first_pipeline_card.vue';
+import GettingStartedCard from './cards/getting_started_card.vue';
+import PipelineConfigReferenceCard from './cards/pipeline_config_reference_card.vue';
+import VisualizeAndLintCard from './cards/visualize_and_lint_card.vue';
export default {
width: {
@@ -11,6 +15,10 @@ export default {
toggleTxt: __('Collapse'),
},
components: {
+ FirstPipelineCard,
+ GettingStartedCard,
+ PipelineConfigReferenceCard,
+ VisualizeAndLintCard,
GlButton,
GlIcon,
},
@@ -55,7 +63,7 @@ export default {
<template>
<aside
aria-live="polite"
- class="gl-fixed gl-right-0 gl-h-full gl-bg-gray-10 gl-transition-medium gl-border-l-solid gl-border-1 gl-border-gray-100"
+ class="gl-fixed gl-right-0 gl-bg-gray-10 gl-shadow-drawer gl-transition-medium gl-border-l-solid gl-border-1 gl-border-gray-100 gl-h-full gl-z-index-9999 gl-overflow-y-auto"
:style="rootStyle"
>
<gl-button
@@ -63,14 +71,19 @@ export default {
class="gl-w-full gl-h-9 gl-rounded-0! gl-border-none! gl-border-b-solid! gl-border-1! gl-border-gray-100 gl-text-decoration-none! gl-outline-0! gl-display-flex"
:class="buttonClass"
:title="__('Toggle sidebar')"
- data-testid="toggleBtn"
@click="toggleDrawer"
>
- <span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">{{
- __('Collapse')
- }}</span>
+ <span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">
+ {{ __('Collapse') }}
+ </span>
<gl-icon data-testid="toggle-icon" :name="buttonIconName" />
</gl-button>
- <div v-if="isExpanded" class="gl-p-5" data-testid="drawer-content"></div>
+ <div v-if="isExpanded" class="gl-h-full gl-p-5" data-testid="drawer-content">
+ <getting-started-card class="gl-mb-4" />
+ <first-pipeline-card class="gl-mb-4" />
+ <visualize-and-lint-card class="gl-mb-4" />
+ <pipeline-config-reference-card />
+ <div class="gl-h-13"></div>
+ </div>
</aside>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue b/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
new file mode 100644
index 00000000000..049504181c4
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ props: {
+ jobName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-w-13 gl-h-6 gl-font-sm gl-bg-white gl-inset-border-1-blue-500 gl-text-center gl-text-truncate gl-rounded-pill gl-px-4 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
+ >
+ {{ jobName }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue b/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue
new file mode 100644
index 00000000000..1017237365b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue
@@ -0,0 +1,43 @@
+<script>
+import { s__ } from '~/locale';
+import DemoJobPill from './demo_job_pill.vue';
+
+export default {
+ i18n: {
+ stageNames: {
+ build: s__('StageName|Build'),
+ test: s__('StageName|Test'),
+ deploy: s__('StageName|Deploy'),
+ },
+ jobNames: {
+ build: s__('JobName|build-job'),
+ test_1: s__('JobName|unit-test'),
+ test_2: s__('JobName|lint-test'),
+ deploy: s__('JobName|deploy-app'),
+ },
+ },
+ stageClasses:
+ 'gl-bg-blue-50 gl-display-flex gl-flex-direction-column gl-align-items-center gl-p-4 gl-rounded-base',
+ titleClasses: 'gl-text-blue-600 gl-mb-4',
+ components: {
+ DemoJobPill,
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-justify-content-center">
+ <div :class="$options.stageClasses" class="gl-mr-5">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.build }}</div>
+ <demo-job-pill :job-name="$options.i18n.jobNames.build" />
+ </div>
+ <div :class="$options.stageClasses" class="gl-mr-5">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.test }}</div>
+ <demo-job-pill class="gl-mb-3" :job-name="$options.i18n.jobNames.test_1" />
+ <demo-job-pill :job-name="$options.i18n.jobNames.test_2" />
+ </div>
+ <div :class="$options.stageClasses">
+ <div :class="$options.titleClasses">{{ $options.i18n.stageNames.deploy }}</div>
+ <demo-job-pill :job-name="$options.i18n.jobNames.deploy" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 9c5681cf74b..361e2b64e0b 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -30,13 +30,19 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
pipelineEtag,
// Add to provide/inject API for static values
ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
defaultBranch,
emptyStateIllustrationPath,
+ helpPaths,
lintHelpPagePath,
+ needsHelpPagePath,
newMergeRequestPath,
+ pipelinePagePath,
projectFullPath,
projectPath,
projectNamespace,
+ runnerHelpPagePath,
ymlHelpPagePath,
} = el?.dataset;
@@ -80,15 +86,21 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
apolloProvider,
provide: {
ciConfigPath,
+ ciExamplesHelpPagePath,
+ ciHelpPagePath,
+ configurationPaths,
defaultBranch,
emptyStateIllustrationPath,
+ helpPaths,
lintHelpPagePath,
+ needsHelpPagePath,
newMergeRequestPath,
+ pipelinePagePath,
projectFullPath,
projectPath,
projectNamespace,
+ runnerHelpPagePath,
ymlHelpPagePath,
- configurationPaths,
},
render(h) {
return h(PipelineEditorApp);
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index 0422bfb13c5..80ed9a32039 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, n__ } from '~/locale';
+import { sprintf, n__, s__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -23,6 +23,8 @@ import {
ROOT_IMAGE_TOOLTIP,
} from '../../constants/index';
+import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_container_repository_tags_count.query.graphql';
+
export default {
name: 'DetailsHeader',
components: { GlButton, GlIcon, TitleArea, MetadataItem },
@@ -35,60 +37,77 @@ export default {
type: Object,
required: true,
},
- metadataLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
disabled: {
type: Boolean,
default: false,
required: false,
},
},
+ data() {
+ return {
+ containerRepository: {},
+ fetchTagsCount: false,
+ };
+ },
+ apollo: {
+ containerRepository: {
+ query: getContainerRepositoryTagsCountQuery,
+ variables() {
+ return {
+ id: this.image.id,
+ };
+ },
+ },
+ },
computed: {
+ imageDetails() {
+ return { ...this.image, ...this.containerRepository };
+ },
visibilityIcon() {
- return this.image?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
+ return this.imageDetails?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
},
timeAgo() {
- return this.timeFormatted(this.image.updatedAt);
+ return this.timeFormatted(this.imageDetails.updatedAt);
},
updatedText() {
return sprintf(UPDATED_AT, { time: this.timeAgo });
},
tagCountText() {
- return n__('%d tag', '%d tags', this.image.tagsCount);
+ if (this.$apollo.queries.containerRepository.loading) {
+ return s__('ContainerRegistry|-- tags');
+ }
+ return n__('%d tag', '%d tags', this.imageDetails.tagsCount);
},
cleanupTextAndTooltip() {
- if (!this.image.project.containerExpirationPolicy?.enabled) {
+ if (!this.imageDetails.project.containerExpirationPolicy?.enabled) {
return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP };
}
return {
[UNSCHEDULED_STATUS]: {
text: sprintf(CLEANUP_UNSCHEDULED_TEXT, {
- time: this.timeFormatted(this.image.project.containerExpirationPolicy.nextRunAt),
+ time: this.timeFormatted(this.imageDetails.project.containerExpirationPolicy.nextRunAt),
}),
},
[SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP },
[ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP },
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
- }[this.image?.expirationPolicyCleanupStatus];
+ }[this.imageDetails?.expirationPolicyCleanupStatus];
},
deleteButtonDisabled() {
- return this.disabled || !this.image.canDelete;
+ return this.disabled || !this.imageDetails.canDelete;
},
rootImageTooltip() {
- return !this.image.name ? ROOT_IMAGE_TOOLTIP : '';
+ return !this.imageDetails.name ? ROOT_IMAGE_TOOLTIP : '';
},
imageName() {
- return this.image.name || ROOT_IMAGE_TEXT;
+ return this.imageDetails.name || ROOT_IMAGE_TEXT;
},
},
};
</script>
<template>
- <title-area :metadata-loading="metadataLoading">
+ <title-area>
<template #title>
<span data-testid="title">
{{ imageName }}
@@ -124,12 +143,7 @@ export default {
/>
</template>
<template #right-actions>
- <gl-button
- v-if="!metadataLoading"
- variant="danger"
- :disabled="deleteButtonDisabled"
- @click="$emit('delete')"
- >
+ <gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
{{ __('Delete image repository') }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
index 0f50531c3c5..88c2e667afd 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -8,7 +8,6 @@ query getContainerRepositoryDetails($id: ID!) {
canDelete
createdAt
updatedAt
- tagsCount
expirationPolicyStartedAt
expirationPolicyCleanupStatus
project {
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index c87c0f847b3..a703c2dd0ac 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
-query getContainerRepositoryDetails(
+query getContainerRepositoryTags(
$id: ID!
$first: Int
$last: Int
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
new file mode 100644
index 00000000000..9092a71edb0
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql
@@ -0,0 +1,6 @@
+query getContainerRepositoryTagsCount($id: ID!) {
+ containerRepository(id: $id) {
+ id
+ tagsCount
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 50feea79747..34ec3b085a5 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -48,14 +48,11 @@ export default {
mixins: [Tracking.mixin()],
inject: ['breadCrumbState', 'config'],
apollo: {
- image: {
+ containerRepository: {
query: getContainerRepositoryDetailsQuery,
variables() {
return this.queryVariables;
},
- update(data) {
- return data.containerRepository;
- },
result() {
this.updateBreadcrumb();
},
@@ -66,7 +63,7 @@ export default {
},
data() {
return {
- image: {},
+ containerRepository: {},
itemsToBeDeleted: [],
isMobile: false,
mutationLoading: false,
@@ -82,12 +79,12 @@ export default {
};
},
isLoading() {
- return this.$apollo.queries.image.loading || this.mutationLoading;
+ return this.$apollo.queries.containerRepository.loading || this.mutationLoading;
},
showPartialCleanupWarning() {
return (
this.config.showUnfinishedTagCleanupCallout &&
- this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
+ this.containerRepository?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
!this.hidePartialCleanupWarning
);
},
@@ -98,13 +95,13 @@ export default {
};
},
pageActionsAreDisabled() {
- return Boolean(this.image?.status);
+ return Boolean(this.containerRepository?.status);
},
},
methods: {
updateBreadcrumb() {
- const name = this.image?.id
- ? this.image?.name || ROOT_IMAGE_TEXT
+ const name = this.containerRepository?.id
+ ? this.containerRepository?.name || ROOT_IMAGE_TEXT
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
this.breadCrumbState.updateName(name);
},
@@ -164,7 +161,7 @@ export default {
},
deleteImage() {
this.deleteImageAlert = true;
- this.itemsToBeDeleted = [{ path: this.image.path }];
+ this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
this.$refs.deleteModal.show();
},
deleteImageError() {
@@ -180,7 +177,7 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
- <template v-if="image">
+ <template v-if="containerRepository">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
@@ -195,11 +192,11 @@ export default {
@dismiss="dismissPartialCleanupWarning"
/>
- <status-alert v-if="image.status" :status="image.status" />
+ <status-alert v-if="containerRepository.status" :status="containerRepository.status" />
<details-header
- :image="image"
- :metadata-loading="isLoading"
+ v-if="!isLoading"
+ :image="containerRepository"
:disabled="pageActionsAreDisabled"
@delete="deleteImage"
/>
@@ -215,7 +212,7 @@ export default {
/>
<delete-image
- :id="image.id"
+ :id="containerRepository.id"
ref="deleteImage"
use-update-fn
@start="deleteImageIniit"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index abc831c8abe..a5d165ebd49 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -1,5 +1,12 @@
<script>
-import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlLink,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
@@ -9,6 +16,7 @@ export default {
GlButtonGroup,
GlDropdown,
GlDropdownItem,
+ GlIcon,
GlLink,
GlSearchBoxByType,
ReviewAppLink,
@@ -71,7 +79,14 @@ export default {
size="small"
css-class="deploy-link js-deploy-url inline"
/>
- <gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
+ <gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
+ <template #button-content>
+ <gl-icon
+ class="dropdown-chevron gl-mx-0!"
+ name="chevron-down"
+ data-testid="mr-wigdet-deployment-dropdown-icon"
+ />
+ </template>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="change in filteredChanges"
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 1504f3ee50f..9b38e842635 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -50,6 +50,12 @@
img.avatar {
margin-right: $gl-padding;
+
+ @include media-breakpoint-down(sm) {
+ width: $gl-spacing-scale-6;
+ height: $gl-spacing-scale-6;
+ margin-right: $gl-padding-8;
+ }
}
.controls {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 801dd44be8e..c2ed709a475 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -676,6 +676,7 @@ $system-note-svg-size: 16px;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
margin-left: 0;
+ transform: translateY(-4px);
}
}
diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb
index 8c2b0300589..d4bff730b06 100644
--- a/app/controllers/concerns/boards_actions.rb
+++ b/app/controllers/concerns/boards_actions.rb
@@ -19,7 +19,7 @@ module BoardsActions
def show
# Add / update the board in the recent visits table
- Boards::Visits::CreateService.new(parent, current_user).execute(board) if request.format.html?
+ board_visit_service.new(parent, current_user).execute(board) if request.format.html?
respond_with_board
end
@@ -52,6 +52,10 @@ module BoardsActions
board_klass.to_type
end
+ def board_visit_service
+ Boards::Visits::CreateService
+ end
+
def serializer
BoardSerializer.new(current_user: current_user)
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 64b7cbfacc3..43f1a1a847d 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -4,6 +4,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
include MembershipActions
include MembersPresentation
include SortingHelper
+ include Gitlab::Utils::StrongMemoize
MEMBER_PER_PAGE_LIMIT = 50
@@ -21,6 +22,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
feature_category :authentication_and_authorization
+ helper_method :can_manage_members?
+
def index
preload_max_access
@sort = params[:sort].presence || sort_value_name
@@ -29,7 +32,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
.new(@group, current_user, params: filter_params)
.execute(include_relations: requested_relations)
- if can_manage_members
+ if can_manage_members?
@skip_groups = @group.related_group_ids
@invited_members = @members.invite
@@ -59,8 +62,10 @@ class Groups::GroupMembersController < Groups::ApplicationController
current_user.max_access_for_group[@group.id] = @group.max_member_access(current_user)
end
- def can_manage_members
- can?(current_user, :admin_group_member, @group)
+ def can_manage_members?
+ strong_memoize(:can_manage_members) do
+ can?(current_user, :admin_group_member, @group)
+ end
end
def present_invited_members(invited_members)
diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index 6ba353fd89a..061e289d8d8 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -9,12 +9,16 @@ module Packages
private
+ def packages_for_project(project)
+ project.packages.installable
+ end
+
def packages_visible_to_user(user, within_group:)
return ::Packages::Package.none unless within_group
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
projects = projects_visible_to_reporters(user, within_group: within_group)
- ::Packages::Package.for_projects(projects.select(:id))
+ ::Packages::Package.for_projects(projects.select(:id)).installable
end
def projects_visible_to_user(user, within_group:)
diff --git a/app/finders/packages/composer/packages_finder.rb b/app/finders/packages/composer/packages_finder.rb
index e63b2ee03fa..b5a1b19216f 100644
--- a/app/finders/packages/composer/packages_finder.rb
+++ b/app/finders/packages/composer/packages_finder.rb
@@ -9,7 +9,7 @@ module Packages
end
def execute
- packages_for_group_projects.composer.preload_composer
+ packages_for_group_projects(installable_only: true).composer.preload_composer
end
end
end
diff --git a/app/finders/packages/conan/package_finder.rb b/app/finders/packages/conan/package_finder.rb
index 26e9182f4e1..8ebdd358ba6 100644
--- a/app/finders/packages/conan/package_finder.rb
+++ b/app/finders/packages/conan/package_finder.rb
@@ -11,7 +11,7 @@ module Packages
end
def execute
- packages_for_current_user.with_name_like(query).order_name_asc if query
+ packages_for_current_user.installable.with_name_like(query).order_name_asc if query
end
private
diff --git a/app/finders/packages/generic/package_finder.rb b/app/finders/packages/generic/package_finder.rb
index 3a260e11fa3..8ec88754901 100644
--- a/app/finders/packages/generic/package_finder.rb
+++ b/app/finders/packages/generic/package_finder.rb
@@ -11,6 +11,7 @@ module Packages
project
.packages
.generic
+ .installable
.by_name_and_version!(package_name, package_version)
end
diff --git a/app/finders/packages/go/package_finder.rb b/app/finders/packages/go/package_finder.rb
index 4573417d11f..553e731895d 100644
--- a/app/finders/packages/go/package_finder.rb
+++ b/app/finders/packages/go/package_finder.rb
@@ -21,6 +21,7 @@ module Packages
@project
.packages
.golang
+ .installable
.with_name(@module_name)
.with_version(@module_version)
end
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index 8771bf90e75..ab12a580e30 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -20,7 +20,7 @@ module Packages
attr_reader :current_user, :group, :params
- def packages_for_group_projects
+ def packages_for_group_projects(installable_only: false)
packages = ::Packages::Package
.including_build_info
.including_project_route
@@ -32,7 +32,7 @@ module Packages
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
packages = filter_by_package_name(packages)
- filter_by_status(packages)
+ installable_only ? packages.installable : filter_by_status(packages)
end
def group_projects_visible_to_current_user
diff --git a/app/finders/packages/maven/package_finder.rb b/app/finders/packages/maven/package_finder.rb
index 3bc2f66c098..fd5444684c6 100644
--- a/app/finders/packages/maven/package_finder.rb
+++ b/app/finders/packages/maven/package_finder.rb
@@ -26,9 +26,9 @@ module Packages
def base
if @project
- packages_for_a_single_project
+ packages_for_project(@project)
elsif @group
- packages_for_multiple_projects
+ packages_visible_to_user(@current_user, within_group: @group)
else
::Packages::Package.none
end
@@ -40,23 +40,6 @@ module Packages
matching_packages
end
-
- # Produces a query that retrieves packages from a single project.
- def packages_for_a_single_project
- @project.packages
- end
-
- # Produces a query that retrieves packages from multiple projects that
- # the current user can view within a group.
- def packages_for_multiple_projects
- packages_visible_to_user(@current_user, within_group: @group)
- end
-
- # Returns the projects that the current user can view within a group.
- def projects_visible_to_current_user
- @group.all_projects
- .public_or_visible_to_user(@current_user)
- end
end
end
end
diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb
index 3b79785d0e1..92ceac297ee 100644
--- a/app/finders/packages/npm/package_finder.rb
+++ b/app/finders/packages/npm/package_finder.rb
@@ -14,6 +14,7 @@ module Packages
def execute
base.npm
.with_name(@package_name)
+ .installable
.last_of_each_version
.preload_files
end
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index 2f66bd145ee..d91ef853a1a 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -23,7 +23,7 @@ module Packages
def base
if project?
- @project_or_group.packages
+ packages_for_project(@project_or_group)
elsif group?
packages_visible_to_user(@current_user, within_group: @project_or_group)
else
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index f1874b77845..368d2028cb5 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -12,6 +12,7 @@ module Packages
.including_build_info
.including_project_route
.including_tags
+ .displayable
.processed
.find(@package_id)
end
diff --git a/app/graphql/queries/epic/epic_children.query.graphql b/app/graphql/queries/epic/epic_children.query.graphql
index c12778109d0..5ee27052f95 100644
--- a/app/graphql/queries/epic/epic_children.query.graphql
+++ b/app/graphql/queries/epic/epic_children.query.graphql
@@ -16,6 +16,10 @@ fragment RelatedTreeBaseEpic on Epic {
adminEpic
createEpic
}
+ descendantWeightSum {
+ closedIssues
+ openedIssues
+ }
descendantCounts {
__typename
openedEpics
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index 193cf49d27e..c31f7843e40 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -12,16 +12,21 @@ module Ci
commit_sha = project.commit ? project.commit.sha : ''
{
"ci-config-path": project.ci_config_path_or_default,
+ "ci-examples-help-page-path" => help_page_path('ci/examples/README'),
+ "ci-help-page-path" => help_page_path('ci/README'),
"commit-sha" => commit_sha,
"default-branch" => project.default_branch,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name": params[:branch_name],
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
"pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
+ "pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
+ "runner-help-page-path" => help_page_path('ci/runners/README'),
"yml-help-page-path" => help_page_path('ci/yaml/README')
}
end
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index 979f0e1ab92..dc273e256a8 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific group a user has visited
class BoardGroupRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :group
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :group, presence: true
validates :board, presence: true
- scope :by_user_group, -> (user, group) { where(user: user, group: group) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, group: board.group, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, group, count: nil)
- visits = by_user_group(user, group).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
- visits.first(count)
+ def self.board_parent_relation
+ :group
end
end
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 509c8f97b83..723afd6feab 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -2,27 +2,19 @@
# Tracks which boards in a specific project a user has visited
class BoardProjectRecentVisit < ApplicationRecord
+ include BoardRecentVisit
+
belongs_to :user
belongs_to :project
belongs_to :board
- validates :user, presence: true
+ validates :user, presence: true
validates :project, presence: true
validates :board, presence: true
- scope :by_user_project, -> (user, project) { where(user: user, project: project) }
-
- def self.visited!(user, board)
- visit = find_or_create_by(user: user, project: board.project, board: board)
- visit.touch if visit.updated_at < Time.current
- rescue ActiveRecord::RecordNotUnique
- retry
- end
-
- def self.latest(user, project, count: nil)
- visits = by_user_project(user, project).order(updated_at: :desc)
- visits = visits.preload(:board) if count && count > 1
+ scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
- visits.first(count)
+ def self.board_parent_relation
+ :project
end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 04af1145769..bb543b39a79 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -68,6 +68,10 @@ class BulkImports::Entity < ApplicationRecord
end
end
+ def encoded_source_full_path
+ ERB::Util.url_encode(source_full_path)
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb
new file mode 100644
index 00000000000..fd4d574ac58
--- /dev/null
+++ b/app/models/concerns/board_recent_visit.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module BoardRecentVisit
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def visited!(user, board)
+ find_or_create_by(
+ "user" => user,
+ board_parent_relation => board.resource_parent,
+ board_relation => board
+ ).tap do |visit|
+ visit.touch
+ end
+ rescue ActiveRecord::RecordNotUnique
+ retry
+ end
+
+ def latest(user, parent, count: nil)
+ visits = by_user_parent(user, parent).order(updated_at: :desc)
+ visits = visits.preload(board_relation)
+
+ visits.first(count)
+ end
+
+ def board_relation
+ :board
+ end
+
+ def board_parent_relation
+ raise NotImplementedError
+ end
+ end
+end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index ed620c5d6c3..bfa27132299 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -6,6 +6,7 @@ class Packages::Package < ApplicationRecord
include Gitlab::Utils::StrongMemoize
DISPLAYABLE_STATUSES = [:default, :error].freeze
+ INSTALLABLE_STATUSES = [:default].freeze
belongs_to :project
belongs_to :creator, class_name: 'User'
@@ -86,6 +87,7 @@ class Packages::Package < ApplicationRecord
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :with_status, ->(status) { where(status: status) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
+ scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5c4b1564914..f1db78501d5 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -106,9 +106,8 @@ class JiraService < IssueTrackerService
end
def help
- "You need to configure Jira before enabling this service. For more details
- read the
- [Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
+ jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
+ s_("JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}.") % { jira_doc_link_start: jira_doc_link_start, link_end: '</a>'.html_safe }
end
def title
diff --git a/app/services/boards/visits/create_service.rb b/app/services/boards/visits/create_service.rb
index 428ed1a8bcc..4d659596803 100644
--- a/app/services/boards/visits/create_service.rb
+++ b/app/services/boards/visits/create_service.rb
@@ -5,13 +5,17 @@ module Boards
class CreateService < Boards::BaseService
def execute(board)
return unless current_user && Gitlab::Database.read_write?
- return unless board.is_a?(Board) # other board types do not support board visits yet
+ return unless board
- if parent.is_a?(Group)
- BoardGroupRecentVisit.visited!(current_user, board)
- else
- BoardProjectRecentVisit.visited!(current_user, board)
- end
+ model.visited!(current_user, board)
+ end
+
+ private
+
+ def model
+ return BoardGroupRecentVisit if parent.is_a?(Group)
+
+ BoardProjectRecentVisit
end
end
end
diff --git a/app/services/packages/nuget/search_service.rb b/app/services/packages/nuget/search_service.rb
index 1eead1e62b3..fea424b3aa8 100644
--- a/app/services/packages/nuget/search_service.rb
+++ b/app/services/packages/nuget/search_service.rb
@@ -103,6 +103,7 @@ module Packages
def nuget_packages
Packages::Package.nuget
+ .displayable
.has_version
.without_nuget_temporary_name
end
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index cea4c533ae3..5da3a94c44b 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,15 +1,14 @@
- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
-- can_manage_members = can?(current_user, :admin_group_member, @group)
-- show_invited_members = can_manage_members && @invited_members.exists?
-- show_access_requests = can_manage_members && @requesters.exists?
+- show_invited_members = can_manage_members? && @invited_members.load.any?
+- show_access_requests = can_manage_members? && @requesters.load.any?
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
.gl-display-flex.gl-flex-wrap
- - if can_manage_members
+ - if can_manage_members?
.gl-w-half.gl-xs-w-full
%h4
= _('Group members')
@@ -21,7 +20,7 @@
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- - if can_manage_members && Feature.disabled?(:invite_members_group_modal, @group)
+ - if can_manage_members? && Feature.disabled?(:invite_members_group_modal, @group)
%hr.gl-mt-4
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
@@ -42,7 +41,7 @@
%span
= _('Members')
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @members.total_count
- - if @group.shared_with_group_links.any?
+ - if @group.shared_with_group_links.present?
%li.nav-item
= link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
%span
@@ -65,7 +64,7 @@
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) }
.loading
.gl-spinner.gl-spinner-md
- - if @group.shared_with_group_links.any?
+ - if @group.shared_with_group_links.present?
#tab-groups.tab-pane
.js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
.loading
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 40ff568c80b..7d435ac03f8 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1836,6 +1836,15 @@
:idempotent:
:tags:
- :exclude_from_kubernetes
+- :name: bulk_imports_export_request
+ :worker_name: BulkImports::ExportRequestWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: bulk_imports_pipeline
:worker_name: BulkImports::PipelineWorker
:feature_category: :importers
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index f6b1aef5069..8ad31c68374 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -24,6 +24,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
created_entities.first(next_batch_size).each do |entity|
create_pipeline_tracker_for(entity)
+ BulkImports::ExportRequestWorker.perform_async(entity.id)
BulkImports::EntityWorker.perform_async(entity.id)
entity.start!
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
new file mode 100644
index 00000000000..cccc24d3bdc
--- /dev/null
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class ExportRequestWorker
+ include ApplicationWorker
+
+ idempotent!
+ worker_has_external_dependencies!
+ feature_category :importers
+
+ GROUP_EXPORTED_URL_PATH = "/groups/%s/export_relations"
+
+ def perform(entity_id)
+ entity = BulkImports::Entity.find(entity_id)
+
+ request_export(entity)
+ end
+
+ private
+
+ def request_export(entity)
+ http_client(entity.bulk_import.configuration)
+ .post(GROUP_EXPORTED_URL_PATH % entity.encoded_source_full_path)
+ end
+
+ def http_client(configuration)
+ @client ||= Clients::Http.new(
+ uri: configuration.url,
+ token: configuration.access_token
+ )
+ end
+ end
+end
diff --git a/changelogs/unreleased/321625-epic_boards-redirect.yml b/changelogs/unreleased/321625-epic_boards-redirect.yml
new file mode 100644
index 00000000000..82d1f336bb8
--- /dev/null
+++ b/changelogs/unreleased/321625-epic_boards-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect to the last visited epic board
+merge_request: 61474
+author:
+type: added
diff --git a/changelogs/unreleased/325508-set-traversal_ids-for-every-namespace.yml b/changelogs/unreleased/325508-set-traversal_ids-for-every-namespace.yml
new file mode 100644
index 00000000000..3867845f4e0
--- /dev/null
+++ b/changelogs/unreleased/325508-set-traversal_ids-for-every-namespace.yml
@@ -0,0 +1,5 @@
+---
+title: Set traversal_ids for every namespace
+merge_request: 57318
+author:
+type: performance
diff --git a/changelogs/unreleased/326229-package-displayable.yml b/changelogs/unreleased/326229-package-displayable.yml
new file mode 100644
index 00000000000..705abe854bd
--- /dev/null
+++ b/changelogs/unreleased/326229-package-displayable.yml
@@ -0,0 +1,5 @@
+---
+title: Include installable and/or displayable packages only in package finders
+merge_request: 59921
+author:
+type: changed
diff --git a/changelogs/unreleased/329778-fine-tune-a-few-queries-found-in-groupmembers-index.yml b/changelogs/unreleased/329778-fine-tune-a-few-queries-found-in-groupmembers-index.yml
new file mode 100644
index 00000000000..790ce9f0989
--- /dev/null
+++ b/changelogs/unreleased/329778-fine-tune-a-few-queries-found-in-groupmembers-index.yml
@@ -0,0 +1,5 @@
+---
+title: Fine tune a few queries found in GroupMembers#index
+merge_request: 60857
+author:
+type: performance
diff --git a/changelogs/unreleased/georgekoltsov-add-export-request-worker.yml b/changelogs/unreleased/georgekoltsov-add-export-request-worker.yml
new file mode 100644
index 00000000000..d97eee4f59a
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-add-export-request-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Add relations export request when Bulk Import is initiated
+merge_request: 61365
+author:
+type: changed
diff --git a/changelogs/unreleased/jira-form-copy-updates.yml b/changelogs/unreleased/jira-form-copy-updates.yml
new file mode 100644
index 00000000000..05129d99f07
--- /dev/null
+++ b/changelogs/unreleased/jira-form-copy-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Improve field descriptions in the Jira integration form
+merge_request: 61205
+author:
+type: changed
diff --git a/changelogs/unreleased/make-comment-actions-larger.yml b/changelogs/unreleased/make-comment-actions-larger.yml
new file mode 100644
index 00000000000..1d7342b0742
--- /dev/null
+++ b/changelogs/unreleased/make-comment-actions-larger.yml
@@ -0,0 +1,5 @@
+---
+title: Increase note actions target size
+merge_request: 59776
+author:
+type: changed
diff --git a/changelogs/unreleased/review-app-button-styles.yml b/changelogs/unreleased/review-app-button-styles.yml
new file mode 100644
index 00000000000..fbbcbd974db
--- /dev/null
+++ b/changelogs/unreleased/review-app-button-styles.yml
@@ -0,0 +1,6 @@
+---
+title: Remove extra padding and margin from merge request widget review app dropdown
+ chevron
+merge_request: 61302
+author:
+type: fixed
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index a3f3af2c83a..36fc6672531 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -56,6 +56,8 @@
- 1
- - bulk_imports_entity
- 1
+- - bulk_imports_export_request
+ - 1
- - bulk_imports_pipeline
- 1
- - bulk_imports_relation_export
diff --git a/db/migrate/20210511104929_add_epic_board_recent_visits_table.rb b/db/migrate/20210511104929_add_epic_board_recent_visits_table.rb
new file mode 100644
index 00000000000..9822276f9c4
--- /dev/null
+++ b/db/migrate/20210511104929_add_epic_board_recent_visits_table.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddEpicBoardRecentVisitsTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ with_lock_retries do
+ unless table_exists?(:boards_epic_board_recent_visits)
+ create_table :boards_epic_board_recent_visits do |t|
+ t.references :user, index: true, null: false, foreign_key: { on_delete: :cascade }
+ t.references :epic_board, index: true, foreign_key: { to_table: :boards_epic_boards, on_delete: :cascade }, null: false
+ t.references :group, index: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
+ t.timestamps_with_timezone null: false
+ end
+ end
+ end
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :boards_epic_board_recent_visits
+ end
+ end
+end
diff --git a/db/migrate/20210511104930_add_index_to_epic_board_recent_visits.rb b/db/migrate/20210511104930_add_index_to_epic_board_recent_visits.rb
new file mode 100644
index 00000000000..1341886c50c
--- /dev/null
+++ b/db/migrate/20210511104930_add_index_to_epic_board_recent_visits.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddIndexToEpicBoardRecentVisits < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_epic_board_recent_visits_on_user_group_and_board'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :boards_epic_board_recent_visits,
+ [:user_id, :group_id, :epic_board_id],
+ name: INDEX_NAME,
+ unique: true
+ end
+
+ def down
+ remove_concurrent_index_by_name :boards_epic_board_recent_visits, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20210506065000_schedule_backfill_traversal_ids.rb b/db/post_migrate/20210506065000_schedule_backfill_traversal_ids.rb
new file mode 100644
index 00000000000..5ae80c1da80
--- /dev/null
+++ b/db/post_migrate/20210506065000_schedule_backfill_traversal_ids.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ScheduleBackfillTraversalIds < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ ROOTS_MIGRATION = 'BackfillNamespaceTraversalIdsRoots'
+ CHILDREN_MIGRATION = 'BackfillNamespaceTraversalIdsChildren'
+ DOWNTIME = false
+ BATCH_SIZE = 1_000
+ SUB_BATCH_SIZE = 100
+ DELAY_INTERVAL = 2.minutes
+
+ disable_ddl_transaction!
+
+ def up
+ # Personal namespaces and top-level groups
+ final_delay = queue_background_migration_jobs_by_range_at_intervals(
+ ::Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsRoots::Namespace.base_query,
+ ROOTS_MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ other_job_arguments: [SUB_BATCH_SIZE],
+ track_jobs: true
+ )
+ final_delay += DELAY_INTERVAL
+
+ # Subgroups
+ queue_background_migration_jobs_by_range_at_intervals(
+ ::Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsChildren::Namespace.base_query,
+ CHILDREN_MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ initial_delay: final_delay,
+ other_job_arguments: [SUB_BATCH_SIZE],
+ track_jobs: true
+ )
+ end
+end
diff --git a/db/schema_migrations/20210506065000 b/db/schema_migrations/20210506065000
new file mode 100644
index 00000000000..5ffe1800cd9
--- /dev/null
+++ b/db/schema_migrations/20210506065000
@@ -0,0 +1 @@
+d286628cce50c469afe899d5ac40f20df8dceb6ee10c6cf49c64fbaeea7e4a2e \ No newline at end of file
diff --git a/db/schema_migrations/20210511104929 b/db/schema_migrations/20210511104929
new file mode 100644
index 00000000000..af4f0ae0c01
--- /dev/null
+++ b/db/schema_migrations/20210511104929
@@ -0,0 +1 @@
+7c2a036033a3f6a3f80755c8ce4a0deab5933084974af4d87e7b97cc446fcbda \ No newline at end of file
diff --git a/db/schema_migrations/20210511104930 b/db/schema_migrations/20210511104930
new file mode 100644
index 00000000000..9c07569e616
--- /dev/null
+++ b/db/schema_migrations/20210511104930
@@ -0,0 +1 @@
+51a8eeb8919e3f59579885b9e316ba8116566ae9b363b5dd750a65f42503c391 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8f05390ae4a..8b7d6737916 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10057,6 +10057,24 @@ CREATE SEQUENCE boards_epic_board_positions_id_seq
ALTER SEQUENCE boards_epic_board_positions_id_seq OWNED BY boards_epic_board_positions.id;
+CREATE TABLE boards_epic_board_recent_visits (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ epic_board_id bigint NOT NULL,
+ group_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE boards_epic_board_recent_visits_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE boards_epic_board_recent_visits_id_seq OWNED BY boards_epic_board_recent_visits.id;
+
CREATE TABLE boards_epic_boards (
id bigint NOT NULL,
hide_backlog_list boolean DEFAULT false NOT NULL,
@@ -19353,6 +19371,8 @@ ALTER TABLE ONLY boards_epic_board_labels ALTER COLUMN id SET DEFAULT nextval('b
ALTER TABLE ONLY boards_epic_board_positions ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_positions_id_seq'::regclass);
+ALTER TABLE ONLY boards_epic_board_recent_visits ALTER COLUMN id SET DEFAULT nextval('boards_epic_board_recent_visits_id_seq'::regclass);
+
ALTER TABLE ONLY boards_epic_boards ALTER COLUMN id SET DEFAULT nextval('boards_epic_boards_id_seq'::regclass);
ALTER TABLE ONLY boards_epic_list_user_preferences ALTER COLUMN id SET DEFAULT nextval('boards_epic_list_user_preferences_id_seq'::regclass);
@@ -20468,6 +20488,9 @@ ALTER TABLE ONLY boards_epic_board_labels
ALTER TABLE ONLY boards_epic_board_positions
ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT boards_epic_board_recent_visits_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY boards_epic_boards
ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id);
@@ -22293,6 +22316,12 @@ CREATE INDEX index_boards_epic_board_positions_on_epic_id ON boards_epic_board_p
CREATE INDEX index_boards_epic_board_positions_on_scoped_relative_position ON boards_epic_board_positions USING btree (epic_board_id, epic_id, relative_position);
+CREATE INDEX index_boards_epic_board_recent_visits_on_epic_board_id ON boards_epic_board_recent_visits USING btree (epic_board_id);
+
+CREATE INDEX index_boards_epic_board_recent_visits_on_group_id ON boards_epic_board_recent_visits USING btree (group_id);
+
+CREATE INDEX index_boards_epic_board_recent_visits_on_user_id ON boards_epic_board_recent_visits USING btree (user_id);
+
CREATE INDEX index_boards_epic_boards_on_group_id ON boards_epic_boards USING btree (group_id);
CREATE INDEX index_boards_epic_list_user_preferences_on_epic_list_id ON boards_epic_list_user_preferences USING btree (epic_list_id);
@@ -22857,6 +22886,8 @@ CREATE INDEX index_environments_on_state_and_auto_stop_at ON environments USING
CREATE UNIQUE INDEX index_epic_board_list_preferences_on_user_and_list ON boards_epic_list_user_preferences USING btree (user_id, epic_list_id);
+CREATE UNIQUE INDEX index_epic_board_recent_visits_on_user_group_and_board ON boards_epic_board_recent_visits USING btree (user_id, group_id, epic_board_id);
+
CREATE INDEX index_epic_issues_on_epic_id ON epic_issues USING btree (epic_id);
CREATE INDEX index_epic_issues_on_epic_id_and_issue_id ON epic_issues USING btree (epic_id, issue_id);
@@ -26626,6 +26657,9 @@ ALTER TABLE ONLY packages_rubygems_metadata
ALTER TABLE ONLY packages_pypi_metadata
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_96c2c18642 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
@@ -26890,6 +26924,9 @@ ALTER TABLE ONLY pages_deployments
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_c4dcba4a3e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY ci_job_artifacts
ADD CONSTRAINT fk_rails_c5137cb2c1 FOREIGN KEY (job_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
@@ -27067,6 +27104,9 @@ ALTER TABLE ONLY draft_notes
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY boards_epic_board_recent_visits
+ ADD CONSTRAINT fk_rails_e77911cf03 FOREIGN KEY (epic_board_id) REFERENCES boards_epic_boards(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY dast_site_tokens
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/api/members.md b/doc/api/members.md
index 4c740247a70..6098a80d0dd 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -288,7 +288,8 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
- "last_activity_on": "2021-01-27"
+ "last_activity_on": "2021-01-27",
+ "membership_type": "group_member"
},
{
"id": 2,
@@ -298,7 +299,8 @@ Example response:
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"email": "john@example.com",
- "last_activity_on": "2021-01-25"
+ "last_activity_on": "2021-01-25",
+ "membership_type": "group_member"
},
{
"id": 3,
@@ -307,7 +309,8 @@ Example response:
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
- "last_activity_on": "2021-01-20"
+ "last_activity_on": "2021-01-20",
+ "membership_type": "group_invite"
}
]
```
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index b7344808989..1bf6cf11526 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -17,7 +17,12 @@ module Banzai
return context[:project] || context[:group] unless ref
return context[:project] if context[:project]&.full_path == ref
- Project.find_by_full_path(ref)
+ if reference_cache.cache_loaded?
+ # optimization to reuse the parent_per_reference query information
+ reference_cache.parent_per_reference[ref || reference_cache.current_parent_path]
+ else
+ Project.find_by_full_path(ref)
+ end
end
end
end
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
index 2763e084de9..08014ccdcce 100644
--- a/lib/banzai/filter/references/abstract_reference_filter.rb
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -8,6 +8,12 @@ module Banzai
class AbstractReferenceFilter < ReferenceFilter
include CrossProjectReference
+ def initialize(doc, context = nil, result = nil)
+ super
+
+ @reference_cache = ReferenceCache.new(self, context)
+ end
+
# REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# reference (which we replace with placeholder during re-scaping). The
# random number helps ensure it's pretty close to unique. Since it's a
@@ -112,6 +118,8 @@ module Banzai
def call
return doc unless project || group || user
+ reference_cache.load_reference_cache(nodes) if respond_to?(:parent_records)
+
ref_pattern = object_reference_pattern
link_pattern = object_class.link_reference_pattern
@@ -174,9 +182,9 @@ module Banzai
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
parent_path = if parent_type == :group
- full_group_path(namespace_ref)
+ reference_cache.full_group_path(namespace_ref)
else
- full_project_path(namespace_ref, project_ref)
+ reference_cache.full_project_path(namespace_ref, project_ref)
end
parent = from_ref_cached(parent_path)
@@ -263,127 +271,6 @@ module Banzai
text
end
- # Returns a Hash containing all object references (e.g. issue IDs) per the
- # project they belong to.
- def references_per_parent
- @references_per ||= {}
-
- @references_per[parent_type] ||= begin
- refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = [
- object_class.link_reference_pattern,
- object_class.reference_pattern
- ].compact.reduce { |a, b| Regexp.union(a, b) }
-
- nodes.each do |node|
- node.to_html.scan(regex) do
- path = if parent_type == :project
- full_project_path($~[:namespace], $~[:project])
- else
- full_group_path($~[:group])
- end
-
- if ident = identifier($~)
- refs[path] << ident
- end
- end
- end
-
- refs
- end
- end
-
- # Returns a Hash containing referenced projects grouped per their full
- # path.
- def parent_per_reference
- @per_reference ||= {}
-
- @per_reference[parent_type] ||= begin
- refs = Set.new
-
- references_per_parent.each do |ref, _|
- refs << ref
- end
-
- find_for_paths(refs.to_a).index_by(&:full_path)
- end
- end
-
- def relation_for_paths(paths)
- klass = parent_type.to_s.camelize.constantize
- result = klass.where_full_path_in(paths)
- return result if parent_type == :group
-
- result.includes(:namespace) if parent_type == :project
- end
-
- # Returns projects for the given paths.
- def find_for_paths(paths)
- if Gitlab::SafeRequestStore.active?
- cache = refs_cache
- to_query = paths - cache.keys
-
- unless to_query.empty?
- records = relation_for_paths(to_query)
-
- found = []
- records.each do |record|
- ref = record.full_path
- get_or_set_cache(cache, ref) { record }
- found << ref
- end
-
- not_found = to_query - found
- not_found.each do |ref|
- get_or_set_cache(cache, ref) { nil }
- end
- end
-
- cache.slice(*paths).values.compact
- else
- relation_for_paths(paths)
- end
- end
-
- def current_parent_path
- @current_parent_path ||= parent&.full_path
- end
-
- def current_project_namespace_path
- @current_project_namespace_path ||= project&.namespace&.full_path
- end
-
- def records_per_parent
- @_records_per_project ||= {}
-
- @_records_per_project[object_class.to_s.underscore] ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- parent_per_reference.each do |path, parent|
- record_ids = references_per_parent[path]
-
- parent_records(parent, record_ids).each do |record|
- hash[parent][record_identifier(record)] = record
- end
- end
-
- hash
- end
- end
-
- private
-
- def full_project_path(namespace, project_ref)
- return current_parent_path unless project_ref
-
- namespace_ref = namespace || current_project_namespace_path
- "#{namespace_ref}/#{project_ref}"
- end
-
- def refs_cache
- Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
- end
-
def parent_type
:project
end
@@ -392,11 +279,9 @@ module Banzai
parent_type == :project ? project : group
end
- def full_group_path(group_ref)
- return current_parent_path unless group_ref
+ private
- group_ref
- end
+ attr_accessor :reference_cache
def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text)
@@ -409,5 +294,3 @@ module Banzai
end
end
end
-
-Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
index 1baafeccbd9..157dc696cc8 100644
--- a/lib/banzai/filter/references/commit_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -19,7 +19,7 @@ module Banzai
def find_object(project, id)
return unless project.is_a?(Project) && project.valid_repo?
- _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+ _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
record
end
@@ -28,7 +28,7 @@ module Banzai
return [] unless noteable.is_a?(MergeRequest)
@referenced_merge_request_commit_shas ||= begin
- referenced_shas = references_per_parent.values.reduce(:|).to_a
+ referenced_shas = reference_cache.references_per_parent.values.reduce(:|).to_a
noteable.all_commit_shas.select do |sha|
referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
end
@@ -66,12 +66,12 @@ module Banzai
extras
end
- private
-
def parent_records(parent, ids)
parent.commits_by(oids: ids.to_a)
end
+ private
+
def noteable
context[:noteable]
end
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
index de8d58de72f..01e1036dcec 100644
--- a/lib/banzai/filter/references/design_reference_filter.rb
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -36,7 +36,7 @@ module Banzai
self.object_class = ::DesignManagement::Design
def find_object(project, identifier)
- records_per_parent[project][identifier]
+ reference_cache.records_per_parent[project][identifier]
end
def parent_records(project, identifiers)
@@ -59,15 +59,6 @@ module Banzai
super.includes(:route, :namespace, :group)
end
- def parent_type
- :project
- end
-
- # optimisation to reuse the parent_per_reference query information
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
-
def url_for_object(design, project)
path_options = { vueroute: design.filename }
Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
diff --git a/lib/banzai/filter/references/issuable_reference_filter.rb b/lib/banzai/filter/references/issuable_reference_filter.rb
index b8ccb926ae9..6349f8542ca 100644
--- a/lib/banzai/filter/references/issuable_reference_filter.rb
+++ b/lib/banzai/filter/references/issuable_reference_filter.rb
@@ -9,11 +9,7 @@ module Banzai
end
def find_object(parent, iid)
- records_per_parent[parent][iid]
- end
-
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
+ reference_cache.records_per_parent[parent][iid]
end
end
end
diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb
index f9668d22d40..9c7c95d97f4 100644
--- a/lib/banzai/filter/references/label_reference_filter.rb
+++ b/lib/banzai/filter/references/label_reference_filter.rb
@@ -17,7 +17,7 @@ module Banzai
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace = $~[:namespace]
project = $~[:project]
- project_path = full_project_path(namespace, project)
+ project_path = reference_cache.full_project_path(namespace, project)
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
if label
@@ -93,7 +93,7 @@ module Banzai
parent = project || group
if project || full_path_ref?(matches)
- project_path = full_project_path(matches[:namespace], matches[:project])
+ project_path = reference_cache.full_project_path(matches[:namespace], matches[:project])
parent_from_ref = from_ref_cached(project_path)
reference = parent_from_ref.to_human_reference(parent)
diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb
index c7e4b8b35a2..31a961f3e73 100644
--- a/lib/banzai/filter/references/milestone_reference_filter.rb
+++ b/lib/banzai/filter/references/milestone_reference_filter.rb
@@ -67,7 +67,7 @@ module Banzai
end
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
- project_path = full_project_path(namespace_ref, project_ref)
+ project_path = reference_cache.full_project_path(namespace_ref, project_ref)
# Returns group if project is not found by path
parent = parent_from_ref(project_path)
diff --git a/lib/banzai/filter/references/reference_cache.rb b/lib/banzai/filter/references/reference_cache.rb
new file mode 100644
index 00000000000..195357a8d3d
--- /dev/null
+++ b/lib/banzai/filter/references/reference_cache.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class ReferenceCache
+ include Gitlab::Utils::StrongMemoize
+ include RequestStoreReferenceCache
+
+ def initialize(filter, context)
+ @filter = filter
+ @context = context
+ end
+
+ def load_reference_cache(nodes)
+ load_references_per_parent(nodes)
+ load_parent_per_reference
+ load_records_per_parent
+
+ @cache_loaded = true
+ end
+
+ # Loads all object references (e.g. issue IDs) per
+ # project/group they belong to.
+ def load_references_per_parent(nodes)
+ @references_per_parent ||= {}
+
+ @references_per_parent[parent_type] ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ path = if parent_type == :project
+ full_project_path($~[:namespace], $~[:project])
+ else
+ full_group_path($~[:group])
+ end
+
+ ident = filter.identifier($~)
+ refs[path] << ident if ident
+ end
+ end
+
+ refs
+ end
+ end
+
+ def references_per_parent
+ @references_per_parent[parent_type]
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def load_parent_per_reference
+ @per_reference ||= {}
+
+ @per_reference[parent_type] ||= begin
+ refs = references_per_parent.keys.to_set
+
+ find_for_paths(refs.to_a).index_by(&:full_path)
+ end
+ end
+
+ def parent_per_reference
+ @per_reference[parent_type]
+ end
+
+ def load_records_per_parent
+ @_records_per_project ||= {}
+
+ @_records_per_project[filter.object_class.to_s.underscore] ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ parent_per_reference.each do |path, parent|
+ record_ids = references_per_parent[path]
+
+ filter.parent_records(parent, record_ids).each do |record|
+ hash[parent][filter.record_identifier(record)] = record
+ end
+ end
+
+ hash
+ end
+ end
+
+ def records_per_parent
+ @_records_per_project[filter.object_class.to_s.underscore]
+ end
+
+ def relation_for_paths(paths)
+ klass = parent_type.to_s.camelize.constantize
+ result = klass.where_full_path_in(paths)
+ return result if parent_type == :group
+
+ result.includes(namespace: :route) if parent_type == :project
+ end
+
+ # Returns projects for the given paths.
+ def find_for_paths(paths)
+ if Gitlab::SafeRequestStore.active?
+ cache = refs_cache
+ to_query = paths - cache.keys
+
+ unless to_query.empty?
+ records = relation_for_paths(to_query)
+
+ found = []
+ records.each do |record|
+ ref = record.full_path
+ get_or_set_cache(cache, ref) { record }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
+ end
+ end
+
+ cache.slice(*paths).values.compact
+ else
+ relation_for_paths(paths)
+ end
+ end
+
+ def current_parent_path
+ strong_memoize(:current_parent_path) do
+ parent&.full_path
+ end
+ end
+
+ def current_project_namespace_path
+ strong_memoize(:current_project_namespace_path) do
+ project&.namespace&.full_path
+ end
+ end
+
+ def full_project_path(namespace, project_ref)
+ return current_parent_path unless project_ref
+
+ namespace_ref = namespace || current_project_namespace_path
+ "#{namespace_ref}/#{project_ref}"
+ end
+
+ def full_group_path(group_ref)
+ return current_parent_path unless group_ref
+
+ group_ref
+ end
+
+ def cache_loaded?
+ !!@cache_loaded
+ end
+
+ private
+
+ attr_accessor :filter, :context
+
+ delegate :project, :group, :parent, :parent_type, to: :filter
+
+ def regex
+ strong_memoize(:regex) do
+ [
+ filter.object_class.link_reference_pattern,
+ filter.object_class.reference_pattern
+ ].compact.reduce { |a, b| Regexp.union(a, b) }
+ end
+ end
+
+ def refs_cache
+ Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::ReferenceCache.prepend_if_ee('EE::Banzai::Filter::References::ReferenceCache')
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
index a83cb12afd3..58436f4505e 100644
--- a/lib/banzai/filter/references/reference_filter.rb
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -97,6 +97,18 @@ module Banzai
@nodes ||= each_node.to_a
end
+ def object_class
+ self.class.object_class
+ end
+
+ def project
+ context[:project]
+ end
+
+ def group
+ context[:group]
+ end
+
private
# Returns a data attribute String to attach to a reference link
@@ -141,14 +153,6 @@ module Banzai
needs :project unless skip_project_check?
end
- def project
- context[:project]
- end
-
- def group
- context[:group]
- end
-
def user
context[:user]
end
@@ -216,10 +220,6 @@ module Banzai
node.is_a?(Nokogiri::XML::Element)
end
- def object_class
- self.class.object_class
- end
-
def object_reference_pattern
@object_reference_pattern ||= object_class.reference_pattern
end
diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb
index 55abb9a15be..c89679f63b5 100644
--- a/lib/bulk_imports/clients/http.rb
+++ b/lib/bulk_imports/clients/http.rb
@@ -28,6 +28,17 @@ module BulkImports
end
end
+ def post(resource, body = {})
+ with_error_handling do
+ Gitlab::HTTP.post(
+ resource_url(resource),
+ headers: request_headers,
+ follow_redirects: false,
+ body: body
+ )
+ end
+ end
+
def each_page(method, resource, query = {}, &block)
return to_enum(__method__, method, resource, query) unless block_given?
diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb
new file mode 100644
index 00000000000..79e7a2f2279
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to set namespaces.traversal_ids in sub-batches, of all namespaces with
+ # a parent and not already set.
+ # rubocop:disable Style/Documentation
+ class BackfillNamespaceTraversalIdsChildren
+ class Namespace < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'namespaces'
+
+ scope :base_query, -> { where.not(parent_id: nil) }
+ end
+
+ PAUSE_SECONDS = 0.1
+
+ def perform(start_id, end_id, sub_batch_size)
+ batch_query = Namespace.base_query.where(id: start_id..end_id)
+ batch_query.each_batch(of: sub_batch_size) do |sub_batch|
+ first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first
+ ranged_query = Namespace.unscoped.base_query.where(id: first..last)
+
+ update_sql = <<~SQL
+ UPDATE namespaces
+ SET traversal_ids = calculated_ids.traversal_ids
+ FROM #{calculated_traversal_ids(ranged_query)} calculated_ids
+ WHERE namespaces.id = calculated_ids.id
+ AND namespaces.traversal_ids = '{}'
+ SQL
+ ActiveRecord::Base.connection.execute(update_sql)
+
+ sleep PAUSE_SECONDS
+ end
+
+ # We have to add all arguments when marking a job as succeeded as they
+ # are all used to track the job by `queue_background_migration_jobs_by_range_at_intervals`
+ mark_job_as_succeeded(start_id, end_id, sub_batch_size)
+ end
+
+ private
+
+ # Calculate the ancestor path for a given set of namespaces.
+ def calculated_traversal_ids(batch)
+ <<~SQL
+ (
+ WITH RECURSIVE cte(source_id, namespace_id, parent_id, height) AS (
+ (
+ SELECT batch.id, batch.id, batch.parent_id, 1
+ FROM (#{batch.to_sql}) AS batch
+ )
+ UNION ALL
+ (
+ SELECT cte.source_id, n.id, n.parent_id, cte.height+1
+ FROM namespaces n, cte
+ WHERE n.id = cte.parent_id
+ )
+ )
+ SELECT flat_hierarchy.source_id as id,
+ array_agg(flat_hierarchy.namespace_id ORDER BY flat_hierarchy.height DESC) as traversal_ids
+ FROM (SELECT * FROM cte FOR UPDATE) flat_hierarchy
+ GROUP BY flat_hierarchy.source_id
+ )
+ SQL
+ end
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ 'BackfillNamespaceTraversalIdsChildren',
+ arguments
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb
new file mode 100644
index 00000000000..f3fc87cbac7
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to set namespaces.traversal_ids in sub-batches, of all namespaces
+ # without a parent and not already set.
+ # rubocop:disable Style/Documentation
+ class BackfillNamespaceTraversalIdsRoots
+ class Namespace < ActiveRecord::Base
+ include ::EachBatch
+
+ self.table_name = 'namespaces'
+
+ scope :base_query, -> { where(parent_id: nil) }
+ end
+
+ PAUSE_SECONDS = 0.1
+
+ def perform(start_id, end_id, sub_batch_size)
+ ranged_query = Namespace.base_query
+ .where(id: start_id..end_id)
+ .where("traversal_ids = '{}'")
+
+ ranged_query.each_batch(of: sub_batch_size) do |sub_batch|
+ sub_batch.update_all('traversal_ids = ARRAY[id]')
+ sleep PAUSE_SECONDS
+ end
+
+ mark_job_as_succeeded(start_id, end_id, sub_batch_size)
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ 'BackfillNamespaceTraversalIdsRoots',
+ arguments
+ )
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 085d16b7bd5..dd1195bebc3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3289,6 +3289,9 @@ msgstr ""
msgid "Allowed Geo IP"
msgstr ""
+msgid "Allowed characters: +, 0-9, -, and spaces."
+msgstr ""
+
msgid "Allowed email domain restriction only permitted for top-level groups"
msgstr ""
@@ -8564,6 +8567,9 @@ msgstr ""
msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
msgstr ""
+msgid "ContainerRegistry|-- tags"
+msgstr ""
+
msgid "ContainerRegistry|Build an image"
msgstr ""
@@ -17586,10 +17592,10 @@ msgstr ""
msgid "Integrations|Failed to unlink namespace. Please try again."
msgstr ""
-msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs"
+msgid "Integrations|Includes Standard, plus the entire commit message, commit hash, and issue IDs"
msgstr ""
-msgid "Integrations|Includes commit title and branch"
+msgid "Integrations|Includes commit title and branch."
msgstr ""
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
@@ -17667,7 +17673,7 @@ msgstr ""
msgid "Integrations|Use default settings"
msgstr ""
-msgid "Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) is created."
+msgid "Integrations|When you mention a Jira issue in a commit or merge request, GitLab creates a remote link and comment (if enabled)."
msgstr ""
msgid "Integrations|You can now close this window and return to the GitLab for Jira application."
@@ -18609,6 +18615,9 @@ msgstr ""
msgid "JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only."
msgstr ""
+msgid "JiraService|You need to configure Jira before enabling this integration. For more details, read the %{jira_doc_link_start}Jira integration documentation%{link_end}."
+msgstr ""
+
msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
msgstr ""
@@ -18657,6 +18666,18 @@ msgstr ""
msgid "Job was retried"
msgstr ""
+msgid "JobName|build-job"
+msgstr ""
+
+msgid "JobName|deploy-app"
+msgstr ""
+
+msgid "JobName|lint-test"
+msgstr ""
+
+msgid "JobName|unit-test"
+msgstr ""
+
msgid "Jobs"
msgstr ""
@@ -23812,6 +23833,54 @@ msgstr ""
msgid "PipelineCharts|Total:"
msgstr ""
+msgid "PipelineEditorTutorial|A typical GitLab pipeline consists of three stages: build, test and deploy. Each stage can have one or more jobs."
+msgstr ""
+
+msgid "PipelineEditorTutorial|Browse %{linkStart}CI/CD examples and templates%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditorTutorial|Get started with GitLab CI/CD"
+msgstr ""
+
+msgid "PipelineEditorTutorial|GitLab CI/CD can automatically build, test, and deploy your application."
+msgstr ""
+
+msgid "PipelineEditorTutorial|If you’re using a self-managed GitLab instance, %{linkStart}make sure your instance has runners available.%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditorTutorial|In the example below, %{codeStart}build%{codeEnd} and %{codeStart}deploy%{codeEnd} each contain one job, and %{codeStart}test%{codeEnd} contains two jobs. Your scripts run in jobs like these."
+msgstr ""
+
+msgid "PipelineEditorTutorial|Learn more about %{linkStart}GitLab CI/CD concepts%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditorTutorial|Make your pipeline more efficient with the %{linkStart}Needs keyword%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditorTutorial|Resources to help with your CI/CD configuration:"
+msgstr ""
+
+msgid "PipelineEditorTutorial|The pipeline stages and jobs are defined in a %{codeStart}.gitlab-ci.yml%{codeEnd} file. You can edit, visualize and validate the syntax in this file by using the Pipeline Editor."
+msgstr ""
+
+msgid "PipelineEditorTutorial|Use the Visualize and Lint tabs in the Pipeline Editor to visualize your pipeline and check for any errors or warnings before committing your changes."
+msgstr ""
+
+msgid "PipelineEditorTutorial|View %{linkStart}.gitlab-ci.yml syntax reference%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditorTutorial|You can use %{linkStart}CI/CD examples and templates%{linkEnd} to get your first %{codeStart}.gitlab-ci.yml%{codeEnd} configuration file started. Your first pipeline runs when you commit the changes."
+msgstr ""
+
+msgid "PipelineEditorTutorial|⚙️ Pipeline configuration reference"
+msgstr ""
+
+msgid "PipelineEditorTutorial|💡 Tip: Visualize and validate your pipeline"
+msgstr ""
+
+msgid "PipelineEditorTutorial|🚀 Run your first pipeline"
+msgstr ""
+
msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty."
msgstr ""
@@ -30512,6 +30581,15 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "StageName|Build"
+msgstr ""
+
+msgid "StageName|Deploy"
+msgstr ""
+
+msgid "StageName|Test"
+msgstr ""
+
msgid "Standard"
msgstr ""
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 54b190a220a..b666f73110a 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Groups::GroupMembersController do
expect(response).to render_template(:index)
end
- context 'user with owner access' do
+ context 'when user can manage members' do
let_it_be(:invited) { create_list(:group_member, 3, :invited, group: group) }
before do
@@ -71,6 +71,19 @@ RSpec.describe Groups::GroupMembersController do
end
end
+ context 'when user cannot manage members' do
+ before do
+ sign_in(user)
+ end
+
+ it 'does not assign invited members or skip_groups', :aggregate_failures do
+ get :index, params: { group_id: group }
+
+ expect(assigns(:invited_members)).to be_nil
+ expect(assigns(:skip_groups)).to be_nil
+ end
+ end
+
context 'when user has owner access to subgroup' do
let_it_be(:nested_group) { create(:group, parent: group) }
let_it_be(:nested_group_user) { create(:user) }
diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb
index c1740ee1796..bad4c482bc6 100644
--- a/spec/finders/concerns/packages/finder_helper_spec.rb
+++ b/spec/finders/concerns/packages/finder_helper_spec.rb
@@ -3,6 +3,30 @@
require 'spec_helper'
RSpec.describe ::Packages::FinderHelper do
+ describe '#packages_for_project' do
+ let_it_be_with_reload(:project1) { create(:project) }
+ let_it_be(:package1) { create(:package, project: project1) }
+ let_it_be(:package2) { create(:package, :error, project: project1) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:package3) { create(:package, project: project2) }
+
+ let(:finder_class) do
+ Class.new do
+ include ::Packages::FinderHelper
+
+ def execute(project1)
+ packages_for_project(project1)
+ end
+ end
+ end
+
+ let(:finder) { finder_class.new }
+
+ subject { finder.execute(project1) }
+
+ it { is_expected.to eq [package1]}
+ end
+
describe '#packages_visible_to_user' do
using RSpec::Parameterized::TableSyntax
@@ -12,6 +36,7 @@ RSpec.describe ::Packages::FinderHelper do
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
let_it_be(:package2) { create(:package, project: project2) }
+ let_it_be(:package3) { create(:package, :error, project: project2) }
let(:finder_class) do
Class.new do
diff --git a/spec/finders/packages/composer/packages_finder_spec.rb b/spec/finders/packages/composer/packages_finder_spec.rb
new file mode 100644
index 00000000000..d4328827de3
--- /dev/null
+++ b/spec/finders/packages/composer/packages_finder_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe ::Packages::Composer::PackagesFinder do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:params) { {} }
+
+ describe '#execute' do
+ let_it_be(:composer_package) { create(:composer_package, project: project) }
+ let_it_be(:composer_package2) { create(:composer_package, project: project) }
+ let_it_be(:error_package) { create(:composer_package, :error, project: project) }
+ let_it_be(:composer_package3) { create(:composer_package) }
+
+ subject { described_class.new(user, group, params).execute }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to match_array([composer_package, composer_package2]) }
+ end
+end
diff --git a/spec/finders/packages/conan/package_finder_spec.rb b/spec/finders/packages/conan/package_finder_spec.rb
index 936a0e5ff4b..b26f8900090 100644
--- a/spec/finders/packages/conan/package_finder_spec.rb
+++ b/spec/finders/packages/conan/package_finder_spec.rb
@@ -11,7 +11,8 @@ RSpec.describe ::Packages::Conan::PackageFinder do
subject { described_class.new(user, query: query).execute }
- context 'packages that are not visible to user' do
+ context 'packages that are not installable' do
+ let!(:conan_package3) { create(:conan_package, :error, project: project) }
let!(:non_visible_project) { create(:project, :private) }
let!(:non_visible_conan_package) { create(:conan_package, project: non_visible_project) }
let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" }
diff --git a/spec/finders/packages/generic/package_finder_spec.rb b/spec/finders/packages/generic/package_finder_spec.rb
index ed34268e7a9..707f943b285 100644
--- a/spec/finders/packages/generic/package_finder_spec.rb
+++ b/spec/finders/packages/generic/package_finder_spec.rb
@@ -23,6 +23,13 @@ RSpec.describe ::Packages::Generic::PackageFinder do
expect(found_package).to eq(package)
end
+ it 'does not find uninstallable packages' do
+ error_package = create(:generic_package, :error, project: project)
+
+ expect { finder.execute!(error_package.name, error_package.version) }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+
it 'raises ActiveRecord::RecordNotFound if package is not found' do
expect { finder.execute!(package.name, '3.1.4') }
.to raise_error(ActiveRecord::RecordNotFound)
diff --git a/spec/finders/packages/go/package_finder_spec.rb b/spec/finders/packages/go/package_finder_spec.rb
index b6fad1e7061..dbcb8255d47 100644
--- a/spec/finders/packages/go/package_finder_spec.rb
+++ b/spec/finders/packages/go/package_finder_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Packages::Go::PackageFinder do
let_it_be(:mod) { create :go_module, project: project }
let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' }
- let_it_be(:package) { create :golang_package, project: project, name: mod.name, version: 'v1.0.1' }
+ let_it_be_with_refind(:package) { create :golang_package, project: project, name: mod.name, version: 'v1.0.1' }
let(:finder) { described_class.new(project, mod_name, version_name) }
@@ -54,6 +54,17 @@ RSpec.describe Packages::Go::PackageFinder do
it { is_expected.to eq(package) }
end
+ context 'with an uninstallable package' do
+ let(:mod_name) { mod.name }
+ let(:version_name) { version.name }
+
+ before do
+ package.update_column(:status, 1)
+ end
+
+ it { is_expected.to eq(nil) }
+ end
+
context 'with an invalid name' do
let(:mod_name) { 'foo/bar' }
let(:version_name) { 'baz' }
diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb
index 9a6bb675248..d5f521ff895 100644
--- a/spec/finders/packages/maven/package_finder_spec.rb
+++ b/spec/finders/packages/maven/package_finder_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
- let_it_be(:package) { create(:maven_package, project: project) }
+ let_it_be_with_refind(:package) { create(:maven_package, project: project) }
let(:param_path) { nil }
let(:param_project) { nil }
@@ -36,6 +36,16 @@ RSpec.describe ::Packages::Maven::PackageFinder do
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ context 'with an uninstallable package' do
+ let(:param_path) { package.maven_metadatum.path }
+
+ before do
+ package.update_column(:status, 1)
+ end
+
+ it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
end
context 'within the project' do
diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb
index f021d800f31..a995f3b96c4 100644
--- a/spec/finders/packages/npm/package_finder_spec.rb
+++ b/spec/finders/packages/npm/package_finder_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
RSpec.describe ::Packages::Npm::PackageFinder do
let_it_be_with_reload(:project) { create(:project)}
- let_it_be(:package) { create(:npm_package, project: project) }
+ let_it_be_with_refind(:package) { create(:npm_package, project: project) }
let(:project) { package.project }
let(:package_name) { package.name }
@@ -46,6 +46,14 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to be_empty }
end
+
+ context 'with an uninstallable package' do
+ before do
+ package.update_column(:status, 1)
+ end
+
+ it { is_expected.to be_empty }
+ end
end
subject { finder.execute }
diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb
index 10b5f6c8ec2..59cca2d06dc 100644
--- a/spec/finders/packages/nuget/package_finder_spec.rb
+++ b/spec/finders/packages/nuget/package_finder_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Packages::Nuget::PackageFinder do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: subgroup) }
- let_it_be(:package1) { create(:nuget_package, project: project) }
+ let_it_be_with_refind(:package1) { create(:nuget_package, project: project) }
let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: project) }
let_it_be(:package3) { create(:nuget_package, name: 'Another.Dummy.Package', project: project) }
let_it_be(:other_package_1) { create(:nuget_package, name: package1.name, version: package1.version) }
@@ -33,6 +33,14 @@ RSpec.describe Packages::Nuget::PackageFinder do
it { is_expected.to be_empty }
end
+ context 'with an uninstallable package' do
+ before do
+ package1.update_column(:status, 1)
+ end
+
+ it { is_expected.to contain_exactly(package2) }
+ end
+
context 'with valid version' do
let(:package_version) { '2.0.0' }
diff --git a/spec/finders/packages/package_finder_spec.rb b/spec/finders/packages/package_finder_spec.rb
index e8c7404a612..6a1d857dad4 100644
--- a/spec/finders/packages/package_finder_spec.rb
+++ b/spec/finders/packages/package_finder_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe ::Packages::PackageFinder do
let_it_be(:project) { create(:project) }
- let_it_be(:maven_package) { create(:maven_package, project: project) }
+ let_it_be_with_refind(:maven_package) { create(:maven_package, project: project) }
describe '#execute' do
let(:package_id) { maven_package.id }
@@ -13,6 +13,16 @@ RSpec.describe ::Packages::PackageFinder do
it { is_expected.to eq(maven_package) }
+ context 'with non-displayable package' do
+ before do
+ maven_package.update_column(:status, 1)
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
+
context 'processing packages' do
let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
let(:package_id) { nuget_package.id }
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
new file mode 100644
index 00000000000..8a4f07c4d88
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -0,0 +1,47 @@
+import { getByRole } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
+import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue';
+
+describe('First pipeline card', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ ciExamplesHelpPagePath: '/pipelines/examples',
+ runnerHelpPagePath: '/help/runners',
+ };
+
+ const createComponent = () => {
+ wrapper = mount(FirstPipelineCard, {
+ provide: {
+ ...defaultProvide,
+ },
+ });
+ };
+
+ const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href;
+ const findPipelinesLink = () => getLinkByName(/examples and templates/i);
+ const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i);
+ const findVisualReference = () => wrapper.findComponent(PipelineVisualReference);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the title', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders the content', () => {
+ expect(findVisualReference().exists()).toBe(true);
+ });
+
+ it('renders the links', () => {
+ expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath);
+ expect(findPipelinesLink()).toContain(defaultProvide.ciExamplesHelpPagePath);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
new file mode 100644
index 00000000000..c592e959068
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -0,0 +1,26 @@
+import { shallowMount } from '@vue/test-utils';
+import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+
+describe('Getting started card', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(GettingStartedCard);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the title', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders the content', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
new file mode 100644
index 00000000000..3c8821d05a7
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -0,0 +1,51 @@
+import { getByRole } from '@testing-library/dom';
+import { mount } from '@vue/test-utils';
+import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
+
+describe('Pipeline config reference card', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ ciExamplesHelpPagePath: 'help/ci/examples/',
+ ciHelpPagePath: 'help/ci/introduction',
+ needsHelpPagePath: 'help/ci/yaml#needs',
+ ymlHelpPagePath: 'help/ci/yaml',
+ };
+
+ const createComponent = () => {
+ wrapper = mount(PipelineConfigReferenceCard, {
+ provide: {
+ ...defaultProvide,
+ },
+ });
+ };
+
+ const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href;
+ const findCiExamplesLink = () => getLinkByName(/CI\/CD examples and templates/i);
+ const findCiIntroLink = () => getLinkByName(/GitLab CI\/CD concepts/i);
+ const findNeedsLink = () => getLinkByName(/Needs keyword/i);
+ const findYmlSyntaxLink = () => getLinkByName(/.gitlab-ci.yml syntax reference/i);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the title', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders the content', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph);
+ });
+
+ it('renders the links', () => {
+ expect(findCiExamplesLink()).toContain(defaultProvide.ciExamplesHelpPagePath);
+ expect(findCiIntroLink()).toContain(defaultProvide.ciHelpPagePath);
+ expect(findNeedsLink()).toContain(defaultProvide.needsHelpPagePath);
+ expect(findYmlSyntaxLink()).toContain(defaultProvide.ymlHelpPagePath);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
new file mode 100644
index 00000000000..bebd2484c1d
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -0,0 +1,26 @@
+import { shallowMount } from '@vue/test-utils';
+import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+
+describe('Visual and Lint card', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(VisualizeAndLintCard);
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the title', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
+ });
+
+ it('renders the content', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 587373c99b4..fea7d90de52 100644
--- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -1,4 +1,9 @@
+import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue';
+import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue';
+import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue';
+import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue';
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
describe('Pipeline editor drawer', () => {
@@ -8,7 +13,12 @@ describe('Pipeline editor drawer', () => {
wrapper = shallowMount(PipelineEditorDrawer);
};
- const findToggleBtn = () => wrapper.find('[data-testid="toggleBtn"]');
+ const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard);
+ const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard);
+ const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard);
+ const findToggleBtn = () => wrapper.findComponent(GlButton);
+ const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard);
+
const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]');
const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
@@ -24,7 +34,7 @@ describe('Pipeline editor drawer', () => {
createComponent();
});
- it('show the left facing arrow icon', () => {
+ it('shows the left facing arrow icon', () => {
expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
});
@@ -51,7 +61,7 @@ describe('Pipeline editor drawer', () => {
await clickToggleBtn();
});
- it('show the right facing arrow icon', () => {
+ it('shows the right facing arrow icon', () => {
expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right');
});
@@ -59,10 +69,17 @@ describe('Pipeline editor drawer', () => {
expect(findCollapseText().exists()).toBe(true);
});
- it('show the drawer content', () => {
+ it('shows the drawer content', () => {
expect(findDrawerContent().exists()).toBe(true);
});
+ it('shows all the introduction cards', () => {
+ expect(findFirstPipelineCard().exists()).toBe(true);
+ expect(findGettingStartedCard().exists()).toBe(true);
+ expect(findPipelineConfigReferenceCard().exists()).toBe(true);
+ expect(findVisualizeAndLintCard().exists()).toBe(true);
+ });
+
it('can close the drawer by clicking on the toggle button', async () => {
expect(findDrawerContent().exists()).toBe(true);
diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
new file mode 100644
index 00000000000..edd2b45569a
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
+
+describe('Demo job pill', () => {
+ let wrapper;
+ const jobName = 'my-build-job';
+
+ const createComponent = () => {
+ wrapper = shallowMount(DemoJobPill, {
+ propsData: {
+ jobName,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the jobName', () => {
+ expect(wrapper.text()).toContain(jobName);
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js
new file mode 100644
index 00000000000..e4834544484
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue';
+import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue';
+
+describe('Demo job pill', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineVisualReference);
+ };
+
+ const findAllDemoJobPills = () => wrapper.findAllComponents(DemoJobPill);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders all stage names', () => {
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.build);
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.test);
+ expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.deploy);
+ });
+
+ it('renders all job pills', () => {
+ expect(findAllDemoJobPills()).toHaveLength(4);
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
index 4fe44a3307a..632f506f4ae 100644
--- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js
@@ -1,7 +1,10 @@
import { GlButton, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import { useFakeDate } from 'helpers/fake_date';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import {
UNSCHEDULED_STATUS,
@@ -16,15 +19,18 @@ import {
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
} from '~/registry/explorer/constants';
+import getContainerRepositoryTagCountQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
+ let apolloProvider;
+ let localVue;
const defaultImage = {
name: 'foo',
updatedAt: '2020-11-03T13:29:21Z',
- tagsCount: 10,
canDelete: true,
project: {
visibility: 'public',
@@ -51,12 +57,31 @@ describe('Details Header', () => {
await wrapper.vm.$nextTick();
};
- const mountComponent = (propsData = { image: defaultImage }) => {
+ const mountComponent = ({
+ propsData = { image: defaultImage },
+ resolver = jest.fn().mockResolvedValue(imageTagsCountMock()),
+ $apollo = undefined,
+ } = {}) => {
+ const mocks = {};
+
+ if ($apollo) {
+ mocks.$apollo = $apollo;
+ } else {
+ localVue = createLocalVue();
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[getContainerRepositoryTagCountQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+ }
+
wrapper = shallowMount(component, {
+ localVue,
+ apolloProvider,
propsData,
directives: {
GlTooltip: createMockDirective(),
},
+ mocks,
stubs: {
TitleArea,
},
@@ -64,41 +89,48 @@ describe('Details Header', () => {
};
afterEach(() => {
+ // if we want to mix createMockApollo and manual mocks we need to reset everything
wrapper.destroy();
+ apolloProvider = undefined;
+ localVue = undefined;
wrapper = null;
});
+
describe('image name', () => {
describe('missing image name', () => {
- it('root image ', () => {
- mountComponent({ image: { ...defaultImage, name: '' } });
+ beforeEach(() => {
+ mountComponent({ propsData: { image: { ...defaultImage, name: '' } } });
+
+ return waitForPromises();
+ });
+ it('root image ', () => {
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
});
it('has an icon', () => {
- mountComponent({ image: { ...defaultImage, name: '' } });
-
expect(findInfoIcon().exists()).toBe(true);
expect(findInfoIcon().props('name')).toBe('information-o');
});
it('has a tooltip', () => {
- mountComponent({ image: { ...defaultImage, name: '' } });
-
const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip');
expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP);
});
});
describe('with image name present', () => {
- it('shows image.name ', () => {
+ beforeEach(() => {
mountComponent();
+
+ return waitForPromises();
+ });
+
+ it('shows image.name ', () => {
expect(findTitle().text()).toContain('foo');
});
it('has no icon', () => {
- mountComponent();
-
expect(findInfoIcon().exists()).toBe(false);
});
});
@@ -111,12 +143,6 @@ describe('Details Header', () => {
expect(findDeleteButton().exists()).toBe(true);
});
- it('is hidden while loading', () => {
- mountComponent({ image: defaultImage, metadataLoading: true });
-
- expect(findDeleteButton().exists()).toBe(false);
- });
-
it('has the correct text', () => {
mountComponent();
@@ -149,7 +175,7 @@ describe('Details Header', () => {
`(
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
({ canDelete, disabled, isDisabled }) => {
- mountComponent({ image: { ...defaultImage, canDelete }, disabled });
+ mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
expect(findDeleteButton().props('disabled')).toBe(isDisabled);
},
@@ -158,15 +184,32 @@ describe('Details Header', () => {
describe('metadata items', () => {
describe('tags count', () => {
+ it('displays "-- tags" while loading', async () => {
+ // here we are forced to mock apollo because `waitForMetadataItems` waits
+ // for two ticks, de facto allowing the promise to resolve, so there is
+ // no way to catch the component as both rendered and in loading state
+ mountComponent({ $apollo: { queries: { containerRepository: { loading: true } } } });
+
+ await waitForMetadataItems();
+
+ expect(findTagsCount().props('text')).toBe('-- tags');
+ });
+
it('when there is more than one tag has the correct text', async () => {
mountComponent();
+
+ await waitForPromises();
await waitForMetadataItems();
- expect(findTagsCount().props('text')).toBe('10 tags');
+ expect(findTagsCount().props('text')).toBe('13 tags');
});
it('when there is one tag has the correct text', async () => {
- mountComponent({ image: { ...defaultImage, tagsCount: 1 } });
+ mountComponent({
+ resolver: jest.fn().mockResolvedValue(imageTagsCountMock({ tagsCount: 1 })),
+ });
+
+ await waitForPromises();
await waitForMetadataItems();
expect(findTagsCount().props('text')).toBe('1 tag');
@@ -208,11 +251,13 @@ describe('Details Header', () => {
'when the status is $status the text is $text and the tooltip is $tooltip',
async ({ status, text, tooltip }) => {
mountComponent({
- image: {
- ...defaultImage,
- expirationPolicyCleanupStatus: status,
- project: {
- containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ propsData: {
+ image: {
+ ...defaultImage,
+ expirationPolicyCleanupStatus: status,
+ project: {
+ containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
+ },
},
},
});
@@ -242,7 +287,9 @@ describe('Details Header', () => {
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
- mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } });
+ mountComponent({
+ propsData: { image: { ...defaultImage, project: { visibility: 'private' } } },
+ });
await waitForMetadataItems();
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index 7d544b71466..fe258dcd4e8 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -113,7 +113,6 @@ export const containerRepositoryMock = {
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
updatedAt: '2020-11-03T13:29:21Z',
- tagsCount: 13,
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
@@ -175,6 +174,16 @@ export const imageTagsMock = (nodes = tagsMock) => ({
},
});
+export const imageTagsCountMock = (override) => ({
+ data: {
+ containerRepository: {
+ id: containerRepositoryMock.id,
+ tagsCount: 13,
+ ...override,
+ },
+ },
+});
+
export const graphQLImageDetailsMock = (override) => ({
data: {
containerRepository: {
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index eb01fb1a7e6..022f6e71fe6 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -292,7 +292,6 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
expect(findDetailsHeader().props()).toMatchObject({
- metadataLoading: false,
image: {
name: containerRepositoryMock.name,
project: {
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
index a5d91468ef2..eb6e3711e2e 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js
@@ -1,4 +1,5 @@
-import { mount } from '@vue/test-utils';
+import { GlDropdown, GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
import { deploymentMockData } from './deployment_mock_data';
@@ -11,14 +12,14 @@ const appButtonText = {
describe('Deployment View App button', () => {
let wrapper;
- const factory = (options = {}) => {
- wrapper = mount(DeploymentViewButton, {
+ const createComponent = (options = {}) => {
+ wrapper = mountExtended(DeploymentViewButton, {
...options,
});
};
beforeEach(() => {
- factory({
+ createComponent({
propsData: {
deployment: deploymentMockData,
appButtonText,
@@ -30,15 +31,21 @@ describe('Deployment View App button', () => {
wrapper.destroy();
});
+ const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink);
+ const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown);
+ const findMrWigdetDeploymentDropdownIcon = () =>
+ wrapper.findByTestId('mr-wigdet-deployment-dropdown-icon');
+ const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink);
+
describe('text', () => {
it('renders text as passed', () => {
- expect(wrapper.find(ReviewAppLink).text()).toContain(appButtonText.text);
+ expect(findReviewAppLink().props().display.text).toBe(appButtonText.text);
});
});
describe('without changes', () => {
beforeEach(() => {
- factory({
+ createComponent({
propsData: {
deployment: { ...deploymentMockData, changes: null },
appButtonText,
@@ -47,13 +54,13 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app without dropdown', () => {
- expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
});
});
describe('with a single change', () => {
beforeEach(() => {
- factory({
+ createComponent({
propsData: {
deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
appButtonText,
@@ -62,21 +69,20 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app without dropdown', () => {
- expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false);
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
});
it('renders the link to the review app linked to to the first change', () => {
const expectedUrl = deploymentMockData.changes[0].external_url;
- const deployUrl = wrapper.find('.js-deploy-url');
- expect(deployUrl.attributes().href).not.toBeNull();
- expect(deployUrl.attributes().href).toEqual(expectedUrl);
+ expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
});
});
describe('with multiple changes', () => {
beforeEach(() => {
- factory({
+ createComponent({
propsData: {
deployment: deploymentMockData,
appButtonText,
@@ -85,18 +91,18 @@ describe('Deployment View App button', () => {
});
it('renders the link to the review app with dropdown', () => {
- expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(true);
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(true);
+ expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(true);
});
it('renders all the links to the review apps', () => {
- const allUrls = wrapper.findAll('.js-deploy-url-menu-item').wrappers;
+ const allUrls = findDeployUrlMenuItems().wrappers;
const expectedUrls = deploymentMockData.changes.map((change) => change.external_url);
expectedUrls.forEach((expectedUrl, idx) => {
const deployUrl = allUrls[idx];
- expect(deployUrl.attributes().href).not.toBeNull();
- expect(deployUrl.attributes().href).toEqual(expectedUrl);
+ expect(deployUrl.attributes('href')).toBe(expectedUrl);
});
});
});
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index e276796f3ec..aacfc3b91c6 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -40,16 +40,21 @@ RSpec.describe Ci::PipelineEditorHelper do
it 'returns pipeline editor data' do
expect(pipeline_editor_data).to eq({
"ci-config-path": project.ci_config_path_or_default,
+ "ci-examples-help-page-path" => help_page_path('ci/examples/README'),
+ "ci-help-page-path" => help_page_path('ci/README'),
"commit-sha" => project.commit.sha,
"default-branch" => project.default_branch,
"empty-state-illustration-path" => 'foo',
"initial-branch-name": nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
+ "pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
+ "runner-help-page-path" => help_page_path('ci/runners/README'),
"yml-help-page-path" => help_page_path('ci/yaml/README')
})
end
@@ -61,16 +66,21 @@ RSpec.describe Ci::PipelineEditorHelper do
it 'returns pipeline editor data' do
expect(pipeline_editor_data).to eq({
"ci-config-path": project.ci_config_path_or_default,
+ "ci-examples-help-page-path" => help_page_path('ci/examples/README'),
+ "ci-help-page-path" => help_page_path('ci/README'),
"commit-sha" => '',
"default-branch" => project.default_branch,
"empty-state-illustration-path" => 'foo',
"initial-branch-name": nil,
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
+ "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'),
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
"pipeline_etag" => '',
+ "pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
+ "runner-help-page-path" => help_page_path('ci/runners/README'),
"yml-help-page-path" => help_page_path('ci/yaml/README')
})
end
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 95b78ceb5d5..60ff15a88e0 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -4,10 +4,12 @@ require 'spec_helper'
RSpec.describe Banzai::CrossProjectReference do
let(:including_class) { Class.new.include(described_class).new }
+ let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {})}
before do
allow(including_class).to receive(:context).and_return({})
allow(including_class).to receive(:parent_from_ref).and_call_original
+ allow(including_class).to receive(:reference_cache).and_return(reference_cache)
end
describe '#parent_from_ref' do
@@ -47,5 +49,18 @@ RSpec.describe Banzai::CrossProjectReference do
expect(including_class.parent_from_ref('cross/reference')).to eq project2
end
end
+
+ context 'when reference cache is loaded' do
+ let(:project2) { double('referenced project') }
+
+ before do
+ allow(reference_cache).to receive(:cache_loaded?).and_return(true)
+ allow(reference_cache).to receive(:parent_per_reference).and_return({ 'cross/reference' => project2 })
+ end
+
+ it 'pulls from the reference cache' do
+ expect(including_class.parent_from_ref('cross/reference')).to eq project2
+ end
+ end
end
end
diff --git a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
index d10b52bf7d0..3cb3ebc42a6 100644
--- a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb
@@ -8,18 +8,6 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
let(:doc) { Nokogiri::HTML.fragment('') }
let(:filter) { described_class.new(doc, project: project) }
- describe '#references_per_parent' do
- let(:doc) { Nokogiri::HTML.fragment("#1 #{project.full_path}#2 #2") }
-
- it 'returns a Hash containing references grouped per parent paths' do
- expect(described_class).to receive(:object_class).exactly(6).times.and_return(Issue)
-
- refs = filter.references_per_parent
-
- expect(refs).to match(a_hash_including(project.full_path => contain_exactly(1, 2)))
- end
- end
-
describe '#data_attributes_for' do
let_it_be(:issue) { create(:issue, project: project) }
@@ -32,74 +20,6 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
end
end
- describe '#parent_per_reference' do
- it 'returns a Hash containing projects grouped per parent paths' do
- expect(filter).to receive(:references_per_parent)
- .and_return({ project.full_path => Set.new([1]) })
-
- expect(filter.parent_per_reference)
- .to eq({ project.full_path => project })
- end
- end
-
- describe '#find_for_paths' do
- context 'with RequestStore disabled' do
- it 'returns a list of Projects for a list of paths' do
- expect(filter.find_for_paths([project.full_path]))
- .to eq([project])
- end
-
- it "return an empty array for paths that don't exist" do
- expect(filter.find_for_paths(['nonexistent/project']))
- .to eq([])
- end
- end
-
- context 'with RequestStore enabled', :request_store do
- it 'returns a list of Projects for a list of paths' do
- expect(filter.find_for_paths([project.full_path]))
- .to eq([project])
- end
-
- context 'when no project with that path exists' do
- it 'returns no value' do
- expect(filter.find_for_paths(['nonexistent/project']))
- .to eq([])
- end
-
- it 'adds the ref to the project refs cache' do
- project_refs_cache = {}
- allow(filter).to receive(:refs_cache).and_return(project_refs_cache)
-
- filter.find_for_paths(['nonexistent/project'])
-
- expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
- end
-
- context 'when the project refs cache includes nil values' do
- before do
- # adds { 'nonexistent/project' => nil } to cache
- filter.from_ref_cached('nonexistent/project')
- end
-
- it "return an empty array for paths that don't exist" do
- expect(filter.find_for_paths(['nonexistent/project']))
- .to eq([])
- end
- end
- end
- end
- end
-
- describe '#current_parent_path' do
- it 'returns the path of the current parent' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(filter.current_parent_path).to eq(project.full_path)
- end
- end
-
context 'abstract methods' do
describe '#find_object' do
it 'raises NotImplementedError' do
diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
index 0e1cb1ade74..88c2494b243 100644
--- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb
@@ -470,24 +470,6 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
end
end
- describe '#records_per_parent' do
- context 'using an internal issue tracker' do
- it 'returns a Hash containing the issues per project' do
- doc = Nokogiri::HTML.fragment('')
- filter = described_class.new(doc, project: project)
-
- expect(filter).to receive(:parent_per_reference)
- .and_return({ project.full_path => project })
-
- expect(filter).to receive(:references_per_parent)
- .and_return({ project.full_path => Set.new([issue.iid]) })
-
- expect(filter.records_per_parent)
- .to eq({ project => { issue.iid => issue } })
- end
- end
- end
-
describe '.references_in' do
let(:merge_request) { create(:merge_request) }
diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb
new file mode 100644
index 00000000000..2e37e34bba5
--- /dev/null
+++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::References::ReferenceCache do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:issue1) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project) }
+ let_it_be(:issue3) { create(:issue, project: project2) }
+ let_it_be(:doc) { Nokogiri::HTML.fragment("#{issue1.to_reference} #{issue2.to_reference} #{issue3.to_reference(full: true)}") }
+
+ let(:filter_class) { Banzai::Filter::References::IssueReferenceFilter }
+ let(:filter) { filter_class.new(doc, project: project) }
+ let(:cache) { described_class.new(filter, { project: project }) }
+
+ describe '#load_references_per_parent' do
+ it 'loads references grouped per parent paths' do
+ cache.load_references_per_parent(filter.nodes)
+
+ expect(cache.references_per_parent).to eq({ project.full_path => [issue1.iid, issue2.iid].to_set,
+ project2.full_path => [issue3.iid].to_set })
+ end
+ end
+
+ describe '#load_parent_per_reference' do
+ it 'returns a Hash containing projects grouped per parent paths' do
+ cache.load_references_per_parent(filter.nodes)
+ cache.load_parent_per_reference
+
+ expect(cache.parent_per_reference).to match({ project.full_path => project, project2.full_path => project2 })
+ end
+ end
+
+ describe '#load_records_per_parent' do
+ it 'returns a Hash containing projects grouped per parent paths' do
+ cache.load_references_per_parent(filter.nodes)
+ cache.load_parent_per_reference
+ cache.load_records_per_parent
+
+ expect(cache.records_per_parent).to match({ project => { issue1.iid => issue1, issue2.iid => issue2 },
+ project2 => { issue3.iid => issue3 } })
+ end
+ end
+
+ describe '#initialize_reference_cache' do
+ it 'does not have an N+1 query problem with cross projects' do
+ doc_single = Nokogiri::HTML.fragment("#1")
+ filter_single = filter_class.new(doc_single, project: project)
+ cache_single = described_class.new(filter_single, { project: project })
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ cache_single.load_references_per_parent(filter_single.nodes)
+ cache_single.load_parent_per_reference
+ cache_single.load_records_per_parent
+ end.count
+
+ # Since this is an issue filter that is not batching issue queries
+ # across projects, we have to account for that.
+ # 1 for both projects, 1 for issues in each project == 3
+ max_count = control_count + 1
+
+ expect do
+ cache.load_references_per_parent(filter.nodes)
+ cache.load_parent_per_reference
+ cache.load_records_per_parent
+ end.not_to exceed_query_limit(max_count)
+ end
+ end
+
+ describe '#find_for_paths' do
+ context 'with RequestStore disabled' do
+ it 'returns a list of Projects for a list of paths' do
+ expect(cache.find_for_paths([project.full_path]))
+ .to eq([project])
+ end
+
+ it 'return an empty array for paths that do not exist' do
+ expect(cache.find_for_paths(['nonexistent/project']))
+ .to eq([])
+ end
+ end
+
+ context 'with RequestStore enabled', :request_store do
+ it 'returns a list of Projects for a list of paths' do
+ expect(cache.find_for_paths([project.full_path]))
+ .to eq([project])
+ end
+
+ context 'when no project with that path exists' do
+ it 'returns no value' do
+ expect(cache.find_for_paths(['nonexistent/project']))
+ .to eq([])
+ end
+
+ it 'adds the ref to the project refs cache' do
+ project_refs_cache = {}
+ allow(cache).to receive(:refs_cache).and_return(project_refs_cache)
+
+ cache.find_for_paths(['nonexistent/project'])
+
+ expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
+ end
+ end
+ end
+ end
+
+ describe '#current_parent_path' do
+ it 'returns the path of the current parent' do
+ expect(cache.current_parent_path).to eq project.full_path
+ end
+ end
+
+ describe '#current_project_namespace_path' do
+ it 'returns the path of the current project namespace' do
+ expect(cache.current_project_namespace_path).to eq project.namespace.full_path
+ end
+ end
+
+ describe '#full_project_path' do
+ it 'returns current parent path when no ref specified' do
+ expect(cache.full_project_path('something', nil)).to eq cache.current_parent_path
+ end
+
+ it 'returns combined namespace and project ref' do
+ expect(cache.full_project_path('something', 'cool')).to eq 'something/cool'
+ end
+
+ it 'returns uses default namespace and project ref when namespace nil' do
+ expect(cache.full_project_path(nil, 'cool')).to eq "#{project.namespace.full_path}/cool"
+ end
+ end
+
+ describe '#full_group_path' do
+ it 'returns current parent path when no group ref specified' do
+ expect(cache.full_group_path(nil)).to eq cache.current_parent_path
+ end
+
+ it 'returns group ref' do
+ expect(cache.full_group_path('cool_group')).to eq 'cool_group'
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb
index 2d841b7fac2..213fa23675e 100644
--- a/spec/lib/bulk_imports/clients/http_spec.rb
+++ b/spec/lib/bulk_imports/clients/http_spec.rb
@@ -8,66 +8,23 @@ RSpec.describe BulkImports::Clients::Http do
let(:uri) { 'http://gitlab.example' }
let(:token) { 'token' }
let(:resource) { 'resource' }
+ let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
subject { described_class.new(uri: uri, token: token) }
- describe '#get' do
- let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
-
- shared_examples 'performs network request' do
- it 'performs network request' do
- expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double)
-
- subject.get(resource)
- end
- end
-
- describe 'request query' do
- include_examples 'performs network request' do
- let(:expected_args) do
- [
- anything,
- hash_including(
- query: {
- page: described_class::DEFAULT_PAGE,
- per_page: described_class::DEFAULT_PER_PAGE
- }
- )
- ]
- end
- end
- end
-
- describe 'request headers' do
- include_examples 'performs network request' do
- let(:expected_args) do
- [
- anything,
- hash_including(
- headers: {
- 'Content-Type' => 'application/json',
- 'Authorization' => "Bearer #{token}"
- }
- )
- ]
- end
- end
- end
+ shared_examples 'performs network request' do
+ it 'performs network request' do
+ expect(Gitlab::HTTP).to receive(method).with(*expected_args).and_return(response_double)
- describe 'request uri' do
- include_examples 'performs network request' do
- let(:expected_args) do
- ['http://gitlab.example:80/api/v4/resource', anything]
- end
- end
+ subject.public_send(method, resource)
end
context 'error handling' do
context 'when error occurred' do
it 'raises ConnectionError' do
- allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
+ allow(Gitlab::HTTP).to receive(method).and_raise(Errno::ECONNREFUSED)
- expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError)
+ expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError)
end
end
@@ -75,12 +32,34 @@ RSpec.describe BulkImports::Clients::Http do
it 'raises ConnectionError' do
response_double = double(code: 503, success?: false)
- allow(Gitlab::HTTP).to receive(:get).and_return(response_double)
+ allow(Gitlab::HTTP).to receive(method).and_return(response_double)
- expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError)
+ expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError)
end
end
end
+ end
+
+ describe '#get' do
+ let(:method) { :get }
+
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ [
+ 'http://gitlab.example:80/api/v4/resource',
+ hash_including(
+ query: {
+ page: described_class::DEFAULT_PAGE,
+ per_page: described_class::DEFAULT_PER_PAGE
+ },
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{token}"
+ }
+ )
+ ]
+ end
+ end
describe '#each_page' do
let(:objects1) { [{ object: 1 }, { object: 2 }] }
@@ -129,4 +108,23 @@ RSpec.describe BulkImports::Clients::Http do
end
end
end
+
+ describe '#post' do
+ let(:method) { :post }
+
+ include_examples 'performs network request' do
+ let(:expected_args) do
+ [
+ 'http://gitlab.example:80/api/v4/resource',
+ hash_including(
+ body: {},
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Bearer #{token}"
+ }
+ )
+ ]
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb
new file mode 100644
index 00000000000..35928deff82
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsChildren, :migration, schema: 20210506065000 do
+ let(:namespaces_table) { table(:namespaces) }
+
+ let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) }
+ let!(:root_group) { namespaces_table.create!(id: 2, name: 'group', path: 'group', type: 'Group', parent_id: nil) }
+ let!(:sub_group) { namespaces_table.create!(id: 3, name: 'subgroup', path: 'subgroup', type: 'Group', parent_id: 2) }
+
+ describe '#perform' do
+ it 'backfills traversal_ids for child namespaces' do
+ described_class.new.perform(1, 3, 5)
+
+ expect(user_namespace.reload.traversal_ids).to eq([])
+ expect(root_group.reload.traversal_ids).to eq([])
+ expect(sub_group.reload.traversal_ids).to eq([root_group.id, sub_group.id])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb
new file mode 100644
index 00000000000..96e43275972
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsRoots, :migration, schema: 20210506065000 do
+ let(:namespaces_table) { table(:namespaces) }
+
+ let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) }
+ let!(:root_group) { namespaces_table.create!(id: 2, name: 'group', path: 'group', type: 'Group', parent_id: nil) }
+ let!(:sub_group) { namespaces_table.create!(id: 3, name: 'subgroup', path: 'subgroup', type: 'Group', parent_id: 2) }
+
+ describe '#perform' do
+ it 'backfills traversal_ids for root namespaces' do
+ described_class.new.perform(1, 3, 5)
+
+ expect(user_namespace.reload.traversal_ids).to eq([user_namespace.id])
+ expect(root_group.reload.traversal_ids).to eq([root_group.id])
+ expect(sub_group.reload.traversal_ids).to eq([])
+ end
+ end
+end
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
index c6fbd263072..d2d287d8e24 100644
--- a/spec/models/board_group_recent_visit_spec.rb
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardGroupRecentVisit do
- let(:user) { create(:user) }
- let(:group) { create(:group) }
- let(:board) { create(:board, group: group) }
+ let_it_be(:board_parent) { create(:group) }
+ let_it_be(:board) { create(:board, group: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_group_recent_visit, group: group, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, group)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :group }
+ let_it_be(:visit_relation) { :board_group_recent_visit }
end
end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
index 145a4f5b1a7..262c3a8faaa 100644
--- a/spec/models/board_project_recent_visit_spec.rb
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe BoardProjectRecentVisit do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:board) { create(:board, project: project) }
+ let_it_be(:board_parent) { create(:project) }
+ let_it_be(:board) { create(:board, project: board_parent) }
describe 'relationships' do
it { is_expected.to belong_to(:user) }
@@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do
it { is_expected.to validate_presence_of(:board) }
end
- describe '#visited' do
- it 'creates a visit if one does not exists' do
- expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
- end
-
- shared_examples 'was visited previously' do
- let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago }
-
- it 'updates the timestamp' do
- freeze_time do
- described_class.visited!(user, board)
-
- expect(described_class.count).to eq 1
- expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
- end
- end
- end
-
- it_behaves_like 'was visited previously'
-
- context 'when we try to create a visit that is not unique' do
- before do
- expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
- expect(described_class).to receive(:find_or_create_by).and_return(visit)
- end
-
- it_behaves_like 'was visited previously'
- end
- end
-
- describe '#latest' do
- def create_visit(time)
- create :board_project_recent_visit, project: project, user: user, updated_at: time
- end
-
- it 'returns the most recent visited' do
- create_visit(7.days.ago)
- create_visit(5.days.ago)
- recent = create_visit(1.day.ago)
-
- expect(described_class.latest(user, project)).to eq recent
- end
-
- it 'returns last 3 visited boards' do
- create_visit(7.days.ago)
- visit1 = create_visit(3.days.ago)
- visit2 = create_visit(2.days.ago)
- visit3 = create_visit(5.days.ago)
-
- expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3])
- end
+ it_behaves_like 'boards recent visit' do
+ let_it_be(:board_relation) { :board }
+ let_it_be(:board_parent_relation) { :project }
+ let_it_be(:visit_relation) { :board_project_recent_visit }
end
end
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index 652ea431696..d1b7125a6e6 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -125,4 +125,13 @@ RSpec.describe BulkImports::Entity, type: :model do
end
end
end
+
+ describe '#encoded_source_full_path' do
+ it 'encodes entity source full path' do
+ expected = 'foo%2Fbar'
+ entity = build(:bulk_import_entity, source_full_path: 'foo/bar')
+
+ expect(entity.encoded_source_full_path).to eq(expected)
+ end
+ end
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index 5d5351eb9fe..ffdb9fc988c 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -660,27 +660,37 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.to match_array([pypi_package]) }
end
- describe '.displayable' do
+ context 'status scopes' do
let_it_be(:hidden_package) { create(:maven_package, :hidden) }
let_it_be(:processing_package) { create(:maven_package, :processing) }
let_it_be(:error_package) { create(:maven_package, :error) }
- subject { described_class.displayable }
+ describe '.displayable' do
+ subject { described_class.displayable }
- it 'does not include non-displayable packages', :aggregate_failures do
- is_expected.to include(error_package)
- is_expected.not_to include(hidden_package)
- is_expected.not_to include(processing_package)
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.to include(error_package)
+ is_expected.not_to include(hidden_package)
+ is_expected.not_to include(processing_package)
+ end
end
- end
- describe '.with_status' do
- let_it_be(:hidden_package) { create(:maven_package, :hidden) }
+ describe '.installable' do
+ subject { described_class.installable }
- subject { described_class.with_status(:hidden) }
+ it 'does not include non-displayable packages', :aggregate_failures do
+ is_expected.not_to include(error_package)
+ is_expected.not_to include(hidden_package)
+ is_expected.not_to include(processing_package)
+ end
+ end
+
+ describe '.with_status' do
+ subject { described_class.with_status(:hidden) }
- it 'returns packages with specified status' do
- is_expected.to match_array([hidden_package])
+ it 'returns packages with specified status' do
+ is_expected.to match_array([hidden_package])
+ end
end
end
end
diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb
index 64faa2cf07b..8910345d170 100644
--- a/spec/services/boards/visits/create_service_spec.rb
+++ b/spec/services/boards/visits/create_service_spec.rb
@@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do
let(:user) { create(:user) }
context 'when a project board' do
- let(:project) { create(:project) }
- let(:project_board) { create(:board, project: project) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:board) { create(:board, project: project) }
- subject(:service) { described_class.new(project_board.resource_parent, user) }
+ let_it_be(:model) { BoardProjectRecentVisit }
- it 'returns nil when there is no user' do
- service.current_user = nil
-
- expect(service.execute(project_board)).to eq nil
- end
-
- it 'returns nil when database is read-only' do
- allow(Gitlab::Database).to receive(:read_only?) { true }
-
- expect(service.execute(project_board)).to eq nil
- end
-
- it 'records the visit' do
- expect(BoardProjectRecentVisit).to receive(:visited!).once
-
- service.execute(project_board)
- end
+ it_behaves_like 'boards recent visit create service'
end
context 'when a group board' do
- let(:group) { create(:group) }
- let(:group_board) { create(:board, group: group) }
-
- subject(:service) { described_class.new(group_board.resource_parent, user) }
-
- it 'returns nil when there is no user' do
- service.current_user = nil
-
- expect(service.execute(group_board)).to eq nil
- end
-
- it 'records the visit' do
- expect(BoardGroupRecentVisit).to receive(:visited!).once
+ let_it_be(:group) { create(:group) }
+ let_it_be(:board) { create(:board, group: group) }
+ let_it_be(:model) { BoardGroupRecentVisit }
- service.execute(group_board)
- end
+ it_behaves_like 'boards recent visit create service'
end
end
end
diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb
index db758dc6672..1838065c5be 100644
--- a/spec/services/packages/nuget/search_service_spec.rb
+++ b/spec/services/packages/nuget/search_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Packages::Nuget::SearchService do
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: subgroup) }
- let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') }
+ let_it_be_with_refind(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') }
let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') }
let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') }
let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') }
@@ -79,6 +79,16 @@ RSpec.describe Packages::Nuget::SearchService do
it { expect_search_results 4, package_a, packages_b, packages_c, package_d }
end
+ context 'with non-displayable packages' do
+ let(:search_term) { '' }
+
+ before do
+ package_a.update_column(:status, 1)
+ end
+
+ it { expect_search_results 3, packages_b, packages_c, package_d }
+ end
+
context 'with prefix search term' do
let(:search_term) { 'dummy' }
diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb
index 2d4e8d0df1f..b3ec2336cca 100644
--- a/spec/support/shared_examples/finders/packages_shared_examples.rb
+++ b/spec/support/shared_examples/finders/packages_shared_examples.rb
@@ -20,9 +20,11 @@ end
RSpec.shared_examples 'concerning package statuses' do
let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) }
+ let_it_be(:error_package) { create(:maven_package, :error, project: project) }
- context 'hidden packages' do
+ context 'displayable packages' do
it { is_expected.not_to include(hidden_package) }
+ it { is_expected.to include(error_package) }
end
context 'with status param' do
diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
new file mode 100644
index 00000000000..68ea460dabc
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'boards recent visit' do
+ let_it_be(:user) { create(:user) }
+
+ describe '#visited' do
+ it 'creates a visit if one does not exists' do
+ expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1)
+ end
+
+ shared_examples 'was visited previously' do
+ let_it_be(:visit) do
+ create(visit_relation,
+ board_parent_relation => board_parent,
+ board_relation => board,
+ user: user,
+ updated_at: 7.days.ago
+ )
+ end
+
+ it 'updates the timestamp' do
+ freeze_time do
+ described_class.visited!(user, board)
+
+ expect(described_class.count).to eq 1
+ expect(described_class.first.updated_at).to be_like_time(Time.zone.now)
+ end
+ end
+ end
+
+ it_behaves_like 'was visited previously'
+
+ context 'when we try to create a visit that is not unique' do
+ before do
+ expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(described_class).to receive(:find_or_create_by).and_return(visit)
+ end
+
+ it_behaves_like 'was visited previously'
+ end
+ end
+
+ describe '#latest' do
+ def create_visit(time)
+ create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time)
+ end
+
+ it 'returns the most recent visited' do
+ create_visit(7.days.ago)
+ create_visit(5.days.ago)
+ recent = create_visit(1.day.ago)
+
+ expect(described_class.latest(user, board_parent)).to eq recent
+ end
+
+ it 'returns last 3 visited boards' do
+ create_visit(7.days.ago)
+ visit1 = create_visit(3.days.ago)
+ visit2 = create_visit(2.days.ago)
+ visit3 = create_visit(5.days.ago)
+
+ expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb
new file mode 100644
index 00000000000..63b5e3a5a84
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'boards recent visit create service' do
+ let_it_be(:user) { create(:user) }
+
+ subject(:service) { described_class.new(board.resource_parent, user) }
+
+ it 'returns nil when there is no user' do
+ service.current_user = nil
+
+ expect(service.execute(board)).to be_nil
+ end
+
+ it 'returns nil when database is read only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(service.execute(board)).to be_nil
+ end
+
+ it 'records the visit' do
+ expect(model).to receive(:visited!).once
+
+ service.execute(board)
+ end
+end
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb
index 5964ec45563..9119394f250 100644
--- a/spec/workers/bulk_import_worker_spec.rb
+++ b/spec/workers/bulk_import_worker_spec.rb
@@ -69,7 +69,7 @@ RSpec.describe BulkImportWorker do
end
context 'when there are created entities to process' do
- it 'marks a batch of entities as started, enqueues BulkImports::EntityWorker and reenqueues' do
+ it 'marks a batch of entities as started, enqueues EntityWorker, ExportRequestWorker and reenqueues' do
stub_const("#{described_class}::DEFAULT_BATCH_SIZE", 1)
bulk_import = create(:bulk_import, :created)
@@ -78,6 +78,7 @@ RSpec.describe BulkImportWorker do
expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id)
expect(BulkImports::EntityWorker).to receive(:perform_async)
+ expect(BulkImports::ExportRequestWorker).to receive(:perform_async)
subject.perform(bulk_import.id)
diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb
new file mode 100644
index 00000000000..f7838279212
--- /dev/null
+++ b/spec/workers/bulk_imports/export_request_worker_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::ExportRequestWorker do
+ let_it_be(:bulk_import) { create(:bulk_import) }
+ let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) }
+ let_it_be(:entity) { create(:bulk_import_entity, source_full_path: 'foo/bar', bulk_import: bulk_import) }
+
+ let(:response_double) { double(code: 200, success?: true, parsed_response: {}) }
+ let(:job_args) { [entity.id] }
+
+ describe '#perform' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return(response_double)
+ end
+
+ include_examples 'an idempotent worker' do
+ it 'requests relations export' do
+ expected = "/groups/foo%2Fbar/export_relations"
+
+ expect_next_instance_of(BulkImports::Clients::Http) do |client|
+ expect(client).to receive(:post).with(expected).twice
+ end
+
+ perform_multiple(job_args)
+ end
+ end
+ end
+end