Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b808458daa
commit
e6a54b33a9
|
@ -96,6 +96,13 @@ retire-js-dependency_scanning:
|
||||||
gemnasium-python-dependency_scanning:
|
gemnasium-python-dependency_scanning:
|
||||||
rules: !reference [".reports:rules:gemnasium-python-dependency_scanning", rules]
|
rules: !reference [".reports:rules:gemnasium-python-dependency_scanning", rules]
|
||||||
|
|
||||||
|
yarn-audit-dependency_scanning:
|
||||||
|
extends: .ds-analyzer
|
||||||
|
image: "registry.gitlab.com/gitlab-org/security-products/analyzers/npm-audit:1.4.0"
|
||||||
|
variables:
|
||||||
|
TOOL: yarn
|
||||||
|
rules: !reference [".reports:rules:yarn-audit-dependency_scanning", rules]
|
||||||
|
|
||||||
# Analyze dependencies for malicious behavior
|
# Analyze dependencies for malicious behavior
|
||||||
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
|
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
|
||||||
.package_hunter-base:
|
.package_hunter-base:
|
||||||
|
|
|
@ -167,6 +167,7 @@
|
||||||
|
|
||||||
.nodejs-patterns: &nodejs-patterns
|
.nodejs-patterns: &nodejs-patterns
|
||||||
- '{package.json,*/package.json,*/*/package.json}'
|
- '{package.json,*/package.json,*/*/package.json}'
|
||||||
|
- '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
|
||||||
|
|
||||||
.python-patterns: &python-patterns
|
.python-patterns: &python-patterns
|
||||||
- '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
|
- '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
|
||||||
|
@ -373,10 +374,6 @@
|
||||||
- ".dockerignore"
|
- ".dockerignore"
|
||||||
- "qa/**/*"
|
- "qa/**/*"
|
||||||
|
|
||||||
.code-shell-patterns: &code-shell-patterns
|
|
||||||
- "bin/**/*"
|
|
||||||
- "tooling/**/*"
|
|
||||||
|
|
||||||
# .code-backstage-qa-patterns + .workhorse-patterns
|
# .code-backstage-qa-patterns + .workhorse-patterns
|
||||||
.setup-test-env-patterns: &setup-test-env-patterns
|
.setup-test-env-patterns: &setup-test-env-patterns
|
||||||
- "{package.json,yarn.lock}"
|
- "{package.json,yarn.lock}"
|
||||||
|
@ -1487,6 +1484,12 @@
|
||||||
when: never
|
when: never
|
||||||
- changes: *python-patterns
|
- changes: *python-patterns
|
||||||
|
|
||||||
|
.reports:rules:yarn-audit-dependency_scanning:
|
||||||
|
rules:
|
||||||
|
- if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/'
|
||||||
|
when: never
|
||||||
|
- changes: *nodejs-patterns
|
||||||
|
|
||||||
.reports:rules:schedule-dast:
|
.reports:rules:schedule-dast:
|
||||||
rules:
|
rules:
|
||||||
- if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
|
- if: '$DAST_DISABLED || $GITLAB_FEATURES !~ /\bdast\b/'
|
||||||
|
@ -1779,13 +1782,6 @@
|
||||||
- changes: *code-backstage-qa-patterns
|
- changes: *code-backstage-qa-patterns
|
||||||
- changes: *startup-css-patterns
|
- changes: *startup-css-patterns
|
||||||
|
|
||||||
###############
|
|
||||||
# Shell rules #
|
|
||||||
###############
|
|
||||||
.shell:rules:
|
|
||||||
rules:
|
|
||||||
- changes: *code-shell-patterns
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
# Test metadata rules #
|
# Test metadata rules #
|
||||||
#######################
|
#######################
|
||||||
|
|
|
@ -107,15 +107,3 @@ feature-flags-usage:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
- tmp/feature_flags/
|
- tmp/feature_flags/
|
||||||
|
|
||||||
shellcheck:
|
|
||||||
extends:
|
|
||||||
- .default-retry
|
|
||||||
- .shell:rules
|
|
||||||
stage: lint
|
|
||||||
needs: []
|
|
||||||
image:
|
|
||||||
name: koalaman/shellcheck-alpine
|
|
||||||
entrypoint: [""]
|
|
||||||
script:
|
|
||||||
- tooling/bin/shellcheck
|
|
||||||
|
|
|
@ -1,22 +1,37 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { parseBoolean } from './lib/utils/common_utils';
|
||||||
import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
|
import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const el = document.querySelector('.js-confirm-danger');
|
const el = document.querySelector('.js-confirm-danger');
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
|
|
||||||
const { phrase, buttonText, confirmDangerMessage } = el.dataset;
|
const {
|
||||||
|
removeFormId = null,
|
||||||
|
phrase,
|
||||||
|
buttonText,
|
||||||
|
buttonTestid = null,
|
||||||
|
confirmDangerMessage,
|
||||||
|
disabled = false,
|
||||||
|
} = el.dataset;
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
|
provide: {
|
||||||
|
confirmDangerMessage,
|
||||||
|
},
|
||||||
render: (createElement) =>
|
render: (createElement) =>
|
||||||
createElement(ConfirmDanger, {
|
createElement(ConfirmDanger, {
|
||||||
props: {
|
props: {
|
||||||
phrase,
|
phrase,
|
||||||
buttonText,
|
buttonText,
|
||||||
|
buttonTestid,
|
||||||
|
disabled: parseBoolean(disabled),
|
||||||
},
|
},
|
||||||
provide: {
|
on: {
|
||||||
confirmDangerMessage,
|
confirm: () => {
|
||||||
|
if (removeFormId) document.getElementById(removeFormId)?.submit();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,10 +10,12 @@ import projectSelect from '~/project_select';
|
||||||
import initSearchSettings from '~/search_settings';
|
import initSearchSettings from '~/search_settings';
|
||||||
import initSettingsPanels from '~/settings_panels';
|
import initSettingsPanels from '~/settings_panels';
|
||||||
import setupTransferEdit from '~/transfer_edit';
|
import setupTransferEdit from '~/transfer_edit';
|
||||||
|
import initConfirmDanger from '~/init_confirm_danger';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initFilePickers();
|
initFilePickers();
|
||||||
initConfirmDangerModal();
|
initConfirmDangerModal();
|
||||||
|
initConfirmDanger();
|
||||||
initSettingsPanels();
|
initSettingsPanels();
|
||||||
dirtySubmitFactory(
|
dirtySubmitFactory(
|
||||||
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
|
document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'),
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
toggleQueryPollingByVisibility,
|
toggleQueryPollingByVisibility,
|
||||||
} from '~/pipelines/components/graph/utils';
|
} from '~/pipelines/components/graph/utils';
|
||||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|
||||||
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
|
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
|
||||||
|
|
||||||
const POLL_INTERVAL = 10000;
|
const POLL_INTERVAL = 10000;
|
||||||
|
@ -37,7 +36,6 @@ export default {
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
PipelineEditorMiniGraph,
|
PipelineEditorMiniGraph,
|
||||||
},
|
},
|
||||||
mixins: [glFeatureFlagMixin()],
|
|
||||||
inject: ['projectFullPath'],
|
inject: ['projectFullPath'],
|
||||||
props: {
|
props: {
|
||||||
commitSha: {
|
commitSha: {
|
||||||
|
@ -172,11 +170,7 @@ export default {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-display-flex gl-flex-wrap">
|
<div class="gl-display-flex gl-flex-wrap">
|
||||||
<pipeline-editor-mini-graph
|
<pipeline-editor-mini-graph :pipeline="pipeline" v-on="$listeners" />
|
||||||
v-if="glFeatures.pipelineEditorMiniGraph"
|
|
||||||
:pipeline="pipeline"
|
|
||||||
v-on="$listeners"
|
|
||||||
/>
|
|
||||||
<gl-button
|
<gl-button
|
||||||
class="gl-mt-2 gl-md-mt-0"
|
class="gl-mt-2 gl-md-mt-0"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
@ -39,6 +39,9 @@ export default {
|
||||||
assignSelf() {
|
assignSelf() {
|
||||||
this.$emit('assign-self');
|
this.$emit('assign-self');
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.$emit('toggle-attention-required', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -58,7 +61,12 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
|
<uncollapsed-assignee-list
|
||||||
|
v-else
|
||||||
|
:users="sortedAssigness"
|
||||||
|
:issuable-type="issuableType"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -32,6 +32,11 @@ export default {
|
||||||
return this.users.length === 0;
|
return this.users.length === 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.$emit('toggle-attention-required', data);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -61,6 +66,7 @@ export default {
|
||||||
:users="users"
|
:users="users"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
class="gl-text-gray-800 gl-mt-2 hide-collapsed"
|
class="gl-text-gray-800 gl-mt-2 hide-collapsed"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -125,6 +125,9 @@ export default {
|
||||||
availability: this.assigneeAvailabilityStatus[username] || '',
|
availability: this.assigneeAvailabilityStatus[username] || '',
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.mediator.toggleAttentionRequired('assignee', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -152,6 +155,7 @@ export default {
|
||||||
:editable="store.editable"
|
:editable="store.editable"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
@assign-self="assignSelf"
|
@assign-self="assignSelf"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { IssuableType } from '~/issue_show/constants';
|
import { IssuableType } from '~/issue_show/constants';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
|
import AttentionRequiredToggle from '../attention_required_toggle.vue';
|
||||||
import AssigneeAvatarLink from './assignee_avatar_link.vue';
|
import AssigneeAvatarLink from './assignee_avatar_link.vue';
|
||||||
import UserNameWithStatus from './user_name_with_status.vue';
|
import UserNameWithStatus from './user_name_with_status.vue';
|
||||||
|
|
||||||
|
@ -9,6 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
AttentionRequiredToggle,
|
||||||
AssigneeAvatarLink,
|
AssigneeAvatarLink,
|
||||||
UserNameWithStatus,
|
UserNameWithStatus,
|
||||||
},
|
},
|
||||||
|
@ -80,6 +82,9 @@ export default {
|
||||||
}
|
}
|
||||||
return u?.status?.availability || '';
|
return u?.status?.availability || '';
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.$emit('toggle-attention-required', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -108,6 +113,12 @@ export default {
|
||||||
}"
|
}"
|
||||||
class="gl-display-inline-block"
|
class="gl-display-inline-block"
|
||||||
>
|
>
|
||||||
|
<attention-required-toggle
|
||||||
|
v-if="showVerticalList && user.can_update_merge_request"
|
||||||
|
:user="user"
|
||||||
|
type="assignee"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
|
/>
|
||||||
<assignee-avatar-link
|
<assignee-avatar-link
|
||||||
:user="user"
|
:user="user"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script>
|
||||||
|
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
i18n: {
|
||||||
|
attentionRequiredReviewer: __('Request attention to review'),
|
||||||
|
attentionRequiredAssignee: __('Request attention'),
|
||||||
|
removeAttentionRequired: __('Remove attention request'),
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
GlButton,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tooltipTitle() {
|
||||||
|
if (this.user.attention_required) {
|
||||||
|
return this.$options.i18n.removeAttentionRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.type === 'reviewer'
|
||||||
|
? this.$options.i18n.attentionRequiredReviewer
|
||||||
|
: this.$options.i18n.attentionRequiredAssignee;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleAttentionRequired() {
|
||||||
|
if (this.loading) return;
|
||||||
|
|
||||||
|
this.$root.$emit(BV_HIDE_TOOLTIP);
|
||||||
|
this.loading = true;
|
||||||
|
this.$emit('toggle-attention-required', {
|
||||||
|
user: this.user,
|
||||||
|
callback: this.toggleAttentionRequiredComplete,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleAttentionRequiredComplete() {
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span v-gl-tooltip.left.viewport="tooltipTitle">
|
||||||
|
<gl-button
|
||||||
|
:loading="loading"
|
||||||
|
:variant="user.attention_required ? 'warning' : 'default'"
|
||||||
|
:icon="user.attention_required ? 'star' : 'star-o'"
|
||||||
|
:aria-label="tooltipTitle"
|
||||||
|
size="small"
|
||||||
|
category="tertiary"
|
||||||
|
@click="toggleAttentionRequired"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
|
@ -49,6 +49,9 @@ export default {
|
||||||
requestReview(data) {
|
requestReview(data) {
|
||||||
this.$emit('request-review', data);
|
this.$emit('request-review', data);
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.$emit('toggle-attention-required', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -70,6 +73,7 @@ export default {
|
||||||
:root-path="rootPath"
|
:root-path="rootPath"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
@request-review="requestReview"
|
@request-review="requestReview"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,6 +88,9 @@ export default {
|
||||||
requestReview(data) {
|
requestReview(data) {
|
||||||
this.mediator.requestReview(data);
|
this.mediator.requestReview(data);
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.mediator.toggleAttentionRequired('reviewer', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -106,6 +109,7 @@ export default {
|
||||||
:editable="store.editable"
|
:editable="store.editable"
|
||||||
:issuable-type="issuableType"
|
:issuable-type="issuableType"
|
||||||
@request-review="requestReview"
|
@request-review="requestReview"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||||
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import { __, sprintf, s__ } from '~/locale';
|
import { __, sprintf, s__ } from '~/locale';
|
||||||
|
import AttentionRequiredToggle from '../attention_required_toggle.vue';
|
||||||
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
|
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
|
||||||
|
|
||||||
const LOADING_STATE = 'loading';
|
const LOADING_STATE = 'loading';
|
||||||
|
@ -14,10 +16,12 @@ export default {
|
||||||
GlButton,
|
GlButton,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
ReviewerAvatarLink,
|
ReviewerAvatarLink,
|
||||||
|
AttentionRequiredToggle,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
|
mixins: [glFeatureFlagsMixin()],
|
||||||
props: {
|
props: {
|
||||||
users: {
|
users: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -76,6 +80,9 @@ export default {
|
||||||
this.loadingStates[userId] = null;
|
this.loadingStates[userId] = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleAttentionRequired(data) {
|
||||||
|
this.$emit('toggle-attention-required', data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
LOADING_STATE,
|
LOADING_STATE,
|
||||||
SUCCESS_STATE,
|
SUCCESS_STATE,
|
||||||
|
@ -90,6 +97,12 @@ export default {
|
||||||
:class="{ 'gl-mb-3': index !== users.length - 1 }"
|
:class="{ 'gl-mb-3': index !== users.length - 1 }"
|
||||||
data-testid="reviewer"
|
data-testid="reviewer"
|
||||||
>
|
>
|
||||||
|
<attention-required-toggle
|
||||||
|
v-if="glFeatures.mrAttentionRequests && user.can_update_merge_request"
|
||||||
|
:user="user"
|
||||||
|
type="reviewer"
|
||||||
|
@toggle-attention-required="toggleAttentionRequired"
|
||||||
|
/>
|
||||||
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
|
<reviewer-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType">
|
||||||
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
|
<div class="gl-ml-3 gl-line-height-normal gl-display-grid">
|
||||||
<span>{{ user.name }}</span>
|
<span>{{ user.name }}</span>
|
||||||
|
@ -113,7 +126,9 @@ export default {
|
||||||
data-testid="re-request-success"
|
data-testid="re-request-success"
|
||||||
/>
|
/>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-else-if="user.can_update_merge_request && user.reviewed"
|
v-else-if="
|
||||||
|
user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests
|
||||||
|
"
|
||||||
v-gl-tooltip.left
|
v-gl-tooltip.left
|
||||||
:title="$options.i18n.reRequestReview"
|
:title="$options.i18n.reRequestReview"
|
||||||
:aria-label="$options.i18n.reRequestReview"
|
:aria-label="$options.i18n.reRequestReview"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
mutation mergeRequestAttentionRequired($projectPath: ID!, $iid: String!, $userId: ID!) {
|
||||||
|
mergeRequestAttentionRequired(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
|
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
|
||||||
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
|
import sidebarDetailsMRQuery from '../queries/sidebarDetailsMR.query.graphql';
|
||||||
|
import attentionRequiredMutation from '../queries/attention_required.mutation.graphql';
|
||||||
|
|
||||||
const queries = {
|
const queries = {
|
||||||
merge_request: sidebarDetailsMRQuery,
|
merge_request: sidebarDetailsMRQuery,
|
||||||
|
@ -90,4 +91,15 @@ export default class SidebarService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attentionRequired(userId) {
|
||||||
|
return gqClient.mutate({
|
||||||
|
mutation: attentionRequiredMutation,
|
||||||
|
variables: {
|
||||||
|
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
|
||||||
|
projectPath: this.fullPath,
|
||||||
|
iid: this.iid.toString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
|
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { __ } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
import toast from '~/vue_shared/plugins/global_toast';
|
import toast from '~/vue_shared/plugins/global_toast';
|
||||||
import { visitUrl } from '../lib/utils/url_utility';
|
import { visitUrl } from '../lib/utils/url_utility';
|
||||||
import Service from './services/sidebar_service';
|
import Service from './services/sidebar_service';
|
||||||
|
@ -56,13 +56,55 @@ export default class SidebarMediator {
|
||||||
return this.service
|
return this.service
|
||||||
.requestReview(userId)
|
.requestReview(userId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.store.updateReviewer(userId);
|
this.store.updateReviewer(userId, 'reviewed');
|
||||||
toast(__('Requested review'));
|
toast(__('Requested review'));
|
||||||
callback(userId, true);
|
callback(userId, true);
|
||||||
})
|
})
|
||||||
.catch(() => callback(userId, false));
|
.catch(() => callback(userId, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toggleAttentionRequired(type, { user, callback }) {
|
||||||
|
try {
|
||||||
|
const isReviewer = type === 'reviewer';
|
||||||
|
const reviewerOrAssignee = isReviewer
|
||||||
|
? this.store.findReviewer(user)
|
||||||
|
: this.store.findAssignee(user);
|
||||||
|
|
||||||
|
if (reviewerOrAssignee.attention_required) {
|
||||||
|
toast(
|
||||||
|
sprintf(__('Removed attention request from @%{username}'), {
|
||||||
|
username: user.username,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await this.service.attentionRequired(user.id);
|
||||||
|
|
||||||
|
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReviewer) {
|
||||||
|
this.store.updateReviewer(user.id, 'attention_required');
|
||||||
|
} else {
|
||||||
|
this.store.updateAssignee(user.id, 'attention_required');
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
} catch (error) {
|
||||||
|
callback();
|
||||||
|
createFlash({
|
||||||
|
message: sprintf(__('Updating the attention request for %{username} failed.'), {
|
||||||
|
username: user.username,
|
||||||
|
}),
|
||||||
|
error,
|
||||||
|
captureError: true,
|
||||||
|
actionConfig: {
|
||||||
|
title: __('Try again'),
|
||||||
|
clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setMoveToProjectId(projectId) {
|
setMoveToProjectId(projectId) {
|
||||||
this.store.setMoveToProjectId(projectId);
|
this.store.setMoveToProjectId(projectId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,11 +82,19 @@ export default class SidebarStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateReviewer(id) {
|
updateAssignee(id, stateKey) {
|
||||||
|
const assignee = this.findAssignee({ id });
|
||||||
|
|
||||||
|
if (assignee) {
|
||||||
|
assignee[stateKey] = !assignee[stateKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateReviewer(id, stateKey) {
|
||||||
const reviewer = this.findReviewer({ id });
|
const reviewer = this.findReviewer({ id });
|
||||||
|
|
||||||
if (reviewer) {
|
if (reviewer) {
|
||||||
reviewer.reviewed = false;
|
reviewer[stateKey] = !reviewer[stateKey];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
buttonTestid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'confirm-danger-button',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modalId: CONFIRM_DANGER_MODAL_ID,
|
modalId: CONFIRM_DANGER_MODAL_ID,
|
||||||
};
|
};
|
||||||
|
@ -37,7 +42,7 @@ export default {
|
||||||
class="gl-button"
|
class="gl-button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
data-testid="confirm-danger-button"
|
:data-testid="buttonTestid"
|
||||||
>{{ buttonText }}</gl-button
|
>{{ buttonText }}</gl-button
|
||||||
>
|
>
|
||||||
<confirm-danger-modal
|
<confirm-danger-modal
|
||||||
|
|
|
@ -93,6 +93,7 @@ export default {
|
||||||
</p>
|
</p>
|
||||||
<gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
|
<gl-form-group :state="isValid" :invalid-feedback="$options.i18n.CONFIRM_DANGER_MODAL_ERROR">
|
||||||
<gl-form-input
|
<gl-form-input
|
||||||
|
id="confirm_name_input"
|
||||||
v-model="confirmationPhrase"
|
v-model="confirmationPhrase"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
data-testid="confirm-danger-input"
|
data-testid="confirm-danger-input"
|
||||||
|
|
|
@ -73,13 +73,23 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload_manifest
|
def upload_manifest
|
||||||
@group.dependency_proxy_manifests.create!(
|
attrs = {
|
||||||
file_name: manifest_file_name,
|
file_name: manifest_file_name,
|
||||||
content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
|
content_type: request.headers[Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER],
|
||||||
digest: request.headers['Docker-Content-Digest'],
|
digest: request.headers[DependencyProxy::Manifest::DIGEST_HEADER],
|
||||||
file: params[:file],
|
file: params[:file],
|
||||||
size: params[:file].size
|
size: params[:file].size
|
||||||
)
|
}
|
||||||
|
|
||||||
|
manifest = @group.dependency_proxy_manifests
|
||||||
|
.active
|
||||||
|
.find_by_file_name(manifest_file_name)
|
||||||
|
|
||||||
|
if manifest
|
||||||
|
manifest.update!(attrs)
|
||||||
|
else
|
||||||
|
@group.dependency_proxy_manifests.create!(attrs)
|
||||||
|
end
|
||||||
|
|
||||||
event_name = tracking_event_name(object_type: :manifest, from_cache: false)
|
event_name = tracking_event_name(object_type: :manifest, from_cache: false)
|
||||||
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
|
track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
|
||||||
|
@ -105,7 +115,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
|
||||||
def send_manifest(manifest, from_cache:)
|
def send_manifest(manifest, from_cache:)
|
||||||
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
|
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
|
||||||
manifest.touch
|
manifest.touch
|
||||||
response.headers['Docker-Content-Digest'] = manifest.digest
|
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
|
||||||
response.headers['Content-Length'] = manifest.size
|
response.headers['Content-Length'] = manifest.size
|
||||||
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
|
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
|
||||||
response.headers['Etag'] = "\"#{manifest.digest}\""
|
response.headers['Etag'] = "\"#{manifest.digest}\""
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
|
||||||
before_action :check_can_collaborate!
|
before_action :check_can_collaborate!
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
|
|
||||||
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
|
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ module Types
|
||||||
|
|
||||||
authorize :read_dependency_proxy
|
authorize :read_dependency_proxy
|
||||||
|
|
||||||
|
field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.'
|
||||||
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
|
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
|
||||||
field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
|
field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
|
||||||
field :file_name, GraphQL::Types::String, null: false, description: 'Name of the manifest.'
|
field :file_name, GraphQL::Types::String, null: false, description: 'Name of the manifest.'
|
||||||
|
|
|
@ -220,6 +220,10 @@ module Types
|
||||||
group.container_repositories.size
|
group.container_repositories.size
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dependency_proxy_manifests
|
||||||
|
group.dependency_proxy_manifests.order_id_desc
|
||||||
|
end
|
||||||
|
|
||||||
def dependency_proxy_image_count
|
def dependency_proxy_image_count
|
||||||
group.dependency_proxy_manifests.count
|
group.dependency_proxy_manifests.count
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Groups
|
||||||
|
module SettingsHelper
|
||||||
|
include GroupsHelper
|
||||||
|
|
||||||
|
def group_settings_confirm_modal_data(group, remove_form_id = nil)
|
||||||
|
{
|
||||||
|
remove_form_id: remove_form_id,
|
||||||
|
button_text: _('Remove group'),
|
||||||
|
button_testid: 'remove-group-button',
|
||||||
|
disabled: group.paid?.to_s,
|
||||||
|
confirm_danger_message: remove_group_message(group),
|
||||||
|
phrase: group.full_path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Groups::SettingsHelper.prepend_mod_with('Groups::SettingsHelper')
|
|
@ -139,8 +139,6 @@ module Clusters
|
||||||
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
|
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
|
||||||
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
|
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
|
||||||
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
|
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
|
||||||
scope :preload_elasticstack, -> { preload(:integration_elastic_stack) }
|
|
||||||
scope :preload_environments, -> { preload(:environments) }
|
|
||||||
|
|
||||||
scope :managed, -> { where(managed: true) }
|
scope :managed, -> { where(managed: true) }
|
||||||
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
|
scope :with_persisted_applications, -> { eager_load(*APPLICATIONS_ASSOCIATIONS) }
|
||||||
|
|
|
@ -8,12 +8,15 @@ class DependencyProxy::Manifest < ApplicationRecord
|
||||||
belongs_to :group
|
belongs_to :group
|
||||||
|
|
||||||
MAX_FILE_SIZE = 10.megabytes.freeze
|
MAX_FILE_SIZE = 10.megabytes.freeze
|
||||||
|
DIGEST_HEADER = 'Docker-Content-Digest'
|
||||||
|
|
||||||
validates :group, presence: true
|
validates :group, presence: true
|
||||||
validates :file, presence: true
|
validates :file, presence: true
|
||||||
validates :file_name, presence: true
|
validates :file_name, presence: true
|
||||||
validates :digest, presence: true
|
validates :digest, presence: true
|
||||||
|
|
||||||
|
scope :order_id_desc, -> { reorder(id: :desc) }
|
||||||
|
|
||||||
mount_file_store_uploader DependencyProxy::FileUploader
|
mount_file_store_uploader DependencyProxy::FileUploader
|
||||||
|
|
||||||
def self.find_by_file_name_or_digest(file_name:, digest:)
|
def self.find_by_file_name_or_digest(file_name:, digest:)
|
||||||
|
|
|
@ -204,6 +204,8 @@ class Issue < ApplicationRecord
|
||||||
before_transition closed: :opened do |issue|
|
before_transition closed: :opened do |issue|
|
||||||
issue.closed_at = nil
|
issue.closed_at = nil
|
||||||
issue.closed_by = nil
|
issue.closed_by = nil
|
||||||
|
|
||||||
|
issue.clear_closure_reason_references
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -379,6 +381,11 @@ class Issue < ApplicationRecord
|
||||||
!duplicated_to_id.nil?
|
!duplicated_to_id.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clear_closure_reason_references
|
||||||
|
self.moved_to_id = nil
|
||||||
|
self.duplicated_to_id = nil
|
||||||
|
end
|
||||||
|
|
||||||
def can_move?(user, to_project = nil)
|
def can_move?(user, to_project = nil)
|
||||||
if to_project
|
if to_project
|
||||||
return false unless user.can?(:admin_issue, to_project)
|
return false unless user.can?(:admin_issue, to_project)
|
||||||
|
|
|
@ -14,7 +14,10 @@ module DependencyProxy
|
||||||
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
|
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
|
||||||
|
|
||||||
if response.success?
|
if response.success?
|
||||||
success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
|
success(
|
||||||
|
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
|
||||||
|
content_type: response.headers['content-type']
|
||||||
|
)
|
||||||
else
|
else
|
||||||
error(response.body, response.code)
|
error(response.body, response.code)
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,7 +20,13 @@ module DependencyProxy
|
||||||
file.write(response.body)
|
file.write(response.body)
|
||||||
file.flush
|
file.flush
|
||||||
|
|
||||||
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
|
yield(
|
||||||
|
success(
|
||||||
|
file: file,
|
||||||
|
digest: response.headers[DependencyProxy::Manifest::DIGEST_HEADER],
|
||||||
|
content_type: response.headers['content-type']
|
||||||
|
)
|
||||||
|
)
|
||||||
ensure
|
ensure
|
||||||
file.close
|
file.close
|
||||||
file.unlink
|
file.unlink
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
- remove_form_id = 'js-remove-group-form'
|
||||||
= render 'groups/settings/export', group: @group
|
= render 'groups/settings/export', group: @group
|
||||||
|
|
||||||
.sub-section
|
.sub-section
|
||||||
|
@ -26,6 +27,6 @@
|
||||||
= f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning'
|
= f.submit s_('GroupSettings|Change group URL'), class: 'btn gl-button btn-warning'
|
||||||
|
|
||||||
= render 'groups/settings/transfer', group: @group
|
= render 'groups/settings/transfer', group: @group
|
||||||
= render 'groups/settings/remove', group: @group
|
= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
|
||||||
= render_if_exists 'groups/settings/restore', group: @group
|
= render_if_exists 'groups/settings/restore', group: @group
|
||||||
= render_if_exists 'groups/settings/immediately_remove', group: @group
|
= render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: remove_form_id
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
|
||||||
|
|
||||||
.sub-section
|
.sub-section
|
||||||
%h4.danger-title= _('Remove group')
|
%h4.danger-title= _('Remove group')
|
||||||
= form_tag(group, method: :delete) do
|
= form_tag(group, method: :delete, id: remove_form_id) do
|
||||||
%p
|
%p
|
||||||
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
|
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
|
||||||
%br
|
%br
|
||||||
%strong= _('Removed group can not be restored!')
|
%strong= _('Removed group can not be restored!')
|
||||||
|
|
||||||
= render 'groups/settings/remove_button', group: group
|
= render 'groups/settings/remove_button', group: group, remove_form_id: remove_form_id
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
- if group.adjourned_deletion?
|
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
|
||||||
= render_if_exists 'groups/settings/adjourned_deletion', group: group
|
|
||||||
- else
|
|
||||||
= render 'groups/settings/permanent_deletion', group: group
|
|
||||||
|
|
||||||
|
- if group.adjourned_deletion?
|
||||||
|
= render_if_exists 'groups/settings/adjourned_deletion', group: group, remove_form_id: remove_form_id
|
||||||
|
- else
|
||||||
|
= render 'groups/settings/permanent_deletion', group: group, remove_form_id: remove_form_id
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
|
||||||
|
|
||||||
- if group.paid?
|
- if group.paid?
|
||||||
.gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } }
|
.gl-alert.gl-alert-info.gl-mb-5{ data: { testid: 'group-has-linked-subscription-alert' } }
|
||||||
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
= sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||||
.gl-alert-body
|
.gl-alert-body
|
||||||
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
|
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
|
||||||
|
|
||||||
= button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-legacy-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' }
|
.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) }
|
||||||
|
|
|
@ -6,9 +6,8 @@
|
||||||
= s_('WikiEmpty|Confluence is enabled')
|
= s_('WikiEmpty|Confluence is enabled')
|
||||||
%p
|
%p
|
||||||
- wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629'
|
- wiki_confluence_epic_link_url = 'https://gitlab.com/groups/gitlab-org/-/epics/3629'
|
||||||
- wiki_confluence_epic_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, url: wiki_confluence_epic_link_url)
|
- wiki_confluence_epic_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wiki_confluence_epic_link_url }
|
||||||
= format(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.").html_safe, wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe)
|
= html_escape(s_("WikiEmpty|You've enabled the Confluence Workspace integration. Your wiki will be viewable directly within Confluence. We are hard at work integrating Confluence more seamlessly into GitLab. If you'd like to stay up to date, follow our %{wiki_confluence_epic_link_start}Confluence epic%{wiki_confluence_epic_link_end}.")) % { wiki_confluence_epic_link_start: wiki_confluence_epic_link_start, wiki_confluence_epic_link_end: '</a>'.html_safe }
|
||||||
= link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do
|
= link_to @project.confluence_integration.confluence_url, target: '_blank', rel: 'noopener noreferrer', class: 'gl-button btn btn-success external-url', title: s_('WikiEmpty|Go to Confluence') do
|
||||||
= sprite_icon('external-link')
|
|
||||||
= s_('WikiEmpty|Go to Confluence')
|
= s_('WikiEmpty|Go to Confluence')
|
||||||
|
= sprite_icon('external-link')
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." || exit
|
cd $(dirname $0)/..
|
||||||
|
|
||||||
app_root=$(pwd)
|
app_root=$(pwd)
|
||||||
sidekiq_workers=${SIDEKIQ_WORKERS:-1}
|
sidekiq_workers=${SIDEKIQ_WORKERS:-1}
|
||||||
sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all)
|
sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all)
|
||||||
sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid"
|
sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid"
|
||||||
sidekiq_logfile="$app_root/log/sidekiq.log"
|
sidekiq_logfile="$app_root/log/sidekiq.log"
|
||||||
|
gitlab_user=$(ls -l config.ru | awk '{print $3}')
|
||||||
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
@ -17,26 +17,26 @@ warn()
|
||||||
|
|
||||||
get_sidekiq_pid()
|
get_sidekiq_pid()
|
||||||
{
|
{
|
||||||
if [ ! -f "$sidekiq_pidfile" ]; then
|
if [ ! -f $sidekiq_pidfile ]; then
|
||||||
warn "No pidfile found at $sidekiq_pidfile; is Sidekiq running?"
|
warn "No pidfile found at $sidekiq_pidfile; is Sidekiq running?"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat "$sidekiq_pidfile"
|
cat $sidekiq_pidfile
|
||||||
}
|
}
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
{
|
{
|
||||||
sidekiq_pid=$(get_sidekiq_pid)
|
sidekiq_pid=$(get_sidekiq_pid)
|
||||||
|
|
||||||
if [ "$sidekiq_pid" ]; then
|
if [ $sidekiq_pid ]; then
|
||||||
kill -TERM "$sidekiq_pid"
|
kill -TERM $sidekiq_pid
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
restart()
|
restart()
|
||||||
{
|
{
|
||||||
if [ -f "$sidekiq_pidfile" ]; then
|
if [ -f $sidekiq_pidfile ]; then
|
||||||
stop
|
stop
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -53,12 +53,12 @@ start_sidekiq()
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# sidekiq-cluster expects an argument per process.
|
# sidekiq-cluster expects an argument per process.
|
||||||
for (( i=1; i<=sidekiq_workers; i++ ))
|
for (( i=1; i<=$sidekiq_workers; i++ ))
|
||||||
do
|
do
|
||||||
processes_args+=("${sidekiq_queues}")
|
processes_args+=("${sidekiq_queues}")
|
||||||
done
|
done
|
||||||
|
|
||||||
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P "$sidekiq_pidfile" -e "$RAILS_ENV" "$@" 2>&1 | tee -a "$sidekiq_logfile"
|
${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@" 2>&1 | tee -a $sidekiq_logfile
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
cd $(dirname $0)/.. || exit 1
|
||||||
app_root=$(pwd)
|
app_root=$(pwd)
|
||||||
|
|
||||||
mail_room_pidfile="$app_root/tmp/pids/mail_room.pid"
|
mail_room_pidfile="$app_root/tmp/pids/mail_room.pid"
|
||||||
|
@ -9,7 +9,8 @@ mail_room_config="$app_root/config/mail_room.yml"
|
||||||
|
|
||||||
get_mail_room_pid()
|
get_mail_room_pid()
|
||||||
{
|
{
|
||||||
pid=$(cat "$mail_room_pidfile")
|
local pid
|
||||||
|
pid=$(cat $mail_room_pidfile)
|
||||||
if [ -z "$pid" ] ; then
|
if [ -z "$pid" ] ; then
|
||||||
echo "Could not find a PID in $mail_room_pidfile"
|
echo "Could not find a PID in $mail_room_pidfile"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -19,13 +20,13 @@ get_mail_room_pid()
|
||||||
|
|
||||||
start()
|
start()
|
||||||
{
|
{
|
||||||
bin/daemon_with_pidfile "$mail_room_pidfile" bundle exec mail_room --log-exit-as json -q -c "$mail_room_config" >> "$mail_room_logfile" 2>&1
|
bin/daemon_with_pidfile $mail_room_pidfile bundle exec mail_room --log-exit-as json -q -c $mail_room_config >> $mail_room_logfile 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
{
|
{
|
||||||
get_mail_room_pid
|
get_mail_room_pid
|
||||||
kill -TERM "$mail_room_pid"
|
kill -TERM $mail_room_pid
|
||||||
}
|
}
|
||||||
|
|
||||||
restart()
|
restart()
|
||||||
|
|
|
@ -32,20 +32,20 @@ if [ -z "$RSYNC" ] ; then
|
||||||
RSYNC=rsync
|
RSYNC=rsync
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! cd "$SRC" ; then
|
if ! cd $SRC ; then
|
||||||
echo "cd $SRC failed"
|
echo "cd $SRC failed"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rsyncjob() {
|
rsyncjob() {
|
||||||
relative_dir="./${1#"$SRC"}"
|
relative_dir="./${1#$SRC}"
|
||||||
|
|
||||||
if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then
|
if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then
|
||||||
echo "rsync $1 failed"
|
echo "rsync $1 failed"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$1" >> "$LOGFILE"
|
echo "$1" >> $LOGFILE
|
||||||
}
|
}
|
||||||
|
|
||||||
export LOGFILE SRC DEST RSYNC
|
export LOGFILE SRC DEST RSYNC
|
||||||
|
|
10
bin/web
10
bin/web
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd $(dirname $0)/..
|
||||||
app_root=$(pwd)
|
app_root=$(pwd)
|
||||||
|
|
||||||
puma_pidfile="$app_root/tmp/pids/puma.pid"
|
puma_pidfile="$app_root/tmp/pids/puma.pid"
|
||||||
|
@ -25,12 +25,12 @@ get_puma_pid()
|
||||||
|
|
||||||
start()
|
start()
|
||||||
{
|
{
|
||||||
spawn_puma "$@" &
|
spawn_puma &
|
||||||
}
|
}
|
||||||
|
|
||||||
start_foreground()
|
start_foreground()
|
||||||
{
|
{
|
||||||
spawn_puma "$@"
|
spawn_puma
|
||||||
}
|
}
|
||||||
|
|
||||||
stop()
|
stop()
|
||||||
|
@ -46,10 +46,10 @@ reload()
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
start "$@"
|
start
|
||||||
;;
|
;;
|
||||||
start_foreground)
|
start_foreground)
|
||||||
start_foreground "$@"
|
start_foreground
|
||||||
;;
|
;;
|
||||||
stop)
|
stop)
|
||||||
stop
|
stop
|
||||||
|
|
|
@ -10,7 +10,6 @@ shift
|
||||||
|
|
||||||
# Use set -a to export all variables defined in env_file.
|
# Use set -a to export all variables defined in env_file.
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${env_file}"
|
. "${env_file}"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
---
|
|
||||||
name: pipeline_editor_mini_graph
|
|
||||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622
|
|
||||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217
|
|
||||||
milestone: '14.4'
|
|
||||||
type: development
|
|
||||||
group: group::pipeline authoring
|
|
||||||
default_enabled: false
|
|
|
@ -9237,6 +9237,7 @@ Dependency proxy manifest.
|
||||||
| <a id="dependencyproxymanifestcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
|
| <a id="dependencyproxymanifestcreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
|
||||||
| <a id="dependencyproxymanifestdigest"></a>`digest` | [`String!`](#string) | Digest of the manifest. |
|
| <a id="dependencyproxymanifestdigest"></a>`digest` | [`String!`](#string) | Digest of the manifest. |
|
||||||
| <a id="dependencyproxymanifestfilename"></a>`fileName` | [`String!`](#string) | Name of the manifest. |
|
| <a id="dependencyproxymanifestfilename"></a>`fileName` | [`String!`](#string) | Name of the manifest. |
|
||||||
|
| <a id="dependencyproxymanifestid"></a>`id` | [`DependencyProxyManifestID!`](#dependencyproxymanifestid) | ID of the manifest. |
|
||||||
| <a id="dependencyproxymanifestimagename"></a>`imageName` | [`String!`](#string) | Name of the image. |
|
| <a id="dependencyproxymanifestimagename"></a>`imageName` | [`String!`](#string) | Name of the image. |
|
||||||
| <a id="dependencyproxymanifestsize"></a>`size` | [`String!`](#string) | Size of the manifest file. |
|
| <a id="dependencyproxymanifestsize"></a>`size` | [`String!`](#string) | Size of the manifest file. |
|
||||||
| <a id="dependencyproxymanifestupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
|
| <a id="dependencyproxymanifestupdatedat"></a>`updatedAt` | [`Time!`](#time) | Date of most recent update. |
|
||||||
|
@ -17280,6 +17281,12 @@ An example `DastSiteValidationID` is: `"gid://gitlab/DastSiteValidation/1"`.
|
||||||
|
|
||||||
Date represented in ISO 8601.
|
Date represented in ISO 8601.
|
||||||
|
|
||||||
|
### `DependencyProxyManifestID`
|
||||||
|
|
||||||
|
A `DependencyProxyManifestID` is a global ID. It is encoded as a string.
|
||||||
|
|
||||||
|
An example `DependencyProxyManifestID` is: `"gid://gitlab/DependencyProxy::Manifest/1"`.
|
||||||
|
|
||||||
### `DesignManagementDesignAtVersionID`
|
### `DesignManagementDesignAtVersionID`
|
||||||
|
|
||||||
A `DesignManagementDesignAtVersionID` is a global ID. It is encoded as a string.
|
A `DesignManagementDesignAtVersionID` is a global ID. It is encoded as a string.
|
||||||
|
|
|
@ -1055,15 +1055,19 @@ Guidance for each individual UI element is in [the word list](word_list.md).
|
||||||
|
|
||||||
To be consistent, use this format when you write navigation steps in a task topic.
|
To be consistent, use this format when you write navigation steps in a task topic.
|
||||||
|
|
||||||
|
```markdown
|
||||||
1. On the top bar, select **Menu > Projects** and find your project.
|
1. On the top bar, select **Menu > Projects** and find your project.
|
||||||
1. On the left sidebar, select **Settings > CI/CD**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
1. Expand **General pipelines**.
|
1. Expand **General pipelines**.
|
||||||
|
```
|
||||||
|
|
||||||
Another example:
|
Another example:
|
||||||
|
|
||||||
|
```markdown
|
||||||
1. On the top bar, select **Menu > Groups** and find your group.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **Settings > CI/CD**.
|
1. On the left sidebar, select **Settings > CI/CD**.
|
||||||
1. Expand **General pipelines**.
|
1. Expand **General pipelines**.
|
||||||
|
```
|
||||||
|
|
||||||
An Admin Area example:
|
An Admin Area example:
|
||||||
|
|
||||||
|
@ -1092,7 +1096,7 @@ For example:
|
||||||
If the UI text sufficiently explains the fields in a section, do not include a task step for every field.
|
If the UI text sufficiently explains the fields in a section, do not include a task step for every field.
|
||||||
Instead, summarize multiple fields in a single task step.
|
Instead, summarize multiple fields in a single task step.
|
||||||
|
|
||||||
Use the phrase **Complete the fields**.
|
Use the phrase **Complete the fields**.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ are accessible.
|
||||||
- **Jira Server**: Your network must allow access to your instance.
|
- **Jira Server**: Your network must allow access to your instance.
|
||||||
- **Jira Cloud**: Your instance must be accessible through the internet.
|
- **Jira Cloud**: Your instance must be accessible through the internet.
|
||||||
|
|
||||||
## Smart commits
|
## Smart Commits
|
||||||
|
|
||||||
When connecting GitLab with Jira with DVCS, you can process your Jira issues using
|
When connecting GitLab with Jira with DVCS, you can process your Jira issues using
|
||||||
special commands, called
|
special commands, called
|
||||||
|
@ -48,17 +48,24 @@ Smart Commits should follow the pattern of:
|
||||||
|
|
||||||
Some examples:
|
Some examples:
|
||||||
|
|
||||||
- Adding a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.`
|
- Add a comment to a Jira issue: `KEY-123 fixes a bug #comment Bug is fixed.`
|
||||||
- Recording time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.`
|
- Record time tracking: `KEY-123 #time 2w 4d 10h 52m Tracking work time.`
|
||||||
- Closing an issue: `KEY-123 #close Closing issue`
|
- Close an issue: `KEY-123 #close Closing issue`
|
||||||
|
|
||||||
A Smart Commit message must not span more than one line (no carriage returns) but
|
A Smart Commit message must not span more than one line (no carriage returns) but
|
||||||
you can still perform multiple actions in a single commit:
|
you can still perform multiple actions in a single commit. For example:
|
||||||
|
|
||||||
- Time tracking, commenting, and transitioning to **Closed**:
|
- Add time tracking, add a comment, and transition to **Closed**:
|
||||||
`KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close`.
|
|
||||||
- Commenting, transitioning to **In-progress**, and time tracking:
|
```plaintext
|
||||||
`KEY-123 #comment started working on the issue #in-progress #time 12d 5h`.
|
KEY-123 #time 2d 5h #comment Task completed ahead of schedule #close
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add a comment, transition to **In-progress**, and add time tracking:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
KEY-123 #comment started working on the issue #in-progress #time 12d 5h
|
||||||
|
```
|
||||||
|
|
||||||
## Configure a GitLab application for DVCS
|
## Configure a GitLab application for DVCS
|
||||||
|
|
||||||
|
@ -69,9 +76,9 @@ you can set up this integration with your own account instead.
|
||||||
|
|
||||||
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
|
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
|
||||||
use to connect to GitLab. This user must be added to each project you want Jira to have access to,
|
use to connect to GitLab. This user must be added to each project you want Jira to have access to,
|
||||||
or have an [Administrator](../../user/permissions.md) role to access all projects.
|
or be an administrator to access all projects.
|
||||||
1. Sign in as the `jira` user.
|
1. Sign in as the `jira` user.
|
||||||
1. In the top right corner, click the account's avatar, and select **Edit profile**.
|
1. On the top bar, in the top right corner, select the user's avatar, and select **Edit profile**.
|
||||||
1. On the left sidebar, select **Applications**.
|
1. On the left sidebar, select **Applications**.
|
||||||
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
|
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
|
||||||
1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab,
|
1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab,
|
||||||
|
@ -86,10 +93,10 @@ you can set up this integration with your own account instead.
|
||||||
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
|
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
|
||||||
|
|
||||||
1. For **Scopes**, select `api` and clear any other checkboxes.
|
1. For **Scopes**, select `api` and clear any other checkboxes.
|
||||||
- The connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks.
|
- The DVCS connector requires a _write-enabled_ `api` scope to automatically create and manage required webhooks.
|
||||||
1. Select **Submit**.
|
1. Select **Submit**.
|
||||||
1. GitLab displays the generated **Application ID**
|
1. Copy the **Application ID** and **Secret** values.
|
||||||
and **Secret** values. Copy these values, as you need them to configure Jira.
|
You need them to configure Jira.
|
||||||
|
|
||||||
## Configure Jira for DVCS
|
## Configure Jira for DVCS
|
||||||
|
|
||||||
|
@ -97,19 +104,21 @@ Configure this connection when you want to import all GitLab commits and branche
|
||||||
for the groups you specify, into Jira. This import takes a few minutes and, after
|
for the groups you specify, into Jira. This import takes a few minutes and, after
|
||||||
it completes, refreshes every 60 minutes:
|
it completes, refreshes every 60 minutes:
|
||||||
|
|
||||||
1. Ensure you have completed the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
|
1. Complete the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
|
||||||
1. Go to your DVCS accounts:
|
1. Go to your DVCS accounts:
|
||||||
- *For Jira Server,* go to **Settings (gear) > Applications > DVCS accounts**.
|
- *For Jira Server,* select **Settings (gear) > Applications > DVCS accounts**.
|
||||||
- *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**.
|
- *For Jira Cloud,* select **Settings (gear) > Products > DVCS accounts**.
|
||||||
1. To create a new integration, select the appropriate value for **Host**:
|
1. To create a new integration, select the appropriate value for **Host**:
|
||||||
- *For Jira versions 8.14 and later:* Select **GitLab** or
|
- *For Jira versions 8.14 and later:* Select **GitLab** or
|
||||||
**GitLab Self-Managed**.
|
**GitLab Self-Managed**.
|
||||||
- *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**.
|
- *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**.
|
||||||
1. For **Team or User Account**, enter either:
|
1. For **Team or User Account**, enter either:
|
||||||
- *For Jira versions 8.14 and later:*
|
- *For Jira versions 8.14 and later:*
|
||||||
- The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
|
- The relative path of a top-level GitLab group that
|
||||||
|
[the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
|
||||||
- *For Jira versions 8.13 and earlier:*
|
- *For Jira versions 8.13 and earlier:*
|
||||||
- The relative path of a top-level GitLab group that [the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
|
- The relative path of a top-level GitLab group that
|
||||||
|
[the GitLab user](#configure-a-gitlab-application-for-dvcs) has access to.
|
||||||
- The relative path of your personal namespace.
|
- The relative path of your personal namespace.
|
||||||
|
|
||||||
1. In the **Host URL** field, enter the URI appropriate for your version of GitLab,
|
1. In the **Host URL** field, enter the URI appropriate for your version of GitLab,
|
||||||
|
@ -120,13 +129,13 @@ it completes, refreshes every 60 minutes:
|
||||||
|
|
||||||
1. For **Client ID**, use the **Application ID** value from the previous section.
|
1. For **Client ID**, use the **Application ID** value from the previous section.
|
||||||
1. For **Client Secret**, use the **Secret** value from the previous section.
|
1. For **Client Secret**, use the **Secret** value from the previous section.
|
||||||
1. Ensure that the rest of the checkboxes are checked.
|
1. Ensure that the rest of the checkboxes are selected.
|
||||||
1. Select **Add** and then **Continue** to create the DVCS account.
|
1. To create the DVCS account, select **Add** and then **Continue**.
|
||||||
1. Jira redirects to GitLab where you have to confirm the authorization,
|
1. Jira redirects to GitLab where you have to confirm the authorization.
|
||||||
and then GitLab redirects back to Jira where you should see the synced
|
GitLab then redirects back to Jira where the synced
|
||||||
projects show up inside the new account.
|
projects should display in the new account.
|
||||||
|
|
||||||
To connect additional GitLab projects from other GitLab top-level groups, or
|
To connect additional GitLab projects from other GitLab top-level groups or
|
||||||
personal namespaces, repeat the previous steps with additional Jira DVCS accounts.
|
personal namespaces, repeat the previous steps with additional Jira DVCS accounts.
|
||||||
|
|
||||||
After you configure the integration, read more about [how to test and use it](development_panel.md).
|
After you configure the integration, read more about [how to test and use it](development_panel.md).
|
||||||
|
@ -172,9 +181,8 @@ Error obtaining access token. Cannot access https://gitlab.example.com from Jira
|
||||||
as GitLab is the TLS client.
|
as GitLab is the TLS client.
|
||||||
- The Jira Development panel integration requires Jira to connect to GitLab, which
|
- The Jira Development panel integration requires Jira to connect to GitLab, which
|
||||||
causes Jira to be the TLS client. If your GitLab server's certificate is not
|
causes Jira to be the TLS client. If your GitLab server's certificate is not
|
||||||
issued by a public certificate authority, the Java Truststore on Jira's server
|
issued by a public certificate authority, add the appropriate certificate
|
||||||
must have the appropriate certificate (such as your organization's
|
(such as your organization's root certificate) to the Java Truststore on Jira's server.
|
||||||
root certificate) added to it .
|
|
||||||
|
|
||||||
Refer to Atlassian's documentation and Atlassian Support for assistance setting
|
Refer to Atlassian's documentation and Atlassian Support for assistance setting
|
||||||
up Jira correctly:
|
up Jira correctly:
|
||||||
|
@ -187,8 +195,8 @@ up Jira correctly:
|
||||||
- If the integration stops working after upgrading Jira's Java runtime, the
|
- If the integration stops working after upgrading Jira's Java runtime, the
|
||||||
`cacerts` Truststore may have been replaced during the upgrade.
|
`cacerts` Truststore may have been replaced during the upgrade.
|
||||||
|
|
||||||
- Troubleshooting connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
- Troubleshoot connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
||||||
using the a java class called `SSLPoke`.
|
using the `SSLPoke` Java class.
|
||||||
- Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`.
|
- Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`.
|
||||||
- Use the same Java runtime as Jira.
|
- Use the same Java runtime as Jira.
|
||||||
- Pass all networking-related parameters that Jira is called with, such as proxy
|
- Pass all networking-related parameters that Jira is called with, such as proxy
|
||||||
|
@ -203,7 +211,7 @@ The message `Successfully connected` indicates a successful TLS handshake.
|
||||||
If there are problems, the Java TLS library generates errors that you can
|
If there are problems, the Java TLS library generates errors that you can
|
||||||
look up for more detail.
|
look up for more detail.
|
||||||
|
|
||||||
### Scope error when connecting Jira via DVCS
|
### Scope error when connecting to Jira using DVCS
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
The requested scope is invalid, unknown, or malformed.
|
The requested scope is invalid, unknown, or malformed.
|
||||||
|
@ -224,12 +232,12 @@ After you complete the **Add New Account** form in Jira and authorize access, yo
|
||||||
encounter these issues:
|
encounter these issues:
|
||||||
|
|
||||||
- An `Error! Failed adding the account: [Error retrieving list of repositories]` error.
|
- An `Error! Failed adding the account: [Error retrieving list of repositories]` error.
|
||||||
- An `Account is already integrated with JIRA` error when you click **Try Again**.
|
- An `Account is already integrated with JIRA` error when you select **Try Again**.
|
||||||
- An account is visible in the DVCS accounts view, but no repositories are listed.
|
- An account is visible in the DVCS accounts view, but no repositories are listed.
|
||||||
|
|
||||||
To resolve this issue:
|
To resolve this issue:
|
||||||
|
|
||||||
- If you're using GitLab Free, be sure you're using GitLab 13.4 or later.
|
- If you're using GitLab Free, ensure you're using GitLab 13.4 or later.
|
||||||
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
|
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
|
||||||
to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
||||||
|
|
||||||
|
@ -243,17 +251,17 @@ This issue occurs when you use the Jira DVCS connector and your integration is c
|
||||||
|
|
||||||
For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
|
For more information and possible fixes, see [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/340160).
|
||||||
|
|
||||||
### Fix synchronization issues
|
### Synchronization issues
|
||||||
|
|
||||||
If Jira displays incorrect information, such as deleted branches, you may have to
|
If Jira displays incorrect information, such as deleted branches, you may have to
|
||||||
resynchronize the information. To do so:
|
resynchronize the information:
|
||||||
|
|
||||||
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.
|
1. In Jira, select **Jira Administration > Applications > DVCS accounts**.
|
||||||
1. At the account (group or subgroup) level, Jira displays an option to
|
1. For the account (group or subgroup), select
|
||||||
**Refresh repositories** in the **{ellipsis_h}** (ellipsis) menu.
|
**Refresh repositories** from the **{ellipsis_h}** (ellipsis) menu.
|
||||||
1. For each project, there's a sync button displayed next to the **last activity** date.
|
1. For each project, next to the **Last activity** date:
|
||||||
- To perform a *soft resync*, click the button.
|
- To perform a *soft resync*, select the sync icon.
|
||||||
- To complete a *full sync*, shift-click the button.
|
- To complete a *full sync*, press `Shift` and select the sync icon.
|
||||||
|
|
||||||
For more information, read
|
For more information, read
|
||||||
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
||||||
|
|
|
@ -90,7 +90,7 @@ module ContainerRegistry
|
||||||
|
|
||||||
def repository_tag_digest(name, reference)
|
def repository_tag_digest(name, reference)
|
||||||
response = faraday.head("/v2/#{name}/manifests/#{reference}")
|
response = faraday.head("/v2/#{name}/manifests/#{reference}")
|
||||||
response.headers['docker-content-digest'] if response.success?
|
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_repository_tag_by_digest(name, reference)
|
def delete_repository_tag_by_digest(name, reference)
|
||||||
|
@ -171,7 +171,7 @@ module ContainerRegistry
|
||||||
req.body = Gitlab::Json.pretty_generate(manifest)
|
req.body = Gitlab::Json.pretty_generate(manifest)
|
||||||
end
|
end
|
||||||
|
|
||||||
response.headers['docker-content-digest'] if response.success?
|
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] if response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -49,6 +49,11 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def offline!
|
def offline!
|
||||||
|
::Gitlab::Database::LoadBalancing::Logger.warn(
|
||||||
|
event: :host_offline,
|
||||||
|
message: 'Marking primary host as offline'
|
||||||
|
)
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ module Gitlab
|
||||||
def query(query)
|
def query(query)
|
||||||
client_query(query)
|
client_query(query)
|
||||||
{ valid: true }
|
{ valid: true }
|
||||||
rescue Gitlab::PrometheusClient::QueryError, Gitlab::HTTP::BlockedUrlError => ex
|
rescue Gitlab::PrometheusClient::QueryError, Gitlab::PrometheusClient::ConnectionError => ex
|
||||||
{ valid: false, error: ex.message }
|
{ valid: false, error: ex.message }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -151,12 +151,8 @@ module Gitlab
|
||||||
|
|
||||||
def get(path, args)
|
def get(path, args)
|
||||||
Gitlab::HTTP.get(path, { query: args }.merge(http_options) )
|
Gitlab::HTTP.get(path, { query: args }.merge(http_options) )
|
||||||
rescue SocketError
|
rescue *Gitlab::HTTP::HTTP_ERRORS => e
|
||||||
raise PrometheusClient::ConnectionError, "Can't connect to #{api_url}"
|
raise PrometheusClient::ConnectionError, e.message
|
||||||
rescue OpenSSL::SSL::SSLError
|
|
||||||
raise PrometheusClient::ConnectionError, "#{api_url} contains invalid SSL data"
|
|
||||||
rescue Errno::ECONNREFUSED
|
|
||||||
raise PrometheusClient::ConnectionError, 'Connection refused'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_management_api_response(response)
|
def handle_management_api_response(response)
|
||||||
|
|
|
@ -37,6 +37,11 @@ module Sidebars
|
||||||
def render?
|
def render?
|
||||||
context.project.has_confluence?
|
context.project.has_confluence?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
override :active_routes
|
||||||
|
def active_routes
|
||||||
|
{ controller: :confluences }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28636,6 +28636,9 @@ msgstr ""
|
||||||
msgid "Remove assignee"
|
msgid "Remove assignee"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Remove attention request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove avatar"
|
msgid "Remove avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -28771,6 +28774,9 @@ msgstr ""
|
||||||
msgid "Removed an issue from an epic."
|
msgid "Removed an issue from an epic."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Removed attention request from @%{username}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Removed group can not be restored!"
|
msgid "Removed group can not be restored!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -29199,6 +29205,12 @@ msgstr ""
|
||||||
msgid "Request a new one"
|
msgid "Request a new one"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Request attention"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Request attention to review"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Request details"
|
msgid "Request details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -29220,6 +29232,9 @@ msgstr ""
|
||||||
msgid "Requested %{time_ago}"
|
msgid "Requested %{time_ago}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Requested attention from @%{username}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Requested design version does not exist."
|
msgid "Requested design version does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -30909,7 +30924,7 @@ msgstr ""
|
||||||
msgid "SecurityReports|Take survey"
|
msgid "SecurityReports|Take survey"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecurityReports|The Vulnerability Report shows the results of the lastest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
|
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
|
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
|
||||||
|
@ -36990,6 +37005,9 @@ msgstr ""
|
||||||
msgid "Updating"
|
msgid "Updating"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Updating the attention request for %{username} failed."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Updating…"
|
msgid "Updating…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -425,28 +425,28 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #authorize_upload_blob' do
|
describe 'POST #authorize_upload_blob' do
|
||||||
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
|
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
|
||||||
let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
|
let(:maximum_size) { DependencyProxy::Blob::MAX_FILE_SIZE }
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
request.headers.merge!(workhorse_internal_api_request_header)
|
request.headers.merge!(workhorse_internal_api_request_header)
|
||||||
|
|
||||||
get :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
|
post :authorize_upload_blob, params: { group_id: group.to_param, image: 'alpine', sha: blob_sha }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'without permission'
|
it_behaves_like 'without permission'
|
||||||
it_behaves_like 'authorize action with permission'
|
it_behaves_like 'authorize action with permission'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #upload_blob' do
|
describe 'POST #upload_blob' do
|
||||||
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
|
let(:blob_sha) { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4' }
|
||||||
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
|
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/#{blob_sha}.gz", 'application/gzip') }
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
request.headers.merge!(workhorse_internal_api_request_header)
|
request.headers.merge!(workhorse_internal_api_request_header)
|
||||||
|
|
||||||
get :upload_blob, params: {
|
post :upload_blob, params: {
|
||||||
group_id: group.to_param,
|
group_id: group.to_param,
|
||||||
image: 'alpine',
|
image: 'alpine',
|
||||||
sha: blob_sha,
|
sha: blob_sha,
|
||||||
|
@ -469,31 +469,45 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #authorize_upload_manifest' do
|
describe 'POST #authorize_upload_manifest' do
|
||||||
let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
|
let(:maximum_size) { DependencyProxy::Manifest::MAX_FILE_SIZE }
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
request.headers.merge!(workhorse_internal_api_request_header)
|
request.headers.merge!(workhorse_internal_api_request_header)
|
||||||
|
|
||||||
get :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
|
post :authorize_upload_manifest, params: { group_id: group.to_param, image: 'alpine', tag: 'latest' }
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'without permission'
|
it_behaves_like 'without permission'
|
||||||
it_behaves_like 'authorize action with permission'
|
it_behaves_like 'authorize action with permission'
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET #upload_manifest' do
|
describe 'POST #upload_manifest' do
|
||||||
let(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
|
let_it_be(:file) { fixture_file_upload("spec/fixtures/dependency_proxy/manifest", 'application/json') }
|
||||||
|
let_it_be(:image) { 'alpine' }
|
||||||
|
let_it_be(:tag) { 'latest' }
|
||||||
|
let_it_be(:content_type) { 'v2/manifest' }
|
||||||
|
let_it_be(:digest) { 'foo' }
|
||||||
|
let_it_be(:file_name) { "#{image}:#{tag}.json" }
|
||||||
|
|
||||||
subject do
|
subject do
|
||||||
request.headers.merge!(workhorse_internal_api_request_header)
|
request.headers.merge!(
|
||||||
|
workhorse_internal_api_request_header.merge!(
|
||||||
get :upload_manifest, params: {
|
{
|
||||||
|
Gitlab::Workhorse::SEND_DEPENDENCY_CONTENT_TYPE_HEADER => content_type,
|
||||||
|
DependencyProxy::Manifest::DIGEST_HEADER => digest
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
params = {
|
||||||
group_id: group.to_param,
|
group_id: group.to_param,
|
||||||
image: 'alpine',
|
image: image,
|
||||||
tag: 'latest',
|
tag: tag,
|
||||||
file: file
|
file: file,
|
||||||
|
file_name: file_name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post :upload_manifest, params: params
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'without permission'
|
it_behaves_like 'without permission'
|
||||||
|
@ -501,13 +515,30 @@ RSpec.describe Groups::DependencyProxyForContainersController do
|
||||||
context 'with a valid user' do
|
context 'with a valid user' do
|
||||||
before do
|
before do
|
||||||
group.add_guest(user)
|
group.add_guest(user)
|
||||||
|
|
||||||
expect_next_found_instance_of(Group) do |instance|
|
|
||||||
expect(instance).to receive_message_chain(:dependency_proxy_manifests, :create!)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
|
it_behaves_like 'a package tracking event', described_class.name, 'pull_manifest'
|
||||||
|
|
||||||
|
context 'with no existing manifest' do
|
||||||
|
it 'creates a manifest' do
|
||||||
|
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
|
||||||
|
|
||||||
|
manifest = group.dependency_proxy_manifests.first.reload
|
||||||
|
expect(manifest.content_type).to eq(content_type)
|
||||||
|
expect(manifest.digest).to eq(digest)
|
||||||
|
expect(manifest.file_name).to eq(file_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with existing stale manifest' do
|
||||||
|
let_it_be(:old_digest) { 'asdf' }
|
||||||
|
let_it_be_with_reload(:manifest) { create(:dependency_proxy_manifest, file_name: file_name, digest: old_digest, group: group) }
|
||||||
|
|
||||||
|
it 'updates the existing manifest' do
|
||||||
|
expect { subject }.to change { group.dependency_proxy_manifests.count }.by(0)
|
||||||
|
.and change { manifest.reload.digest }.from(old_digest).to(digest)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,12 @@ RSpec.describe 'User views the Confluence page' do
|
||||||
|
|
||||||
visit project_wikis_confluence_path(project)
|
visit project_wikis_confluence_path(project)
|
||||||
|
|
||||||
|
expect(page).to have_css('.nav-sidebar li.active', text: 'Confluence', match: :first)
|
||||||
|
|
||||||
element = page.find('.row.empty-state')
|
element = page.find('.row.empty-state')
|
||||||
|
|
||||||
expect(element).to have_link('Go to Confluence', href: service.confluence_url)
|
expect(element).to have_link('Go to Confluence', href: service.confluence_url)
|
||||||
|
expect(element).to have_link('Confluence epic', href: 'https://gitlab.com/groups/gitlab-org/-/epics/3629')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not show the page when the Confluence integration disabled' do
|
it 'does not show the page when the Confluence integration disabled' do
|
||||||
|
|
|
@ -16,7 +16,7 @@ describe('Pipeline Status', () => {
|
||||||
let mockApollo;
|
let mockApollo;
|
||||||
let mockPipelineQuery;
|
let mockPipelineQuery;
|
||||||
|
|
||||||
const createComponentWithApollo = (glFeatures = {}) => {
|
const createComponentWithApollo = () => {
|
||||||
const handlers = [[getPipelineQuery, mockPipelineQuery]];
|
const handlers = [[getPipelineQuery, mockPipelineQuery]];
|
||||||
mockApollo = createMockApollo(handlers);
|
mockApollo = createMockApollo(handlers);
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@ describe('Pipeline Status', () => {
|
||||||
commitSha: mockCommitSha,
|
commitSha: mockCommitSha,
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
glFeatures,
|
|
||||||
projectFullPath: mockProjectFullPath,
|
projectFullPath: mockProjectFullPath,
|
||||||
},
|
},
|
||||||
stubs: { GlLink, GlSprintf },
|
stubs: { GlLink, GlSprintf },
|
||||||
|
@ -106,8 +105,8 @@ describe('Pipeline Status', () => {
|
||||||
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
|
expect(findPipelineViewBtn().attributes('href')).toBe(detailsPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render the pipeline mini graph', () => {
|
it('renders the pipeline mini graph', () => {
|
||||||
expect(findPipelineEditorMiniGraph().exists()).toBe(false);
|
expect(findPipelineEditorMiniGraph().exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,19 +149,4 @@ describe('Pipeline Status', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when feature flag for pipeline mini graph is enabled', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockPipelineQuery.mockResolvedValue({
|
|
||||||
data: { project: mockProjectPipeline() },
|
|
||||||
});
|
|
||||||
|
|
||||||
createComponentWithApollo({ pipelineEditorMiniGraph: true });
|
|
||||||
waitForPromises();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders the pipeline mini graph', () => {
|
|
||||||
expect(findPipelineEditorMiniGraph().exists()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { GlButton } from '@gitlab/ui';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue';
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
function factory(propsData = {}) {
|
||||||
|
wrapper = mount(AttentionRequiredToggle, { propsData });
|
||||||
|
}
|
||||||
|
|
||||||
|
const findToggle = () => wrapper.findComponent(GlButton);
|
||||||
|
|
||||||
|
describe('Attention require toggle', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders button', () => {
|
||||||
|
factory({ type: 'reviewer', user: { attention_required: false } });
|
||||||
|
|
||||||
|
expect(findToggle().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
attentionRequired | icon
|
||||||
|
${true} | ${'star'}
|
||||||
|
${false} | ${'star-o'}
|
||||||
|
`(
|
||||||
|
'renders $icon icon when attention_required is $attentionRequired',
|
||||||
|
({ attentionRequired, icon }) => {
|
||||||
|
factory({ type: 'reviewer', user: { attention_required: attentionRequired } });
|
||||||
|
|
||||||
|
expect(findToggle().props('icon')).toBe(icon);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
attentionRequired | variant
|
||||||
|
${true} | ${'warning'}
|
||||||
|
${false} | ${'default'}
|
||||||
|
`(
|
||||||
|
'renders button with variant $variant when attention_required is $attentionRequired',
|
||||||
|
({ attentionRequired, variant }) => {
|
||||||
|
factory({ type: 'reviewer', user: { attention_required: attentionRequired } });
|
||||||
|
|
||||||
|
expect(findToggle().props('variant')).toBe(variant);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('emits toggle-attention-required on click', async () => {
|
||||||
|
factory({ type: 'reviewer', user: { attention_required: true } });
|
||||||
|
|
||||||
|
await findToggle().trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('toggle-attention-required')[0]).toEqual([
|
||||||
|
{
|
||||||
|
user: { attention_required: true },
|
||||||
|
callback: expect.anything(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets loading on click', async () => {
|
||||||
|
factory({ type: 'reviewer', user: { attention_required: true } });
|
||||||
|
|
||||||
|
await findToggle().trigger('click');
|
||||||
|
|
||||||
|
expect(findToggle().props('loading')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
type | attentionRequired | tooltip
|
||||||
|
${'reviewer'} | ${true} | ${AttentionRequiredToggle.i18n.removeAttentionRequired}
|
||||||
|
${'reviewer'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredReviewer}
|
||||||
|
${'assignee'} | ${false} | ${AttentionRequiredToggle.i18n.attentionRequiredAssignee}
|
||||||
|
`(
|
||||||
|
'sets tooltip as $tooltip when attention_required is $attentionRequired and type is $type',
|
||||||
|
({ type, attentionRequired, tooltip }) => {
|
||||||
|
factory({ type, user: { attention_required: attentionRequired } });
|
||||||
|
|
||||||
|
expect(findToggle().attributes('aria-label')).toBe(tooltip);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
|
@ -1,5 +1,6 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
import AttentionRequiredToggle from '~/sidebar/components/attention_required_toggle.vue';
|
||||||
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
|
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
|
||||||
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
|
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
|
||||||
import userDataMock from '../../user_data_mock';
|
import userDataMock from '../../user_data_mock';
|
||||||
|
@ -9,7 +10,7 @@ describe('UncollapsedReviewerList component', () => {
|
||||||
|
|
||||||
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
|
const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
|
||||||
|
|
||||||
function createComponent(props = {}) {
|
function createComponent(props = {}, glFeatures = {}) {
|
||||||
const propsData = {
|
const propsData = {
|
||||||
users: [],
|
users: [],
|
||||||
rootPath: TEST_HOST,
|
rootPath: TEST_HOST,
|
||||||
|
@ -18,6 +19,9 @@ describe('UncollapsedReviewerList component', () => {
|
||||||
|
|
||||||
wrapper = shallowMount(UncollapsedReviewerList, {
|
wrapper = shallowMount(UncollapsedReviewerList, {
|
||||||
propsData,
|
propsData,
|
||||||
|
provide: {
|
||||||
|
glFeatures,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,4 +114,18 @@ describe('UncollapsedReviewerList component', () => {
|
||||||
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
|
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides re-request review button when attentionRequired feature flag is enabled', () => {
|
||||||
|
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
|
||||||
|
|
||||||
|
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits toggle-attention-required', () => {
|
||||||
|
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
|
||||||
|
|
||||||
|
wrapper.find(AttentionRequiredToggle).vm.$emit('toggle-attention-required', 'data');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('toggle-attention-required')[0]).toEqual(['data']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,11 @@ import * as urlUtility from '~/lib/utils/url_utility';
|
||||||
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
|
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
|
||||||
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
||||||
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
import SidebarStore from '~/sidebar/stores/sidebar_store';
|
||||||
|
import toast from '~/vue_shared/plugins/global_toast';
|
||||||
import Mock from './mock_data';
|
import Mock from './mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/vue_shared/plugins/global_toast');
|
||||||
|
|
||||||
describe('Sidebar mediator', () => {
|
describe('Sidebar mediator', () => {
|
||||||
const { mediator: mediatorMockData } = Mock;
|
const { mediator: mediatorMockData } = Mock;
|
||||||
let mock;
|
let mock;
|
||||||
|
@ -115,4 +118,56 @@ describe('Sidebar mediator', () => {
|
||||||
urlSpy.mockRestore();
|
urlSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('toggleAttentionRequired', () => {
|
||||||
|
let attentionRequiredService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
attentionRequiredService = jest
|
||||||
|
.spyOn(mediator.service, 'attentionRequired')
|
||||||
|
.mockResolvedValue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls attentionRequired service method', async () => {
|
||||||
|
mediator.store.reviewers = [{ id: 1, attention_required: false, username: 'root' }];
|
||||||
|
|
||||||
|
await mediator.toggleAttentionRequired('reviewer', {
|
||||||
|
user: { id: 1, username: 'root' },
|
||||||
|
callback: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(attentionRequiredService).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
type | method
|
||||||
|
${'reviewer'} | ${'findReviewer'}
|
||||||
|
`('finds $type', ({ type, method }) => {
|
||||||
|
const methodSpy = jest.spyOn(mediator.store, method);
|
||||||
|
|
||||||
|
mediator.toggleAttentionRequired(type, { user: { id: 1 }, callback: jest.fn() });
|
||||||
|
|
||||||
|
expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
attentionRequired | toastMessage
|
||||||
|
${true} | ${'Removed attention request from @root'}
|
||||||
|
${false} | ${'Requested attention from @root'}
|
||||||
|
`(
|
||||||
|
'it creates toast $toastMessage when attention_required is $attentionRequired',
|
||||||
|
async ({ attentionRequired, toastMessage }) => {
|
||||||
|
mediator.store.reviewers = [
|
||||||
|
{ id: 1, attention_required: attentionRequired, username: 'root' },
|
||||||
|
];
|
||||||
|
|
||||||
|
await mediator.toggleAttentionRequired('reviewer', {
|
||||||
|
user: { id: 1, username: 'root' },
|
||||||
|
callback: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast).toHaveBeenCalledWith(toastMessage);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
||||||
RSpec.describe GitlabSchema.types['DependencyProxyManifest'] do
|
RSpec.describe GitlabSchema.types['DependencyProxyManifest'] do
|
||||||
it 'includes dependency proxy manifest fields' do
|
it 'includes dependency proxy manifest fields' do
|
||||||
expected_fields = %w[
|
expected_fields = %w[
|
||||||
file_name image_name size created_at updated_at digest
|
id file_name image_name size created_at updated_at digest
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Groups::SettingsHelper do
|
||||||
|
include GroupsHelper
|
||||||
|
|
||||||
|
let_it_be(:group) { create(:group, path: "foo") }
|
||||||
|
|
||||||
|
describe('#group_settings_confirm_modal_data') do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
fake_form_id = "fake_form_id"
|
||||||
|
|
||||||
|
where(:is_paid, :is_button_disabled, :form_value_id) do
|
||||||
|
true | "true" | nil
|
||||||
|
true | "true" | fake_form_id
|
||||||
|
false | "false" | nil
|
||||||
|
false | "false" | fake_form_id
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it "returns expected parameters" do
|
||||||
|
allow(group).to receive(:paid?).and_return(is_paid)
|
||||||
|
|
||||||
|
expected = helper.group_settings_confirm_modal_data(group, form_value_id)
|
||||||
|
expect(expected).to eq({
|
||||||
|
button_text: "Remove group",
|
||||||
|
confirm_danger_message: remove_group_message(group),
|
||||||
|
remove_form_id: form_value_id,
|
||||||
|
phrase: group.full_path,
|
||||||
|
button_testid: "remove-group-button",
|
||||||
|
disabled: is_button_disabled
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -279,7 +279,7 @@ RSpec.describe ContainerRegistry::Client do
|
||||||
it 'uploads the manifest and returns the digest' do
|
it 'uploads the manifest and returns the digest' do
|
||||||
stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
|
stub_request(:put, "http://container-registry/v2/path/manifests/tagA")
|
||||||
.with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers)
|
.with(body: "{\n \"foo\": \"bar\"\n}", headers: manifest_headers)
|
||||||
.to_return(status: 200, body: "", headers: { 'docker-content-digest' => 'sha256:123' })
|
.to_return(status: 200, body: "", headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:123' })
|
||||||
|
|
||||||
expect_new_faraday(timeout: false)
|
expect_new_faraday(timeout: false)
|
||||||
|
|
||||||
|
|
|
@ -213,7 +213,7 @@ RSpec.describe ContainerRegistry::Tag do
|
||||||
before do
|
before do
|
||||||
stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
|
stub_request(:head, 'http://registry.gitlab/v2/group/test/manifests/tag')
|
||||||
.with(headers: headers)
|
.with(headers: headers)
|
||||||
.to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
|
.to_return(status: 200, headers: { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:digest' })
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#digest' do
|
describe '#digest' do
|
||||||
|
|
|
@ -51,7 +51,11 @@ RSpec.describe Gitlab::Database::LoadBalancing::PrimaryHost do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#offline!' do
|
describe '#offline!' do
|
||||||
it 'does nothing' do
|
it 'logs the event but does nothing else' do
|
||||||
|
expect(Gitlab::Database::LoadBalancing::Logger).to receive(:warn)
|
||||||
|
.with(hash_including(event: :host_offline))
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
expect(host.offline!).to be_nil
|
expect(host.offline!).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,36 +107,14 @@ RSpec.describe Gitlab::PrometheusClient do
|
||||||
let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
|
let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
|
||||||
|
|
||||||
shared_examples 'exceptions are raised' do
|
shared_examples 'exceptions are raised' do
|
||||||
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SocketError is rescued' do
|
Gitlab::HTTP::HTTP_ERRORS.each do |error|
|
||||||
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
|
it "raises a Gitlab::PrometheusClient::ConnectionError when a #{error} is rescued" do
|
||||||
|
req_stub = stub_prometheus_request_with_exception(prometheus_url, error.new)
|
||||||
|
|
||||||
expect { subject }
|
expect { subject }
|
||||||
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Can't connect to #{prometheus_url}")
|
.to raise_error(Gitlab::PrometheusClient::ConnectionError, kind_of(String))
|
||||||
expect(req_stub).to have_been_requested
|
expect(req_stub).to have_been_requested
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a SSLError is rescued' do
|
|
||||||
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
|
|
||||||
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "#{prometheus_url} contains invalid SSL data")
|
|
||||||
expect(req_stub).to have_been_requested
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError is rescued' do
|
|
||||||
req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError)
|
|
||||||
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
|
|
||||||
expect(req_stub).to have_been_requested
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'raises a Gitlab::PrometheusClient::ConnectionError error when a Gitlab::HTTP::ResponseError with a code is rescued' do
|
|
||||||
req_stub = stub_prometheus_request_with_exception(prometheus_url, Gitlab::HTTP::ResponseError.new(code: 400))
|
|
||||||
|
|
||||||
expect { subject }
|
|
||||||
.to raise_error(Gitlab::PrometheusClient::ConnectionError, "Network connection error")
|
|
||||||
expect(req_stub).to have_been_requested
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -165,6 +165,14 @@ RSpec.describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
|
||||||
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
|
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when client raises Gitlab::PrometheusClient::ConnectionError" do
|
||||||
|
before do
|
||||||
|
stub_any_prometheus_request.to_raise(Gitlab::PrometheusClient::ConnectionError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to include(success: false, result: kind_of(String)) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#build_query_args' do
|
describe '#build_query_args' do
|
||||||
|
|
|
@ -15,6 +15,17 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
|
||||||
it { is_expected.to validate_presence_of(:digest) }
|
it { is_expected.to validate_presence_of(:digest) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'scopes' do
|
||||||
|
let_it_be(:manifest_one) { create(:dependency_proxy_manifest) }
|
||||||
|
let_it_be(:manifest_two) { create(:dependency_proxy_manifest) }
|
||||||
|
let_it_be(:manifests) { [manifest_one, manifest_two] }
|
||||||
|
let_it_be(:ids) { manifests.map(&:id) }
|
||||||
|
|
||||||
|
it 'order_id_desc' do
|
||||||
|
expect(described_class.where(id: ids).order_id_desc.to_a).to eq [manifest_two, manifest_one]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'file is being stored' do
|
describe 'file is being stored' do
|
||||||
subject { create(:dependency_proxy_manifest) }
|
subject { create(:dependency_proxy_manifest) }
|
||||||
|
|
||||||
|
|
|
@ -306,7 +306,7 @@ RSpec.describe Issue do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#reopen' do
|
describe '#reopen' do
|
||||||
let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
|
let_it_be_with_reload(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) }
|
||||||
|
|
||||||
it 'sets closed_at to nil when an issue is reopened' do
|
it 'sets closed_at to nil when an issue is reopened' do
|
||||||
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
|
expect { issue.reopen }.to change { issue.closed_at }.to(nil)
|
||||||
|
@ -316,6 +316,22 @@ RSpec.describe Issue do
|
||||||
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
|
expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'clears moved_to_id for moved issues' do
|
||||||
|
moved_issue = create(:issue)
|
||||||
|
|
||||||
|
issue.update!(moved_to_id: moved_issue.id)
|
||||||
|
|
||||||
|
expect { issue.reopen }.to change { issue.moved_to_id }.from(moved_issue.id).to(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'clears duplicated_to_id for duplicated issues' do
|
||||||
|
duplicate_issue = create(:issue)
|
||||||
|
|
||||||
|
issue.update!(duplicated_to_id: duplicate_issue.id)
|
||||||
|
|
||||||
|
expect { issue.reopen }.to change { issue.duplicated_to_id }.from(duplicate_issue.id).to(nil)
|
||||||
|
end
|
||||||
|
|
||||||
it 'changes the state to opened' do
|
it 'changes the state to opened' do
|
||||||
expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
|
expect { issue.reopen }.to change { issue.state_id }.from(described_class.available_states[:closed]).to(described_class.available_states[:opened])
|
||||||
end
|
end
|
||||||
|
|
|
@ -116,4 +116,26 @@ RSpec.describe 'getting dependency proxy manifests in a group' do
|
||||||
|
|
||||||
expect(dependency_proxy_image_count_response).to eq(manifests.size)
|
expect(dependency_proxy_image_count_response).to eq(manifests.size)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'sorting and pagination' do
|
||||||
|
let(:data_path) { ['group', :dependencyProxyManifests] }
|
||||||
|
let(:current_user) { owner }
|
||||||
|
|
||||||
|
context 'with default sorting' do
|
||||||
|
let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} }
|
||||||
|
|
||||||
|
it_behaves_like 'sorted paginated query' do
|
||||||
|
let(:sort_param) { '' }
|
||||||
|
let(:first_param) { 2 }
|
||||||
|
let(:all_records) { descending_manifests }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_query(params)
|
||||||
|
# remove sort since the type does not accept sorting, but be future proof
|
||||||
|
graphql_query_for('group', { 'fullPath' => group.full_path },
|
||||||
|
query_nodes(:dependencyProxyManifests, :id, include_pagination_info: true, args: params.merge(sort: nil))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
|
||||||
let(:token) { Digest::SHA256.hexdigest('123') }
|
let(:token) { Digest::SHA256.hexdigest('123') }
|
||||||
let(:headers) do
|
let(:headers) do
|
||||||
{
|
{
|
||||||
'docker-content-digest' => dependency_proxy_manifest.digest,
|
DependencyProxy::Manifest::DIGEST_HEADER => dependency_proxy_manifest.digest,
|
||||||
'content-type' => dependency_proxy_manifest.content_type
|
'content-type' => dependency_proxy_manifest.content_type
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -100,8 +100,8 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
|
||||||
let(:content_type) { 'new-content-type' }
|
let(:content_type) { 'new-content-type' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
|
stub_manifest_head(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
|
||||||
stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
|
stub_manifest_download(image, tag, headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type })
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'returning no manifest'
|
it_behaves_like 'returning no manifest'
|
||||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::HeadManifestService do
|
||||||
let(:content_type) { 'foo' }
|
let(:content_type) { 'foo' }
|
||||||
let(:headers) do
|
let(:headers) do
|
||||||
{
|
{
|
||||||
'docker-content-digest' => digest,
|
DependencyProxy::Manifest::DIGEST_HEADER => digest,
|
||||||
'content-type' => content_type
|
'content-type' => content_type
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe DependencyProxy::PullManifestService do
|
||||||
let(:digest) { '12345' }
|
let(:digest) { '12345' }
|
||||||
let(:content_type) { 'foo' }
|
let(:content_type) { 'foo' }
|
||||||
let(:headers) do
|
let(:headers) do
|
||||||
{ 'docker-content-digest' => digest, 'content-type' => content_type }
|
{ DependencyProxy::Manifest::DIGEST_HEADER => digest, 'content-type' => content_type }
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
|
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
|
||||||
|
|
|
@ -31,14 +31,14 @@ RSpec.shared_context 'container repository delete tags service shared context' d
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' })
|
def stub_put_manifest_request(tag, status = 200, headers = { DependencyProxy::Manifest::DIGEST_HEADER => 'sha256:dummy' })
|
||||||
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
|
stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
|
||||||
.to_return(status: status, body: '', headers: headers)
|
.to_return(status: status, body: '', headers: headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_tag_digest(tag, digest)
|
def stub_tag_digest(tag, digest)
|
||||||
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
|
stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}")
|
||||||
.to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest })
|
.to_return(status: 200, body: '', headers: { DependencyProxy::Manifest::DIGEST_HEADER => digest })
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_digest_config(digest, created_at)
|
def stub_digest_config(digest, created_at)
|
||||||
|
|
|
@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do
|
||||||
subject
|
subject
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
|
expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).to eq(manifest.digest)
|
||||||
expect(response.headers['Content-Length']).to eq(manifest.size)
|
expect(response.headers['Content-Length']).to eq(manifest.size)
|
||||||
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
|
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
|
||||||
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
|
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
|
||||||
|
|
|
@ -9,8 +9,8 @@ RSpec.describe 'groups/settings/_remove.html.haml' do
|
||||||
|
|
||||||
render 'groups/settings/remove', group: group
|
render 'groups/settings/remove', group: group
|
||||||
|
|
||||||
expect(rendered).to have_selector '[data-testid="remove-group-button"]'
|
expect(rendered).to have_selector '[data-button-testid="remove-group-button"]'
|
||||||
expect(rendered).not_to have_selector '[data-testid="remove-group-button"].disabled'
|
expect(rendered).not_to have_selector '[data-button-testid="remove-group-button"].disabled'
|
||||||
expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]'
|
expect(rendered).not_to have_selector '[data-testid="group-has-linked-subscription-alert"]'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
root="$(cd "$(dirname "$0")/../.." || exit ; pwd -P)"
|
|
||||||
|
|
||||||
if [ $# -ne 0 ]; then
|
|
||||||
shellcheck --exclude=SC1071 --external-sources "$@"
|
|
||||||
else
|
|
||||||
find \
|
|
||||||
"${root}/bin" \
|
|
||||||
"${root}/tooling" \
|
|
||||||
-type f \
|
|
||||||
-not -path "*.swp" \
|
|
||||||
-not -path "*.rb" \
|
|
||||||
-not -path "*.js" \
|
|
||||||
-not -path "*.md" \
|
|
||||||
-not -path "*.haml" \
|
|
||||||
-not -path "*/Gemfile*" \
|
|
||||||
-not -path '*/.bundle*' \
|
|
||||||
-not -path '*/Makefile*' \
|
|
||||||
-print0 \
|
|
||||||
| xargs -0 shellcheck --exclude=SC1071 --external-sources --
|
|
||||||
fi
|
|
Loading…
Reference in New Issue