Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c33a9adb70
commit
e3042fc5ce
117 changed files with 2043 additions and 671 deletions
|
@ -1 +1 @@
|
||||||
4f0cd9404f31511f5051e49b363adc06aa3ec365
|
30ae36f781ee979330b1f170d81c97c319c2fff1
|
||||||
|
|
|
@ -79,6 +79,7 @@ export default {
|
||||||
:toggle-class="toggleClass"
|
:toggle-class="toggleClass"
|
||||||
:boundary="getBoundaryElement()"
|
:boundary="getBoundaryElement()"
|
||||||
menu-class="dropdown-extended-height"
|
menu-class="dropdown-extended-height"
|
||||||
|
category="tertiary"
|
||||||
no-flip
|
no-flip
|
||||||
right
|
right
|
||||||
lazy
|
lazy
|
||||||
|
|
|
@ -16,13 +16,13 @@ const commentDetailOptions = [
|
||||||
{
|
{
|
||||||
value: 'standard',
|
value: 'standard',
|
||||||
label: s__('Integrations|Standard'),
|
label: s__('Integrations|Standard'),
|
||||||
help: s__('Integrations|Includes commit title and branch'),
|
help: s__('Integrations|Includes commit title and branch.'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'all_details',
|
value: 'all_details',
|
||||||
label: s__('Integrations|All details'),
|
label: s__('Integrations|All details'),
|
||||||
help: s__(
|
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]"
|
label-for="service[trigger]"
|
||||||
:description="
|
:description="
|
||||||
s__(
|
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).',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|
|
@ -278,7 +278,6 @@ export default {
|
||||||
v-if="canResolve"
|
v-if="canResolve"
|
||||||
ref="resolveButton"
|
ref="resolveButton"
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
size="small"
|
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
:variant="resolveVariant"
|
:variant="resolveVariant"
|
||||||
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
|
:class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
|
||||||
|
@ -292,7 +291,7 @@ export default {
|
||||||
<template v-if="canAwardEmoji">
|
<template v-if="canAwardEmoji">
|
||||||
<emoji-picker
|
<emoji-picker
|
||||||
v-if="glFeatures.improvedEmojiPicker"
|
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"
|
@click="setAwardEmoji"
|
||||||
>
|
>
|
||||||
<template #button-content>
|
<template #button-content>
|
||||||
|
@ -305,10 +304,9 @@ export default {
|
||||||
v-else
|
v-else
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
|
: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"
|
category="tertiary"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="small"
|
|
||||||
:title="$options.i18n.addReactionLabel"
|
:title="$options.i18n.addReactionLabel"
|
||||||
:aria-label="$options.i18n.addReactionLabel"
|
:aria-label="$options.i18n.addReactionLabel"
|
||||||
data-position="right"
|
data-position="right"
|
||||||
|
@ -336,7 +334,6 @@ export default {
|
||||||
:title="$options.i18n.editCommentLabel"
|
:title="$options.i18n.editCommentLabel"
|
||||||
:aria-label="$options.i18n.editCommentLabel"
|
:aria-label="$options.i18n.editCommentLabel"
|
||||||
icon="pencil"
|
icon="pencil"
|
||||||
size="small"
|
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button js-note-edit"
|
class="note-action-button js-note-edit"
|
||||||
data-qa-selector="note_edit_button"
|
data-qa-selector="note_edit_button"
|
||||||
|
@ -347,7 +344,6 @@ export default {
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
:title="$options.i18n.deleteCommentLabel"
|
:title="$options.i18n.deleteCommentLabel"
|
||||||
:aria-label="$options.i18n.deleteCommentLabel"
|
:aria-label="$options.i18n.deleteCommentLabel"
|
||||||
size="small"
|
|
||||||
icon="remove"
|
icon="remove"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button js-note-delete"
|
class="note-action-button js-note-delete"
|
||||||
|
@ -360,7 +356,6 @@ export default {
|
||||||
:title="$options.i18n.moreActionsLabel"
|
:title="$options.i18n.moreActionsLabel"
|
||||||
:aria-label="$options.i18n.moreActionsLabel"
|
:aria-label="$options.i18n.moreActionsLabel"
|
||||||
icon="ellipsis_v"
|
icon="ellipsis_v"
|
||||||
size="small"
|
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button more-actions-toggle"
|
class="note-action-button more-actions-toggle"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
|
|
|
@ -22,7 +22,6 @@ export default {
|
||||||
data-track-event="click_button"
|
data-track-event="click_button"
|
||||||
data-track-label="reply_comment_button"
|
data-track-label="reply_comment_button"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
size="small"
|
|
||||||
icon="comment"
|
icon="comment"
|
||||||
:title="$options.i18n.buttonText"
|
:title="$options.i18n.buttonText"
|
||||||
:aria-label="$options.i18n.buttonText"
|
:aria-label="$options.i18n.buttonText"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,6 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||||
import { __ } from '~/locale';
|
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 {
|
export default {
|
||||||
width: {
|
width: {
|
||||||
|
@ -11,6 +15,10 @@ export default {
|
||||||
toggleTxt: __('Collapse'),
|
toggleTxt: __('Collapse'),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
FirstPipelineCard,
|
||||||
|
GettingStartedCard,
|
||||||
|
PipelineConfigReferenceCard,
|
||||||
|
VisualizeAndLintCard,
|
||||||
GlButton,
|
GlButton,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
},
|
},
|
||||||
|
@ -55,7 +63,7 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
aria-live="polite"
|
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"
|
:style="rootStyle"
|
||||||
>
|
>
|
||||||
<gl-button
|
<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="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"
|
:class="buttonClass"
|
||||||
:title="__('Toggle sidebar')"
|
:title="__('Toggle sidebar')"
|
||||||
data-testid="toggleBtn"
|
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">{{
|
<span v-if="isExpanded" class="gl-text-gray-500 gl-mr-3" data-testid="collapse-text">
|
||||||
__('Collapse')
|
{{ __('Collapse') }}
|
||||||
}}</span>
|
</span>
|
||||||
<gl-icon data-testid="toggle-icon" :name="buttonIconName" />
|
<gl-icon data-testid="toggle-icon" :name="buttonIconName" />
|
||||||
</gl-button>
|
</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>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -30,13 +30,19 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
||||||
pipelineEtag,
|
pipelineEtag,
|
||||||
// Add to provide/inject API for static values
|
// Add to provide/inject API for static values
|
||||||
ciConfigPath,
|
ciConfigPath,
|
||||||
|
ciExamplesHelpPagePath,
|
||||||
|
ciHelpPagePath,
|
||||||
defaultBranch,
|
defaultBranch,
|
||||||
emptyStateIllustrationPath,
|
emptyStateIllustrationPath,
|
||||||
|
helpPaths,
|
||||||
lintHelpPagePath,
|
lintHelpPagePath,
|
||||||
|
needsHelpPagePath,
|
||||||
newMergeRequestPath,
|
newMergeRequestPath,
|
||||||
|
pipelinePagePath,
|
||||||
projectFullPath,
|
projectFullPath,
|
||||||
projectPath,
|
projectPath,
|
||||||
projectNamespace,
|
projectNamespace,
|
||||||
|
runnerHelpPagePath,
|
||||||
ymlHelpPagePath,
|
ymlHelpPagePath,
|
||||||
} = el?.dataset;
|
} = el?.dataset;
|
||||||
|
|
||||||
|
@ -80,15 +86,21 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
|
||||||
apolloProvider,
|
apolloProvider,
|
||||||
provide: {
|
provide: {
|
||||||
ciConfigPath,
|
ciConfigPath,
|
||||||
|
ciExamplesHelpPagePath,
|
||||||
|
ciHelpPagePath,
|
||||||
|
configurationPaths,
|
||||||
defaultBranch,
|
defaultBranch,
|
||||||
emptyStateIllustrationPath,
|
emptyStateIllustrationPath,
|
||||||
|
helpPaths,
|
||||||
lintHelpPagePath,
|
lintHelpPagePath,
|
||||||
|
needsHelpPagePath,
|
||||||
newMergeRequestPath,
|
newMergeRequestPath,
|
||||||
|
pipelinePagePath,
|
||||||
projectFullPath,
|
projectFullPath,
|
||||||
projectPath,
|
projectPath,
|
||||||
projectNamespace,
|
projectNamespace,
|
||||||
|
runnerHelpPagePath,
|
||||||
ymlHelpPagePath,
|
ymlHelpPagePath,
|
||||||
configurationPaths,
|
|
||||||
},
|
},
|
||||||
render(h) {
|
render(h) {
|
||||||
return h(PipelineEditorApp);
|
return h(PipelineEditorApp);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
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 MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
|
@ -23,6 +23,8 @@ import {
|
||||||
ROOT_IMAGE_TOOLTIP,
|
ROOT_IMAGE_TOOLTIP,
|
||||||
} from '../../constants/index';
|
} from '../../constants/index';
|
||||||
|
|
||||||
|
import getContainerRepositoryTagsCountQuery from '../../graphql/queries/get_container_repository_tags_count.query.graphql';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DetailsHeader',
|
name: 'DetailsHeader',
|
||||||
components: { GlButton, GlIcon, TitleArea, MetadataItem },
|
components: { GlButton, GlIcon, TitleArea, MetadataItem },
|
||||||
|
@ -35,60 +37,77 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
metadataLoading: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
containerRepository: {},
|
||||||
|
fetchTagsCount: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
containerRepository: {
|
||||||
|
query: getContainerRepositoryTagsCountQuery,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
id: this.image.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
imageDetails() {
|
||||||
|
return { ...this.image, ...this.containerRepository };
|
||||||
|
},
|
||||||
visibilityIcon() {
|
visibilityIcon() {
|
||||||
return this.image?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
|
return this.imageDetails?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
|
||||||
},
|
},
|
||||||
timeAgo() {
|
timeAgo() {
|
||||||
return this.timeFormatted(this.image.updatedAt);
|
return this.timeFormatted(this.imageDetails.updatedAt);
|
||||||
},
|
},
|
||||||
updatedText() {
|
updatedText() {
|
||||||
return sprintf(UPDATED_AT, { time: this.timeAgo });
|
return sprintf(UPDATED_AT, { time: this.timeAgo });
|
||||||
},
|
},
|
||||||
tagCountText() {
|
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() {
|
cleanupTextAndTooltip() {
|
||||||
if (!this.image.project.containerExpirationPolicy?.enabled) {
|
if (!this.imageDetails.project.containerExpirationPolicy?.enabled) {
|
||||||
return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP };
|
return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
[UNSCHEDULED_STATUS]: {
|
[UNSCHEDULED_STATUS]: {
|
||||||
text: sprintf(CLEANUP_UNSCHEDULED_TEXT, {
|
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 },
|
[SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP },
|
||||||
[ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP },
|
[ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP },
|
||||||
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
|
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
|
||||||
}[this.image?.expirationPolicyCleanupStatus];
|
}[this.imageDetails?.expirationPolicyCleanupStatus];
|
||||||
},
|
},
|
||||||
deleteButtonDisabled() {
|
deleteButtonDisabled() {
|
||||||
return this.disabled || !this.image.canDelete;
|
return this.disabled || !this.imageDetails.canDelete;
|
||||||
},
|
},
|
||||||
rootImageTooltip() {
|
rootImageTooltip() {
|
||||||
return !this.image.name ? ROOT_IMAGE_TOOLTIP : '';
|
return !this.imageDetails.name ? ROOT_IMAGE_TOOLTIP : '';
|
||||||
},
|
},
|
||||||
imageName() {
|
imageName() {
|
||||||
return this.image.name || ROOT_IMAGE_TEXT;
|
return this.imageDetails.name || ROOT_IMAGE_TEXT;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<title-area :metadata-loading="metadataLoading">
|
<title-area>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span data-testid="title">
|
<span data-testid="title">
|
||||||
{{ imageName }}
|
{{ imageName }}
|
||||||
|
@ -124,12 +143,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #right-actions>
|
<template #right-actions>
|
||||||
<gl-button
|
<gl-button variant="danger" :disabled="deleteButtonDisabled" @click="$emit('delete')">
|
||||||
v-if="!metadataLoading"
|
|
||||||
variant="danger"
|
|
||||||
:disabled="deleteButtonDisabled"
|
|
||||||
@click="$emit('delete')"
|
|
||||||
>
|
|
||||||
{{ __('Delete image repository') }}
|
{{ __('Delete image repository') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,7 +8,6 @@ query getContainerRepositoryDetails($id: ID!) {
|
||||||
canDelete
|
canDelete
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
tagsCount
|
|
||||||
expirationPolicyStartedAt
|
expirationPolicyStartedAt
|
||||||
expirationPolicyCleanupStatus
|
expirationPolicyCleanupStatus
|
||||||
project {
|
project {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||||
|
|
||||||
query getContainerRepositoryDetails(
|
query getContainerRepositoryTags(
|
||||||
$id: ID!
|
$id: ID!
|
||||||
$first: Int
|
$first: Int
|
||||||
$last: Int
|
$last: Int
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
query getContainerRepositoryTagsCount($id: ID!) {
|
||||||
|
containerRepository(id: $id) {
|
||||||
|
id
|
||||||
|
tagsCount
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,14 +48,11 @@ export default {
|
||||||
mixins: [Tracking.mixin()],
|
mixins: [Tracking.mixin()],
|
||||||
inject: ['breadCrumbState', 'config'],
|
inject: ['breadCrumbState', 'config'],
|
||||||
apollo: {
|
apollo: {
|
||||||
image: {
|
containerRepository: {
|
||||||
query: getContainerRepositoryDetailsQuery,
|
query: getContainerRepositoryDetailsQuery,
|
||||||
variables() {
|
variables() {
|
||||||
return this.queryVariables;
|
return this.queryVariables;
|
||||||
},
|
},
|
||||||
update(data) {
|
|
||||||
return data.containerRepository;
|
|
||||||
},
|
|
||||||
result() {
|
result() {
|
||||||
this.updateBreadcrumb();
|
this.updateBreadcrumb();
|
||||||
},
|
},
|
||||||
|
@ -66,7 +63,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
image: {},
|
containerRepository: {},
|
||||||
itemsToBeDeleted: [],
|
itemsToBeDeleted: [],
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
mutationLoading: false,
|
mutationLoading: false,
|
||||||
|
@ -82,12 +79,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isLoading() {
|
isLoading() {
|
||||||
return this.$apollo.queries.image.loading || this.mutationLoading;
|
return this.$apollo.queries.containerRepository.loading || this.mutationLoading;
|
||||||
},
|
},
|
||||||
showPartialCleanupWarning() {
|
showPartialCleanupWarning() {
|
||||||
return (
|
return (
|
||||||
this.config.showUnfinishedTagCleanupCallout &&
|
this.config.showUnfinishedTagCleanupCallout &&
|
||||||
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
|
this.containerRepository?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
|
||||||
!this.hidePartialCleanupWarning
|
!this.hidePartialCleanupWarning
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -98,13 +95,13 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
pageActionsAreDisabled() {
|
pageActionsAreDisabled() {
|
||||||
return Boolean(this.image?.status);
|
return Boolean(this.containerRepository?.status);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateBreadcrumb() {
|
updateBreadcrumb() {
|
||||||
const name = this.image?.id
|
const name = this.containerRepository?.id
|
||||||
? this.image?.name || ROOT_IMAGE_TEXT
|
? this.containerRepository?.name || ROOT_IMAGE_TEXT
|
||||||
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
|
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
|
||||||
this.breadCrumbState.updateName(name);
|
this.breadCrumbState.updateName(name);
|
||||||
},
|
},
|
||||||
|
@ -164,7 +161,7 @@ export default {
|
||||||
},
|
},
|
||||||
deleteImage() {
|
deleteImage() {
|
||||||
this.deleteImageAlert = true;
|
this.deleteImageAlert = true;
|
||||||
this.itemsToBeDeleted = [{ path: this.image.path }];
|
this.itemsToBeDeleted = [{ path: this.containerRepository.path }];
|
||||||
this.$refs.deleteModal.show();
|
this.$refs.deleteModal.show();
|
||||||
},
|
},
|
||||||
deleteImageError() {
|
deleteImageError() {
|
||||||
|
@ -180,7 +177,7 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-gl-resize-observer="handleResize" class="gl-my-3">
|
<div v-gl-resize-observer="handleResize" class="gl-my-3">
|
||||||
<template v-if="image">
|
<template v-if="containerRepository">
|
||||||
<delete-alert
|
<delete-alert
|
||||||
v-model="deleteAlertType"
|
v-model="deleteAlertType"
|
||||||
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
|
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
|
||||||
|
@ -195,11 +192,11 @@ export default {
|
||||||
@dismiss="dismissPartialCleanupWarning"
|
@dismiss="dismissPartialCleanupWarning"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<status-alert v-if="image.status" :status="image.status" />
|
<status-alert v-if="containerRepository.status" :status="containerRepository.status" />
|
||||||
|
|
||||||
<details-header
|
<details-header
|
||||||
:image="image"
|
v-if="!isLoading"
|
||||||
:metadata-loading="isLoading"
|
:image="containerRepository"
|
||||||
:disabled="pageActionsAreDisabled"
|
:disabled="pageActionsAreDisabled"
|
||||||
@delete="deleteImage"
|
@delete="deleteImage"
|
||||||
/>
|
/>
|
||||||
|
@ -215,7 +212,7 @@ export default {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<delete-image
|
<delete-image
|
||||||
:id="image.id"
|
:id="containerRepository.id"
|
||||||
ref="deleteImage"
|
ref="deleteImage"
|
||||||
use-update-fn
|
use-update-fn
|
||||||
@start="deleteImageIniit"
|
@start="deleteImageIniit"
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
<script>
|
<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 autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||||
import ReviewAppLink from '../review_app_link.vue';
|
import ReviewAppLink from '../review_app_link.vue';
|
||||||
|
|
||||||
|
@ -9,6 +16,7 @@ export default {
|
||||||
GlButtonGroup,
|
GlButtonGroup,
|
||||||
GlDropdown,
|
GlDropdown,
|
||||||
GlDropdownItem,
|
GlDropdownItem,
|
||||||
|
GlIcon,
|
||||||
GlLink,
|
GlLink,
|
||||||
GlSearchBoxByType,
|
GlSearchBoxByType,
|
||||||
ReviewAppLink,
|
ReviewAppLink,
|
||||||
|
@ -71,7 +79,14 @@ export default {
|
||||||
size="small"
|
size="small"
|
||||||
css-class="deploy-link js-deploy-url inline"
|
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-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
|
||||||
<gl-dropdown-item
|
<gl-dropdown-item
|
||||||
v-for="change in filteredChanges"
|
v-for="change in filteredChanges"
|
||||||
|
|
|
@ -50,6 +50,12 @@
|
||||||
|
|
||||||
img.avatar {
|
img.avatar {
|
||||||
margin-right: $gl-padding;
|
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 {
|
.controls {
|
||||||
|
|
|
@ -676,6 +676,7 @@ $system-note-svg-size: 16px;
|
||||||
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
|
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
|
||||||
float: none;
|
float: none;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ module BoardsActions
|
||||||
|
|
||||||
def show
|
def show
|
||||||
# Add / update the board in the recent visits table
|
# 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
|
respond_with_board
|
||||||
end
|
end
|
||||||
|
@ -52,6 +52,10 @@ module BoardsActions
|
||||||
board_klass.to_type
|
board_klass.to_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def board_visit_service
|
||||||
|
Boards::Visits::CreateService
|
||||||
|
end
|
||||||
|
|
||||||
def serializer
|
def serializer
|
||||||
BoardSerializer.new(current_user: current_user)
|
BoardSerializer.new(current_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,6 +4,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||||
include MembershipActions
|
include MembershipActions
|
||||||
include MembersPresentation
|
include MembersPresentation
|
||||||
include SortingHelper
|
include SortingHelper
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
MEMBER_PER_PAGE_LIMIT = 50
|
MEMBER_PER_PAGE_LIMIT = 50
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||||
|
|
||||||
feature_category :authentication_and_authorization
|
feature_category :authentication_and_authorization
|
||||||
|
|
||||||
|
helper_method :can_manage_members?
|
||||||
|
|
||||||
def index
|
def index
|
||||||
preload_max_access
|
preload_max_access
|
||||||
@sort = params[:sort].presence || sort_value_name
|
@sort = params[:sort].presence || sort_value_name
|
||||||
|
@ -29,7 +32,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||||
.new(@group, current_user, params: filter_params)
|
.new(@group, current_user, params: filter_params)
|
||||||
.execute(include_relations: requested_relations)
|
.execute(include_relations: requested_relations)
|
||||||
|
|
||||||
if can_manage_members
|
if can_manage_members?
|
||||||
@skip_groups = @group.related_group_ids
|
@skip_groups = @group.related_group_ids
|
||||||
|
|
||||||
@invited_members = @members.invite
|
@invited_members = @members.invite
|
||||||
|
@ -59,9 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController
|
||||||
current_user.max_access_for_group[@group.id] = @group.max_member_access(current_user)
|
current_user.max_access_for_group[@group.id] = @group.max_member_access(current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_manage_members
|
def can_manage_members?
|
||||||
|
strong_memoize(:can_manage_members) do
|
||||||
can?(current_user, :admin_group_member, @group)
|
can?(current_user, :admin_group_member, @group)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def present_invited_members(invited_members)
|
def present_invited_members(invited_members)
|
||||||
present_members(invited_members
|
present_members(invited_members
|
||||||
|
|
|
@ -9,12 +9,16 @@ module Packages
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def packages_for_project(project)
|
||||||
|
project.packages.installable
|
||||||
|
end
|
||||||
|
|
||||||
def packages_visible_to_user(user, within_group:)
|
def packages_visible_to_user(user, within_group:)
|
||||||
return ::Packages::Package.none unless within_group
|
return ::Packages::Package.none unless within_group
|
||||||
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
|
return ::Packages::Package.none unless Ability.allowed?(user, :read_group, within_group)
|
||||||
|
|
||||||
projects = projects_visible_to_reporters(user, within_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
|
end
|
||||||
|
|
||||||
def projects_visible_to_user(user, within_group:)
|
def projects_visible_to_user(user, within_group:)
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Packages
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
packages_for_group_projects.composer.preload_composer
|
packages_for_group_projects(installable_only: true).composer.preload_composer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ module Packages
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -11,6 +11,7 @@ module Packages
|
||||||
project
|
project
|
||||||
.packages
|
.packages
|
||||||
.generic
|
.generic
|
||||||
|
.installable
|
||||||
.by_name_and_version!(package_name, package_version)
|
.by_name_and_version!(package_name, package_version)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ module Packages
|
||||||
@project
|
@project
|
||||||
.packages
|
.packages
|
||||||
.golang
|
.golang
|
||||||
|
.installable
|
||||||
.with_name(@module_name)
|
.with_name(@module_name)
|
||||||
.with_version(@module_version)
|
.with_version(@module_version)
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,7 @@ module Packages
|
||||||
|
|
||||||
attr_reader :current_user, :group, :params
|
attr_reader :current_user, :group, :params
|
||||||
|
|
||||||
def packages_for_group_projects
|
def packages_for_group_projects(installable_only: false)
|
||||||
packages = ::Packages::Package
|
packages = ::Packages::Package
|
||||||
.including_build_info
|
.including_build_info
|
||||||
.including_project_route
|
.including_project_route
|
||||||
|
@ -32,7 +32,7 @@ module Packages
|
||||||
packages = filter_with_version(packages)
|
packages = filter_with_version(packages)
|
||||||
packages = filter_by_package_type(packages)
|
packages = filter_by_package_type(packages)
|
||||||
packages = filter_by_package_name(packages)
|
packages = filter_by_package_name(packages)
|
||||||
filter_by_status(packages)
|
installable_only ? packages.installable : filter_by_status(packages)
|
||||||
end
|
end
|
||||||
|
|
||||||
def group_projects_visible_to_current_user
|
def group_projects_visible_to_current_user
|
||||||
|
|
|
@ -26,9 +26,9 @@ module Packages
|
||||||
|
|
||||||
def base
|
def base
|
||||||
if @project
|
if @project
|
||||||
packages_for_a_single_project
|
packages_for_project(@project)
|
||||||
elsif @group
|
elsif @group
|
||||||
packages_for_multiple_projects
|
packages_visible_to_user(@current_user, within_group: @group)
|
||||||
else
|
else
|
||||||
::Packages::Package.none
|
::Packages::Package.none
|
||||||
end
|
end
|
||||||
|
@ -40,23 +40,6 @@ module Packages
|
||||||
|
|
||||||
matching_packages
|
matching_packages
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ module Packages
|
||||||
def execute
|
def execute
|
||||||
base.npm
|
base.npm
|
||||||
.with_name(@package_name)
|
.with_name(@package_name)
|
||||||
|
.installable
|
||||||
.last_of_each_version
|
.last_of_each_version
|
||||||
.preload_files
|
.preload_files
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Packages
|
||||||
|
|
||||||
def base
|
def base
|
||||||
if project?
|
if project?
|
||||||
@project_or_group.packages
|
packages_for_project(@project_or_group)
|
||||||
elsif group?
|
elsif group?
|
||||||
packages_visible_to_user(@current_user, within_group: @project_or_group)
|
packages_visible_to_user(@current_user, within_group: @project_or_group)
|
||||||
else
|
else
|
||||||
|
|
|
@ -12,6 +12,7 @@ module Packages
|
||||||
.including_build_info
|
.including_build_info
|
||||||
.including_project_route
|
.including_project_route
|
||||||
.including_tags
|
.including_tags
|
||||||
|
.displayable
|
||||||
.processed
|
.processed
|
||||||
.find(@package_id)
|
.find(@package_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,10 @@ fragment RelatedTreeBaseEpic on Epic {
|
||||||
adminEpic
|
adminEpic
|
||||||
createEpic
|
createEpic
|
||||||
}
|
}
|
||||||
|
descendantWeightSum {
|
||||||
|
closedIssues
|
||||||
|
openedIssues
|
||||||
|
}
|
||||||
descendantCounts {
|
descendantCounts {
|
||||||
__typename
|
__typename
|
||||||
openedEpics
|
openedEpics
|
||||||
|
|
|
@ -12,16 +12,21 @@ module Ci
|
||||||
commit_sha = project.commit ? project.commit.sha : ''
|
commit_sha = project.commit ? project.commit.sha : ''
|
||||||
{
|
{
|
||||||
"ci-config-path": project.ci_config_path_or_default,
|
"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,
|
"commit-sha" => commit_sha,
|
||||||
"default-branch" => project.default_branch,
|
"default-branch" => project.default_branch,
|
||||||
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
|
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
|
||||||
"initial-branch-name": params[:branch_name],
|
"initial-branch-name": params[:branch_name],
|
||||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
"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,
|
"new-merge-request-path" => namespace_project_new_merge_request_path,
|
||||||
"pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
|
"pipeline_etag" => project.commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
|
||||||
|
"pipeline-page-path" => project_pipelines_path(project),
|
||||||
"project-path" => project.path,
|
"project-path" => project.path,
|
||||||
"project-full-path" => project.full_path,
|
"project-full-path" => project.full_path,
|
||||||
"project-namespace" => project.namespace.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')
|
"yml-help-page-path" => help_page_path('ci/yaml/README')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
# Tracks which boards in a specific group a user has visited
|
# Tracks which boards in a specific group a user has visited
|
||||||
class BoardGroupRecentVisit < ApplicationRecord
|
class BoardGroupRecentVisit < ApplicationRecord
|
||||||
|
include BoardRecentVisit
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :group
|
belongs_to :group
|
||||||
belongs_to :board
|
belongs_to :board
|
||||||
|
@ -10,19 +12,9 @@ class BoardGroupRecentVisit < ApplicationRecord
|
||||||
validates :group, presence: true
|
validates :group, presence: true
|
||||||
validates :board, presence: true
|
validates :board, presence: true
|
||||||
|
|
||||||
scope :by_user_group, -> (user, group) { where(user: user, group: group) }
|
scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
|
||||||
|
|
||||||
def self.visited!(user, board)
|
def self.board_parent_relation
|
||||||
visit = find_or_create_by(user: user, group: board.group, board: board)
|
:group
|
||||||
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
|
|
||||||
|
|
||||||
visits.first(count)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
# Tracks which boards in a specific project a user has visited
|
# Tracks which boards in a specific project a user has visited
|
||||||
class BoardProjectRecentVisit < ApplicationRecord
|
class BoardProjectRecentVisit < ApplicationRecord
|
||||||
|
include BoardRecentVisit
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :board
|
belongs_to :board
|
||||||
|
@ -10,19 +12,9 @@ class BoardProjectRecentVisit < ApplicationRecord
|
||||||
validates :project, presence: true
|
validates :project, presence: true
|
||||||
validates :board, presence: true
|
validates :board, presence: true
|
||||||
|
|
||||||
scope :by_user_project, -> (user, project) { where(user: user, project: project) }
|
scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
|
||||||
|
|
||||||
def self.visited!(user, board)
|
def self.board_parent_relation
|
||||||
visit = find_or_create_by(user: user, project: board.project, board: board)
|
:project
|
||||||
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
|
|
||||||
|
|
||||||
visits.first(count)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -68,6 +68,10 @@ class BulkImports::Entity < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def encoded_source_full_path
|
||||||
|
ERB::Util.url_encode(source_full_path)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def validate_parent_is_a_group
|
def validate_parent_is_a_group
|
||||||
|
|
34
app/models/concerns/board_recent_visit.rb
Normal file
34
app/models/concerns/board_recent_visit.rb
Normal file
|
@ -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
|
|
@ -6,6 +6,7 @@ class Packages::Package < ApplicationRecord
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
DISPLAYABLE_STATUSES = [:default, :error].freeze
|
DISPLAYABLE_STATUSES = [:default, :error].freeze
|
||||||
|
INSTALLABLE_STATUSES = [:default].freeze
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :creator, class_name: 'User'
|
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_package_type, ->(package_type) { where(package_type: package_type) }
|
||||||
scope :with_status, ->(status) { where(status: status) }
|
scope :with_status, ->(status) { where(status: status) }
|
||||||
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
|
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
|
||||||
|
scope :installable, -> { with_status(INSTALLABLE_STATUSES) }
|
||||||
scope :including_build_info, -> { includes(pipelines: :user) }
|
scope :including_build_info, -> { includes(pipelines: :user) }
|
||||||
scope :including_project_route, -> { includes(project: { namespace: :route }) }
|
scope :including_project_route, -> { includes(project: { namespace: :route }) }
|
||||||
scope :including_tags, -> { includes(:tags) }
|
scope :including_tags, -> { includes(:tags) }
|
||||||
|
|
|
@ -106,9 +106,8 @@ class JiraService < IssueTrackerService
|
||||||
end
|
end
|
||||||
|
|
||||||
def help
|
def help
|
||||||
"You need to configure Jira before enabling this service. For more details
|
jira_doc_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_url('integration/jira/index.html') }
|
||||||
read the
|
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 }
|
||||||
[Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def title
|
def title
|
||||||
|
|
|
@ -5,13 +5,17 @@ module Boards
|
||||||
class CreateService < Boards::BaseService
|
class CreateService < Boards::BaseService
|
||||||
def execute(board)
|
def execute(board)
|
||||||
return unless current_user && Gitlab::Database.read_write?
|
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)
|
model.visited!(current_user, board)
|
||||||
BoardGroupRecentVisit.visited!(current_user, board)
|
|
||||||
else
|
|
||||||
BoardProjectRecentVisit.visited!(current_user, board)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def model
|
||||||
|
return BoardGroupRecentVisit if parent.is_a?(Group)
|
||||||
|
|
||||||
|
BoardProjectRecentVisit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -103,6 +103,7 @@ module Packages
|
||||||
|
|
||||||
def nuget_packages
|
def nuget_packages
|
||||||
Packages::Package.nuget
|
Packages::Package.nuget
|
||||||
|
.displayable
|
||||||
.has_version
|
.has_version
|
||||||
.without_nuget_temporary_name
|
.without_nuget_temporary_name
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
- add_page_specific_style 'page_bundles/members'
|
- add_page_specific_style 'page_bundles/members'
|
||||||
- page_title _('Group members')
|
- page_title _('Group members')
|
||||||
- can_manage_members = can?(current_user, :admin_group_member, @group)
|
- show_invited_members = can_manage_members? && @invited_members.load.any?
|
||||||
- show_invited_members = can_manage_members && @invited_members.exists?
|
- show_access_requests = can_manage_members? && @requesters.load.any?
|
||||||
- show_access_requests = can_manage_members && @requesters.exists?
|
|
||||||
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
|
- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
|
||||||
|
|
||||||
.js-remove-member-modal
|
.js-remove-member-modal
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
.col-lg-12
|
.col-lg-12
|
||||||
.gl-display-flex.gl-flex-wrap
|
.gl-display-flex.gl-flex-wrap
|
||||||
- if can_manage_members
|
- if can_manage_members?
|
||||||
.gl-w-half.gl-xs-w-full
|
.gl-w-half.gl-xs-w-full
|
||||||
%h4
|
%h4
|
||||||
= _('Group members')
|
= _('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-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') } }
|
.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
|
= 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
|
%hr.gl-mt-4
|
||||||
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
|
||||||
%li.nav-tab{ role: 'presentation' }
|
%li.nav-tab{ role: 'presentation' }
|
||||||
|
@ -42,7 +41,7 @@
|
||||||
%span
|
%span
|
||||||
= _('Members')
|
= _('Members')
|
||||||
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @members.total_count
|
%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
|
%li.nav-item
|
||||||
= link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
|
= link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
|
||||||
%span
|
%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 } }) }
|
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) }
|
||||||
.loading
|
.loading
|
||||||
.gl-spinner.gl-spinner-md
|
.gl-spinner.gl-spinner-md
|
||||||
- if @group.shared_with_group_links.any?
|
- if @group.shared_with_group_links.present?
|
||||||
#tab-groups.tab-pane
|
#tab-groups.tab-pane
|
||||||
.js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
|
.js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
|
||||||
.loading
|
.loading
|
||||||
|
|
|
@ -1836,6 +1836,15 @@
|
||||||
:idempotent:
|
:idempotent:
|
||||||
:tags:
|
:tags:
|
||||||
- :exclude_from_kubernetes
|
- :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
|
- :name: bulk_imports_pipeline
|
||||||
:worker_name: BulkImports::PipelineWorker
|
:worker_name: BulkImports::PipelineWorker
|
||||||
:feature_category: :importers
|
:feature_category: :importers
|
||||||
|
|
|
@ -24,6 +24,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
created_entities.first(next_batch_size).each do |entity|
|
created_entities.first(next_batch_size).each do |entity|
|
||||||
create_pipeline_tracker_for(entity)
|
create_pipeline_tracker_for(entity)
|
||||||
|
|
||||||
|
BulkImports::ExportRequestWorker.perform_async(entity.id)
|
||||||
BulkImports::EntityWorker.perform_async(entity.id)
|
BulkImports::EntityWorker.perform_async(entity.id)
|
||||||
|
|
||||||
entity.start!
|
entity.start!
|
||||||
|
|
33
app/workers/bulk_imports/export_request_worker.rb
Normal file
33
app/workers/bulk_imports/export_request_worker.rb
Normal file
|
@ -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
|
5
changelogs/unreleased/321625-epic_boards-redirect.yml
Normal file
5
changelogs/unreleased/321625-epic_boards-redirect.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Redirect to the last visited epic board
|
||||||
|
merge_request: 61474
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Set traversal_ids for every namespace
|
||||||
|
merge_request: 57318
|
||||||
|
author:
|
||||||
|
type: performance
|
5
changelogs/unreleased/326229-package-displayable.yml
Normal file
5
changelogs/unreleased/326229-package-displayable.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Include installable and/or displayable packages only in package finders
|
||||||
|
merge_request: 59921
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fine tune a few queries found in GroupMembers#index
|
||||||
|
merge_request: 60857
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add relations export request when Bulk Import is initiated
|
||||||
|
merge_request: 61365
|
||||||
|
author:
|
||||||
|
type: changed
|
5
changelogs/unreleased/jira-form-copy-updates.yml
Normal file
5
changelogs/unreleased/jira-form-copy-updates.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Improve field descriptions in the Jira integration form
|
||||||
|
merge_request: 61205
|
||||||
|
author:
|
||||||
|
type: changed
|
5
changelogs/unreleased/make-comment-actions-larger.yml
Normal file
5
changelogs/unreleased/make-comment-actions-larger.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Increase note actions target size
|
||||||
|
merge_request: 59776
|
||||||
|
author:
|
||||||
|
type: changed
|
6
changelogs/unreleased/review-app-button-styles.yml
Normal file
6
changelogs/unreleased/review-app-button-styles.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Remove extra padding and margin from merge request widget review app dropdown
|
||||||
|
chevron
|
||||||
|
merge_request: 61302
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -56,6 +56,8 @@
|
||||||
- 1
|
- 1
|
||||||
- - bulk_imports_entity
|
- - bulk_imports_entity
|
||||||
- 1
|
- 1
|
||||||
|
- - bulk_imports_export_request
|
||||||
|
- 1
|
||||||
- - bulk_imports_pipeline
|
- - bulk_imports_pipeline
|
||||||
- 1
|
- 1
|
||||||
- - bulk_imports_relation_export
|
- - bulk_imports_relation_export
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
1
db/schema_migrations/20210506065000
Normal file
1
db/schema_migrations/20210506065000
Normal file
|
@ -0,0 +1 @@
|
||||||
|
d286628cce50c469afe899d5ac40f20df8dceb6ee10c6cf49c64fbaeea7e4a2e
|
1
db/schema_migrations/20210511104929
Normal file
1
db/schema_migrations/20210511104929
Normal file
|
@ -0,0 +1 @@
|
||||||
|
7c2a036033a3f6a3f80755c8ce4a0deab5933084974af4d87e7b97cc446fcbda
|
1
db/schema_migrations/20210511104930
Normal file
1
db/schema_migrations/20210511104930
Normal file
|
@ -0,0 +1 @@
|
||||||
|
51a8eeb8919e3f59579885b9e316ba8116566ae9b363b5dd750a65f42503c391
|
|
@ -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;
|
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 (
|
CREATE TABLE boards_epic_boards (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
hide_backlog_list boolean DEFAULT false 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_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_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);
|
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
|
ALTER TABLE ONLY boards_epic_board_positions
|
||||||
ADD CONSTRAINT boards_epic_board_positions_pkey PRIMARY KEY (id);
|
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
|
ALTER TABLE ONLY boards_epic_boards
|
||||||
ADD CONSTRAINT boards_epic_boards_pkey PRIMARY KEY (id);
|
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_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_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);
|
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_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 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);
|
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
|
ALTER TABLE ONLY packages_pypi_metadata
|
||||||
ADD CONSTRAINT fk_rails_9698717cdd FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY packages_dependency_links
|
||||||
ADD CONSTRAINT fk_rails_96ef1c00d3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY merge_request_user_mentions
|
||||||
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY ci_job_artifacts
|
||||||
ADD CONSTRAINT fk_rails_c5137cb2c1 FOREIGN KEY (job_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY namespace_package_settings
|
||||||
ADD CONSTRAINT fk_rails_e773444769 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY dast_site_tokens
|
||||||
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_rails_e84f721a8e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -288,7 +288,8 @@ Example response:
|
||||||
"state": "active",
|
"state": "active",
|
||||||
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
||||||
"web_url": "http://192.168.1.8:3000/root",
|
"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,
|
"id": 2,
|
||||||
|
@ -298,7 +299,8 @@ Example response:
|
||||||
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
||||||
"web_url": "http://192.168.1.8:3000/root",
|
"web_url": "http://192.168.1.8:3000/root",
|
||||||
"email": "john@example.com",
|
"email": "john@example.com",
|
||||||
"last_activity_on": "2021-01-25"
|
"last_activity_on": "2021-01-25",
|
||||||
|
"membership_type": "group_member"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
|
@ -307,7 +309,8 @@ Example response:
|
||||||
"state": "active",
|
"state": "active",
|
||||||
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
|
||||||
"web_url": "http://192.168.1.8:3000/root",
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
|
@ -17,7 +17,12 @@ module Banzai
|
||||||
return context[:project] || context[:group] unless ref
|
return context[:project] || context[:group] unless ref
|
||||||
return context[:project] if context[:project]&.full_path == ref
|
return context[:project] if context[:project]&.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)
|
Project.find_by_full_path(ref)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,12 @@ module Banzai
|
||||||
class AbstractReferenceFilter < ReferenceFilter
|
class AbstractReferenceFilter < ReferenceFilter
|
||||||
include CrossProjectReference
|
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_PLACEHOLDER is used for re-escaping HTML text except found
|
||||||
# reference (which we replace with placeholder during re-scaping). The
|
# reference (which we replace with placeholder during re-scaping). The
|
||||||
# random number helps ensure it's pretty close to unique. Since it's a
|
# random number helps ensure it's pretty close to unique. Since it's a
|
||||||
|
@ -112,6 +118,8 @@ module Banzai
|
||||||
def call
|
def call
|
||||||
return doc unless project || group || user
|
return doc unless project || group || user
|
||||||
|
|
||||||
|
reference_cache.load_reference_cache(nodes) if respond_to?(:parent_records)
|
||||||
|
|
||||||
ref_pattern = object_reference_pattern
|
ref_pattern = object_reference_pattern
|
||||||
link_pattern = object_class.link_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)
|
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
|
||||||
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
|
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
|
||||||
parent_path = if parent_type == :group
|
parent_path = if parent_type == :group
|
||||||
full_group_path(namespace_ref)
|
reference_cache.full_group_path(namespace_ref)
|
||||||
else
|
else
|
||||||
full_project_path(namespace_ref, project_ref)
|
reference_cache.full_project_path(namespace_ref, project_ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
parent = from_ref_cached(parent_path)
|
parent = from_ref_cached(parent_path)
|
||||||
|
@ -263,127 +271,6 @@ module Banzai
|
||||||
text
|
text
|
||||||
end
|
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
|
def parent_type
|
||||||
:project
|
:project
|
||||||
end
|
end
|
||||||
|
@ -392,11 +279,9 @@ module Banzai
|
||||||
parent_type == :project ? project : group
|
parent_type == :project ? project : group
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_group_path(group_ref)
|
private
|
||||||
return current_parent_path unless group_ref
|
|
||||||
|
|
||||||
group_ref
|
attr_accessor :reference_cache
|
||||||
end
|
|
||||||
|
|
||||||
def escape_with_placeholders(text, placeholder_data)
|
def escape_with_placeholders(text, placeholder_data)
|
||||||
escaped = escape_html_entities(text)
|
escaped = escape_html_entities(text)
|
||||||
|
@ -409,5 +294,3 @@ module Banzai
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ module Banzai
|
||||||
def find_object(project, id)
|
def find_object(project, id)
|
||||||
return unless project.is_a?(Project) && project.valid_repo?
|
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
|
record
|
||||||
end
|
end
|
||||||
|
@ -28,7 +28,7 @@ module Banzai
|
||||||
return [] unless noteable.is_a?(MergeRequest)
|
return [] unless noteable.is_a?(MergeRequest)
|
||||||
|
|
||||||
@referenced_merge_request_commit_shas ||= begin
|
@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|
|
noteable.all_commit_shas.select do |sha|
|
||||||
referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
|
referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
|
||||||
end
|
end
|
||||||
|
@ -66,12 +66,12 @@ module Banzai
|
||||||
extras
|
extras
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parent_records(parent, ids)
|
def parent_records(parent, ids)
|
||||||
parent.commits_by(oids: ids.to_a)
|
parent.commits_by(oids: ids.to_a)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def noteable
|
def noteable
|
||||||
context[:noteable]
|
context[:noteable]
|
||||||
end
|
end
|
||||||
|
|
|
@ -36,7 +36,7 @@ module Banzai
|
||||||
self.object_class = ::DesignManagement::Design
|
self.object_class = ::DesignManagement::Design
|
||||||
|
|
||||||
def find_object(project, identifier)
|
def find_object(project, identifier)
|
||||||
records_per_parent[project][identifier]
|
reference_cache.records_per_parent[project][identifier]
|
||||||
end
|
end
|
||||||
|
|
||||||
def parent_records(project, identifiers)
|
def parent_records(project, identifiers)
|
||||||
|
@ -59,15 +59,6 @@ module Banzai
|
||||||
super.includes(:route, :namespace, :group)
|
super.includes(:route, :namespace, :group)
|
||||||
end
|
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)
|
def url_for_object(design, project)
|
||||||
path_options = { vueroute: design.filename }
|
path_options = { vueroute: design.filename }
|
||||||
Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
|
Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
|
||||||
|
|
|
@ -9,11 +9,7 @@ module Banzai
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_object(parent, iid)
|
def find_object(parent, iid)
|
||||||
records_per_parent[parent][iid]
|
reference_cache.records_per_parent[parent][iid]
|
||||||
end
|
|
||||||
|
|
||||||
def parent_from_ref(ref)
|
|
||||||
parent_per_reference[ref || current_parent_path]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ module Banzai
|
||||||
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
|
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
|
||||||
namespace = $~[:namespace]
|
namespace = $~[:namespace]
|
||||||
project = $~[:project]
|
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])
|
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
|
||||||
|
|
||||||
if label
|
if label
|
||||||
|
@ -93,7 +93,7 @@ module Banzai
|
||||||
parent = project || group
|
parent = project || group
|
||||||
|
|
||||||
if project || full_path_ref?(matches)
|
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)
|
parent_from_ref = from_ref_cached(project_path)
|
||||||
reference = parent_from_ref.to_human_reference(parent)
|
reference = parent_from_ref.to_human_reference(parent)
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ module Banzai
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
|
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
|
# Returns group if project is not found by path
|
||||||
parent = parent_from_ref(project_path)
|
parent = parent_from_ref(project_path)
|
||||||
|
|
178
lib/banzai/filter/references/reference_cache.rb
Normal file
178
lib/banzai/filter/references/reference_cache.rb
Normal file
|
@ -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')
|
|
@ -97,6 +97,18 @@ module Banzai
|
||||||
@nodes ||= each_node.to_a
|
@nodes ||= each_node.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def object_class
|
||||||
|
self.class.object_class
|
||||||
|
end
|
||||||
|
|
||||||
|
def project
|
||||||
|
context[:project]
|
||||||
|
end
|
||||||
|
|
||||||
|
def group
|
||||||
|
context[:group]
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Returns a data attribute String to attach to a reference link
|
# Returns a data attribute String to attach to a reference link
|
||||||
|
@ -141,14 +153,6 @@ module Banzai
|
||||||
needs :project unless skip_project_check?
|
needs :project unless skip_project_check?
|
||||||
end
|
end
|
||||||
|
|
||||||
def project
|
|
||||||
context[:project]
|
|
||||||
end
|
|
||||||
|
|
||||||
def group
|
|
||||||
context[:group]
|
|
||||||
end
|
|
||||||
|
|
||||||
def user
|
def user
|
||||||
context[:user]
|
context[:user]
|
||||||
end
|
end
|
||||||
|
@ -216,10 +220,6 @@ module Banzai
|
||||||
node.is_a?(Nokogiri::XML::Element)
|
node.is_a?(Nokogiri::XML::Element)
|
||||||
end
|
end
|
||||||
|
|
||||||
def object_class
|
|
||||||
self.class.object_class
|
|
||||||
end
|
|
||||||
|
|
||||||
def object_reference_pattern
|
def object_reference_pattern
|
||||||
@object_reference_pattern ||= object_class.reference_pattern
|
@object_reference_pattern ||= object_class.reference_pattern
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,6 +28,17 @@ module BulkImports
|
||||||
end
|
end
|
||||||
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)
|
def each_page(method, resource, query = {}, &block)
|
||||||
return to_enum(__method__, method, resource, query) unless block_given?
|
return to_enum(__method__, method, resource, query) unless block_given?
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -3289,6 +3289,9 @@ msgstr ""
|
||||||
msgid "Allowed Geo IP"
|
msgid "Allowed Geo IP"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Allowed characters: +, 0-9, -, and spaces."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Allowed email domain restriction only permitted for top-level groups"
|
msgid "Allowed email domain restriction only permitted for top-level groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -8564,6 +8567,9 @@ msgstr ""
|
||||||
msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
|
msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ContainerRegistry|-- tags"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "ContainerRegistry|Build an image"
|
msgid "ContainerRegistry|Build an image"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17586,10 +17592,10 @@ msgstr ""
|
||||||
msgid "Integrations|Failed to unlink namespace. Please try again."
|
msgid "Integrations|Failed to unlink namespace. Please try again."
|
||||||
msgstr ""
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|Includes commit title and branch"
|
msgid "Integrations|Includes commit title and branch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
|
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"
|
msgid "Integrations|Use default settings"
|
||||||
msgstr ""
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|You can now close this window and return to the GitLab for Jira application."
|
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."
|
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 ""
|
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 ;"
|
msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -18657,6 +18666,18 @@ msgstr ""
|
||||||
msgid "Job was retried"
|
msgid "Job was retried"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JobName|build-job"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JobName|deploy-app"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JobName|lint-test"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "JobName|unit-test"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Jobs"
|
msgid "Jobs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -23812,6 +23833,54 @@ msgstr ""
|
||||||
msgid "PipelineCharts|Total:"
|
msgid "PipelineCharts|Total:"
|
||||||
msgstr ""
|
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."
|
msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -30512,6 +30581,15 @@ msgstr ""
|
||||||
msgid "Stage removed"
|
msgid "Stage removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "StageName|Build"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "StageName|Deploy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "StageName|Test"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Standard"
|
msgid "Standard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ RSpec.describe Groups::GroupMembersController do
|
||||||
expect(response).to render_template(:index)
|
expect(response).to render_template(:index)
|
||||||
end
|
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) }
|
let_it_be(:invited) { create_list(:group_member, 3, :invited, group: group) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
@ -71,6 +71,19 @@ RSpec.describe Groups::GroupMembersController do
|
||||||
end
|
end
|
||||||
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
|
context 'when user has owner access to subgroup' do
|
||||||
let_it_be(:nested_group) { create(:group, parent: group) }
|
let_it_be(:nested_group) { create(:group, parent: group) }
|
||||||
let_it_be(:nested_group_user) { create(:user) }
|
let_it_be(:nested_group_user) { create(:user) }
|
||||||
|
|
|
@ -3,6 +3,30 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ::Packages::FinderHelper do
|
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
|
describe '#packages_visible_to_user' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
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(:subgroup) { create(:group, parent: group) }
|
||||||
let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
|
let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
|
||||||
let_it_be(:package2) { create(:package, project: project2) }
|
let_it_be(:package2) { create(:package, project: project2) }
|
||||||
|
let_it_be(:package3) { create(:package, :error, project: project2) }
|
||||||
|
|
||||||
let(:finder_class) do
|
let(:finder_class) do
|
||||||
Class.new do
|
Class.new do
|
||||||
|
|
25
spec/finders/packages/composer/packages_finder_spec.rb
Normal file
25
spec/finders/packages/composer/packages_finder_spec.rb
Normal file
|
@ -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
|
|
@ -11,7 +11,8 @@ RSpec.describe ::Packages::Conan::PackageFinder do
|
||||||
|
|
||||||
subject { described_class.new(user, query: query).execute }
|
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_project) { create(:project, :private) }
|
||||||
let!(:non_visible_conan_package) { create(:conan_package, project: non_visible_project) }
|
let!(:non_visible_conan_package) { create(:conan_package, project: non_visible_project) }
|
||||||
let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" }
|
let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" }
|
||||||
|
|
|
@ -23,6 +23,13 @@ RSpec.describe ::Packages::Generic::PackageFinder do
|
||||||
expect(found_package).to eq(package)
|
expect(found_package).to eq(package)
|
||||||
end
|
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
|
it 'raises ActiveRecord::RecordNotFound if package is not found' do
|
||||||
expect { finder.execute!(package.name, '3.1.4') }
|
expect { finder.execute!(package.name, '3.1.4') }
|
||||||
.to raise_error(ActiveRecord::RecordNotFound)
|
.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Packages::Go::PackageFinder do
|
||||||
|
|
||||||
let_it_be(:mod) { create :go_module, project: project }
|
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(: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) }
|
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) }
|
it { is_expected.to eq(package) }
|
||||||
end
|
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
|
context 'with an invalid name' do
|
||||||
let(:mod_name) { 'foo/bar' }
|
let(:mod_name) { 'foo/bar' }
|
||||||
let(:version_name) { 'baz' }
|
let(:version_name) { 'baz' }
|
||||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:project) { create(:project, namespace: 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_path) { nil }
|
||||||
let(:param_project) { nil }
|
let(:param_project) { nil }
|
||||||
|
@ -36,6 +36,16 @@ RSpec.describe ::Packages::Maven::PackageFinder do
|
||||||
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'within the project' do
|
context 'within the project' do
|
||||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ::Packages::Npm::PackageFinder do
|
RSpec.describe ::Packages::Npm::PackageFinder do
|
||||||
let_it_be_with_reload(:project) { create(:project)}
|
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(:project) { package.project }
|
||||||
let(:package_name) { package.name }
|
let(:package_name) { package.name }
|
||||||
|
@ -46,6 +46,14 @@ RSpec.describe ::Packages::Npm::PackageFinder do
|
||||||
|
|
||||||
it { is_expected.to be_empty }
|
it { is_expected.to be_empty }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with an uninstallable package' do
|
||||||
|
before do
|
||||||
|
package.update_column(:status, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_empty }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { finder.execute }
|
subject { finder.execute }
|
||||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Packages::Nuget::PackageFinder do
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:subgroup) { create(:group, parent: group) }
|
let_it_be(:subgroup) { create(:group, parent: group) }
|
||||||
let_it_be(:project) { create(:project, namespace: subgroup) }
|
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(: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(: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) }
|
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 }
|
it { is_expected.to be_empty }
|
||||||
end
|
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
|
context 'with valid version' do
|
||||||
let(:package_version) { '2.0.0' }
|
let(:package_version) { '2.0.0' }
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ::Packages::PackageFinder do
|
RSpec.describe ::Packages::PackageFinder do
|
||||||
let_it_be(:project) { create(:project) }
|
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
|
describe '#execute' do
|
||||||
let(:package_id) { maven_package.id }
|
let(:package_id) { maven_package.id }
|
||||||
|
@ -13,6 +13,16 @@ RSpec.describe ::Packages::PackageFinder do
|
||||||
|
|
||||||
it { is_expected.to eq(maven_package) }
|
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
|
context 'processing packages' do
|
||||||
let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
|
let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
|
||||||
let(:package_id) { nuget_package.id }
|
let(:package_id) { nuget_package.id }
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,9 @@
|
||||||
|
import { GlButton } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
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';
|
import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
|
||||||
|
|
||||||
describe('Pipeline editor drawer', () => {
|
describe('Pipeline editor drawer', () => {
|
||||||
|
@ -8,7 +13,12 @@ describe('Pipeline editor drawer', () => {
|
||||||
wrapper = shallowMount(PipelineEditorDrawer);
|
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 findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]');
|
||||||
const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
|
const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]');
|
||||||
const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
|
const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]');
|
||||||
|
@ -24,7 +34,7 @@ describe('Pipeline editor drawer', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('show the left facing arrow icon', () => {
|
it('shows the left facing arrow icon', () => {
|
||||||
expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
|
expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,7 +61,7 @@ describe('Pipeline editor drawer', () => {
|
||||||
await clickToggleBtn();
|
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');
|
expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,10 +69,17 @@ describe('Pipeline editor drawer', () => {
|
||||||
expect(findCollapseText().exists()).toBe(true);
|
expect(findCollapseText().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('show the drawer content', () => {
|
it('shows the drawer content', () => {
|
||||||
expect(findDrawerContent().exists()).toBe(true);
|
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 () => {
|
it('can close the drawer by clicking on the toggle button', async () => {
|
||||||
expect(findDrawerContent().exists()).toBe(true);
|
expect(findDrawerContent().exists()).toBe(true);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,10 @@
|
||||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
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 { useFakeDate } from 'helpers/fake_date';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
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 component from '~/registry/explorer/components/details_page/details_header.vue';
|
||||||
import {
|
import {
|
||||||
UNSCHEDULED_STATUS,
|
UNSCHEDULED_STATUS,
|
||||||
|
@ -16,15 +19,18 @@ import {
|
||||||
ROOT_IMAGE_TEXT,
|
ROOT_IMAGE_TEXT,
|
||||||
ROOT_IMAGE_TOOLTIP,
|
ROOT_IMAGE_TOOLTIP,
|
||||||
} from '~/registry/explorer/constants';
|
} 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 TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||||
|
import { imageTagsCountMock } from '../../mock_data';
|
||||||
|
|
||||||
describe('Details Header', () => {
|
describe('Details Header', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let apolloProvider;
|
||||||
|
let localVue;
|
||||||
|
|
||||||
const defaultImage = {
|
const defaultImage = {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
updatedAt: '2020-11-03T13:29:21Z',
|
updatedAt: '2020-11-03T13:29:21Z',
|
||||||
tagsCount: 10,
|
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
project: {
|
project: {
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
|
@ -51,12 +57,31 @@ describe('Details Header', () => {
|
||||||
await wrapper.vm.$nextTick();
|
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, {
|
wrapper = shallowMount(component, {
|
||||||
|
localVue,
|
||||||
|
apolloProvider,
|
||||||
propsData,
|
propsData,
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: createMockDirective(),
|
GlTooltip: createMockDirective(),
|
||||||
},
|
},
|
||||||
|
mocks,
|
||||||
stubs: {
|
stubs: {
|
||||||
TitleArea,
|
TitleArea,
|
||||||
},
|
},
|
||||||
|
@ -64,41 +89,48 @@ describe('Details Header', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
// if we want to mix createMockApollo and manual mocks we need to reset everything
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
apolloProvider = undefined;
|
||||||
|
localVue = undefined;
|
||||||
wrapper = null;
|
wrapper = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('image name', () => {
|
describe('image name', () => {
|
||||||
describe('missing image name', () => {
|
describe('missing image name', () => {
|
||||||
it('root image ', () => {
|
beforeEach(() => {
|
||||||
mountComponent({ image: { ...defaultImage, name: '' } });
|
mountComponent({ propsData: { image: { ...defaultImage, name: '' } } });
|
||||||
|
|
||||||
|
return waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('root image ', () => {
|
||||||
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
|
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has an icon', () => {
|
it('has an icon', () => {
|
||||||
mountComponent({ image: { ...defaultImage, name: '' } });
|
|
||||||
|
|
||||||
expect(findInfoIcon().exists()).toBe(true);
|
expect(findInfoIcon().exists()).toBe(true);
|
||||||
expect(findInfoIcon().props('name')).toBe('information-o');
|
expect(findInfoIcon().props('name')).toBe('information-o');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a tooltip', () => {
|
it('has a tooltip', () => {
|
||||||
mountComponent({ image: { ...defaultImage, name: '' } });
|
|
||||||
|
|
||||||
const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip');
|
const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip');
|
||||||
expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP);
|
expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with image name present', () => {
|
describe('with image name present', () => {
|
||||||
it('shows image.name ', () => {
|
beforeEach(() => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
return waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows image.name ', () => {
|
||||||
expect(findTitle().text()).toContain('foo');
|
expect(findTitle().text()).toContain('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has no icon', () => {
|
it('has no icon', () => {
|
||||||
mountComponent();
|
|
||||||
|
|
||||||
expect(findInfoIcon().exists()).toBe(false);
|
expect(findInfoIcon().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -111,12 +143,6 @@ describe('Details Header', () => {
|
||||||
expect(findDeleteButton().exists()).toBe(true);
|
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', () => {
|
it('has the correct text', () => {
|
||||||
mountComponent();
|
mountComponent();
|
||||||
|
|
||||||
|
@ -149,7 +175,7 @@ describe('Details Header', () => {
|
||||||
`(
|
`(
|
||||||
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
|
'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled',
|
||||||
({ canDelete, disabled, isDisabled }) => {
|
({ canDelete, disabled, isDisabled }) => {
|
||||||
mountComponent({ image: { ...defaultImage, canDelete }, disabled });
|
mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } });
|
||||||
|
|
||||||
expect(findDeleteButton().props('disabled')).toBe(isDisabled);
|
expect(findDeleteButton().props('disabled')).toBe(isDisabled);
|
||||||
},
|
},
|
||||||
|
@ -158,15 +184,32 @@ describe('Details Header', () => {
|
||||||
|
|
||||||
describe('metadata items', () => {
|
describe('metadata items', () => {
|
||||||
describe('tags count', () => {
|
describe('tags count', () => {
|
||||||
it('when there is more than one tag has the correct text', async () => {
|
it('displays "-- tags" while loading', async () => {
|
||||||
mountComponent();
|
// 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();
|
await waitForMetadataItems();
|
||||||
|
|
||||||
expect(findTagsCount().props('text')).toBe('10 tags');
|
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('13 tags');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when there is one tag has the correct text', async () => {
|
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();
|
await waitForMetadataItems();
|
||||||
|
|
||||||
expect(findTagsCount().props('text')).toBe('1 tag');
|
expect(findTagsCount().props('text')).toBe('1 tag');
|
||||||
|
@ -208,6 +251,7 @@ describe('Details Header', () => {
|
||||||
'when the status is $status the text is $text and the tooltip is $tooltip',
|
'when the status is $status the text is $text and the tooltip is $tooltip',
|
||||||
async ({ status, text, tooltip }) => {
|
async ({ status, text, tooltip }) => {
|
||||||
mountComponent({
|
mountComponent({
|
||||||
|
propsData: {
|
||||||
image: {
|
image: {
|
||||||
...defaultImage,
|
...defaultImage,
|
||||||
expirationPolicyCleanupStatus: status,
|
expirationPolicyCleanupStatus: status,
|
||||||
|
@ -215,6 +259,7 @@ describe('Details Header', () => {
|
||||||
containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
|
containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
await waitForMetadataItems();
|
await waitForMetadataItems();
|
||||||
|
|
||||||
|
@ -242,7 +287,9 @@ describe('Details Header', () => {
|
||||||
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
|
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
|
||||||
});
|
});
|
||||||
it('shows an eye slashed when the project is not public', async () => {
|
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();
|
await waitForMetadataItems();
|
||||||
|
|
||||||
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
|
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
|
||||||
|
|
|
@ -113,7 +113,6 @@ export const containerRepositoryMock = {
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
createdAt: '2020-11-03T13:29:21Z',
|
createdAt: '2020-11-03T13:29:21Z',
|
||||||
updatedAt: '2020-11-03T13:29:21Z',
|
updatedAt: '2020-11-03T13:29:21Z',
|
||||||
tagsCount: 13,
|
|
||||||
expirationPolicyStartedAt: null,
|
expirationPolicyStartedAt: null,
|
||||||
expirationPolicyCleanupStatus: 'UNSCHEDULED',
|
expirationPolicyCleanupStatus: 'UNSCHEDULED',
|
||||||
project: {
|
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) => ({
|
export const graphQLImageDetailsMock = (override) => ({
|
||||||
data: {
|
data: {
|
||||||
containerRepository: {
|
containerRepository: {
|
||||||
|
|
|
@ -292,7 +292,6 @@ describe('Details Page', () => {
|
||||||
|
|
||||||
await waitForApolloRequestRender();
|
await waitForApolloRequestRender();
|
||||||
expect(findDetailsHeader().props()).toMatchObject({
|
expect(findDetailsHeader().props()).toMatchObject({
|
||||||
metadataLoading: false,
|
|
||||||
image: {
|
image: {
|
||||||
name: containerRepositoryMock.name,
|
name: containerRepositoryMock.name,
|
||||||
project: {
|
project: {
|
||||||
|
|
|
@ -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 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 ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
|
||||||
import { deploymentMockData } from './deployment_mock_data';
|
import { deploymentMockData } from './deployment_mock_data';
|
||||||
|
@ -11,14 +12,14 @@ const appButtonText = {
|
||||||
describe('Deployment View App button', () => {
|
describe('Deployment View App button', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
const factory = (options = {}) => {
|
const createComponent = (options = {}) => {
|
||||||
wrapper = mount(DeploymentViewButton, {
|
wrapper = mountExtended(DeploymentViewButton, {
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
factory({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
deployment: deploymentMockData,
|
deployment: deploymentMockData,
|
||||||
appButtonText,
|
appButtonText,
|
||||||
|
@ -30,15 +31,21 @@ describe('Deployment View App button', () => {
|
||||||
wrapper.destroy();
|
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', () => {
|
describe('text', () => {
|
||||||
it('renders text as passed', () => {
|
it('renders text as passed', () => {
|
||||||
expect(wrapper.find(ReviewAppLink).text()).toContain(appButtonText.text);
|
expect(findReviewAppLink().props().display.text).toBe(appButtonText.text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('without changes', () => {
|
describe('without changes', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
factory({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
deployment: { ...deploymentMockData, changes: null },
|
deployment: { ...deploymentMockData, changes: null },
|
||||||
appButtonText,
|
appButtonText,
|
||||||
|
@ -47,13 +54,13 @@ describe('Deployment View App button', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the link to the review app without dropdown', () => {
|
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', () => {
|
describe('with a single change', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
factory({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
|
deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
|
||||||
appButtonText,
|
appButtonText,
|
||||||
|
@ -62,21 +69,20 @@ describe('Deployment View App button', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the link to the review app without dropdown', () => {
|
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', () => {
|
it('renders the link to the review app linked to to the first change', () => {
|
||||||
const expectedUrl = deploymentMockData.changes[0].external_url;
|
const expectedUrl = deploymentMockData.changes[0].external_url;
|
||||||
const deployUrl = wrapper.find('.js-deploy-url');
|
|
||||||
|
|
||||||
expect(deployUrl.attributes().href).not.toBeNull();
|
expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
|
||||||
expect(deployUrl.attributes().href).toEqual(expectedUrl);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with multiple changes', () => {
|
describe('with multiple changes', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
factory({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
deployment: deploymentMockData,
|
deployment: deploymentMockData,
|
||||||
appButtonText,
|
appButtonText,
|
||||||
|
@ -85,18 +91,18 @@ describe('Deployment View App button', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the link to the review app with dropdown', () => {
|
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', () => {
|
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);
|
const expectedUrls = deploymentMockData.changes.map((change) => change.external_url);
|
||||||
|
|
||||||
expectedUrls.forEach((expectedUrl, idx) => {
|
expectedUrls.forEach((expectedUrl, idx) => {
|
||||||
const deployUrl = allUrls[idx];
|
const deployUrl = allUrls[idx];
|
||||||
|
|
||||||
expect(deployUrl.attributes().href).not.toBeNull();
|
expect(deployUrl.attributes('href')).toBe(expectedUrl);
|
||||||
expect(deployUrl.attributes().href).toEqual(expectedUrl);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,16 +40,21 @@ RSpec.describe Ci::PipelineEditorHelper do
|
||||||
it 'returns pipeline editor data' do
|
it 'returns pipeline editor data' do
|
||||||
expect(pipeline_editor_data).to eq({
|
expect(pipeline_editor_data).to eq({
|
||||||
"ci-config-path": project.ci_config_path_or_default,
|
"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,
|
"commit-sha" => project.commit.sha,
|
||||||
"default-branch" => project.default_branch,
|
"default-branch" => project.default_branch,
|
||||||
"empty-state-illustration-path" => 'foo',
|
"empty-state-illustration-path" => 'foo',
|
||||||
"initial-branch-name": nil,
|
"initial-branch-name": nil,
|
||||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
"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',
|
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
|
||||||
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
|
"pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha),
|
||||||
|
"pipeline-page-path" => project_pipelines_path(project),
|
||||||
"project-path" => project.path,
|
"project-path" => project.path,
|
||||||
"project-full-path" => project.full_path,
|
"project-full-path" => project.full_path,
|
||||||
"project-namespace" => project.namespace.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')
|
"yml-help-page-path" => help_page_path('ci/yaml/README')
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
@ -61,16 +66,21 @@ RSpec.describe Ci::PipelineEditorHelper do
|
||||||
it 'returns pipeline editor data' do
|
it 'returns pipeline editor data' do
|
||||||
expect(pipeline_editor_data).to eq({
|
expect(pipeline_editor_data).to eq({
|
||||||
"ci-config-path": project.ci_config_path_or_default,
|
"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,
|
"default-branch" => project.default_branch,
|
||||||
"empty-state-illustration-path" => 'foo',
|
"empty-state-illustration-path" => 'foo',
|
||||||
"initial-branch-name": nil,
|
"initial-branch-name": nil,
|
||||||
"lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'),
|
"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',
|
"new-merge-request-path" => '/mock/project/-/merge_requests/new',
|
||||||
"pipeline_etag" => '',
|
"pipeline_etag" => '',
|
||||||
|
"pipeline-page-path" => project_pipelines_path(project),
|
||||||
"project-path" => project.path,
|
"project-path" => project.path,
|
||||||
"project-full-path" => project.full_path,
|
"project-full-path" => project.full_path,
|
||||||
"project-namespace" => project.namespace.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')
|
"yml-help-page-path" => help_page_path('ci/yaml/README')
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,12 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Banzai::CrossProjectReference do
|
RSpec.describe Banzai::CrossProjectReference do
|
||||||
let(:including_class) { Class.new.include(described_class).new }
|
let(:including_class) { Class.new.include(described_class).new }
|
||||||
|
let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {})}
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(including_class).to receive(:context).and_return({})
|
allow(including_class).to receive(:context).and_return({})
|
||||||
allow(including_class).to receive(:parent_from_ref).and_call_original
|
allow(including_class).to receive(:parent_from_ref).and_call_original
|
||||||
|
allow(including_class).to receive(:reference_cache).and_return(reference_cache)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#parent_from_ref' do
|
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
|
expect(including_class.parent_from_ref('cross/reference')).to eq project2
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue